mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
feat: zdt 加减改走 epoch 级核心并扩展相对点载荷
This commit is contained in:
@@ -7,9 +7,10 @@ use oxide_types::value::JsValue;
|
||||
|
||||
use super::common::native_try;
|
||||
use super::common::{
|
||||
canonical_time_zone, civil_from_days, days_from_civil, ensure_duration, get_double_prop, get_instant_epoch_ns,
|
||||
initialize_temporal_receiver, is_ctor_call, local_to_epoch_ns, native_engine_error, plain_date_time_object_parts,
|
||||
receiver_obj, temporal_option_number, temporal_option_string, temporal_option_value, zoned_date_time_plain_parts,
|
||||
canonical_time_zone, civil_from_days, days_from_civil, ensure_duration, get_calendar_id, get_double_prop,
|
||||
get_instant_epoch_ns, initialize_temporal_receiver, is_ctor_call, local_to_epoch_ns, native_engine_error,
|
||||
plain_date_time_object_parts, receiver_obj, temporal_option_number, temporal_option_string, temporal_option_value,
|
||||
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,
|
||||
@@ -210,6 +211,16 @@ pub(crate) fn duration_time_nanoseconds(values: &[f64; 10]) -> Option<i128> {
|
||||
Some(total)
|
||||
}
|
||||
|
||||
/// 时长时间部分(时到纳秒,不含日分量)的纳秒合计。
|
||||
pub(crate) fn duration_time_only_nanoseconds(values: &[f64; 10]) -> Option<i128> {
|
||||
const SCALES: [i128; 6] = [3_600_000_000_000, 60_000_000_000, 1_000_000_000, 1_000_000, 1_000, 1];
|
||||
let mut total = 0_i128;
|
||||
for (value, scale) in values[4..].iter().zip(SCALES) {
|
||||
total = total.checked_add(duration_component_integer(*value)?.checked_mul(scale)?)?;
|
||||
}
|
||||
Some(total)
|
||||
}
|
||||
|
||||
fn validate_duration_values<H: VmHost>(vm: &mut H, values: &[f64; 10]) -> Result<(), JsValue> {
|
||||
let mut sign = 0_i8;
|
||||
for value in values {
|
||||
@@ -284,42 +295,79 @@ fn duration_values(obj: &JsObject) -> [f64; 10] {
|
||||
std::array::from_fn(|index| get_double_prop(obj, index))
|
||||
}
|
||||
|
||||
/// 归一 options.relativeTo 为相对日期 `(y, m, d)`。
|
||||
/// relativeTo 归一后的相对点载荷:Wall(墙历日期)或 Zoned(epoch + 时区 + 日历)。
|
||||
enum RelativePoint {
|
||||
Wall {
|
||||
date: (i128, i128, i128),
|
||||
/// 本批调用方只消费日期,zoned epoch 路径消费日历。
|
||||
#[allow(dead_code)]
|
||||
calendar: String,
|
||||
},
|
||||
Zoned {
|
||||
epoch_ns: i128,
|
||||
time_zone: String,
|
||||
/// 本批调用方只消费 epoch 与时区,zoned epoch 路径消费日历。
|
||||
#[allow(dead_code)]
|
||||
calendar: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// 相对点的墙历日期。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. string 先检测 `[tz]` 注解:含注解走 zoned 解析(offset-vs-zone 校验,reject 语义);
|
||||
/// 无注解走 plain 解析(纯日期、日期时间、带偏移的 plain 串)。
|
||||
/// 2. PlainDateTime / PlainDate 对象 → 直读 prop 0/1/2。
|
||||
/// 3. ZonedDateTime → zoned_date_time_plain_parts 取本地 (y,m,d)。
|
||||
/// 4. Instant → epoch 纳秒按 UTC 分解(div_euclid(DAY_NS) → civil_from_days)。
|
||||
/// 5. 其他对象按 property bag 解析(ToRelativeTemporalObject,constrain)。
|
||||
/// 6. undefined → None;其余原始值 → TypeError。
|
||||
/// 1. Wall → 直接返回日期。
|
||||
/// 2. Zoned → 区偏移(epoch 依赖查找)+ civil_from_days 反推墙历日期。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - Zoned 支是本批过渡路径(TODO: 69.3.4.2 切 epoch 路径),算法与旧 zoned
|
||||
/// 字符串支保持逐字节一致。
|
||||
fn relative_point_wall_date<H: VmHost>(vm: &mut H, point: &RelativePoint) -> Result<(i128, i128, i128), JsValue> {
|
||||
match point {
|
||||
RelativePoint::Wall { date, .. } => Ok(*date),
|
||||
RelativePoint::Zoned { epoch_ns, time_zone, .. } => {
|
||||
let offset_seconds = zone_offset_seconds(time_zone, epoch_ns.div_euclid(1_000_000_000) as i64)
|
||||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))?;
|
||||
let wall_ns = epoch_ns + i128::from(offset_seconds) * 1_000_000_000;
|
||||
Ok(civil_from_days(wall_ns.div_euclid(DAY_NS)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 归一 options.relativeTo 为相对点(Wall 墙历日期 / Zoned epoch+时区+日历)。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. string 先检测 `[tz]` 注解:含注解走 zoned 解析(offset-vs-zone 校验,reject 语义)
|
||||
/// 得 Zoned 载荷;无注解走 plain 解析(纯日期、日期时间、带偏移的 plain 串)得 Wall 载荷。
|
||||
/// 2. PlainDateTime / PlainDate 对象 → 直读 prop 0/1/2,Wall 载荷。
|
||||
/// 3. ZonedDateTime → Zoned 载荷(zoned_date_time_plain_parts 范围检查后直读三槽)。
|
||||
/// 4. 其他对象(含 Instant)按 property bag 解析(ToRelativeTemporalObject,constrain),Wall 载荷。
|
||||
/// 5. undefined → None;其余原始值 → TypeError。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 调用方须已把 raw 从 options 取出(读序由调用方保证)。
|
||||
/// - 本函数只取日期分量;时间分量由调用方的 duration 分量另行加。
|
||||
/// - bag 支 offset 语法校验先于数值字段转换;offset-vs-zone 匹配校验在字段转换之后。
|
||||
fn duration_relative_to_date<H: VmHost>(
|
||||
vm: &mut H, relative_raw: JsValue,
|
||||
) -> Result<Option<(i128, i128, i128)>, JsValue> {
|
||||
fn duration_relative_to_date<H: VmHost>(vm: &mut H, relative_raw: JsValue) -> Result<Option<RelativePoint>, JsValue> {
|
||||
if relative_raw.is_undefined() {
|
||||
return Ok(None);
|
||||
}
|
||||
if relative_raw.is_string() {
|
||||
let text = to_string(relative_raw);
|
||||
// 含 [tz] 注解走 zoned 路径(offset-vs-zone 校验,reject 语义):
|
||||
// 按 instant 解析 + 注解时区偏移(epoch 依赖查找)反推墙历日期。
|
||||
// 按 instant 解析,时区与日历直取解析产物。
|
||||
if extract_time_zone_annotation(&text).is_some() {
|
||||
let (epoch_ns, time_zone_id, _calendar) = zoned_date_time_string_parts(vm, &text, "reject")?;
|
||||
let offset_seconds = zone_offset_seconds(&time_zone_id, epoch_ns.div_euclid(1_000_000_000) as i64)
|
||||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))?;
|
||||
let wall_ns = epoch_ns + i128::from(offset_seconds) * 1_000_000_000;
|
||||
let (year, month, day) = civil_from_days(wall_ns.div_euclid(DAY_NS));
|
||||
return Ok(Some((year, month, day)));
|
||||
return Ok(Some(RelativePoint::Zoned {
|
||||
epoch_ns,
|
||||
time_zone: time_zone_id,
|
||||
calendar: "iso8601".to_string(),
|
||||
}));
|
||||
}
|
||||
// 无注解走 plain 路径:纯日期、日期时间、带偏移的 plain 串(偏移不作时区标识符)。
|
||||
if let Ok((year, month, day, _time_ns)) = parse_plain_date_time_string(&text) {
|
||||
return Ok(Some((i128::from(year), i128::from(month), i128::from(day))));
|
||||
return Ok(Some(RelativePoint::Wall {
|
||||
date: (i128::from(year), i128::from(month), i128::from(day)),
|
||||
calendar: "iso8601".to_string(),
|
||||
}));
|
||||
}
|
||||
return Err(crate::error::create_range_error(vm, "invalid relativeTo"));
|
||||
}
|
||||
@@ -332,24 +380,30 @@ fn duration_relative_to_date<H: VmHost>(
|
||||
}
|
||||
let obj = unsafe { &*ptr };
|
||||
if obj.is_plain_date_time_obj() || obj.is_plain_date_obj() {
|
||||
return Ok(Some((
|
||||
i128::from(get_double_prop(obj, 0) as i32),
|
||||
i128::from(get_double_prop(obj, 1) as u32),
|
||||
i128::from(get_double_prop(obj, 2) as u32),
|
||||
)));
|
||||
let calendar = get_calendar_id(obj, if obj.is_plain_date_obj() { 3 } else { 4 });
|
||||
return Ok(Some(RelativePoint::Wall {
|
||||
date: (
|
||||
i128::from(get_double_prop(obj, 0) as i32),
|
||||
i128::from(get_double_prop(obj, 1) as u32),
|
||||
i128::from(get_double_prop(obj, 2) as u32),
|
||||
),
|
||||
calendar,
|
||||
}));
|
||||
}
|
||||
if obj.is_zoned_date_time_obj() {
|
||||
let (year, month, day, _time_ns) = zoned_date_time_plain_parts(vm, obj)?;
|
||||
return Ok(Some((i128::from(year), i128::from(month), i128::from(day))));
|
||||
}
|
||||
if obj.is_instant_obj() {
|
||||
// 保留 zoned_date_time_plain_parts 范围检查(墙历日期时间越界直接抛错)。
|
||||
zoned_date_time_plain_parts(vm, obj)?;
|
||||
let Some(epoch_ns) = get_instant_epoch_ns(obj) else {
|
||||
return Err(crate::error::create_range_error(vm, "invalid Instant"));
|
||||
return Err(crate::error::create_range_error(vm, "invalid ZonedDateTime"));
|
||||
};
|
||||
let (year, month, day) = civil_from_days(epoch_ns.div_euclid(DAY_NS));
|
||||
return Ok(Some((year, month, day)));
|
||||
return Ok(Some(RelativePoint::Zoned {
|
||||
epoch_ns,
|
||||
time_zone: to_string(obj.get_prop_at(1)),
|
||||
calendar: get_calendar_id(obj, 2),
|
||||
}));
|
||||
}
|
||||
// property bag:按 ToRelativeTemporalObject 依次校验 calendar/timeZone/offset 再读字段。
|
||||
// 规范 13.19 无 Instant 分支:Instant 对象落入此处按字段解析(缺 day 抛错)。
|
||||
let calendar_raw = temporal_option_value(vm, obj, relative_raw, "calendar")?;
|
||||
if !calendar_raw.is_undefined() {
|
||||
return Err(crate::error::create_type_error(vm, "invalid relativeTo"));
|
||||
@@ -379,7 +433,7 @@ fn duration_relative_to_date<H: VmHost>(
|
||||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid offset"))?;
|
||||
Some(seconds)
|
||||
};
|
||||
let (year, month, day, time_ns, _calendar) = plain_date_time_object_parts(vm, relative_raw, obj, true, false)?;
|
||||
let (year, month, day, time_ns, calendar) = plain_date_time_object_parts(vm, relative_raw, obj, true, false)?;
|
||||
// offset-vs-zone 校验(对象支精确一致、reject 语义):offset 与 timeZone 同时在场时,
|
||||
// 候选 epoch 点 zone 偏移须与输入偏移秒级相等。
|
||||
if let (Some(offset), Some(tz_id)) = (bag_offset_seconds, time_zone_id.as_deref()) {
|
||||
@@ -391,7 +445,10 @@ fn duration_relative_to_date<H: VmHost>(
|
||||
return Err(crate::error::create_range_error(vm, "offset and time zone disagree"));
|
||||
}
|
||||
}
|
||||
Ok(Some((i128::from(year), i128::from(month), i128::from(day))))
|
||||
Ok(Some(RelativePoint::Wall {
|
||||
date: (i128::from(year), i128::from(month), i128::from(day)),
|
||||
calendar: calendar.unwrap_or_else(|| "iso8601".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
fn format_duration_number(value: f64) -> String {
|
||||
@@ -485,7 +542,7 @@ pub fn duration_total<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
// 含日历单位(year/month/week)时,days 及以上的 total 必须走 relativeTo 日历路径。
|
||||
let has_calendar_units = values[0] != 0.0 || values[1] != 0.0 || values[2] != 0.0;
|
||||
|
||||
// relativeTo:统一经 duration_relative_to_date 归一(支持 string/PD/PDT/ZDT/Instant/bag)。
|
||||
// relativeTo:统一经 duration_relative_to_date 归一(支持 string/PD/PDT/ZDT/bag)。
|
||||
let relative_date = match duration_relative_to_date(vm, relative_raw) {
|
||||
Ok(relative) => relative,
|
||||
Err(error) => return NativeResult::Err(error),
|
||||
@@ -494,12 +551,14 @@ pub fn duration_total<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
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,用纪元纳秒窗口计算分数总量。
|
||||
let Some(rel_date) = relative_date else {
|
||||
let Some(point) = relative_date else {
|
||||
return NativeResult::Err(crate::error::create_range_error(
|
||||
vm,
|
||||
&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"));
|
||||
};
|
||||
@@ -845,7 +904,7 @@ fn duration_apply_to_relative<H: VmHost>(
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. one/two 各自 duration_like_values 归一(读序 one → two → options)。
|
||||
/// 2. options.relativeTo 经 duration_relative_to_date 归一(含 ZDT/Instant/bag 分支)。
|
||||
/// 2. options.relativeTo 经 duration_relative_to_date 归一(含 ZDT/bag 分支)。
|
||||
/// 3. 无 relativeTo 且无日历单位:按 duration_time_nanoseconds 直接比大小(fast path)。
|
||||
/// 4. 无 relativeTo 但含日历单位:抛 RangeError,日历单位无法脱离具体相对点折算。
|
||||
/// 5. 有 relativeTo:每个 duration 应用相对点得 (date, time),先比日期再比时间(字典序)。
|
||||
@@ -897,8 +956,10 @@ pub fn duration_compare<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
.ok_or_else(|| { crate::error::create_range_error(vm, "duration is out of range") }));
|
||||
one_ns.cmp(&two_ns)
|
||||
}
|
||||
Some(rel) => {
|
||||
Some(point) => {
|
||||
// 有 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));
|
||||
let date_cmp = compare_iso_date(date1, date2);
|
||||
@@ -1127,12 +1188,14 @@ pub fn duration_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
}
|
||||
|
||||
// 日历路径:需要 relativeTo,把 duration 应用后按 nudge_iso_difference 舍入平衡。
|
||||
let Some(rel) = relative_date else {
|
||||
let Some(point) = relative_date else {
|
||||
return NativeResult::Err(crate::error::create_range_error(
|
||||
vm,
|
||||
"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"));
|
||||
};
|
||||
|
||||
@@ -501,3 +501,121 @@ fn zone_aware_string_offset_vs_zone() {
|
||||
assert_eq!(extract_time_zone_annotation("2000-01-01T00:00[u-ca=iso8601]"), None);
|
||||
assert_eq!(extract_time_zone_annotation("2000-01-01T00:00[-00:44:30]"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_zoned_disambiguation_four_quadrants() {
|
||||
use super::zoned_date_time::{disambiguate_possible_epoch_nanoseconds, get_possible_epoch_nanoseconds};
|
||||
|
||||
// NY 重叠 2025-11-02T01:30:双候选 [05:30Z, 06:30Z] 升序。
|
||||
let cands = get_possible_epoch_nanoseconds("America/New_York", 2025, 11, 2, 5_400_000_000_000).unwrap();
|
||||
assert_eq!(cands, vec![1_762_061_400_000_000_000, 1_762_065_000_000_000_000]);
|
||||
let iso = (2025_i128, 11, 2);
|
||||
let time_ns = 5_400_000_000_000;
|
||||
assert_eq!(
|
||||
disambiguate_possible_epoch_nanoseconds("America/New_York", iso, time_ns, "earlier", &cands),
|
||||
Ok(1_762_061_400_000_000_000)
|
||||
);
|
||||
assert_eq!(
|
||||
disambiguate_possible_epoch_nanoseconds("America/New_York", iso, time_ns, "later", &cands),
|
||||
Ok(1_762_065_000_000_000_000)
|
||||
);
|
||||
assert_eq!(
|
||||
disambiguate_possible_epoch_nanoseconds("America/New_York", iso, time_ns, "compatible", &cands),
|
||||
Ok(1_762_061_400_000_000_000)
|
||||
);
|
||||
assert!(disambiguate_possible_epoch_nanoseconds("America/New_York", iso, time_ns, "reject", &cands).is_err());
|
||||
|
||||
// NY 间隙 2025-03-09T02:30:零候选,compatible 平移 +1h 取 07:30Z,earlier 取 06:30Z。
|
||||
let empty = get_possible_epoch_nanoseconds("America/New_York", 2025, 3, 9, 9_000_000_000_000).unwrap();
|
||||
assert!(empty.is_empty());
|
||||
let iso = (2025_i128, 3, 9);
|
||||
let time_ns = 9_000_000_000_000;
|
||||
assert_eq!(
|
||||
disambiguate_possible_epoch_nanoseconds("America/New_York", iso, time_ns, "compatible", &empty),
|
||||
Ok(1_741_505_400_000_000_000)
|
||||
);
|
||||
assert_eq!(
|
||||
disambiguate_possible_epoch_nanoseconds("America/New_York", iso, time_ns, "earlier", &empty),
|
||||
Ok(1_741_501_800_000_000_000)
|
||||
);
|
||||
assert_eq!(
|
||||
disambiguate_possible_epoch_nanoseconds("America/New_York", iso, time_ns, "later", &empty),
|
||||
Ok(1_741_505_400_000_000_000)
|
||||
);
|
||||
assert!(disambiguate_possible_epoch_nanoseconds("America/New_York", iso, time_ns, "reject", &empty).is_err());
|
||||
|
||||
// NY 无歧义 2025-06-15T12:00(EDT):单候选 16:00Z。
|
||||
let cands = get_possible_epoch_nanoseconds("America/New_York", 2025, 6, 15, 43_200_000_000_000).unwrap();
|
||||
assert_eq!(cands, vec![1_750_003_200_000_000_000]);
|
||||
|
||||
// Apia 24 小时间隙 2011-12-30T12:00:零候选,compatible 平移 +24h 取 2011-12-30T22:00Z,
|
||||
// earlier 取 2011-12-29T22:00Z。
|
||||
let empty = get_possible_epoch_nanoseconds("Pacific/Apia", 2011, 12, 30, 43_200_000_000_000).unwrap();
|
||||
assert!(empty.is_empty());
|
||||
let iso = (2011_i128, 12, 30);
|
||||
let time_ns = 43_200_000_000_000;
|
||||
assert_eq!(
|
||||
disambiguate_possible_epoch_nanoseconds("Pacific/Apia", iso, time_ns, "compatible", &empty),
|
||||
Ok(1_325_282_400_000_000_000)
|
||||
);
|
||||
assert_eq!(
|
||||
disambiguate_possible_epoch_nanoseconds("Pacific/Apia", iso, time_ns, "earlier", &empty),
|
||||
Ok(1_325_196_000_000_000_000)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_zoned_datetime_pure_end_to_end() {
|
||||
use super::common::MAX_INSTANT_NS;
|
||||
use super::zoned_date_time::{add_zoned_datetime_pure, AddZonedError};
|
||||
|
||||
// 快速路径(UTC):无日期分量,时间部分 epoch 级加。
|
||||
let values = [0.0_f64, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0];
|
||||
assert_eq!(add_zoned_datetime_pure(0, "UTC", "iso8601", &values, true), Ok(7_200_000_000_000));
|
||||
|
||||
// 固定偏移区 +01:00:+1 天保持本地时刻不变(epoch 前进整日)。
|
||||
let values = [0.0_f64, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
|
||||
assert_eq!(add_zoned_datetime_pure(0, "+01:00", "iso8601", &values, true), Ok(86_400_000_000_000));
|
||||
|
||||
// NY 跨 DST 结束(2024-11-02T06:00-04:00 加 1 月 → 12-02T06:00 EST,epoch 11:00Z)。
|
||||
let values = [0.0_f64, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
|
||||
assert_eq!(
|
||||
add_zoned_datetime_pure(1_730_541_600_000_000_000, "America/New_York", "iso8601", &values, true),
|
||||
Ok(1_733_137_200_000_000_000)
|
||||
);
|
||||
|
||||
// Apia 跨 24 小时间隙减 1 天:2011-12-31T00:00+14:00 减 1 天落间隙起点 2011-12-30T00:00,
|
||||
// compatible 平移 +24h 回 2011-12-31T00:00,epoch 落回 transition 瞬间(与输入同刻)。
|
||||
let values = [0.0_f64, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
|
||||
assert_eq!(
|
||||
add_zoned_datetime_pure(1_325_239_200_000_000_000, "Pacific/Apia", "iso8601", &values, true),
|
||||
Ok(1_325_239_200_000_000_000)
|
||||
);
|
||||
|
||||
// 下边界:-MAX instant 再加 -1 天,墙历越出 PlainDateTime 范围(引擎的 instant 与
|
||||
// plain 范围同为 10^8 天量级,ISO 日边界检查先于 instant 检查触发),与旧路径的
|
||||
// 中间日期检查同为 RangeError「invalid date-time」。
|
||||
let values = [0.0_f64, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
|
||||
assert_eq!(
|
||||
add_zoned_datetime_pure(-MAX_INSTANT_NS, "UTC", "iso8601", &values, true),
|
||||
Err(AddZonedError::OutOfDateTimeRange)
|
||||
);
|
||||
|
||||
// 上边界:+MAX instant 快速路径加 1 小时溢出。
|
||||
let values = [0.0_f64, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0];
|
||||
assert_eq!(
|
||||
add_zoned_datetime_pure(MAX_INSTANT_NS, "UTC", "iso8601", &values, true),
|
||||
Err(AddZonedError::OutOfInstantRange)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calendar_date_add_iso_constrain_semantics() {
|
||||
use super::zoned_date_time::{calendar_date_add_iso, AddZonedError};
|
||||
|
||||
// 月末截断:constrain 钳到目标月末,reject 抛 DayOutOfRange。
|
||||
assert_eq!(calendar_date_add_iso((2025, 1, 31), 0, 1, 0, 0, true), Ok((2025, 2, 28)));
|
||||
assert_eq!(calendar_date_add_iso((2025, 1, 31), 0, 1, 0, 0, false), Err(AddZonedError::DayOutOfRange));
|
||||
// 闰年二月:constrain 钳到 29。
|
||||
assert_eq!(calendar_date_add_iso((2024, 1, 31), 0, 1, 0, 0, true), Ok((2024, 2, 29)));
|
||||
}
|
||||
|
||||
@@ -1164,25 +1164,102 @@ pub(crate) fn tz_canonical_name(name: &str) -> Option<&'static str> {
|
||||
/// # 边界与前提
|
||||
/// - 早于首个 transition 取 LMT;晚于末个 transition 取末偏移(`last_offset`
|
||||
/// 与末次 transition 一致,故 `partition_point` 已覆盖)。
|
||||
///
|
||||
/// # 注意事项
|
||||
/// - 只支持正向查找(epoch → 偏移);local → epoch 的逆换算(ambiguous /
|
||||
/// nonexistent)由调用方迭代,不在本函数。
|
||||
pub(crate) fn tz_offset_seconds(zone: &str, epoch_s: i64) -> Option<i64> {
|
||||
let z = tz_zone_data(zone)?;
|
||||
fn tz_offset_seconds_of(z: &TzifZone, epoch_s: i64) -> i64 {
|
||||
if z.transitions.is_empty() {
|
||||
return Some(i64::from(z.last_offset));
|
||||
return i64::from(z.last_offset);
|
||||
}
|
||||
let idx = z.transitions.partition_point(|t| t.0 <= epoch_s);
|
||||
if idx == 0 {
|
||||
Some(i64::from(z.lmt_offset))
|
||||
i64::from(z.lmt_offset)
|
||||
} else if idx == z.transitions.len() {
|
||||
Some(i64::from(z.last_offset))
|
||||
i64::from(z.last_offset)
|
||||
} else {
|
||||
Some(i64::from(z.transitions[idx - 1].1))
|
||||
i64::from(z.transitions[idx - 1].1)
|
||||
}
|
||||
}
|
||||
|
||||
/// 查某区在 `epoch_s`(纪元秒)生效的 UTC 偏移(秒)。
|
||||
pub(crate) fn tz_offset_seconds(zone: &str, epoch_s: i64) -> Option<i64> {
|
||||
tz_zone_data(zone).map(|z| tz_offset_seconds_of(z, epoch_s))
|
||||
}
|
||||
|
||||
/// GetNamedTimeZoneEpochNanoseconds(规范 14.6.3,宿主定义):墙历时刻的全部候选纪元。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. 取墙历(按 UTC 解释)相邻的三个区间偏移:所在区间、下一 transition 后、
|
||||
/// 上一 transition 前。
|
||||
/// 2. 对每个偏移 o 构造候选 e = wall - o;校验 e 落在 o 的区间内
|
||||
/// (offset_at(e) == o),成立才收。
|
||||
/// 3. 去重升序返回(重叠 = 2 个、间隙 = 0 个、无歧义 = 1 个)。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 区表未命中返回 `None`(调用方决定错误)。
|
||||
/// - 偏移量域 ±24 小时,有效候选必在墙历(按 UTC 解释)±24 小时内,
|
||||
/// 三个相邻区间覆盖全部候选。
|
||||
pub(crate) fn named_zone_possible_epoch_ns(zone: &str, wall_ns: i128) -> Option<Vec<i128>> {
|
||||
let z = tz_zone_data(zone)?;
|
||||
let wall_s = wall_ns.div_euclid(1_000_000_000) as i64;
|
||||
let idx = z.transitions.partition_point(|t| t.0 <= wall_s);
|
||||
let current = if idx == 0 { z.lmt_offset } else { z.transitions[idx - 1].1 };
|
||||
let mut candidates = Vec::with_capacity(3);
|
||||
let mut consider = |offset: i32| {
|
||||
let candidate = wall_ns - i128::from(offset) * 1_000_000_000;
|
||||
if tz_offset_seconds_of(z, wall_s - i64::from(offset)) == i64::from(offset) {
|
||||
candidates.push(candidate);
|
||||
}
|
||||
};
|
||||
consider(current);
|
||||
if idx < z.transitions.len() {
|
||||
consider(z.transitions[idx].1);
|
||||
}
|
||||
if idx > 0 {
|
||||
let previous = if idx - 1 == 0 { z.lmt_offset } else { z.transitions[idx - 2].1 };
|
||||
consider(previous);
|
||||
}
|
||||
candidates.sort_unstable();
|
||||
candidates.dedup();
|
||||
Some(candidates)
|
||||
}
|
||||
|
||||
/// 找 0 候选墙历所落间隙的 transition,返回间隙前后偏移(秒)。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. 墙历(按 UTC 解释)在 transition 前区间:查下一 transition 是否正偏移跳变
|
||||
/// 且墙历落间隙 (t + o_before, t + o_after)。
|
||||
/// 2. 墙历在 transition 后区间:查上一 transition 同条件。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 区表未命中或墙历不落在任何间隙内返回 `None`(调用方决定错误)。
|
||||
pub(crate) fn tz_gap_offsets(zone: &str, wall_ns: i128) -> Option<(i64, i64)> {
|
||||
let z = tz_zone_data(zone)?;
|
||||
let wall_s = wall_ns.div_euclid(1_000_000_000) as i64;
|
||||
let idx = z.transitions.partition_point(|t| t.0 <= wall_s);
|
||||
// 墙历在 transition 前区间:间隙由下一 transition 产生。
|
||||
// 间隙起点(墙历 = t + o_before)候选集为空,计入间隙。
|
||||
if idx < z.transitions.len() {
|
||||
let (t, offset_after) = z.transitions[idx];
|
||||
let offset_before = if idx == 0 { z.lmt_offset } else { z.transitions[idx - 1].1 };
|
||||
if offset_after > offset_before
|
||||
&& wall_ns >= i128::from(t) * 1_000_000_000 + i128::from(offset_before) * 1_000_000_000
|
||||
&& wall_ns < i128::from(t) * 1_000_000_000 + i128::from(offset_after) * 1_000_000_000
|
||||
{
|
||||
return Some((i64::from(offset_before), i64::from(offset_after)));
|
||||
}
|
||||
}
|
||||
// 墙历在 transition 后区间:间隙由上一 transition 产生。
|
||||
if idx > 0 {
|
||||
let (t, offset_after) = z.transitions[idx - 1];
|
||||
let offset_before = if idx - 1 == 0 { z.lmt_offset } else { z.transitions[idx - 2].1 };
|
||||
if offset_after > offset_before
|
||||
&& wall_ns >= i128::from(t) * 1_000_000_000 + i128::from(offset_before) * 1_000_000_000
|
||||
&& wall_ns < i128::from(t) * 1_000_000_000 + i128::from(offset_after) * 1_000_000_000
|
||||
{
|
||||
return Some((i64::from(offset_before), i64::from(offset_after)));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 统一时区偏移查找(epoch 依赖):固定偏移区(数值偏移串)走快速路径恒返常量,
|
||||
/// IANA 区走 transition 表二分。
|
||||
///
|
||||
|
||||
@@ -16,14 +16,16 @@ use super::common::{
|
||||
temporal_option_number, temporal_option_string, temporal_option_value, temporal_overflow, temporal_string_strict,
|
||||
zoned_date_time_plain_parts, FractionalSecondDigitsInput, MAX_INSTANT_NS,
|
||||
};
|
||||
use super::difference::{difference_core, parse_difference_settings, MAX_ISO_DAY};
|
||||
use super::difference::{
|
||||
add_days_iso, date_duration_sign, difference_core, parse_difference_settings, DAY_NS, MAX_ISO_DAY,
|
||||
};
|
||||
use super::instant::{
|
||||
instant_rounding_mode, instant_string_without_annotations, parse_instant_string, primitive_to_bigint,
|
||||
round_instant_ns,
|
||||
};
|
||||
use super::time_zone::zone_offset_seconds;
|
||||
use super::time_zone::{named_zone_possible_epoch_ns, tz_gap_offsets, zone_offset_seconds};
|
||||
use super::{
|
||||
days_in_month_iso, duration_component_integer, duration_like_values, is_leap_year_iso, make_duration,
|
||||
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,
|
||||
};
|
||||
@@ -1686,33 +1688,226 @@ pub(crate) fn start_of_day_epoch_ns_by_days(days: i128, offset_seconds: i64) ->
|
||||
(start.unsigned_abs() <= MAX_INSTANT_NS as u128).then_some(start)
|
||||
}
|
||||
|
||||
/// ZDT add/subtract 核心:本地分量叠加 duration(复用 plain_date_time_apply_duration 算法),
|
||||
/// 先做中间日期检查(AddZonedDateTime 的中间日期越界校验),再经 local_to_epoch_ns 转回。
|
||||
/// AddZonedDateTime 核心的错误分类(包装层统一转 RangeError)。
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) enum AddZonedError {
|
||||
/// 时区标识符未命中区表(fixed_offset_seconds 与区表均未命中)。
|
||||
InvalidTimeZone,
|
||||
/// 墙历日期时间越出 PlainDateTime 范围(±(10^8+1) 天,边界互斥)。
|
||||
OutOfDateTimeRange,
|
||||
/// 候选或最终纪元越出 Instant 范围(±MAX instant)。
|
||||
OutOfInstantRange,
|
||||
/// 日分量越界且 overflow = reject。
|
||||
DayOutOfRange,
|
||||
/// 年/月组合无效(历法无此月)。
|
||||
InvalidDate,
|
||||
/// 时间分量纳秒合计越界。
|
||||
OutOfTimeRange,
|
||||
}
|
||||
|
||||
/// AddInstant(规范 8.5.5):epoch 级加时间纳秒,做 Instant 范围检查。
|
||||
pub(crate) fn add_instant(epoch_ns: i128, time_ns: i128) -> Result<i128, AddZonedError> {
|
||||
let result = epoch_ns.checked_add(time_ns).ok_or(AddZonedError::OutOfInstantRange)?;
|
||||
if result.unsigned_abs() > MAX_INSTANT_NS as u128 {
|
||||
return Err(AddZonedError::OutOfInstantRange);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// CalendarDateAdd(ISO 8601 日历):日期叠加年/月/周/日。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. branding + receiver 本地分量 + 时区偏移。
|
||||
/// 2. duration_like_values 归一 + temporal_overflow 解析(读序:duration → options)。
|
||||
/// 3. 时间增量(hours 起)与日期增量(y/m/w/d)分别按 sign 取反;时间部分
|
||||
/// div_euclid/rem_euclid 拆出 extra_days/new_time_ns。
|
||||
/// 4. 日期部分月份/日钳制叠加后先验中间日期(+ 原 time_ns 须在 PlainDateTime 范围)→ RangeError。
|
||||
/// 5. 时间进位合并 → 最终 (yy, mm, dd, new_time_ns) → valid_plain_date_time_range 校验。
|
||||
/// 6. local_to_epoch_ns + MAX_INSTANT_NS 校验 → make_zoned_date_time 保时区/日历槽。
|
||||
/// 1. 年/月推进(月末截断:constrain 钳到目标月末,reject 抛 DayOutOfRange)。
|
||||
/// 2. 周*7 + 日推进。
|
||||
pub(crate) fn calendar_date_add_iso(
|
||||
date: (i128, i128, i128), years: i128, months: i128, weeks: i128, days: i128, constrain: bool,
|
||||
) -> Result<(i128, i128, i128), AddZonedError> {
|
||||
let total = date.0 * 12 + (date.1 - 1) + years * 12 + months;
|
||||
let ny = total.div_euclid(12);
|
||||
let nm = total.rem_euclid(12) + 1;
|
||||
let max_day = days_in_month(ny, nm).ok_or(AddZonedError::InvalidDate)?;
|
||||
let new_day = if date.2 > max_day {
|
||||
if constrain {
|
||||
max_day
|
||||
} else {
|
||||
return Err(AddZonedError::DayOutOfRange);
|
||||
}
|
||||
} else {
|
||||
date.2
|
||||
};
|
||||
Ok(add_days_iso((ny, nm, new_day), weeks * 7 + days))
|
||||
}
|
||||
|
||||
/// GetPossibleEpochNanoseconds(规范 11.1.13):墙历日期时间的全部候选纪元。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. 固定偏移区:偏移为常量,候选唯一(墙历按 UTC 解释减偏移),
|
||||
/// 平衡后日期过 CheckISODaysRange。
|
||||
/// 2. IANA 区:named_zone_possible_epoch_ns 枚举候选集(重叠 2、间隙 0、无歧义 1)。
|
||||
/// 3. 候选超 ±MAX instant 抛范围错。
|
||||
pub(crate) fn get_possible_epoch_nanoseconds(
|
||||
time_zone: &str, year: i32, month: u32, day: u32, time_ns: i128,
|
||||
) -> Result<Vec<i128>, AddZonedError> {
|
||||
let wall_ns = days_from_civil(i128::from(year), i128::from(month), i128::from(day))
|
||||
.checked_mul(DAY_NS)
|
||||
.and_then(|value| value.checked_add(time_ns))
|
||||
.ok_or(AddZonedError::OutOfDateTimeRange)?;
|
||||
if let Some(offset) = fixed_offset_seconds(time_zone) {
|
||||
// 固定偏移区:偏移恒定,候选唯一(BalanceISODateTime 语义)。
|
||||
let epoch = wall_ns
|
||||
.checked_sub(i128::from(offset) * 1_000_000_000)
|
||||
.ok_or(AddZonedError::OutOfDateTimeRange)?;
|
||||
if wall_ns.div_euclid(DAY_NS).unsigned_abs() > MAX_ISO_DAY as u128 {
|
||||
return Err(AddZonedError::OutOfDateTimeRange);
|
||||
}
|
||||
if epoch.unsigned_abs() > MAX_INSTANT_NS as u128 {
|
||||
return Err(AddZonedError::OutOfInstantRange);
|
||||
}
|
||||
return Ok(vec![epoch]);
|
||||
}
|
||||
let candidates = named_zone_possible_epoch_ns(time_zone, wall_ns).ok_or(AddZonedError::InvalidTimeZone)?;
|
||||
for &epoch in &candidates {
|
||||
if epoch.unsigned_abs() > MAX_INSTANT_NS as u128 {
|
||||
return Err(AddZonedError::OutOfInstantRange);
|
||||
}
|
||||
}
|
||||
Ok(candidates)
|
||||
}
|
||||
|
||||
/// DisambiguatePossibleEpochNanoseconds(规范 11.1.12):从候选集选唯一纪元。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. 1 候选 → 直接返回。
|
||||
/// 2. 2 候选(重叠)→ compatible/earlier 取较早纪元,later 取较晚,reject 抛 RangeError。
|
||||
/// 3. 0 候选(间隙)→ 按间隙前后偏移差平移墙历(earlier 反向、compatible/later 正向),
|
||||
/// 重枚举候选集后取首(earlier)/末(compatible、later)。
|
||||
pub(crate) fn disambiguate_possible_epoch_nanoseconds(
|
||||
time_zone: &str, iso_date: (i128, i128, i128), time_ns: i128, disambiguation: &str, candidates: &[i128],
|
||||
) -> Result<i128, AddZonedError> {
|
||||
if !candidates.is_empty() {
|
||||
if disambiguation == "reject" {
|
||||
return Err(AddZonedError::OutOfDateTimeRange);
|
||||
}
|
||||
return Ok(if disambiguation == "later" {
|
||||
*candidates.last().unwrap()
|
||||
} else {
|
||||
candidates[0]
|
||||
});
|
||||
}
|
||||
// 0 候选(间隙):按间隙前后偏移差平移墙历。
|
||||
if disambiguation == "reject" {
|
||||
return Err(AddZonedError::OutOfDateTimeRange);
|
||||
}
|
||||
let wall_ns = days_from_civil(iso_date.0, iso_date.1, iso_date.2) * DAY_NS + time_ns;
|
||||
let (offset_before, offset_after) = tz_gap_offsets(time_zone, wall_ns).ok_or(AddZonedError::OutOfDateTimeRange)?;
|
||||
let shift = i128::from(offset_after - offset_before) * 1_000_000_000;
|
||||
let shifted = wall_ns + if disambiguation == "earlier" { -shift } else { shift };
|
||||
let days = shifted.div_euclid(DAY_NS);
|
||||
let (year, month, day) = civil_from_days(days);
|
||||
let shifted_time = shifted.rem_euclid(DAY_NS);
|
||||
let shifted_candidates =
|
||||
get_possible_epoch_nanoseconds(time_zone, year as i32, month as u32, day as u32, shifted_time)?;
|
||||
if shifted_candidates.is_empty() {
|
||||
return Err(AddZonedError::OutOfDateTimeRange);
|
||||
}
|
||||
Ok(if disambiguation == "earlier" {
|
||||
shifted_candidates[0]
|
||||
} else {
|
||||
*shifted_candidates.last().unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
/// AddZonedDateTime(规范 6.5.5)核心:epoch 级叠加 duration。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. DateDurationSign = 0 快速路径:AddInstant 直接加时间部分。
|
||||
/// 2. GetISODateTimeFor:墙历分量 = epoch + 区偏移(epoch 依赖查找)后拆日与时间,
|
||||
/// 墙历须过 PlainDateTime 范围。
|
||||
/// 3. CalendarDateAdd:日期分量(y/m/w/d)按 constrain 语义叠加。
|
||||
/// 4. 中间日期检查:(addedDate, 原 time) 过 PlainDateTime 范围。
|
||||
/// 5. GetEpochNanosecondsFor(compatible):固定偏移区单候选;IANA 区经
|
||||
/// GetPossibleEpochNanoseconds + DisambiguatePossibleEpochNanoseconds。
|
||||
/// 6. AddInstant:时间部分 epoch 级加,范围检查。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - duration 全 0 → 值不变的新对象(blank-duration 语义)。
|
||||
/// - 固定偏移下"中间 epoch + 时间增量"与"合并后 local_to_epoch_ns"严格相等,无需分步换算。
|
||||
/// - 全程 i128 纳秒,不经 f64。
|
||||
/// - calendar 参数透传(引擎仅 iso8601 日历)。
|
||||
pub(crate) fn add_zoned_datetime_pure(
|
||||
epoch_ns: i128, time_zone: &str, _calendar: &str, values: &[f64; 10], constrain: bool,
|
||||
) -> Result<i128, AddZonedError> {
|
||||
// 快速路径:无日期分量,时间部分 epoch 级加。
|
||||
if date_duration_sign(values) == 0 {
|
||||
let time_ns = duration_time_only_nanoseconds(values).ok_or(AddZonedError::OutOfTimeRange)?;
|
||||
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);
|
||||
if !valid_plain_date_time_range(year as i32, month as u32, day as u32, time_ns as f64) {
|
||||
return Err(AddZonedError::OutOfDateTimeRange);
|
||||
}
|
||||
// CalendarDateAdd:日期分量(constrain 钳月末,reject 抛错)。
|
||||
let added = calendar_date_add_iso(
|
||||
(year, month, day),
|
||||
values[0] as i128,
|
||||
values[1] as i128,
|
||||
values[2] as i128,
|
||||
values[3] as i128,
|
||||
constrain,
|
||||
)?;
|
||||
// 中间日期检查(ISODateTimeWithinLimits)。
|
||||
if !valid_plain_date_time_range(added.0 as i32, added.1 as u32, added.2 as u32, time_ns as f64) {
|
||||
return Err(AddZonedError::OutOfDateTimeRange);
|
||||
}
|
||||
// GetEpochNanosecondsFor(compatible)。
|
||||
let candidates =
|
||||
get_possible_epoch_nanoseconds(time_zone, added.0 as i32, added.1 as u32, added.2 as u32, time_ns)?;
|
||||
let intermediate_ns =
|
||||
disambiguate_possible_epoch_nanoseconds(time_zone, added, time_ns, "compatible", &candidates)?;
|
||||
// AddInstant:时间部分(不含日分量,日分量已按日历叠加)epoch 级加。
|
||||
let time_ns = duration_time_only_nanoseconds(values).ok_or(AddZonedError::OutOfTimeRange)?;
|
||||
add_instant(intermediate_ns, time_ns)
|
||||
}
|
||||
|
||||
/// AddZonedDateTime 的 VM 包装:核心错误统一转 RangeError。
|
||||
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 {
|
||||
AddZonedError::InvalidTimeZone => crate::error::create_range_error(vm, "invalid time zone"),
|
||||
AddZonedError::OutOfDateTimeRange => crate::error::create_range_error(vm, "invalid date-time"),
|
||||
AddZonedError::OutOfInstantRange => {
|
||||
crate::error::create_range_error(vm, "ZonedDateTime outside supported range")
|
||||
}
|
||||
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"),
|
||||
})
|
||||
}
|
||||
|
||||
/// ZDT add/subtract 核心:读 receiver 槽后经 add_zoned_datetime 核心叠加 duration。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. branding + 槽 0 epoch / 槽 1 时区 / 槽 2 日历。
|
||||
/// 2. duration_like_values 归一 + temporal_overflow 解析(读序:duration → options)。
|
||||
/// 3. 按 sign 取反分量后调核心,make_zoned_date_time 保时区/日历槽。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - duration 全 0 → 值不变的新对象(blank-duration 语义,由核心快速路径保持)。
|
||||
fn zoned_date_time_apply_duration<H: VmHost>(vm: &mut H, args: &[u8], sign: i64) -> NativeResult {
|
||||
let ptr = native_try!(receiver_obj(vm, args));
|
||||
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_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),
|
||||
let Some(epoch_ns) = get_instant_epoch_ns(obj) else {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid ZonedDateTime"));
|
||||
};
|
||||
let time_zone_id = to_string(obj.get_prop_at(1));
|
||||
let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
|
||||
let values = match duration_like_values(vm, val) {
|
||||
let raw_values = match duration_like_values(vm, val) {
|
||||
Ok(values) => values,
|
||||
Err(error) => return NativeResult::Err(error),
|
||||
};
|
||||
@@ -1720,56 +1915,12 @@ fn zoned_date_time_apply_duration<H: VmHost>(vm: &mut H, args: &[u8], sign: i64)
|
||||
Ok(constrain) => constrain,
|
||||
Err(error) => return NativeResult::Err(error),
|
||||
};
|
||||
const DAY_NS: i128 = 86_400_000_000_000;
|
||||
// 时间字段(hours 起)单独换算纳秒;days 由日期部分处理,避免重复计入。
|
||||
let [_, _, _, _, h, min, s, ms, us, ns] = values;
|
||||
let time_delta = duration_component_integer(h).unwrap_or(0) * 3_600_000_000_000
|
||||
+ duration_component_integer(min).unwrap_or(0) * 60_000_000_000
|
||||
+ duration_component_integer(s).unwrap_or(0) * 1_000_000_000
|
||||
+ duration_component_integer(ms).unwrap_or(0) * 1_000_000
|
||||
+ duration_component_integer(us).unwrap_or(0) * 1_000
|
||||
+ duration_component_integer(ns).unwrap_or(0);
|
||||
let total_ns = time_ns as i128 + time_delta * sign as i128;
|
||||
let extra_days = total_ns.div_euclid(DAY_NS);
|
||||
let new_time_ns = total_ns.rem_euclid(DAY_NS);
|
||||
|
||||
let [y, m, w, d, ..] = values;
|
||||
let months =
|
||||
(duration_component_integer(y).unwrap_or(0) * 12 + duration_component_integer(m).unwrap_or(0)) * sign as i128;
|
||||
let total_month = i128::from(year) * 12 + i128::from(month) - 1 + months;
|
||||
let ny = total_month.div_euclid(12);
|
||||
let nm = total_month.rem_euclid(12) + 1;
|
||||
let Some(max_day) = days_in_month(ny, nm) else {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid date"));
|
||||
};
|
||||
let day = i128::from(day);
|
||||
let new_day = if day > max_day {
|
||||
if !constrain {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "day out of range"));
|
||||
}
|
||||
max_day
|
||||
} else {
|
||||
day
|
||||
};
|
||||
let day_delta = duration_component_integer(w).unwrap_or(0) * 7 + duration_component_integer(d).unwrap_or(0);
|
||||
// 中间检查:CalendarDateAdd 后的日期 + 原 time_ns 须在 PlainDateTime 范围
|
||||
// (±MAX instant 的 {days:∓1} 在此拦截)。
|
||||
let added_days = days_from_civil(ny, nm, new_day) + day_delta * sign as i128;
|
||||
let (iy, im, id) = civil_from_days(added_days);
|
||||
if !valid_plain_date_time_range(iy as i32, im as u32, id as u32, time_ns) {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time"));
|
||||
}
|
||||
let total_days = added_days + extra_days;
|
||||
let (yy, mm, dd) = civil_from_days(total_days);
|
||||
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_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")),
|
||||
};
|
||||
// add/subtract:按 sign 取反分量(零分量保持零)。
|
||||
let values =
|
||||
std::array::from_fn(|index| if raw_values[index] == 0.0 { 0.0 } else { raw_values[index] * sign as f64 });
|
||||
let calendar_id = get_calendar_id(obj, 2);
|
||||
make_zoned_date_time(vm, epoch_ns, &time_zone_id, &calendar_id)
|
||||
let epoch = native_try!(add_zoned_datetime(vm, epoch_ns, &time_zone_id, &calendar_id, &values, constrain));
|
||||
make_zoned_date_time(vm, epoch, &time_zone_id, &calendar_id)
|
||||
}
|
||||
|
||||
/// `Temporal.ZonedDateTime.prototype.add(durationLike, options)`。
|
||||
|
||||
@@ -2491,14 +2491,12 @@ fn duration_compare_relative_zdt_and_instant() {
|
||||
),
|
||||
0.0
|
||||
);
|
||||
// Instant relativeTo(UTC 分解)同结果。
|
||||
assert_eq!(
|
||||
num(
|
||||
&mut vm,
|
||||
"Temporal.Duration.compare('P1M','P30D',{relativeTo: Temporal.Instant.fromEpochNanoseconds(7776000000000000n)})"
|
||||
),
|
||||
0.0
|
||||
);
|
||||
// Instant relativeTo:规范 13.19 无 Instant 分支,落入 bag 支按字段解析,缺 day 抛错。
|
||||
assert!(eval(
|
||||
&mut vm,
|
||||
"Temporal.Duration.compare('P1M','P30D',{relativeTo: Temporal.Instant.fromEpochNanoseconds(7776000000000000n)})"
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user