feat: ZonedDateTime.with/withPlainTime/withCalendar 字段合并(offset 默认 prefer + 时间串歧义解析)

This commit is contained in:
2026-08-16 23:49:30 +08:00
parent fce2257f08
commit 88a08cff11
3 changed files with 1008 additions and 16 deletions
+725 -18
View File
@@ -1432,15 +1432,21 @@ fn is_time_zone_annotation_value(annotation: &str) -> bool {
false false
} }
/// 读 ZonedDateTime.from 的 options(disambiguation → offset 顺序),先 Get 后统一白名单校验。 /// 读 ZonedDateTime 的 options(disambiguation → offset 顺序),逐项 Get/转换/白名单校验。
///
/// # 步骤
/// 1. options 为 undefined 时返回默认(offset=default_offset,disambiguation=compatible)。
/// 2. 先 Get disambiguation 并立即转换 + 校验,再 Get offset 并立即转换 + 校验。
/// ///
/// # 边界与前提 /// # 边界与前提
/// - options 为 undefined 时返回默认(offset=reject,disambiguation=compatible)。 /// - default_offset 由调用方决定(from 用 reject,with 用 prefer)。
/// - options 为非对象原始值抛 TypeError;选项值不在白名单抛 RangeError。 /// - options 为非对象原始值抛 TypeError;选项值不在白名单抛 RangeError。
fn zoned_date_time_options<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<(String, String), JsValue> { fn zoned_date_time_options<H: VmHost>(
vm: &mut H, args: &[u8], default_offset: &str,
) -> Result<(String, String), JsValue> {
let options = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; let options = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() };
if options.is_undefined() { if options.is_undefined() {
return Ok(("reject".to_string(), "compatible".to_string())); return Ok((default_offset.to_string(), "compatible".to_string()));
} }
if !options.is_object() { if !options.is_object() {
return Err(crate::error::create_type_error(vm, "options must be an object")); return Err(crate::error::create_type_error(vm, "options must be an object"));
@@ -1451,25 +1457,28 @@ fn zoned_date_time_options<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<(String
} }
let options_obj = unsafe { &*ptr }; let options_obj = unsafe { &*ptr };
// 先 Get 再校验:disambiguation 在前,offset 在后。 // 逐个选项处理:Get → ToString → 白名单,保证用户代码可观察的读序为
// disambiguation 完整处理完后再处理 offset。
let disambiguation_raw = temporal_option_value(vm, options_obj, options, "disambiguation")?; let disambiguation_raw = temporal_option_value(vm, options_obj, options, "disambiguation")?;
let offset_raw = temporal_option_value(vm, options_obj, options, "offset")?;
let disambiguation = if disambiguation_raw.is_undefined() { let disambiguation = if disambiguation_raw.is_undefined() {
"compatible".to_string() "compatible".to_string()
} else { } else {
temporal_option_string(vm, disambiguation_raw)? let value = temporal_option_string(vm, disambiguation_raw)?;
}; if !matches!(value.as_str(), "compatible" | "earlier" | "later" | "reject") {
let offset = if offset_raw.is_undefined() {
"reject".to_string()
} else {
temporal_option_string(vm, offset_raw)?
};
if !matches!(offset.as_str(), "prefer" | "use" | "ignore" | "reject") {
return Err(crate::error::create_range_error(vm, "invalid offset"));
}
if !matches!(disambiguation.as_str(), "compatible" | "earlier" | "later" | "reject") {
return Err(crate::error::create_range_error(vm, "invalid disambiguation")); return Err(crate::error::create_range_error(vm, "invalid disambiguation"));
} }
value
};
let offset_raw = temporal_option_value(vm, options_obj, options, "offset")?;
let offset = if offset_raw.is_undefined() {
default_offset.to_string()
} else {
let value = temporal_option_string(vm, offset_raw)?;
if !matches!(value.as_str(), "prefer" | "use" | "ignore" | "reject") {
return Err(crate::error::create_range_error(vm, "invalid offset"));
}
value
};
Ok((offset, disambiguation)) Ok((offset, disambiguation))
} }
@@ -1719,7 +1728,7 @@ fn zoned_date_time_bag_parts<H: VmHost>(
/// - disambiguation 本批仅做选项值校验,固定偏移时区下四个取值算法等价。 /// - disambiguation 本批仅做选项值校验,固定偏移时区下四个取值算法等价。
pub fn zoned_date_time_from<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult { pub fn zoned_date_time_from<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; let value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let (offset_mode, disambiguation) = native_try!(zoned_date_time_options(vm, args)); let (offset_mode, disambiguation) = native_try!(zoned_date_time_options(vm, args, "reject"));
let (epoch_ns, time_zone_id, calendar_id) = let (epoch_ns, time_zone_id, calendar_id) =
native_try!(zoned_date_time_like_epoch_ns(vm, value, &offset_mode, &disambiguation)); native_try!(zoned_date_time_like_epoch_ns(vm, value, &offset_mode, &disambiguation));
make_zoned_date_time(vm, epoch_ns, &time_zone_id, &calendar_id) make_zoned_date_time(vm, epoch_ns, &time_zone_id, &calendar_id)
@@ -2014,6 +2023,649 @@ pub fn zoned_date_time_equals<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResul
NativeResult::Ok(JsValue::bool(epoch_equal && zone_equal && calendar_equal)) NativeResult::Ok(JsValue::bool(epoch_equal && zone_equal && calendar_equal))
} }
/// with 字段合并的中间结果:未钳制的合并分量 + partial month/monthCode + bag offset。
struct ZdtMergedFields {
year: i32,
month: Option<f64>,
day: f64,
hour: f64,
minute: f64,
second: f64,
millisecond: f64,
microsecond: f64,
nanosecond: f64,
month_code: Option<(f64, bool)>,
bag_offset: Option<i32>,
}
/// ToPrimitive(String) 后要求结果为字符串,否则 TypeError(ParseMonthCode/ToOffsetString 语义)。
fn temporal_string_strict<H: VmHost>(vm: &mut H, value: JsValue) -> Result<String, JsValue> {
let primitive = oxide_runtime_api::to_primitive(value, oxide_runtime_api::ToPrimitiveHint::String, vm)
.map_err(|error| native_engine_error(vm, &error))?;
if !primitive.is_string() {
return Err(crate::error::create_type_error(vm, "expected a string"));
}
Ok(to_string(primitive))
}
/// RejectObjectWithCalendarOrTimeZone:非对象 / Temporal 实例 / calendar、timeZone 非 undefined
/// 均返回 TypeError(with 的 partial 参数校验)。
///
/// # 边界与前提
/// - Temporal 内部槽检查先于 calendar/timeZone 的 Get(IsPartialTemporalObject 步骤序)。
/// - 参数校验通过后调用方才能读字段。
fn reject_partial_object_with_calendar_or_time_zone<H: VmHost>(vm: &mut H, value: JsValue) -> Result<(), JsValue> {
if !value.is_object() {
return Err(crate::error::create_type_error(vm, "invalid argument"));
}
let ptr = value.as_js_object_ptr();
if ptr.is_null() {
return Err(crate::error::create_type_error(vm, "invalid argument"));
}
let obj = unsafe { &*ptr };
if obj.is_plain_date_obj()
|| obj.is_plain_date_time_obj()
|| obj.is_plain_time_obj()
|| obj.is_zoned_date_time_obj()
{
return Err(crate::error::create_type_error(vm, "invalid argument"));
}
let calendar = temporal_option_value(vm, obj, value, "calendar")?;
if !calendar.is_undefined() {
return Err(crate::error::create_type_error(vm, "invalid argument"));
}
let time_zone = temporal_option_value(vm, obj, value, "timeZone")?;
if !time_zone.is_undefined() {
return Err(crate::error::create_type_error(vm, "invalid argument"));
}
Ok(())
}
/// with 的部分字段读取与合并:按字典序读 bag 字段,与 receiver 默认分量合并。
///
/// # 步骤
/// 1. 数值字段 ToNumber→trunc(NaN/±Inf RangeError,day 额外拒绝 <1);monthCode/offset 走 ToString。
/// 2. undefined 不覆盖;至少一个字段有定义否则 TypeError。
/// 3. 返回合并后的原始分量(未钳制)+ monthCode 解析 + bag offset 分钟。
///
/// # 边界与前提
/// - 调用方须先完成 RejectObjectWithCalendarOrTimeZone(calendar/timeZone 已拒绝)。
/// - monthCode 的闰月/超界/与 month 冲突校验延迟到选项解析后(对齐 spec 读序)。
/// - offset 经 ToOffsetString:非字符串 TypeError、坏格式 RangeError。
fn zoned_date_time_with_fields<H: VmHost>(
vm: &mut H, value: JsValue, defaults: (i32, u32, u32, f64),
) -> Result<ZdtMergedFields, JsValue> {
let ptr = value.as_js_object_ptr();
if ptr.is_null() {
return Err(crate::error::create_type_error(vm, "invalid argument"));
}
let obj = unsafe { &*ptr };
// 读序(字典序):day → hour → microsecond → millisecond → minute → month →
// monthCode → nanosecond → offset → second → year。
let day_raw = temporal_option_value(vm, obj, value, "day")?;
let hour_raw = temporal_option_value(vm, obj, value, "hour")?;
let microsecond_raw = temporal_option_value(vm, obj, value, "microsecond")?;
let millisecond_raw = temporal_option_value(vm, obj, value, "millisecond")?;
let minute_raw = temporal_option_value(vm, obj, value, "minute")?;
let month_raw = temporal_option_value(vm, obj, value, "month")?;
let month_code_raw = temporal_option_value(vm, obj, value, "monthCode")?;
let nanosecond_raw = temporal_option_value(vm, obj, value, "nanosecond")?;
let offset_raw = temporal_option_value(vm, obj, value, "offset")?;
let second_raw = temporal_option_value(vm, obj, value, "second")?;
let year_raw = temporal_option_value(vm, obj, value, "year")?;
// 至少一个字段有定义,否则 TypeError(object-must-contain-at-least-one-property)。
if [
day_raw,
hour_raw,
microsecond_raw,
millisecond_raw,
minute_raw,
month_raw,
month_code_raw,
nanosecond_raw,
offset_raw,
second_raw,
year_raw,
]
.iter()
.all(|raw| raw.is_undefined())
{
return Err(crate::error::create_type_error(vm, "no properties present"));
}
// 数值字段:ToNumber→trunc;NaN/±Inf RangeError;day 额外要求 ≥1。
let convert_integer = |vm: &mut H, raw: JsValue| -> Result<Option<f64>, JsValue> {
if raw.is_undefined() {
Ok(None)
} else {
let number = temporal_option_number(vm, raw)?;
if !number.is_finite() {
return Err(crate::error::create_range_error(vm, "invalid date-time component"));
}
Ok(Some(number.trunc()))
}
};
let day = convert_integer(vm, day_raw)?;
let hour = convert_integer(vm, hour_raw)?;
let microsecond = convert_integer(vm, microsecond_raw)?;
let millisecond = convert_integer(vm, millisecond_raw)?;
let minute = convert_integer(vm, minute_raw)?;
let month = convert_integer(vm, month_raw)?;
let nanosecond = convert_integer(vm, nanosecond_raw)?;
let second = convert_integer(vm, second_raw)?;
let year = convert_integer(vm, year_raw)?;
if let Some(day) = day {
if day < 1.0 {
return Err(crate::error::create_range_error(vm, "invalid date-time component"));
}
}
// monthCode:ToString 后格式校验(M + 两位数字 + 可选 L),闰月/超界留到解析阶段。
let month_code = if month_code_raw.is_undefined() {
None
} else {
let code = temporal_string_strict(vm, month_code_raw)?;
let digits_ok =
code.len() >= 3 && code.starts_with('M') && code.as_bytes()[1..3].iter().all(u8::is_ascii_digit);
let well_formed = digits_ok && (code.len() == 3 || (code.len() == 4 && code.ends_with('L')));
if !well_formed {
return Err(crate::error::create_range_error(vm, "invalid monthCode"));
}
let number = code[1..3]
.parse::<f64>()
.map_err(|_| crate::error::create_range_error(vm, "invalid monthCode"))?;
Some((number, code.ends_with('L')))
};
// offset:ToString → 字符串语法校验(小数秒 ≤9 位),分钟数供 offset 选项决策。
let bag_offset = if offset_raw.is_undefined() {
None
} else {
let offset_input = temporal_string_strict(vm, offset_raw)?;
let minutes = parse_any_offset_minutes(&offset_input)
.ok_or_else(|| crate::error::create_range_error(vm, "invalid offset"))?;
if !valid_offset_fraction(&offset_input) {
return Err(crate::error::create_range_error(vm, "invalid offset"));
}
Some(minutes)
};
let (receiver_year, _receiver_month, receiver_day, receiver_time_ns) = defaults;
let (rh, rm, rs, rms, rus, rns) = plain_time_components(receiver_time_ns);
Ok(ZdtMergedFields {
year: year.unwrap_or(receiver_year as f64) as i32,
month,
day: day.unwrap_or(receiver_day as f64),
hour: hour.unwrap_or(rh as f64),
minute: minute.unwrap_or(rm as f64),
second: second.unwrap_or(rs as f64),
millisecond: millisecond.unwrap_or(rms as f64),
microsecond: microsecond.unwrap_or(rus as f64),
nanosecond: nanosecond.unwrap_or(rns as f64),
month_code,
bag_offset,
})
}
/// `Temporal.ZonedDateTime.prototype.with(temporalZonedDateTimeLike, options)`。
///
/// # 步骤
/// 1. branding + IsPartialTemporalObject(calendar/timeZone/Temporal 实例 → TypeError)。
/// 2. receiver 本地分量与偏移;zoned_date_time_with_fields 读字段合并。
/// 3. options:disambiguation → offset(默认 prefer)→ overflow(逐项 Get/校验)。
/// 4. monthCode 闰月/超界/冲突校验 + constrain/reject 钳制 + PlainDateTime 范围校验。
/// 5. offset 选项决策 → local_to_epoch_ns → Instant 范围校验 → make_zoned_date_time。
///
/// # 边界与前提
/// - 字段读取先于 options 解析(options-wrong-type 先报字段错误)。
/// - 选项解析先于 monthCode 算法校验(options-read-before-algorithmic-validation)。
/// - disambiguation 在固定偏移时区下四取值算法等价,仅做白名单校验。
pub fn zoned_date_time_with<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 value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
native_try!(reject_partial_object_with_calendar_or_time_zone(vm, value));
let time_zone_id = to_string(obj.get_prop_at(1));
let offset_minutes = native_try!(zoned_date_time_offset_minutes(vm, obj));
let (year, month, day, time_ns) = native_try!(zoned_date_time_plain_parts(vm, obj));
let merged = native_try!(zoned_date_time_with_fields(vm, value, (year, month, day, time_ns)));
// options:字段读取完成后解析(disambiguation → offset → overflow)。
let (offset_mode, _disambiguation) = native_try!(zoned_date_time_options(vm, args, "prefer"));
let constrain = native_try!(temporal_overflow(vm, args));
// CalendarResolveFields(ISO):partial 的 monthCode 闰月 / 超 12 拒绝;month 与
// monthCode 冲突仅在两者都来自 partial 时校验(partial 有 monthCode 时 receiver
// 的 month 被覆盖,不参与比较)。
let receiver_month_f = month as f64;
let merged_month = match (merged.month, merged.month_code) {
(_, Some((_, true))) => {
return NativeResult::Err(crate::error::create_range_error(vm, "monthCode is not valid for ISO calendar"));
}
(Some(month), Some((code, false))) => {
if code > 12.0 {
return NativeResult::Err(crate::error::create_range_error(
vm,
"monthCode is not valid for ISO calendar",
));
}
if month != code {
return NativeResult::Err(crate::error::create_range_error(vm, "month and monthCode disagree"));
}
code
}
(None, Some((code, false))) => {
if code > 12.0 {
return NativeResult::Err(crate::error::create_range_error(
vm,
"monthCode is not valid for ISO calendar",
));
}
code
}
(Some(month), None) => month,
(None, None) => receiver_month_f,
};
// RegulateISODate:constrain 钳制月/日,reject 直接校验。
let (year, month, day) = if constrain {
let month = merged_month.clamp(1.0, 12.0);
let max_day = days_in_month(i128::from(merged.year), month as i128).unwrap_or(31) as f64;
(merged.year, month, merged.day.clamp(1.0, max_day))
} else {
if !valid_iso_date(merged.year, merged_month as u32, merged.day as u32) {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time component"));
}
(merged.year, merged_month, merged.day)
};
// RegulateTime:constrain 钳制时/分/秒/亚秒,reject 直接校验。
let (hour, minute, second, millisecond, microsecond, nanosecond) = if constrain {
(
merged.hour.clamp(0.0, 23.0),
merged.minute.clamp(0.0, 59.0),
merged.second.clamp(0.0, 59.0),
merged.millisecond.clamp(0.0, 999.0),
merged.microsecond.clamp(0.0, 999.0),
merged.nanosecond.clamp(0.0, 999.0),
)
} else {
let values = [
merged.hour,
merged.minute,
merged.second,
merged.millisecond,
merged.microsecond,
merged.nanosecond,
];
if !valid_plain_time(
values[0] as u32,
values[1] as u32,
values[2] as u32,
values[3] as u32,
values[4] as u32,
values[5] as u32,
) {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid time component"));
}
(values[0], values[1], values[2], values[3], values[4], values[5])
};
let (year, month, day) = (year, month as u32, day as u32);
let total_ns = hour * 3_600_000_000_000.0
+ minute * 60_000_000_000.0
+ second * 1_000_000_000.0
+ millisecond * 1_000_000.0
+ microsecond * 1_000.0
+ nanosecond;
if !valid_plain_date_time_range(year, month, day, total_ns) {
return NativeResult::Err(crate::error::create_range_error(vm, "date-time out of range"));
}
// InterpretISODateTimeOffset(固定偏移简化):按 offset 选项在 bag 偏移与时区偏移间选择。
let epoch_ns = match (merged.bag_offset, offset_mode.as_str()) {
(None, _) => local_to_epoch_ns(year, month, day, total_ns, offset_minutes),
(Some(offset), "use") => local_to_epoch_ns(year, month, day, total_ns, offset),
(Some(_), "ignore") => local_to_epoch_ns(year, month, day, total_ns, offset_minutes),
(Some(offset), "prefer") => {
if offset == offset_minutes {
local_to_epoch_ns(year, month, day, total_ns, offset)
} else {
local_to_epoch_ns(year, month, day, total_ns, offset_minutes)
}
}
(Some(offset), "reject") => {
if offset == offset_minutes {
local_to_epoch_ns(year, month, day, total_ns, offset)
} else {
return NativeResult::Err(crate::error::create_range_error(vm, "offset and time zone disagree"));
}
}
_ => unreachable!(),
}
.ok_or_else(|| crate::error::create_range_error(vm, "invalid date-time"));
let epoch_ns = native_try!(epoch_ns);
if epoch_ns.unsigned_abs() > MAX_INSTANT_NS as u128 {
return NativeResult::Err(crate::error::create_range_error(vm, "ZonedDateTime outside supported range"));
}
let calendar_id = get_calendar_id(obj, 2);
make_zoned_date_time(vm, epoch_ns, &time_zone_id, &calendar_id)
}
/// `Temporal.ZonedDateTime.prototype.withCalendar(calendar)`:换日历槽,epoch/时区不变。
///
/// # 步骤
/// 1. branding 校验 receiver 为 ZDT。
/// 2. 参数经 temporal_calendar_id(宽松版)解析:字符串白名单/ISO 串 → ID,
/// PlainDate/PDT/ZDT 对象读日历槽(不触发 getter)。
/// 3. make_zoned_date_time 重建对象,仅替换日历槽。
///
/// # 边界与前提
/// - 缺参 / undefined → TypeError;非字符串非对象(number/null 等)→ TypeError。
/// - 非法日历串 → RangeError;日历 ID 大小写不敏感。
pub fn zoned_date_time_with_calendar<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 calendar_value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
if calendar_value.is_undefined() {
return NativeResult::Err(crate::error::create_type_error(vm, "calendar is required"));
}
let calendar = native_try!(temporal_calendar_id(vm, calendar_value)).unwrap_or_else(|| "iso8601".to_string());
let Some(epoch_ns) = get_instant_epoch_ns(obj) else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid ZonedDateTime"));
};
let time_zone_id = to_string(obj.get_prop_at(1));
make_zoned_date_time(vm, epoch_ns, &time_zone_id, &calendar)
}
/// 校验 offset 串的小数秒位数 ≤9(parse_any_offset_minutes 不限制位数,此处补查)。
fn valid_offset_fraction(value: &str) -> bool {
let Some(dot) = value.find(['.', ',']) else {
return true;
};
let fraction = &value[dot + 1..];
fraction.len() <= 9 && fraction.bytes().all(|byte| byte.is_ascii_digit())
}
/// 时间串的日期歧义判定:形如 YYYY-MM / MMDD / YYYYMM / MM-DD 且作为日期合法 → 歧义。
///
/// # 边界与前提
/// - 2 月按闰年处理(0229 判歧义、0230 不判)。
/// - 月/日非法(13、00、2 月 30 日等)不算歧义,按时间解析。
fn is_ambiguous_date_string(s: &str) -> bool {
let bytes = s.as_bytes();
let all_digits = |range: std::ops::Range<usize>| {
bytes.get(range.clone()).is_some_and(|part| part.iter().all(u8::is_ascii_digit))
};
let two_digits = |at: usize| -> Option<i32> {
if at + 1 >= bytes.len() {
None
} else {
Some((bytes[at] - b'0') as i32 * 10 + (bytes[at + 1] - b'0') as i32)
}
};
let day_in_month = |month: i32, day: i32| -> bool {
// 2 月按闰年(29 天)判定歧义。
let max = match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => 29,
_ => return false,
};
day >= 1 && day <= max
};
match s.len() {
4 if all_digits(0..4) => {
let (month, day) = (two_digits(0).unwrap(), two_digits(2).unwrap());
day_in_month(month, day)
}
6 if all_digits(0..6) => (1..=12).contains(&two_digits(4).unwrap()),
5 if bytes[2] == b'-' && all_digits(0..2) && all_digits(3..5) => {
let (month, day) = (two_digits(0).unwrap(), two_digits(3).unwrap());
day_in_month(month, day)
}
7 if bytes[4] == b'-' && all_digits(0..4) && all_digits(5..7) => (1..=12).contains(&two_digits(5).unwrap()),
_ => false,
}
}
/// 解析时间主体(时[:分[:秒[.小数]]] 或 HHMM / HHMMSS),闰秒按前一秒。
fn parse_plain_clock(clock: &str) -> Option<f64> {
let (clock, fraction) = match clock.find(['.', ',']) {
Some(index) => (&clock[..index], Some(&clock[index + 1..])),
None => (clock, None),
};
let subsecond = match fraction {
Some(digits) => {
if digits.is_empty() || digits.len() > 9 || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
let mut padded = digits.to_string();
padded.extend(std::iter::repeat('0').take(9 - digits.len()));
padded.parse::<u32>().ok()?
}
None => 0,
};
let (hour, minute, second) = if clock.contains(':') {
let fields: Vec<&str> = clock.split(':').collect();
if fields.is_empty()
|| fields.len() > 3
|| fields
.iter()
.any(|field| field.len() != 2 || !field.bytes().all(|byte| byte.is_ascii_digit()))
{
return None;
}
if fraction.is_some() && fields.len() < 3 {
return None;
}
(
fields[0].parse::<u32>().ok()?,
fields.get(1).map_or(Ok(0), |field| field.parse()).ok()?,
fields.get(2).map_or(Ok(0), |field| field.parse()).ok()?,
)
} else {
if !clock.bytes().all(|byte| byte.is_ascii_digit())
|| clock.len() % 2 != 0
|| clock.len() > 6
|| clock.is_empty()
{
return None;
}
if fraction.is_some() && clock.len() < 6 {
return None;
}
let read2 = |at: usize| (clock.as_bytes()[at] - b'0') as u32 * 10 + (clock.as_bytes()[at + 1] - b'0') as u32;
(
read2(0),
if clock.len() >= 4 { read2(2) } else { 0 },
if clock.len() >= 6 { read2(4) } else { 0 },
)
};
let second = if second == 60 { 59 } else { second };
if hour > 23 || minute > 59 || second > 59 {
return None;
}
Some(
hour as f64 * 3_600_000_000_000.0
+ minute as f64 * 60_000_000_000.0
+ second as f64 * 1_000_000_000.0
+ subsecond as f64,
)
}
/// ParseTemporalTimeString:时间串 → 当日纳秒(offset/注解被忽略、Z 拒绝)。
///
/// # 边界与前提
/// - Z/z designator → None;纯日期串 → None(不隐式午夜)。
/// - 日期+时间("1976-11-18T12:34...")→ 取 T 后时间部分;空格分隔需带日期部分。
/// - 无 T 的数字/连字符形式先做日期歧义判定:日期合法 → None(须 T 前缀);
/// 日期非法 → 按时间解析(HHMM-UU 形式,offset 被忽略)。
/// - 小数秒 ≤9 位;offset 小数秒 ≤9 位;闰秒按前一秒。
fn parse_plain_time_string(input: &str) -> Option<f64> {
let text = input.trim();
if text.contains('\u{2212}') {
return None;
}
let body = instant_string_without_annotations(text)?;
if body.contains(['Z', 'z']) {
return None;
}
let sep = body.find(['T', 't', ' ']);
let (date_part, time_part, has_t) = match sep {
Some(0) => ("", &body[1..], matches!(body.as_bytes().first(), Some(b'T' | b't'))),
Some(index) => (&body[..index], &body[index + 1..], matches!(body.as_bytes()[index], b'T' | b't')),
None => ("", body, false),
};
if !has_t && sep == Some(0) {
// 前导空格不能替代 T 前缀(无日期部分的时间串)。
return None;
}
if !date_part.is_empty() {
// 完整日期部分必须可解析(负零年等由 parse_iso_date 拒绝)。
if parse_iso_date(date_part).is_err() {
return None;
}
return parse_plain_time_spec(time_part);
}
if !has_t && !time_part.contains(':') && is_ambiguous_date_string(time_part) {
return None;
}
parse_plain_time_spec(time_part)
}
/// 时间主体 + 尾部 offset(剥离并忽略):offset 语法非法返回 None。
fn parse_plain_time_spec(s: &str) -> Option<f64> {
let offset_start = s
.char_indices()
.rev()
.find_map(|(index, ch)| matches!(ch, '+' | '-').then_some(index));
let (clock, offset) = match offset_start {
Some(index) => (&s[..index], &s[index..]),
None => (s, ""),
};
if !offset.is_empty() && (parse_any_offset_minutes(offset).is_none() || !valid_offset_fraction(offset)) {
return None;
}
parse_plain_clock(clock)
}
/// ToTemporalTime:PlainTime 对象 / ZDT / PlainDateTime / 字符串 / property bag → 当日纳秒。
///
/// # 步骤
/// 1. undefined → 0(午夜);字符串走 parse_plain_time_string(失败 RangeError)。
/// 2. PlainTime 读槽 0;ZDT 用其自身时区取本地时间;PlainDateTime 读时间槽。
/// 3. 其余对象按 ToTemporalTimeRecord 读时间字段(无字段 TypeError)。
///
/// # 边界与前提
/// - 非字符串原始值(number/bigint/null/boolean)→ TypeError;Symbol → TypeError。
/// - bag 字段缺省 0(完整模式),越界值按 constrain 钳制(second=60 → 59)。
fn plain_time_like_ns<H: VmHost>(vm: &mut H, value: JsValue) -> Result<f64, JsValue> {
if value.is_undefined() {
return Ok(0.0);
}
if value.is_string() {
return parse_plain_time_string(&to_string(value))
.ok_or_else(|| crate::error::create_range_error(vm, "invalid ISO 8601 time"));
}
if value.is_object() {
let ptr = value.as_js_object_ptr();
if !ptr.is_null() {
let obj = unsafe { &*ptr };
if obj.is_plain_time_obj() {
return Ok(get_double_prop(obj, 0));
}
if obj.is_zoned_date_time_obj() {
return zoned_date_time_plain_parts(vm, obj).map(|(_, _, _, time_ns)| time_ns);
}
if obj.is_plain_date_time_obj() {
return Ok(get_double_prop(obj, 3));
}
return plain_time_bag_ns(vm, value, obj);
}
}
Err(crate::error::create_type_error(vm, "cannot convert to PlainTime"))
}
/// ToTemporalTimeRecord(完整模式):按字母序读时间字段,缺省 0,无字段 TypeError。
fn plain_time_bag_ns<H: VmHost>(vm: &mut H, value: JsValue, obj: &JsObject) -> Result<f64, JsValue> {
let hour_raw = temporal_option_value(vm, obj, value, "hour")?;
let microsecond_raw = temporal_option_value(vm, obj, value, "microsecond")?;
let millisecond_raw = temporal_option_value(vm, obj, value, "millisecond")?;
let minute_raw = temporal_option_value(vm, obj, value, "minute")?;
let nanosecond_raw = temporal_option_value(vm, obj, value, "nanosecond")?;
let second_raw = temporal_option_value(vm, obj, value, "second")?;
if [hour_raw, microsecond_raw, millisecond_raw, minute_raw, nanosecond_raw, second_raw]
.iter()
.all(|raw| raw.is_undefined())
{
return Err(crate::error::create_type_error(vm, "no time units present"));
}
let mut convert = |raw: JsValue| -> Result<f64, JsValue> {
if raw.is_undefined() {
Ok(0.0)
} else {
let number = temporal_option_number(vm, raw)?;
if !number.is_finite() {
return Err(crate::error::create_range_error(vm, "invalid time component"));
}
Ok(number.trunc())
}
};
let hour = convert(hour_raw)?;
let microsecond = convert(microsecond_raw)?;
let millisecond = convert(millisecond_raw)?;
let minute = convert(minute_raw)?;
let nanosecond = convert(nanosecond_raw)?;
let second = convert(second_raw)?;
// RegulateTime(constrain):越界钳制(second=60 → 59)。
let hour = hour.clamp(0.0, 23.0);
let minute = minute.clamp(0.0, 59.0);
let second = second.clamp(0.0, 59.0);
let millisecond = millisecond.clamp(0.0, 999.0);
let microsecond = microsecond.clamp(0.0, 999.0);
let nanosecond = nanosecond.clamp(0.0, 999.0);
Ok(hour * 3_600_000_000_000.0
+ minute * 60_000_000_000.0
+ second * 1_000_000_000.0
+ millisecond * 1_000_000.0
+ microsecond * 1_000.0
+ nanosecond)
}
/// `Temporal.ZonedDateTime.prototype.withPlainTime(plainTimeLike)`。
///
/// # 步骤
/// 1. branding + receiver 本地分量与偏移。
/// 2. plain_time_like_ns 取当日纳秒(undefined → 午夜)。
/// 3. local_to_epoch_ns 换算 + Instant 范围校验 → make_zoned_date_time(保时区/日历槽)。
///
/// # 边界与前提
/// - 本地分量越 PlainDateTime 范围 / epoch 越 Instant 界 → RangeError。
/// - ZDT 参数用其自身时区取本地时间(不用 receiver 时区)。
pub fn zoned_date_time_with_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 (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 plain_time_like = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let time_ns = native_try!(plain_time_like_ns(vm, plain_time_like));
let epoch_ns = local_to_epoch_ns(year, month, day, time_ns, offset_minutes)
.ok_or_else(|| crate::error::create_range_error(vm, "invalid date-time"));
let epoch_ns = native_try!(epoch_ns);
if epoch_ns.unsigned_abs() > MAX_INSTANT_NS as u128 {
return NativeResult::Err(crate::error::create_range_error(vm, "ZonedDateTime outside supported range"));
}
let time_zone_id = to_string(obj.get_prop_at(1));
let calendar_id = get_calendar_id(obj, 2);
make_zoned_date_time(vm, epoch_ns, &time_zone_id, &calendar_id)
}
/// until/since 的 other 转换(ToTemporalZonedDateTime 语义,仅取 epoch 参与差值)。 /// until/since 的 other 转换(ToTemporalZonedDateTime 语义,仅取 epoch 参与差值)。
/// ///
/// # 步骤 /// # 步骤
@@ -3979,6 +4631,10 @@ fn parse_temporal_calendar_string(input: &str) -> Result<String, String> {
if parse_plain_date_time_string(trimmed).is_ok() { if parse_plain_date_time_string(trimmed).is_ok() {
return Ok("iso8601".to_string()); return Ok("iso8601".to_string());
} }
// 纯时间字符串(TemporalTimeString),例如 "15:23" / "T0030" / "152330.1-08"。
if parse_plain_time_string(trimmed).is_some() {
return Ok("iso8601".to_string());
}
// 部分日期:YYYY-MM 或 MM-DD(可带注解) // 部分日期:YYYY-MM 或 MM-DD(可带注解)
let text = trimmed.to_owned(); let text = trimmed.to_owned();
let annotation_start = text.find('[').unwrap_or(text.len()); let annotation_start = text.find('[').unwrap_or(text.len());
@@ -6088,4 +6744,55 @@ mod tests {
assert_eq!(extract_time_zone_annotation("1976-11-18T15:23:30[+24:00]"), None); assert_eq!(extract_time_zone_annotation("1976-11-18T15:23:30[+24:00]"), None);
assert_eq!(extract_time_zone_annotation("1976-11-18T15:23:30[]"), None); assert_eq!(extract_time_zone_annotation("1976-11-18T15:23:30[]"), None);
} }
#[test]
fn parse_plain_time_string_cases() {
// 明确时间:T 前缀 / 冒号 / 非法日期数字串 / 带 offset 与注解。
assert_eq!(parse_plain_time_string("T00:30"), Some(1_800_000_000_000.0));
assert_eq!(parse_plain_time_string("T0030"), Some(1_800_000_000_000.0));
assert_eq!(parse_plain_time_string("12:34:56.987654321+00:00"), Some(45_296_987_654_321.0));
assert_eq!(parse_plain_time_string("12:34:56.987654321+00:00[UTC]"), Some(45_296_987_654_321.0));
assert_eq!(parse_plain_time_string("1976-11-18T12:34:56.987654321+00:00"), Some(45_296_987_654_321.0));
assert_eq!(parse_plain_time_string("1976-11-18 12:34:56.987654321"), Some(45_296_987_654_321.0));
assert_eq!(parse_plain_time_string("1314"), Some(47_640_000_000_000.0)); // 13:14
assert_eq!(parse_plain_time_string("0631"), Some(23_460_000_000_000.0)); // 06:31
assert_eq!(parse_plain_time_string("2021-13"), Some(73_260_000_000_000.0)); // 20:21 + offset -13 忽略
// 歧义日期 → None(须 T 前缀)。
assert_eq!(parse_plain_time_string("2019-10-01"), None);
assert_eq!(parse_plain_time_string("1214"), None); // MMDD 合法
assert_eq!(parse_plain_time_string("0229"), None); // 闰年 2 月 29 判歧义
assert_eq!(parse_plain_time_string("1130"), None);
assert_eq!(parse_plain_time_string("12-14"), None); // MM-DD 合法
assert_eq!(parse_plain_time_string("202112"), None); // YYYYMM 合法
assert_eq!(parse_plain_time_string("2021-12"), None); // YYYY-MM 合法
assert_eq!(parse_plain_time_string("2021-12[-12:00]"), None);
assert_eq!(parse_plain_time_string("202112[UTC]"), None);
assert_eq!(parse_plain_time_string("1214[u-ca=iso8601]"), None);
// T 前缀后歧义消除;空格不能替代 T。
assert!(parse_plain_time_string("T2021-12").is_some());
assert_eq!(parse_plain_time_string(" 2021-12"), None);
// Z designator / 越界 / 多小数 / 负零年。
assert_eq!(parse_plain_time_string("09:00:00Z"), None);
assert_eq!(parse_plain_time_string("2019-10-01T09:00:00Z"), None);
assert_eq!(parse_plain_time_string("24:00"), None);
assert_eq!(parse_plain_time_string("12:34:56.1234567890"), None);
assert_eq!(parse_plain_time_string("-000000-12-07T03:24:30"), None);
// 闰秒按前一秒。
assert_eq!(parse_plain_time_string("2016-12-31T23:59:60"), Some(86_399_000_000_000.0));
}
#[test]
fn parse_plain_time_string_offsets_and_fractions() {
// offset 小数秒 ≤9 位合法,>9 位拒绝;逗号小数接受。
assert_eq!(
parse_plain_time_string("12:34:56.987654321+00:00:00.000000000"),
Some(45_296_987_654_321.0)
);
assert_eq!(parse_plain_time_string("12:34:56.987654321+00:00:00,0"), Some(45_296_987_654_321.0));
assert_eq!(parse_plain_time_string("00:00:00.1234567891"), None);
assert_eq!(parse_plain_time_string("00+00:00:00.1234567891"), None);
// 日期+offset 但无时间部分 → 拒绝。
assert_eq!(parse_plain_time_string("2022-09-15Z"), None);
assert_eq!(parse_plain_time_string("2022-09-15+00:00"), None);
}
} }
@@ -414,6 +414,17 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
), ),
("until", oxide_builtins::temporal::zoned_date_time_until::<crate::vm::Vm> as *const (), 1), ("until", oxide_builtins::temporal::zoned_date_time_until::<crate::vm::Vm> as *const (), 1),
("since", oxide_builtins::temporal::zoned_date_time_since::<crate::vm::Vm> as *const (), 1), ("since", oxide_builtins::temporal::zoned_date_time_since::<crate::vm::Vm> as *const (), 1),
("with", oxide_builtins::temporal::zoned_date_time_with::<crate::vm::Vm> as *const (), 1),
(
"withCalendar",
oxide_builtins::temporal::zoned_date_time_with_calendar::<crate::vm::Vm> as *const (),
1,
),
(
"withPlainTime",
oxide_builtins::temporal::zoned_date_time_with_plain_time::<crate::vm::Vm> as *const (),
1,
),
], ],
); );
@@ -1645,3 +1645,277 @@ fn zoned_date_time_until_bag_default_time_zone() {
.unwrap(); .unwrap();
assert_eq!(str_val(&vm, r), "PT1H|true"); assert_eq!(str_val(&vm, r), "PT1H|true");
} }
// -- ZonedDateTime.prototype.with / withCalendar / withPlainTime --
#[test]
fn zoned_date_time_with_partial_merge() {
let mut vm = Vm::new();
// 单字段覆盖与多字段合并:未给字段沿用 receiver 本地分量。
let r = eval(
&mut vm,
"const z = new Temporal.ZonedDateTime(0n, 'UTC');
z.with({ year: 2019 }).toString() + '|' +
z.with({ hour: 12, minute: 34, nanosecond: 5 }).toString() + '|' +
z.with({ month: 5, second: 15 }).toString()",
)
.unwrap();
assert_eq!(
str_val(&vm, r),
"2019-01-01T00:00:00+00:00[UTC]|1970-01-01T12:34:00.000000005+00:00[UTC]|1970-05-01T00:00:15+00:00[UTC]"
);
}
#[test]
fn zoned_date_time_with_undefined_fields_not_copied() {
let mut vm = Vm::new();
// year: undefined 不覆盖,其余字段覆盖(copy-properties-not-undefined 语义)。
let r = eval(
&mut vm,
"const d1 = new Temporal.ZonedDateTime(1_000_000_000_000_000_789n, 'UTC');
const d2 = d1.with({ day: 1, hour: 10, year: undefined });
d2.year === 2001 && d2.month === 9 && d2.day === 1 && d2.hour === 10 &&
d2.minute === 46 && d2.second === 40 && d2.nanosecond === 789",
)
.unwrap();
assert!(r.as_bool());
}
#[test]
fn zoned_date_time_with_rejects_calendar_time_zone_and_temporal_objects() {
let mut vm = Vm::new();
// calendar/timeZone 字段、Temporal 实例参数、字符串参数均 TypeError。
let r = eval(
&mut vm,
"const z = new Temporal.ZonedDateTime(0n, 'UTC');
(() => { try { z.with({ month: 2, calendar: 'iso8601' }); return 'no-throw'; }
catch (e) { return e instanceof TypeError; } })() + '|' +
(() => { try { z.with({ month: 2, timeZone: 'UTC' }); return 'no-throw'; }
catch (e) { return e instanceof TypeError; } })() + '|' +
(() => { try { z.with(new Temporal.PlainDate(1976, 11, 18)); return 'no-throw'; }
catch (e) { return e instanceof TypeError; } })() + '|' +
(() => { try { z.with('1976-11-18T12:00+00:00[UTC]'); return 'no-throw'; }
catch (e) { return e instanceof TypeError; } })() + '|' +
(() => { try { z.with({}); 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_with_offset_option() {
let mut vm = Vm::new();
// offset 默认 prefer;use 用 bag 偏移;reject 冲突 RangeError;非法值各类型错误。
let r = eval(
&mut vm,
"const z = new Temporal.ZonedDateTime(0n, 'UTC');
const dt = new Temporal.ZonedDateTime(1572757201_000_000_000n, '-03:30');
(dt.with({ minute: 31 }).epochNanoseconds === 1572757261_000_000_000n) + '|' +
(dt.with({ minute: 31 }, {}).epochNanoseconds === 1572757261_000_000_000n) + '|' +
(z.with({ offset: '+01:00' }, { offset: 'use' }).epochNanoseconds === -3_600_000_000_000n) + '|' +
(z.with({ offset: '+01:00' }, { offset: 'prefer' }).epochNanoseconds === 0n) + '|' +
(() => { try { z.with({ offset: '+01:00' }, { offset: 'reject' }); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })() + '|' +
(() => { try { z.with({ offset: 0 }); return 'no-throw'; }
catch (e) { return e instanceof TypeError; } })() + '|' +
(() => { try { z.with({ offset: '00:00' }); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })()",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true|true|true|true|true|true|true");
}
#[test]
fn zoned_date_time_with_overflow_constrain_and_reject() {
let mut vm = Vm::new();
// constrain 钳制月/日/时/亚秒;reject 越界 RangeError;monthCode 冲突/闰月 RangeError。
let r = eval(
&mut vm,
"const z = new Temporal.ZonedDateTime(0n, 'UTC');
z.with({ month: 29 }).toString() + '|' +
z.with({ hour: 29 }).toString() + '|' +
z.with({ nanosecond: 9000 }).toString() + '|' +
(() => { try { z.with({ month: 29 }, { overflow: 'reject' }); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })() + '|' +
(() => { try { z.with({ month: 5, monthCode: 'M06' }); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })() + '|' +
(() => { try { z.with({ monthCode: 'M08L' }); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })() + '|' +
(z.with({ monthCode: 'M05' }).toString() === '1970-05-01T00:00:00+00:00[UTC]')",
)
.unwrap();
assert_eq!(
str_val(&vm, r),
"1970-12-01T00:00:00+00:00[UTC]|1970-01-01T23:00:00+00:00[UTC]|1970-01-01T00:00:00.000000999+00:00[UTC]|true|true|true|true"
);
}
#[test]
fn zoned_date_time_with_month_code_constrain_day() {
let mut vm = Vm::new();
// monthCode 换月后日钳制:1 月 31 日 → 2 月 28 日(constrain);reject → RangeError。
let r = eval(
&mut vm,
"const z = Temporal.ZonedDateTime.from({ year: 2019, monthCode: 'M01', day: 31, hour: 12, minute: 34, timeZone: 'UTC' });
z.with({ monthCode: 'M02' }).toString() + '|' +
(() => { try { z.with({ monthCode: 'M02' }, { overflow: 'reject' }); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })()",
)
.unwrap();
assert_eq!(str_val(&vm, r), "2019-02-28T12:34:00+00:00[UTC]|true");
}
#[test]
fn zoned_date_time_with_range_errors_and_options_order() {
let mut vm = Vm::new();
// 越界墙钟 / offset use 越界 → RangeError;字段先于 options 校验。
let r = eval(
&mut vm,
"( () => { try { new Temporal.ZonedDateTime(0n, 'UTC').with({ year: -271821, month: 4, day: 19, hour: 1 }); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })() + '|' +
( () => { try { new Temporal.ZonedDateTime(-864n * 10n**19n, 'UTC').with({ offset: '+01' }, { offset: 'use' }); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })() + '|' +
( () => { try { new Temporal.ZonedDateTime(0n, 'UTC').with({ day: 5 }, null); return 'no-throw'; }
catch (e) { return e instanceof TypeError; } })() + '|' +
( () => { try { new Temporal.ZonedDateTime(0n, 'UTC').with({ day: -1 }, null); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })()",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true|true|true|true");
}
#[test]
fn zoned_date_time_with_calendar_swaps_calendar_slot() {
let mut vm = Vm::new();
// 换日历槽:epoch/时区不变、返回新对象;大小写不敏感;ISO 串与时间串接受。
let r = eval(
&mut vm,
"const c = new Temporal.ZonedDateTime(0n, 'UTC', 'hebrew');
const w = c.withCalendar('iso8601');
(w !== c) + '|' + w.calendarId + '|' + w.epochNanoseconds + '|' + w.timeZoneId + '|' +
c.withCalendar('iSo8601').calendarId + '|' +
c.withCalendar('2020-01-01').calendarId + '|' +
c.withCalendar('15:23').calendarId + '|' +
c.withCalendar(new Temporal.ZonedDateTime(0n, 'UTC', 'japanese')).calendarId",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true|iso8601|0|UTC|iso8601|iso8601|iso8601|japanese");
}
#[test]
fn zoned_date_time_with_calendar_errors() {
let mut vm = Vm::new();
// 缺参/undefined/非字符串非对象 → TypeError;非法串 → RangeError。
let r = eval(
&mut vm,
"const c = new Temporal.ZonedDateTime(0n, 'UTC');
(() => { try { c.withCalendar(); return 'no-throw'; }
catch (e) { return e instanceof TypeError; } })() + '|' +
(() => { try { c.withCalendar(undefined); return 'no-throw'; }
catch (e) { return e instanceof TypeError; } })() + '|' +
(() => { try { c.withCalendar(42); return 'no-throw'; }
catch (e) { return e instanceof TypeError; } })() + '|' +
(() => { try { c.withCalendar('notacal'); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })()",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true|true|true|true");
}
#[test]
fn zoned_date_time_with_plain_time_defaults_and_bag() {
let mut vm = Vm::new();
// undefined → 午夜;bag 缺省字段为 0;second=60 按 59 钳制。
let r = eval(
&mut vm,
"const p = new Temporal.ZonedDateTime(957270896_987_654_321n, 'UTC');
p.withPlainTime().toString() + '|' +
p.withPlainTime(undefined).toString() + '|' +
p.withPlainTime({ minute: 30 }).toString() + '|' +
p.withPlainTime({ hour: 23, minute: 59, second: 60 }).toString() + '|' +
p.withPlainTime(new Temporal.PlainTime(11, 22)).hour",
)
.unwrap();
assert_eq!(
str_val(&vm, r),
"2000-05-02T00:00:00+00:00[UTC]|2000-05-02T00:00:00+00:00[UTC]|2000-05-02T00:30:00+00:00[UTC]|2000-05-02T23:59:59+00:00[UTC]|11"
);
}
#[test]
fn zoned_date_time_with_plain_time_strings() {
let mut vm = Vm::new();
// 时间串 / 日期+时间 / 歧义串须 T 前缀 / Z 与纯日期拒绝 / offset 忽略。
let r = eval(
&mut vm,
"const p = new Temporal.ZonedDateTime(957270896_987_654_321n, 'UTC');
p.withPlainTime('12:34').toString() + '|' +
p.withPlainTime('1976-11-18T15:23:30.123456789+00:00').toString() + '|' +
p.withPlainTime('T2021-12').hour + '|' +
(() => { try { p.withPlainTime('2021-12'); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })() + '|' +
(() => { try { p.withPlainTime('09:00:00Z'); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })() + '|' +
(() => { try { p.withPlainTime('2019-10-01'); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })() + '|' +
(() => { try { p.withPlainTime({}); return 'no-throw'; }
catch (e) { return e instanceof TypeError; } })()",
)
.unwrap();
assert_eq!(
str_val(&vm, r),
"2000-05-02T12:34:00+00:00[UTC]|2000-05-02T15:23:30.123456789+00:00[UTC]|20|true|true|true|true"
);
}
#[test]
fn zoned_date_time_with_plain_time_zoned_date_time_argument() {
let mut vm = Vm::new();
// ZDT 参数用其自身时区取本地时间(负偏移平衡负时间单位);负 epoch 模运算正确。
let r = eval(
&mut vm,
"const dtz = new Temporal.ZonedDateTime(3661_001_001_001n, '-00:02');
const r1 = new Temporal.ZonedDateTime(86400_000_000_000n, 'UTC').withPlainTime(dtz);
(r1.hour === 0 && r1.minute === 59) + '|' +
(new Temporal.ZonedDateTime(0n, 'UTC')
.withPlainTime(new Temporal.ZonedDateTime(-13849764_999_999_999n, 'UTC'))
.epochNanoseconds === 60635_000_000_001n)",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true|true");
}
#[test]
fn zoned_date_time_with_plain_time_out_of_range() {
let mut vm = Vm::new();
// 本地分量越界(±MAX 边界)→ RangeError(start-of-day 与 epoch 越界两条路径)。
let r = eval(
&mut vm,
"( () => { try { new Temporal.ZonedDateTime(-864n * 10n**19n, '-01').withPlainTime(); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })() + '|' +
( () => { try { new Temporal.ZonedDateTime(-864n * 10n**19n, '+01').withPlainTime('00:00'); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })() + '|' +
( () => { try { new Temporal.ZonedDateTime(864n * 10n**19n, 'UTC').withPlainTime('01:00'); return 'no-throw'; }
catch (e) { return e instanceof RangeError; } })()",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true|true|true");
}
#[test]
fn zoned_date_time_with_family_branding() {
let mut vm = Vm::new();
// 非 ZDT receiver 调三个方法均 TypeError。
let r = eval(
&mut vm,
"( () => { try { Temporal.ZonedDateTime.prototype.with.call({}, { year: 2019 }); return 'no-throw'; }
catch (e) { return e instanceof TypeError; } })() + '|' +
( () => { try { Temporal.ZonedDateTime.prototype.withCalendar.call({}, 'iso8601'); return 'no-throw'; }
catch (e) { return e instanceof TypeError; } })() + '|' +
( () => { try { Temporal.ZonedDateTime.prototype.withPlainTime.call({}); return 'no-throw'; }
catch (e) { return e instanceof TypeError; } })()",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true|true|true");
}