From 430f922ef3133bc5e49df2692bc5844e3912401b Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Mon, 17 Aug 2026 01:27:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20ZonedDateTime.compare=20=E8=B7=A8?= =?UTF-8?q?=E5=AE=9E=E4=BE=8B=E6=8C=89=20epoch=20=E6=AF=94=E8=BE=83?= =?UTF-8?q?=EF=BC=88=E5=A4=8D=E7=94=A8=20from=20=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E5=99=A8=EF=BC=8C=E5=BF=BD=E7=95=A5=E6=97=B6=E5=8C=BA/?= =?UTF-8?q?=E6=97=A5=E5=8E=86=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/temporal.rs | 22 ++++++ crates/oxide_vm/src/bindings/bind_temporal.rs | 9 ++- .../oxide_vm/tests/builtins_temporal_tests.rs | 68 +++++++++++++++++++ 3 files changed, 98 insertions(+), 1 deletion(-) diff --git a/crates/oxide_builtins/src/temporal.rs b/crates/oxide_builtins/src/temporal.rs index 3211f984..9bdcec6f 100644 --- a/crates/oxide_builtins/src/temporal.rs +++ b/crates/oxide_builtins/src/temporal.rs @@ -1734,6 +1734,28 @@ pub fn zoned_date_time_from(vm: &mut H, args: &[u8]) -> NativeResult make_zoned_date_time(vm, epoch_ns, &time_zone_id, &calendar_id) } +/// `Temporal.ZonedDateTime.compare(one, two)`:按纪元纳秒比较两 ZDT,忽略时区 ID 与日历。 +/// +/// # 步骤 +/// 1. 两参数各经 zoned_date_time_like_epoch_ns 解析(默认 offset=reject + disambiguation=compatible)。 +/// 2. 仅比较 epoch 纳秒,返回 -1/0/1。 +/// +/// # 边界与前提 +/// - 参数可为 ZDT 对象 / ISO 字符串 / property bag;解析失败抛错。 +/// - 同 epoch 不同时区或日历恒相等(比较不读时区/日历槽)。 +pub fn zoned_date_time_compare(vm: &mut H, args: &[u8]) -> NativeResult { + let one = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let two = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; + let one = native_try!(zoned_date_time_like_epoch_ns(vm, one, "reject", "compatible")); + let two = native_try!(zoned_date_time_like_epoch_ns(vm, two, "reject", "compatible")); + let ordering = one.0.cmp(&two.0); + NativeResult::Ok(JsValue::int(match ordering { + std::cmp::Ordering::Less => -1, + std::cmp::Ordering::Equal => 0, + std::cmp::Ordering::Greater => 1, + })) +} + /// 时区注解文本:命名区原样返回,偏移区规范化为 `±HH:MM` 带冒号;critical 时 `!` 置于括号内。 fn format_time_zone_annotation(time_zone_id: &str, critical: bool) -> String { let inner = if matches!(time_zone_id.as_bytes().first(), Some(b'+') | Some(b'-')) { diff --git a/crates/oxide_vm/src/bindings/bind_temporal.rs b/crates/oxide_vm/src/bindings/bind_temporal.rs index c8feaeb6..ad454902 100644 --- a/crates/oxide_vm/src/bindings/bind_temporal.rs +++ b/crates/oxide_vm/src/bindings/bind_temporal.rs @@ -152,7 +152,14 @@ pub fn bind_temporal(core: &Arc, session: &KernelSession, global: &m world, zoned_date_time_ctor, core, - &[("from", oxide_builtins::temporal::zoned_date_time_from:: as *const (), 1)], + &[ + ("from", oxide_builtins::temporal::zoned_date_time_from:: as *const (), 1), + ( + "compare", + oxide_builtins::temporal::zoned_date_time_compare:: as *const (), + 2, + ), + ], ); let zoned_date_time_proto_ptr = world.zoned_date_time_proto.as_ptr() as *mut JsObject; diff --git a/crates/oxide_vm/tests/builtins_temporal_tests.rs b/crates/oxide_vm/tests/builtins_temporal_tests.rs index 05e6d11c..c221b296 100644 --- a/crates/oxide_vm/tests/builtins_temporal_tests.rs +++ b/crates/oxide_vm/tests/builtins_temporal_tests.rs @@ -1487,6 +1487,74 @@ fn zoned_date_time_from_options_validation() { assert_eq!(str_val(&vm, r), "true|true|true"); } +#[test] +fn zoned_date_time_compare_orders_by_epoch() { + let mut vm = Vm::new(); + // 按 epoch 大小比较而非墙钟:epoch 大者胜,返回 -1/0/1。 + let r = eval( + &mut vm, + "const a = new Temporal.ZonedDateTime(217175010123456789n, 'UTC'); + const b = new Temporal.ZonedDateTime(217175010223456789n, 'UTC'); + Temporal.ZonedDateTime.compare(a, a) + '|' + + Temporal.ZonedDateTime.compare(a, b) + '|' + + Temporal.ZonedDateTime.compare(b, a)", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "0|-1|1"); +} + +#[test] +fn zoned_date_time_compare_ignores_time_zone_and_calendar() { + let mut vm = Vm::new(); + // 同 epoch 不同时区/日历 → 0(只按 epoch 比较)。 + let r = eval( + &mut vm, + "const a = new Temporal.ZonedDateTime(0n, '+01:00', 'hebrew'); + const b = new Temporal.ZonedDateTime(0n, 'UTC'); + const c = new Temporal.ZonedDateTime(1n, '-05:00'); + Temporal.ZonedDateTime.compare(a, b) + '|' + + Temporal.ZonedDateTime.compare(b, c)", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "0|-1"); +} + +#[test] +fn zoned_date_time_compare_accepts_string_and_bag() { + let mut vm = Vm::new(); + // 字符串 / property bag 参数走 from 解析器归一为 epoch 后比较。 + let r = eval( + &mut vm, + "const zdt = new Temporal.ZonedDateTime(0n, 'UTC'); + Temporal.ZonedDateTime.compare('1970-01-01T00:00Z[UTC]', zdt) + '|' + + Temporal.ZonedDateTime.compare( + { year: 1969, month: 12, day: 31, hour: 19, minute: 0, timeZone: '-05:00' }, + zdt) + '|' + + Temporal.ZonedDateTime.compare(zdt, '1970-01-01T00:00:01Z[UTC]')", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "0|0|-1"); +} + +#[test] +fn zoned_date_time_compare_error_paths() { + let mut vm = Vm::new(); + // 非法字符串 / 缺 timeZone 的 bag / 非转换原始值均抛错(RangeError/TypeError)。 + let r = eval( + &mut vm, + "( () => { try { Temporal.ZonedDateTime.compare('garbage', '1970-01-01T00:00Z[UTC]'); return 'no-throw'; } + catch (e) { return e instanceof RangeError; } })() + '|' + + (() => { try { Temporal.ZonedDateTime.compare( + { year: 2000, month: 1, day: 1 }, + '1970-01-01T00:00Z[UTC]'); return 'no-throw'; } + catch (e) { return e instanceof TypeError; } })() + '|' + + (() => { try { Temporal.ZonedDateTime.compare(123, 456); return 'no-throw'; } + catch (e) { return e instanceof TypeError; } })()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "true|true|true"); +} + #[test] fn zoned_date_time_until_defaults_to_hours() { let mut vm = Vm::new();