feat: ZonedDateTime.add/subtract 时长运算(复用 PDT 叠加算法 + 中间日期检查 + 时区/日历槽保真)

This commit is contained in:
2026-08-17 00:02:00 +08:00
parent 88a08cff11
commit f6f5628cba
3 changed files with 226 additions and 0 deletions
@@ -425,6 +425,12 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
oxide_builtins::temporal::zoned_date_time_with_plain_time::<crate::vm::Vm> as *const (),
1,
),
("add", oxide_builtins::temporal::zoned_date_time_add::<crate::vm::Vm> as *const (), 1),
(
"subtract",
oxide_builtins::temporal::zoned_date_time_subtract::<crate::vm::Vm> as *const (),
1,
),
],
);
@@ -1919,3 +1919,127 @@ fn zoned_date_time_with_family_branding() {
.unwrap();
assert_eq!(str_val(&vm, r), "true|true|true");
}
#[test]
fn zoned_date_time_add_duration_object() {
let mut vm = Vm::new();
// add-duration.js 对拍:240h + 800ns 叠加到负 epoch。
let r = eval(
&mut vm,
"const z = new Temporal.ZonedDateTime(-560174321098766n, 'UTC');
z.add(new Temporal.Duration(0, 0, 0, 0, 240, 0, 0, 0, 0, 800)).epochNanoseconds === 303825678902034n",
)
.unwrap();
assert!(r.as_bool());
}
#[test]
fn zoned_date_time_add_constrain_and_month_boundary() {
let mut vm = Vm::new();
// 月末 + 1 月 constrain 钳制到 2 月 28;reject 抛 RangeError;月份/日叠加顺序一致。
let r = eval(
&mut vm,
"const z = new Temporal.ZonedDateTime(0n, 'UTC');
z.with({ day: 31 }).add({ months: 1 }).toString() + '|' +
(() => { try { z.with({ day: 31 }).add({ months: 1 }, { overflow: 'reject' }); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })() + '|' +
(z.add({ months: 1 }).add({ days: 1 }).epochNanoseconds === z.add({ months: 1, days: 1 }).epochNanoseconds)",
)
.unwrap();
assert_eq!(str_val(&vm, r), "1970-02-28T00:00:00+00:00[UTC]|true|true");
}
#[test]
fn zoned_date_time_add_subtract_are_inverse() {
let mut vm = Vm::new();
// 同一 duration 符号反转互逆;blank duration 返回等值对象。
let r = eval(
&mut vm,
"const z = new Temporal.ZonedDateTime(1_700_000_000_123_456_789n, '+05:30');
(z.add({ days: 5, hours: 3 }).subtract({ days: 5, hours: 3 }).epochNanoseconds === z.epochNanoseconds) + '|' +
(z.subtract({ months: 1 }).add({ months: 1 }).epochNanoseconds === z.epochNanoseconds) + '|' +
(z.add(new Temporal.Duration()).epochNanoseconds === z.epochNanoseconds) + '|' +
(z.add('PT0S').toString() === z.toString())",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true|true|true|true");
}
#[test]
fn zoned_date_time_add_subtract_intermediate_check() {
let mut vm = Vm::new();
// ±MAX instant 的 {days:∓1}:中间日期越 PlainDateTime 范围 → RangeError。
let r = eval(
&mut vm,
"const min = new Temporal.ZonedDateTime(-8640000000000000000000n, 'UTC');
const max = new Temporal.ZonedDateTime(8640000000000000000000n, 'UTC');
(() => { try { min.add({ days: -1 }); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })() + '|' +
(() => { try { min.subtract({ days: 1 }); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })() + '|' +
(() => { try { max.add({ days: 1 }); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })() + '|' +
(() => { try { max.subtract({ days: -1 }); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })()",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true|true|true|true");
}
#[test]
fn zoned_date_time_add_slots_preserved() {
let mut vm = Vm::new();
// 运算结果保留 receiver 时区/日历槽。
let r = eval(
&mut vm,
"const z = new Temporal.ZonedDateTime(0n, 'UTC', 'gregory');
const a = z.add({ years: 1 });
(a.timeZoneId === 'UTC') + '|' +
(a.calendarId === 'gregory') + '|' +
(a.toString() === '1971-01-01T00:00:00+00:00[UTC]') + '|' +
(z.subtract({ years: 1 }).toString() === '1969-01-01T00:00:00+00:00[UTC]')",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true|true|true|true");
}
#[test]
fn zoned_date_time_add_string_and_time_fields() {
let mut vm = Vm::new();
// duration 字符串与时间各字段(含跨天进位)合成正确。
let r = eval(
&mut vm,
"const z = new Temporal.ZonedDateTime(0n, 'UTC');
z.add('PT2H30M').toString() + '|' +
z.add({ hours: 24, minutes: 30 }).toString() + '|' +
z.add({ milliseconds: 1500, microseconds: 2, nanoseconds: 3 }).toString() + '|' +
z.subtract('PT1H').toString()",
)
.unwrap();
assert_eq!(
str_val(&vm, r),
"1970-01-01T02:30:00+00:00[UTC]|1970-01-02T00:30:00+00:00[UTC]|1970-01-01T00:00:01.500002003+00:00[UTC]|1969-12-31T23:00:00+00:00[UTC]"
);
}
#[test]
fn zoned_date_time_add_error_paths() {
let mut vm = Vm::new();
// 缺参 TypeError、混合符号 duration RangeError、epoch 越界 RangeError、branding TypeError。
let r = eval(
&mut vm,
"const z = new Temporal.ZonedDateTime(0n, 'UTC');
(() => { try { z.add(); return 'no-throw'; }
catch (e) { return e instanceof TypeError; } })() + '|' +
(() => { try { z.add({ days: 1, hours: -1 }); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })() + '|' +
(() => { try { new Temporal.ZonedDateTime(8640000000000000000000n, 'UTC').add({ hours: 1 }); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })() + '|' +
(() => { try { Temporal.ZonedDateTime.prototype.add.call({}, {}); return 'no-throw'; }
catch (e) { return e instanceof TypeError; } })() + '|' +
(() => { try { Temporal.ZonedDateTime.prototype.subtract.call({}, {}); return 'no-throw'; }
catch (e) { return e instanceof TypeError; } })()",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true|true|true|true|true");
}