Files
OxideJS-New/crates/oxide_builtins/src/temporal/common.rs
T

1423 lines
57 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Temporal 命名空间对象的存储约定:各类型实例的载荷按固定 prop 槽(对象
//! 内置属性表的下标位置)写入,槽外不设隐藏字段。
//!
//! 槽位布局:
//! - Temporal.Instant:prop 0 存纪元纳秒(BigInt)。
//! - Temporal.Duration:prop 0-9 依次存年、月、周、日、时、分、秒、毫秒、
//! 微秒、纳秒(均为 f64)。
//! - Temporal.PlainDate:prop 0-3 存年、月、日、日历 ID。
//! - Temporal.PlainTime:prop 0 存午夜后纳秒(f64)。
//! - Temporal.PlainDateTime:prop 0-4 存年、月、日、午夜后纳秒、日历 ID。
//! - Temporal.PlainMonthDay:prop 0-3 存月、日、参考年、日历 ID。
//! - Temporal.PlainYearMonth:prop 0-3 存年、月、参考日、日历 ID。
//! - Temporal.ZonedDateTime:prop 0-2 存纪元纳秒、时区 ID、日历 ID。
//!
//! 日历 ID 未显式给定时统一取 "iso8601"。
use num_traits::ToPrimitive;
use oxide_kernel::shape_forge::EMPTY_SHAPE_ID;
use oxide_runtime_api::{to_number, to_string, NativeResult, ProtoKind, VmHost};
use oxide_types::object::JsObject;
use oxide_types::value::JsValue;
use super::time_zone::{tz_canonical_name, zone_offset_seconds};
use super::{
parse_iso_date, parse_plain_date_time_string, parse_plain_time_string, valid_iso_date, valid_plain_date_time_range,
valid_plain_time,
};
pub(crate) const MAX_INSTANT_NS: i128 = 8_640_000_000_000_000_000_000;
macro_rules! native_try {
($expr:expr) => {
match $expr {
Ok(value) => value,
Err(err) => return NativeResult::Err(err),
}
};
}
pub(crate) use native_try;
pub(crate) fn get_double_prop(obj: &JsObject, pos: usize) -> f64 {
let v = obj.get_prop_at(pos);
if v.is_double() {
v.as_double()
} else {
f64::NAN
}
}
/// 读对象日历槽:字符串槽直接返回,undefined/非 string 兜底 `"iso8601"`。
/// 只读 prop 槽,不触发属性 getter,getter 与日历传播共用。
pub(crate) fn get_calendar_id(obj: &JsObject, slot: usize) -> String {
let value = obj.get_prop_at(slot);
if value.is_string() {
to_string(value)
} else {
"iso8601".to_string()
}
}
pub(crate) fn get_instant_epoch_ns(obj: &JsObject) -> Option<i128> {
let value = obj.get_prop_at(0);
if value.is_bigint() {
return Some(unsafe { oxide_runtime_api::bigint_data(value) }.to_i128().unwrap_or(i128::MAX));
}
if value.is_int() || value.is_double() {
let n = to_number(value);
// f64 转 i128:超大值饱和到 i128 边界。
if n.is_finite() && n >= i128::MIN as f64 && n <= i128::MAX as f64 {
return Some(n as i128);
}
return Some(i128::MAX);
}
None
}
pub(crate) fn ensure_instant<H: VmHost>(vm: &mut H, obj: &JsObject) -> Result<(), JsValue> {
if !obj.is_instant_obj() {
return Err(crate::error::create_type_error(vm, "called on incompatible receiver"));
}
Ok(())
}
pub(crate) fn ensure_plain_date<H: VmHost>(vm: &mut H, obj: &JsObject) -> Result<(), JsValue> {
if !obj.is_plain_date_obj() {
return Err(crate::error::create_type_error(vm, "called on incompatible receiver"));
}
Ok(())
}
pub(crate) fn ensure_plain_time<H: VmHost>(vm: &mut H, obj: &JsObject) -> Result<(), JsValue> {
if !obj.is_plain_time_obj() {
return Err(crate::error::create_type_error(vm, "called on incompatible receiver"));
}
Ok(())
}
pub(crate) fn ensure_plain_date_time<H: VmHost>(vm: &mut H, obj: &JsObject) -> Result<(), JsValue> {
if !obj.is_plain_date_time_obj() {
return Err(crate::error::create_type_error(vm, "called on incompatible receiver"));
}
Ok(())
}
pub(crate) fn ensure_duration<H: VmHost>(vm: &mut H, obj: &JsObject) -> Result<(), JsValue> {
if !obj.is_duration_obj() {
return Err(crate::error::create_type_error(vm, "called on incompatible receiver"));
}
Ok(())
}
pub(crate) fn ensure_zoned_date_time<H: VmHost>(vm: &mut H, obj: &JsObject) -> Result<(), JsValue> {
if !obj.is_zoned_date_time_obj() {
return Err(crate::error::create_type_error(vm, "called on incompatible receiver"));
}
Ok(())
}
pub(crate) fn receiver_obj<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<*mut JsObject, JsValue> {
let raw = vm.reg(if args.is_empty() { 0 } else { args[0] });
if !raw.is_object() {
return Err(crate::error::create_type_error(vm, "called on incompatible receiver"));
}
let ptr = raw.as_js_object_ptr();
if ptr.is_null() {
return Err(crate::error::create_type_error(vm, "called on incompatible receiver"));
}
Ok(ptr)
}
pub(crate) fn initialize_temporal_receiver<H: VmHost, const N: usize>(
vm: &mut H, args: &[u8], type_tag: u8, values: [JsValue; N],
) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &mut *ptr };
obj.type_tag = type_tag;
for (index, value) in values.into_iter().enumerate() {
obj.set_prop_at(index, value);
}
NativeResult::Ok(JsValue::undefined())
}
/// 读 receiver 是否为构造调用:`new` 时 this 的原型链包含相应构造器的 prototype。
pub(crate) fn is_ctor_call<H: VmHost>(vm: &mut H, args: &[u8], proto_ptr: *const JsObject) -> bool {
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
if !this_val.is_object() {
return false;
}
let ptr = this_val.as_js_object_ptr();
if ptr.is_null() {
return false;
}
let obj = unsafe { &*ptr };
let mut this_proto = obj.proto();
while this_proto.is_object() {
let this_proto_ptr = this_proto.as_js_object_ptr();
if this_proto_ptr.is_null() {
return false;
}
if std::ptr::eq(this_proto_ptr, proto_ptr) {
return true;
}
this_proto = unsafe { &*this_proto_ptr }.proto();
}
false
}
pub(crate) fn make_instant<H: VmHost>(vm: &mut H, epoch_ns: i128) -> NativeResult {
let proto = JsValue::from_js_object(vm.builtin_proto(ProtoKind::InstantProto));
let epoch_value = vm.new_bigint(num_bigint::BigInt::from(epoch_ns));
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto);
obj.type_tag = JsObject::OBJ_TYPE_INSTANT;
obj.set_prop_at(0, epoch_value);
NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj)))
}
pub(crate) fn make_zoned_date_time<H: VmHost>(
vm: &mut H, epoch_ns: i128, time_zone_id: &str, calendar: &str,
) -> NativeResult {
let proto = JsValue::from_js_object(vm.builtin_proto(ProtoKind::ZonedDateTimeProto));
let epoch_value = vm.new_bigint(num_bigint::BigInt::from(epoch_ns));
let time_zone_value = vm.new_string(time_zone_id);
let calendar_value = vm.new_string(calendar);
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto);
obj.type_tag = JsObject::OBJ_TYPE_ZONED_DATE_TIME;
obj.set_prop_at(0, epoch_value);
obj.set_prop_at(1, time_zone_value);
obj.set_prop_at(2, calendar_value);
NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj)))
}
pub(crate) fn native_engine_error<H: VmHost>(vm: &mut H, error: &str) -> JsValue {
vm.take_uncaught_value()
.unwrap_or_else(|| crate::error::create_from_text(vm, error))
}
pub(crate) fn temporal_option_value<H: VmHost>(
vm: &mut H, obj: &JsObject, receiver: JsValue, name: &str,
) -> Result<JsValue, JsValue> {
let key = vm.new_string(name);
let si = vm.property_key_si(key);
vm.ordinary_get(obj, si, receiver)
.map_err(|error| native_engine_error(vm, &error))
}
pub(crate) fn temporal_option_number<H: VmHost>(vm: &mut H, value: JsValue) -> Result<f64, JsValue> {
let primitive = oxide_runtime_api::to_primitive(value, oxide_runtime_api::ToPrimitiveHint::Number, vm)
.map_err(|error| native_engine_error(vm, &error))?;
if primitive.is_symbol() || primitive.is_bigint() {
return Err(crate::error::create_type_error(vm, "cannot convert option to number"));
}
Ok(to_number(primitive))
}
pub(crate) fn temporal_option_string<H: VmHost>(vm: &mut H, value: JsValue) -> Result<String, JsValue> {
let primitive = oxide_runtime_api::to_primitive(value, oxide_runtime_api::ToPrimitiveHint::String, vm)
.map_err(|error| native_engine_error(vm, &error))?;
if primitive.is_symbol() {
return Err(crate::error::create_type_error(vm, "cannot convert option to string"));
}
Ok(to_string(primitive))
}
/// ToPrimitive(String) 后要求结果为字符串,否则 TypeError(ParseMonthCode/ToOffsetString 语义)。
pub(crate) fn temporal_string_strict<H: VmHost>(vm: &mut H, value: JsValue) -> Result<String, JsValue> {
let primitive = oxide_runtime_api::to_primitive(value, oxide_runtime_api::ToPrimitiveHint::String, vm)
.map_err(|error| native_engine_error(vm, &error))?;
if !primitive.is_string() {
return Err(crate::error::create_type_error(vm, "expected a string"));
}
Ok(to_string(primitive))
}
/// 校验 `with` 族方法的 partial 参数并排除日历/时区载体。
///
/// 非对象、已初始化的 Temporal 日期时间类实例,或 calendar、timeZone 任一
/// 非 undefined 的对象都抛 TypeError(RejectObjectWithCalendarOrTimeZone 语义)。
///
/// # 边界与前提
/// - 内部类型标签检查先于 calendar、timeZone 属性的读取,两步的先后不可交换。
/// - 只有本函数返回成功,调用方才能继续读取 partial 的字段。
pub(crate) fn reject_partial_object_with_calendar_or_time_zone<H: VmHost>(
vm: &mut H, value: JsValue,
) -> Result<(), JsValue> {
if !value.is_object() {
return Err(crate::error::create_type_error(vm, "invalid argument"));
}
let ptr = value.as_js_object_ptr();
if ptr.is_null() {
return Err(crate::error::create_type_error(vm, "invalid argument"));
}
let obj = unsafe { &*ptr };
if obj.is_plain_date_obj()
|| obj.is_plain_date_time_obj()
|| obj.is_plain_time_obj()
|| obj.is_zoned_date_time_obj()
|| obj.is_plain_month_day_obj()
|| obj.is_plain_year_month_obj()
{
return Err(crate::error::create_type_error(vm, "invalid argument"));
}
let calendar = temporal_option_value(vm, obj, value, "calendar")?;
if !calendar.is_undefined() {
return Err(crate::error::create_type_error(vm, "invalid argument"));
}
let time_zone = temporal_option_value(vm, obj, value, "timeZone")?;
if !time_zone.is_undefined() {
return Err(crate::error::create_type_error(vm, "invalid argument"));
}
Ok(())
}
pub(crate) fn plain_date_time_object_parts<H: VmHost>(
vm: &mut H, value: JsValue, obj: &JsObject, constrain: bool, ignore_time: bool,
) -> Result<(i32, u32, u32, f64, Option<String>), JsValue> {
let calendar_raw = temporal_option_value(vm, obj, value, "calendar")?;
let calendar = temporal_calendar_id(vm, calendar_raw)?;
// 先按规范顺序读取全部原始字段,暂不转换类型;PlainDate 路径忽略时间字段。
let (
day_raw,
hour_raw,
microsecond_raw,
millisecond_raw,
minute_raw,
month_raw,
month_code_raw,
nanosecond_raw,
second_raw,
year_raw,
) = if ignore_time {
(
temporal_option_value(vm, obj, value, "day")?,
JsValue::undefined(),
JsValue::undefined(),
JsValue::undefined(),
JsValue::undefined(),
temporal_option_value(vm, obj, value, "month")?,
temporal_option_value(vm, obj, value, "monthCode")?,
JsValue::undefined(),
JsValue::undefined(),
temporal_option_value(vm, obj, value, "year")?,
)
} else {
(
temporal_option_value(vm, obj, value, "day")?,
temporal_option_value(vm, obj, value, "hour")?,
temporal_option_value(vm, obj, value, "microsecond")?,
temporal_option_value(vm, obj, value, "millisecond")?,
temporal_option_value(vm, obj, value, "minute")?,
temporal_option_value(vm, obj, value, "month")?,
temporal_option_value(vm, obj, value, "monthCode")?,
temporal_option_value(vm, obj, value, "nanosecond")?,
temporal_option_value(vm, obj, value, "second")?,
temporal_option_value(vm, obj, value, "year")?,
)
};
// 缺失必填字段先抛 TypeError(先于任何 RangeError 值校验)。
if day_raw.is_undefined() {
return Err(crate::error::create_type_error(vm, "day is required"));
}
if year_raw.is_undefined() {
return Err(crate::error::create_type_error(vm, "year is required"));
}
if month_raw.is_undefined() && month_code_raw.is_undefined() {
return Err(crate::error::create_type_error(vm, "month is required"));
}
// monthCode 必须是 string 类型,否则 TypeError。
let month_code = if month_code_raw.is_undefined() {
None
} else {
if !month_code_raw.is_string() {
return Err(crate::error::create_type_error(vm, "monthCode must be a string"));
}
let code = to_string(month_code_raw);
if !month_code_well_formed(&code) {
return Err(crate::error::create_range_error(vm, "invalid monthCode"));
}
Some(code)
};
// 数值字段类型转换:Symbol/BigInt 抛 TypeError。
let mut convert_number = |_name: &str, raw: JsValue| -> Result<Option<f64>, JsValue> {
if raw.is_undefined() {
Ok(None)
} else {
temporal_option_number(vm, raw).map(|number| Some(number.trunc()))
}
};
let (day, hour, microsecond, millisecond, minute, month, nanosecond, second, year) = (
convert_number("day", day_raw)?,
convert_number("hour", hour_raw)?.unwrap_or(0.0),
convert_number("microsecond", microsecond_raw)?.unwrap_or(0.0),
convert_number("millisecond", millisecond_raw)?.unwrap_or(0.0),
convert_number("minute", minute_raw)?.unwrap_or(0.0),
convert_number("month", month_raw)?,
convert_number("nanosecond", nanosecond_raw)?.unwrap_or(0.0),
convert_number("second", second_raw)?.unwrap_or(0.0),
convert_number("year", year_raw)?,
);
resolve_date_time_parts(
vm,
calendar,
DateTimeParts {
year,
month,
month_code,
day,
hour,
minute,
second,
millisecond,
microsecond,
nanosecond,
},
constrain,
ignore_time,
)
}
/// monthCode 语法(well-formed):M 后两位数字,可选 L 后缀(第 4 位)。
pub(crate) fn month_code_well_formed(code: &str) -> bool {
code.len() >= 3
&& code.starts_with('M')
&& code.as_bytes()[1..3].iter().all(u8::is_ascii_digit)
&& (code.len() == 3 || (code.len() == 4 && code.ends_with('L')))
}
/// 已转换的日期时间 property bag 字段值(resolve_date_time_parts 的输入)。
pub(crate) struct DateTimeParts {
pub year: Option<f64>,
pub month: Option<f64>,
pub month_code: Option<String>,
pub day: Option<f64>,
pub hour: f64,
pub minute: f64,
pub second: f64,
pub millisecond: f64,
pub microsecond: f64,
pub nanosecond: f64,
}
/// 日期时间 property bag 校验共享核:monthCode ISO 适配 + constrain 钳制 + 范围检查。
///
/// # 步骤
/// 1. monthCode ISO 适配:仅 M01-M12,L 后缀与超界拒绝,与 month 冲突拒绝(RangeError)。
/// 2. day/year 兜底必填(调用方已保证);全分量有限性 + 非负检查(RangeError)。
/// 3. constrain 钳制:月 1..12、日当月末日、时 0..23、分秒 0..59、毫秒/微秒/纳秒 0..999。
/// 4. 有界整型转换 + valid_iso_date / valid_plain_time / valid_plain_date_time_range 检查。
///
/// # 边界与前提
/// - 调用方须已完成必填字段检查(year/day/month 或 monthCode)与逐字段类型转换。
/// - 字段读序与转换由调用方负责,本核只消费已转换值。
pub(crate) fn resolve_date_time_parts<H: VmHost>(
vm: &mut H, calendar: Option<String>, parts: DateTimeParts, constrain: bool, ignore_time: bool,
) -> Result<(i32, u32, u32, f64, Option<String>), JsValue> {
let DateTimeParts {
year,
month,
month_code,
day,
hour,
minute,
second,
millisecond,
microsecond,
nanosecond,
} = parts;
// monthCode 适配 ISO 日历:仅 M01-M12,L 后缀不支持;无效抛 RangeError。
let month_code = match month_code {
Some(code) => {
if code.ends_with('L') {
return Err(crate::error::create_range_error(vm, "monthCode is not valid for ISO calendar"));
}
let number = code[1..3]
.parse::<f64>()
.map_err(|_| crate::error::create_range_error(vm, "invalid monthCode"))?;
if !(1.0..=12.0).contains(&number) {
return Err(crate::error::create_range_error(vm, "monthCode is not valid for ISO calendar"));
}
Some(number)
}
None => None,
};
let month = match (month, month_code) {
(Some(month), Some(code)) if month != code => {
return Err(crate::error::create_range_error(vm, "month and monthCode disagree"));
}
(Some(month), _) => month,
(None, Some(code)) => code,
(None, None) => unreachable!(),
};
let day = day.ok_or_else(|| crate::error::create_type_error(vm, "day is required"))?;
let year = year.ok_or_else(|| crate::error::create_type_error(vm, "year is required"))?;
let mut values = [year, month, day, hour, minute, second, millisecond, microsecond, nanosecond];
if values.iter().any(|number| !number.is_finite()) || values[1..].iter().any(|number| *number < 0.0) {
return Err(crate::error::create_range_error(vm, "invalid date-time component"));
}
if constrain {
if values[1] < 1.0 || values[2] < 1.0 {
return Err(crate::error::create_range_error(vm, "invalid date-time component"));
}
values[1] = values[1].clamp(1.0, 12.0);
values[2] = values[2].clamp(1.0, days_in_month(values[0] as i128, values[1] as i128).unwrap_or(31) as f64);
values[3] = values[3].clamp(0.0, 23.0);
values[4] = values[4].clamp(0.0, 59.0);
values[5] = values[5].clamp(0.0, 59.0);
values[6] = values[6].clamp(0.0, 999.0);
values[7] = values[7].clamp(0.0, 999.0);
values[8] = values[8].clamp(0.0, 999.0);
}
let [year, month, day, hour, minute, second, millisecond, microsecond, nanosecond] = values;
// 分量值可能超出目标整型范围(f64 截断后仍超 i32/u32 上限),先做有界转换。
// 超出范围后 valid_iso_date / valid_plain_time 会拒绝,此处兜住 debug 模式 panic。
let year = if year.is_finite() && year >= i32::MIN as f64 && year <= i32::MAX as f64 {
year as i32
} else {
return Err(crate::error::create_range_error(vm, "invalid date-time component"));
};
let mut to_u32 = |v: f64| -> Result<u32, JsValue> {
if v.is_finite() && v >= 0.0 && v <= u32::MAX as f64 {
Ok(v as u32)
} else {
Err(crate::error::create_range_error(vm, "invalid date-time component"))
}
};
let month = to_u32(month)?;
let day = to_u32(day)?;
let hour = to_u32(hour)?;
let minute = to_u32(minute)?;
let second = to_u32(second)?;
let millisecond = to_u32(millisecond)?;
let microsecond = to_u32(microsecond)?;
let nanosecond = to_u32(nanosecond)?;
if !valid_iso_date(year, month, day)
|| (!ignore_time && !valid_plain_time(hour, minute, second, millisecond, microsecond, nanosecond))
{
return Err(crate::error::create_range_error(vm, "invalid date-time component"));
}
let total_ns = hour as f64 * 3_600_000_000_000.0
+ minute as f64 * 60_000_000_000.0
+ second as f64 * 1_000_000_000.0
+ millisecond as f64 * 1_000_000.0
+ microsecond as f64 * 1_000.0
+ nanosecond as f64;
if !ignore_time && !valid_plain_date_time_range(year, month, day, total_ns) {
return Err(crate::error::create_range_error(vm, "invalid date-time component"));
}
Ok((year, month, day, total_ns, calendar))
}
pub(crate) fn temporal_overflow<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<bool, JsValue> {
let options = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() };
if options.is_undefined() {
return Ok(true);
}
if !options.is_object() {
return Err(crate::error::create_type_error(vm, "options must be an object"));
}
let ptr = options.as_js_object_ptr();
if ptr.is_null() {
return Err(crate::error::create_type_error(vm, "options must be an object"));
}
let raw = temporal_option_value(vm, unsafe { &*ptr }, options, "overflow")?;
if raw.is_undefined() {
return Ok(true);
}
let value = temporal_option_string(vm, raw)?;
match value.as_str() {
"constrain" => Ok(true),
"reject" => Ok(false),
_ => Err(crate::error::create_range_error(vm, "invalid overflow")),
}
}
/// Temporal 数字分量转换(number-only 路径):先 ToPrimitive(Number)——对象经
/// valueOf/toString 取值;Symbol/BigInt → TypeError;NaN/±Inf → RangeError
/// (undefined 与不可解析串都归 NaN,由此统一抛 RangeError);其余截断取整。
pub(crate) fn temporal_number_component<H: VmHost>(vm: &mut H, value: JsValue) -> Result<f64, JsValue> {
let primitive = oxide_runtime_api::to_primitive(value, oxide_runtime_api::ToPrimitiveHint::Number, vm)
.map_err(|error| native_engine_error(vm, &error))?;
if primitive.is_symbol() || primitive.is_bigint() {
return Err(crate::error::create_type_error(vm, "invalid number"));
}
let number = to_number(primitive);
if number.is_infinite() || number.is_nan() {
return Err(crate::error::create_range_error(vm, "invalid number"));
}
Ok(number.trunc())
}
/// ISO 年-月是否落在 PlainYearMonth 可表示范围(ISOYearMonthWithinLimits):
/// 年超 ±271821..=275760、或 -271821 年 3 月及以前、+275760 年 10 月及以后越界。
pub(crate) fn iso_year_month_within_limits(year: i32, month: u32) -> bool {
if !(-271_821..=275_760).contains(&year) {
return false;
}
!((year == -271_821 && month < 4) || (year == 275_760 && month > 9))
}
/// toString 的 calendarName 选项取值(缺省 = auto;auto 与 never 的差异仅在注解)。
pub(crate) enum ShowCalendar {
Auto,
Always,
Critical,
Never,
}
/// ISO 串尾部日历注解(FormatCalendarAnnotation):never 与 auto+iso8601 为空;
/// critical 前置 `!`;其余为 `[u-ca=…]`。
pub(crate) fn calendar_annotation(calendar: &str, show: ShowCalendar) -> String {
match (show, calendar) {
(ShowCalendar::Never, _) => String::new(),
(ShowCalendar::Auto, "iso8601") => String::new(),
(ShowCalendar::Always, _) => format!("[u-ca={calendar}]"),
(ShowCalendar::Critical, _) => format!("[!u-ca={calendar}]"),
(ShowCalendar::Auto, _) => format!("[u-ca={calendar}]"),
}
}
/// 读并校验 toString 的 calendarName 选项:options 非对象 → TypeError;
/// 值非字符串先 ToPrimitive(String) 转换(Symbol → TypeError);
/// 仅 auto/always/never/critical 合法(大小写敏感),否则 RangeError。
pub(crate) fn temporal_to_show_calendar<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<ShowCalendar, JsValue> {
let options = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
if options.is_undefined() {
return Ok(ShowCalendar::Auto);
}
if !options.is_object() {
return Err(crate::error::create_type_error(vm, "options must be an object"));
}
let ptr = options.as_js_object_ptr();
if ptr.is_null() {
return Err(crate::error::create_type_error(vm, "options must be an object"));
}
let raw = temporal_option_value(vm, unsafe { &*ptr }, options, "calendarName")?;
if raw.is_undefined() {
return Ok(ShowCalendar::Auto);
}
let value = temporal_option_string(vm, raw)?;
match value.as_str() {
"always" => Ok(ShowCalendar::Always),
"critical" => Ok(ShowCalendar::Critical),
"never" => Ok(ShowCalendar::Never),
"auto" => Ok(ShowCalendar::Auto),
_ => Err(crate::error::create_range_error(vm, "invalid calendarName")),
}
}
/// bag 日历解析:undefined → iso8601;字符串走宽松日历串解析(18-ID 白名单归一小写,
/// 合法 ISO 日期-时间串归一 iso8601,含 YYYY-MM / MM-DD 部分日期);
/// PD/PDT/ZDT/PMD/PYM 实例 → 直读日历槽;其余对象与原始值 → TypeError。
pub(crate) fn month_day_bag_calendar_id<H: VmHost>(vm: &mut H, value: JsValue) -> Result<String, JsValue> {
if value.is_undefined() {
return Ok("iso8601".to_string());
}
if value.is_string() {
let text = to_string(value);
return parse_temporal_calendar_string(&text)
.map_err(|_| crate::error::create_range_error(vm, "invalid calendar"));
}
if value.is_object() {
let ptr = value.as_js_object_ptr();
if !ptr.is_null() {
let obj = unsafe { &*ptr };
let slot = if obj.is_plain_date_obj() {
Some(3)
} else if obj.is_plain_date_time_obj() {
Some(4)
} else if obj.is_zoned_date_time_obj() {
Some(2)
} else if obj.is_plain_month_day_obj() || obj.is_plain_year_month_obj() {
Some(3)
} else {
None
};
if let Some(slot) = slot {
return Ok(get_calendar_id(obj, slot));
}
}
return Err(crate::error::create_type_error(vm, "invalid calendar"));
}
Err(crate::error::create_type_error(vm, "invalid calendar"))
}
pub(crate) fn parse_digits(bytes: &[u8], cursor: &mut usize, count: usize) -> Option<i128> {
let end = cursor.checked_add(count)?;
let digits = bytes.get(*cursor..end)?;
if !digits.iter().all(u8::is_ascii_digit) {
return None;
}
*cursor = end;
digits
.iter()
.try_fold(0_i128, |value, digit| value.checked_mul(10)?.checked_add((digit - b'0') as i128))
}
pub(crate) fn is_leap_year(year: i128) -> bool {
year.rem_euclid(4) == 0 && (year.rem_euclid(100) != 0 || year.rem_euclid(400) == 0)
}
pub(crate) fn days_in_month(year: i128, month: i128) -> Option<i128> {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => Some(31),
4 | 6 | 9 | 11 => Some(30),
2 => Some(if is_leap_year(year) { 29 } else { 28 }),
_ => None,
}
}
pub(crate) fn days_from_civil(mut year: i128, month: i128, day: i128) -> i128 {
year -= i128::from(month <= 2);
let era = year.div_euclid(400);
let year_of_era = year - era * 400;
let shifted_month = month + if month > 2 { -3 } else { 9 };
let day_of_year = (153 * shifted_month + 2) / 5 + day - 1;
let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
era * 146_097 + day_of_era - 719_468
}
pub(crate) enum FractionalSecondDigitsInput {
Auto,
Number(f64),
String(String),
}
pub(crate) fn parse_fractional_second_digits<H: VmHost>(
vm: &mut H, input: FractionalSecondDigitsInput,
) -> Result<Option<usize>, JsValue> {
match input {
FractionalSecondDigitsInput::Auto => Ok(None),
FractionalSecondDigitsInput::String(value) if value == "auto" => Ok(None),
FractionalSecondDigitsInput::String(_) => {
Err(crate::error::create_range_error(vm, "invalid fractionalSecondDigits"))
}
FractionalSecondDigitsInput::Number(value) => {
let digits = value.floor();
if !digits.is_finite() || !(0.0..=9.0).contains(&digits) {
return Err(crate::error::create_range_error(vm, "invalid fractionalSecondDigits"));
}
Ok(Some(digits as usize))
}
}
}
pub(crate) fn parse_offset_seconds(value: &str) -> Option<i64> {
let bytes = value.as_bytes();
if bytes.len() != 6 || !matches!(bytes[0], b'+' | b'-') || bytes[3] != b':' {
return None;
}
if !bytes[1..3].iter().all(u8::is_ascii_digit) || !bytes[4..6].iter().all(u8::is_ascii_digit) {
return None;
}
let hours = i64::from(bytes[1] - b'0') * 10 + i64::from(bytes[2] - b'0');
let minutes = i64::from(bytes[4] - b'0') * 10 + i64::from(bytes[5] - b'0');
if hours > 23 || minutes > 59 {
return None;
}
let magnitude = hours * 3600 + minutes * 60;
Some(if bytes[0] == b'-' { -magnitude } else { magnitude })
}
/// 数值偏移串统一解析为秒(±HH:MM / ±HH / ±HHMM / ±HH:MM:SS 四形式)。
///
/// # 边界与前提
/// - 四形式以外的串(含带小数秒的 ±HH:MM:SS.xxx)返回 None。
/// - 小时 ≤ 23、分钟/秒 ≤ 59,越界返回 None。
pub(crate) fn fixed_offset_seconds(value: &str) -> Option<i64> {
let bytes = value.as_bytes();
let sign = match bytes.first() {
Some(b'+') => 1_i64,
Some(b'-') => -1_i64,
_ => return None,
};
let digits = |range: std::ops::Range<usize>| -> Option<i64> {
if bytes[range.clone()].iter().all(u8::is_ascii_digit) {
Some(bytes[range].iter().map(|b| i64::from(b - b'0')).fold(0, |acc, d| acc * 10 + d))
} else {
None
}
};
// ±HH:MM(6 字符):canonical_time_zone 接受面与 zone_offset_seconds 快速路径共用。
if bytes.len() == 6 && bytes[3] == b':' {
let hour = digits(1..3)?;
let minute = digits(4..6)?;
return (hour <= 23 && minute <= 59).then_some(sign * (hour * 3600 + minute * 60));
}
// ±HH(3 字符):小时制简写,分钟恒 0。
if bytes.len() == 3 {
let hour = digits(1..3)?;
return (hour <= 23).then_some(sign * hour * 3600);
}
// ±HHMM(5 字符):无冒号归一形式。
if bytes.len() == 5 {
let hour = digits(1..3)?;
let minute = digits(3..5)?;
return (hour <= 23 && minute <= 59).then_some(sign * (hour * 3600 + minute * 60));
}
// ±HH:MM:SS(9 字符):亚分钟偏移,zone_offset_seconds 快速路径专用。
if bytes.len() == 9 && bytes[3] == b':' && bytes[6] == b':' {
let hour = digits(1..3)?;
let minute = digits(4..6)?;
let second = digits(7..9)?;
return (hour <= 23 && minute <= 59 && second <= 59).then_some(sign * (hour * 3600 + minute * 60 + second));
}
None
}
/// 时区标识符规范化:UTC/Z 归一为 "UTC",数值偏移串原样保留,IANA 区名解别名后
/// 返回规范名。
///
/// # 步骤
/// 1. UTC/Z(大小写折叠)→ "UTC"。
/// 2. 数值偏移串(±HH:MM / ±HH / ±HHMM)原样保留为时区 ID。
/// 3. IANA 区名(含 legacy 别名)命中区表 → 规范名。
/// 4. 带注解的日期时间以最后一个方括号内标识符为准;日期时间串尾偏移按偏移串处理。
///
/// # 边界与前提
/// - 返回只含规范名,不含偏移:偏移是 epoch 的函数,由 zone_offset_seconds 统一提供。
/// - 非法形式(越界小时/分钟、-000000 前缀、未知区名)返回 None。
pub(crate) fn canonical_time_zone(value: &str) -> Option<String> {
let input = value.trim();
if input.eq_ignore_ascii_case("UTC") || input.eq_ignore_ascii_case("Z") {
return Some("UTC".to_string());
}
if parse_offset_seconds(input).is_some() {
return Some(input.to_string());
}
// ±HH 基本偏移("+01" / "-05"):3 字符,hour ≤ 23,分钟恒 0。仅放宽构造器时区接受面。
if input.len() == 3
&& matches!(input.as_bytes()[0], b'+' | b'-')
&& input.as_bytes()[1..3].iter().all(u8::is_ascii_digit)
{
let hour = (input.as_bytes()[1] - b'0') as i64 * 10 + (input.as_bytes()[2] - b'0') as i64;
if hour <= 23 {
return Some(input.to_string());
}
return None;
}
// ±HHMM 基本偏移("+0000" / "-0530"):5 字符,无冒号,分钟位可选非零。4 位无冒号归一形式。
if input.len() == 5
&& matches!(input.as_bytes()[0], b'+' | b'-')
&& input.as_bytes()[1..5].iter().all(u8::is_ascii_digit)
{
let hour = (input.as_bytes()[1] - b'0') as i64 * 10 + (input.as_bytes()[2] - b'0') as i64;
let minute = (input.as_bytes()[3] - b'0') as i64 * 10 + (input.as_bytes()[4] - b'0') as i64;
if hour <= 23 && minute <= 59 {
return Some(input.to_string());
}
return None;
}
if input.starts_with("-000000") {
return None;
}
// IANA 区名(含 legacy 别名):解别名后返回规范名。
if let Some(name) = tz_canonical_name(input) {
return Some(name.to_string());
}
// 带 annotation 的日期时间以最后一个方括号内标识符为准。
if let Some(open) = input.rfind('[') {
if !input.ends_with(']') || open + 2 > input.len() {
return None;
}
let annotation = &input[open + 1..input.len() - 1];
if annotation.eq_ignore_ascii_case("UTC") {
return Some("UTC".to_string());
}
if parse_offset_seconds(annotation).is_some() {
return Some(annotation.to_string());
}
return tz_canonical_name(annotation).map(|name| name.to_string());
}
let time_start = input.find(['T', 't', ' '])?;
let time = &input[time_start + 1..];
if time.ends_with(['Z', 'z']) {
return Some("UTC".to_string());
}
let offset_start = time
.char_indices()
.rev()
.find_map(|(index, ch)| matches!(ch, '+' | '-').then_some(index))?;
let offset_id = &time[offset_start..];
parse_offset_seconds(offset_id).map(|_| offset_id.to_string())
}
pub(crate) fn civil_from_days(days: i128) -> (i128, i128, i128) {
let shifted = days + 719_468;
let era = shifted.div_euclid(146_097);
let day_of_era = shifted - era * 146_097;
let year_of_era = (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
let mut year = year_of_era + era * 400;
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
let month_prime = (5 * day_of_year + 2) / 153;
let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
let month = month_prime + if month_prime < 10 { 3 } else { -9 };
year += i128::from(month <= 2);
(year, month, day)
}
pub(crate) fn format_iso_year(year: i128) -> String {
if (0..=9_999).contains(&year) {
format!("{year:04}")
} else if year >= 0 {
format!("+{year:06}")
} else {
format!("-{:06}", -year)
}
}
pub(crate) fn validate_temporal_annotation_suffix(mut suffix: &str) -> Result<(), String> {
let mut calendar_seen = false;
let mut saw_critical_calendar = false;
let mut time_zone_seen = false;
while !suffix.is_empty() {
if !suffix.starts_with('[') {
return Err("invalid annotation suffix".into());
}
let Some(end) = suffix.find(']') else {
return Err("unterminated annotation".into());
};
if end == 1 {
return Err("empty annotation".into());
}
let annotation = &suffix[1..end];
let (critical, body) = annotation.strip_prefix('!').map_or((false, annotation), |body| (true, body));
if let Some((key, value)) = body.split_once('=') {
if key.bytes().any(|byte| byte.is_ascii_uppercase()) {
return Err("annotation keys must be lowercase".into());
}
if key == "u-ca" {
if calendar_seen {
if critical || saw_critical_calendar {
return Err("invalid calendar annotation".into());
}
} else if is_builtin_calendar_id(value).is_none() {
return Err("invalid calendar annotation".into());
} else {
calendar_seen = true;
saw_critical_calendar = critical;
}
} else if critical {
return Err("unknown critical annotation".into());
}
} else {
if time_zone_seen {
return Err("invalid time-zone annotation".into());
}
time_zone_seen = true;
}
suffix = &suffix[end + 1..];
}
Ok(())
}
/// 剥离 ISO 偏移(±HH、±HHMM、±HH:MM、±HHMMSS、±HH:MM:SS,秒可带小数),返回剩余部分。
pub(crate) fn strip_iso_offset(input: &str) -> Result<&str, String> {
let bytes = input.as_bytes();
if bytes.len() < 3 || !matches!(bytes[0], b'+' | b'-') {
return Err("invalid ISO offset".into());
}
let colon = bytes.len() > 3 && bytes[3] == b':';
let mut i = 1usize;
let take_digits = |count: usize, cursor: &mut usize| -> Result<(), String> {
if *cursor + count > bytes.len() || !bytes[*cursor..*cursor + count].iter().all(u8::is_ascii_digit) {
return Err("invalid ISO offset".into());
}
*cursor += count;
Ok(())
};
take_digits(2, &mut i)?;
if i < bytes.len() {
if colon {
if bytes[i] != b':' {
return Err("invalid ISO offset".into());
}
i += 1;
}
if i + 2 <= bytes.len() && bytes[i..i + 2].iter().all(u8::is_ascii_digit) {
i += 2;
if i < bytes.len() {
if colon {
if bytes[i] != b':' {
return Err("invalid ISO offset".into());
}
i += 1;
}
if i + 2 <= bytes.len() && bytes[i..i + 2].iter().all(u8::is_ascii_digit) {
i += 2;
if i < bytes.len() && matches!(bytes[i], b'.' | b',') {
i += 1;
let start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
if i == start {
return Err("invalid ISO offset fraction".into());
}
if i - start > 9 {
return Err("invalid ISO offset fraction".into());
}
}
}
}
}
}
let digits: String = input[1..i].chars().filter(|ch| ch.is_ascii_digit()).take(6).collect();
if digits.len() < 2 {
return Err("invalid ISO offset".into());
}
let hour: u32 = digits[..2].parse().map_err(|_| "invalid ISO offset".to_string())?;
let minute: u32 = if digits.len() >= 4 {
digits[2..4].parse().map_err(|_| "invalid ISO offset".to_string())?
} else {
0
};
let second: u32 = if digits.len() >= 6 {
digits[4..6].parse().map_err(|_| "invalid ISO offset".to_string())?
} else {
0
};
if hour > 23 || minute > 59 || second > 59 {
return Err("invalid ISO offset".into());
}
Ok(&input[i..])
}
/// ParseTemporalDateString:PlainDate 字符串(时间部分可选且被忽略,仅按日期做范围校验)。
pub(crate) fn parse_plain_date_string(input: &str) -> Result<(i32, u32, u32), String> {
parse_temporal_string_impl(input, false).map(|(year, month, day, _)| (year, month, day))
}
pub(crate) fn parse_temporal_string_impl(
input: &str, enforce_date_time_range: bool,
) -> Result<(i32, u32, u32, f64), String> {
let trimmed = input.trim();
if trimmed.contains('\u{2212}') {
return Err("variant minus sign is not valid for PlainDateTime".into());
}
let text = trimmed.to_owned();
let annotation_start = text.find('[').unwrap_or(text.len());
validate_temporal_annotation_suffix(&text[annotation_start..])?;
let text = &text[..annotation_start];
if text.contains('Z') || text.contains('z') {
return Err("UTC designator is not valid for PlainDateTime".into());
}
let separator = text.find(['T', 't', ' ']);
let date_part = match separator {
Some(index) => &text[..index],
None => text,
};
let (year, month, day) = parse_iso_date(date_part)?;
let Some(separator) = separator else {
if !valid_iso_date(year, month, day)
|| (enforce_date_time_range && !valid_plain_date_time_range(year, month, day, 0.0))
{
return Err("invalid ISO date".into());
}
return Ok((year, month, day, 0.0));
};
let time_and_suffix = &text[separator + 1..];
let time_end = time_and_suffix
.char_indices()
.find_map(|(index, ch)| matches!(ch, '+' | '-').then_some(index))
.unwrap_or(time_and_suffix.len());
if time_end < time_and_suffix.len() && !strip_iso_offset(&time_and_suffix[time_end..])?.is_empty() {
return Err("invalid ISO offset".into());
}
let time = &time_and_suffix[..time_end];
if time.is_empty() {
return Err("missing ISO time".into());
}
let (clock, fraction) = match time.find(['.', ',']) {
Some(index) => (&time[..index], Some(&time[index + 1..])),
None => (time, None),
};
let fields = if clock.contains(':') {
clock.split(':').collect::<Vec<_>>()
} else {
if clock.len() % 2 != 0 || clock.len() > 6 {
return Err("invalid ISO time".into());
}
clock
.as_bytes()
.chunks(2)
.map(std::str::from_utf8)
.collect::<Result<Vec<_>, _>>()
.map_err(|_| "invalid ISO time".to_string())?
};
if fields.is_empty() || fields.len() > 3 || fields.iter().any(|field| field.len() != 2) {
return Err("invalid ISO time".into());
}
if fraction.is_some() && fields.len() < 3 {
return Err("fractional hours and minutes are not valid".into());
}
let parse_field = |index: usize| -> Result<u32, String> {
fields
.get(index)
.map_or(Ok(0), |field| field.parse().map_err(|_| "invalid ISO time".to_string()))
};
let hour = parse_field(0)?;
let minute = parse_field(1)?;
let mut second = parse_field(2)?;
if second == 60 {
second = 59;
}
let subsecond = match fraction {
Some(digits) if !digits.is_empty() && digits.len() <= 9 && digits.bytes().all(|byte| byte.is_ascii_digit()) => {
let mut padded = digits.to_owned();
padded.extend(std::iter::repeat('0').take(9 - digits.len()));
padded.parse::<u32>().map_err(|_| "invalid ISO fraction".to_string())?
}
Some(_) => return Err("invalid ISO fraction".into()),
None => 0,
};
let millisecond = subsecond / 1_000_000;
let microsecond = subsecond / 1_000 % 1_000;
let nanosecond = subsecond % 1_000;
if !valid_plain_time(hour, minute, second, millisecond, microsecond, nanosecond) {
return Err("invalid ISO time".into());
}
let total_ns = hour as f64 * 3_600_000_000_000.0
+ minute as f64 * 60_000_000_000.0
+ second as f64 * 1_000_000_000.0
+ subsecond as f64;
if !valid_iso_date(year, month, day)
|| (enforce_date_time_range && !valid_plain_date_time_range(year, month, day, total_ns))
{
return Err("invalid ISO date".into());
}
Ok((year, month, day, total_ns))
}
/// 规范 BuiltinCalendarID 全集(18 项)。匹配用 eq_ignore_ascii_case(ASCII 折叠,
/// 禁 to_lowercase:U+0130 点 I 会被 Unicode 折叠成 i+组合符,误判为 iso8601)。
pub(crate) const CALENDAR_ID_WHITELIST: [&str; 18] = [
"buddhist",
"chinese",
"coptic",
"dangi",
"ethioaa",
"ethiopic",
"gregory",
"hebrew",
"indian",
"islamic",
"islamic-civil",
"islamic-rcy",
"islamic-tbla",
"islamic-umalqura",
"iso8601",
"japanese",
"persian",
"roc",
];
/// 严格白名单匹配:命中返回规范小写形式,未命中返回 None。
pub(crate) fn is_builtin_calendar_id(value: &str) -> Option<&'static str> {
CALENDAR_ID_WHITELIST.iter().find(|id| value.eq_ignore_ascii_case(id)).copied()
}
/// 严格日历 ID 解析:只走 18 项白名单,拒绝 ISO 串(含注解/compact/extended/空串)。
/// 供构造器日历参数与注解值判定使用。
pub(crate) fn parse_temporal_calendar_id_strict(input: &str) -> Result<String, String> {
let trimmed = input.trim();
is_builtin_calendar_id(trimmed)
.map(str::to_string)
.ok_or_else(|| "invalid calendar".to_string())
}
/// ParseTemporalCalendarString:日历标识符 = 18 项内置日历 ID(ASCII 大小写不敏感)
/// 或合法 ISO 日期(-时间)字符串(含部分日期 YYYY-MM / MM-DD,可选时间、偏移、注解)。
/// 返回规范化日历 ID:白名单命中返回规范小写,ISO 串路径恒为 "iso8601"。
pub(crate) fn parse_temporal_calendar_string(input: &str) -> Result<String, String> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err("invalid calendar".into());
}
if let Some(id) = is_builtin_calendar_id(trimmed) {
return Ok(id.to_string());
}
if trimmed.contains('\u{2212}') {
return Err("variant minus sign is not valid for calendar".into());
}
// 完整日期时间字符串(含注解校验),例如 2020-01-01T00:00:00.000000000[u-ca=iso8601]
if parse_plain_date_time_string(trimmed).is_ok() {
return Ok("iso8601".to_string());
}
// 纯时间字符串(TemporalTimeString),例如 "15:23" / "T0030" / "152330.1-08"。
if parse_plain_time_string(trimmed).is_some() {
return Ok("iso8601".to_string());
}
// 部分日期:YYYY-MM 或 MM-DD(可带注解)
let text = trimmed.to_owned();
let annotation_start = text.find('[').unwrap_or(text.len());
validate_temporal_annotation_suffix(&text[annotation_start..])?;
let text = &text[..annotation_start];
if text.contains(['Z', 'z']) {
return Err("UTC designator is not valid for calendar".into());
}
parse_partial_calendar_date(text)?;
Ok("iso8601".to_string())
}
/// 部分 ISO 日期(无时间):YYYY[-MM[-DD]] 或 MM-DD;校验月份/日期基本范围并拒绝负零年。
pub(crate) fn parse_partial_calendar_date(input: &str) -> Result<(), String> {
let bytes = input.as_bytes();
let mut i = 0usize;
let signed = i < bytes.len() && matches!(bytes[i], b'+' | b'-');
let negative = signed && bytes[i] == b'-';
if signed {
i += 1;
}
let start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
let digits = &input[start..i];
let rest = &input[i..];
if digits.is_empty() {
return Err("invalid calendar date".into());
}
// MM-DD 形式(无年份)
if digits.len() == 2 {
if !rest.starts_with('-') {
return Err("invalid calendar date".into());
}
let day_part = &rest[1..];
if day_part.len() != 2 || !day_part.bytes().all(|byte| byte.is_ascii_digit()) {
return Err("invalid calendar day".into());
}
let month: u32 = digits.parse().map_err(|_| "invalid calendar month".to_string())?;
let day: u32 = day_part.parse().map_err(|_| "invalid calendar day".to_string())?;
if month == 0 || month > 12 || day == 0 || day > 31 {
return Err("invalid calendar date".into());
}
return Ok(());
}
if !(4..=6).contains(&digits.len()) {
return Err("invalid calendar year".into());
}
if negative && digits.bytes().all(|byte| byte == b'0') {
return Err("invalid calendar negative zero year".into());
}
let mut rest2 = rest;
let mut month: Option<u32> = None;
let mut day: Option<u32> = None;
if rest2.starts_with('-') {
rest2 = &rest2[1..];
if rest2.len() < 2 || !rest2.as_bytes()[..2].iter().all(u8::is_ascii_digit) {
return Err("invalid calendar month".into());
}
month = Some(rest2[..2].parse().map_err(|_| "invalid calendar month".to_string())?);
rest2 = &rest2[2..];
if rest2.starts_with('-') {
rest2 = &rest2[1..];
if rest2.len() != 2 || !rest2.bytes().all(|byte| byte.is_ascii_digit()) {
return Err("invalid calendar day".into());
}
day = Some(rest2.parse().map_err(|_| "invalid calendar day".to_string())?);
rest2 = "";
}
}
if !rest2.is_empty() {
return Err("invalid trailing calendar content".into());
}
if month.is_some_and(|m| m == 0 || m > 12) || day.is_some_and(|d| d == 0 || d > 31) {
return Err("invalid calendar date".into());
}
Ok(())
}
/// ToTemporalCalendar(宽松版,property bag 路径用):undefined → None;
/// string → 宽松解析(白名单 ID 或 ISO 串);Temporal 实例 → 读日历槽(不触发属性 getter);
/// 其他类型 → TypeError。
pub(crate) fn temporal_calendar_id<H: VmHost>(vm: &mut H, value: JsValue) -> Result<Option<String>, JsValue> {
if value.is_undefined() {
return Ok(None);
}
if value.is_string() {
return parse_temporal_calendar_string(&to_string(value))
.map(Some)
.map_err(|_| crate::error::create_range_error(vm, "invalid calendar"));
}
if value.is_object() {
let ptr = value.as_js_object_ptr();
if !ptr.is_null() {
let obj = unsafe { &*ptr };
if obj.is_plain_date_obj() {
return Ok(Some(get_calendar_id(obj, 3)));
}
if obj.is_plain_date_time_obj() {
return Ok(Some(get_calendar_id(obj, 4)));
}
if obj.is_zoned_date_time_obj() {
return Ok(Some(get_calendar_id(obj, 2)));
}
if obj.is_plain_month_day_obj() || obj.is_plain_year_month_obj() {
return Ok(Some(get_calendar_id(obj, 3)));
}
}
}
Err(crate::error::create_type_error(vm, "invalid calendar"))
}
/// ToTemporalCalendar 严格版(构造器日历参数用):字符串只走 18 项白名单,拒绝 ISO 串;
/// undefined / 其他分支与宽松版一致。
pub(crate) fn temporal_calendar_id_strict<H: VmHost>(vm: &mut H, value: JsValue) -> Result<Option<String>, JsValue> {
if value.is_string() {
return parse_temporal_calendar_id_strict(&to_string(value))
.map(Some)
.map_err(|_| crate::error::create_range_error(vm, "invalid calendar"));
}
temporal_calendar_id(vm, value)
}
/// 偏移秒数格式化为 `±HH:MM`;秒数非分钟整数倍时补秒段(`±HH:MM:SS`)。
///
/// # 边界与前提
/// - 输入为有符号偏移秒(|offset| ≤ 24 小时),负值取绝对值后拼负号。
pub(crate) fn format_offset_seconds_text(offset_seconds: i64) -> String {
let sign = if offset_seconds < 0 { '-' } else { '+' };
let magnitude = offset_seconds.abs();
if magnitude % 60 != 0 {
format!("{sign}{:02}:{:02}:{:02}", magnitude / 3600, (magnitude % 3600) / 60, magnitude % 60)
} else {
format!("{sign}{:02}:{:02}", magnitude / 3600, (magnitude % 3600) / 60)
}
}
/// 将 ZonedDateTime 按时区偏移转换为本地 PlainDateTime 分量。
pub(crate) fn zoned_date_time_plain_parts<H: VmHost>(
vm: &mut H, obj: &JsObject,
) -> Result<(i32, u32, u32, f64), JsValue> {
let Some(epoch_ns) = get_instant_epoch_ns(obj) else {
return Err(crate::error::create_range_error(vm, "invalid ZonedDateTime"));
};
let time_zone_id = to_string(obj.get_prop_at(1));
let Some(offset_seconds) = zone_offset_seconds(&time_zone_id, epoch_ns.div_euclid(1_000_000_000) as i64) else {
return Err(crate::error::create_range_error(vm, "invalid time zone"));
};
const DAY_NS: i128 = 86_400_000_000_000;
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) as f64;
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) {
return Err(crate::error::create_range_error(vm, "invalid date-time"));
}
Ok((year as i32, month as u32, day as u32, time_ns))
}
/// 本地墙钟分量 + 时区偏移(秒)→ 纪元纳秒(zoned_date_time_plain_parts 的逆)。
///
/// # 边界与前提
/// - (year, month, day, time_ns) 须已通过 valid_iso_date / valid_plain_time 校验(调用方保证)。
/// - 仅做 checked 溢出防护,Instant 范围校验由调用方按需执行。
pub(crate) fn local_to_epoch_ns(year: i32, month: u32, day: u32, time_ns: f64, offset_seconds: i64) -> Option<i128> {
let days = days_from_civil(i128::from(year), i128::from(month), i128::from(day));
days.checked_mul(86_400_000_000_000)?
.checked_add(time_ns as i128)?
.checked_sub(i128::from(offset_seconds) * 1_000_000_000)
}
pub(crate) fn make_plain_date<H: VmHost>(vm: &mut H, year: i32, month: u32, day: u32, calendar: &str) -> NativeResult {
let proto = JsValue::from_js_object(vm.builtin_proto(ProtoKind::PlainDateProto));
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto);
obj.type_tag = JsObject::OBJ_TYPE_PLAIN_DATE;
obj.set_prop_at(0, JsValue::float(year as f64));
obj.set_prop_at(1, JsValue::float(month as f64));
obj.set_prop_at(2, JsValue::float(day as f64));
obj.set_prop_at(3, vm.new_string(calendar));
NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj)))
}
pub(crate) fn make_plain_time<H: VmHost>(vm: &mut H, total_ns: f64) -> NativeResult {
let proto = JsValue::from_js_object(vm.builtin_proto(ProtoKind::PlainTimeProto));
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto);
obj.type_tag = JsObject::OBJ_TYPE_PLAIN_TIME;
obj.set_prop_at(0, JsValue::float(total_ns));
NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj)))
}
pub(crate) fn make_plain_date_time<H: VmHost>(
vm: &mut H, year: i32, month: u32, day: u32, total_ns: f64, calendar: &str,
) -> NativeResult {
let proto = JsValue::from_js_object(vm.builtin_proto(ProtoKind::PlainDateTimeProto));
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto);
obj.type_tag = JsObject::OBJ_TYPE_PLAIN_DATE_TIME;
obj.set_prop_at(0, JsValue::float(year as f64));
obj.set_prop_at(1, JsValue::float(month as f64));
obj.set_prop_at(2, JsValue::float(day as f64));
obj.set_prop_at(3, JsValue::float(total_ns));
obj.set_prop_at(4, vm.new_string(calendar));
NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj)))
}
pub(crate) fn make_duration<H: VmHost>(vm: &mut H, values: [f64; 10]) -> NativeResult {
let proto = JsValue::from_js_object(vm.builtin_proto(ProtoKind::DurationProto));
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto);
obj.type_tag = JsObject::OBJ_TYPE_DURATION;
for (index, value) in values.into_iter().enumerate() {
obj.set_prop_at(index, JsValue::float(value));
}
NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj)))
}
// Temporal.PlainMonthDay / Temporal.PlainYearMonth 对象地基:
// 数字分量转换、槽对象构造、构造器与 getter/toString/toJSON。
/// 构造 PlainMonthDay 实例对象(槽 0-3 = 月/日/参考年/日历 ID)。
/// from/toPlainDate 等返回新对象的成员使用;构造器走 receiver 初始化路径。
pub(crate) fn make_plain_month_day<H: VmHost>(
vm: &mut H, month: u32, day: u32, ref_year: i32, calendar: &str,
) -> NativeResult {
let proto = JsValue::from_js_object(vm.builtin_proto(ProtoKind::PlainMonthDayProto));
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto);
obj.type_tag = JsObject::OBJ_TYPE_PLAIN_MONTH_DAY;
obj.set_prop_at(0, JsValue::float(month as f64));
obj.set_prop_at(1, JsValue::float(day as f64));
obj.set_prop_at(2, JsValue::float(ref_year as f64));
obj.set_prop_at(3, vm.new_string(calendar));
NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj)))
}
/// 构造 PlainYearMonth 实例对象(槽 0-3 = 年/月/参考日/日历 ID)。
/// from 等返回新对象的成员使用;构造器走 receiver 初始化路径。
pub(crate) fn make_plain_year_month<H: VmHost>(
vm: &mut H, year: i32, month: u32, ref_day: u32, calendar: &str,
) -> NativeResult {
let proto = JsValue::from_js_object(vm.builtin_proto(ProtoKind::PlainYearMonthProto));
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto);
obj.type_tag = JsObject::OBJ_TYPE_PLAIN_YEAR_MONTH;
obj.set_prop_at(0, JsValue::float(year as f64));
obj.set_prop_at(1, JsValue::float(month as f64));
obj.set_prop_at(2, JsValue::float(ref_day as f64));
obj.set_prop_at(3, vm.new_string(calendar));
NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj)))
}
pub(crate) fn ensure_plain_month_day<H: VmHost>(vm: &mut H, obj: &JsObject) -> Result<(), JsValue> {
if !obj.is_plain_month_day_obj() {
return Err(crate::error::create_type_error(vm, "called on incompatible receiver"));
}
Ok(())
}
pub(crate) fn ensure_plain_year_month<H: VmHost>(vm: &mut H, obj: &JsObject) -> Result<(), JsValue> {
if !obj.is_plain_year_month_obj() {
return Err(crate::error::create_type_error(vm, "called on incompatible receiver"));
}
Ok(())
}