feat: temporal IANA 区接受面与 epoch 依赖偏移读取路径加亚分钟偏移 getter

This commit is contained in:
2026-09-28 01:38:23 +00:00
parent 9b0a10f1f3
commit 4dece4146b
7 changed files with 264 additions and 84 deletions
+94 -15
View File
@@ -20,9 +20,10 @@ use oxide_runtime_api::{to_number, to_string, NativeResult, VmHost};
use oxide_types::object::JsObject; use oxide_types::object::JsObject;
use oxide_types::value::JsValue; use oxide_types::value::JsValue;
use super::time_zone::{tz_canonical_name, zone_offset_seconds};
use super::{ use super::{
instant_time_zone_offset, parse_iso_date, parse_plain_date_time_string, parse_plain_time_string, valid_iso_date, parse_iso_date, parse_plain_date_time_string, parse_plain_time_string, valid_iso_date, valid_plain_date_time_range,
valid_plain_date_time_range, valid_plain_time, valid_plain_time,
}; };
pub(crate) const MAX_INSTANT_NS: i128 = 8_640_000_000_000_000_000_000; pub(crate) const MAX_INSTANT_NS: i128 = 8_640_000_000_000_000_000_000;
@@ -658,13 +659,71 @@ pub(crate) fn parse_offset_seconds(value: &str) -> Option<i64> {
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, i64)> { /// 数值偏移串统一解析为秒(±HH:MM / ±HH / ±HHMM / ±HH:MM:SS 四形式)。
///
/// # 边界与前提
/// - 四形式以外的串(含带小数秒的 ±HH:MM:SS.xxx)返回 None。
/// - 小时 ≤ 23、分钟/秒 ≤ 59,越界返回 None。
pub(crate) fn fixed_offset_seconds(value: &str) -> Option<i64> {
let bytes = value.as_bytes();
let sign = match bytes.first() {
Some(b'+') => 1_i64,
Some(b'-') => -1_i64,
_ => return None,
};
let digits = |range: std::ops::Range<usize>| -> Option<i64> {
if bytes[range.clone()].iter().all(u8::is_ascii_digit) {
Some(bytes[range].iter().map(|b| i64::from(b - b'0')).fold(0, |acc, d| acc * 10 + d))
} else {
None
}
};
// ±HH:MM(6 字符):canonical_time_zone 接受面与 zone_offset_seconds 快速路径共用。
if bytes.len() == 6 && bytes[3] == b':' {
let hour = digits(1..3)?;
let minute = digits(4..6)?;
return (hour <= 23 && minute <= 59).then_some(sign * (hour * 3600 + minute * 60));
}
// ±HH(3 字符):小时制简写,分钟恒 0。
if bytes.len() == 3 {
let hour = digits(1..3)?;
return (hour <= 23).then_some(sign * hour * 3600);
}
// ±HHMM(5 字符):无冒号归一形式。
if bytes.len() == 5 {
let hour = digits(1..3)?;
let minute = digits(3..5)?;
return (hour <= 23 && minute <= 59).then_some(sign * (hour * 3600 + minute * 60));
}
// ±HH:MM:SS(9 字符):亚分钟偏移,zone_offset_seconds 快速路径专用。
if bytes.len() == 9 && bytes[3] == b':' && bytes[6] == b':' {
let hour = digits(1..3)?;
let minute = digits(4..6)?;
let second = digits(7..9)?;
return (hour <= 23 && minute <= 59 && second <= 59).then_some(sign * (hour * 3600 + minute * 60 + second));
}
None
}
/// 时区标识符规范化:UTC/Z 归一为 "UTC",数值偏移串原样保留,IANA 区名解别名后
/// 返回规范名。
///
/// # 步骤
/// 1. UTC/Z(大小写折叠)→ "UTC"。
/// 2. 数值偏移串(±HH:MM / ±HH / ±HHMM)原样保留为时区 ID。
/// 3. IANA 区名(含 legacy 别名)命中区表 → 规范名。
/// 4. 带注解的日期时间以最后一个方括号内标识符为准;日期时间串尾偏移按偏移串处理。
///
/// # 边界与前提
/// - 返回只含规范名,不含偏移:偏移是 epoch 的函数,由 zone_offset_seconds 统一提供。
/// - 非法形式(越界小时/分钟、-000000 前缀、未知区名)返回 None。
pub(crate) fn canonical_time_zone(value: &str) -> Option<String> {
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());
} }
if let Some(offset) = parse_offset_seconds(input) { if parse_offset_seconds(input).is_some() {
return Some((input.to_string(), offset)); return Some(input.to_string());
} }
// ±HH 基本偏移("+01" / "-05"):3 字符,hour ≤ 23,分钟恒 0。仅放宽构造器时区接受面。 // ±HH 基本偏移("+01" / "-05"):3 字符,hour ≤ 23,分钟恒 0。仅放宽构造器时区接受面。
@@ -674,8 +733,7 @@ pub(crate) fn canonical_time_zone(value: &str) -> Option<(String, i64)> {
{ {
let hour = (input.as_bytes()[1] - b'0') as i64 * 10 + (input.as_bytes()[2] - b'0') as i64; 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 }; return Some(input.to_string());
return Some((input.to_string(), sign * hour * 3600));
} }
return None; return None;
} }
@@ -688,8 +746,7 @@ pub(crate) fn canonical_time_zone(value: &str) -> Option<(String, i64)> {
let hour = (input.as_bytes()[1] - b'0') as i64 * 10 + (input.as_bytes()[2] - b'0') as i64; 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 i64 * 10 + (input.as_bytes()[4] - b'0') as i64; 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 }; return Some(input.to_string());
return Some((input.to_string(), sign * (hour * 3600 + minute * 60)));
} }
return None; return None;
} }
@@ -697,6 +754,11 @@ pub(crate) fn canonical_time_zone(value: &str) -> Option<(String, i64)> {
return None; return None;
} }
// IANA 区名(含 legacy 别名):解别名后返回规范名。
if let Some(name) = tz_canonical_name(input) {
return Some(name.to_string());
}
// 带 annotation 的日期时间以最后一个方括号内标识符为准。 // 带 annotation 的日期时间以最后一个方括号内标识符为准。
if let Some(open) = input.rfind('[') { if let Some(open) = input.rfind('[') {
if !input.ends_with(']') || open + 2 > input.len() { if !input.ends_with(']') || open + 2 > input.len() {
@@ -704,22 +766,25 @@ pub(crate) fn canonical_time_zone(value: &str) -> Option<(String, i64)> {
} }
let annotation = &input[open + 1..input.len() - 1]; let annotation = &input[open + 1..input.len() - 1];
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());
} }
return parse_offset_seconds(annotation).map(|offset| (annotation.to_string(), offset)); if parse_offset_seconds(annotation).is_some() {
return Some(annotation.to_string());
}
return tz_canonical_name(annotation).map(|name| name.to_string());
} }
let time_start = input.find(['T', 't', ' '])?; let time_start = input.find(['T', 't', ' '])?;
let time = &input[time_start + 1..]; let time = &input[time_start + 1..];
if time.ends_with(['Z', 'z']) { if time.ends_with(['Z', 'z']) {
return Some(("UTC".to_string(), 0)); return Some("UTC".to_string());
} }
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))?;
let offset_id = &time[offset_start..]; let offset_id = &time[offset_start..];
parse_offset_seconds(offset_id).map(|offset| (offset_id.to_string(), offset)) parse_offset_seconds(offset_id).map(|_| offset_id.to_string())
} }
pub(crate) fn civil_from_days(days: i128) -> (i128, i128, i128) { pub(crate) fn civil_from_days(days: i128) -> (i128, i128, i128) {
@@ -1154,6 +1219,20 @@ pub(crate) fn temporal_calendar_id_strict<H: VmHost>(vm: &mut H, value: JsValue)
temporal_calendar_id(vm, value) temporal_calendar_id(vm, value)
} }
/// 偏移秒数格式化为 `±HH:MM`;秒数非分钟整数倍时补秒段(`±HH:MM:SS`)。
///
/// # 边界与前提
/// - 输入为有符号偏移秒(|offset| ≤ 24 小时),负值取绝对值后拼负号。
pub(crate) fn format_offset_seconds_text(offset_seconds: i64) -> String {
let sign = if offset_seconds < 0 { '-' } else { '+' };
let magnitude = offset_seconds.abs();
if magnitude % 60 != 0 {
format!("{sign}{:02}:{:02}:{:02}", magnitude / 3600, (magnitude % 3600) / 60, magnitude % 60)
} else {
format!("{sign}{:02}:{:02}", magnitude / 3600, (magnitude % 3600) / 60)
}
}
/// 将 ZonedDateTime 按时区偏移转换为本地 PlainDateTime 分量。 /// 将 ZonedDateTime 按时区偏移转换为本地 PlainDateTime 分量。
pub(crate) fn zoned_date_time_plain_parts<H: VmHost>( pub(crate) fn zoned_date_time_plain_parts<H: VmHost>(
vm: &mut H, obj: &JsObject, vm: &mut H, obj: &JsObject,
@@ -1162,7 +1241,7 @@ 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_seconds) = instant_time_zone_offset(&time_zone_id) else { let Some(offset_seconds) = zone_offset_seconds(&time_zone_id, epoch_ns.div_euclid(1_000_000_000) as i64) 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;
@@ -15,7 +15,8 @@ use super::difference::{
add_date_duration, add_days_iso, compare_iso_date, date_duration_sign, date_until_iso, nudge_iso_difference, add_date_duration, add_days_iso, compare_iso_date, date_duration_sign, date_until_iso, nudge_iso_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, InstantRoundingMode};
use super::time_zone::zone_offset_seconds;
use super::zoned_date_time::{parse_any_offset_seconds, 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};
@@ -306,11 +307,11 @@ fn duration_relative_to_date<H: VmHost>(
if let Ok((year, month, day, _time_ns)) = parse_plain_date_time_string(&text) { 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)))); return Ok(Some((i128::from(year), i128::from(month), i128::from(day))));
} }
// 回退支:plain 首支解析失败的串(含 Z 的串或非法注解)。命名区当前全部 RangeError // 回退支:plain 首支解析失败的串(含 Z 的串、IANA 区注解串)。
// (canonical_time_zone 拒 IANA),可解析的带注解串已被 plain 首支接受。 // 按 instant 解析 + 注解时区偏移(epoch 依赖查找)反推墙历日期。
// 按 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_seconds = instant_time_zone_offset(&time_zone_id).unwrap_or(0); let offset_seconds = zone_offset_seconds(&time_zone_id, epoch_ns.div_euclid(1_000_000_000) as i64)
.ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))?;
let wall_ns = epoch_ns + i128::from(offset_seconds) * 1_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)));
+10 -14
View File
@@ -7,12 +7,14 @@ use oxide_types::object::JsObject;
use oxide_types::value::JsValue; use oxide_types::value::JsValue;
use super::common::native_try; use super::common::native_try;
use super::time_zone::zone_offset_seconds;
use super::{ use super::{
balance_instant_difference, canonical_time_zone, civil_from_days, days_from_civil, days_in_month, balance_instant_difference, canonical_time_zone, civil_from_days, days_from_civil, days_in_month,
duration_like_values, duration_time_nanoseconds, ensure_instant, format_iso_year, get_instant_epoch_ns, duration_like_values, duration_time_nanoseconds, ensure_instant, format_iso_year, format_offset_seconds_text,
initialize_temporal_receiver, is_ctor_call, make_duration, make_instant, make_zoned_date_time, native_engine_error, get_instant_epoch_ns, initialize_temporal_receiver, is_ctor_call, make_duration, make_instant,
parse_digits, parse_fractional_second_digits, receiver_obj, round_instant_difference, temporal_option_number, make_zoned_date_time, native_engine_error, parse_digits, parse_fractional_second_digits, receiver_obj,
temporal_option_string, temporal_option_value, FractionalSecondDigitsInput, MAX_INSTANT_NS, round_instant_difference, temporal_option_number, temporal_option_string, temporal_option_value,
FractionalSecondDigitsInput, MAX_INSTANT_NS,
}; };
pub(crate) fn instant_string_without_annotations(input: &str) -> Option<&str> { pub(crate) fn instant_string_without_annotations(input: &str) -> Option<&str> {
@@ -727,11 +729,7 @@ 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<i64> { pub(crate) fn format_instant_iso(
canonical_time_zone(value).map(|(_, offset)| offset)
}
fn format_instant_iso(
epoch_ns: i128, offset_seconds: Option<i64>, 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;
@@ -767,9 +765,7 @@ fn format_instant_iso(
} }
if let Some(offset) = offset_seconds { if let Some(offset) = offset_seconds {
let sign = if offset < 0 { '-' } else { '+' }; output.push_str(&format_offset_seconds_text(offset));
let magnitude = offset.abs();
output.push_str(&format!("{sign}{:02}:{:02}", magnitude / 3600, (magnitude % 3600) / 60));
} else { } else {
output.push('Z'); output.push('Z');
} }
@@ -857,7 +853,7 @@ pub fn instant_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
return NativeResult::Err(crate::error::create_type_error(vm, "invalid time zone")); return NativeResult::Err(crate::error::create_type_error(vm, "invalid time zone"));
} }
let time_zone = to_string(time_zone_raw); let time_zone = to_string(time_zone_raw);
match instant_time_zone_offset(&time_zone) { match zone_offset_seconds(&time_zone, epoch_ns.div_euclid(1_000_000_000) as i64) {
Some(offset) => Some(offset), Some(offset) => Some(offset),
None => return NativeResult::Err(crate::error::create_range_error(vm, "invalid time zone")), None => return NativeResult::Err(crate::error::create_range_error(vm, "invalid time zone")),
} }
@@ -900,7 +896,7 @@ pub fn instant_to_zoned_date_time_iso<H: VmHost>(vm: &mut H, args: &[u8]) -> Nat
return NativeResult::Err(crate::error::create_type_error(vm, "invalid time zone")); return NativeResult::Err(crate::error::create_type_error(vm, "invalid time zone"));
} }
let input = to_string(raw); let input = to_string(raw);
let Some((time_zone_id, _)) = canonical_time_zone(&input) else { let Some(time_zone_id) = canonical_time_zone(&input) else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid time zone")); return NativeResult::Err(crate::error::create_range_error(vm, "invalid time zone"));
}; };
make_zoned_date_time(vm, epoch_ns, &time_zone_id, "iso8601") make_zoned_date_time(vm, epoch_ns, &time_zone_id, "iso8601")
@@ -19,6 +19,8 @@ pub use plain_date_time::*;
pub use plain_month_day::*; pub use plain_month_day::*;
pub use plain_time::*; pub use plain_time::*;
pub use plain_year_month::*; pub use plain_year_month::*;
#[cfg(test)]
use time_zone::*;
pub use zoned_date_time::*; pub use zoned_date_time::*;
#[cfg(test)] #[cfg(test)]
+54 -6
View File
@@ -164,10 +164,10 @@ 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 保留原串(偏移秒值由 fixed_offset_seconds 单独提供)。
assert_eq!(canonical_time_zone("+0000"), Some(("+0000".to_string(), 0))); assert_eq!(canonical_time_zone("+0000"), Some("+0000".to_string()));
assert_eq!(canonical_time_zone("-0530"), Some(("-0530".to_string(), -19800))); assert_eq!(canonical_time_zone("-0530"), Some("-0530".to_string()));
assert_eq!(canonical_time_zone("+2330"), Some(("+2330".to_string(), 84600))); assert_eq!(canonical_time_zone("+2330"), Some("+2330".to_string()));
// 非法分钟/小时拒绝。 // 非法分钟/小时拒绝。
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);
@@ -182,8 +182,8 @@ fn offset_seconds_representation_sub_minute() {
assert_eq!(parse_any_offset_seconds("+01:00:00"), Some(3600)); 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))); assert_eq!(parse_any_offset_seconds("-23:59:59"), Some(-(23 * 3600 + 59 * 60 + 59)));
// 固定偏移秒值与分钟值 ×60 一致(±HH:MM 六字符形)。 // 固定偏移秒值与分钟值 ×60 一致(±HH:MM 六字符形)。
assert_eq!(canonical_time_zone("+01:00"), Some(("+01:00".to_string(), 3600))); assert_eq!(canonical_time_zone("+01:00"), Some("+01:00".to_string()));
assert_eq!(canonical_time_zone("-05:30"), Some(("-05:30".to_string(), -19800))); assert_eq!(canonical_time_zone("-05:30"), Some("-05:30".to_string()));
// 亚分钟偏移经 local_to_epoch_ns 与 parse_instant_string 互逆对拍。 // 亚分钟偏移经 local_to_epoch_ns 与 parse_instant_string 互逆对拍。
assert_eq!( assert_eq!(
parse_instant_string("2024-01-01T00:00:00-00:44:30"), parse_instant_string("2024-01-01T00:00:00-00:44:30"),
@@ -385,3 +385,51 @@ fn plain_time_apply_duration_ignores_date_units() {
// 忽略 days:time_delta 不含 DAY_NS 分量。 // 忽略 days:time_delta 不含 DAY_NS 分量。
assert_eq!(time_delta % 86_400_000_000_000, 9_000_000_000_000); assert_eq!(time_delta % 86_400_000_000_000, 9_000_000_000_000);
} }
#[test]
fn canonical_time_zone_iana_acceptance() {
// IANA 区名(含 legacy 别名)解到规范名;非法区名拒绝。
assert_eq!(canonical_time_zone("America/New_York"), Some("America/New_York".to_string()));
assert_eq!(canonical_time_zone("Asia/Calcutta"), Some("Asia/Kolkata".to_string()));
assert_eq!(canonical_time_zone("Etc/Ignored"), Some("Etc/UTC".to_string()));
assert_eq!(canonical_time_zone("Not/AZone"), None);
// UTC / 数值偏移臂不变(只返回规范名,不再返回偏移)。
assert_eq!(canonical_time_zone("UTC"), Some("UTC".to_string()));
assert_eq!(canonical_time_zone("Z"), Some("UTC".to_string()));
assert_eq!(canonical_time_zone("+01:00"), Some("+01:00".to_string()));
// 注解串:IANA 区注解解到规范名。
assert_eq!(canonical_time_zone("2024-01-01T00:00:00[UTC]"), Some("UTC".to_string()));
assert_eq!(
canonical_time_zone("2024-01-01T00:00:00[America/New_York]"),
Some("America/New_York".to_string())
);
}
#[test]
fn zone_offset_seconds_fixed_and_iana() {
// 固定区退化情形:偏移与 epoch 无关,恒返常量(含亚分钟偏移)。
assert_eq!(zone_offset_seconds("+05:30", 0), Some(19800));
assert_eq!(zone_offset_seconds("-00:44:30", 1_000_000_000), Some(-2670));
assert_eq!(zone_offset_seconds("UTC", 0), Some(0));
// IANA 区:1970 前后两点(EST / EDT 边界 2000-04-02T07:00Z,纽约本地 02:00)。
assert_eq!(zone_offset_seconds("America/New_York", 0), Some(-18000));
assert_eq!(zone_offset_seconds("America/New_York", 954_658_800), Some(-14400));
assert_eq!(zone_offset_seconds("America/New_York", 954_658_800 - 1), Some(-18000));
// 非法区名返回 None。
assert_eq!(zone_offset_seconds("Not/AZone", 0), None);
}
#[test]
fn sub_minute_offset_getter_branch() {
// Monrovia -00:44:30:四格式化面补秒段;整分钟偏移不补。
assert_eq!(format_offset_seconds_text(-2670), "-00:44:30");
assert_eq!(format_offset_seconds_text(2670), "+00:44:30");
assert_eq!(format_offset_seconds_text(3600), "+01:00");
assert_eq!(format_time_zone_annotation("-00:44:30", false), "[-00:44:30]");
let iso = format_zoned_date_time_iso(0, -2670, "-00:44:30", "iso8601", true, None, "auto", "auto", "auto");
assert_eq!(iso, Some("1969-12-31T23:15:30-00:44:30[-00:44:30]".to_string()));
assert_eq!(
format_instant_iso(0, Some(-2670), true, None),
Some("1969-12-31T23:15:30-00:44:30".to_string())
);
}
@@ -13,6 +13,8 @@
use std::sync::LazyLock; use std::sync::LazyLock;
use super::common::fixed_offset_seconds;
/// 单个时区的解析结果:transition 表加偏移查找所需的预存值。 /// 单个时区的解析结果:transition 表加偏移查找所需的预存值。
/// ///
/// `transitions` 为 `(纪元秒, 该时刻起生效的偏移秒)`,按纪元秒严格升序; /// `transitions` 为 `(纪元秒, 该时刻起生效的偏移秒)`,按纪元秒严格升序;
@@ -1121,7 +1123,6 @@ static ZONE_TABLE: LazyLock<ZoneTable> = LazyLock::new(|| {
/// ///
/// # 注意事项 /// # 注意事项
/// - 首次调用触发全量解析(毫秒级),之后为纯二分。 /// - 首次调用触发全量解析(毫秒级),之后为纯二分。
#[allow(dead_code)]
pub(crate) fn tz_zone_data(name: &str) -> Option<&'static TzifZone> { pub(crate) fn tz_zone_data(name: &str) -> Option<&'static TzifZone> {
let target = match LEGACY_ALIASES.binary_search_by(|a| a.0.cmp(name)) { let target = match LEGACY_ALIASES.binary_search_by(|a| a.0.cmp(name)) {
Ok(i) => LEGACY_ALIASES[i].1, Ok(i) => LEGACY_ALIASES[i].1,
@@ -1133,6 +1134,26 @@ pub(crate) fn tz_zone_data(name: &str) -> Option<&'static TzifZone> {
} }
} }
/// 按 IANA 区名(含 legacy 别名)查规范区名。
///
/// # 步骤
/// 1. 先解 legacy 别名(`LEGACY_ALIASES` 二分),未命中则原名。
/// 2. 在区表二分查找,命中返回其规范名(`&'static str`)。
///
/// # 边界与前提
/// - 别名只解一层:别名目标是现行区名,不再二次解别名。
pub(crate) fn tz_canonical_name(name: &str) -> Option<&'static str> {
let target = match LEGACY_ALIASES.binary_search_by(|a| a.0.cmp(name)) {
Ok(i) => LEGACY_ALIASES[i].1,
Err(_) => name,
};
ZONE_TABLE
.zones
.binary_search_by(|z| z.0.cmp(target))
.ok()
.map(|i| ZONE_TABLE.zones[i].0)
}
/// 查某区在 `epoch_s`(纪元秒)生效的 UTC 偏移(秒)。 /// 查某区在 `epoch_s`(纪元秒)生效的 UTC 偏移(秒)。
/// ///
/// # 步骤 /// # 步骤
@@ -1147,7 +1168,6 @@ pub(crate) fn tz_zone_data(name: &str) -> Option<&'static TzifZone> {
/// # 注意事项 /// # 注意事项
/// - 只支持正向查找(epoch → 偏移);local → epoch 的逆换算(ambiguous / /// - 只支持正向查找(epoch → 偏移);local → epoch 的逆换算(ambiguous /
/// nonexistent)由调用方迭代,不在本函数。 /// nonexistent)由调用方迭代,不在本函数。
#[allow(dead_code)]
pub(crate) fn tz_offset_seconds(zone: &str, epoch_s: i64) -> Option<i64> { pub(crate) fn tz_offset_seconds(zone: &str, epoch_s: i64) -> Option<i64> {
let z = tz_zone_data(zone)?; let z = tz_zone_data(zone)?;
if z.transitions.is_empty() { if z.transitions.is_empty() {
@@ -1163,6 +1183,23 @@ pub(crate) fn tz_offset_seconds(zone: &str, epoch_s: i64) -> Option<i64> {
} }
} }
/// 统一时区偏移查找(epoch 依赖):固定偏移区(数值偏移串)走快速路径恒返常量,
/// IANA 区走 transition 表二分。
///
/// # 步骤
/// 1. 数值偏移串(±HH:MM / ±HH / ±HHMM / ±HH:MM:SS)直接解析为秒返回。
/// 2. 未命中走 `tz_offset_seconds`(IANA 区,含 legacy 别名)。
///
/// # 边界与前提
/// - 固定偏移区是 zone-aware 路径的退化情形:偏移与 epoch 无关,恒返回常量。
/// - 非法区名(非数值偏移串且不在区表)返回 `None`。
pub(crate) fn zone_offset_seconds(zone: &str, epoch_s: i64) -> Option<i64> {
if let Some(offset) = fixed_offset_seconds(zone) {
return Some(offset);
}
tz_offset_seconds(zone, epoch_s)
}
/// 判断 `name` 是否为可接受的 IANA 区名(含 legacy 别名)。 /// 判断 `name` 是否为可接受的 IANA 区名(含 legacy 别名)。
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) fn tz_is_iana_zone(name: &str) -> bool { pub(crate) fn tz_is_iana_zone(name: &str) -> bool {
@@ -8,19 +8,20 @@ use oxide_types::value::JsValue;
use super::common::native_try; use super::common::native_try;
use super::common::{ use super::common::{
canonical_time_zone, civil_from_days, days_from_civil, days_in_month, ensure_zoned_date_time, format_iso_year, canonical_time_zone, civil_from_days, days_from_civil, days_in_month, ensure_zoned_date_time, fixed_offset_seconds,
get_calendar_id, get_double_prop, get_instant_epoch_ns, initialize_temporal_receiver, is_ctor_call, format_iso_year, format_offset_seconds_text, get_calendar_id, get_double_prop, get_instant_epoch_ns,
local_to_epoch_ns, make_instant, make_zoned_date_time, parse_digits, parse_fractional_second_digits, initialize_temporal_receiver, is_ctor_call, local_to_epoch_ns, make_instant, make_zoned_date_time, parse_digits,
parse_temporal_string_impl, plain_date_time_object_parts, receiver_obj, parse_fractional_second_digits, parse_temporal_string_impl, plain_date_time_object_parts, receiver_obj,
reject_partial_object_with_calendar_or_time_zone, temporal_calendar_id, temporal_calendar_id_strict, reject_partial_object_with_calendar_or_time_zone, temporal_calendar_id, temporal_calendar_id_strict,
temporal_option_number, temporal_option_string, temporal_option_value, temporal_overflow, temporal_string_strict, temporal_option_number, temporal_option_string, temporal_option_value, temporal_overflow, temporal_string_strict,
zoned_date_time_plain_parts, FractionalSecondDigitsInput, MAX_INSTANT_NS, zoned_date_time_plain_parts, FractionalSecondDigitsInput, MAX_INSTANT_NS,
}; };
use super::difference::{difference_core, parse_difference_settings, MAX_ISO_DAY}; use super::difference::{difference_core, parse_difference_settings, MAX_ISO_DAY};
use super::instant::{ use super::instant::{
instant_rounding_mode, instant_string_without_annotations, instant_time_zone_offset, parse_instant_string, instant_rounding_mode, instant_string_without_annotations, parse_instant_string, primitive_to_bigint,
primitive_to_bigint, round_instant_ns, round_instant_ns,
}; };
use super::time_zone::zone_offset_seconds;
use super::{ use super::{
days_in_month_iso, duration_component_integer, duration_like_values, is_leap_year_iso, make_duration, days_in_month_iso, duration_component_integer, duration_like_values, is_leap_year_iso, make_duration,
make_plain_date, make_plain_date_time, make_plain_time, plain_time_components, plain_time_like_ns, valid_iso_date, make_plain_date, make_plain_date_time, make_plain_time, plain_time_components, plain_time_like_ns, valid_iso_date,
@@ -45,7 +46,7 @@ pub fn zoned_date_time_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> Native
return NativeResult::Err(crate::error::create_type_error(vm, "invalid time zone")); return NativeResult::Err(crate::error::create_type_error(vm, "invalid time zone"));
} }
let time_zone_input = to_string(vm.reg(args[2])); let time_zone_input = to_string(vm.reg(args[2]));
let Some((time_zone_id, _)) = canonical_time_zone(&time_zone_input) else { let Some(time_zone_id) = canonical_time_zone(&time_zone_input) else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid time zone")); return NativeResult::Err(crate::error::create_range_error(vm, "invalid time zone"));
}; };
let calendar = if args.len() >= 4 && !vm.reg(args[3]).is_undefined() { let calendar = if args.len() >= 4 && !vm.reg(args[3]).is_undefined() {
@@ -295,10 +296,11 @@ fn zoned_date_time_like_epoch_ns<H: VmHost>(
/// 字符串分支:instant 解析 + 时区注解提取,按 offset 选项决定最终 epoch。 /// 字符串分支:instant 解析 + 时区注解提取,按 offset 选项决定最终 epoch。
/// ///
/// # 步骤 /// # 步骤
/// 1. extract_time_zone_annotation 取注解 → canonical_time_zone 得时区 ID 与偏移。 /// 1. extract_time_zone_annotation 取注解 → canonical_time_zone 得时区 ID。
/// 2. 主体剥注解后按 Z / 数值偏移 / 无偏移分路:Z 定 exact time,有偏移经 parse_instant_string /// 2. 主体剥注解后按 Z / 数值偏移 / 无偏移分路:Z 定 exact time,有偏移经 parse_instant_string
/// 反推墙钟,无偏移直接解析墙钟。 /// 反推墙钟,无偏移直接解析墙钟。
/// 3. 按 offset 选项 use/ignore/prefer/reject 决策 epoch,并做 Instant 范围校验。 /// 3. 时区偏移按候选 epoch(墙钟按 UTC 解释)单次查找。
/// 4. 按 offset 选项 use/ignore/prefer/reject 决策 epoch,并做 Instant 范围校验。
/// ///
/// # 边界与前提 /// # 边界与前提
/// - 字符串缺时区注解或注解无法规范化抛 RangeError;主体非法抛 RangeError。 /// - 字符串缺时区注解或注解无法规范化抛 RangeError;主体非法抛 RangeError。
@@ -309,9 +311,8 @@ pub(crate) fn zoned_date_time_string_parts<H: VmHost>(
let Some((tz_annotation, _critical)) = extract_time_zone_annotation(input) else { let Some((tz_annotation, _critical)) = extract_time_zone_annotation(input) else {
return Err(crate::error::create_range_error(vm, "invalid time zone")); return Err(crate::error::create_range_error(vm, "invalid time zone"));
}; };
let Some((time_zone_id, time_zone_offset)) = canonical_time_zone(&tz_annotation) else { let time_zone_id =
return Err(crate::error::create_range_error(vm, "invalid time zone")); canonical_time_zone(&tz_annotation).ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))?;
};
let Some(body) = instant_string_without_annotations(input) else { let Some(body) = instant_string_without_annotations(input) else {
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"));
}; };
@@ -342,6 +343,11 @@ pub(crate) fn zoned_date_time_string_parts<H: VmHost>(
let wall_epoch = let wall_epoch =
|offset_seconds: i64| local_to_epoch_ns(wall_parts.0, wall_parts.1, wall_parts.2, wall_parts.3, offset_seconds); |offset_seconds: i64| local_to_epoch_ns(wall_parts.0, wall_parts.1, wall_parts.2, wall_parts.3, offset_seconds);
// 时区偏移是 epoch 的函数:候选 epoch 取墙钟按 UTC 解释的单次查找近似。
let candidate_ns = wall_epoch(0).ok_or_else(|| crate::error::create_range_error(vm, "invalid date-time"))?;
let time_zone_offset = zone_offset_seconds(&time_zone_id, candidate_ns.div_euclid(1_000_000_000) as i64)
.ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))?;
// 按 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 {
@@ -395,9 +401,8 @@ fn zoned_date_time_bag_parts<H: VmHost>(
return Err(crate::error::create_type_error(vm, "timeZone is required")); return Err(crate::error::create_type_error(vm, "timeZone is required"));
} }
let time_zone_input = temporal_option_string(vm, time_zone_raw)?; let time_zone_input = temporal_option_string(vm, time_zone_raw)?;
let Some((time_zone_id, time_zone_offset)) = canonical_time_zone(&time_zone_input) else { let time_zone_id = canonical_time_zone(&time_zone_input)
return Err(crate::error::create_range_error(vm, "invalid time zone")); .ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))?;
};
// offset 可选:语法校验(ToOffsetString 语义)先于数值字段转换。 // offset 可选:语法校验(ToOffsetString 语义)先于数值字段转换。
let offset_raw = temporal_option_value(vm, obj, value, "offset")?; let offset_raw = temporal_option_value(vm, obj, value, "offset")?;
@@ -406,7 +411,7 @@ fn zoned_date_time_bag_parts<H: VmHost>(
} else { } else {
let offset_input = temporal_option_string(vm, offset_raw)?; let offset_input = temporal_option_string(vm, offset_raw)?;
Some( Some(
instant_time_zone_offset(&offset_input) fixed_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"))?,
) )
}; };
@@ -414,6 +419,12 @@ fn zoned_date_time_bag_parts<H: VmHost>(
// 年月日字段与 calendar:内部先读 calendar 再按字母序读字段并做类型转换校验。 // 年月日字段与 calendar:内部先读 calendar 再按字母序读字段并做类型转换校验。
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)?;
// 时区偏移是 epoch 的函数:候选 epoch 取墙钟按 UTC 解释的单次查找近似。
let candidate_ns = local_to_epoch_ns(year, month, day, total_ns, 0)
.ok_or_else(|| crate::error::create_range_error(vm, "invalid date-time"))?;
let time_zone_offset = zone_offset_seconds(&time_zone_id, candidate_ns.div_euclid(1_000_000_000) as i64)
.ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))?;
// 按 offset 选项决定 epoch(InterpretISODateTimeOffset 固定偏移简化:候选恒唯一)。 // 按 offset 选项决定 epoch(InterpretISODateTimeOffset 固定偏移简化:候选恒唯一)。
let epoch_ns = match (bag_offset_seconds, 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),
@@ -482,13 +493,11 @@ pub fn zoned_date_time_compare<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResu
})) }))
} }
/// 时区注解文本:命名区原样返回,偏移区规范化为 `±HH:MM` 带冒号;critical 时 `!` 置于括号内。 /// 时区注解文本:命名区原样返回,偏移区规范化为 `±HH:MM` 带冒号(秒非零时 `±HH:MM:SS`);
/// 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_seconds = instant_time_zone_offset(time_zone_id).unwrap_or(0); format_offset_seconds_text(fixed_offset_seconds(time_zone_id).unwrap_or(0))
let sign = if offset_seconds < 0 { '-' } else { '+' };
let magnitude = offset_seconds.abs();
format!("{sign}{:02}:{:02}", magnitude / 3600, (magnitude % 3600) / 60)
} else { } else {
time_zone_id.to_string() time_zone_id.to_string()
}; };
@@ -504,7 +513,7 @@ pub(crate) fn format_time_zone_annotation(time_zone_id: &str, critical: bool) ->
/// # 边界与前提 /// # 边界与前提
/// - epoch_ns 须已按 quantum 舍入(调用方保证),此处仅做本地墙钟分解。 /// - epoch_ns 须已按 quantum 舍入(调用方保证),此处仅做本地墙钟分解。
/// - offset_name/time_zone_name 取 auto/never/critical,calendar_name 取 auto/never/always/critical。 /// - offset_name/time_zone_name 取 auto/never/critical,calendar_name 取 auto/never/always/critical。
/// - 偏移段恒 `±HH:MM` 带冒号;时区注解命名区原样、偏移区规范化。 /// - 偏移段 `±HH:MM` 带冒号(秒非零时 `±HH:MM:SS`);时区注解命名区原样、偏移区规范化。
#[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_seconds: i64, 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,
@@ -544,12 +553,10 @@ 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_seconds < 0 { '-' } else { '+' };
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 / 3600, (magnitude % 3600) / 60)); output.push_str(&format_offset_seconds_text(offset_seconds));
} }
// timeZoneName 注解:auto 显示,never 省略,critical 时 `!` 置于括号内。 // timeZoneName 注解:auto 显示,never 省略,critical 时 `!` 置于括号内。
@@ -736,7 +743,7 @@ pub fn zoned_date_time_with_time_zone<H: VmHost>(vm: &mut H, args: &[u8]) -> Nat
return NativeResult::Err(crate::error::create_type_error(vm, "invalid time zone")); return NativeResult::Err(crate::error::create_type_error(vm, "invalid time zone"));
} }
let input = to_string(raw); let input = to_string(raw);
let Some((time_zone_id, _)) = canonical_time_zone(&input) else { let Some(time_zone_id) = canonical_time_zone(&input) else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid time zone")); return NativeResult::Err(crate::error::create_range_error(vm, "invalid time zone"));
}; };
make_zoned_date_time(vm, epoch_ns, &time_zone_id, &calendar_id) make_zoned_date_time(vm, epoch_ns, &time_zone_id, &calendar_id)
@@ -1207,7 +1214,10 @@ 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_seconds = instant_time_zone_offset(default_tz_id) // 时区偏移是 epoch 的函数:候选 epoch 取墙钟按 UTC 解释的单次查找近似。
let candidate_ns = local_to_epoch_ns(year, month, day, total_ns, 0)
.ok_or_else(|| crate::error::create_range_error(vm, "invalid date-time"))?;
let offset_seconds = zone_offset_seconds(default_tz_id, candidate_ns.div_euclid(1_000_000_000) as i64)
.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_seconds) 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"))
@@ -1224,15 +1234,14 @@ fn zoned_date_time_other_bag_epoch_ns<H: VmHost>(
vm: &mut H, value: JsValue, obj: &JsObject, default_tz_id: &str, vm: &mut H, value: JsValue, obj: &JsObject, default_tz_id: &str,
) -> Result<i128, JsValue> { ) -> Result<i128, JsValue> {
let time_zone_raw = temporal_option_value(vm, obj, value, "timeZone")?; let time_zone_raw = temporal_option_value(vm, obj, value, "timeZone")?;
let time_zone_offset = if time_zone_raw.is_undefined() { let time_zone_id = if time_zone_raw.is_undefined() {
instant_time_zone_offset(default_tz_id) default_tz_id.to_string()
.ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))?
} else { } else {
if !time_zone_raw.is_string() { if !time_zone_raw.is_string() {
return Err(crate::error::create_type_error(vm, "invalid time zone")); return Err(crate::error::create_type_error(vm, "invalid time zone"));
} }
let input = to_string(time_zone_raw); canonical_time_zone(&to_string(time_zone_raw))
instant_time_zone_offset(&input).ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))? .ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))?
}; };
let offset_raw = temporal_option_value(vm, obj, value, "offset")?; let offset_raw = temporal_option_value(vm, obj, value, "offset")?;
@@ -1244,12 +1253,17 @@ fn zoned_date_time_other_bag_epoch_ns<H: VmHost>(
} }
let offset_input = to_string(offset_raw); let offset_input = to_string(offset_raw);
Some( Some(
instant_time_zone_offset(&offset_input) fixed_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"))?,
) )
}; };
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)?;
// 时区偏移是 epoch 的函数:候选 epoch 取墙钟按 UTC 解释的单次查找近似。
let candidate_ns = local_to_epoch_ns(year, month, day, total_ns, 0)
.ok_or_else(|| crate::error::create_range_error(vm, "invalid date-time"))?;
let time_zone_offset = zone_offset_seconds(&time_zone_id, candidate_ns.div_euclid(1_000_000_000) as i64)
.ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))?;
if let Some(offset) = bag_offset_seconds { 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"));
@@ -1266,7 +1280,7 @@ fn zoned_date_time_other_bag_epoch_ns<H: VmHost>(
/// ZDT 差值核心:两 ZDT 的 epoch 差 → receiver 时区下的本地墙钟分量差 → difference_core。 /// ZDT 差值核心:两 ZDT 的 epoch 差 → receiver 时区下的本地墙钟分量差 → difference_core。
/// ///
/// # 步骤 /// # 步骤
/// 1. receiver 三槽读取:epoch、offset 分钟、时区 ID,本地分量经 zoned_date_time_plain_parts。 /// 1. receiver 三槽读取:epoch、偏移秒、时区 ID,本地分量经 zoned_date_time_plain_parts。
/// 2. other → epoch(zoned_date_time_other_epoch_ns,receiver 时区作 bag 默认)。 /// 2. other → epoch(zoned_date_time_other_epoch_ns,receiver 时区作 bag 默认)。
/// 3. epoch 相等快速路径:先于任何分量计算返回全零时长。 /// 3. epoch 相等快速路径:先于任何分量计算返回全零时长。
/// 4. other 本地分量:epoch + offset → div_euclid/rem_euclid 拆墙钟(receiver 时区)。 /// 4. other 本地分量:epoch + offset → div_euclid/rem_euclid 拆墙钟(receiver 时区)。
@@ -1342,7 +1356,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_seconds; /// 2. receiver:ensure_zoned_date_time → epoch_r;zone_offset_seconds(tz, epoch_r) 得 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 +1377,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_seconds) = instant_time_zone_offset(&time_zone_id) else { let Some(offset_seconds) = zone_offset_seconds(&time_zone_id, epoch_r.div_euclid(1_000_000_000) as i64) 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);
@@ -1569,21 +1583,24 @@ 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 时区偏移(秒):epoch 依赖查找(槽 0 epoch 秒 + 槽 1 时区名 → zone_offset_seconds)。
/// 调用方须先 ensure_zoned_date_time。
fn zoned_date_time_offset_seconds<H: VmHost>(vm: &mut H, obj: &JsObject) -> Result<i64, JsValue> { fn zoned_date_time_offset_seconds<H: VmHost>(vm: &mut H, obj: &JsObject) -> Result<i64, JsValue> {
let Some(epoch_ns) = get_instant_epoch_ns(obj) else {
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));
instant_time_zone_offset(&time_zone_id).ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone")) zone_offset_seconds(&time_zone_id, epoch_ns.div_euclid(1_000_000_000) as i64)
.ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))
} }
/// `Temporal.ZonedDateTime.prototype.offset`:由偏移秒数规范化为 ±HH:MM 字符串。 /// `Temporal.ZonedDateTime.prototype.offset`:由偏移秒数规范化为 `±HH:MM` 字符串(秒非零时 `±HH:MM:SS`)。
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_seconds = native_try!(zoned_date_time_offset_seconds(vm, obj)); let offset_seconds = native_try!(zoned_date_time_offset_seconds(vm, obj));
let sign = if offset_seconds < 0 { '-' } else { '+' }; NativeResult::Ok(vm.new_string(&format_offset_seconds_text(offset_seconds)))
let magnitude = offset_seconds.abs();
NativeResult::Ok(vm.new_string(&format!("{sign}{:02}:{:02}", magnitude / 3600, (magnitude % 3600) / 60)))
} }
/// `Temporal.ZonedDateTime.prototype.offsetNanoseconds`:偏移秒数换算纳秒(f64 精确域内)。 /// `Temporal.ZonedDateTime.prototype.offsetNanoseconds`:偏移秒数换算纳秒(f64 精确域内)。