feat: 实现 toZonedDateTime 与 getTimeZoneTransition

This commit is contained in:
2026-10-16 01:58:42 +00:00
parent 743ca73b24
commit 6dbf6be3a8
8 changed files with 408 additions and 21 deletions
+1 -1
View File
@@ -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");
}