refactor: temporal 偏移表示由 i32 分钟升级为 i64 秒

This commit is contained in:
2026-09-27 23:34:25 +00:00
parent 51a79ed17c
commit f824afd041
6 changed files with 146 additions and 122 deletions
+18 -18
View File
@@ -641,7 +641,7 @@ pub(crate) fn parse_fractional_second_digits<H: VmHost>(
} }
} }
pub(crate) fn parse_offset_minutes(value: &str) -> Option<i32> { pub(crate) fn parse_offset_seconds(value: &str) -> Option<i64> {
let bytes = value.as_bytes(); let bytes = value.as_bytes();
if bytes.len() != 6 || !matches!(bytes[0], b'+' | b'-') || bytes[3] != b':' { if bytes.len() != 6 || !matches!(bytes[0], b'+' | b'-') || bytes[3] != b':' {
return None; return None;
@@ -649,21 +649,21 @@ pub(crate) fn parse_offset_minutes(value: &str) -> Option<i32> {
if !bytes[1..3].iter().all(u8::is_ascii_digit) || !bytes[4..6].iter().all(u8::is_ascii_digit) { if !bytes[1..3].iter().all(u8::is_ascii_digit) || !bytes[4..6].iter().all(u8::is_ascii_digit) {
return None; return None;
} }
let hours = i32::from(bytes[1] - b'0') * 10 + i32::from(bytes[2] - b'0'); let hours = i64::from(bytes[1] - b'0') * 10 + i64::from(bytes[2] - b'0');
let minutes = i32::from(bytes[4] - b'0') * 10 + i32::from(bytes[5] - b'0'); let minutes = i64::from(bytes[4] - b'0') * 10 + i64::from(bytes[5] - b'0');
if hours > 23 || minutes > 59 { if hours > 23 || minutes > 59 {
return None; return None;
} }
let magnitude = hours * 60 + minutes; let magnitude = hours * 3600 + minutes * 60;
Some(if bytes[0] == b'-' { -magnitude } else { magnitude }) Some(if bytes[0] == b'-' { -magnitude } else { magnitude })
} }
pub(crate) fn canonical_time_zone(value: &str) -> Option<(String, i32)> { pub(crate) fn canonical_time_zone(value: &str) -> Option<(String, i64)> {
let input = value.trim(); let input = value.trim();
if input.eq_ignore_ascii_case("UTC") || input.eq_ignore_ascii_case("Z") { if input.eq_ignore_ascii_case("UTC") || input.eq_ignore_ascii_case("Z") {
return Some(("UTC".to_string(), 0)); return Some(("UTC".to_string(), 0));
} }
if let Some(offset) = parse_offset_minutes(input) { if let Some(offset) = parse_offset_seconds(input) {
return Some((input.to_string(), offset)); return Some((input.to_string(), offset));
} }
@@ -672,10 +672,10 @@ pub(crate) fn canonical_time_zone(value: &str) -> Option<(String, i32)> {
&& matches!(input.as_bytes()[0], b'+' | b'-') && matches!(input.as_bytes()[0], b'+' | b'-')
&& input.as_bytes()[1..3].iter().all(u8::is_ascii_digit) && input.as_bytes()[1..3].iter().all(u8::is_ascii_digit)
{ {
let hour = (input.as_bytes()[1] - b'0') as i32 * 10 + (input.as_bytes()[2] - b'0') as i32; let hour = (input.as_bytes()[1] - b'0') as i64 * 10 + (input.as_bytes()[2] - b'0') as i64;
if hour <= 23 { if hour <= 23 {
let sign = if input.as_bytes()[0] == b'-' { -1 } else { 1 }; let sign = if input.as_bytes()[0] == b'-' { -1 } else { 1 };
return Some((input.to_string(), sign * hour * 60)); return Some((input.to_string(), sign * hour * 3600));
} }
return None; return None;
} }
@@ -685,11 +685,11 @@ pub(crate) fn canonical_time_zone(value: &str) -> Option<(String, i32)> {
&& matches!(input.as_bytes()[0], b'+' | b'-') && matches!(input.as_bytes()[0], b'+' | b'-')
&& input.as_bytes()[1..5].iter().all(u8::is_ascii_digit) && input.as_bytes()[1..5].iter().all(u8::is_ascii_digit)
{ {
let hour = (input.as_bytes()[1] - b'0') as i32 * 10 + (input.as_bytes()[2] - b'0') as i32; let hour = (input.as_bytes()[1] - b'0') as i64 * 10 + (input.as_bytes()[2] - b'0') as i64;
let minute = (input.as_bytes()[3] - b'0') as i32 * 10 + (input.as_bytes()[4] - b'0') as i32; let minute = (input.as_bytes()[3] - b'0') as i64 * 10 + (input.as_bytes()[4] - b'0') as i64;
if hour <= 23 && minute <= 59 { if hour <= 23 && minute <= 59 {
let sign = if input.as_bytes()[0] == b'-' { -1 } else { 1 }; let sign = if input.as_bytes()[0] == b'-' { -1 } else { 1 };
return Some((input.to_string(), sign * (hour * 60 + minute))); return Some((input.to_string(), sign * (hour * 3600 + minute * 60)));
} }
return None; return None;
} }
@@ -706,7 +706,7 @@ pub(crate) fn canonical_time_zone(value: &str) -> Option<(String, i32)> {
if annotation.eq_ignore_ascii_case("UTC") { if annotation.eq_ignore_ascii_case("UTC") {
return Some(("UTC".to_string(), 0)); return Some(("UTC".to_string(), 0));
} }
return parse_offset_minutes(annotation).map(|offset| (annotation.to_string(), offset)); return parse_offset_seconds(annotation).map(|offset| (annotation.to_string(), offset));
} }
let time_start = input.find(['T', 't', ' '])?; let time_start = input.find(['T', 't', ' '])?;
@@ -719,7 +719,7 @@ pub(crate) fn canonical_time_zone(value: &str) -> Option<(String, i32)> {
.rev() .rev()
.find_map(|(index, ch)| matches!(ch, '+' | '-').then_some(index))?; .find_map(|(index, ch)| matches!(ch, '+' | '-').then_some(index))?;
let offset_id = &time[offset_start..]; let offset_id = &time[offset_start..];
parse_offset_minutes(offset_id).map(|offset| (offset_id.to_string(), offset)) parse_offset_seconds(offset_id).map(|offset| (offset_id.to_string(), offset))
} }
pub(crate) fn civil_from_days(days: i128) -> (i128, i128, i128) { pub(crate) fn civil_from_days(days: i128) -> (i128, i128, i128) {
@@ -1162,11 +1162,11 @@ pub(crate) fn zoned_date_time_plain_parts<H: VmHost>(
return Err(crate::error::create_range_error(vm, "invalid ZonedDateTime")); return Err(crate::error::create_range_error(vm, "invalid ZonedDateTime"));
}; };
let time_zone_id = to_string(obj.get_prop_at(1)); let time_zone_id = to_string(obj.get_prop_at(1));
let Some(offset_minutes) = instant_time_zone_offset(&time_zone_id) else { let Some(offset_seconds) = instant_time_zone_offset(&time_zone_id) else {
return Err(crate::error::create_range_error(vm, "invalid time zone")); return Err(crate::error::create_range_error(vm, "invalid time zone"));
}; };
const DAY_NS: i128 = 86_400_000_000_000; const DAY_NS: i128 = 86_400_000_000_000;
let local_ns = epoch_ns + i128::from(offset_minutes) * 60_000_000_000; let local_ns = epoch_ns + i128::from(offset_seconds) * 1_000_000_000;
let days = local_ns.div_euclid(DAY_NS); let days = local_ns.div_euclid(DAY_NS);
let time_ns = local_ns.rem_euclid(DAY_NS) as f64; let time_ns = local_ns.rem_euclid(DAY_NS) as f64;
let (year, month, day) = civil_from_days(days); let (year, month, day) = civil_from_days(days);
@@ -1176,16 +1176,16 @@ pub(crate) fn zoned_date_time_plain_parts<H: VmHost>(
Ok((year as i32, month as u32, day as u32, time_ns)) Ok((year as i32, month as u32, day as u32, time_ns))
} }
/// 本地墙钟分量 + 时区偏移(分钟)→ 纪元纳秒(zoned_date_time_plain_parts 的逆)。 /// 本地墙钟分量 + 时区偏移(秒)→ 纪元纳秒(zoned_date_time_plain_parts 的逆)。
/// ///
/// # 边界与前提 /// # 边界与前提
/// - (year, month, day, time_ns) 须已通过 valid_iso_date / valid_plain_time 校验(调用方保证)。 /// - (year, month, day, time_ns) 须已通过 valid_iso_date / valid_plain_time 校验(调用方保证)。
/// - 仅做 checked 溢出防护,Instant 范围校验由调用方按需执行。 /// - 仅做 checked 溢出防护,Instant 范围校验由调用方按需执行。
pub(crate) fn local_to_epoch_ns(year: i32, month: u32, day: u32, time_ns: f64, offset_minutes: i32) -> Option<i128> { pub(crate) fn local_to_epoch_ns(year: i32, month: u32, day: u32, time_ns: f64, offset_seconds: i64) -> Option<i128> {
let days = days_from_civil(i128::from(year), i128::from(month), i128::from(day)); let days = days_from_civil(i128::from(year), i128::from(month), i128::from(day));
days.checked_mul(86_400_000_000_000)? days.checked_mul(86_400_000_000_000)?
.checked_add(time_ns as i128)? .checked_add(time_ns as i128)?
.checked_sub(i128::from(offset_minutes) * 60_000_000_000) .checked_sub(i128::from(offset_seconds) * 1_000_000_000)
} }
pub(crate) fn make_plain_date<H: VmHost>(vm: &mut H, year: i32, month: u32, day: u32, calendar: &str) -> NativeResult { pub(crate) fn make_plain_date<H: VmHost>(vm: &mut H, year: i32, month: u32, day: u32, calendar: &str) -> NativeResult {
@@ -16,7 +16,7 @@ use super::difference::{
nudge_window, plain_date_time_unit_index, round_instant_difference, DifferenceSettings, DAY_NS, MAX_ISO_DAY, nudge_window, plain_date_time_unit_index, round_instant_difference, DifferenceSettings, DAY_NS, MAX_ISO_DAY,
}; };
use super::instant::{instant_rounding_mode, instant_time_zone_offset, InstantRoundingMode}; use super::instant::{instant_rounding_mode, instant_time_zone_offset, InstantRoundingMode};
use super::zoned_date_time::{parse_any_offset_minutes, valid_offset_fraction, zoned_date_time_string_parts}; use super::zoned_date_time::{parse_any_offset_seconds, valid_offset_fraction, zoned_date_time_string_parts};
use super::{make_duration, parse_plain_date_time_string}; use super::{make_duration, parse_plain_date_time_string};
fn parse_duration_string(input: &str) -> Option<[f64; 10]> { fn parse_duration_string(input: &str) -> Option<[f64; 10]> {
@@ -310,8 +310,8 @@ fn duration_relative_to_date<H: VmHost>(
// (canonical_time_zone 拒 IANA),可解析的带注解串已被 plain 首支接受。 // (canonical_time_zone 拒 IANA),可解析的带注解串已被 plain 首支接受。
// 按 instant 解析 + 注解时区反推墙钟日期。 // 按 instant 解析 + 注解时区反推墙钟日期。
let (epoch_ns, time_zone_id, _calendar) = zoned_date_time_string_parts(vm, &text, "reject")?; 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 offset_seconds = instant_time_zone_offset(&time_zone_id).unwrap_or(0);
let wall_ns = epoch_ns + i128::from(offset_minutes) * 60_000_000_000; let wall_ns = epoch_ns + i128::from(offset_seconds) * 1_000_000_000;
let (year, month, day) = civil_from_days(wall_ns.div_euclid(DAY_NS)); let (year, month, day) = civil_from_days(wall_ns.div_euclid(DAY_NS));
return Ok(Some((year, month, day))); return Ok(Some((year, month, day)));
} }
@@ -363,7 +363,7 @@ fn duration_relative_to_date<H: VmHost>(
return Err(crate::error::create_type_error(vm, "invalid offset")); return Err(crate::error::create_type_error(vm, "invalid offset"));
} }
let offset_input = to_string(offset_raw); let offset_input = to_string(offset_raw);
if parse_any_offset_minutes(&offset_input).is_none() || !valid_offset_fraction(&offset_input) { if parse_any_offset_seconds(&offset_input).is_none() || !valid_offset_fraction(&offset_input) {
return Err(crate::error::create_range_error(vm, "invalid offset")); return Err(crate::error::create_range_error(vm, "invalid offset"));
} }
} }
@@ -727,15 +727,15 @@ pub fn instant_since<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
instant_difference(vm, args, true) instant_difference(vm, args, true)
} }
pub(crate) fn instant_time_zone_offset(value: &str) -> Option<i32> { pub(crate) fn instant_time_zone_offset(value: &str) -> Option<i64> {
canonical_time_zone(value).map(|(_, offset)| offset) canonical_time_zone(value).map(|(_, offset)| offset)
} }
fn format_instant_iso( fn format_instant_iso(
epoch_ns: i128, offset_minutes: Option<i32>, include_seconds: bool, fractional_digits: Option<usize>, epoch_ns: i128, offset_seconds: Option<i64>, include_seconds: bool, fractional_digits: Option<usize>,
) -> Option<String> { ) -> Option<String> {
const DAY_NS: i128 = 86_400_000_000_000; const DAY_NS: i128 = 86_400_000_000_000;
let offset_ns = i128::from(offset_minutes.unwrap_or(0)).checked_mul(60_000_000_000)?; let offset_ns = i128::from(offset_seconds.unwrap_or(0)).checked_mul(1_000_000_000)?;
let local_ns = epoch_ns.checked_add(offset_ns)?; let local_ns = epoch_ns.checked_add(offset_ns)?;
let days = local_ns.div_euclid(DAY_NS); let days = local_ns.div_euclid(DAY_NS);
let mut time_ns = local_ns.rem_euclid(DAY_NS); let mut time_ns = local_ns.rem_euclid(DAY_NS);
@@ -766,10 +766,10 @@ fn format_instant_iso(
} }
} }
if let Some(offset) = offset_minutes { if let Some(offset) = offset_seconds {
let sign = if offset < 0 { '-' } else { '+' }; let sign = if offset < 0 { '-' } else { '+' };
let magnitude = offset.abs(); let magnitude = offset.abs();
output.push_str(&format!("{sign}{:02}:{:02}", magnitude / 60, magnitude % 60)); output.push_str(&format!("{sign}{:02}:{:02}", magnitude / 3600, (magnitude % 3600) / 60));
} else { } else {
output.push('Z'); output.push('Z');
} }
@@ -850,7 +850,7 @@ pub fn instant_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
None => (1, true, None), None => (1, true, None),
}, },
}; };
let offset_minutes = if time_zone_raw.is_undefined() { let offset_seconds = if time_zone_raw.is_undefined() {
None None
} else { } else {
if !time_zone_raw.is_string() { if !time_zone_raw.is_string() {
@@ -868,7 +868,7 @@ pub fn instant_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if rounded_ns.unsigned_abs() > MAX_INSTANT_NS as u128 { if rounded_ns.unsigned_abs() > MAX_INSTANT_NS as u128 {
return NativeResult::Err(crate::error::create_range_error(vm, "Instant outside supported range")); return NativeResult::Err(crate::error::create_range_error(vm, "Instant outside supported range"));
} }
match format_instant_iso(rounded_ns, offset_minutes, include_seconds, output_digits) { match format_instant_iso(rounded_ns, offset_seconds, include_seconds, output_digits) {
Some(output) => NativeResult::Ok(vm.new_string_owned(output)), Some(output) => NativeResult::Ok(vm.new_string_owned(output)),
None => NativeResult::Err(crate::error::create_range_error(vm, "invalid Instant")), None => NativeResult::Err(crate::error::create_range_error(vm, "invalid Instant")),
} }
@@ -8,7 +8,7 @@ use super::common::native_try;
use super::{ use super::{
difference_core, duration_component_integer, duration_like_values, ensure_plain_time, get_double_prop, difference_core, duration_component_integer, duration_like_values, ensure_plain_time, get_double_prop,
initialize_temporal_receiver, instant_rounding_mode, instant_string_without_annotations, is_ctor_call, initialize_temporal_receiver, instant_rounding_mode, instant_string_without_annotations, is_ctor_call,
make_plain_time, parse_any_offset_minutes, parse_difference_settings, parse_fractional_second_digits, make_plain_time, parse_any_offset_seconds, parse_difference_settings, parse_fractional_second_digits,
parse_iso_date, receiver_obj, reject_partial_object_with_calendar_or_time_zone, round_instant_ns, parse_iso_date, receiver_obj, reject_partial_object_with_calendar_or_time_zone, round_instant_ns,
temporal_number_component, temporal_option_number, temporal_option_string, temporal_option_value, temporal_number_component, temporal_option_number, temporal_option_string, temporal_option_value,
temporal_overflow, valid_offset_fraction, zoned_date_time_plain_parts, FractionalSecondDigitsInput, temporal_overflow, valid_offset_fraction, zoned_date_time_plain_parts, FractionalSecondDigitsInput,
@@ -177,7 +177,7 @@ fn parse_plain_time_spec(s: &str) -> Option<f64> {
Some(index) => (&s[..index], &s[index..]), Some(index) => (&s[..index], &s[index..]),
None => (s, ""), None => (s, ""),
}; };
if !offset.is_empty() && (parse_any_offset_minutes(offset).is_none() || !valid_offset_fraction(offset)) { if !offset.is_empty() && (parse_any_offset_seconds(offset).is_none() || !valid_offset_fraction(offset)) {
return None; return None;
} }
parse_plain_clock(clock) parse_plain_clock(clock)
+41 -17
View File
@@ -78,16 +78,16 @@ fn calendar_id_slot_fallback() {
fn start_of_day_epoch_ns_instant_range() { fn start_of_day_epoch_ns_instant_range() {
// 当地午夜回推 epoch 越 Instant 界(±MAX)返回 None,供 startOfDay/hoursInDay RangeError 依据。 // 当地午夜回推 epoch 越 Instant 界(±MAX)返回 None,供 startOfDay/hoursInDay RangeError 依据。
// -100000001 天 + 1h < -MAX → 越界。 // -100000001 天 + 1h < -MAX → 越界。
assert_eq!(start_of_day_epoch_ns(-271_821, 4, 19, -60), None); assert_eq!(start_of_day_epoch_ns(-271_821, 4, 19, -3600), None);
// -100000000 天(-271821-04-20)UTC 当地午夜恰为 -MAX,在边界内。 // -100000000 天(-271821-04-20)UTC 当地午夜恰为 -MAX,在边界内。
assert_eq!(start_of_day_epoch_ns(-271_821, 4, 20, 0), Some(-8_640_000_000_000_000_000_000)); assert_eq!(start_of_day_epoch_ns(-271_821, 4, 20, 0), Some(-8_640_000_000_000_000_000_000));
// 同日期 +1h 偏移使当地午夜 -MAX - 1h 越界。 // 同日期 +1h 偏移使当地午夜 -MAX - 1h 越界。
assert_eq!(start_of_day_epoch_ns(-271_821, 4, 20, 60), None); assert_eq!(start_of_day_epoch_ns(-271_821, 4, 20, 3600), None);
// 明日边界越界:+100000001 天(UTC)。 // 明日边界越界:+100000001 天(UTC)。
assert_eq!(start_of_day_epoch_ns_by_days(100_000_001, 0), None); assert_eq!(start_of_day_epoch_ns_by_days(100_000_001, 0), None);
// 正常日期返回当地午夜。 // 正常日期返回当地午夜。
assert_eq!(start_of_day_epoch_ns(1970, 1, 1, 0), Some(0)); assert_eq!(start_of_day_epoch_ns(1970, 1, 1, 0), Some(0));
assert_eq!(start_of_day_epoch_ns(1970, 1, 1, 60), Some(-3_600_000_000_000)); assert_eq!(start_of_day_epoch_ns(1970, 1, 1, 3600), Some(-3_600_000_000_000));
} }
#[test] #[test]
@@ -104,20 +104,20 @@ fn format_time_zone_annotation_normalizes_offset() {
#[test] #[test]
fn format_zoned_date_time_iso_annotations() { fn format_zoned_date_time_iso_annotations() {
// 偏移/时区/日历注解各取值组合,验证段序 `{offset}[{tz}][{ca}]` 与 critical 前缀。 // 偏移/时区/日历注解各取值组合,验证段序 `{offset}[{tz}][{ca}]` 与 critical 前缀。
let base = format_zoned_date_time_iso(0, 60, "+01:00", "iso8601", true, None, "auto", "auto", "auto"); let base = format_zoned_date_time_iso(0, 3600, "+01:00", "iso8601", true, None, "auto", "auto", "auto");
assert_eq!(base, Some("1970-01-01T01:00:00+01:00[+01:00]".to_string())); assert_eq!(base, Some("1970-01-01T01:00:00+01:00[+01:00]".to_string()));
// offset never 省略偏移段,时区注解仍显示。 // offset never 省略偏移段,时区注解仍显示。
let no_offset = format_zoned_date_time_iso(0, 60, "+01:00", "iso8601", true, None, "never", "auto", "auto"); let no_offset = format_zoned_date_time_iso(0, 3600, "+01:00", "iso8601", true, None, "never", "auto", "auto");
assert_eq!(no_offset, Some("1970-01-01T01:00:00[+01:00]".to_string())); assert_eq!(no_offset, Some("1970-01-01T01:00:00[+01:00]".to_string()));
// calendarName always 追加日历注解。 // calendarName always 追加日历注解。
let ca_always = format_zoned_date_time_iso(0, 60, "+01:00", "iso8601", true, None, "auto", "auto", "always"); let ca_always = format_zoned_date_time_iso(0, 3600, "+01:00", "iso8601", true, None, "auto", "auto", "always");
assert_eq!(ca_always, Some("1970-01-01T01:00:00+01:00[+01:00][u-ca=iso8601]".to_string())); assert_eq!(ca_always, Some("1970-01-01T01:00:00+01:00[+01:00][u-ca=iso8601]".to_string()));
// timeZoneName/calendarName critical 均 `!` 置于括号内。 // timeZoneName/calendarName critical 均 `!` 置于括号内。
let critical = format_zoned_date_time_iso(0, 60, "+01:00", "iso8601", true, None, "auto", "critical", "critical"); let critical = format_zoned_date_time_iso(0, 3600, "+01:00", "iso8601", true, None, "auto", "critical", "critical");
assert_eq!(critical, Some("1970-01-01T01:00:00+01:00[!+01:00][!u-ca=iso8601]".to_string())); assert_eq!(critical, Some("1970-01-01T01:00:00+01:00[!+01:00][!u-ca=iso8601]".to_string()));
// offset critical 偏移段前加 !。 // offset critical 偏移段前加 !。
let offset_critical = let offset_critical =
format_zoned_date_time_iso(0, 60, "+01:00", "iso8601", true, None, "critical", "auto", "auto"); format_zoned_date_time_iso(0, 3600, "+01:00", "iso8601", true, None, "critical", "auto", "auto");
assert_eq!(offset_critical, Some("1970-01-01T01:00:00!+01:00[+01:00]".to_string())); assert_eq!(offset_critical, Some("1970-01-01T01:00:00!+01:00[+01:00]".to_string()));
} }
@@ -132,14 +132,17 @@ fn format_zoned_date_time_iso_epoch_rounding_cross_midnight() {
#[test] #[test]
fn local_to_epoch_ns_roundtrip() { fn local_to_epoch_ns_roundtrip() {
// 与 parse_instant_string 互逆对拍:同一时刻的本地分量 + 偏移换算回 epoch 一致。 // 与 parse_instant_string 互逆对拍:同一时刻的本地分量 + 偏移换算回 epoch 一致。
assert_eq!(parse_instant_string("2024-01-01T00:00:00+01:00"), local_to_epoch_ns(2024, 1, 1, 0.0, 60),); assert_eq!(
parse_instant_string("2024-01-01T00:00:00+01:00"),
local_to_epoch_ns(2024, 1, 1, 0.0, 3600),
);
assert_eq!( assert_eq!(
parse_instant_string("1969-07-16T13:32:01.234567891Z"), parse_instant_string("1969-07-16T13:32:01.234567891Z"),
local_to_epoch_ns(1969, 7, 16, 48_721_234_567_891.0, 0), local_to_epoch_ns(1969, 7, 16, 48_721_234_567_891.0, 0),
); );
// startOfDay 最小边界:-271821-04-20 加 1h 再回推 1h 偏移回到 -MAX。 // startOfDay 最小边界:-271821-04-20 加 1h 再回推 1h 偏移回到 -MAX。
assert_eq!( assert_eq!(
local_to_epoch_ns(-271821, 4, 20, 3_600_000_000_000.0, 60), local_to_epoch_ns(-271821, 4, 20, 3_600_000_000_000.0, 3600),
Some(-8_640_000_000_000_000_000_000), Some(-8_640_000_000_000_000_000_000),
); );
} }
@@ -161,16 +164,37 @@ fn zoned_date_time_string_wall_day_range_boundary() {
#[test] #[test]
fn canonical_time_zone_4_digit_offset() { fn canonical_time_zone_4_digit_offset() {
// ±HHMM 无冒号形式归一:ID 保留原串,offset 分钟数正确换算。 // ±HHMM 无冒号形式归一:ID 保留原串,offset 秒数正确换算。
assert_eq!(canonical_time_zone("+0000"), Some(("+0000".to_string(), 0))); assert_eq!(canonical_time_zone("+0000"), Some(("+0000".to_string(), 0)));
assert_eq!(canonical_time_zone("-0530"), Some(("-0530".to_string(), -330))); assert_eq!(canonical_time_zone("-0530"), Some(("-0530".to_string(), -19800)));
assert_eq!(canonical_time_zone("+2330"), Some(("+2330".to_string(), 1410))); assert_eq!(canonical_time_zone("+2330"), Some(("+2330".to_string(), 84600)));
// 非法分钟/小时拒绝。 // 非法分钟/小时拒绝。
assert_eq!(canonical_time_zone("+2400"), None); assert_eq!(canonical_time_zone("+2400"), None);
assert_eq!(canonical_time_zone("+0060"), None); assert_eq!(canonical_time_zone("+0060"), None);
assert_eq!(canonical_time_zone("+123"), None); // 长度不符 assert_eq!(canonical_time_zone("+123"), None); // 长度不符
} }
#[test]
fn offset_seconds_representation_sub_minute() {
// i64 秒表示保留亚分钟偏移(±HH:MM:SS 秒位),旧分钟表示丢弃秒位。
assert_eq!(parse_any_offset_seconds("+00:44:30"), Some(2670));
assert_eq!(parse_any_offset_seconds("-00:44:30"), Some(-2670));
assert_eq!(parse_any_offset_seconds("+01:00:00"), Some(3600));
assert_eq!(parse_any_offset_seconds("-23:59:59"), Some(-(23 * 3600 + 59 * 60 + 59)));
// 固定偏移秒值与分钟值 ×60 一致(±HH:MM 六字符形)。
assert_eq!(canonical_time_zone("+01:00"), Some(("+01:00".to_string(), 3600)));
assert_eq!(canonical_time_zone("-05:30"), Some(("-05:30".to_string(), -19800)));
// 亚分钟偏移经 local_to_epoch_ns 与 parse_instant_string 互逆对拍。
assert_eq!(
parse_instant_string("2024-01-01T00:00:00-00:44:30"),
local_to_epoch_ns(2024, 1, 1, 0.0, -2670),
);
assert_eq!(
parse_instant_string("2024-01-01T00:00:00+00:44:30"),
local_to_epoch_ns(2024, 1, 1, 0.0, 2670),
);
}
#[test] #[test]
fn extract_time_zone_annotation_forms() { fn extract_time_zone_annotation_forms() {
// UTC / critical / 数值偏移各形式提取;u-ca 注解跳过,时区注解在前后均能取到。 // UTC / critical / 数值偏移各形式提取;u-ca 注解跳过,时区注解在前后均能取到。
@@ -275,15 +299,15 @@ fn zoned_date_time_round_else_path_epoch() {
let quantum = 3_600_000_000_000 * 4; let quantum = 3_600_000_000_000 * 4;
let rounded = round_instant_ns(55_410_123_456_789, quantum, InstantRoundingMode::HalfExpand).unwrap(); let rounded = round_instant_ns(55_410_123_456_789, quantum, InstantRoundingMode::HalfExpand).unwrap();
assert_eq!(rounded, 57_600_000_000_000); assert_eq!(rounded, 57_600_000_000_000);
// 本地墙钟日回推:offset 60 分,local_to_epoch_ns 得目标 epoch。 // 本地墙钟日回推:offset 3600 秒,local_to_epoch_ns 得目标 epoch。
let epoch = local_to_epoch_ns(1976, 11, 18, rounded as f64, 60).unwrap(); let epoch = local_to_epoch_ns(1976, 11, 18, rounded as f64, 3600).unwrap();
assert_eq!(epoch, 217_177_200_000_000_000); assert_eq!(epoch, 217_177_200_000_000_000);
} }
#[test] #[test]
fn zoned_date_time_round_day_path_epoch() { fn zoned_date_time_round_day_path_epoch() {
// 同日本地午夜 startNs(2513 天,offset 60 分),dayProgress 舍到次日 → 217206000000000000。 // 同日本地午夜 startNs(2513 天,offset 3600 秒),dayProgress 舍到次日 → 217206000000000000。
let start_ns = start_of_day_epoch_ns(1976, 11, 18, 60).unwrap(); let start_ns = start_of_day_epoch_ns(1976, 11, 18, 3600).unwrap();
assert_eq!(start_ns, 217_119_600_000_000_000); assert_eq!(start_ns, 217_119_600_000_000_000);
let day_progress = 217_175_010_123_456_789 - start_ns; let day_progress = 217_175_010_123_456_789 - start_ns;
let rounded = round_instant_ns(day_progress, 86_400_000_000_000, InstantRoundingMode::HalfExpand).unwrap(); let rounded = round_instant_ns(day_progress, 86_400_000_000_000, InstantRoundingMode::HalfExpand).unwrap();
@@ -208,24 +208,24 @@ fn zoned_date_time_options<H: VmHost>(
Ok((offset, disambiguation)) Ok((offset, disambiguation))
} }
/// 从剥注解后的 Instant 主体提取字符串内数值偏移分钟数(±HH / ±HHMM / ±HH:MM / 亚秒形式)。 /// 从剥注解后的 Instant 主体提取字符串内数值偏移秒数(±HH / ±HHMM / ±HH:MM / 亚秒形式)。
/// Z/z 结尾视为 0 分钟;无偏移或偏移不可提取返回 None。 /// Z/z 结尾视为 0 秒;无偏移或偏移不可提取返回 None。
fn extract_string_offset_minutes(body: &str) -> Option<i32> { fn extract_string_offset_seconds(body: &str) -> Option<i64> {
let time_start = body.find(['T', 't', ' '])?; let time_start = body.find(['T', 't', ' '])?;
let time = &body[time_start + 1..]; let time = &body[time_start + 1..];
let offset_start = time let offset_start = time
.char_indices() .char_indices()
.rev() .rev()
.find_map(|(index, ch)| matches!(ch, '+' | '-').then_some(index))?; .find_map(|(index, ch)| matches!(ch, '+' | '-').then_some(index))?;
parse_any_offset_minutes(&time[offset_start..]) parse_any_offset_seconds(&time[offset_start..])
} }
/// 解析数值偏移串(±HH / ±HHMM / ±HH:MM / ±HHMMSS / ±HH:MM:SS,可带小数秒)为分钟数。 /// 解析数值偏移串(±HH / ±HHMM / ±HH:MM / ±HHMMSS / ±HH:MM:SS,可带小数秒)为秒数。
pub(crate) fn parse_any_offset_minutes(value: &str) -> Option<i32> { pub(crate) fn parse_any_offset_seconds(value: &str) -> Option<i64> {
let bytes = value.as_bytes(); let bytes = value.as_bytes();
let sign = match bytes.first() { let sign = match bytes.first() {
Some(b'+') => 1_i32, Some(b'+') => 1_i64,
Some(b'-') => -1_i32, Some(b'-') => -1_i64,
_ => return None, _ => return None,
}; };
let mut cursor = 1usize; let mut cursor = 1usize;
@@ -256,7 +256,7 @@ pub(crate) fn parse_any_offset_minutes(value: &str) -> Option<i32> {
if cursor != bytes.len() || hour > 23 || minute > 59 || second > 59 { if cursor != bytes.len() || hour > 23 || minute > 59 || second > 59 {
return None; return None;
} }
Some(sign * (hour * 60 + minute) as i32) Some((hour * 3600 + minute * 60 + second) as i64 * sign)
} }
/// 从 ZDT 对象 / ISO 字符串 / property bag 解析 (epoch_ns, time_zone_id, calendar_id) 三元组。 /// 从 ZDT 对象 / ISO 字符串 / property bag 解析 (epoch_ns, time_zone_id, calendar_id) 三元组。
@@ -316,15 +316,15 @@ pub(crate) fn zoned_date_time_string_parts<H: VmHost>(
return Err(crate::error::create_range_error(vm, "invalid ISO 8601 date-time")); return Err(crate::error::create_range_error(vm, "invalid ISO 8601 date-time"));
}; };
let has_utc_designator = body.ends_with(['Z', 'z']); let has_utc_designator = body.ends_with(['Z', 'z']);
let string_offset_minutes = if has_utc_designator { Some(0) } else { extract_string_offset_minutes(body) }; let string_offset_seconds = if has_utc_designator { Some(0) } else { extract_string_offset_seconds(body) };
// 有偏移/Z 时经 parse_instant_string 得 epoch 并反推墙钟;否则直接解析墙钟。 // 有偏移/Z 时经 parse_instant_string 得 epoch 并反推墙钟;否则直接解析墙钟。
const DAY_NS: i128 = 86_400_000_000_000; const DAY_NS: i128 = 86_400_000_000_000;
let (epoch_from_string, wall_parts) = if string_offset_minutes.is_some() { let (epoch_from_string, wall_parts) = if string_offset_seconds.is_some() {
let epoch_ns = parse_instant_string(input) let epoch_ns = parse_instant_string(input)
.ok_or_else(|| crate::error::create_range_error(vm, "invalid ISO 8601 date-time"))?; .ok_or_else(|| crate::error::create_range_error(vm, "invalid ISO 8601 date-time"))?;
let offset_minutes = string_offset_minutes.unwrap_or(0); let offset_seconds = string_offset_seconds.unwrap_or(0);
let wall_ns = epoch_ns + i128::from(offset_minutes) * 60_000_000_000; let wall_ns = epoch_ns + i128::from(offset_seconds) * 1_000_000_000;
let days = wall_ns.div_euclid(DAY_NS); let days = wall_ns.div_euclid(DAY_NS);
let (year, month, day) = civil_from_days(days); let (year, month, day) = civil_from_days(days);
(Some(epoch_ns), (year as i32, month as u32, day as u32, wall_ns.rem_euclid(DAY_NS) as f64)) (Some(epoch_ns), (year as i32, month as u32, day as u32, wall_ns.rem_euclid(DAY_NS) as f64))
@@ -340,14 +340,14 @@ pub(crate) fn zoned_date_time_string_parts<H: VmHost>(
return Err(crate::error::create_range_error(vm, "date-time out of range")); return Err(crate::error::create_range_error(vm, "date-time out of range"));
} }
let wall_epoch = let wall_epoch =
|offset_minutes: i32| local_to_epoch_ns(wall_parts.0, wall_parts.1, wall_parts.2, wall_parts.3, offset_minutes); |offset_seconds: i64| local_to_epoch_ns(wall_parts.0, wall_parts.1, wall_parts.2, wall_parts.3, offset_seconds);
// 按 offsetBehaviour 决策:Z → exact(墙钟 epoch);无偏移 → wall(墙钟 + 时区偏移); // 按 offsetBehaviour 决策:Z → exact(墙钟 epoch);无偏移 → wall(墙钟 + 时区偏移);
// 有偏移 → 按 offset 选项在字符串偏移与时区偏移之间选择。 // 有偏移 → 按 offset 选项在字符串偏移与时区偏移之间选择。
let epoch_ns = if has_utc_designator { let epoch_ns = if has_utc_designator {
epoch_from_string epoch_from_string
} else { } else {
match string_offset_minutes { match string_offset_seconds {
None => wall_epoch(time_zone_offset), None => wall_epoch(time_zone_offset),
Some(offset) => match offset_mode { Some(offset) => match offset_mode {
"use" => epoch_from_string, "use" => epoch_from_string,
@@ -401,7 +401,7 @@ fn zoned_date_time_bag_parts<H: VmHost>(
// offset 可选:语法校验(ToOffsetString 语义)先于数值字段转换。 // offset 可选:语法校验(ToOffsetString 语义)先于数值字段转换。
let offset_raw = temporal_option_value(vm, obj, value, "offset")?; let offset_raw = temporal_option_value(vm, obj, value, "offset")?;
let bag_offset_minutes = if offset_raw.is_undefined() { let bag_offset_seconds = if offset_raw.is_undefined() {
None None
} else { } else {
let offset_input = temporal_option_string(vm, offset_raw)?; let offset_input = temporal_option_string(vm, offset_raw)?;
@@ -415,7 +415,7 @@ fn zoned_date_time_bag_parts<H: VmHost>(
let (year, month, day, total_ns, calendar) = plain_date_time_object_parts(vm, value, obj, false, false)?; let (year, month, day, total_ns, calendar) = plain_date_time_object_parts(vm, value, obj, false, false)?;
// 按 offset 选项决定 epoch(InterpretISODateTimeOffset 固定偏移简化:候选恒唯一)。 // 按 offset 选项决定 epoch(InterpretISODateTimeOffset 固定偏移简化:候选恒唯一)。
let epoch_ns = match (bag_offset_minutes, offset_mode) { let epoch_ns = match (bag_offset_seconds, offset_mode) {
(None, _) | (Some(_), "ignore") => local_to_epoch_ns(year, month, day, total_ns, time_zone_offset), (None, _) | (Some(_), "ignore") => local_to_epoch_ns(year, month, day, total_ns, time_zone_offset),
(Some(offset), "use") => local_to_epoch_ns(year, month, day, total_ns, offset), (Some(offset), "use") => local_to_epoch_ns(year, month, day, total_ns, offset),
(Some(offset), "prefer") => { (Some(offset), "prefer") => {
@@ -485,10 +485,10 @@ pub fn zoned_date_time_compare<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResu
/// 时区注解文本:命名区原样返回,偏移区规范化为 `±HH:MM` 带冒号;critical 时 `!` 置于括号内。 /// 时区注解文本:命名区原样返回,偏移区规范化为 `±HH:MM` 带冒号;critical 时 `!` 置于括号内。
pub(crate) fn format_time_zone_annotation(time_zone_id: &str, critical: bool) -> String { pub(crate) fn format_time_zone_annotation(time_zone_id: &str, critical: bool) -> String {
let inner = if matches!(time_zone_id.as_bytes().first(), Some(b'+') | Some(b'-')) { let inner = if matches!(time_zone_id.as_bytes().first(), Some(b'+') | Some(b'-')) {
let offset_minutes = instant_time_zone_offset(time_zone_id).unwrap_or(0); let offset_seconds = instant_time_zone_offset(time_zone_id).unwrap_or(0);
let sign = if offset_minutes < 0 { '-' } else { '+' }; let sign = if offset_seconds < 0 { '-' } else { '+' };
let magnitude = offset_minutes.abs(); let magnitude = offset_seconds.abs();
format!("{sign}{:02}:{:02}", magnitude / 60, magnitude % 60) format!("{sign}{:02}:{:02}", magnitude / 3600, (magnitude % 3600) / 60)
} else { } else {
time_zone_id.to_string() time_zone_id.to_string()
}; };
@@ -507,11 +507,11 @@ pub(crate) fn format_time_zone_annotation(time_zone_id: &str, critical: bool) ->
/// - 偏移段恒 `±HH:MM` 带冒号;时区注解命名区原样、偏移区规范化。 /// - 偏移段恒 `±HH:MM` 带冒号;时区注解命名区原样、偏移区规范化。
#[expect(clippy::too_many_arguments)] #[expect(clippy::too_many_arguments)]
pub(crate) fn format_zoned_date_time_iso( pub(crate) fn format_zoned_date_time_iso(
epoch_ns: i128, offset_minutes: i32, time_zone_id: &str, calendar_id: &str, include_seconds: bool, epoch_ns: i128, offset_seconds: i64, time_zone_id: &str, calendar_id: &str, include_seconds: bool,
output_digits: Option<usize>, offset_name: &str, time_zone_name: &str, calendar_name: &str, output_digits: Option<usize>, offset_name: &str, time_zone_name: &str, calendar_name: &str,
) -> Option<String> { ) -> Option<String> {
const DAY_NS: i128 = 86_400_000_000_000; const DAY_NS: i128 = 86_400_000_000_000;
let offset_ns = i128::from(offset_minutes).checked_mul(60_000_000_000)?; let offset_ns = i128::from(offset_seconds).checked_mul(1_000_000_000)?;
let local_ns = epoch_ns.checked_add(offset_ns)?; let local_ns = epoch_ns.checked_add(offset_ns)?;
let days = local_ns.div_euclid(DAY_NS); let days = local_ns.div_euclid(DAY_NS);
let mut time_ns = local_ns.rem_euclid(DAY_NS); let mut time_ns = local_ns.rem_euclid(DAY_NS);
@@ -544,12 +544,12 @@ pub(crate) fn format_zoned_date_time_iso(
// offset 段:auto/always 显示,never 省略,critical 段前加 !。 // offset 段:auto/always 显示,never 省略,critical 段前加 !。
if offset_name != "never" { if offset_name != "never" {
let sign = if offset_minutes < 0 { '-' } else { '+' }; let sign = if offset_seconds < 0 { '-' } else { '+' };
let magnitude = offset_minutes.abs(); let magnitude = offset_seconds.abs();
if offset_name == "critical" { if offset_name == "critical" {
output.push('!'); output.push('!');
} }
output.push_str(&format!("{sign}{:02}:{:02}", magnitude / 60, magnitude % 60)); output.push_str(&format!("{sign}{:02}:{:02}", magnitude / 3600, (magnitude % 3600) / 60));
} }
// timeZoneName 注解:auto 显示,never 省略,critical 时 `!` 置于括号内。 // timeZoneName 注解:auto 显示,never 省略,critical 时 `!` 置于括号内。
@@ -577,7 +577,7 @@ pub fn zoned_date_time_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeRe
return NativeResult::Err(crate::error::create_range_error(vm, "invalid ZonedDateTime")); return NativeResult::Err(crate::error::create_range_error(vm, "invalid ZonedDateTime"));
}; };
let time_zone_id = to_string(obj.get_prop_at(1)); let time_zone_id = to_string(obj.get_prop_at(1));
let offset_minutes = native_try!(zoned_date_time_offset_minutes(vm, obj)); let offset_seconds = native_try!(zoned_date_time_offset_seconds(vm, obj));
let calendar_id = get_calendar_id(obj, 2); let calendar_id = get_calendar_id(obj, 2);
let options_value = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; let options_value = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) };
@@ -679,7 +679,7 @@ pub fn zoned_date_time_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeRe
} }
match format_zoned_date_time_iso( match format_zoned_date_time_iso(
rounded_ns, rounded_ns,
offset_minutes, offset_seconds,
&time_zone_id, &time_zone_id,
&calendar_id, &calendar_id,
include_seconds, include_seconds,
@@ -783,7 +783,7 @@ struct ZdtMergedFields {
microsecond: f64, microsecond: f64,
nanosecond: f64, nanosecond: f64,
month_code: Option<(f64, bool)>, month_code: Option<(f64, bool)>,
bag_offset: Option<i32>, bag_offset: Option<i64>,
} }
/// with 的部分字段读取与合并:按字典序读 bag 字段,与 receiver 默认分量合并。 /// with 的部分字段读取与合并:按字典序读 bag 字段,与 receiver 默认分量合并。
@@ -791,7 +791,7 @@ struct ZdtMergedFields {
/// # 步骤 /// # 步骤
/// 1. 数值字段 ToNumber→trunc(NaN/±Inf RangeError,day 额外拒绝 <1);monthCode/offset 走 ToString。 /// 1. 数值字段 ToNumber→trunc(NaN/±Inf RangeError,day 额外拒绝 <1);monthCode/offset 走 ToString。
/// 2. undefined 不覆盖;至少一个字段有定义否则 TypeError。 /// 2. undefined 不覆盖;至少一个字段有定义否则 TypeError。
/// 3. 返回合并后的原始分量(未钳制)+ monthCode 解析 + bag offset 分钟。 /// 3. 返回合并后的原始分量(未钳制)+ monthCode 解析 + bag offset 秒。
/// ///
/// # 边界与前提 /// # 边界与前提
/// - 调用方须先完成 RejectObjectWithCalendarOrTimeZone(calendar/timeZone 已拒绝)。 /// - 调用方须先完成 RejectObjectWithCalendarOrTimeZone(calendar/timeZone 已拒绝)。
@@ -888,17 +888,17 @@ fn zoned_date_time_with_fields<H: VmHost>(
}; };
// offset 先经 ToString,再按偏移字符串语法校验(小数秒最多 9 位),解析出的 // offset 先经 ToString,再按偏移字符串语法校验(小数秒最多 9 位),解析出的
// 分钟数留给 offset 选项决策使用。 // 秒数留给 offset 选项决策使用。
let bag_offset = if offset_raw.is_undefined() { let bag_offset = if offset_raw.is_undefined() {
None None
} else { } else {
let offset_input = temporal_string_strict(vm, offset_raw)?; let offset_input = temporal_string_strict(vm, offset_raw)?;
let minutes = parse_any_offset_minutes(&offset_input) let seconds = parse_any_offset_seconds(&offset_input)
.ok_or_else(|| crate::error::create_range_error(vm, "invalid offset"))?; .ok_or_else(|| crate::error::create_range_error(vm, "invalid offset"))?;
if !valid_offset_fraction(&offset_input) { if !valid_offset_fraction(&offset_input) {
return Err(crate::error::create_range_error(vm, "invalid offset")); return Err(crate::error::create_range_error(vm, "invalid offset"));
} }
Some(minutes) Some(seconds)
}; };
let (receiver_year, _receiver_month, receiver_day, receiver_time_ns) = defaults; let (receiver_year, _receiver_month, receiver_day, receiver_time_ns) = defaults;
@@ -942,7 +942,7 @@ pub fn zoned_date_time_with<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
native_try!(reject_partial_object_with_calendar_or_time_zone(vm, value)); native_try!(reject_partial_object_with_calendar_or_time_zone(vm, value));
let time_zone_id = to_string(obj.get_prop_at(1)); let time_zone_id = to_string(obj.get_prop_at(1));
let offset_minutes = native_try!(zoned_date_time_offset_minutes(vm, obj)); let offset_seconds = native_try!(zoned_date_time_offset_seconds(vm, obj));
let (year, month, day, time_ns) = native_try!(zoned_date_time_plain_parts(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))); let merged = native_try!(zoned_date_time_with_fields(vm, value, (year, month, day, time_ns)));
@@ -1040,18 +1040,18 @@ pub fn zoned_date_time_with<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
// InterpretISODateTimeOffset(固定偏移简化):按 offset 选项在 bag 偏移与时区偏移间选择。 // InterpretISODateTimeOffset(固定偏移简化):按 offset 选项在 bag 偏移与时区偏移间选择。
let epoch_ns = match (merged.bag_offset, offset_mode.as_str()) { let epoch_ns = match (merged.bag_offset, offset_mode.as_str()) {
(None, _) => local_to_epoch_ns(year, month, day, total_ns, offset_minutes), (None, _) => local_to_epoch_ns(year, month, day, total_ns, offset_seconds),
(Some(offset), "use") => local_to_epoch_ns(year, month, day, total_ns, offset), (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(_), "ignore") => local_to_epoch_ns(year, month, day, total_ns, offset_seconds),
(Some(offset), "prefer") => { (Some(offset), "prefer") => {
if offset == offset_minutes { if offset == offset_seconds {
local_to_epoch_ns(year, month, day, total_ns, offset) local_to_epoch_ns(year, month, day, total_ns, offset)
} else { } else {
local_to_epoch_ns(year, month, day, total_ns, offset_minutes) local_to_epoch_ns(year, month, day, total_ns, offset_seconds)
} }
} }
(Some(offset), "reject") => { (Some(offset), "reject") => {
if offset == offset_minutes { if offset == offset_seconds {
local_to_epoch_ns(year, month, day, total_ns, offset) local_to_epoch_ns(year, month, day, total_ns, offset)
} else { } else {
return NativeResult::Err(crate::error::create_range_error(vm, "offset and time zone disagree")); return NativeResult::Err(crate::error::create_range_error(vm, "offset and time zone disagree"));
@@ -1095,7 +1095,7 @@ pub fn zoned_date_time_with_calendar<H: VmHost>(vm: &mut H, args: &[u8]) -> Nati
make_zoned_date_time(vm, epoch_ns, &time_zone_id, &calendar) make_zoned_date_time(vm, epoch_ns, &time_zone_id, &calendar)
} }
/// 校验 offset 串的小数秒位数 ≤9(parse_any_offset_minutes 不限制位数,此处补查)。 /// 校验 offset 串的小数秒位数 ≤9(parse_any_offset_seconds 不限制位数,此处补查)。
pub(crate) fn valid_offset_fraction(value: &str) -> bool { pub(crate) fn valid_offset_fraction(value: &str) -> bool {
let Some(dot) = value.find(['.', ',']) else { let Some(dot) = value.find(['.', ',']) else {
return true; return true;
@@ -1119,10 +1119,10 @@ pub fn zoned_date_time_with_plain_time<H: VmHost>(vm: &mut H, args: &[u8]) -> Na
let obj = unsafe { &*ptr }; let obj = unsafe { &*ptr };
native_try!(ensure_zoned_date_time(vm, obj)); native_try!(ensure_zoned_date_time(vm, obj));
let (year, month, day, _) = native_try!(zoned_date_time_plain_parts(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 offset_seconds = native_try!(zoned_date_time_offset_seconds(vm, obj));
let plain_time_like = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; 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 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) let epoch_ns = local_to_epoch_ns(year, month, day, time_ns, offset_seconds)
.ok_or_else(|| crate::error::create_range_error(vm, "invalid date-time")); .ok_or_else(|| crate::error::create_range_error(vm, "invalid date-time"));
let epoch_ns = native_try!(epoch_ns); let epoch_ns = native_try!(epoch_ns);
if epoch_ns.unsigned_abs() > MAX_INSTANT_NS as u128 { if epoch_ns.unsigned_abs() > MAX_INSTANT_NS as u128 {
@@ -1207,9 +1207,9 @@ fn zoned_date_time_other_wall_epoch_ns<H: VmHost>(
if !valid_iso_date(year, month, day) || !total_ns.is_finite() || !(0.0..86_400_000_000_000.0).contains(&total_ns) { if !valid_iso_date(year, month, day) || !total_ns.is_finite() || !(0.0..86_400_000_000_000.0).contains(&total_ns) {
return Err(crate::error::create_range_error(vm, "invalid date-time component")); return Err(crate::error::create_range_error(vm, "invalid date-time component"));
} }
let offset_minutes = instant_time_zone_offset(default_tz_id) let offset_seconds = instant_time_zone_offset(default_tz_id)
.ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))?; .ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))?;
local_to_epoch_ns(year, month, day, total_ns, offset_minutes) local_to_epoch_ns(year, month, day, total_ns, offset_seconds)
.ok_or_else(|| crate::error::create_range_error(vm, "invalid date-time")) .ok_or_else(|| crate::error::create_range_error(vm, "invalid date-time"))
} }
@@ -1236,7 +1236,7 @@ fn zoned_date_time_other_bag_epoch_ns<H: VmHost>(
}; };
let offset_raw = temporal_option_value(vm, obj, value, "offset")?; let offset_raw = temporal_option_value(vm, obj, value, "offset")?;
let bag_offset_minutes = if offset_raw.is_undefined() { let bag_offset_seconds = if offset_raw.is_undefined() {
None None
} else { } else {
if !offset_raw.is_string() { if !offset_raw.is_string() {
@@ -1250,7 +1250,7 @@ fn zoned_date_time_other_bag_epoch_ns<H: VmHost>(
}; };
let (year, month, day, total_ns, _calendar) = plain_date_time_object_parts(vm, value, obj, false, false)?; let (year, month, day, total_ns, _calendar) = plain_date_time_object_parts(vm, value, obj, false, false)?;
if let Some(offset) = bag_offset_minutes { if let Some(offset) = bag_offset_seconds {
if offset != time_zone_offset { if offset != time_zone_offset {
return Err(crate::error::create_range_error(vm, "offset and time zone disagree")); return Err(crate::error::create_range_error(vm, "offset and time zone disagree"));
} }
@@ -1279,7 +1279,7 @@ fn zoned_date_time_difference<H: VmHost>(vm: &mut H, args: &[u8], since: bool) -
let Some(epoch_r) = get_instant_epoch_ns(obj) else { let Some(epoch_r) = get_instant_epoch_ns(obj) else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid ZonedDateTime")); return NativeResult::Err(crate::error::create_range_error(vm, "invalid ZonedDateTime"));
}; };
let offset_min_r = native_try!(zoned_date_time_offset_minutes(vm, obj)); let offset_seconds_r = native_try!(zoned_date_time_offset_seconds(vm, obj));
let time_zone_id = to_string(obj.get_prop_at(1)); let time_zone_id = to_string(obj.get_prop_at(1));
let (y_r, m_r, d_r, time_ns_r) = native_try!(zoned_date_time_plain_parts(vm, obj)); let (y_r, m_r, d_r, time_ns_r) = native_try!(zoned_date_time_plain_parts(vm, obj));
@@ -1293,7 +1293,7 @@ fn zoned_date_time_difference<H: VmHost>(vm: &mut H, args: &[u8], since: bool) -
// other 按 receiver 时区偏移拆本地墙钟(负 epoch 用 div_euclid/rem_euclid 保持非负余数)。 // other 按 receiver 时区偏移拆本地墙钟(负 epoch 用 div_euclid/rem_euclid 保持非负余数)。
const DAY_NS: i128 = 86_400_000_000_000; const DAY_NS: i128 = 86_400_000_000_000;
let local_ns_o = epoch_o + i128::from(offset_min_r) * 60_000_000_000; let local_ns_o = epoch_o + i128::from(offset_seconds_r) * 1_000_000_000;
let days_o = local_ns_o.div_euclid(DAY_NS); let days_o = local_ns_o.div_euclid(DAY_NS);
let (y_o, m_o, d_o) = civil_from_days(days_o); let (y_o, m_o, d_o) = civil_from_days(days_o);
let time_ns_o = local_ns_o.rem_euclid(DAY_NS); let time_ns_o = local_ns_o.rem_euclid(DAY_NS);
@@ -1342,7 +1342,7 @@ pub(crate) fn zoned_date_time_round_unit(value: &str) -> Option<(i128, i128)> {
/// # 步骤 /// # 步骤
/// 1. roundTo 解析:undefined → TypeError;字符串 → {smallestUnit: 串};对象 → 依次 Get /// 1. roundTo 解析:undefined → TypeError;字符串 → {smallestUnit: 串};对象 → 依次 Get
/// roundingIncrement → roundingMode → smallestUnit,全部读完再统一校验。 /// roundingIncrement → roundingMode → smallestUnit,全部读完再统一校验。
/// 2. receiver:ensure_zoned_date_time → epoch_r;instant_time_zone_offset(tz) 得 offset_min; /// 2. receiver:ensure_zoned_date_time → epoch_r;instant_time_zone_offset(tz) 得 offset_seconds;
/// zoned_date_time_plain_parts 得 (y, m, d, time_ns)。 /// zoned_date_time_plain_parts 得 (y, m, d, time_ns)。
/// 3. 单位表查 smallestUnit(None → RangeError "invalid smallest unit")。 /// 3. 单位表查 smallestUnit(None → RangeError "invalid smallest unit")。
/// 4. roundingIncrement 校验:1..=1e9 且真因子(increment < units_per_day 且 /// 4. roundingIncrement 校验:1..=1e9 且真因子(increment < units_per_day 且
@@ -1363,7 +1363,7 @@ pub fn zoned_date_time_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
return NativeResult::Err(crate::error::create_range_error(vm, "invalid ZonedDateTime")); return NativeResult::Err(crate::error::create_range_error(vm, "invalid ZonedDateTime"));
}; };
let time_zone_id = to_string(obj.get_prop_at(1)); let time_zone_id = to_string(obj.get_prop_at(1));
let Some(offset_min) = instant_time_zone_offset(&time_zone_id) else { let Some(offset_seconds) = instant_time_zone_offset(&time_zone_id) else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid time zone")); return NativeResult::Err(crate::error::create_range_error(vm, "invalid time zone"));
}; };
let calendar_id = get_calendar_id(obj, 2); let calendar_id = get_calendar_id(obj, 2);
@@ -1435,11 +1435,11 @@ pub fn zoned_date_time_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
const DAY_NS: i128 = 86_400_000_000_000; const DAY_NS: i128 = 86_400_000_000_000;
let result_ns = if unit_ns == DAY_NS { let result_ns = if unit_ns == DAY_NS {
// day 路径:startNs/endNs 双算(越界 → RangeError),dayProgress 固定偏移下 ∈ [0, DAY)。 // day 路径:startNs/endNs 双算(越界 → RangeError),dayProgress 固定偏移下 ∈ [0, DAY)。
let start_ns = native_try!(start_of_day_epoch_ns(y, m, d, offset_min) let start_ns = native_try!(start_of_day_epoch_ns(y, m, d, offset_seconds)
.ok_or_else(|| crate::error::create_range_error(vm, "invalid start of day"))); .ok_or_else(|| crate::error::create_range_error(vm, "invalid start of day")));
native_try!(start_of_day_epoch_ns_by_days( native_try!(start_of_day_epoch_ns_by_days(
days_from_civil(i128::from(y), i128::from(m), i128::from(d)) + 1, days_from_civil(i128::from(y), i128::from(m), i128::from(d)) + 1,
offset_min, offset_seconds,
) )
.ok_or_else(|| crate::error::create_range_error(vm, "invalid start of day"))); .ok_or_else(|| crate::error::create_range_error(vm, "invalid start of day")));
let day_progress = epoch_r - start_ns; let day_progress = epoch_r - start_ns;
@@ -1458,7 +1458,7 @@ pub fn zoned_date_time_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
let Some(rounded_time) = round_instant_ns(time_ns as i128, quantum_ns, mode) else { let Some(rounded_time) = round_instant_ns(time_ns as i128, quantum_ns, mode) else {
return NativeResult::Err(crate::error::create_range_error(vm, "ZonedDateTime outside supported range")); return NativeResult::Err(crate::error::create_range_error(vm, "ZonedDateTime outside supported range"));
}; };
let Some(result) = local_to_epoch_ns(y, m, d, rounded_time as f64, offset_min) else { let Some(result) = local_to_epoch_ns(y, m, d, rounded_time as f64, offset_seconds) else {
return NativeResult::Err(crate::error::create_range_error(vm, "ZonedDateTime outside supported range")); return NativeResult::Err(crate::error::create_range_error(vm, "ZonedDateTime outside supported range"));
}; };
result result
@@ -1569,30 +1569,30 @@ zoned_date_time_brand_only_getter!(zoned_date_time_months_in_year, JsValue::floa
zoned_date_time_brand_only_getter!(zoned_date_time_era, JsValue::undefined()); zoned_date_time_brand_only_getter!(zoned_date_time_era, JsValue::undefined());
zoned_date_time_brand_only_getter!(zoned_date_time_era_year, JsValue::undefined()); zoned_date_time_brand_only_getter!(zoned_date_time_era_year, JsValue::undefined());
/// 读 ZDT 时区偏移(分钟);槽 1 解析失败返回 RangeError。调用方须先 ensure_zoned_date_time。 /// 读 ZDT 时区偏移(秒);槽 1 解析失败返回 RangeError。调用方须先 ensure_zoned_date_time。
fn zoned_date_time_offset_minutes<H: VmHost>(vm: &mut H, obj: &JsObject) -> Result<i32, JsValue> { fn zoned_date_time_offset_seconds<H: VmHost>(vm: &mut H, obj: &JsObject) -> Result<i64, JsValue> {
let time_zone_id = to_string(obj.get_prop_at(1)); let time_zone_id = to_string(obj.get_prop_at(1));
instant_time_zone_offset(&time_zone_id).ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone")) instant_time_zone_offset(&time_zone_id).ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))
} }
/// `Temporal.ZonedDateTime.prototype.offset`:由偏移分钟数规范化为 ±HH:MM 字符串。 /// `Temporal.ZonedDateTime.prototype.offset`:由偏移秒数规范化为 ±HH:MM 字符串。
pub fn zoned_date_time_offset<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult { pub fn zoned_date_time_offset<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args)); let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr }; let obj = unsafe { &*ptr };
native_try!(ensure_zoned_date_time(vm, obj)); native_try!(ensure_zoned_date_time(vm, obj));
let offset_minutes = native_try!(zoned_date_time_offset_minutes(vm, obj)); let offset_seconds = native_try!(zoned_date_time_offset_seconds(vm, obj));
let sign = if offset_minutes < 0 { '-' } else { '+' }; let sign = if offset_seconds < 0 { '-' } else { '+' };
let magnitude = offset_minutes.abs(); let magnitude = offset_seconds.abs();
NativeResult::Ok(vm.new_string(&format!("{sign}{:02}:{:02}", magnitude / 60, magnitude % 60))) NativeResult::Ok(vm.new_string(&format!("{sign}{:02}:{:02}", magnitude / 3600, (magnitude % 3600) / 60)))
} }
/// `Temporal.ZonedDateTime.prototype.offsetNanoseconds`:偏移分钟数换算纳秒(f64 精确域内)。 /// `Temporal.ZonedDateTime.prototype.offsetNanoseconds`:偏移秒数换算纳秒(f64 精确域内)。
pub fn zoned_date_time_offset_nanoseconds<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult { pub fn zoned_date_time_offset_nanoseconds<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args)); let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr }; let obj = unsafe { &*ptr };
native_try!(ensure_zoned_date_time(vm, obj)); native_try!(ensure_zoned_date_time(vm, obj));
let offset_minutes = native_try!(zoned_date_time_offset_minutes(vm, obj)); let offset_seconds = native_try!(zoned_date_time_offset_seconds(vm, obj));
NativeResult::Ok(JsValue::float(offset_minutes as f64 * 60_000_000_000.0)) NativeResult::Ok(JsValue::float(offset_seconds as f64 * 1_000_000_000.0))
} }
/// `Temporal.ZonedDateTime.prototype.monthCode`:ISO 日历下恒为 `M{month:02}` 补零格式。 /// `Temporal.ZonedDateTime.prototype.monthCode`:ISO 日历下恒为 `M{month:02}` 补零格式。
@@ -1610,26 +1610,26 @@ pub fn zoned_date_time_hours_in_day<H: VmHost>(vm: &mut H, args: &[u8]) -> Nativ
let obj = unsafe { &*ptr }; let obj = unsafe { &*ptr };
native_try!(ensure_zoned_date_time(vm, obj)); native_try!(ensure_zoned_date_time(vm, obj));
let (year, month, day, _) = native_try!(zoned_date_time_plain_parts(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 offset_seconds = native_try!(zoned_date_time_offset_seconds(vm, obj));
let today = native_try!(start_of_day_epoch_ns(year, month, day, offset_minutes) let today = native_try!(start_of_day_epoch_ns(year, month, day, offset_seconds)
.ok_or_else(|| crate::error::create_range_error(vm, "invalid date"))); .ok_or_else(|| crate::error::create_range_error(vm, "invalid date")));
let days = days_from_civil(i128::from(year), i128::from(month), i128::from(day)); let days = days_from_civil(i128::from(year), i128::from(month), i128::from(day));
let tomorrow = native_try!(start_of_day_epoch_ns_by_days(days + 1, offset_minutes) let tomorrow = native_try!(start_of_day_epoch_ns_by_days(days + 1, offset_seconds)
.ok_or_else(|| crate::error::create_range_error(vm, "invalid date"))); .ok_or_else(|| crate::error::create_range_error(vm, "invalid date")));
NativeResult::Ok(JsValue::float((tomorrow - today) as f64 / 3_600_000_000_000.0)) NativeResult::Ok(JsValue::float((tomorrow - today) as f64 / 3_600_000_000_000.0))
} }
/// 求本地日期在给定时区偏移下的当地午夜纪元纳秒(GetStartOfDay 语义), /// 求本地日期在给定时区偏移下的当地午夜纪元纳秒(GetStartOfDay 语义),
/// 结果超出 Instant 范围时返回 None。 /// 结果超出 Instant 范围时返回 None。
pub(crate) fn start_of_day_epoch_ns(year: i32, month: u32, day: u32, offset_minutes: i32) -> Option<i128> { pub(crate) fn start_of_day_epoch_ns(year: i32, month: u32, day: u32, offset_seconds: i64) -> Option<i128> {
start_of_day_epoch_ns_by_days(days_from_civil(i128::from(year), i128::from(month), i128::from(day)), offset_minutes) start_of_day_epoch_ns_by_days(days_from_civil(i128::from(year), i128::from(month), i128::from(day)), offset_seconds)
} }
/// 按日数直接算当地午夜(hoursInDay 的明日边界复用:days+1 不经 civil 回填)。 /// 按日数直接算当地午夜(hoursInDay 的明日边界复用:days+1 不经 civil 回填)。
pub(crate) fn start_of_day_epoch_ns_by_days(days: i128, offset_minutes: i32) -> Option<i128> { pub(crate) fn start_of_day_epoch_ns_by_days(days: i128, offset_seconds: i64) -> Option<i128> {
let start = days let start = days
.checked_mul(86_400_000_000_000)? .checked_mul(86_400_000_000_000)?
.checked_sub(i128::from(offset_minutes) * 60_000_000_000)?; .checked_sub(i128::from(offset_seconds) * 1_000_000_000)?;
(start.unsigned_abs() <= MAX_INSTANT_NS as u128).then_some(start) (start.unsigned_abs() <= MAX_INSTANT_NS as u128).then_some(start)
} }
@@ -1653,7 +1653,7 @@ fn zoned_date_time_apply_duration<H: VmHost>(vm: &mut H, args: &[u8], sign: i64)
let obj = unsafe { &*ptr }; let obj = unsafe { &*ptr };
native_try!(ensure_zoned_date_time(vm, obj)); native_try!(ensure_zoned_date_time(vm, obj));
let time_zone_id = to_string(obj.get_prop_at(1)); let time_zone_id = to_string(obj.get_prop_at(1));
let offset_minutes = native_try!(zoned_date_time_offset_minutes(vm, obj)); let offset_seconds = native_try!(zoned_date_time_offset_seconds(vm, obj));
let (year, month, day, time_ns) = match zoned_date_time_plain_parts(vm, obj) { let (year, month, day, time_ns) = match zoned_date_time_plain_parts(vm, obj) {
Ok(parts) => parts, Ok(parts) => parts,
Err(error) => return NativeResult::Err(error), Err(error) => return NativeResult::Err(error),
@@ -1711,7 +1711,7 @@ fn zoned_date_time_apply_duration<H: VmHost>(vm: &mut H, args: &[u8], sign: i64)
if !valid_plain_date_time_range(yy as i32, mm as u32, dd as u32, new_time_ns as f64) { if !valid_plain_date_time_range(yy as i32, mm as u32, dd as u32, new_time_ns as f64) {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time")); return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time"));
} }
let epoch_ns = match local_to_epoch_ns(yy as i32, mm as u32, dd as u32, new_time_ns as f64, offset_minutes) { let epoch_ns = match local_to_epoch_ns(yy as i32, mm as u32, dd as u32, new_time_ns as f64, offset_seconds) {
Some(epoch_ns) if epoch_ns.unsigned_abs() <= MAX_INSTANT_NS as u128 => epoch_ns, Some(epoch_ns) if epoch_ns.unsigned_abs() <= MAX_INSTANT_NS as u128 => epoch_ns,
_ => return NativeResult::Err(crate::error::create_range_error(vm, "ZonedDateTime outside supported range")), _ => return NativeResult::Err(crate::error::create_range_error(vm, "ZonedDateTime outside supported range")),
}; };
@@ -1788,8 +1788,8 @@ pub fn zoned_date_time_start_of_day<H: VmHost>(vm: &mut H, args: &[u8]) -> Nativ
let obj = unsafe { &*ptr }; let obj = unsafe { &*ptr };
native_try!(ensure_zoned_date_time(vm, obj)); native_try!(ensure_zoned_date_time(vm, obj));
let (year, month, day, _) = native_try!(zoned_date_time_plain_parts(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 offset_seconds = native_try!(zoned_date_time_offset_seconds(vm, obj));
let Some(epoch_ns) = start_of_day_epoch_ns(year, month, day, offset_minutes) else { let Some(epoch_ns) = start_of_day_epoch_ns(year, month, day, offset_seconds) else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid date")); return NativeResult::Err(crate::error::create_range_error(vm, "invalid date"));
}; };
make_zoned_date_time(vm, epoch_ns, &to_string(obj.get_prop_at(1)), &get_calendar_id(obj, 2)) make_zoned_date_time(vm, epoch_ns, &to_string(obj.get_prop_at(1)), &get_calendar_id(obj, 2))