mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
feat: ZonedDateTime.add/subtract 时长运算(复用 PDT 叠加算法 + 中间日期检查 + 时区/日历槽保真)
This commit is contained in:
@@ -6162,6 +6162,102 @@ pub fn plain_date_time_subtract<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeRes
|
||||
plain_date_time_apply_duration(vm, args, -1)
|
||||
}
|
||||
|
||||
/// ZDT add/subtract 核心:本地分量叠加 duration(复用 plain_date_time_apply_duration 算法),
|
||||
/// 先做中间日期检查(AddZonedDateTime 的中间日期越界校验),再经 local_to_epoch_ns 转回。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. branding + receiver 本地分量 + 时区偏移。
|
||||
/// 2. duration_like_values 归一 + temporal_overflow 解析(读序:duration → options)。
|
||||
/// 3. 时间增量(hours 起)与日期增量(y/m/w/d)分别按 sign 取反;时间部分
|
||||
/// div_euclid/rem_euclid 拆出 extra_days/new_time_ns。
|
||||
/// 4. 日期部分月份/日钳制叠加后先验中间日期(+ 原 time_ns 须在 PlainDateTime 范围)→ RangeError。
|
||||
/// 5. 时间进位合并 → 最终 (yy, mm, dd, new_time_ns) → valid_plain_date_time_range 校验。
|
||||
/// 6. local_to_epoch_ns + MAX_INSTANT_NS 校验 → make_zoned_date_time 保时区/日历槽。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - duration 全 0 → 值不变的新对象(blank-duration 语义)。
|
||||
/// - 固定偏移下"中间 epoch + 时间增量"与"合并后 local_to_epoch_ns"严格相等,无需分步换算。
|
||||
fn zoned_date_time_apply_duration<H: VmHost>(vm: &mut H, args: &[u8], sign: i64) -> NativeResult {
|
||||
let ptr = native_try!(receiver_obj(vm, args));
|
||||
let obj = unsafe { &*ptr };
|
||||
native_try!(ensure_zoned_date_time(vm, obj));
|
||||
let time_zone_id = to_string(obj.get_prop_at(1));
|
||||
let offset_minutes = native_try!(zoned_date_time_offset_minutes(vm, obj));
|
||||
let (year, month, day, time_ns) = match zoned_date_time_plain_parts(vm, obj) {
|
||||
Ok(parts) => parts,
|
||||
Err(error) => return NativeResult::Err(error),
|
||||
};
|
||||
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 constrain = match temporal_overflow(vm, args) {
|
||||
Ok(constrain) => constrain,
|
||||
Err(error) => return NativeResult::Err(error),
|
||||
};
|
||||
const DAY_NS: i128 = 86_400_000_000_000;
|
||||
// 时间字段(hours 起)单独换算纳秒;days 由日期部分处理,避免重复计入。
|
||||
let [_, _, _, _, h, min, s, ms, us, ns] = values;
|
||||
let time_delta = duration_component_integer(h).unwrap_or(0) * 3_600_000_000_000
|
||||
+ duration_component_integer(min).unwrap_or(0) * 60_000_000_000
|
||||
+ duration_component_integer(s).unwrap_or(0) * 1_000_000_000
|
||||
+ duration_component_integer(ms).unwrap_or(0) * 1_000_000
|
||||
+ duration_component_integer(us).unwrap_or(0) * 1_000
|
||||
+ duration_component_integer(ns).unwrap_or(0);
|
||||
let total_ns = time_ns as i128 + time_delta * sign as i128;
|
||||
let extra_days = total_ns.div_euclid(DAY_NS);
|
||||
let new_time_ns = total_ns.rem_euclid(DAY_NS);
|
||||
|
||||
let [y, m, w, d, ..] = values;
|
||||
let months =
|
||||
(duration_component_integer(y).unwrap_or(0) * 12 + duration_component_integer(m).unwrap_or(0)) * sign as i128;
|
||||
let total_month = i128::from(year) * 12 + i128::from(month) - 1 + months;
|
||||
let ny = total_month.div_euclid(12);
|
||||
let nm = total_month.rem_euclid(12) + 1;
|
||||
let Some(max_day) = days_in_month(ny, nm) else {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid date"));
|
||||
};
|
||||
let day = i128::from(day);
|
||||
let new_day = if day > max_day {
|
||||
if !constrain {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "day out of range"));
|
||||
}
|
||||
max_day
|
||||
} else {
|
||||
day
|
||||
};
|
||||
let day_delta = duration_component_integer(w).unwrap_or(0) * 7 + duration_component_integer(d).unwrap_or(0);
|
||||
// 中间检查:CalendarDateAdd 后的日期 + 原 time_ns 须在 PlainDateTime 范围
|
||||
// (±MAX instant 的 {days:∓1} 在此拦截)。
|
||||
let added_days = days_from_civil(ny, nm, new_day) + day_delta * sign as i128;
|
||||
let (iy, im, id) = civil_from_days(added_days);
|
||||
if !valid_plain_date_time_range(iy as i32, im as u32, id as u32, time_ns) {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time"));
|
||||
}
|
||||
let total_days = added_days + extra_days;
|
||||
let (yy, mm, dd) = civil_from_days(total_days);
|
||||
if !valid_plain_date_time_range(yy as i32, mm as u32, dd as u32, new_time_ns as f64) {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time"));
|
||||
}
|
||||
let epoch_ns = match local_to_epoch_ns(yy as i32, mm as u32, dd as u32, new_time_ns as f64, offset_minutes) {
|
||||
Some(epoch_ns) if epoch_ns.unsigned_abs() <= MAX_INSTANT_NS as u128 => epoch_ns,
|
||||
_ => return NativeResult::Err(crate::error::create_range_error(vm, "ZonedDateTime outside supported range")),
|
||||
};
|
||||
let calendar_id = get_calendar_id(obj, 2);
|
||||
make_zoned_date_time(vm, epoch_ns, &time_zone_id, &calendar_id)
|
||||
}
|
||||
|
||||
/// `Temporal.ZonedDateTime.prototype.add(durationLike, options)`。
|
||||
pub fn zoned_date_time_add<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
zoned_date_time_apply_duration(vm, args, 1)
|
||||
}
|
||||
|
||||
/// `Temporal.ZonedDateTime.prototype.subtract(durationLike, options)`。
|
||||
pub fn zoned_date_time_subtract<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
zoned_date_time_apply_duration(vm, args, -1)
|
||||
}
|
||||
|
||||
/// `Temporal.PlainDateTime.prototype.toJSON()`:输出默认 ISO 日期时间。
|
||||
pub fn plain_date_time_to_json<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
plain_date_time_iso_string(vm, args)
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user