feat: ZonedDateTime.compare 跨实例按 epoch 比较(复用 from 解析器,忽略时区/日历)

This commit is contained in:
2026-08-17 01:27:54 +08:00
parent 5bedce985a
commit 430f922ef3
3 changed files with 98 additions and 1 deletions
+22
View File
@@ -1734,6 +1734,28 @@ pub fn zoned_date_time_from<H: VmHost>(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<H: VmHost>(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'-')) {
@@ -152,7 +152,14 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
world,
zoned_date_time_ctor,
core,
&[("from", oxide_builtins::temporal::zoned_date_time_from::<crate::vm::Vm> as *const (), 1)],
&[
("from", oxide_builtins::temporal::zoned_date_time_from::<crate::vm::Vm> as *const (), 1),
(
"compare",
oxide_builtins::temporal::zoned_date_time_compare::<crate::vm::Vm> as *const (),
2,
),
],
);
let zoned_date_time_proto_ptr = world.zoned_date_time_proto.as_ptr() as *mut JsObject;
@@ -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();