mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
feat: ZonedDateTime.with/withPlainTime/withCalendar 字段合并(offset 默认 prefer + 时间串歧义解析)
This commit is contained in:
@@ -1432,15 +1432,21 @@ fn is_time_zone_annotation_value(annotation: &str) -> bool {
|
||||
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。
|
||||
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() };
|
||||
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() {
|
||||
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 };
|
||||
|
||||
// 先 Get 再校验:disambiguation 在前,offset 在后。
|
||||
// 逐个选项处理:Get → ToString → 白名单,保证用户代码可观察的读序为
|
||||
// disambiguation 完整处理完后再处理 offset。
|
||||
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() {
|
||||
"compatible".to_string()
|
||||
} else {
|
||||
temporal_option_string(vm, disambiguation_raw)?
|
||||
let value = temporal_option_string(vm, disambiguation_raw)?;
|
||||
if !matches!(value.as_str(), "compatible" | "earlier" | "later" | "reject") {
|
||||
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() {
|
||||
"reject".to_string()
|
||||
default_offset.to_string()
|
||||
} else {
|
||||
temporal_option_string(vm, offset_raw)?
|
||||
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
|
||||
};
|
||||
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"));
|
||||
}
|
||||
Ok((offset, disambiguation))
|
||||
}
|
||||
|
||||
@@ -1719,7 +1728,7 @@ fn zoned_date_time_bag_parts<H: VmHost>(
|
||||
/// - disambiguation 本批仅做选项值校验,固定偏移时区下四个取值算法等价。
|
||||
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 (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) =
|
||||
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)
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
/// 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 参与差值)。
|
||||
///
|
||||
/// # 步骤
|
||||
@@ -3979,6 +4631,10 @@ fn parse_temporal_calendar_string(input: &str) -> Result<String, String> {
|
||||
if parse_plain_date_time_string(trimmed).is_ok() {
|
||||
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(可带注解)
|
||||
let text = trimmed.to_owned();
|
||||
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[]"), 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),
|
||||
("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();
|
||||
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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user