diff --git a/crates/oxide_builtins/src/temporal.rs b/crates/oxide_builtins/src/temporal.rs index efc9380c..e2b49ad6 100644 --- a/crates/oxide_builtins/src/temporal.rs +++ b/crates/oxide_builtins/src/temporal.rs @@ -1576,6 +1576,67 @@ pub fn zoned_date_time_value_of(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(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(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) => { diff --git a/crates/oxide_vm/src/bindings/bind_temporal.rs b/crates/oxide_vm/src/bindings/bind_temporal.rs index b3c38b34..89b56311 100644 --- a/crates/oxide_vm/src/bindings/bind_temporal.rs +++ b/crates/oxide_vm/src/bindings/bind_temporal.rs @@ -396,6 +396,16 @@ pub fn bind_temporal(core: &Arc, session: &KernelSession, global: &m oxide_builtins::temporal::zoned_date_time_value_of:: as *const (), 0, ), + ( + "withTimeZone", + oxide_builtins::temporal::zoned_date_time_with_time_zone:: as *const (), + 1, + ), + ( + "equals", + oxide_builtins::temporal::zoned_date_time_equals:: as *const (), + 1, + ), ], ); diff --git a/crates/oxide_vm/tests/builtins_temporal_tests.rs b/crates/oxide_vm/tests/builtins_temporal_tests.rs index ae32b65b..193d1c21 100644 --- a/crates/oxide_vm/tests/builtins_temporal_tests.rs +++ b/crates/oxide_vm/tests/builtins_temporal_tests.rs @@ -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"); +}