feat: ZonedDateTime.withTimeZone 与 equals 基础实现

This commit is contained in:
2026-08-16 22:07:38 +08:00
parent 93c80ff9c3
commit ea8631643d
3 changed files with 136 additions and 0 deletions
+61
View File
@@ -1576,6 +1576,67 @@ pub fn zoned_date_time_value_of<H: VmHost>(vm: &mut H, _args: &[u8]) -> NativeRe
NativeResult::Err(crate::error::create_type_error(vm, "Temporal.ZonedDateTime has no valueOf"))
}
/// `Temporal.ZonedDateTime.prototype.withTimeZone(timeZone)`:返回同一 instant 换时区槽的新 ZDT。
///
/// # 步骤
/// 1. branding 校验 receiver 为 ZDT。
/// 2. 读槽 0 epoch 与槽 2 calendar(保不变),参数经 canonical_time_zone 解析新时区。
/// 3. make_zoned_date_time 重建对象,仅替换时区槽。
///
/// # 边界与前提
/// - 参数须为字符串;非字符串抛 TypeError。无法解析的时区串抛 RangeError。
/// - epoch 与 calendar 槽原样保留,仅时区 ID 变化。
pub fn zoned_date_time_with_time_zone<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_zoned_date_time(vm, obj));
let Some(epoch_ns) = get_instant_epoch_ns(obj) else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid ZonedDateTime"));
};
let calendar_id = get_calendar_id(obj, 2);
if args.len() < 2 {
return NativeResult::Err(crate::error::create_type_error(vm, "timeZone is required"));
}
let raw = vm.reg(args[1]);
if !raw.is_string() {
return NativeResult::Err(crate::error::create_type_error(vm, "invalid time zone"));
}
let input = to_string(raw);
let Some((time_zone_id, _)) = canonical_time_zone(&input) else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid time zone"));
};
make_zoned_date_time(vm, epoch_ns, &time_zone_id, &calendar_id)
}
/// `Temporal.ZonedDateTime.prototype.equals(other)`:与另一 ZDT 按 epoch/时区/日历三槽比较。
///
/// # 边界与前提
/// - receiver 须为 ZDT,否则 TypeError。
/// - 参数仅支持 ZDT 对象:三槽(epoch/时区/日历)全等才返回 true。
/// - 非 ZDT 对象或非对象参数:S3 基础范围外,返回 false(完整比较语义待后续)。
pub fn zoned_date_time_equals<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_zoned_date_time(vm, obj));
let other = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) };
if !other.is_object() {
return NativeResult::Ok(JsValue::bool(false));
}
let other_ptr = other.as_js_object_ptr();
if other_ptr.is_null() {
return NativeResult::Ok(JsValue::bool(false));
}
let other_obj = unsafe { &*other_ptr };
if !other_obj.is_zoned_date_time_obj() {
return NativeResult::Ok(JsValue::bool(false));
}
let epoch_equal = get_instant_epoch_ns(obj) == get_instant_epoch_ns(other_obj);
let zone_equal = to_string(obj.get_prop_at(1)) == to_string(other_obj.get_prop_at(1));
let calendar_equal = get_calendar_id(obj, 2) == get_calendar_id(other_obj, 2);
NativeResult::Ok(JsValue::bool(epoch_equal && zone_equal && calendar_equal))
}
/// ZDT 字段 getter 宏:branding 后读本地分量,按选择函数取字段。
macro_rules! zoned_date_time_parts_getter {
($name:ident, $select:expr) => {
@@ -396,6 +396,16 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
oxide_builtins::temporal::zoned_date_time_value_of::<crate::vm::Vm> as *const (),
0,
),
(
"withTimeZone",
oxide_builtins::temporal::zoned_date_time_with_time_zone::<crate::vm::Vm> as *const (),
1,
),
(
"equals",
oxide_builtins::temporal::zoned_date_time_equals::<crate::vm::Vm> as *const (),
1,
),
],
);
@@ -1269,3 +1269,68 @@ fn zoned_date_time_to_string_options_validation() {
.unwrap();
assert_eq!(str_val(&vm, r), "true|true|true|true");
}
#[test]
fn zoned_date_time_with_time_zone_swaps_zone_keeps_instant() {
let mut vm = Vm::new();
// 换时区后 instant 保留、返回新对象、timeZoneId 更新;大小写不敏感 + ±HH 均可用。
let r = eval(
&mut vm,
"const z = new Temporal.ZonedDateTime(0n, 'UTC');
const w = z.withTimeZone('+01:00');
w.timeZoneId + '|' + w.epochNanoseconds + '|' +
(w !== z) + '|' + z.timeZoneId + '|' +
new Temporal.ZonedDateTime(0n, 'uTc').withTimeZone('+01').timeZoneId",
)
.unwrap();
assert_eq!(str_val(&vm, r), "+01:00|0|true|UTC|+01");
}
#[test]
fn zoned_date_time_with_time_zone_bad_receiver_or_zone() {
let mut vm = Vm::new();
// 非 ZDT receiver 抛 TypeError;非法时区串抛 RangeError;非字符串抛 TypeError。
let r = eval(
&mut vm,
"( () => { try { Temporal.ZonedDateTime.prototype.withTimeZone.call({}, 'UTC'); return 'no-throw'; }
catch (e) { return e instanceof TypeError; } })() + '|' +
( () => { try { new Temporal.ZonedDateTime(0n, 'UTC').withTimeZone('Not/AZone'); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })() + '|' +
( () => { try { new Temporal.ZonedDateTime(0n, 'UTC').withTimeZone(42); return 'no-throw'; }
catch (e) { return e instanceof TypeError; } })()",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true|true|true");
}
#[test]
fn zoned_date_time_equals_compares_epoch_zone_calendar() {
let mut vm = Vm::new();
// 同 instant/时区/日历 true;异 instant false;异时区 false;异日历 false。
let r = eval(
&mut vm,
"const a = new Temporal.ZonedDateTime(0n, 'UTC');
const b = new Temporal.ZonedDateTime(0n, 'UTC');
const c = new Temporal.ZonedDateTime(1n, 'UTC');
const d = new Temporal.ZonedDateTime(0n, '-05:00');
const e = new Temporal.ZonedDateTime(0n, 'UTC', 'japanese');
a.equals(b) + '|' + a.equals(c) + '|' + a.equals(d) + '|' + a.equals(e)",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true|false|false|false");
}
#[test]
fn zoned_date_time_equals_receiver_branding_and_non_zoned_arg() {
let mut vm = Vm::new();
// 非 ZDT receiver 抛 TypeError;非 ZDT 对象参数返回 false。
let r = eval(
&mut vm,
"( () => { try { Temporal.ZonedDateTime.prototype.equals.call({}, new Temporal.ZonedDateTime(0n, 'UTC')); return 'no-throw'; }
catch (e) { return e instanceof TypeError; } })() + '|' +
new Temporal.ZonedDateTime(0n, 'UTC').equals({}) + '|' +
new Temporal.ZonedDateTime(0n, 'UTC').equals('2021-01-01T00:00:00+00:00[UTC]')",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true|false|false");
}