feat: Temporal 构造器严格校验日历并真实存槽(含 fast path 与日历传播)

This commit is contained in:
2026-08-16 17:30:01 +08:00
parent 4fdc4ef91d
commit 4906e1502d
2 changed files with 213 additions and 43 deletions
+92 -43
View File
@@ -1295,18 +1295,14 @@ pub fn zoned_date_time_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> Native
let Some((time_zone_id, _)) = canonical_time_zone(&time_zone_input) else { let Some((time_zone_id, _)) = canonical_time_zone(&time_zone_input) else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid time zone")); return NativeResult::Err(crate::error::create_range_error(vm, "invalid time zone"));
}; };
if args.len() >= 4 && !vm.reg(args[3]).is_undefined() { let calendar = if args.len() >= 4 && !vm.reg(args[3]).is_undefined() {
let calendar = vm.reg(args[3]); native_try!(temporal_calendar_id_strict(vm, vm.reg(args[3]))).unwrap_or_else(|| "iso8601".to_string())
if !calendar.is_string() { } else {
return NativeResult::Err(crate::error::create_type_error(vm, "invalid calendar")); "iso8601".to_string()
} };
if !to_string(calendar).eq_ignore_ascii_case("iso8601") {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid calendar"));
}
}
let epoch_value = vm.new_bigint(num_bigint::BigInt::from(epoch_ns)); let epoch_value = vm.new_bigint(num_bigint::BigInt::from(epoch_ns));
let time_zone_value = vm.new_string(&time_zone_id); let time_zone_value = vm.new_string(&time_zone_id);
let calendar_value = vm.new_string("iso8601"); let calendar_value = vm.new_string(&calendar);
initialize_temporal_receiver( initialize_temporal_receiver(
vm, vm,
args, args,
@@ -2384,18 +2380,29 @@ pub fn plain_date_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResul
if !(-100_000_001..=100_000_000).contains(&day_count) { if !(-100_000_001..=100_000_000).contains(&day_count) {
return NativeResult::Err(crate::error::create_range_error(vm, "ISO date is out of range")); return NativeResult::Err(crate::error::create_range_error(vm, "ISO date is out of range"));
} }
let calendar = if args.len() > 4 && !vm.reg(args[4]).is_undefined() {
native_try!(temporal_calendar_id_strict(vm, vm.reg(args[4]))).unwrap_or_else(|| "iso8601".to_string())
} else {
"iso8601".to_string()
};
let calendar_value = vm.new_string(&calendar);
initialize_temporal_receiver( initialize_temporal_receiver(
vm, vm,
args, args,
JsObject::OBJ_TYPE_PLAIN_DATE, JsObject::OBJ_TYPE_PLAIN_DATE,
[JsValue::float(year as f64), JsValue::float(month as f64), JsValue::float(day as f64)], [
JsValue::float(year as f64),
JsValue::float(month as f64),
JsValue::float(day as f64),
calendar_value,
],
) )
} }
/// `Temporal.PlainDate.from(value, options)`:接受 ISO 日期字符串或 `{year, month, day}` 对象。 /// `Temporal.PlainDate.from(value, options)`:接受 ISO 日期字符串或 `{year, month, day}` 对象。
pub fn plain_date_from<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult { pub fn plain_date_from<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let val = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; let val = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) };
let (year, month, day) = if val.is_string() { let (year, month, day, calendar) = if val.is_string() {
// 规范顺序:先 ParseTemporalDateString,再 ToTemporalOverflow(options)。 // 规范顺序:先 ParseTemporalDateString,再 ToTemporalOverflow(options)。
let ymd = match parse_plain_date_string(&to_string(val)) { let ymd = match parse_plain_date_string(&to_string(val)) {
Ok(ymd) => ymd, Ok(ymd) => ymd,
@@ -2404,7 +2411,7 @@ pub fn plain_date_from<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
} }
}; };
native_try!(temporal_overflow(vm, args)); native_try!(temporal_overflow(vm, args));
ymd (ymd.0, ymd.1, ymd.2, None)
} else { } else {
let constrain = native_try!(temporal_overflow(vm, args)); let constrain = native_try!(temporal_overflow(vm, args));
match object_date_ymd(vm, val, constrain) { match object_date_ymd(vm, val, constrain) {
@@ -2420,7 +2427,7 @@ pub fn plain_date_from<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if !(-100_000_001..=100_000_000).contains(&day_count) { if !(-100_000_001..=100_000_000).contains(&day_count) {
return NativeResult::Err(crate::error::create_range_error(vm, "ISO date is out of range")); return NativeResult::Err(crate::error::create_range_error(vm, "ISO date is out of range"));
} }
make_plain_date(vm, year, month, day, "iso8601") make_plain_date(vm, year, month, day, calendar.as_deref().unwrap_or("iso8601"))
} }
fn plain_date_ymd<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<(f64, f64, f64), JsValue> { fn plain_date_ymd<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<(f64, f64, f64), JsValue> {
@@ -2644,13 +2651,11 @@ pub fn plain_date_time_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> Native
return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time component")); return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time component"));
} }
if args.len() > 10 && !vm.reg(args[10]).is_undefined() { let calendar = if args.len() > 10 && !vm.reg(args[10]).is_undefined() {
let calendar = vm.reg(args[10]); native_try!(temporal_calendar_id_strict(vm, vm.reg(args[10]))).unwrap_or_else(|| "iso8601".to_string())
let calendar = native_try!(temporal_option_string(vm, calendar)); } else {
if !calendar.eq_ignore_ascii_case("iso8601") { "iso8601".to_string()
return NativeResult::Err(crate::error::create_range_error(vm, "unsupported calendar")); };
}
}
let total_ns = hour as f64 * 3_600_000_000_000.0 let total_ns = hour as f64 * 3_600_000_000_000.0
+ minute as f64 * 60_000_000_000.0 + minute as f64 * 60_000_000_000.0
@@ -2661,6 +2666,7 @@ pub fn plain_date_time_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> Native
if !valid_plain_date_time_range(year, month, day, total_ns) { if !valid_plain_date_time_range(year, month, day, total_ns) {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time component")); return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time component"));
} }
let calendar_value = vm.new_string(&calendar);
initialize_temporal_receiver( initialize_temporal_receiver(
vm, vm,
args, args,
@@ -2670,6 +2676,7 @@ pub fn plain_date_time_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> Native
JsValue::float(month as f64), JsValue::float(month as f64),
JsValue::float(day as f64), JsValue::float(day as f64),
JsValue::float(total_ns), JsValue::float(total_ns),
calendar_value,
], ],
) )
} }
@@ -3037,8 +3044,8 @@ fn parse_partial_calendar_date(input: &str) -> Result<(), String> {
} }
/// ToTemporalCalendar(宽松版,property bag 路径用):undefined → None; /// ToTemporalCalendar(宽松版,property bag 路径用):undefined → None;
/// string → 宽松解析(白名单 ID 或 ISO 串);其他类型 → TypeError。 /// string → 宽松解析(白名单 ID 或 ISO 串);Temporal 实例 → 读日历槽(不触发属性 getter);
/// 日历对象的内部槽 fast path 在日历槽接入后补充。 /// 其他类型 → TypeError。
fn temporal_calendar_id<H: VmHost>(vm: &mut H, value: JsValue) -> Result<Option<String>, JsValue> { fn temporal_calendar_id<H: VmHost>(vm: &mut H, value: JsValue) -> Result<Option<String>, JsValue> {
if value.is_undefined() { if value.is_undefined() {
return Ok(None); return Ok(None);
@@ -3048,12 +3055,26 @@ fn temporal_calendar_id<H: VmHost>(vm: &mut H, value: JsValue) -> Result<Option<
.map(Some) .map(Some)
.map_err(|_| crate::error::create_range_error(vm, "invalid calendar")); .map_err(|_| crate::error::create_range_error(vm, "invalid calendar"));
} }
if value.is_object() {
let ptr = value.as_js_object_ptr();
if !ptr.is_null() {
let obj = unsafe { &*ptr };
if obj.is_plain_date_obj() {
return Ok(Some(get_calendar_id(obj, 3)));
}
if obj.is_plain_date_time_obj() {
return Ok(Some(get_calendar_id(obj, 4)));
}
if obj.is_zoned_date_time_obj() {
return Ok(Some(get_calendar_id(obj, 2)));
}
}
}
Err(crate::error::create_type_error(vm, "invalid calendar")) Err(crate::error::create_type_error(vm, "invalid calendar"))
} }
/// ToTemporalCalendar 严格版(构造器日历参数用):字符串只走 18 项白名单,拒绝 ISO 串; /// ToTemporalCalendar 严格版(构造器日历参数用):字符串只走 18 项白名单,拒绝 ISO 串;
/// undefined / 其他分支与宽松版一致。 /// undefined / 其他分支与宽松版一致。
#[expect(dead_code)] // 构造器日历参数接入前的预留入口
fn temporal_calendar_id_strict<H: VmHost>(vm: &mut H, value: JsValue) -> Result<Option<String>, JsValue> { fn temporal_calendar_id_strict<H: VmHost>(vm: &mut H, value: JsValue) -> Result<Option<String>, JsValue> {
if value.is_string() { if value.is_string() {
return parse_temporal_calendar_id_strict(&to_string(value)) return parse_temporal_calendar_id_strict(&to_string(value))
@@ -3264,7 +3285,7 @@ fn plain_date_time_like_parts<H: VmHost>(
get_double_prop(obj, 1) as u32, get_double_prop(obj, 1) as u32,
get_double_prop(obj, 2) as u32, get_double_prop(obj, 2) as u32,
get_double_prop(obj, 3), get_double_prop(obj, 3),
None, // 实例日历传播在日历槽接入后补齐 Some(get_calendar_id(obj, 4)),
)); ));
} }
if obj.is_plain_date_obj() { if obj.is_plain_date_obj() {
@@ -3273,12 +3294,12 @@ fn plain_date_time_like_parts<H: VmHost>(
get_double_prop(obj, 1) as u32, get_double_prop(obj, 1) as u32,
get_double_prop(obj, 2) as u32, get_double_prop(obj, 2) as u32,
0.0, 0.0,
None, // 实例日历传播在日历槽接入后补齐 Some(get_calendar_id(obj, 3)),
)); ));
} }
if obj.is_zoned_date_time_obj() { if obj.is_zoned_date_time_obj() {
return zoned_date_time_plain_parts(vm, obj) return zoned_date_time_plain_parts(vm, obj)
.map(|(year, month, day, total_ns)| (year, month, day, total_ns, None)); .map(|(year, month, day, total_ns)| (year, month, day, total_ns, Some(get_calendar_id(obj, 2))));
} }
plain_date_time_object_parts(vm, value, obj, constrain, false) plain_date_time_object_parts(vm, value, obj, constrain, false)
} }
@@ -3322,8 +3343,8 @@ pub fn plain_date_time_from<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
make_plain_date_time(vm, year, month, day, total_ns, "iso8601") make_plain_date_time(vm, year, month, day, total_ns, "iso8601")
} else { } else {
let constrain = native_try!(temporal_overflow(vm, args)); let constrain = native_try!(temporal_overflow(vm, args));
let (year, month, day, total_ns, _) = native_try!(plain_date_time_like_parts(vm, value, constrain)); let (year, month, day, total_ns, calendar) = native_try!(plain_date_time_like_parts(vm, value, constrain));
make_plain_date_time(vm, year, month, day, total_ns, "iso8601") make_plain_date_time(vm, year, month, day, total_ns, calendar.as_deref().unwrap_or("iso8601"))
} }
} }
@@ -3504,10 +3525,12 @@ pub fn plain_date_time_calendar_id<H: VmHost>(vm: &mut H, args: &[u8]) -> Native
NativeResult::Ok(vm.new_string(&get_calendar_id(obj, 4))) NativeResult::Ok(vm.new_string(&get_calendar_id(obj, 4)))
} }
/// 返回仅保留日期分量的新 `Temporal.PlainDate`。 /// 返回仅保留日期分量的新 `Temporal.PlainDate`,日历从 receiver 槽继承。
pub fn plain_date_time_to_plain_date<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult { pub fn plain_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 };
let (year, month, day, _) = native_try!(plain_date_time_parts(vm, args)); let (year, month, day, _) = native_try!(plain_date_time_parts(vm, args));
make_plain_date(vm, year, month, day, "iso8601") make_plain_date(vm, year, month, day, &get_calendar_id(obj, 4))
} }
/// 返回仅保留时间分量的新 `Temporal.PlainTime`。 /// 返回仅保留时间分量的新 `Temporal.PlainTime`。
@@ -3651,6 +3674,8 @@ pub fn plain_date_time_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeRe
/// 按 duration-like 分量对 PlainDateTime 做加减:先平衡时间(溢出为天), /// 按 duration-like 分量对 PlainDateTime 做加减:先平衡时间(溢出为天),
/// 再按年/月/周/日推进日期,最后校验范围。 /// 再按年/月/周/日推进日期,最后校验范围。
fn plain_date_time_apply_duration<H: VmHost>(vm: &mut H, args: &[u8], sign: i64) -> NativeResult { fn plain_date_time_apply_duration<H: VmHost>(vm: &mut H, args: &[u8], sign: i64) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
let (year, month, day, time_ns) = match plain_date_time_parts(vm, args) { let (year, month, day, time_ns) = match plain_date_time_parts(vm, args) {
Ok(parts) => parts, Ok(parts) => parts,
Err(error) => return NativeResult::Err(error), Err(error) => return NativeResult::Err(error),
@@ -3701,7 +3726,7 @@ fn plain_date_time_apply_duration<H: VmHost>(vm: &mut H, args: &[u8], sign: i64)
if !valid_plain_date_time_range(yy as i32, mm as u32, dd as u32, new_time_ns as f64) { if !valid_plain_date_time_range(yy as i32, mm as u32, dd as u32, new_time_ns as f64) {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time")); return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time"));
} }
make_plain_date_time(vm, yy as i32, mm as u32, dd as u32, new_time_ns as f64, "iso8601") make_plain_date_time(vm, yy as i32, mm as u32, dd as u32, new_time_ns as f64, &get_calendar_id(obj, 4))
} }
/// PlainDateTime 差值单位层级:year=0 … nanosecond=9;"auto" 仅限 largestUnit。 /// PlainDateTime 差值单位层级:year=0 … nanosecond=9;"auto" 仅限 largestUnit。
@@ -4471,9 +4496,11 @@ fn plain_date_ymd_checked<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<(i32, u3
Ok(ymd) Ok(ymd)
} }
/// 从对象式日期字段(`{year, month, day}`)读三字段;PlainDate 对象直接读内部槽; /// 从对象式日期字段(`{year, month, day}`)读三字段与日历;PlainDate/PlainDateTime 实例
/// 缺字段返回 None。 /// 直接读内部槽;缺字段返回 None。
fn object_date_ymd<H: VmHost>(vm: &mut H, val: JsValue, constrain: bool) -> Result<(i32, u32, u32), JsValue> { fn object_date_ymd<H: VmHost>(
vm: &mut H, val: JsValue, constrain: bool,
) -> Result<(i32, u32, u32, Option<String>), JsValue> {
if !val.is_object() { if !val.is_object() {
return Err(crate::error::create_type_error(vm, "cannot convert to PlainDate")); return Err(crate::error::create_type_error(vm, "cannot convert to PlainDate"));
} }
@@ -4482,25 +4509,35 @@ fn object_date_ymd<H: VmHost>(vm: &mut H, val: JsValue, constrain: bool) -> Resu
return Err(crate::error::create_type_error(vm, "cannot convert to PlainDate")); return Err(crate::error::create_type_error(vm, "cannot convert to PlainDate"));
} }
let obj = unsafe { &*ptr }; let obj = unsafe { &*ptr };
if obj.is_plain_date_obj() || obj.is_plain_date_time_obj() { if obj.is_plain_date_time_obj() {
return Ok(( return Ok((
get_double_prop(obj, 0) as i32, get_double_prop(obj, 0) as i32,
get_double_prop(obj, 1) as u32, get_double_prop(obj, 1) as u32,
get_double_prop(obj, 2) as u32, get_double_prop(obj, 2) as u32,
Some(get_calendar_id(obj, 4)),
));
}
if obj.is_plain_date_obj() {
return Ok((
get_double_prop(obj, 0) as i32,
get_double_prop(obj, 1) as u32,
get_double_prop(obj, 2) as u32,
Some(get_calendar_id(obj, 3)),
)); ));
} }
if obj.is_zoned_date_time_obj() { if obj.is_zoned_date_time_obj() {
let (year, month, day, _) = zoned_date_time_plain_parts(vm, obj)?; let (year, month, day, _) = zoned_date_time_plain_parts(vm, obj)?;
return Ok((year, month, day)); return Ok((year, month, day, Some(get_calendar_id(obj, 2))));
} }
let (year, month, day, _, _) = plain_date_time_object_parts(vm, val, obj, constrain, true)?; let (year, month, day, _, calendar) = plain_date_time_object_parts(vm, val, obj, constrain, true)?;
Ok((year, month, day)) Ok((year, month, day, calendar))
} }
fn date_like_ymd<H: VmHost>(vm: &mut H, val: JsValue) -> Result<(i32, u32, u32), JsValue> { fn date_like_ymd<H: VmHost>(vm: &mut H, val: JsValue) -> Result<(i32, u32, u32, Option<String>), JsValue> {
let ymd = if val.is_string() { let ymd = if val.is_string() {
parse_plain_date_string(&to_string(val)) let (y, m, d) = parse_plain_date_string(&to_string(val))
.map_err(|_| crate::error::create_range_error(vm, "invalid ISO 8601 date"))? .map_err(|_| crate::error::create_range_error(vm, "invalid ISO 8601 date"))?;
(y, m, d, None)
} else { } else {
object_date_ymd(vm, val, true)? object_date_ymd(vm, val, true)?
}; };
@@ -4702,6 +4739,8 @@ pub fn plain_date_to_json<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
/// 按 duration-like 日期字段对日期做加减(Temporal 大单位运算,月份不足日时取月末)。 /// 按 duration-like 日期字段对日期做加减(Temporal 大单位运算,月份不足日时取月末)。
fn date_apply_duration<H: VmHost>(vm: &mut H, args: &[u8], sign: i64) -> NativeResult { fn date_apply_duration<H: VmHost>(vm: &mut H, args: &[u8], sign: i64) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
let date = match plain_date_naive(vm, args) { let date = match plain_date_naive(vm, args) {
Ok(d) => d, Ok(d) => d,
Err(e) => return NativeResult::Err(e), Err(e) => return NativeResult::Err(e),
@@ -4741,7 +4780,7 @@ fn date_apply_duration<H: VmHost>(vm: &mut H, args: &[u8], sign: i64) -> NativeR
return NativeResult::Err(crate::error::create_range_error(vm, "date out of range")); return NativeResult::Err(crate::error::create_range_error(vm, "date out of range"));
} }
} }
make_plain_date(vm, result.year(), result.month(), result.day(), "iso8601") make_plain_date(vm, result.year(), result.month(), result.day(), &get_calendar_id(obj, 3))
} }
/// `Temporal.PlainDate.prototype.add(durationLike)`。 /// `Temporal.PlainDate.prototype.add(durationLike)`。
@@ -4882,4 +4921,14 @@ mod tests {
assert!(validate_temporal_annotation_suffix("[u-ca=iso8601][u-ca=discord]").is_ok()); assert!(validate_temporal_annotation_suffix("[u-ca=iso8601][u-ca=discord]").is_ok());
assert!(validate_temporal_annotation_suffix("[u-ca=iso8601][!u-ca=iso8601]").is_err()); assert!(validate_temporal_annotation_suffix("[u-ca=iso8601][!u-ca=iso8601]").is_err());
} }
#[test]
fn calendar_id_slot_fallback() {
// 字符串槽原值返回由 VM 层构造器/getter 测试覆盖;此处验证 undefined 与非 string 槽兜底 iso8601。
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::undefined());
obj.set_prop_at(3, JsValue::undefined());
assert_eq!(get_calendar_id(&obj, 3), "iso8601");
obj.set_prop_at(3, JsValue::int(7));
assert_eq!(get_calendar_id(&obj, 3), "iso8601");
}
} }
@@ -844,3 +844,124 @@ fn plain_date_time_add_balances_months_days_and_time() {
.unwrap(); .unwrap();
assert_eq!(str_val(&vm, r), "2001:6:18:12:34:0:3:0:1997:11:30:2:29:true"); assert_eq!(str_val(&vm, r), "2001:6:18:12:34:0:3:0:1997:11:30:2:29:true");
} }
#[test]
fn plain_date_constructor_stores_real_calendar() {
let mut vm = Vm::new();
// 规范日历 ID 存槽:大小写折叠为规范小写。
let r = eval(
&mut vm,
"new Temporal.PlainDate(2000, 5, 2, 'gregory').calendarId + ',' +
new Temporal.PlainDate(2000, 5, 2, 'iSo8601').calendarId + ',' +
new Temporal.PlainDate(2000, 5, 2).calendarId",
)
.unwrap();
assert_eq!(str_val(&vm, r), "gregory,iso8601,iso8601");
}
#[test]
fn plain_date_constructor_rejects_invalid_calendar() {
let mut vm = Vm::new();
// 未在白名单 → RangeError;ISO 串拒绝;对象(非 Temporal 实例)→ TypeError。
let r = eval(
&mut vm,
"(() => { try { new Temporal.PlainDate(2000, 5, 2, 'notacal'); return false; }
catch (e) { return e instanceof RangeError; } })() + ',' +
(() => { try { new Temporal.PlainDate(2000, 5, 2, '1997-12-04[u-ca=iso8601]'); return false; }
catch (e) { return e instanceof RangeError; } })() + ',' +
(() => { try { new Temporal.PlainDate(2000, 5, 2, {}); return false; }
catch (e) { return e instanceof TypeError; } })()",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true,true,true");
}
#[test]
fn plain_date_time_constructor_stores_real_calendar() {
let mut vm = Vm::new();
let r = eval(
&mut vm,
"new Temporal.PlainDateTime(2000, 5, 2, 12, 0, 0, 0, 0, 0, 'hebrew').calendarId + ',' +
new Temporal.PlainDateTime(2000, 5, 2).calendarId",
)
.unwrap();
assert_eq!(str_val(&vm, r), "hebrew,iso8601");
}
#[test]
fn plain_date_time_constructor_wrong_calendar_is_type_error() {
let mut vm = Vm::new();
// 规范要求 `{}` 日历 → TypeError(strict 走日历 ID 校验而非宽松 to-option-string)。
let r = eval(
&mut vm,
"(() => { try { new Temporal.PlainDateTime(2000, 5, 2, 12, 0, 0, 0, 0, 0, {}); return false; }
catch (e) { return e instanceof TypeError; } })()",
)
.unwrap();
assert!(r.as_bool());
}
#[test]
fn zoned_date_time_constructor_stores_real_calendar() {
let mut vm = Vm::new();
let r = eval(
&mut vm,
"new Temporal.ZonedDateTime(0n, 'UTC', 'hebrew').calendarId + ',' +
new Temporal.ZonedDateTime(0n, 'UTC').calendarId",
)
.unwrap();
assert_eq!(str_val(&vm, r), "hebrew,iso8601");
}
#[test]
fn zoned_date_time_constructor_wrong_calendar_is_type_error() {
let mut vm = Vm::new();
let r = eval(
&mut vm,
"(() => { try { new Temporal.ZonedDateTime(0n, 'UTC', {}); return false; }
catch (e) { return e instanceof TypeError; } })()",
)
.unwrap();
assert!(r.as_bool());
}
#[test]
fn temporal_instance_as_calendar_fast_path_reads_slot() {
let mut vm = Vm::new();
// Temporal 实例作 property bag 的 calendar:直接读内部槽,不触发 calendar 属性 getter。
let r = eval(
&mut vm,
"const pd = new Temporal.PlainDate(2000, 5, 2, 'gregory');
const pdt = new Temporal.PlainDateTime(2000, 5, 2, 12, 0, 0, 0, 0, 0, 'hebrew');
const zdt = new Temporal.ZonedDateTime(0n, 'UTC', 'japanese');
Object.defineProperty(pd, 'calendar', { get() { throw new Error('getter'); } });
Object.defineProperty(pdt, 'calendar', { get() { throw new Error('getter'); } });
Object.defineProperty(zdt, 'calendar', { get() { throw new Error('getter'); } });
const a = Temporal.PlainDate.from({ year: 2000, month: 5, day: 2, calendar: pd }).calendarId;
const b = Temporal.PlainDateTime.from({ year: 2000, month: 5, day: 2, hour: 12, calendar: pdt }).calendarId;
const c = Temporal.PlainDateTime.from({ year: 2000, month: 5, day: 2, hour: 12, calendar: zdt }).calendarId;
a + ',' + b + ',' + c",
)
.unwrap();
assert_eq!(str_val(&vm, r), "gregory,hebrew,japanese");
}
#[test]
fn plain_date_from_instance_preserves_calendar() {
let mut vm = Vm::new();
// from(实例) 复制日历槽;add/subtract 与 toPlainDate 传播日历。
let r = eval(
&mut vm,
"const pd = new Temporal.PlainDate(2000, 5, 2, 'gregory');
const pdt = new Temporal.PlainDateTime(2000, 5, 2, 12, 0, 0, 0, 0, 0, 'hebrew');
Temporal.PlainDate.from(pd).calendarId + ',' +
Temporal.PlainDateTime.from(pd).calendarId + ',' +
Temporal.PlainDateTime.from(pdt).calendarId + ',' +
pdt.toPlainDate().calendarId + ',' +
pd.add({ days: 1 }).calendarId + ',' +
pd.subtract({ days: 1 }).calendarId + ',' +
pdt.add({ days: 1 }).calendarId",
)
.unwrap();
assert_eq!(str_val(&vm, r), "gregory,gregory,hebrew,hebrew,gregory,gregory,hebrew");
}