From db0521f9503ad726ddc446346417ed2d2eb1500a Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 12 Sep 2026 06:51:10 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Temporal.PlainYearMonth=20add/subtract?= =?UTF-8?q?=E2=80=94=E2=80=94=E6=97=B6=E9=95=BF=E8=BD=AC=E6=8D=A2=E5=A4=8D?= =?UTF-8?q?=E7=94=A8=E3=80=81=E4=BD=8E=E9=98=B6=E5=8D=95=E4=BD=8D=E6=8B=92?= =?UTF-8?q?=E7=BB=9D=E4=B8=8E=E6=8C=89=E6=97=A5=201=20=E7=9A=84=20(?= =?UTF-8?q?=E5=B9=B4,=E6=9C=88)=20=E8=8C=83=E5=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/temporal.rs | 65 +++++++++++++++ crates/oxide_vm/src/bindings/bind_temporal.rs | 6 ++ .../oxide_vm/tests/builtins_temporal_tests.rs | 82 +++++++++++++++++++ 3 files changed, 153 insertions(+) diff --git a/crates/oxide_builtins/src/temporal.rs b/crates/oxide_builtins/src/temporal.rs index 30d46b3d..bccb9f02 100644 --- a/crates/oxide_builtins/src/temporal.rs +++ b/crates/oxide_builtins/src/temporal.rs @@ -8772,6 +8772,71 @@ pub fn plain_year_month_equals(vm: &mut H, args: &[u8]) -> NativeResu NativeResult::Ok(JsValue::bool(equal)) } +/// add/subtract 的 (年, 月) 可表示性检查,按日 = 1 计算(ISODateWithinLimits)。 +/// 与 ISOYearMonthWithinLimits 的差异仅在 -271821 年:该年 4 月 1 日早于最早 +/// 可表示的日期时间,故 4 月及以前越界(5 月起在界内);+275760 年须 9 月及以前。 +fn iso_year_month_day1_within_limits(year: i64, month: i64) -> bool { + if !(-271_821_i64..=275_760_i64).contains(&year) { + return false; + } + !((year == -271_821 && month < 5) || (year == 275_760 && month > 9)) +} + +/// `Temporal.PlainYearMonth.prototype.add/subtract(durationLike [, options])` 核心。 +/// +/// # 步骤 +/// 1. receiver branding(TypeError)。 +/// 2. ToTemporalDuration(串 / bag / 实例;TypeError、RangeError 各自语义)。 +/// 3. 读 overflow(规范顺序先于后续算法校验;ISO 日历下取值不可观测,仅校验)。 +/// 4. weeks / days / 时间分量任一非零 → RangeError。 +/// 5. receiver (年, 月) 按日 1 检查可表示性 → RangeError。 +/// 6. 年、月分量折算绝对月数后相加再平衡(div/rem_euclid)。 +/// 7. 结果 (年, 月) 按日 1 检查可表示性 → RangeError。 +/// +/// # 边界与副作用 +/// - 结果参考日恒 1,日历标识取自 receiver 槽 3。 +/// - subtract 与 add 共用本函数,sign 参数取反时长分量。 +fn plain_year_month_apply_duration(vm: &mut H, args: &[u8], sign: i64) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_plain_year_month(vm, obj)); + let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let values = match duration_like_values(vm, val) { + Ok(values) => values, + Err(error) => return NativeResult::Err(error), + }; + let _overflow = native_try!(temporal_overflow(vm, args)); + if values[2..].iter().any(|value| *value != 0.0) { + return NativeResult::Err(crate::error::create_range_error( + vm, + "year-month cannot add weeks, days, or time units", + )); + } + let year = get_double_prop(obj, 0) as i64; + let month = get_double_prop(obj, 1) as i64; + if !iso_year_month_day1_within_limits(year, month) { + return NativeResult::Err(crate::error::create_range_error(vm, "year-month is out of range")); + } + let months = (values[0] as i64) * 12 + values[1] as i64; + let absolute = year * 12 + month - 1 + sign * months; + let result_year = absolute.div_euclid(12); + let result_month = absolute.rem_euclid(12) + 1; + if !iso_year_month_day1_within_limits(result_year, result_month) { + return NativeResult::Err(crate::error::create_range_error(vm, "year-month is out of range")); + } + make_plain_year_month(vm, result_year as i32, result_month as u32, 1, &get_calendar_id(obj, 3)) +} + +/// `Temporal.PlainYearMonth.prototype.add(durationLike)`。 +pub fn plain_year_month_add(vm: &mut H, args: &[u8]) -> NativeResult { + plain_year_month_apply_duration(vm, args, 1) +} + +/// `Temporal.PlainYearMonth.prototype.subtract(durationLike)`。 +pub fn plain_year_month_subtract(vm: &mut H, args: &[u8]) -> NativeResult { + plain_year_month_apply_duration(vm, args, -1) +} + /// `Temporal.PlainMonthDay.prototype.equals(other)`:比较 (月, 日, 参考年) 三元与日历标识; /// other 经 ToTemporalMonthDay(串 / 字段对象 / 实例)转换。 pub fn plain_month_day_equals(vm: &mut H, args: &[u8]) -> NativeResult { diff --git a/crates/oxide_vm/src/bindings/bind_temporal.rs b/crates/oxide_vm/src/bindings/bind_temporal.rs index a3d60e25..05c41a62 100644 --- a/crates/oxide_vm/src/bindings/bind_temporal.rs +++ b/crates/oxide_vm/src/bindings/bind_temporal.rs @@ -1143,6 +1143,12 @@ pub fn bind_temporal(core: &Arc, session: &KernelSession, global: &m oxide_builtins::temporal::plain_year_month_equals:: as *const (), 1, ), + ("add", oxide_builtins::temporal::plain_year_month_add:: as *const (), 1), + ( + "subtract", + oxide_builtins::temporal::plain_year_month_subtract:: as *const (), + 1, + ), ], ); bind_well_known_data_property( diff --git a/crates/oxide_vm/tests/builtins_temporal_tests.rs b/crates/oxide_vm/tests/builtins_temporal_tests.rs index 7e04464c..9a5c43d9 100644 --- a/crates/oxide_vm/tests/builtins_temporal_tests.rs +++ b/crates/oxide_vm/tests/builtins_temporal_tests.rs @@ -3324,3 +3324,85 @@ fn plain_year_month_equals_matrix() { "true|false|false|false|true|true|true|true|true|true|TypeError|TypeError|TypeError|RangeError|TypeError|RangeError|TypeError|true" ); } + +#[test] +fn plain_year_month_add_matrix() { + let mut vm = Vm::new(); + // add:ToTemporalDuration(串/bag/实例)→ 读 overflow → weeks/days/时间分量 + // 非零 RangeError;(年,月) 折绝对月数平衡;接收方与结果均按日 1 查可表示性 + // (-271821-04 加零时长也 RangeError);结果参考日恒 1。 + let r = eval( + &mut vm, + "(() => { + const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } }; + const ym = (s) => Temporal.PlainYearMonth.from(s); + return [ + ym('2019-11').add({months: 2}).toString(), + ym('2019-11').add({years: 1}).toString(), + ym('2019-11').add({months: -2}).toString(), + ym('2000-05').add('P3M').toString(), + ym('1994-11').add(Temporal.Duration.from('P18Y7M')).toString(), + new Temporal.PlainYearMonth(2025, 8).add(new Temporal.Duration()).toString(), + ym('2019-11').add({years: 1, months: 2}).toString(), + ym('1970-01').add('-P273790Y8M').toString(), + ym('1970-01').add('P3285488M').toString(), + kind(() => ym('2019-11').add({days: 1})), + kind(() => ym('2019-11').add({hours: 1})), + kind(() => ym('2019-11').add(new Temporal.Duration(0, 0, 0, -1))), + kind(() => ym('2019-11').add({years: 1, months: -3})), + kind(() => ym('2019-11').add({year: 1})), + kind(() => ym('2019-11').add({})), + kind(() => ym('2019-11').add(7)), + kind(() => ym('2019-11').add('')), + kind(() => ym('2019-11').add({months: 1.5})), + kind(() => ym('2019-11').add({months: Infinity})), + kind(() => ym('2019-11').add('P4294967296M')), + kind(() => ym('+275760-09').add({months: 1})), + kind(() => ym('-271821-04').add(new Temporal.Duration())), + kind(() => ym('2019-11').add({months: 1}, null)), + ym('2023-05').add({months: 12}, {overflow: 'reject'}).toString(), + ym('2023-05').add({months: 12}, {overflow: 'constrain'}).toString(), + ].join('|'); + })()", + ) + .unwrap(); + assert_eq!( + str_val(&vm, r), + "2020-01|2020-11|2019-09|2000-08|2013-06|2025-08|2021-01|-271821-05|+275760-09|RangeError|RangeError|RangeError|RangeError|TypeError|TypeError|TypeError|RangeError|RangeError|RangeError|RangeError|RangeError|RangeError|TypeError|2024-05|2024-05" + ); +} + +#[test] +fn plain_year_month_subtract_matrix() { + let mut vm = Vm::new(); + // subtract 与 add 同核(sign 取反);日历保留;低阶单位与非零时间分量 + // RangeError;单数键 TypeError;接收方/结果按日 1 可表示性。 + let r = eval( + &mut vm, + "(() => { + const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } }; + const ym = (s) => Temporal.PlainYearMonth.from(s); + return [ + ym('2013-06').subtract(Temporal.Duration.from('P18Y7M')).toString(), + ym('2000-05').subtract('P3M').toString(), + ym('2019-11').subtract({years: 1, months: 2}).toString(), + ym('+275760-09').subtract({months: 1}).toString(), + ym('+275760-09').subtract({years: 1}).toString(), + ym('1970-01').subtract('P273790Y8M').toString(), + kind(() => ym('2019-11').subtract({weeks: 1})), + kind(() => ym('2019-11').subtract({seconds: 1})), + kind(() => ym('2019-11').subtract({months: -1, years: 1})), + kind(() => ym('2019-11').subtract({month: 1})), + kind(() => ym('-271821-04').subtract({months: 1})), + kind(() => ym('-271821-04').subtract(new Temporal.Duration())), + kind(() => ym('2019-11').subtract({months: 1}, 1)), + kind(() => ym('2019-11').subtract('junk')), + ].join('|'); + })()", + ) + .unwrap(); + assert_eq!( + str_val(&vm, r), + "1994-11|2000-02|2018-09|+275760-08|+275759-09|-271821-05|RangeError|RangeError|RangeError|TypeError|RangeError|RangeError|TypeError|RangeError" + ); +}