mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
feat: temporal zoned 相对点补 compare/total/round epoch 分支
This commit is contained in:
@@ -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")),
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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。
|
||||
///
|
||||
/// # 步骤
|
||||
|
||||
Reference in New Issue
Block a user