mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-24 18:51: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::time_zone::zone_offset_seconds;
|
||||||
use super::zoned_date_time::{
|
use super::zoned_date_time::{
|
||||||
add_zoned_datetime, difference_zoned_datetime_with_rounding_vm, difference_zoned_datetime_with_total_vm,
|
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};
|
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 载荷。
|
/// 2. PlainDateTime / PlainDate 对象 → 直读 prop 0/1/2,Wall 载荷。
|
||||||
/// 3. ZonedDateTime → Zoned 载荷(zoned_date_time_plain_parts 范围检查后直读三槽)。
|
/// 3. ZonedDateTime → Zoned 载荷(zoned_date_time_plain_parts 范围检查后直读三槽)。
|
||||||
/// 4. 其他对象(含 Instant)按 property bag 解析:字段按规范字典序单读(calendar 先,
|
/// 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。
|
/// 5. undefined → None;其余原始值 → TypeError。
|
||||||
///
|
///
|
||||||
/// # 边界与前提
|
/// # 边界与前提
|
||||||
/// - 调用方须已把 raw 从 options 取出(读序由调用方保证)。
|
/// - 调用方须已把 raw 从 options 取出(读序由调用方保证)。
|
||||||
/// - bag 支 offset 语法校验在字典序位(nanosecond 后)立即完成,先于后续字段转换;
|
/// - 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> {
|
fn duration_relative_to_date<H: VmHost>(vm: &mut H, relative_raw: JsValue) -> Result<Option<RelativePoint>, JsValue> {
|
||||||
if relative_raw.is_undefined() {
|
if relative_raw.is_undefined() {
|
||||||
return Ok(None);
|
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。
|
// year 与 era/eraYear 同场指向不同年 RangeError。
|
||||||
let year = merge_bag_era_year(vm, year, era, era_year)?;
|
let year = merge_bag_era_year(vm, year, era, era_year)?;
|
||||||
|
|
||||||
// 袋支尾部保持 Wall 载荷(Zoned 载荷归后续任务)。
|
|
||||||
let (year, month, day, time_ns, calendar) = resolve_date_time_parts(
|
let (year, month, day, time_ns, calendar) = resolve_date_time_parts(
|
||||||
vm,
|
vm,
|
||||||
calendar,
|
calendar,
|
||||||
@@ -506,21 +508,47 @@ fn duration_relative_to_date<H: VmHost>(vm: &mut H, relative_raw: JsValue) -> Re
|
|||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
// offset-vs-zone 校验(对象支精确一致、reject 语义):offset 与 timeZone 同时在场时,
|
let calendar = calendar.unwrap_or_else(|| "iso8601".to_string());
|
||||||
// 候选 epoch 点 zone 偏移须与输入偏移秒级相等。
|
// timeZone 缺省:Wall 载荷(墙历路径)。
|
||||||
if let (Some(offset), Some(tz_id)) = (bag_offset_seconds, time_zone_id.as_deref()) {
|
let Some(time_zone) = time_zone_id else {
|
||||||
let candidate_ns = local_to_epoch_ns(year, month, day, time_ns, 0)
|
return Ok(Some(RelativePoint::Wall {
|
||||||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid date-time"))?;
|
date: (i128::from(year), i128::from(month), i128::from(day)),
|
||||||
let zone_offset = zone_offset_seconds(tz_id, candidate_ns.div_euclid(1_000_000_000) as i64)
|
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"))?;
|
.ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))?;
|
||||||
if zone_offset != offset {
|
if zone_offset != offset {
|
||||||
return Err(crate::error::create_range_error(vm, "offset and time zone disagree"));
|
return Err(crate::error::create_range_error(vm, "offset and time zone disagree"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Some(RelativePoint::Wall {
|
let candidates = get_possible_epoch_nanoseconds(&time_zone, year, month, day, time_ns as i128)
|
||||||
date: (i128::from(year), i128::from(month), i128::from(day)),
|
.map_err(|error| map_add_zoned_error(vm, error))?;
|
||||||
calendar: calendar.unwrap_or_else(|| "iso8601".to_string()),
|
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)。
|
/// 袋支数值字段:undefined → None,其余 ToPrimitive(Number) 截断(Symbol/BigInt TypeError)。
|
||||||
|
|||||||
@@ -1873,7 +1873,7 @@ pub(crate) fn add_zoned_datetime<H: VmHost>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// AddZonedError 到 JS RangeError 的映射(各 zoned 面 VM 包装共用)。
|
/// 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 {
|
match error {
|
||||||
AddZonedError::InvalidTimeZone => crate::error::create_range_error(vm, "invalid time zone"),
|
AddZonedError::InvalidTimeZone => crate::error::create_range_error(vm, "invalid time zone"),
|
||||||
AddZonedError::OutOfDateTimeRange => crate::error::create_range_error(vm, "invalid date-time"),
|
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"
|
"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