mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
feat: 实现 toZonedDateTime 与 getTimeZoneTransition
This commit is contained in:
+1
-1
@@ -25,7 +25,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.19.21"
|
||||
version = "0.19.22"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
rust-version = "1.80"
|
||||
|
||||
@@ -975,6 +975,42 @@ pub(crate) fn parse_time_zone_string(value: &str) -> Option<String> {
|
||||
whole_minute_offset(offset_id).map(|_| offset_id.to_string())
|
||||
}
|
||||
|
||||
/// ToTemporalTimeZoneIdentifier:ZDT 对象直返时区槽,字符串经 parse_time_zone_string
|
||||
/// 规范化,其余值抛 TypeError。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. ZDT 对象读槽 1(时区标识符)直接返回。
|
||||
/// 2. 字符串走 parse_time_zone_string(ParseTemporalTimeZoneString 语义,
|
||||
/// 未命中 RangeError)。
|
||||
/// 3. 非字符串非 ZDT(含普通对象)TypeError。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 字符串接受面与规范一致:UTC/Z、整数分钟偏移串、IANA 区名、带注解与尾偏移的
|
||||
/// 日期时间串;亚分钟偏移拒绝。
|
||||
pub(crate) fn to_temporal_time_zone_identifier<H: VmHost>(
|
||||
vm: &mut H, value: JsValue,
|
||||
) -> Result<String, JsValue> {
|
||||
if value.is_object() {
|
||||
let ptr = value.as_js_object_ptr();
|
||||
if !ptr.is_null() {
|
||||
let obj = unsafe { &*ptr };
|
||||
if obj.is_zoned_date_time_obj() {
|
||||
let slot = obj.get_prop_at(1);
|
||||
if slot.is_string() {
|
||||
return Ok(to_string(slot));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Err(crate::error::create_type_error(vm, "invalid time zone"));
|
||||
}
|
||||
if value.is_string() {
|
||||
let input = to_string(value);
|
||||
return parse_time_zone_string(&input)
|
||||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"));
|
||||
}
|
||||
Err(crate::error::create_type_error(vm, "invalid time zone"))
|
||||
}
|
||||
|
||||
pub(crate) fn civil_from_days(days: i128) -> (i128, i128, i128) {
|
||||
let shifted = days + 719_468;
|
||||
let era = shifted.div_euclid(146_097);
|
||||
|
||||
@@ -12,13 +12,17 @@ use super::common::{
|
||||
parse_round_options, plain_date_time_object_parts, receiver_obj, reject_partial_object_with_calendar_or_time_zone,
|
||||
resolve_date_time_parts, temporal_calendar_id, temporal_calendar_id_strict, temporal_number_component,
|
||||
temporal_option_string, temporal_option_value, temporal_overflow, temporal_to_show_calendar,
|
||||
zoned_date_time_plain_parts, DateTimeParts, ShowCalendar,
|
||||
to_temporal_time_zone_identifier, zoned_date_time_plain_parts, DateTimeParts, ShowCalendar,
|
||||
};
|
||||
use super::difference::{difference_core, parse_difference_settings, IsoDateTime};
|
||||
use super::duration::duration_like_values;
|
||||
use super::zoned_date_time::{
|
||||
disambiguate_possible_epoch_nanoseconds, get_possible_epoch_nanoseconds, get_start_of_day_epoch_ns,
|
||||
map_add_zoned_error,
|
||||
};
|
||||
use super::{
|
||||
make_plain_date, make_plain_date_time, make_plain_month_day, make_plain_year_month, plain_time_like_ns,
|
||||
round_instant_ns,
|
||||
make_plain_date, make_plain_date_time, make_plain_month_day, make_plain_year_month, make_zoned_date_time,
|
||||
plain_time_like_ns, round_instant_ns,
|
||||
};
|
||||
|
||||
/// 校验 ISO 日期分量(month 1-12、day 按月份与闰年,不依赖 chrono 年份范围)。
|
||||
@@ -669,6 +673,73 @@ pub fn plain_date_to_plain_date_time<H: VmHost>(vm: &mut H, args: &[u8]) -> Nati
|
||||
make_plain_date_time(vm, year, month, day, time_ns, &calendar)
|
||||
}
|
||||
|
||||
/// `Temporal.PlainDate.prototype.toZonedDateTime(item)`:解析时区与 plainTime,
|
||||
/// plainTime 缺省时走 GetStartOfDay 取当日首个有效墙历时刻的纪元。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. receiver branding;读槽 0-3(年/月/日/日历)。
|
||||
/// 2. item 对象:先 Get timeZone(undefined 回落 item 本身),再 Get plainTime;
|
||||
/// 非对象:timeZone 即 item,plainTime 取 undefined。
|
||||
/// 3. plainTime 为 undefined:GetStartOfDay(午夜候选为空时取间隙 transition 纪元)。
|
||||
/// 4. plainTime 在场:ToTemporalTime → ISODateTimeWithinLimits →
|
||||
/// GetPossibleEpochNanoseconds + Disambiguate(compatible)。
|
||||
/// 5. make_zoned_date_time 收尾。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 读序:timeZone 先于 plainTime(语料 order-of-operations 钉死)。
|
||||
/// - 时区臂非字符串非 ZDT 是 TypeError,非法区名是 RangeError。
|
||||
pub fn plain_date_to_zoned_datetime<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let ptr = native_try!(receiver_obj(vm, args));
|
||||
let obj = unsafe { &*ptr };
|
||||
native_try!(ensure_plain_date(vm, obj));
|
||||
let year = get_double_prop(obj, 0) as i32;
|
||||
let month = get_double_prop(obj, 1) as u32;
|
||||
let day = get_double_prop(obj, 2) as u32;
|
||||
let calendar = get_calendar_id(obj, 3);
|
||||
let item = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
|
||||
// 读序:timeZone 先于 plainTime。
|
||||
let (time_zone_like, temporal_time) = if item.is_object() {
|
||||
let ptr = item.as_js_object_ptr();
|
||||
if ptr.is_null() {
|
||||
return NativeResult::Err(crate::error::create_type_error(vm, "invalid time zone"));
|
||||
}
|
||||
let item_obj = unsafe { &*ptr };
|
||||
let time_zone_like = native_try!(temporal_option_value(vm, item_obj, item, "timeZone"));
|
||||
if time_zone_like.is_undefined() {
|
||||
(item, JsValue::undefined())
|
||||
} else {
|
||||
let temporal_time = native_try!(temporal_option_value(vm, item_obj, item, "plainTime"));
|
||||
(time_zone_like, temporal_time)
|
||||
}
|
||||
} else {
|
||||
(item, JsValue::undefined())
|
||||
};
|
||||
let time_zone_id = native_try!(to_temporal_time_zone_identifier(vm, time_zone_like));
|
||||
let time_ns = native_try!(plain_time_like_ns(vm, temporal_time));
|
||||
if temporal_time.is_undefined() {
|
||||
let epoch = get_start_of_day_epoch_ns(&time_zone_id, year, month, day)
|
||||
.map_err(|error| map_add_zoned_error(vm, error));
|
||||
let epoch_ns = native_try!(epoch);
|
||||
return make_zoned_date_time(vm, epoch_ns, &time_zone_id, &calendar);
|
||||
}
|
||||
if !valid_plain_date_time_range(year, month, day, time_ns) {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time"));
|
||||
}
|
||||
let candidates = get_possible_epoch_nanoseconds(&time_zone_id, year, month, day, time_ns as i128)
|
||||
.map_err(|error| map_add_zoned_error(vm, error));
|
||||
let candidates = native_try!(candidates);
|
||||
let epoch = disambiguate_possible_epoch_nanoseconds(
|
||||
&time_zone_id,
|
||||
(i128::from(year), i128::from(month), i128::from(day)),
|
||||
time_ns as i128,
|
||||
"compatible",
|
||||
&candidates,
|
||||
)
|
||||
.map_err(|error| map_add_zoned_error(vm, error));
|
||||
let epoch_ns = native_try!(epoch);
|
||||
make_zoned_date_time(vm, epoch_ns, &time_zone_id, &calendar)
|
||||
}
|
||||
|
||||
/// `Temporal.PlainDate.prototype.toPlainMonthDay()`:纯拷贝月/日,参考年固定 1972。
|
||||
pub fn plain_date_to_plain_month_day<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let ptr = native_try!(receiver_obj(vm, args));
|
||||
|
||||
@@ -10,13 +10,16 @@ use super::{
|
||||
civil_from_days, days_from_civil, days_in_month, days_in_month_iso, difference_core, duration_component_integer,
|
||||
duration_like_values, ensure_plain_date_time, format_iso_year, get_calendar_id, get_double_prop,
|
||||
initialize_temporal_receiver, instant_rounding_mode, is_ctor_call, is_leap_year_iso, make_plain_date,
|
||||
make_plain_date_time, make_plain_time, parse_difference_settings, parse_fractional_second_digits,
|
||||
parse_round_options, parse_temporal_string_impl, plain_date_time_object_parts, plain_time_components,
|
||||
plain_time_like_ns, receiver_obj, reject_partial_object_with_calendar_or_time_zone, resolve_date_time_parts,
|
||||
round_instant_ns, temporal_calendar_id, temporal_calendar_id_strict, temporal_number_component,
|
||||
temporal_option_number, temporal_option_string, temporal_option_value, temporal_overflow, valid_iso_date,
|
||||
valid_plain_date_time_range, valid_plain_time, zoned_date_time_plain_parts, DateTimeParts,
|
||||
FractionalSecondDigitsInput, IsoDateTime,
|
||||
make_plain_date_time, make_plain_time, make_zoned_date_time, parse_difference_settings,
|
||||
parse_fractional_second_digits, parse_round_options, parse_temporal_string_impl, plain_date_time_object_parts,
|
||||
plain_time_components, plain_time_like_ns, receiver_obj, reject_partial_object_with_calendar_or_time_zone,
|
||||
resolve_date_time_parts, round_instant_ns, temporal_calendar_id, temporal_calendar_id_strict,
|
||||
temporal_number_component, temporal_option_number, temporal_option_string, temporal_option_value,
|
||||
temporal_overflow, to_temporal_time_zone_identifier, valid_iso_date, valid_plain_date_time_range, valid_plain_time,
|
||||
zoned_date_time_plain_parts, DateTimeParts, FractionalSecondDigitsInput, IsoDateTime,
|
||||
};
|
||||
use super::zoned_date_time::{
|
||||
disambiguate_possible_epoch_nanoseconds, get_possible_epoch_nanoseconds, map_add_zoned_error,
|
||||
};
|
||||
// ───────────────────── PlainDateTime 基础方法 ─────────────────────
|
||||
|
||||
@@ -531,6 +534,69 @@ pub fn plain_date_time_to_plain_time<H: VmHost>(vm: &mut H, args: &[u8]) -> Nati
|
||||
make_plain_time(vm, total_ns)
|
||||
}
|
||||
|
||||
/// `Temporal.PlainDateTime.prototype.toZonedDateTime(temporalTimeZoneLike, [options])`:
|
||||
/// 解析时区与 disambiguation 后求墙历日期时间的纪元,返回新 ZDT。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. receiver branding;读槽 0-3(年/月/日/时间纳秒)与槽 4 日历。
|
||||
/// 2. ToTemporalTimeZoneIdentifier(ZDT 对象 / 字符串 / 其余 TypeError)。
|
||||
/// 3. options 读 disambiguation(缺省 compatible;白名单校验),先于算法校验。
|
||||
/// 4. GetPossibleEpochNanoseconds + DisambiguatePossibleEpochNanoseconds。
|
||||
/// 5. make_zoned_date_time 收尾。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 读序:timeZone 先于 options(语料 order-of-operations 钉死)。
|
||||
/// - options 非对象原始值(含 null)TypeError;非法 disambiguation 值 RangeError。
|
||||
pub fn plain_date_time_to_zoned_datetime<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let ptr = native_try!(receiver_obj(vm, args));
|
||||
let obj = unsafe { &*ptr };
|
||||
native_try!(ensure_plain_date_time(vm, obj));
|
||||
let year = get_double_prop(obj, 0) as i32;
|
||||
let month = get_double_prop(obj, 1) as u32;
|
||||
let day = get_double_prop(obj, 2) as u32;
|
||||
let time_ns = get_double_prop(obj, 3);
|
||||
let calendar = get_calendar_id(obj, 4);
|
||||
let time_zone_like = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
|
||||
let time_zone_id = native_try!(to_temporal_time_zone_identifier(vm, time_zone_like));
|
||||
// options:非对象原始值 TypeError;读 disambiguation(先于算法校验)。
|
||||
let options = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() };
|
||||
let disambiguation = if options.is_undefined() {
|
||||
"compatible".to_string()
|
||||
} else {
|
||||
if !options.is_object() {
|
||||
return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object"));
|
||||
}
|
||||
let ptr = options.as_js_object_ptr();
|
||||
if ptr.is_null() {
|
||||
return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object"));
|
||||
}
|
||||
let options_obj = unsafe { &*ptr };
|
||||
let raw = native_try!(temporal_option_value(vm, options_obj, options, "disambiguation"));
|
||||
if raw.is_undefined() {
|
||||
"compatible".to_string()
|
||||
} else {
|
||||
let value = native_try!(temporal_option_string(vm, raw));
|
||||
if !matches!(value.as_str(), "compatible" | "earlier" | "later" | "reject") {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid disambiguation"));
|
||||
}
|
||||
value
|
||||
}
|
||||
};
|
||||
let candidates = get_possible_epoch_nanoseconds(&time_zone_id, year, month, day, time_ns as i128)
|
||||
.map_err(|error| map_add_zoned_error(vm, error));
|
||||
let candidates = native_try!(candidates);
|
||||
let epoch = disambiguate_possible_epoch_nanoseconds(
|
||||
&time_zone_id,
|
||||
(i128::from(year), i128::from(month), i128::from(day)),
|
||||
time_ns as i128,
|
||||
&disambiguation,
|
||||
&candidates,
|
||||
)
|
||||
.map_err(|error| map_add_zoned_error(vm, error));
|
||||
let epoch_ns = native_try!(epoch);
|
||||
make_zoned_date_time(vm, epoch_ns, &time_zone_id, &calendar)
|
||||
}
|
||||
|
||||
/// `Temporal.PlainDateTime.prototype.withCalendar(calendar)`:只换日历槽,日期时间分量不变。
|
||||
///
|
||||
/// # 步骤
|
||||
|
||||
@@ -1221,7 +1221,7 @@ pub(crate) fn named_zone_possible_epoch_ns(zone: &str, wall_ns: i128) -> Option<
|
||||
Some(candidates)
|
||||
}
|
||||
|
||||
/// 找 0 候选墙历所落间隙的 transition,返回间隙前后偏移(秒)。
|
||||
/// 找 0 候选墙历所落间隙的 transition:返回 (transition 纪元秒, 间隙前偏移, 间隙后偏移)。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. 墙历(按 UTC 解释)在 transition 前区间:查下一 transition 是否正偏移跳变
|
||||
@@ -1229,13 +1229,12 @@ pub(crate) fn named_zone_possible_epoch_ns(zone: &str, wall_ns: i128) -> Option<
|
||||
/// 2. 墙历在 transition 后区间:查上一 transition 同条件。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 区表未命中或墙历不落在任何间隙内返回 `None`(调用方决定错误)。
|
||||
pub(crate) fn tz_gap_offsets(zone: &str, wall_ns: i128) -> Option<(i64, i64)> {
|
||||
let z = tz_zone_data(zone)?;
|
||||
/// - 墙历不落在任何间隙内返回 `None`(调用方决定错误)。
|
||||
/// - 间隙起点(墙历 = t + o_before)候选集为空,计入间隙。
|
||||
fn tz_gap_transition(z: &TzifZone, wall_ns: i128) -> Option<(i64, i64, i64)> {
|
||||
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 };
|
||||
@@ -1243,7 +1242,7 @@ pub(crate) fn tz_gap_offsets(zone: &str, wall_ns: i128) -> Option<(i64, i64)> {
|
||||
&& 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)));
|
||||
return Some((t, i64::from(offset_before), i64::from(offset_after)));
|
||||
}
|
||||
}
|
||||
// 墙历在 transition 后区间:间隙由上一 transition 产生。
|
||||
@@ -1254,12 +1253,31 @@ pub(crate) fn tz_gap_offsets(zone: &str, wall_ns: i128) -> Option<(i64, i64)> {
|
||||
&& 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)));
|
||||
return Some((t, i64::from(offset_before), i64::from(offset_after)));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 找 0 候选墙历所落间隙的 transition,返回间隙前后偏移(秒)。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 区表未命中或墙历不落在任何间隙内返回 `None`(调用方决定错误)。
|
||||
pub(crate) fn tz_gap_offsets(zone: &str, wall_ns: i128) -> Option<(i64, i64)> {
|
||||
let z = tz_zone_data(zone)?;
|
||||
let (_, before, after) = tz_gap_transition(z, wall_ns)?;
|
||||
Some((before, after))
|
||||
}
|
||||
|
||||
/// 找 0 候选墙历所落间隙的 transition 纪元秒(GetStartOfDay 间隙臂专用)。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 区表未命中或墙历不落在任何间隙内返回 `None`(调用方决定错误)。
|
||||
pub(crate) fn tz_gap_transition_epoch(zone: &str, wall_ns: i128) -> Option<i64> {
|
||||
let z = tz_zone_data(zone)?;
|
||||
Some(tz_gap_transition(z, wall_ns)?.0)
|
||||
}
|
||||
|
||||
/// 统一时区偏移查找(epoch 依赖):固定偏移区(数值偏移串)走快速路径恒返常量,
|
||||
/// IANA 区走 transition 表二分。
|
||||
///
|
||||
|
||||
@@ -25,7 +25,10 @@ use super::instant::{
|
||||
instant_rounding_mode, instant_string_without_annotations, parse_instant_string, primitive_to_bigint,
|
||||
round_instant_ns, InstantRoundingMode,
|
||||
};
|
||||
use super::time_zone::{named_zone_possible_epoch_ns, tz_gap_offsets, zone_offset_seconds};
|
||||
use super::time_zone::{
|
||||
named_zone_possible_epoch_ns, tz_gap_offsets, tz_gap_transition_epoch, tz_next_transition,
|
||||
tz_prev_transition, 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_components_signed,
|
||||
@@ -1621,6 +1624,89 @@ pub fn zoned_date_time_offset_nanoseconds<H: VmHost>(vm: &mut H, args: &[u8]) ->
|
||||
NativeResult::Ok(JsValue::float(offset_seconds as f64 * 1_000_000_000.0))
|
||||
}
|
||||
|
||||
/// `Temporal.ZonedDateTime.prototype.getTimeZoneTransition(directionParam)`:
|
||||
/// 查 next/previous 时间区 transition,返回 transition 纪元处的新 ZDT;
|
||||
/// 固定偏移区与无 transition 返回 null。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. receiver branding;读槽 0(纪元)/1(时区)/2(日历)。
|
||||
/// 2. directionParam:undefined → TypeError;字符串直接取值;对象读
|
||||
/// direction 属性;其余原始值 → TypeError。
|
||||
/// 3. GetDirectionOption:缺省/undefined → RangeError;ToString(Symbol →
|
||||
/// TypeError);非 next/previous → RangeError。
|
||||
/// 4. 固定偏移区 → null。
|
||||
/// 5. next 取严格大于当前纪元的 transition,previous 取严格小于的;
|
||||
/// next 超上界(t > nsMaxInstant)或 previous 超下界(t < nsMinInstant)
|
||||
/// 或无 transition → null。
|
||||
/// 6. make_zoned_date_time 收尾。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 读序先于固定偏移判定(direction 读取在 IsOffsetTimeZoneIdentifier 之前)。
|
||||
pub fn zoned_date_time_get_time_zone_transition<H: VmHost>(vm: &mut H, args: &[u8]) -> 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 calendar = get_calendar_id(obj, 2);
|
||||
let Some(epoch_ns) = get_instant_epoch_ns(obj) else {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid ZonedDateTime"));
|
||||
};
|
||||
let direction_param = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
|
||||
if direction_param.is_undefined() {
|
||||
return NativeResult::Err(crate::error::create_type_error(vm, "direction is required"));
|
||||
}
|
||||
// 读序:direction 先于固定偏移判定。字符串包装 bag 后取值即原串;
|
||||
// 对象走 Get(getter 可观察)。
|
||||
let direction_raw = if direction_param.is_string() {
|
||||
direction_param
|
||||
} else if direction_param.is_object() {
|
||||
let ptr = direction_param.as_js_object_ptr();
|
||||
if ptr.is_null() {
|
||||
return NativeResult::Err(crate::error::create_type_error(
|
||||
vm,
|
||||
"direction must be a string or an object",
|
||||
));
|
||||
}
|
||||
let options = unsafe { &*ptr };
|
||||
native_try!(temporal_option_value(vm, options, direction_param, "direction"))
|
||||
} else {
|
||||
return NativeResult::Err(crate::error::create_type_error(
|
||||
vm,
|
||||
"direction must be a string or an object",
|
||||
));
|
||||
};
|
||||
let direction = if direction_raw.is_undefined() {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "direction is required"));
|
||||
} else {
|
||||
let value = native_try!(temporal_option_string(vm, direction_raw));
|
||||
if !matches!(value.as_str(), "next" | "previous") {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid direction"));
|
||||
}
|
||||
value
|
||||
};
|
||||
// 固定偏移区:无 transition。
|
||||
if fixed_offset_seconds(&time_zone_id).is_some() {
|
||||
return NativeResult::Ok(JsValue::null());
|
||||
}
|
||||
let epoch_s = epoch_ns.div_euclid(1_000_000_000) as i64;
|
||||
let transition_s = if direction == "next" {
|
||||
tz_next_transition(&time_zone_id, epoch_s)
|
||||
} else {
|
||||
tz_prev_transition(&time_zone_id, epoch_s)
|
||||
};
|
||||
let Some(transition_s) = transition_s else {
|
||||
return NativeResult::Ok(JsValue::null());
|
||||
};
|
||||
let transition_ns = i128::from(transition_s) * 1_000_000_000;
|
||||
// 规范 11.1.3/11.1.4:next 要求 t ≤ nsMaxInstant,previous 要求 t ≥ nsMinInstant。
|
||||
if (direction == "next" && transition_ns > MAX_INSTANT_NS)
|
||||
|| (direction == "previous" && transition_ns < -MAX_INSTANT_NS)
|
||||
{
|
||||
return NativeResult::Ok(JsValue::null());
|
||||
}
|
||||
make_zoned_date_time(vm, transition_ns, &time_zone_id, &calendar)
|
||||
}
|
||||
|
||||
/// `Temporal.ZonedDateTime.prototype.monthCode`:ISO 日历下恒为 `M{month:02}` 补零格式。
|
||||
pub fn zoned_date_time_month_code<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let ptr = native_try!(receiver_obj(vm, args));
|
||||
@@ -1659,6 +1745,33 @@ 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)
|
||||
}
|
||||
|
||||
/// GetStartOfDay(规范 11.1.14):当地日期首个有效墙历时刻的纪元纳秒。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. 枚举午夜候选集(GetPossibleEpochNanoseconds):非空取首(固定偏移区候选唯一)。
|
||||
/// 2. 候选集为空(午夜落 DST 间隙):取间隙 transition 纪元 t——间隙
|
||||
/// [t + oBefore, t + oAfter) 中首个有效墙历 t + oAfter 恰对应纪元 t。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 不复用 start_of_day_epoch_ns(naive `days*DAY_NS - offset`,午夜落间隙时
|
||||
/// 返回 transition 前墙历,合规范错误)。
|
||||
/// - 候选超 Instant 范围抛 OutOfInstantRange;transition 纪元超范围同理。
|
||||
pub(crate) fn get_start_of_day_epoch_ns(
|
||||
time_zone: &str, year: i32, month: u32, day: u32,
|
||||
) -> Result<i128, AddZonedError> {
|
||||
let candidates = get_possible_epoch_nanoseconds(time_zone, year, month, day, 0)?;
|
||||
if let Some(&first) = candidates.first() {
|
||||
return Ok(first);
|
||||
}
|
||||
let wall_ns = days_from_civil(i128::from(year), i128::from(month), i128::from(day)) * DAY_NS;
|
||||
let transition = tz_gap_transition_epoch(time_zone, wall_ns).ok_or(AddZonedError::OutOfDateTimeRange)?;
|
||||
let epoch = i128::from(transition) * 1_000_000_000;
|
||||
if epoch.unsigned_abs() > MAX_INSTANT_NS as u128 {
|
||||
return Err(AddZonedError::OutOfInstantRange);
|
||||
}
|
||||
Ok(epoch)
|
||||
}
|
||||
|
||||
/// AddZonedDateTime 核心的错误分类(包装层统一转 RangeError)。
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) enum AddZonedError {
|
||||
@@ -1749,14 +1862,18 @@ pub(crate) fn get_possible_epoch_nanoseconds(
|
||||
/// DisambiguatePossibleEpochNanoseconds(规范 11.1.12):从候选集选唯一纪元。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. 1 候选 → 直接返回。
|
||||
/// 1. 1 候选 → 直接返回(含 reject,规范 2a 在 reject 判定之前)。
|
||||
/// 2. 2 候选(重叠)→ compatible/earlier 取较早纪元,later 取较晚,reject 抛 RangeError。
|
||||
/// 3. 0 候选(间隙)→ 按间隙前后偏移差平移墙历(earlier 反向、compatible/later 正向),
|
||||
/// 重枚举候选集后取首(earlier)/末(compatible、later)。
|
||||
/// 重枚举候选集后取首(earlier)/末(compatible、later);reject 抛 RangeError。
|
||||
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() {
|
||||
// 单候选(固定偏移区 / 无歧义墙历):直接返回,含 reject 也不抛。
|
||||
if candidates.len() == 1 {
|
||||
return Ok(candidates[0]);
|
||||
}
|
||||
if disambiguation == "reject" {
|
||||
return Err(AddZonedError::OutOfDateTimeRange);
|
||||
}
|
||||
|
||||
@@ -464,6 +464,11 @@ pub fn bind_temporal(env: BindEnv<'_>, global: &mut JsObject) {
|
||||
oxide_builtins::temporal::zoned_date_time_start_of_day::<crate::vm::Vm> as *const (),
|
||||
0,
|
||||
),
|
||||
(
|
||||
"getTimeZoneTransition",
|
||||
oxide_builtins::temporal::zoned_date_time_get_time_zone_transition::<crate::vm::Vm> as *const (),
|
||||
1,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -628,6 +633,11 @@ pub fn bind_temporal(env: BindEnv<'_>, global: &mut JsObject) {
|
||||
oxide_builtins::temporal::plain_date_to_plain_year_month::<crate::vm::Vm> as *const (),
|
||||
0,
|
||||
),
|
||||
(
|
||||
"toZonedDateTime",
|
||||
oxide_builtins::temporal::plain_date_to_zoned_datetime::<crate::vm::Vm> as *const (),
|
||||
1,
|
||||
),
|
||||
("round", oxide_builtins::temporal::plain_date_round::<crate::vm::Vm> as *const (), 1),
|
||||
],
|
||||
);
|
||||
@@ -854,6 +864,11 @@ pub fn bind_temporal(env: BindEnv<'_>, global: &mut JsObject) {
|
||||
oxide_builtins::temporal::plain_date_time_to_plain_time::<crate::vm::Vm> as *const (),
|
||||
0,
|
||||
),
|
||||
(
|
||||
"toZonedDateTime",
|
||||
oxide_builtins::temporal::plain_date_time_to_zoned_datetime::<crate::vm::Vm> as *const (),
|
||||
1,
|
||||
),
|
||||
(
|
||||
"valueOf",
|
||||
oxide_builtins::temporal::plain_date_time_value_of::<crate::vm::Vm> as *const (),
|
||||
|
||||
@@ -5017,3 +5017,67 @@ fn now_iso_method_bindings_are_plain_functions() {
|
||||
.unwrap();
|
||||
assert_eq!(str_val(&vm, r), "true|true|true|true|true");
|
||||
}
|
||||
|
||||
// -- toZonedDateTime / getTimeZoneTransition 回归钉 --
|
||||
|
||||
#[test]
|
||||
fn plain_date_to_zoned_datetime_fixed_offset_and_errors() {
|
||||
let mut vm = Vm::new();
|
||||
// 固定偏移区:GetStartOfDay 取当地午夜(墙历减偏移),候选唯一;
|
||||
// reject 对单候选不抛(规范 2a 在 reject 判定之前);缺区 / 非法区抛错。
|
||||
let r = eval(
|
||||
&mut vm,
|
||||
"(new Temporal.PlainDate(2019, 2, 16).toZonedDateTime('+03:30').epochNanoseconds === 1550262600000000000n) + '|' +
|
||||
(() => { try { return new Temporal.PlainDate(2019, 2, 16).toZonedDateTime('+03:30', { disambiguation: 'reject' }).epochNanoseconds === 1550262600000000000n; }
|
||||
catch (e) { return false; } })() + '|' +
|
||||
(() => { try { new Temporal.PlainDate(2019, 2, 16).toZonedDateTime(); return 'no'; }
|
||||
catch (e) { return e instanceof TypeError; } })() + '|' +
|
||||
(() => { try { new Temporal.PlainDate(2019, 2, 16).toZonedDateTime('Not/AZone'); return 'no'; }
|
||||
catch (e) { return e instanceof RangeError; } })()",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(str_val(&vm, r), "true|true|true|true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_date_time_to_zoned_datetime_fixed_offset_and_errors() {
|
||||
let mut vm = Vm::new();
|
||||
// 固定偏移区:墙历减偏移得唯一候选;reject 对单候选不抛(曾误抛 RangeError);
|
||||
// options 为原始值 / 非法 disambiguation 抛错。
|
||||
let r = eval(
|
||||
&mut vm,
|
||||
"(new Temporal.PlainDateTime(2019, 2, 16, 23, 45).toZonedDateTime('+03:30').epochNanoseconds === 1550348100000000000n) + '|' +
|
||||
(() => { try { return new Temporal.PlainDateTime(2019, 2, 16, 23, 45).toZonedDateTime('+03:30', { disambiguation: 'reject' }).epochNanoseconds === 1550348100000000000n; }
|
||||
catch (e) { return false; } })() + '|' +
|
||||
(() => { try { new Temporal.PlainDateTime(2019, 2, 16, 23, 45).toZonedDateTime('+03:30', 'earlier'); return 'no'; }
|
||||
catch (e) { return e instanceof TypeError; } })() + '|' +
|
||||
(() => { try { new Temporal.PlainDateTime(2019, 2, 16, 23, 45).toZonedDateTime('+03:30', { disambiguation: 'sideways' }); return 'no'; }
|
||||
catch (e) { return e instanceof RangeError; } })()",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(str_val(&vm, r), "true|true|true|true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zdt_get_time_zone_transition_fixed_and_errors() {
|
||||
let mut vm = Vm::new();
|
||||
// 固定偏移区无 transition → null;缺 direction / 非法值 / 对象缺 direction 抛错;
|
||||
// IANA 区 next/previous 严格大于/小于当前纪元,对象形式与字符串形式等价。
|
||||
let r = eval(
|
||||
&mut vm,
|
||||
"(new Temporal.ZonedDateTime(1000n, '+03:30').getTimeZoneTransition('next') === null) + '|' +
|
||||
(() => { try { new Temporal.ZonedDateTime(1000n, 'America/Santiago').getTimeZoneTransition(); return 'no'; }
|
||||
catch (e) { return e instanceof TypeError; } })() + '|' +
|
||||
(() => { try { new Temporal.ZonedDateTime(1000n, 'America/Santiago').getTimeZoneTransition('sideways'); return 'no'; }
|
||||
catch (e) { return e instanceof RangeError; } })() + '|' +
|
||||
(() => { try { new Temporal.ZonedDateTime(1000n, 'America/Santiago').getTimeZoneTransition({}); return 'no'; }
|
||||
catch (e) { return e instanceof RangeError; } })() + '|' +
|
||||
(() => { const z = new Temporal.ZonedDateTime(1520817600000000000n, 'America/Santiago');
|
||||
return z.getTimeZoneTransition('next').epochNanoseconds > z.epochNanoseconds &&
|
||||
z.epochNanoseconds > z.getTimeZoneTransition('previous').epochNanoseconds; })() + '|' +
|
||||
(() => { const z = new Temporal.ZonedDateTime(1520817600000000000n, 'America/Santiago');
|
||||
return z.getTimeZoneTransition({ direction: 'next' }).epochNanoseconds === z.getTimeZoneTransition('next').epochNanoseconds; })()",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(str_val(&vm, r), "true|true|true|true|true|true");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user