feat: implement PlainDateTime add/subtract arithmetic
This commit is contained in:
@@ -2962,6 +2962,72 @@ pub fn plain_date_time_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeRe
|
||||
NativeResult::Ok(vm.new_string(&output))
|
||||
}
|
||||
|
||||
/// 按 duration-like 分量对 PlainDateTime 做加减:先平衡时间(溢出为天),
|
||||
/// 再按年/月/周/日推进日期,最后校验范围。
|
||||
fn plain_date_time_apply_duration<H: VmHost>(vm: &mut H, args: &[u8], sign: i64) -> NativeResult {
|
||||
let (year, month, day, time_ns) = match plain_date_time_parts(vm, args) {
|
||||
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 plain_date_time_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);
|
||||
let total_days = days_from_civil(ny, nm, new_day) + extra_days + day_delta * sign as i128;
|
||||
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"));
|
||||
}
|
||||
make_plain_date_time(vm, yy as i32, mm as u32, dd as u32, new_time_ns as f64)
|
||||
}
|
||||
|
||||
/// `Temporal.PlainDateTime.prototype.add(durationLike, options)`。
|
||||
pub fn plain_date_time_add<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
plain_date_time_apply_duration(vm, args, 1)
|
||||
}
|
||||
|
||||
/// `Temporal.PlainDateTime.prototype.subtract(durationLike, options)`。
|
||||
pub fn plain_date_time_subtract<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
plain_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)
|
||||
|
||||
@@ -510,6 +510,16 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
|
||||
oxide_builtins::temporal::plain_date_time_to_json::<crate::vm::Vm> as *const (),
|
||||
0,
|
||||
),
|
||||
(
|
||||
"add",
|
||||
oxide_builtins::temporal::plain_date_time_add::<crate::vm::Vm> as *const (),
|
||||
1,
|
||||
),
|
||||
(
|
||||
"subtract",
|
||||
oxide_builtins::temporal::plain_date_time_subtract::<crate::vm::Vm> as *const (),
|
||||
1,
|
||||
),
|
||||
],
|
||||
);
|
||||
bind_well_known_data_property(
|
||||
|
||||
@@ -821,3 +821,26 @@ fn zdt_subclass_compare_debug() {
|
||||
.unwrap();
|
||||
assert_eq!(str_val(&vm, r), "number:-1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_date_time_add_balances_months_days_and_time() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(
|
||||
&mut vm,
|
||||
"const a = Temporal.PlainDateTime.from('1997-12-01T12:34');
|
||||
const b = a.add(new Temporal.Duration(3, 6, 0, 17));
|
||||
const c = a.add(new Temporal.Duration(0, 1, 0, 0, 36));
|
||||
const d = a.subtract(new Temporal.Duration(0, 0, 0, 1));
|
||||
const constrained = new Temporal.PlainDateTime(2020, 1, 31, 15, 0).add({ months: 1 });
|
||||
const rejected = (() => { try {
|
||||
new Temporal.PlainDateTime(2020, 1, 31, 15, 0).add({ months: 1 }, { overflow: 'reject' });
|
||||
return false;
|
||||
} catch (e) { return e instanceof RangeError; } })();
|
||||
[b.year, b.month, b.day, b.hour, b.minute, b.second,
|
||||
c.day, c.hour,
|
||||
d.year, d.month, d.day,
|
||||
constrained.month, constrained.day, rejected].join(':')",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(str_val(&vm, r), "2001:6:18:12:34:0:3:0:1997:11:30:2:29:true");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user