feat: temporal zoned 相对点补 compare/total/round epoch 分支

This commit is contained in:
2026-09-28 15:04:36 +00:00
parent 95155380b9
commit 9a9264c2e1
4 changed files with 624 additions and 37 deletions
@@ -77,6 +77,41 @@ pub(crate) const DAY_NS: i128 = 86_400_000_000_000;
pub(crate) const MAX_ISO_DAY: i128 = 100_000_000;
/// 时间单位长度表(时到纳秒,下标 = 单位索引 - 4)。
pub(crate) const TIME_UNIT_NS: [i128; 6] = [3_600_000_000_000, 60_000_000_000, 1_000_000_000, 1_000_000, 1_000, 1];
/// 内部时长记录:日期部分(年/月/周/日)加时间部分(纳秒,不含日分量)。
pub(crate) struct InternalDuration {
pub(crate) date: [f64; 4],
pub(crate) time_ns: i128,
}
impl InternalDuration {
/// 转十分量数组:时间部分按向零截断分解为时/分/秒/毫秒/微秒/纳秒(与时长同号)。
pub(crate) fn to_values(&self) -> [f64; 10] {
let mut values = [0.0; 10];
values[0..4].copy_from_slice(&self.date);
let time = plain_time_components_signed(self.time_ns);
for index in 0..6 {
values[4 + index] = time[index] as f64;
}
values
}
}
/// InternalDurationSign(规范 7.5.15):日期部分符号优先,其次时间部分符号。
pub(crate) fn internal_duration_sign(values: &[f64; 10]) -> i128 {
let date_sign = date_duration_sign(values);
if date_sign != 0 {
return date_sign;
}
match super::duration::duration_time_only_nanoseconds(values) {
Some(time_ns) if time_ns < 0 => -1,
Some(time_ns) if time_ns > 0 => 1,
_ => 0,
}
}
/// 比较两个 ISO 日期,返回 -1/0/+1。
pub(crate) fn compare_iso_date(a: (i128, i128, i128), b: (i128, i128, i128)) -> i128 {
if a.0 != b.0 {
@@ -345,13 +380,25 @@ pub(crate) fn nudge_window(
(r1, r2, start, end)
}
/// NudgeToCalendarUnit 结果:取整后时长、取整后纪元、窗口是否扩窗(或取整到上界),
/// 以及以取整单位计的分数总量(窗口端点间的相对位置)。
pub(crate) struct NudgeCalendarResult {
pub(crate) duration: [f64; 10],
pub(crate) nudged_epoch: i128,
pub(crate) did_expand: bool,
pub(crate) total: f64,
}
/// 对年/月/周单位的差值取整:把候选窗口端点换算成纪元纳秒,按目标时刻在窗口
/// 中的相对位置决定取整方向(NudgeToCalendarUnit 语义)。
///
/// `epoch_of_date` 是窗口端点日期的纪元换算闭包:plain 面传墙钟换算(行为逐字节
/// 不变),zoned 面传 GetEpochNanosecondsFor(compatible) 换算。
#[allow(clippy::too_many_arguments)]
fn nudge_to_calendar_unit(
sign: i128, values: &[f64; 10], origin_epoch: i128, dest_epoch: i128, date1: (i128, i128, i128), time1_ns: i128,
increment: i128, unit: usize, mode: InstantRoundingMode,
) -> Result<([f64; 10], i128, bool), ()> {
pub(crate) fn nudge_to_calendar_unit(
sign: i128, values: &[f64; 10], origin_epoch: i128, dest_epoch: i128, date1: (i128, i128, i128), increment: i128,
unit: usize, mode: InstantRoundingMode, epoch_of_date: &dyn Fn((i128, i128, i128)) -> Result<i128, ()>,
) -> Result<NudgeCalendarResult, ()> {
let epoch_of = |dur: &[f64; 10]| -> Result<i128, ()> {
if date_duration_sign(dur) == 0 {
return Ok(origin_epoch);
@@ -361,7 +408,7 @@ fn nudge_to_calendar_unit(
if days.abs() > MAX_ISO_DAY {
return Err(());
}
Ok(days * DAY_NS + time1_ns)
epoch_of_date(date)
};
let mut did_expand = false;
let (mut r1, mut r2, mut start_dur, mut end_dur) = nudge_window(sign, values, date1, increment, unit, false);
@@ -400,14 +447,24 @@ fn nudge_to_calendar_unit(
did_expand = did_expand || rounded_unit == r2.abs();
let duration = if rounded_unit == r2.abs() { end_dur } else { start_dur };
let nudged = if did_expand { end_epoch } else { start_epoch };
Ok((duration, nudged, did_expand))
// 分数总量:r1 加窗口内相对位置(increment 为 1 时即规范 [[Total]])。
let total = (denominator as f64 * r1 as f64 + numerator as f64 * sign as f64) / denominator as f64;
Ok(NudgeCalendarResult {
duration,
nudged_epoch: nudged,
did_expand,
total,
})
}
/// 舍入结果越过小单位边界时向更大的单位逐级进位,并把被进位单位的低位分量
/// 清零,最多进位到 largest 指定的单位(BubbleRelativeDuration 语义)。
fn bubble_relative_duration(
sign: i128, mut values: [f64; 10], nudged_epoch: i128, date1: (i128, i128, i128), time1_ns: i128, largest: usize,
start_unit: usize,
///
/// `epoch_of_date` 是进位候选日期的纪元换算闭包:plain 面传墙钟换算(行为逐字节
/// 不变),zoned 面传 GetEpochNanosecondsFor(compatible) 换算。
pub(crate) fn bubble_relative_duration(
sign: i128, mut values: [f64; 10], nudged_epoch: i128, date1: (i128, i128, i128), largest: usize,
start_unit: usize, epoch_of_date: &dyn Fn((i128, i128, i128)) -> Result<i128, ()>,
) -> Result<[f64; 10], ()> {
if start_unit == 0 {
return Ok(values);
@@ -446,7 +503,7 @@ fn bubble_relative_duration(
let end_date = add_date_duration(date1, &end_dur);
// 进位候选边界可能越出 ISO 日期范围,仅用于换算比较用的纪元纳秒并判断
// 是否进位,因此这里不做范围校验。
let end_epoch = days_from_civil(end_date.0, end_date.1, end_date.2) * DAY_NS + time1_ns;
let end_epoch = epoch_of_date(end_date)?;
let reached_end = if sign > 0 { nudged_epoch >= end_epoch } else { nudged_epoch <= end_epoch };
if reached_end {
values = end_dur;
@@ -742,6 +799,10 @@ pub(crate) fn nudge_iso_difference<H: VmHost>(
let date1 = start;
let time1_ns = start_time_ns;
// plain 面纪元换算闭包:墙钟(日 × 24h + 时间),行为与旧内联算法逐字节一致。
let epoch_of_date = |date: (i128, i128, i128)| -> Result<i128, ()> {
Ok(days_from_civil(date.0, date.1, date.2) * DAY_NS + time1_ns)
};
let mut date2 = end;
let mut time_ns = end_time_ns - time1_ns;
let time_sign = if time_ns > 0 {
@@ -830,7 +891,7 @@ pub(crate) fn nudge_iso_difference<H: VmHost>(
}
if did_expand_days {
let nudged = dest_epoch + (rounded_ns - total_ns);
match bubble_relative_duration(sign, values, nudged, date1, time1_ns, largest_index, 3) {
match bubble_relative_duration(sign, values, nudged, date1, largest_index, 3, &epoch_of_date) {
Ok(bubbled) => values = bubbled,
Err(()) => return Err(crate::error::create_range_error(vm, "difference is out of range")),
}
@@ -839,22 +900,33 @@ pub(crate) fn nudge_iso_difference<H: VmHost>(
} else {
// year/month/week 长度随日历变化,无法折算成固定纳秒:改用纪元纳秒窗口
// 取整,按目标时刻落在候选窗口内的相对位置决定取整方向(NudgeToCalendarUnit 语义)。
let (mut values, nudged_epoch, did_expand) = match nudge_to_calendar_unit(
let outcome = match nudge_to_calendar_unit(
sign,
&date_values,
origin_epoch,
dest_epoch,
date1,
time1_ns,
increment,
smallest_index,
mode,
&epoch_of_date,
) {
Ok(result) => result,
Err(()) => return Err(crate::error::create_range_error(vm, "difference is out of range")),
};
let mut values = outcome.duration;
let nudged_epoch = outcome.nudged_epoch;
let did_expand = outcome.did_expand;
if did_expand && smallest_index != 2 {
match bubble_relative_duration(sign, values, nudged_epoch, date1, time1_ns, largest_index, smallest_index) {
match bubble_relative_duration(
sign,
values,
nudged_epoch,
date1,
largest_index,
smallest_index,
&epoch_of_date,
) {
Ok(bubbled) => values = bubbled,
Err(()) => return Err(crate::error::create_range_error(vm, "difference is out of range")),
}
+100 -11
View File
@@ -13,12 +13,14 @@ use super::common::{
zoned_date_time_plain_parts,
};
use super::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,
add_date_duration, add_days_iso, balance_instant_difference, compare_iso_date, date_duration_sign, date_until_iso,
nudge_iso_difference, nudge_window, plain_date_time_unit_index, round_instant_difference, DifferenceSettings,
InternalDuration, DAY_NS, MAX_ISO_DAY,
};
use super::instant::{instant_rounding_mode, InstantRoundingMode};
use super::time_zone::zone_offset_seconds;
use super::zoned_date_time::{
add_zoned_datetime, difference_zoned_datetime_with_rounding_vm, difference_zoned_datetime_with_total_vm,
extract_time_zone_annotation, parse_any_offset_seconds, valid_offset_fraction, zoned_date_time_string_parts,
};
use super::{make_duration, parse_plain_date_time_string};
@@ -299,15 +301,13 @@ fn duration_values(obj: &JsObject) -> [f64; 10] {
enum RelativePoint {
Wall {
date: (i128, i128, i128),
/// 本批调用方只消费日期,zoned epoch 路径消费日历。
/// 当前仅 iso8601 日历,zoned 路径不消费日历。
#[allow(dead_code)]
calendar: String,
},
Zoned {
epoch_ns: i128,
time_zone: String,
/// 本批调用方只消费 epoch 与时区,zoned epoch 路径消费日历。
#[allow(dead_code)]
calendar: String,
},
}
@@ -319,8 +319,7 @@ enum RelativePoint {
/// 2. Zoned → 区偏移(epoch 依赖查找)+ civil_from_days 反推墙历日期。
///
/// # 边界与前提
/// - Zoned 支是本批过渡路径(TODO: 69.3.4.2 切 epoch 路径),算法与旧 zoned
/// 字符串支保持逐字节一致。
/// - Zoned 支仅用于 compare 的两支纯时间落回路径(24h 语义与规范 plain 路径一致)。
fn relative_point_wall_date<H: VmHost>(vm: &mut H, point: &RelativePoint) -> Result<(i128, i128, i128), JsValue> {
match point {
RelativePoint::Wall { date, .. } => Ok(*date),
@@ -548,6 +547,15 @@ pub fn duration_total<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
Err(error) => return NativeResult::Err(error),
};
// Zoned 载荷:AddZonedDateTime → DifferenceZonedDateTimeWithTotal(门与单位无关)。
if let Some(RelativePoint::Zoned { epoch_ns, time_zone, calendar }) = &relative_date {
let target = native_try!(add_zoned_datetime(vm, *epoch_ns, time_zone, calendar, &values, true));
let total = native_try!(difference_zoned_datetime_with_total_vm(
vm, *epoch_ns, target, time_zone, calendar, unit_index
));
return NativeResult::Ok(JsValue::float(total));
}
const UNIT_NS: [i128; 6] = [3_600_000_000_000, 60_000_000_000, 1_000_000_000, 1_000_000, 1_000, 1];
if unit_index <= 2 || (unit_index == 3 && has_calendar_units) {
// 日历单位:需要 relativeTo,用纪元纳秒窗口计算分数总量。
@@ -557,7 +565,6 @@ pub fn duration_total<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
&format!("a starting point is required for {} total", unit_raw),
));
};
// Zoned 载荷暂走墙钟路径(TODO: 69.3.4.2 切 epoch 路径)。
let rel_date = native_try!(relative_point_wall_date(vm, &point));
let Some(time_ns_base) = duration_time_nanoseconds(&values) else {
return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range"));
@@ -900,6 +907,52 @@ fn duration_apply_to_relative<H: VmHost>(
Ok((end, target_time))
}
/// DefaultTemporalLargestUnit(规范 7.5.17):首个非零分量索引(全零视为 nanosecond)。
pub(crate) fn default_temporal_largest_unit(values: &[f64; 10]) -> usize {
for (index, value) in values.iter().enumerate() {
if *value != 0.0 {
return index;
}
}
9
}
/// TemporalDurationFromInternal(规范 7.5.8):日期部分直传,时间部分按 largestUnit 平衡。
///
/// # 步骤
/// 1. largestUnit 为日期类(day 及以上):时间全量分解,days 折回日期部分。
/// 2. largestUnit 为时间类:从该单位向下分解。
pub(crate) fn internal_duration_to_values(internal: &InternalDuration, largest: usize) -> [f64; 10] {
let mut values = [0.0; 10];
values[0..4].copy_from_slice(&internal.date);
let sign = if internal.time_ns < 0 { -1 } else { 1 };
if largest <= 3 {
let mut remainder = internal.time_ns.unsigned_abs();
let hours = remainder / 3_600_000_000_000;
remainder %= 3_600_000_000_000;
let minutes = remainder / 60_000_000_000;
remainder %= 60_000_000_000;
let seconds = remainder / 1_000_000_000;
remainder %= 1_000_000_000;
let milliseconds = remainder / 1_000_000;
remainder %= 1_000_000;
let microseconds = remainder / 1_000;
let nanoseconds = remainder % 1_000;
let days = hours / 24;
values[3] = internal.date[3] + sign as f64 * days as f64;
values[4] = (hours % 24) as f64 * sign as f64;
values[5] = minutes as f64 * sign as f64;
values[6] = seconds as f64 * sign as f64;
values[7] = milliseconds as f64 * sign as f64;
values[8] = microseconds as f64 * sign as f64;
values[9] = nanoseconds as f64 * sign as f64;
} else {
let time_values = balance_instant_difference(internal.time_ns, largest - 4).unwrap_or([0.0; 10]);
values[4..10].copy_from_slice(&time_values[4..10]);
}
values
}
/// `Temporal.Duration.compare(one, two, options)`:按相对点比较两个时长。
///
/// # 步骤
@@ -957,8 +1010,22 @@ pub fn duration_compare<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
one_ns.cmp(&two_ns)
}
Some(point) => {
// Zoned 载荷且任一支最大单位为日期类:epoch 级比较(门是 OR)。
// 两支均纯时间时落回 24h 墙钟路径(与规范 plain 路径一致)。
if let RelativePoint::Zoned { epoch_ns, time_zone, calendar } = &point {
let largest1 = default_temporal_largest_unit(&one);
let largest2 = default_temporal_largest_unit(&two);
if largest1 <= 3 || largest2 <= 3 {
let after1 = native_try!(add_zoned_datetime(vm, *epoch_ns, time_zone, calendar, &one, true));
let after2 = native_try!(add_zoned_datetime(vm, *epoch_ns, time_zone, calendar, &two, true));
return NativeResult::Ok(JsValue::int(match after1.cmp(&after2) {
std::cmp::Ordering::Less => -1,
std::cmp::Ordering::Equal => 0,
std::cmp::Ordering::Greater => 1,
}));
}
}
// 有 relativeTo:各 duration 应用后先比日期再比时间。
// Zoned 载荷暂走墙钟路径(TODO: 69.3.4.2 切 epoch 路径)。
let rel = native_try!(relative_point_wall_date(vm, &point));
let (date1, time1) = native_try!(duration_apply_to_relative(vm, rel, &one));
let (date2, time2) = native_try!(duration_apply_to_relative(vm, rel, &two));
@@ -1136,7 +1203,30 @@ pub fn duration_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
};
// 日历单位存在或目标为日历单位时走 relativeTo 路径,否则走纯时间路径。
let needs_relative = values[..3].iter().any(|value| *value != 0.0) || largest < 3 || smallest_index < 3;
// Zoned 相对点无条件走 zoned 路径(DST 感知的日长,24h 路径失准)。
let needs_relative = values[..3].iter().any(|value| *value != 0.0)
|| largest < 3
|| smallest_index < 3
|| matches!(&relative_date, Some(RelativePoint::Zoned { .. }));
// Zoned 载荷:AddZonedDateTime → DifferenceZonedDateTimeWithRounding(门与单位无关)。
if let Some(RelativePoint::Zoned { epoch_ns, time_zone, calendar }) = &relative_date {
let target = native_try!(add_zoned_datetime(vm, *epoch_ns, time_zone, calendar, &values, true));
let internal = native_try!(difference_zoned_datetime_with_rounding_vm(
vm,
*epoch_ns,
target,
time_zone,
calendar,
largest,
increment,
smallest_index,
mode,
));
// largestUnit 为日期类时降为 hour。
let largest = if largest <= 3 { 4 } else { largest };
let result = internal_duration_to_values(&internal, largest);
return make_duration(vm, result);
}
if !needs_relative {
// 纯时间路径:按 24 小时/天将 days..nanoseconds 汇总为纳秒,按 smallest 舍入后平衡到 largest。
let Some(time_ns) = duration_time_nanoseconds(&values) else {
@@ -1194,7 +1284,6 @@ pub fn duration_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
"a starting point is required for rounding calendar units",
));
};
// Zoned 载荷暂走墙钟路径(TODO: 69.3.4.2 切 epoch 路径)。
let rel = native_try!(relative_point_wall_date(vm, &point));
let Some(time_ns_total) = duration_time_nanoseconds(&values) else {
return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range"));
+105
View File
@@ -619,3 +619,108 @@ fn calendar_date_add_iso_constrain_semantics() {
// 闰年二月:constrain 钳到 29。
assert_eq!(calendar_date_add_iso((2024, 1, 31), 0, 1, 0, 0, true), Ok((2024, 2, 29)));
}
#[test]
fn zoned_difference_day_correction() {
use super::zoned_date_time::{difference_zoned_datetime, get_epoch_nanos_for};
// 同日纯时间差:无日期部分,时间部分为纳秒差。
let ns1 = get_epoch_nanos_for("America/New_York", (2025, 6, 15), 43_200_000_000_000).unwrap();
let ns2 = get_epoch_nanos_for("America/New_York", (2025, 6, 15), 66_600_000_000_000).unwrap();
let d = difference_zoned_datetime(ns1, ns2, "America/New_York", "iso8601", 3).unwrap();
assert_eq!(d.date, [0.0, 0.0, 0.0, 0.0]);
assert_eq!(d.time_ns, 23_400_000_000_000);
// 跨 DST 间隙日(23 小时):墙历时刻相同,日期部分 1 天、时间部分 0。
let ns1 = get_epoch_nanos_for("America/New_York", (2025, 3, 8), 43_200_000_000_000).unwrap();
let ns2 = get_epoch_nanos_for("America/New_York", (2025, 3, 9), 43_200_000_000_000).unwrap();
let d = difference_zoned_datetime(ns1, ns2, "America/New_York", "iso8601", 3).unwrap();
assert_eq!(d.date, [0.0, 0.0, 0.0, 1.0]);
assert_eq!(d.time_ns, 0);
// dayCorrection 循环推进:12 小时跨间隙日,日期部分 0、时间部分 12 小时。
let ns2b = get_epoch_nanos_for("America/New_York", (2025, 3, 9), 0).unwrap();
let d = difference_zoned_datetime(ns1, ns2b, "America/New_York", "iso8601", 3).unwrap();
assert_eq!(d.date, [0.0, 0.0, 0.0, 0.0]);
assert_eq!(d.time_ns, 43_200_000_000_000);
// 反向(sign 为正):日期部分为负一天、时间部分 0。
let d = difference_zoned_datetime(ns2, ns1, "America/New_York", "iso8601", 3).unwrap();
assert_eq!(d.date, [0.0, 0.0, 0.0, -1.0]);
assert_eq!(d.time_ns, 0);
}
#[test]
fn zoned_total_dst_fraction() {
use super::zoned_date_time::{difference_zoned_datetime_with_total, get_epoch_nanos_for};
// 整日差(23 小时间隙日):总量 1。
let ns1 = get_epoch_nanos_for("America/New_York", (2025, 3, 8), 43_200_000_000_000).unwrap();
let ns2 = get_epoch_nanos_for("America/New_York", (2025, 3, 9), 43_200_000_000_000).unwrap();
let total = difference_zoned_datetime_with_total(ns1, ns2, "America/New_York", "iso8601", 3).unwrap();
assert!((total - 1.0).abs() < 1e-12);
// 12 小时跨 23 小时间隙日:总量 12/23(按实际日长折算,非 24 小时)。
let ns2b = get_epoch_nanos_for("America/New_York", (2025, 3, 9), 0).unwrap();
let total = difference_zoned_datetime_with_total(ns1, ns2b, "America/New_York", "iso8601", 3).unwrap();
assert!((total - 12.0 / 23.0).abs() < 1e-12);
}
#[test]
fn zoned_round_nudge_to_zoned_time() {
use super::duration::internal_duration_to_values;
use super::instant::InstantRoundingMode;
use super::zoned_date_time::{
add_zoned_datetime_pure, difference_zoned_datetime_with_rounding, get_epoch_nanos_for,
};
// Apia 2011-12-30 整日跳过(24 小时间隙):起点墙历为 12-31 00:30。
let origin = get_epoch_nanos_for("Pacific/Apia", (2011, 12, 30), 1_800_000_000_000).unwrap();
// P25H 按小时取整:跨间隙日不产生进位,保持 25 小时(非 1 天 1 小时)。
let p25h = [0.0_f64, 0.0, 0.0, 0.0, 25.0, 0.0, 0.0, 0.0, 0.0, 0.0];
let target = add_zoned_datetime_pure(origin, "Pacific/Apia", "iso8601", &p25h, true).unwrap();
let internal = difference_zoned_datetime_with_rounding(
origin,
target,
"Pacific/Apia",
"iso8601",
4,
1,
4,
InstantRoundingMode::Trunc,
)
.unwrap();
let values = internal_duration_to_values(&internal, 4);
assert_eq!(values, [0.0, 0.0, 0.0, 0.0, 25.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
// P1DT26H 按小时取整:日长 24 小时,取整后 2 天 2 小时。
let p1dt26h = [0.0_f64, 0.0, 0.0, 1.0, 26.0, 0.0, 0.0, 0.0, 0.0, 0.0];
let target = add_zoned_datetime_pure(origin, "Pacific/Apia", "iso8601", &p1dt26h, true).unwrap();
let internal = difference_zoned_datetime_with_rounding(
origin,
target,
"Pacific/Apia",
"iso8601",
3,
1,
4,
InstantRoundingMode::Trunc,
)
.unwrap();
let values = internal_duration_to_values(&internal, 4);
assert_eq!(values, [0.0, 0.0, 0.0, 2.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
}
#[test]
fn zoned_compare_pure_epoch() {
use super::zoned_date_time::{add_zoned_datetime_pure, get_epoch_nanos_for};
// 跨 23 小时间隙日:P1D(实际 23 小时)短于 P24H(实际 24 小时)。
let origin = get_epoch_nanos_for("America/New_York", (2025, 3, 8), 43_200_000_000_000).unwrap();
let p1d = [0.0_f64, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
let p24h = [0.0_f64, 0.0, 0.0, 0.0, 24.0, 0.0, 0.0, 0.0, 0.0, 0.0];
let after1d = add_zoned_datetime_pure(origin, "America/New_York", "iso8601", &p1d, true).unwrap();
let after24h = add_zoned_datetime_pure(origin, "America/New_York", "iso8601", &p24h, true).unwrap();
assert!(after1d < after24h);
}
@@ -17,17 +17,19 @@ use super::common::{
zoned_date_time_plain_parts, FractionalSecondDigitsInput, MAX_INSTANT_NS,
};
use super::difference::{
add_days_iso, date_duration_sign, difference_core, parse_difference_settings, DAY_NS, MAX_ISO_DAY,
add_days_iso, bubble_relative_duration, compare_iso_date, date_duration_sign, date_until_iso, difference_core,
internal_duration_sign, nudge_to_calendar_unit, parse_difference_settings, round_instant_difference,
InternalDuration, DAY_NS, MAX_ISO_DAY, TIME_UNIT_NS,
};
use super::instant::{
instant_rounding_mode, instant_string_without_annotations, parse_instant_string, primitive_to_bigint,
round_instant_ns,
round_instant_ns, InstantRoundingMode,
};
use super::time_zone::{named_zone_possible_epoch_ns, tz_gap_offsets, zone_offset_seconds};
use super::{
days_in_month_iso, duration_like_values, duration_time_only_nanoseconds, 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,
valid_plain_date_time_range,
make_plain_date, make_plain_date_time, make_plain_time, plain_time_components, plain_time_components_signed,
plain_time_like_ns, valid_iso_date, valid_plain_date_time_range,
};
/// `Temporal.ZonedDateTime` 构造器:保存纪元纳秒、固定偏移或 UTC 时区以及 ISO 日历。
@@ -1835,12 +1837,7 @@ pub(crate) fn add_zoned_datetime_pure(
return add_instant(epoch_ns, time_ns);
}
// GetISODateTimeFor:墙历分量。
let offset_seconds = zone_offset_seconds(time_zone, epoch_ns.div_euclid(1_000_000_000) as i64)
.ok_or(AddZonedError::InvalidTimeZone)?;
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);
let (year, month, day) = civil_from_days(days);
let ((year, month, day), time_ns) = get_iso_date_time_for(time_zone, epoch_ns)?;
if !valid_plain_date_time_range(year as i32, month as u32, day as u32, time_ns as f64) {
return Err(AddZonedError::OutOfDateTimeRange);
}
@@ -1868,10 +1865,16 @@ pub(crate) fn add_zoned_datetime_pure(
}
/// AddZonedDateTime 的 VM 包装:核心错误统一转 RangeError。
fn add_zoned_datetime<H: VmHost>(
pub(crate) fn add_zoned_datetime<H: VmHost>(
vm: &mut H, epoch_ns: i128, time_zone: &str, calendar: &str, values: &[f64; 10], constrain: bool,
) -> Result<i128, JsValue> {
add_zoned_datetime_pure(epoch_ns, time_zone, calendar, values, constrain).map_err(|error| match error {
add_zoned_datetime_pure(epoch_ns, time_zone, calendar, values, constrain)
.map_err(|error| map_add_zoned_error(vm, error))
}
/// AddZonedError 到 JS RangeError 的映射(各 zoned 面 VM 包装共用)。
fn map_add_zoned_error<H: VmHost>(vm: &mut H, error: AddZonedError) -> JsValue {
match error {
AddZonedError::InvalidTimeZone => crate::error::create_range_error(vm, "invalid time zone"),
AddZonedError::OutOfDateTimeRange => crate::error::create_range_error(vm, "invalid date-time"),
AddZonedError::OutOfInstantRange => {
@@ -1880,9 +1883,327 @@ fn add_zoned_datetime<H: VmHost>(
AddZonedError::DayOutOfRange => crate::error::create_range_error(vm, "day out of range"),
AddZonedError::InvalidDate => crate::error::create_range_error(vm, "invalid date"),
AddZonedError::OutOfTimeRange => crate::error::create_range_error(vm, "duration is out of range"),
}
}
/// DifferenceZonedDateTimeWithRounding 的 VM 包装:错误映射为 RangeError。
#[allow(clippy::too_many_arguments)]
pub(crate) fn difference_zoned_datetime_with_rounding_vm<H: VmHost>(
vm: &mut H, ns1: i128, ns2: i128, time_zone: &str, calendar: &str, largest_unit: usize, increment: i128,
smallest_unit: usize, mode: InstantRoundingMode,
) -> Result<InternalDuration, JsValue> {
difference_zoned_datetime_with_rounding(ns1, ns2, time_zone, calendar, largest_unit, increment, smallest_unit, mode)
.map_err(|error| map_add_zoned_error(vm, error))
}
/// DifferenceZonedDateTimeWithTotal 的 VM 包装:错误映射为 RangeError。
pub(crate) fn difference_zoned_datetime_with_total_vm<H: VmHost>(
vm: &mut H, ns1: i128, ns2: i128, time_zone: &str, calendar: &str, unit: usize,
) -> Result<f64, JsValue> {
difference_zoned_datetime_with_total(ns1, ns2, time_zone, calendar, unit)
.map_err(|error| map_add_zoned_error(vm, error))
}
/// GetISODateTimeFor(规范 11.1.10):epoch 对应的墙历日期时间。
///
/// # 步骤
/// 1. 区偏移(epoch 依赖查找)加到 epoch 得墙历纳秒。
/// 2. 拆日与时间(欧几里得除,负 epoch 同样正确)。
///
/// # 边界与前提
/// - 时区名非法(不在区表且非固定偏移串)返回 InvalidTimeZone。
pub(crate) fn get_iso_date_time_for(
time_zone: &str, epoch_ns: i128,
) -> Result<((i128, i128, i128), i128), AddZonedError> {
let offset_seconds = zone_offset_seconds(time_zone, epoch_ns.div_euclid(1_000_000_000) as i64)
.ok_or(AddZonedError::InvalidTimeZone)?;
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);
let (year, month, day) = civil_from_days(days);
Ok(((year, month, day), time_ns))
}
/// GetEpochNanosecondsFor(规范 11.1.11,compatible 消歧):墙历日期时间的纪元。
///
/// # 步骤
/// 1. 候选集(固定偏移区单候选;IANA 区 0/1/2 候选)。
/// 2. compatible 消歧(重叠取早、间隙平移后取后侧)。
///
/// # 边界与前提
/// - 日期年份越出 i32 表示范围视为越界(候选换算无意义)。
/// - 候选超 Instant 范围或区表未命中返回对应错误。
pub(crate) fn get_epoch_nanos_for(
time_zone: &str, date: (i128, i128, i128), time_ns: i128,
) -> Result<i128, AddZonedError> {
if date.0 < i32::MIN as i128 || date.0 > i32::MAX as i128 {
return Err(AddZonedError::OutOfDateTimeRange);
}
let candidates = get_possible_epoch_nanoseconds(time_zone, date.0 as i32, date.1 as u32, date.2 as u32, time_ns)?;
disambiguate_possible_epoch_nanoseconds(time_zone, date, time_ns, "compatible", &candidates)
}
/// zoned 面纪元换算闭包工厂:候选日期 → (日期, time1) 的 GetEpochNanosecondsFor(compatible) 纪元。
fn zoned_epoch_of_date<'a>(time_zone: &'a str, time1_ns: i128) -> impl Fn((i128, i128, i128)) -> Result<i128, ()> + 'a {
move |date| get_epoch_nanos_for(time_zone, date, time1_ns).map_err(|_| ())
}
/// 时间差纳秒的符号(-1/0/+1)。
fn time_duration_sign(ns: i128) -> i128 {
if ns < 0 {
-1
} else if ns > 0 {
1
} else {
0
}
}
/// DifferenceZonedDateTime(规范 6.5.6):两个 epoch 的差,平衡后结果中无大于
/// largestUnit 的非零单位,计入日历 reckoning 与时区偏移变化。
///
/// # 步骤
/// 1. 同墙历日 → 纯时间差(ZeroDateDuration + 纳秒差)。
/// 2. dayCorrection 循环:按 sign 平移终点日期,经 GetEpochNanosecondsFor(compatible)
/// 取候选点,直到时间差符号与总差符号相反。
/// 3. 日期部分按 CalendarDateUntil(largestUnit 与 day 取大者)分解。
///
/// # 边界与前提
/// - 全程 i128 纳秒;中间点时间取起点墙历时间(规范 (intermediateDate, startDT.Time))。
/// - calendar 参数透传(引擎仅 iso8601 日历)。
pub(crate) fn difference_zoned_datetime(
ns1: i128, ns2: i128, time_zone: &str, _calendar: &str, largest_unit: usize,
) -> Result<InternalDuration, AddZonedError> {
if ns1 == ns2 {
return Ok(InternalDuration { date: [0.0; 4], time_ns: 0 });
}
let (start_date, start_time) = get_iso_date_time_for(time_zone, ns1)?;
let (end_date, end_time) = get_iso_date_time_for(time_zone, ns2)?;
if compare_iso_date(start_date, end_date) == 0 {
return Ok(InternalDuration {
date: [0.0; 4],
time_ns: ns2 - ns1,
});
}
let sign = if ns2 - ns1 < 0 { 1 } else { -1 };
let max_day_correction = if sign == -1 { 2 } else { 1 };
let mut day_correction = 0_i128;
let mut time_duration = end_time - start_time;
if time_duration_sign(time_duration) == sign {
day_correction += 1;
}
let mut success = false;
let mut intermediate_date = end_date;
while day_correction <= max_day_correction && !success {
intermediate_date = add_days_iso(end_date, day_correction * sign);
let intermediate_ns = get_epoch_nanos_for(time_zone, intermediate_date, start_time)?;
time_duration = ns2 - intermediate_ns;
if sign != time_duration_sign(time_duration) {
success = true;
}
day_correction += 1;
}
if !success {
return Err(AddZonedError::OutOfDateTimeRange);
}
let date_largest = largest_unit.min(3);
let date_values = date_until_iso(start_date, intermediate_date, date_largest);
Ok(InternalDuration {
date: [date_values[0], date_values[1], date_values[2], date_values[3]],
time_ns: time_duration,
})
}
/// DifferenceZonedDateTimeWithRounding(规范 6.5.7):两个 epoch 的差按设置取整。
///
/// # 步骤
/// 1. largestUnit 为时间单位 → DifferenceInstant(8.5.6:纳秒差按单位取整)。
/// 2. 否则 DifferenceZonedDateTime;smallestUnit 为 nanosecond 且 increment 为 1 时直接返回。
/// 3. 否则 RoundRelativeDuration(zoned 路由)。
///
/// # 边界与前提
/// - smallestUnit 不大于 largestUnit(调用方已校验)。
#[allow(clippy::too_many_arguments)]
pub(crate) fn difference_zoned_datetime_with_rounding(
ns1: i128, ns2: i128, time_zone: &str, _calendar: &str, largest_unit: usize, increment: i128, smallest_unit: usize,
mode: InstantRoundingMode,
) -> Result<InternalDuration, AddZonedError> {
if largest_unit >= 4 {
let quantum = TIME_UNIT_NS[smallest_unit - 4]
.checked_mul(increment)
.ok_or(AddZonedError::OutOfTimeRange)?;
let rounded = round_instant_difference(ns2 - ns1, quantum, mode).ok_or(AddZonedError::OutOfTimeRange)?;
return Ok(InternalDuration {
date: [0.0; 4],
time_ns: rounded,
});
}
let difference = difference_zoned_datetime(ns1, ns2, time_zone, _calendar, largest_unit)?;
if smallest_unit == 9 && increment == 1 {
return Ok(difference);
}
let (iso_date, iso_time) = get_iso_date_time_for(time_zone, ns1)?;
round_relative_duration_zoned(
difference,
ns1,
ns2,
(iso_date, iso_time),
time_zone,
largest_unit,
increment,
smallest_unit,
mode,
)
}
/// RoundRelativeDuration(规范 7.5.38)的 zoned 路由:日历单位(或 day)走日历
/// 窗口取整,时间单位走 NudgeToZonedTime;扩窗后进位到 largestUnit。
///
/// # 步骤
/// 1. 路由:smallestUnit 为日历单位或 day → NudgeToCalendarUnit(zoned 闭包);
/// 否则 NudgeToZonedTime。
/// 2. 扩窗且 smallestUnit 非 week → BubbleRelativeDuration(startUnit 取 smallestUnit 与 day 的较大者)。
#[allow(clippy::too_many_arguments)]
fn round_relative_duration_zoned(
difference: InternalDuration, origin_ns: i128, dest_ns: i128, iso: ((i128, i128, i128), i128), time_zone: &str,
largest_unit: usize, increment: i128, smallest_unit: usize, mode: InstantRoundingMode,
) -> Result<InternalDuration, AddZonedError> {
let values = difference.to_values();
let sign = if internal_duration_sign(&values) < 0 { -1 } else { 1 };
let (iso_date, iso_time) = iso;
let epoch_of_date = zoned_epoch_of_date(time_zone, iso_time);
// zoned 面时区恒在场:irregular = smallestUnit 为日历单位或 day。
let irregular = smallest_unit <= 3;
let (mut result, nudged_epoch, did_expand) = if irregular {
let outcome = nudge_to_calendar_unit(
sign,
&values,
origin_ns,
dest_ns,
iso_date,
increment,
smallest_unit,
mode,
&epoch_of_date,
)
.map_err(|_| AddZonedError::OutOfDateTimeRange)?;
(outcome.duration, outcome.nudged_epoch, outcome.did_expand)
} else {
nudge_to_zoned_time(sign, &values, iso_date, iso_time, time_zone, increment, smallest_unit, mode)?
};
if did_expand && smallest_unit != 2 {
let start_unit = smallest_unit.min(3);
result =
bubble_relative_duration(sign, result, nudged_epoch, iso_date, largest_unit, start_unit, &epoch_of_date)
.map_err(|_| AddZonedError::OutOfDateTimeRange)?;
}
Ok(InternalDuration {
date: [result[0], result[1], result[2], result[3]],
time_ns: duration_time_only_nanoseconds(&result).ok_or(AddZonedError::OutOfTimeRange)?,
})
}
/// NudgeToZonedTime(规范 7.5.35):相对 zoned 起点按时间单位取整,计入日长
/// 可能非 24 小时(DST 日)的情形。
///
/// # 步骤
/// 1. start = CalendarDateAdd(constrain);end = start 加 sign 天;两端点经
/// GetEpochNanosecondsFor(compatible) 换算,daySpan 天然含 DST 日长。
/// 2. 时间部分按 increment × 单位长度取整;越过日边界时记一天进位并重新取整余量。
///
/// # 边界与前提
/// - unit 为时间单位(时到纳秒);day 单位走日历窗口路径,不进本函数。
#[allow(clippy::too_many_arguments)]
fn nudge_to_zoned_time(
sign: i128, values: &[f64; 10], iso_date: (i128, i128, i128), iso_time: i128, time_zone: &str, increment: i128,
unit: usize, mode: InstantRoundingMode,
) -> Result<([f64; 10], i128, bool), AddZonedError> {
let time_ns = duration_time_only_nanoseconds(values).ok_or(AddZonedError::OutOfTimeRange)?;
let start = calendar_date_add_iso(
iso_date,
values[0] as i128,
values[1] as i128,
values[2] as i128,
values[3] as i128,
true,
)?;
let start_epoch = get_epoch_nanos_for(time_zone, start, iso_time)?;
let end_epoch = get_epoch_nanos_for(time_zone, add_days_iso(start, sign), iso_time)?;
let day_span = end_epoch - start_epoch;
let quantum = TIME_UNIT_NS[unit - 4]
.checked_mul(increment)
.ok_or(AddZonedError::OutOfTimeRange)?;
let mut rounded = round_instant_difference(time_ns, quantum, mode).ok_or(AddZonedError::OutOfTimeRange)?;
let beyond = rounded - day_span;
let (day_delta, nudged_epoch, did_round_beyond_day) = if time_duration_sign(beyond) != -sign {
rounded = round_instant_difference(beyond, quantum, mode).ok_or(AddZonedError::OutOfTimeRange)?;
(sign, end_epoch + rounded, true)
} else {
(0, start_epoch + rounded, false)
};
let mut result = *values;
result[3] = (values[3] as i128 + day_delta) as f64;
let time_values = plain_time_components_signed(rounded);
for index in 0..6 {
result[4 + index] = time_values[index] as f64;
}
Ok((result, nudged_epoch, did_round_beyond_day))
}
/// DifferenceZonedDateTimeWithTotal(规范 6.5.8):两个 epoch 的差以 unit 计的总量。
///
/// # 步骤
/// 1. unit 为时间单位 → TotalTimeDuration 快路径(纳秒差直接除单位长度)。
/// 2. 否则 DifferenceZonedDateTime + TotalRelativeDuration。
pub(crate) fn difference_zoned_datetime_with_total(
ns1: i128, ns2: i128, time_zone: &str, _calendar: &str, unit: usize,
) -> Result<f64, AddZonedError> {
if unit >= 4 {
let difference = ns2 - ns1;
return Ok(difference as f64 / TIME_UNIT_NS[unit - 4] as f64);
}
let difference = difference_zoned_datetime(ns1, ns2, time_zone, _calendar, unit)?;
let (iso_date, iso_time) = get_iso_date_time_for(time_zone, ns1)?;
total_relative_duration(&difference, ns1, ns2, (iso_date, iso_time), time_zone, unit)
}
/// TotalRelativeDuration(规范 7.5.39):时长以 unit 计的总量,相对起点折算。
///
/// # 步骤
/// 1. unit 为日历单位或(时区在场且 unit 为 day)→ NudgeToCalendarUnit
/// (increment = 1、trunc)取 [[Total]]。
/// 2. 否则 TotalTimeDuration(24h 日折算,zoned 面不可达)。
fn total_relative_duration(
difference: &InternalDuration, origin_ns: i128, dest_ns: i128, iso: ((i128, i128, i128), i128), time_zone: &str,
unit: usize,
) -> Result<f64, AddZonedError> {
let values = difference.to_values();
// zoned 面时区恒在场:unit ≤ day 恒走日历窗口路径。
if unit <= 3 {
let sign = if internal_duration_sign(&values) < 0 { -1 } else { 1 };
let (iso_date, iso_time) = iso;
let epoch_of_date = zoned_epoch_of_date(time_zone, iso_time);
let outcome = nudge_to_calendar_unit(
sign,
&values,
origin_ns,
dest_ns,
iso_date,
1,
unit,
InstantRoundingMode::Trunc,
&epoch_of_date,
)
.map_err(|_| AddZonedError::OutOfDateTimeRange)?;
return Ok(outcome.total);
}
let time_ns = duration_time_only_nanoseconds(&values).ok_or(AddZonedError::OutOfTimeRange)?;
let with_days = time_ns
.checked_add((values[3] as i128) * DAY_NS)
.ok_or(AddZonedError::OutOfTimeRange)?;
Ok(with_days as f64 / TIME_UNIT_NS[unit - 4] as f64)
}
/// ZDT add/subtract 核心:读 receiver 槽后经 add_zoned_datetime 核心叠加 duration。
///
/// # 步骤