mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
refactor: temporal 偏移表示由 i32 分钟升级为 i64 秒
This commit is contained in:
@@ -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();
|
||||
if bytes.len() != 6 || !matches!(bytes[0], b'+' | b'-') || bytes[3] != b':' {
|
||||
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) {
|
||||
return None;
|
||||
}
|
||||
let hours = i32::from(bytes[1] - b'0') * 10 + i32::from(bytes[2] - b'0');
|
||||
let minutes = i32::from(bytes[4] - b'0') * 10 + i32::from(bytes[5] - b'0');
|
||||
let hours = i64::from(bytes[1] - b'0') * 10 + i64::from(bytes[2] - b'0');
|
||||
let minutes = i64::from(bytes[4] - b'0') * 10 + i64::from(bytes[5] - b'0');
|
||||
if hours > 23 || minutes > 59 {
|
||||
return None;
|
||||
}
|
||||
let magnitude = hours * 60 + minutes;
|
||||
let magnitude = hours * 3600 + minutes * 60;
|
||||
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();
|
||||
if input.eq_ignore_ascii_case("UTC") || input.eq_ignore_ascii_case("Z") {
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -672,10 +672,10 @@ pub(crate) fn canonical_time_zone(value: &str) -> Option<(String, i32)> {
|
||||
&& matches!(input.as_bytes()[0], b'+' | b'-')
|
||||
&& 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 {
|
||||
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;
|
||||
}
|
||||
@@ -685,11 +685,11 @@ pub(crate) fn canonical_time_zone(value: &str) -> Option<(String, i32)> {
|
||||
&& matches!(input.as_bytes()[0], b'+' | b'-')
|
||||
&& 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 minute = (input.as_bytes()[3] - b'0') as i32 * 10 + (input.as_bytes()[4] - 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 i64 * 10 + (input.as_bytes()[4] - b'0') as i64;
|
||||
if hour <= 23 && minute <= 59 {
|
||||
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;
|
||||
}
|
||||
@@ -706,7 +706,7 @@ pub(crate) fn canonical_time_zone(value: &str) -> Option<(String, i32)> {
|
||||
if annotation.eq_ignore_ascii_case("UTC") {
|
||||
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', ' '])?;
|
||||
@@ -719,7 +719,7 @@ pub(crate) fn canonical_time_zone(value: &str) -> Option<(String, i32)> {
|
||||
.rev()
|
||||
.find_map(|(index, ch)| matches!(ch, '+' | '-').then_some(index))?;
|
||||
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) {
|
||||
@@ -1162,11 +1162,11 @@ pub(crate) fn zoned_date_time_plain_parts<H: VmHost>(
|
||||
return Err(crate::error::create_range_error(vm, "invalid ZonedDateTime"));
|
||||
};
|
||||
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"));
|
||||
};
|
||||
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 time_ns = local_ns.rem_euclid(DAY_NS) as f64;
|
||||
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))
|
||||
}
|
||||
|
||||
/// 本地墙钟分量 + 时区偏移(分钟)→ 纪元纳秒(zoned_date_time_plain_parts 的逆)。
|
||||
/// 本地墙钟分量 + 时区偏移(秒)→ 纪元纳秒(zoned_date_time_plain_parts 的逆)。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - (year, month, day, time_ns) 须已通过 valid_iso_date / valid_plain_time 校验(调用方保证)。
|
||||
/// - 仅做 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));
|
||||
days.checked_mul(86_400_000_000_000)?
|
||||
.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 {
|
||||
|
||||
@@ -16,7 +16,7 @@ use super::difference::{
|
||||
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::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};
|
||||
|
||||
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 首支接受。
|
||||
// 按 instant 解析 + 注解时区反推墙钟日期。
|
||||
let (epoch_ns, time_zone_id, _calendar) = zoned_date_time_string_parts(vm, &text, "reject")?;
|
||||
let offset_minutes = instant_time_zone_offset(&time_zone_id).unwrap_or(0);
|
||||
let wall_ns = epoch_ns + i128::from(offset_minutes) * 60_000_000_000;
|
||||
let offset_seconds = instant_time_zone_offset(&time_zone_id).unwrap_or(0);
|
||||
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));
|
||||
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"));
|
||||
}
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -727,15 +727,15 @@ pub fn instant_since<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
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)
|
||||
}
|
||||
|
||||
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> {
|
||||
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 days = local_ns.div_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 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 {
|
||||
output.push('Z');
|
||||
}
|
||||
@@ -850,7 +850,7 @@ pub fn instant_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
None => (1, true, None),
|
||||
},
|
||||
};
|
||||
let offset_minutes = if time_zone_raw.is_undefined() {
|
||||
let offset_seconds = if time_zone_raw.is_undefined() {
|
||||
None
|
||||
} else {
|
||||
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 {
|
||||
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)),
|
||||
None => NativeResult::Err(crate::error::create_range_error(vm, "invalid Instant")),
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use super::common::native_try;
|
||||
use super::{
|
||||
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,
|
||||
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,
|
||||
temporal_number_component, temporal_option_number, temporal_option_string, temporal_option_value,
|
||||
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..]),
|
||||
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;
|
||||
}
|
||||
parse_plain_clock(clock)
|
||||
|
||||
@@ -78,16 +78,16 @@ fn calendar_id_slot_fallback() {
|
||||
fn start_of_day_epoch_ns_instant_range() {
|
||||
// 当地午夜回推 epoch 越 Instant 界(±MAX)返回 None,供 startOfDay/hoursInDay RangeError 依据。
|
||||
// -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,在边界内。
|
||||
assert_eq!(start_of_day_epoch_ns(-271_821, 4, 20, 0), Some(-8_640_000_000_000_000_000_000));
|
||||
// 同日期 +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)。
|
||||
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, 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]
|
||||
@@ -104,20 +104,20 @@ fn format_time_zone_annotation_normalizes_offset() {
|
||||
#[test]
|
||||
fn format_zoned_date_time_iso_annotations() {
|
||||
// 偏移/时区/日历注解各取值组合,验证段序 `{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()));
|
||||
// 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()));
|
||||
// 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()));
|
||||
// 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()));
|
||||
// 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()));
|
||||
}
|
||||
|
||||
@@ -132,14 +132,17 @@ fn format_zoned_date_time_iso_epoch_rounding_cross_midnight() {
|
||||
#[test]
|
||||
fn local_to_epoch_ns_roundtrip() {
|
||||
// 与 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!(
|
||||
parse_instant_string("1969-07-16T13:32:01.234567891Z"),
|
||||
local_to_epoch_ns(1969, 7, 16, 48_721_234_567_891.0, 0),
|
||||
);
|
||||
// startOfDay 最小边界:-271821-04-20 加 1h 再回推 1h 偏移回到 -MAX。
|
||||
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),
|
||||
);
|
||||
}
|
||||
@@ -161,16 +164,37 @@ fn zoned_date_time_string_wall_day_range_boundary() {
|
||||
|
||||
#[test]
|
||||
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("-0530"), Some(("-0530".to_string(), -330)));
|
||||
assert_eq!(canonical_time_zone("+2330"), Some(("+2330".to_string(), 1410)));
|
||||
assert_eq!(canonical_time_zone("-0530"), Some(("-0530".to_string(), -19800)));
|
||||
assert_eq!(canonical_time_zone("+2330"), Some(("+2330".to_string(), 84600)));
|
||||
// 非法分钟/小时拒绝。
|
||||
assert_eq!(canonical_time_zone("+2400"), None);
|
||||
assert_eq!(canonical_time_zone("+0060"), 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]
|
||||
fn extract_time_zone_annotation_forms() {
|
||||
// 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 rounded = round_instant_ns(55_410_123_456_789, quantum, InstantRoundingMode::HalfExpand).unwrap();
|
||||
assert_eq!(rounded, 57_600_000_000_000);
|
||||
// 本地墙钟日回推:offset 60 分,local_to_epoch_ns 得目标 epoch。
|
||||
let epoch = local_to_epoch_ns(1976, 11, 18, rounded as f64, 60).unwrap();
|
||||
// 本地墙钟日回推:offset 3600 秒,local_to_epoch_ns 得目标 epoch。
|
||||
let epoch = local_to_epoch_ns(1976, 11, 18, rounded as f64, 3600).unwrap();
|
||||
assert_eq!(epoch, 217_177_200_000_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoned_date_time_round_day_path_epoch() {
|
||||
// 同日本地午夜 startNs(2513 天,offset 60 分),dayProgress 舍到次日 → 217206000000000000。
|
||||
let start_ns = start_of_day_epoch_ns(1976, 11, 18, 60).unwrap();
|
||||
// 同日本地午夜 startNs(2513 天,offset 3600 秒),dayProgress 舍到次日 → 217206000000000000。
|
||||
let start_ns = start_of_day_epoch_ns(1976, 11, 18, 3600).unwrap();
|
||||
assert_eq!(start_ns, 217_119_600_000_000_000);
|
||||
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();
|
||||
|
||||
@@ -208,24 +208,24 @@ fn zoned_date_time_options<H: VmHost>(
|
||||
Ok((offset, disambiguation))
|
||||
}
|
||||
|
||||
/// 从剥注解后的 Instant 主体提取字符串内数值偏移分钟数(±HH / ±HHMM / ±HH:MM / 亚秒形式)。
|
||||
/// Z/z 结尾视为 0 分钟;无偏移或偏移不可提取返回 None。
|
||||
fn extract_string_offset_minutes(body: &str) -> Option<i32> {
|
||||
/// 从剥注解后的 Instant 主体提取字符串内数值偏移秒数(±HH / ±HHMM / ±HH:MM / 亚秒形式)。
|
||||
/// Z/z 结尾视为 0 秒;无偏移或偏移不可提取返回 None。
|
||||
fn extract_string_offset_seconds(body: &str) -> Option<i64> {
|
||||
let time_start = body.find(['T', 't', ' '])?;
|
||||
let time = &body[time_start + 1..];
|
||||
let offset_start = time
|
||||
.char_indices()
|
||||
.rev()
|
||||
.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,可带小数秒)为分钟数。
|
||||
pub(crate) fn parse_any_offset_minutes(value: &str) -> Option<i32> {
|
||||
/// 解析数值偏移串(±HH / ±HHMM / ±HH:MM / ±HHMMSS / ±HH:MM:SS,可带小数秒)为秒数。
|
||||
pub(crate) fn parse_any_offset_seconds(value: &str) -> Option<i64> {
|
||||
let bytes = value.as_bytes();
|
||||
let sign = match bytes.first() {
|
||||
Some(b'+') => 1_i32,
|
||||
Some(b'-') => -1_i32,
|
||||
Some(b'+') => 1_i64,
|
||||
Some(b'-') => -1_i64,
|
||||
_ => return None,
|
||||
};
|
||||
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 {
|
||||
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) 三元组。
|
||||
@@ -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"));
|
||||
};
|
||||
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 并反推墙钟;否则直接解析墙钟。
|
||||
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)
|
||||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid ISO 8601 date-time"))?;
|
||||
let offset_minutes = string_offset_minutes.unwrap_or(0);
|
||||
let wall_ns = epoch_ns + i128::from(offset_minutes) * 60_000_000_000;
|
||||
let offset_seconds = string_offset_seconds.unwrap_or(0);
|
||||
let wall_ns = epoch_ns + i128::from(offset_seconds) * 1_000_000_000;
|
||||
let days = wall_ns.div_euclid(DAY_NS);
|
||||
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))
|
||||
@@ -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"));
|
||||
}
|
||||
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(墙钟 + 时区偏移);
|
||||
// 有偏移 → 按 offset 选项在字符串偏移与时区偏移之间选择。
|
||||
let epoch_ns = if has_utc_designator {
|
||||
epoch_from_string
|
||||
} else {
|
||||
match string_offset_minutes {
|
||||
match string_offset_seconds {
|
||||
None => wall_epoch(time_zone_offset),
|
||||
Some(offset) => match offset_mode {
|
||||
"use" => epoch_from_string,
|
||||
@@ -401,7 +401,7 @@ fn zoned_date_time_bag_parts<H: VmHost>(
|
||||
|
||||
// offset 可选:语法校验(ToOffsetString 语义)先于数值字段转换。
|
||||
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
|
||||
} else {
|
||||
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)?;
|
||||
|
||||
// 按 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),
|
||||
(Some(offset), "use") => local_to_epoch_ns(year, month, day, total_ns, offset),
|
||||
(Some(offset), "prefer") => {
|
||||
@@ -485,10 +485,10 @@ pub fn zoned_date_time_compare<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResu
|
||||
/// 时区注解文本:命名区原样返回,偏移区规范化为 `±HH:MM` 带冒号;critical 时 `!` 置于括号内。
|
||||
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 offset_minutes = instant_time_zone_offset(time_zone_id).unwrap_or(0);
|
||||
let sign = if offset_minutes < 0 { '-' } else { '+' };
|
||||
let magnitude = offset_minutes.abs();
|
||||
format!("{sign}{:02}:{:02}", magnitude / 60, magnitude % 60)
|
||||
let offset_seconds = instant_time_zone_offset(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 {
|
||||
time_zone_id.to_string()
|
||||
};
|
||||
@@ -507,11 +507,11 @@ pub(crate) fn format_time_zone_annotation(time_zone_id: &str, critical: bool) ->
|
||||
/// - 偏移段恒 `±HH:MM` 带冒号;时区注解命名区原样、偏移区规范化。
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
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,
|
||||
) -> Option<String> {
|
||||
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 days = local_ns.div_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 段前加 !。
|
||||
if offset_name != "never" {
|
||||
let sign = if offset_minutes < 0 { '-' } else { '+' };
|
||||
let magnitude = offset_minutes.abs();
|
||||
let sign = if offset_seconds < 0 { '-' } else { '+' };
|
||||
let magnitude = offset_seconds.abs();
|
||||
if offset_name == "critical" {
|
||||
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 时 `!` 置于括号内。
|
||||
@@ -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"));
|
||||
};
|
||||
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 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(
|
||||
rounded_ns,
|
||||
offset_minutes,
|
||||
offset_seconds,
|
||||
&time_zone_id,
|
||||
&calendar_id,
|
||||
include_seconds,
|
||||
@@ -783,7 +783,7 @@ struct ZdtMergedFields {
|
||||
microsecond: f64,
|
||||
nanosecond: f64,
|
||||
month_code: Option<(f64, bool)>,
|
||||
bag_offset: Option<i32>,
|
||||
bag_offset: Option<i64>,
|
||||
}
|
||||
|
||||
/// with 的部分字段读取与合并:按字典序读 bag 字段,与 receiver 默认分量合并。
|
||||
@@ -791,7 +791,7 @@ struct ZdtMergedFields {
|
||||
/// # 步骤
|
||||
/// 1. 数值字段 ToNumber→trunc(NaN/±Inf RangeError,day 额外拒绝 <1);monthCode/offset 走 ToString。
|
||||
/// 2. undefined 不覆盖;至少一个字段有定义否则 TypeError。
|
||||
/// 3. 返回合并后的原始分量(未钳制)+ monthCode 解析 + bag offset 分钟。
|
||||
/// 3. 返回合并后的原始分量(未钳制)+ monthCode 解析 + bag offset 秒。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 调用方须先完成 RejectObjectWithCalendarOrTimeZone(calendar/timeZone 已拒绝)。
|
||||
@@ -888,17 +888,17 @@ fn zoned_date_time_with_fields<H: VmHost>(
|
||||
};
|
||||
|
||||
// offset 先经 ToString,再按偏移字符串语法校验(小数秒最多 9 位),解析出的
|
||||
// 分钟数留给 offset 选项决策使用。
|
||||
// 秒数留给 offset 选项决策使用。
|
||||
let bag_offset = if offset_raw.is_undefined() {
|
||||
None
|
||||
} else {
|
||||
let offset_input = temporal_string_strict(vm, offset_raw)?;
|
||||
let minutes = parse_any_offset_minutes(&offset_input)
|
||||
let seconds = parse_any_offset_seconds(&offset_input)
|
||||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid offset"))?;
|
||||
if !valid_offset_fraction(&offset_input) {
|
||||
return Err(crate::error::create_range_error(vm, "invalid offset"));
|
||||
}
|
||||
Some(minutes)
|
||||
Some(seconds)
|
||||
};
|
||||
|
||||
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));
|
||||
|
||||
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 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 偏移与时区偏移间选择。
|
||||
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(_), "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") => {
|
||||
if offset == offset_minutes {
|
||||
if offset == offset_seconds {
|
||||
local_to_epoch_ns(year, month, day, total_ns, offset)
|
||||
} 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") => {
|
||||
if offset == offset_minutes {
|
||||
if offset == offset_seconds {
|
||||
local_to_epoch_ns(year, month, day, total_ns, offset)
|
||||
} else {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "offset and time zone disagree"));
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/// 校验 offset 串的小数秒位数 ≤9(parse_any_offset_minutes 不限制位数,此处补查)。
|
||||
/// 校验 offset 串的小数秒位数 ≤9(parse_any_offset_seconds 不限制位数,此处补查)。
|
||||
pub(crate) fn valid_offset_fraction(value: &str) -> bool {
|
||||
let Some(dot) = value.find(['.', ',']) else {
|
||||
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 };
|
||||
native_try!(ensure_zoned_date_time(vm, obj));
|
||||
let (year, month, day, _) = native_try!(zoned_date_time_plain_parts(vm, obj));
|
||||
let offset_minutes = native_try!(zoned_date_time_offset_minutes(vm, obj));
|
||||
let 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 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"));
|
||||
let epoch_ns = native_try!(epoch_ns);
|
||||
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) {
|
||||
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"))?;
|
||||
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"))
|
||||
}
|
||||
|
||||
@@ -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 bag_offset_minutes = if offset_raw.is_undefined() {
|
||||
let bag_offset_seconds = if offset_raw.is_undefined() {
|
||||
None
|
||||
} else {
|
||||
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)?;
|
||||
if let Some(offset) = bag_offset_minutes {
|
||||
if let Some(offset) = bag_offset_seconds {
|
||||
if offset != time_zone_offset {
|
||||
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 {
|
||||
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 (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 保持非负余数)。
|
||||
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 (y_o, m_o, d_o) = civil_from_days(days_o);
|
||||
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
|
||||
/// 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)。
|
||||
/// 3. 单位表查 smallestUnit(None → RangeError "invalid smallest unit")。
|
||||
/// 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"));
|
||||
};
|
||||
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"));
|
||||
};
|
||||
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;
|
||||
let result_ns = if unit_ns == DAY_NS {
|
||||
// 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")));
|
||||
native_try!(start_of_day_epoch_ns_by_days(
|
||||
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")));
|
||||
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 {
|
||||
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"));
|
||||
};
|
||||
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_year, JsValue::undefined());
|
||||
|
||||
/// 读 ZDT 时区偏移(分钟);槽 1 解析失败返回 RangeError。调用方须先 ensure_zoned_date_time。
|
||||
fn zoned_date_time_offset_minutes<H: VmHost>(vm: &mut H, obj: &JsObject) -> Result<i32, JsValue> {
|
||||
/// 读 ZDT 时区偏移(秒);槽 1 解析失败返回 RangeError。调用方须先 ensure_zoned_date_time。
|
||||
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));
|
||||
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 {
|
||||
let ptr = native_try!(receiver_obj(vm, args));
|
||||
let obj = unsafe { &*ptr };
|
||||
native_try!(ensure_zoned_date_time(vm, obj));
|
||||
let offset_minutes = native_try!(zoned_date_time_offset_minutes(vm, obj));
|
||||
let sign = if offset_minutes < 0 { '-' } else { '+' };
|
||||
let magnitude = offset_minutes.abs();
|
||||
NativeResult::Ok(vm.new_string(&format!("{sign}{:02}:{:02}", magnitude / 60, magnitude % 60)))
|
||||
let offset_seconds = native_try!(zoned_date_time_offset_seconds(vm, obj));
|
||||
let sign = if offset_seconds < 0 { '-' } else { '+' };
|
||||
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 精确域内)。
|
||||
pub fn zoned_date_time_offset_nanoseconds<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let ptr = native_try!(receiver_obj(vm, args));
|
||||
let obj = unsafe { &*ptr };
|
||||
native_try!(ensure_zoned_date_time(vm, obj));
|
||||
let offset_minutes = native_try!(zoned_date_time_offset_minutes(vm, obj));
|
||||
NativeResult::Ok(JsValue::float(offset_minutes as f64 * 60_000_000_000.0))
|
||||
let offset_seconds = native_try!(zoned_date_time_offset_seconds(vm, obj));
|
||||
NativeResult::Ok(JsValue::float(offset_seconds as f64 * 1_000_000_000.0))
|
||||
}
|
||||
|
||||
/// `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 };
|
||||
native_try!(ensure_zoned_date_time(vm, obj));
|
||||
let (year, month, day, _) = native_try!(zoned_date_time_plain_parts(vm, obj));
|
||||
let offset_minutes = native_try!(zoned_date_time_offset_minutes(vm, obj));
|
||||
let today = native_try!(start_of_day_epoch_ns(year, month, day, offset_minutes)
|
||||
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_seconds)
|
||||
.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 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")));
|
||||
NativeResult::Ok(JsValue::float((tomorrow - today) as f64 / 3_600_000_000_000.0))
|
||||
}
|
||||
|
||||
/// 求本地日期在给定时区偏移下的当地午夜纪元纳秒(GetStartOfDay 语义),
|
||||
/// 结果超出 Instant 范围时返回 None。
|
||||
pub(crate) fn start_of_day_epoch_ns(year: i32, month: u32, day: u32, offset_minutes: i32) -> Option<i128> {
|
||||
start_of_day_epoch_ns_by_days(days_from_civil(i128::from(year), i128::from(month), i128::from(day)), offset_minutes)
|
||||
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_seconds)
|
||||
}
|
||||
|
||||
/// 按日数直接算当地午夜(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
|
||||
.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)
|
||||
}
|
||||
|
||||
@@ -1653,7 +1653,7 @@ fn zoned_date_time_apply_duration<H: VmHost>(vm: &mut H, args: &[u8], sign: i64)
|
||||
let obj = unsafe { &*ptr };
|
||||
native_try!(ensure_zoned_date_time(vm, obj));
|
||||
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) {
|
||||
Ok(parts) => parts,
|
||||
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) {
|
||||
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,
|
||||
_ => 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 };
|
||||
native_try!(ensure_zoned_date_time(vm, obj));
|
||||
let (year, month, day, _) = native_try!(zoned_date_time_plain_parts(vm, obj));
|
||||
let offset_minutes = native_try!(zoned_date_time_offset_minutes(vm, obj));
|
||||
let Some(epoch_ns) = start_of_day_epoch_ns(year, month, day, offset_minutes) else {
|
||||
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_seconds) else {
|
||||
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))
|
||||
|
||||
Reference in New Issue
Block a user