From f824afd0418c70ac96889c99a6e06e43d57aced7 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sun, 27 Sep 2026 23:34:25 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20temporal=20=E5=81=8F=E7=A7=BB?= =?UTF-8?q?=E8=A1=A8=E7=A4=BA=E7=94=B1=20i32=20=E5=88=86=E9=92=9F=E5=8D=87?= =?UTF-8?q?=E7=BA=A7=E4=B8=BA=20i64=20=E7=A7=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/temporal/common.rs | 36 ++--- .../oxide_builtins/src/temporal/duration.rs | 8 +- crates/oxide_builtins/src/temporal/instant.rs | 14 +- .../oxide_builtins/src/temporal/plain_time.rs | 4 +- crates/oxide_builtins/src/temporal/tests.rs | 58 +++++-- .../src/temporal/zoned_date_time.rs | 148 +++++++++--------- 6 files changed, 146 insertions(+), 122 deletions(-) diff --git a/crates/oxide_builtins/src/temporal/common.rs b/crates/oxide_builtins/src/temporal/common.rs index 1bea5462..3f1defef 100644 --- a/crates/oxide_builtins/src/temporal/common.rs +++ b/crates/oxide_builtins/src/temporal/common.rs @@ -641,7 +641,7 @@ pub(crate) fn parse_fractional_second_digits( } } -pub(crate) fn parse_offset_minutes(value: &str) -> Option { +pub(crate) fn parse_offset_seconds(value: &str) -> Option { 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 { 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( 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( 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 { +pub(crate) fn local_to_epoch_ns(year: i32, month: u32, day: u32, time_ns: f64, offset_seconds: i64) -> Option { 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(vm: &mut H, year: i32, month: u32, day: u32, calendar: &str) -> NativeResult { diff --git a/crates/oxide_builtins/src/temporal/duration.rs b/crates/oxide_builtins/src/temporal/duration.rs index 8eb79325..814cfaa9 100644 --- a/crates/oxide_builtins/src/temporal/duration.rs +++ b/crates/oxide_builtins/src/temporal/duration.rs @@ -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( // (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( 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")); } } diff --git a/crates/oxide_builtins/src/temporal/instant.rs b/crates/oxide_builtins/src/temporal/instant.rs index 17811d7e..f32eba5c 100644 --- a/crates/oxide_builtins/src/temporal/instant.rs +++ b/crates/oxide_builtins/src/temporal/instant.rs @@ -727,15 +727,15 @@ pub fn instant_since(vm: &mut H, args: &[u8]) -> NativeResult { instant_difference(vm, args, true) } -pub(crate) fn instant_time_zone_offset(value: &str) -> Option { +pub(crate) fn instant_time_zone_offset(value: &str) -> Option { canonical_time_zone(value).map(|(_, offset)| offset) } fn format_instant_iso( - epoch_ns: i128, offset_minutes: Option, include_seconds: bool, fractional_digits: Option, + epoch_ns: i128, offset_seconds: Option, include_seconds: bool, fractional_digits: Option, ) -> Option { 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(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(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")), } diff --git a/crates/oxide_builtins/src/temporal/plain_time.rs b/crates/oxide_builtins/src/temporal/plain_time.rs index a1467e80..9e6279b3 100644 --- a/crates/oxide_builtins/src/temporal/plain_time.rs +++ b/crates/oxide_builtins/src/temporal/plain_time.rs @@ -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 { 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) diff --git a/crates/oxide_builtins/src/temporal/tests.rs b/crates/oxide_builtins/src/temporal/tests.rs index ed2be3d1..852b60f9 100644 --- a/crates/oxide_builtins/src/temporal/tests.rs +++ b/crates/oxide_builtins/src/temporal/tests.rs @@ -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(); diff --git a/crates/oxide_builtins/src/temporal/zoned_date_time.rs b/crates/oxide_builtins/src/temporal/zoned_date_time.rs index 1dc8b12d..a95ef840 100644 --- a/crates/oxide_builtins/src/temporal/zoned_date_time.rs +++ b/crates/oxide_builtins/src/temporal/zoned_date_time.rs @@ -208,24 +208,24 @@ fn zoned_date_time_options( Ok((offset, disambiguation)) } -/// 从剥注解后的 Instant 主体提取字符串内数值偏移分钟数(±HH / ±HHMM / ±HH:MM / 亚秒形式)。 -/// Z/z 结尾视为 0 分钟;无偏移或偏移不可提取返回 None。 -fn extract_string_offset_minutes(body: &str) -> Option { +/// 从剥注解后的 Instant 主体提取字符串内数值偏移秒数(±HH / ±HHMM / ±HH:MM / 亚秒形式)。 +/// Z/z 结尾视为 0 秒;无偏移或偏移不可提取返回 None。 +fn extract_string_offset_seconds(body: &str) -> Option { 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 { +/// 解析数值偏移串(±HH / ±HHMM / ±HH:MM / ±HHMMSS / ±HH:MM:SS,可带小数秒)为秒数。 +pub(crate) fn parse_any_offset_seconds(value: &str) -> Option { 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 { 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( 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( 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( // 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( 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(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, offset_name: &str, time_zone_name: &str, calendar_name: &str, ) -> Option { 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(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(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, + bag_offset: Option, } /// 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( }; // 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(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(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(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(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( 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( }; 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( }; 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(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(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(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(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(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(vm: &mut H, obj: &JsObject) -> Result { +/// 读 ZDT 时区偏移(秒);槽 1 解析失败返回 RangeError。调用方须先 ensure_zoned_date_time。 +fn zoned_date_time_offset_seconds(vm: &mut H, obj: &JsObject) -> Result { 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(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(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(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 { - 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 { + 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 { +pub(crate) fn start_of_day_epoch_ns_by_days(days: i128, offset_seconds: i64) -> Option { 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(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(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(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))