mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-24 02:31:13 +08:00
feat: temporal 袋支 relativeTo 时区在场产 Zoned 载荷
This commit is contained in:
@@ -21,7 +21,8 @@ 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,
|
||||
disambiguate_possible_epoch_nanoseconds, extract_time_zone_annotation, get_possible_epoch_nanoseconds,
|
||||
map_add_zoned_error, parse_any_offset_seconds, valid_offset_fraction, zoned_date_time_string_parts,
|
||||
};
|
||||
use super::{make_duration, parse_plain_date_time_string};
|
||||
|
||||
@@ -340,13 +341,15 @@ fn relative_point_wall_date<H: VmHost>(vm: &mut H, point: &RelativePoint) -> Res
|
||||
/// 2. PlainDateTime / PlainDate 对象 → 直读 prop 0/1/2,Wall 载荷。
|
||||
/// 3. ZonedDateTime → Zoned 载荷(zoned_date_time_plain_parts 范围检查后直读三槽)。
|
||||
/// 4. 其他对象(含 Instant)按 property bag 解析:字段按规范字典序单读(calendar 先,
|
||||
/// 非 iso8601 日历读 era/eraYear),逐字段 Get 后立即转换,constrain,Wall 载荷。
|
||||
/// 非 iso8601 日历读 era/eraYear),逐字段 Get 后立即转换,constrain;
|
||||
/// timeZone 缺省 → Wall 载荷;在场 → InterpretISODateTimeOffset(wall/option 两行为)
|
||||
/// 得 Zoned 载荷。
|
||||
/// 5. undefined → None;其余原始值 → TypeError。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 调用方须已把 raw 从 options 取出(读序由调用方保证)。
|
||||
/// - bag 支 offset 语法校验在字典序位(nanosecond 后)立即完成,先于后续字段转换;
|
||||
/// offset-vs-zone 匹配校验在字段转换之后。
|
||||
/// offset-vs-zone 匹配校验在字段转换之后;timeZone 在场时经候选消歧/精确匹配产 Zoned 载荷。
|
||||
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);
|
||||
@@ -487,7 +490,6 @@ fn duration_relative_to_date<H: VmHost>(vm: &mut H, relative_raw: JsValue) -> Re
|
||||
// year 与 era/eraYear 同场指向不同年 RangeError。
|
||||
let year = merge_bag_era_year(vm, year, era, era_year)?;
|
||||
|
||||
// 袋支尾部保持 Wall 载荷(Zoned 载荷归后续任务)。
|
||||
let (year, month, day, time_ns, calendar) = resolve_date_time_parts(
|
||||
vm,
|
||||
calendar,
|
||||
@@ -506,21 +508,47 @@ fn duration_relative_to_date<H: VmHost>(vm: &mut H, relative_raw: JsValue) -> Re
|
||||
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()) {
|
||||
let candidate_ns = local_to_epoch_ns(year, month, day, time_ns, 0)
|
||||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid date-time"))?;
|
||||
let zone_offset = zone_offset_seconds(tz_id, candidate_ns.div_euclid(1_000_000_000) as i64)
|
||||
let calendar = calendar.unwrap_or_else(|| "iso8601".to_string());
|
||||
// timeZone 缺省:Wall 载荷(墙历路径)。
|
||||
let Some(time_zone) = time_zone_id else {
|
||||
return Ok(Some(RelativePoint::Wall {
|
||||
date: (i128::from(year), i128::from(month), i128::from(day)),
|
||||
calendar,
|
||||
}));
|
||||
};
|
||||
// timeZone 在场:InterpretISODateTimeOffset(袋支恒 match-exactly)。
|
||||
// offset 在场(~option~):候选 epoch 点 zone 偏移与输入偏移秒级精确相等(reject 语义),
|
||||
// 再从候选集选命中候选(candidateOffset = utcEpochNs - candidate,秒级含亚分钟)。
|
||||
// offset 缺省(~wall~):compatible 消歧(重叠取 earlier、间隙平移取 later 侧)。
|
||||
// 固定偏移区退化:候选恒 1、偏移恒常量,与 local_to_epoch_ns 数值恒等。
|
||||
let wall_ns = local_to_epoch_ns(year, month, day, time_ns, 0)
|
||||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid date-time"))?;
|
||||
if let Some(offset) = bag_offset_seconds {
|
||||
let zone_offset = zone_offset_seconds(&time_zone, wall_ns.div_euclid(1_000_000_000) as i64)
|
||||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))?;
|
||||
if zone_offset != offset {
|
||||
return Err(crate::error::create_range_error(vm, "offset and time zone disagree"));
|
||||
}
|
||||
}
|
||||
Ok(Some(RelativePoint::Wall {
|
||||
date: (i128::from(year), i128::from(month), i128::from(day)),
|
||||
calendar: calendar.unwrap_or_else(|| "iso8601".to_string()),
|
||||
}))
|
||||
let candidates = get_possible_epoch_nanoseconds(&time_zone, year, month, day, time_ns as i128)
|
||||
.map_err(|error| map_add_zoned_error(vm, error))?;
|
||||
let epoch_ns = if let Some(offset) = bag_offset_seconds {
|
||||
candidates
|
||||
.iter()
|
||||
.find(|candidate| (wall_ns - *candidate) / 1_000_000_000 == i128::from(offset))
|
||||
.copied()
|
||||
.ok_or_else(|| crate::error::create_range_error(vm, "offset and time zone disagree"))?
|
||||
} else {
|
||||
disambiguate_possible_epoch_nanoseconds(
|
||||
&time_zone,
|
||||
(i128::from(year), i128::from(month), i128::from(day)),
|
||||
time_ns as i128,
|
||||
"compatible",
|
||||
&candidates,
|
||||
)
|
||||
.map_err(|error| map_add_zoned_error(vm, error))?
|
||||
};
|
||||
Ok(Some(RelativePoint::Zoned { epoch_ns, time_zone, calendar }))
|
||||
}
|
||||
|
||||
/// 袋支数值字段:undefined → None,其余 ToPrimitive(Number) 截断(Symbol/BigInt TypeError)。
|
||||
|
||||
@@ -1873,7 +1873,7 @@ pub(crate) fn add_zoned_datetime<H: VmHost>(
|
||||
}
|
||||
|
||||
/// AddZonedError 到 JS RangeError 的映射(各 zoned 面 VM 包装共用)。
|
||||
fn map_add_zoned_error<H: VmHost>(vm: &mut H, error: AddZonedError) -> JsValue {
|
||||
pub(crate) 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"),
|
||||
|
||||
@@ -4028,3 +4028,38 @@ fn relativeto_bag_read_order_calendar_first_dictionary() {
|
||||
"calendar,day,hour,microsecond,millisecond,minute,month,monthCode,nanosecond,second,year"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relativeto_bag_zoned_payload() {
|
||||
let mut vm = Vm::new();
|
||||
// 袋支 Zoned 载荷(timeZone 在场):固定区 wall/option 行为 P1D 恒 24h(与 local_to_epoch_ns
|
||||
// 数值恒等红线);IANA 区 DST 经候选消歧——间隙日 25h 使 P1D = 23h 且
|
||||
// P1D < P24H(epoch 级日长)、间隙墙历 compatible 平移后不抛错、重叠日 23h 使 P1D = 25h;
|
||||
// offset 与 zone 冲突(含间隙无候选)RangeError。
|
||||
let r = eval(
|
||||
&mut vm,
|
||||
"(() => {
|
||||
const kind = (fn) => { try { return String(fn()); } catch (e) { return e.constructor.name; } };
|
||||
const d1 = new Temporal.Duration(0, 0, 0, 1);
|
||||
const d24 = new Temporal.Duration(0, 0, 0, 0, 24);
|
||||
const total = (rel) => d1.total({ unit: 'hours', relativeTo: rel });
|
||||
const cmp = (rel) => Temporal.Duration.compare(d1, d24, { relativeTo: rel });
|
||||
return [
|
||||
kind(() => total({ year: 2025, month: 3, day: 9, hour: 2, minute: 30, timeZone: 'UTC' })),
|
||||
kind(() => total({ year: 2025, month: 3, day: 9, hour: 2, minute: 30, timeZone: '+05:30' })),
|
||||
kind(() => total({ year: 2025, month: 3, day: 9, hour: 2, minute: 30, timeZone: 'UTC', offset: '+00:00' })),
|
||||
kind(() => total({ year: 2025, month: 3, day: 9, hour: 2, minute: 30, timeZone: '+05:30', offset: '+05:30' })),
|
||||
kind(() => total({ year: 1970, month: 1, day: 1, offset: '+00:45:00.000000000', timeZone: '+00:45' })),
|
||||
kind(() => total({ year: 2025, month: 3, day: 9, hour: 0, minute: 30, timeZone: 'America/New_York' })),
|
||||
kind(() => cmp({ year: 2025, month: 3, day: 9, hour: 0, minute: 30, timeZone: 'America/New_York' })),
|
||||
kind(() => total({ year: 2025, month: 3, day: 9, hour: 2, minute: 30, timeZone: 'America/New_York' })),
|
||||
kind(() => total({ year: 2025, month: 11, day: 2, hour: 1, minute: 30, timeZone: 'America/New_York' })),
|
||||
kind(() => total({ year: 2025, month: 11, day: 2, hour: 1, minute: 30, timeZone: 'America/New_York', offset: '-04:00' })),
|
||||
kind(() => total({ year: 2025, month: 3, day: 9, hour: 2, minute: 30, timeZone: 'America/New_York', offset: '-05:00' })),
|
||||
kind(() => total({ year: 2025, month: 3, day: 9, hour: 2, timeZone: 'UTC', offset: '+05:30' })),
|
||||
].join('|');
|
||||
})()",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(str_val(&vm, r), "24|24|24|24|24|23|-1|24|25|25|RangeError|RangeError");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user