diff --git a/crates/oxide_builtins/src/temporal/duration.rs b/crates/oxide_builtins/src/temporal/duration.rs index 9c1581ef..8eb79325 100644 --- a/crates/oxide_builtins/src/temporal/duration.rs +++ b/crates/oxide_builtins/src/temporal/duration.rs @@ -306,7 +306,9 @@ fn duration_relative_to_date( if let Ok((year, month, day, _time_ns)) = parse_plain_date_time_string(&text) { return Ok(Some((i128::from(year), i128::from(month), i128::from(day)))); } - // ZonedDateTime-like(含 Z 或时区注解):按 instant 解析 + 注解时区反推墙钟日期。 + // 回退支:plain 首支解析失败的串(含 Z 的串或非法注解)。命名区当前全部 RangeError + // (canonical_time_zone 拒 IANA),可解析的带注解串已被 plain 首支接受。 + // 按 instant 解析 + 注解时区反推墙钟日期。 let (epoch_ns, time_zone_id, _calendar) = zoned_date_time_string_parts(vm, &text, "reject")?; let offset_minutes = instant_time_zone_offset(&time_zone_id).unwrap_or(0); let wall_ns = epoch_ns + i128::from(offset_minutes) * 60_000_000_000; @@ -651,7 +653,7 @@ pub fn duration_add(vm: &mut H, args: &[u8]) -> NativeResult { /// 4. 平衡后存储时间分量(含 days,𝔽 舍入)的纳秒加权和校验 2^53 秒上限。 /// /// # 边界与前提 -/// - 任一侧含日历单位抛 RangeError(本批无 relativeTo 支持)。 +/// - add/subtract 无 relativeTo 选项,任一侧含 years/months/weeks 即抛 RangeError。 /// - 全零与 nanosecond 分量和为 0 时直接返回零时长。 fn add_duration_values(vm: &mut H, receiver: &[f64; 10], other: &[f64; 10]) -> Result<[f64; 10], JsValue> { if receiver[..3].iter().any(|value| *value != 0.0) || other[..3].iter().any(|value| *value != 0.0) { @@ -1088,24 +1090,15 @@ pub fn duration_round(vm: &mut H, args: &[u8]) -> NativeResult { unit -= 1; } } - // 范围校验:对每个时间分量按纳秒刻度检查是否达到 2^53 秒上限。 - const MAX_TIME_NANOSECONDS: f64 = (1_i128 << 53) as f64 * 1_000_000_000.0; - const UNIT_SCALES: [f64; 7] = [ - 86_400_000_000_000.0, - 3_600_000_000_000.0, - 60_000_000_000.0, - 1_000_000_000.0, - 1_000_000.0, - 1_000.0, - 1.0, - ]; - for (index, scale) in (3..10).zip(UNIT_SCALES) { - if result[index] != 0.0 && result[index].abs() * scale >= MAX_TIME_NANOSECONDS { - return NativeResult::Err(crate::error::create_range_error( - vm, - "duration time fields are out of range", - )); - } + // 范围校验:判据作用在存储分量(平衡后 𝔽 舍入结果)的纳秒加权和上,与 + // add/subtract 同式;加权和覆盖逐分量检查的盲区(总和落在 [2^53 s, 2^53 s + + // 86400 s) 而各分量均低于上限的缝隙带)。 + const MAX_TIME_NANOSECONDS: i128 = (1_i128 << 53) * 1_000_000_000; + let Some(stored_ns) = duration_time_nanoseconds(&result) else { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid duration")); + }; + if stored_ns.abs() >= MAX_TIME_NANOSECONDS { + return NativeResult::Err(crate::error::create_range_error(vm, "duration time fields are out of range")); } return make_duration(vm, result); } diff --git a/crates/oxide_vm/tests/builtins_temporal_tests.rs b/crates/oxide_vm/tests/builtins_temporal_tests.rs index 348498f5..1294bf0a 100644 --- a/crates/oxide_vm/tests/builtins_temporal_tests.rs +++ b/crates/oxide_vm/tests/builtins_temporal_tests.rs @@ -2629,6 +2629,33 @@ fn duration_total_relative_zdt() { assert_eq!(r.as_double(), 1.0); } +#[test] +fn duration_round_weighted_sum_range_boundary() { + let mut vm = Vm::new(); + // round 纯时间路径范围判据按存储分量加权和(spec IsValidDuration),非逐分量: + // 输入 2^53 s − 30 s 合法,expand 舍到 minute 后 2^53 s + 28 s 越界 → RangeError + // (平衡后各分量均低于上限);负镜像同抛;2^53 s − 1 ns 输入舍到 day 得 + // 104249991374 天(2^53 s − 27392 s)→ 接受。 + let r = eval( + &mut vm, + "(() => { + const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } }; + return [ + kind(() => new Temporal.Duration(0, 0, 0, 104249991374, 7, 36, 2) + .round({ smallestUnit: 'minute', roundingMode: 'expand' })), + kind(() => new Temporal.Duration(0, 0, 0, -104249991374, -7, -36, -2) + .round({ smallestUnit: 'minute', roundingMode: 'expand' })), + kind(() => new Temporal.Duration(0, 0, 0, 104249991374, 7, 36, 31, 999, 999, 999) + .round({ smallestUnit: 'day' })), + new Temporal.Duration(0, 0, 0, 104249991374, 7, 36, 31, 999, 999, 999) + .round({ smallestUnit: 'day' }).toString(), + ].join('|'); + })()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "RangeError|RangeError|no|P104249991374D"); +} + // -- Temporal.PlainMonthDay / PlainYearMonth(E 批)-- #[test]