builtins/vm: ZDT 直读 5 方法绑定(toInstant/toPlainDate/toPlainDateTime/toPlainTime/startOfDay)——纪元槽直读零时区换算、本地分量经 plain_parts 换算、startOfDay 当地午夜含范围校验,ZDT 语料 45 文件 fail→pass

This commit is contained in:
2026-09-13 17:06:10 +00:00
parent 3f70af3a25
commit 650b8c00cc
3 changed files with 206 additions and 0 deletions
+66
View File
@@ -7194,6 +7194,72 @@ pub fn zoned_date_time_subtract<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeRes
zoned_date_time_apply_duration(vm, args, -1)
}
/// `Temporal.ZonedDateTime.prototype.toInstant()`:直读纪元槽构造 Instant,零时区换算。
///
/// # 边界与前提
/// - 纪元槽非 BigInt 值(品牌对象被篡改)→ RangeError。
pub fn zoned_date_time_to_instant<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"));
};
make_instant(vm, epoch_ns)
}
/// `Temporal.ZonedDateTime.prototype.toPlainDate()`:本地日期分量 + 日历槽传播。
///
/// # 边界与前提
/// - 本地分量经 zoned_date_time_plain_parts 换算:时区槽解析失败或日期时间越界 → RangeError。
pub fn zoned_date_time_to_plain_date<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 (year, month, day, _) = native_try!(zoned_date_time_plain_parts(vm, obj));
make_plain_date(vm, year, month, day, &get_calendar_id(obj, 2))
}
/// `Temporal.ZonedDateTime.prototype.toPlainDateTime()`:本地全分量 + 日历槽传播。
///
/// # 边界与前提
/// - 本地分量经 zoned_date_time_plain_parts 换算:时区槽解析失败或日期时间越界 → RangeError。
pub fn zoned_date_time_to_plain_date_time<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 (year, month, day, time_ns) = native_try!(zoned_date_time_plain_parts(vm, obj));
make_plain_date_time(vm, year, month, day, time_ns, &get_calendar_id(obj, 2))
}
/// `Temporal.ZonedDateTime.prototype.toPlainTime()`:本地时间分量(PlainTime 无日历槽)。
///
/// # 边界与前提
/// - 本地分量经 zoned_date_time_plain_parts 换算:时区槽解析失败或日期时间越界 → RangeError。
pub fn zoned_date_time_to_plain_time<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 (_, _, _, time_ns) = native_try!(zoned_date_time_plain_parts(vm, obj));
make_plain_time(vm, time_ns)
}
/// `Temporal.ZonedDateTime.prototype.startOfDay()`:当地午夜 ZDT,时区/日历槽保留。
///
/// # 边界与前提
/// - 当地午夜换算委托 start_of_day_epoch_ns(含 Instant 范围校验),越界 → RangeError。
pub fn zoned_date_time_start_of_day<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 (year, month, day, _) = native_try!(zoned_date_time_plain_parts(vm, obj));
let offset_minutes = native_try!(zoned_date_time_offset_minutes(vm, obj));
let Some(epoch_ns) = start_of_day_epoch_ns(year, month, day, offset_minutes) else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid date"));
};
make_zoned_date_time(vm, epoch_ns, &to_string(obj.get_prop_at(1)), &get_calendar_id(obj, 2))
}
/// `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)
@@ -439,6 +439,31 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
oxide_builtins::temporal::zoned_date_time_subtract::<crate::vm::Vm> as *const (),
1,
),
(
"toInstant",
oxide_builtins::temporal::zoned_date_time_to_instant::<crate::vm::Vm> as *const (),
0,
),
(
"toPlainDate",
oxide_builtins::temporal::zoned_date_time_to_plain_date::<crate::vm::Vm> as *const (),
0,
),
(
"toPlainDateTime",
oxide_builtins::temporal::zoned_date_time_to_plain_date_time::<crate::vm::Vm> as *const (),
0,
),
(
"toPlainTime",
oxide_builtins::temporal::zoned_date_time_to_plain_time::<crate::vm::Vm> as *const (),
0,
),
(
"startOfDay",
oxide_builtins::temporal::zoned_date_time_start_of_day::<crate::vm::Vm> as *const (),
0,
),
],
);
@@ -3624,3 +3624,118 @@ fn plain_year_month_until_since_matrix() {
"1,1|-1,-1|0,13|1,1|1,1|1,1|RangeError|0,0|0,0|0,0|18,7|18,7|18,7|18,7|18,7|0,0|TypeError|-42,-11|-42,-11|TypeError|RangeError|TypeError|TypeError|RangeError|RangeError|5,1|RangeError|RangeError|RangeError|RangeError|RangeError|RangeError|RangeError|RangeError|RangeError|1|273790,8|RangeError|RangeError|-273790,-8|0,0|RangeError|RangeError|2,0|1,0|-1,0|1,0|2,0|2,0|3,0|4,0|2,5|0,30|RangeError|1,0"
);
}
// -- ZDT 直读 5 方法(69.1.1) --
#[test]
fn zoned_date_time_to_instant_direct_read() {
let mut vm = Vm::new();
// 纪元槽直读零时区换算:epoch 0 / 近年日期 / 负 epoch;branding 与构造器调用均 TypeError。
let r = eval(
&mut vm,
"(new Temporal.ZonedDateTime(0n, 'UTC').toInstant().epochNanoseconds === 0n) + '|' +
(Temporal.ZonedDateTime.from('2019-10-29T10:46:38.271986102+01:00[+01:00]').toInstant().epochNanoseconds === Temporal.Instant.from('2019-10-29T09:46:38.271986102Z').epochNanoseconds) + '|' +
(new Temporal.ZonedDateTime(-13849764999999999n, 'UTC').toInstant().epochNanoseconds === -13849764999999999n) + '|' +
(() => { try { Temporal.ZonedDateTime.prototype.toInstant.call({}); return 'no-throw'; }
catch (e) { return e instanceof TypeError; } })() + '|' +
(() => { try { new Temporal.ZonedDateTime.prototype.toInstant(); return 'no-throw'; }
catch (e) { return e instanceof TypeError; } })()",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true|true|true|true|true");
}
#[test]
fn zoned_date_time_to_plain_date_basic_and_calendar() {
let mut vm = Vm::new();
// 本地日期分量 + 日历槽传播;分量重组 PlainDate 回读一致。
let r = eval(
&mut vm,
"const d = Temporal.ZonedDateTime.from('2019-10-29T09:46:38.271986102[-07:00]').toPlainDate();
(d.year === 2019) + '|' + (d.month === 10) + '|' + (d.monthCode === 'M10') + '|' + (d.day === 29) + '|' +
(new Temporal.ZonedDateTime(0n, 'UTC', 'japanese').toPlainDate().calendarId === 'japanese') + '|' +
(new Temporal.PlainDate(d.year, d.month, d.day).toString() === '2019-10-29') + '|' +
(new Temporal.PlainDate(d.year, d.month, d.day).calendarId === 'iso8601')",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true|true|true|true|true|true|true");
}
#[test]
fn zoned_date_time_to_plain_datetime_three_pins() {
let mut vm = Vm::new();
// basic 三钉(epoch 0 / post-epoch 偏移区 / pre-epoch Z 注解)+ limits 双边界 +
// 负 epoch 取模正确性钉。
let r = eval(
&mut vm,
"(new Temporal.ZonedDateTime(0n, 'UTC').toPlainDateTime().toString() === '1970-01-01T00:00:00') + '|' +
(Temporal.ZonedDateTime.from('2014-11-21T14:32:27.234567891[+01:00]').toPlainDateTime().toString() === '2014-11-21T14:32:27.234567891') + '|' +
(Temporal.ZonedDateTime.from('1969-07-16T13:32:01.234567891Z[-04:00]').toPlainDateTime().toString() === '1969-07-16T09:32:01.234567891') + '|' +
(() => { const d = new Temporal.ZonedDateTime(-8640000000000000000000n, '-23:59').toPlainDateTime();
return d.year === -271821 && d.month === 4 && d.day === 19 && d.hour === 0 && d.minute === 1; })() + '|' +
(() => { const d = new Temporal.ZonedDateTime(8640000000000000000000n, '+23:59').toPlainDateTime();
return d.year === 275760 && d.month === 9 && d.day === 13 && d.hour === 23 && d.minute === 59; })() + '|' +
(new Temporal.ZonedDateTime(-13849764999999999n, 'UTC').toPlainDateTime().toString() === '1969-07-24T16:50:35.000000001')",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true|true|true|true|true|true");
}
#[test]
fn zdt_to_plain_time_offset_balance() {
let mut vm = Vm::new();
// 负偏移平衡向昨日借时:1001n @ -00:02 → 23:58:00.000001001(us 1 ns 1);
// Z 注解 + 负偏移区纯偏移平移;负 epoch 取模钉。
let r = eval(
&mut vm,
"(new Temporal.ZonedDateTime(1001n, '-00:02').toPlainTime().toString() === '23:58:00.000001001') + '|' +
(Temporal.ZonedDateTime.from('2019-10-29T09:46:38.271986102Z[-07:00]').toPlainTime().toString() === '02:46:38.271986102') + '|' +
(new Temporal.ZonedDateTime(-13849764999999999n, 'UTC').toPlainTime().toString() === '16:50:35.000000001')",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true|true|true");
}
#[test]
fn zdt_start_of_day_basic_and_subclassing() {
let mut vm = Vm::new();
// 当地午夜:跨日余数落到当日 0 点;返回新对象(原型 = ZDT.prototype,子类忽略)全零分量。
let r = eval(
&mut vm,
"const a = new Temporal.ZonedDateTime(10000n * 86400000000000n + 7272123456789n, 'UTC');
(a.startOfDay().epochNanoseconds === 10000n * 86400000000000n) + '|' +
(() => { const s = new Temporal.ZonedDateTime(88000123456789n, 'UTC').startOfDay();
return s.epochNanoseconds === 86400000000000n && s.year === 1970 && s.month === 1 && s.day === 2 &&
s.hour === 0 && s.minute === 0 && s.second === 0 && s.millisecond === 0 && s.microsecond === 0 && s.nanosecond === 0; })() + '|' +
(Object.getPrototypeOf(new Temporal.ZonedDateTime(88000123456789n, 'UTC').startOfDay()) === Temporal.ZonedDateTime.prototype)",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true|true|true");
}
#[test]
fn zdt_start_of_day_range_and_direct_read_prop_shape() {
let mut vm = Vm::new();
// ±MAX 边界三例(-01/+01 午夜越界 RangeError、+00 恰在界内 equals)+ 5 直读方法属性形态
//(function / length 0 / name / branding / 非构造器)。
let r = eval(
&mut vm,
"(() => { try { new Temporal.ZonedDateTime(-864n * 10n**19n, '-01').startOfDay(); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })() + '|' +
(() => { try { new Temporal.ZonedDateTime(-864n * 10n**19n, '+01').startOfDay(); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })() + '|' +
(() => { const z = new Temporal.ZonedDateTime(-864n * 10n**19n, '+00'); return z.startOfDay().equals(z); })() + '|' +
['toInstant', 'toPlainDate', 'toPlainDateTime', 'toPlainTime', 'startOfDay'].map((m) => typeof Temporal.ZonedDateTime.prototype[m] === 'function').join('|') + '|' +
['toInstant', 'toPlainDate', 'toPlainDateTime', 'toPlainTime', 'startOfDay'].map((m) => Temporal.ZonedDateTime.prototype[m].length === 0).join('|') + '|' +
['toInstant', 'toPlainDate', 'toPlainDateTime', 'toPlainTime', 'startOfDay'].map((m) => Temporal.ZonedDateTime.prototype[m].name === m).join('|') + '|' +
['toInstant', 'toPlainDate', 'toPlainDateTime', 'toPlainTime', 'startOfDay'].map((m) => { try { Temporal.ZonedDateTime.prototype[m].call({}); return 'no-throw'; }
catch (e) { return e instanceof TypeError; } }).join('|') + '|' +
['toInstant', 'toPlainDate', 'toPlainDateTime', 'toPlainTime', 'startOfDay'].map((m) => { try { new (Temporal.ZonedDateTime.prototype[m])(); return 'no-throw'; }
catch (e) { return e instanceof TypeError; } }).join('|')",
)
.unwrap();
assert_eq!(
str_val(&vm, r),
"true|true|true|true|true|true|true|true|true|true|true|true|true|true|true|true|true|true|true|true|true|true|true|true|true|true|true|true"
);
}