use chrono::{Datelike, Days, NaiveDate, Utc}; use num_traits::ToPrimitive; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_runtime_api::{to_number, to_string, NativeResult, VmHost}; use oxide_types::object::JsObject; use oxide_types::value::JsValue; // Temporal 命名空间的最小实现子集:Temporal.Now / Temporal.Instant / // Temporal.PlainDate / Temporal.PlainTime / Temporal.PlainDateTime / Temporal.ZonedDateTime。 // 内部数据按对象类型存入 prop 槽: // Instant 存纪元纳秒(BigInt,prop 0)、PlainDate 存年/月/日/日历 ID(prop 0-3)、 // PlainTime 存午夜后纳秒(f64,prop 0)、PlainDateTime 存年/月/日/午夜后纳秒/日历 ID(prop 0-4), // ZonedDateTime 存纪元纳秒、时区 ID、日历 ID(prop 0-2)。 // 日历 ID 未显式给定时统一取 "iso8601"。 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), } }; } 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 与日历传播共用。 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() } } fn get_instant_epoch_ns(obj: &JsObject) -> Option { 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() { return Some(to_number(value) as i128); } None } fn ensure_instant(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(()) } fn ensure_plain_date(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(()) } fn ensure_plain_time(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(()) } fn ensure_plain_date_time(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(()) } fn ensure_duration(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(()) } fn ensure_zoned_date_time(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(()) } fn receiver_obj(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) } fn initialize_temporal_receiver( 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。 fn is_ctor_call(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 } fn make_instant(vm: &mut H, epoch_ns: i128) -> NativeResult { let proto = JsValue::from_js_object(vm.session().builtin_world().instant_proto.as_ptr() as *mut JsObject); 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))) } fn make_zoned_date_time(vm: &mut H, epoch_ns: i128, time_zone_id: &str, calendar: &str) -> NativeResult { let proto = JsValue::from_js_object(vm.session().builtin_world().zoned_date_time_proto.as_ptr() as *mut JsObject); 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))) } fn parse_digits(bytes: &[u8], cursor: &mut usize, count: usize) -> Option { 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)) } fn is_leap_year(year: i128) -> bool { year.rem_euclid(4) == 0 && (year.rem_euclid(100) != 0 || year.rem_euclid(400) == 0) } fn days_in_month(year: i128, month: i128) -> Option { 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, } } 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 } fn instant_string_without_annotations(input: &str) -> Option<&str> { let Some(first_annotation) = input.find('[') else { return Some(input); }; let mut rest = &input[first_annotation..]; let mut saw_calendar = false; let mut saw_critical_calendar = false; let mut saw_time_zone = false; while !rest.is_empty() { let body_start = rest.strip_prefix('[')?; let close = body_start.find(']')?; let body = &body_start[..close]; rest = &body_start[close + 1..]; if body.is_empty() || (!rest.is_empty() && !rest.starts_with('[')) { return None; } let (critical, annotation) = match body.strip_prefix('!') { Some(value) => (true, value), None => (false, body), }; if let Some((key, _value)) = annotation.split_once('=') { if key.is_empty() || !key .bytes() .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'-' | b'_')) { return None; } if key == "u-ca" { if saw_calendar && (critical || saw_critical_calendar) { return None; } saw_calendar = true; saw_critical_calendar |= critical; } else if critical { return None; } } else { if saw_time_zone || annotation.is_empty() { return None; } if matches!(annotation.as_bytes().first(), Some(b'+' | b'-')) { let bytes = annotation.as_bytes(); let hour_only = bytes.len() == 3 && bytes[1..3].iter().all(u8::is_ascii_digit); let hour_minute = bytes.len() == 6 && bytes[3] == b':' && bytes[1..3].iter().all(u8::is_ascii_digit) && bytes[4..6].iter().all(u8::is_ascii_digit); if !hour_only && !hour_minute { return None; } let hour = (bytes[1] - b'0') * 10 + bytes[2] - b'0'; let minute = if hour_minute { (bytes[4] - b'0') * 10 + bytes[5] - b'0' } else { 0 }; if hour > 23 || minute > 59 { return None; } } saw_time_zone = true; } } Some(&input[..first_annotation]) } /// 解析带 UTC 标识或数值偏移的 Temporal ISO 日期时间到纪元纳秒。 /// /// # 边界与前提 /// - 支持扩展年份、compact 格式、annotation 和亚分钟 offset。 /// - 闰秒按前一秒解释;超出 Instant 范围或语法无效时返回 `None`。 fn parse_instant_string(s: &str) -> Option { let input = s.trim(); let bytes = instant_string_without_annotations(input)?.as_bytes(); // 解析公历日期;带符号年份固定为六位,负零扩展年份无效。 let mut cursor = 0usize; let negative_extended_year = bytes.first() == Some(&b'-'); let year_sign = match bytes.first().copied() { Some(b'+') => { cursor += 1; 1_i128 } Some(b'-') => { cursor += 1; -1_i128 } _ => 1_i128, }; let year_digits = if cursor == 0 { 4 } else { 6 }; let year_magnitude = parse_digits(bytes, &mut cursor, year_digits)?; if negative_extended_year && year_magnitude == 0 { return None; } let year = year_sign * year_magnitude; let separated_date = bytes.get(cursor) == Some(&b'-'); if separated_date { cursor += 1; } let month = parse_digits(bytes, &mut cursor, 2)?; if separated_date { if bytes.get(cursor) != Some(&b'-') { return None; } cursor += 1; } let day = parse_digits(bytes, &mut cursor, 2)?; // 时间允许小时、分钟或秒精度,日期与时间可用 T、t 或空格分隔。 if !matches!(bytes.get(cursor), Some(b'T' | b't' | b' ')) { return None; } cursor += 1; let hour = parse_digits(bytes, &mut cursor, 2)?; let colon_time = bytes.get(cursor) == Some(&b':'); let minute = if colon_time { cursor += 1; parse_digits(bytes, &mut cursor, 2)? } else if matches!(bytes.get(cursor), Some(byte) if byte.is_ascii_digit()) { parse_digits(bytes, &mut cursor, 2)? } else { 0 }; let (second, has_second) = if bytes.get(cursor) == Some(&b':') { cursor += 1; (parse_digits(bytes, &mut cursor, 2)?, true) } else if !colon_time && matches!(bytes.get(cursor), Some(byte) if byte.is_ascii_digit()) { (parse_digits(bytes, &mut cursor, 2)?, true) } else { (0, false) }; let mut subsecond_ns = 0_i128; if matches!(bytes.get(cursor), Some(b'.' | b',')) { if !has_second { return None; } cursor += 1; let fraction_start = cursor; while matches!(bytes.get(cursor), Some(byte) if byte.is_ascii_digit()) { cursor += 1; } let fraction_len = cursor - fraction_start; if fraction_len == 0 || fraction_len > 9 { return None; } let mut fraction_cursor = fraction_start; subsecond_ns = parse_digits(bytes, &mut fraction_cursor, fraction_len)?; subsecond_ns *= 10_i128.pow((9 - fraction_len) as u32); } // 数值 offset 支持 basic/extended 形式及秒以下精度。 let offset_ns = match bytes.get(cursor).copied() { Some(b'Z' | b'z') => { cursor += 1; 0_i128 } Some(sign @ (b'+' | b'-')) => { cursor += 1; let offset_hour = parse_digits(bytes, &mut cursor, 2)?; let colon_format = bytes.get(cursor) == Some(&b':'); let offset_minute = if colon_format { cursor += 1; parse_digits(bytes, &mut cursor, 2)? } else if matches!(bytes.get(cursor), Some(byte) if byte.is_ascii_digit()) { parse_digits(bytes, &mut cursor, 2)? } else { 0 }; let (offset_second, has_offset_second) = if colon_format && bytes.get(cursor) == Some(&b':') { cursor += 1; (parse_digits(bytes, &mut cursor, 2)?, true) } else if !colon_format && matches!(bytes.get(cursor), Some(byte) if byte.is_ascii_digit()) { (parse_digits(bytes, &mut cursor, 2)?, true) } else { (0, false) }; let mut offset_subsecond_ns = 0_i128; if matches!(bytes.get(cursor), Some(b'.' | b',')) { if !has_offset_second { return None; } cursor += 1; let fraction_start = cursor; while matches!(bytes.get(cursor), Some(byte) if byte.is_ascii_digit()) { cursor += 1; } let fraction_len = cursor - fraction_start; if fraction_len == 0 || fraction_len > 9 { return None; } let mut fraction_cursor = fraction_start; offset_subsecond_ns = parse_digits(bytes, &mut fraction_cursor, fraction_len)?; offset_subsecond_ns *= 10_i128.pow((9 - fraction_len) as u32); } if offset_hour > 23 || offset_minute > 59 || offset_second > 59 { return None; } let magnitude = (offset_hour * 3_600 + offset_minute * 60 + offset_second) * 1_000_000_000 + offset_subsecond_ns; if sign == b'-' { -magnitude } else { magnitude } } _ => return None, }; if cursor != bytes.len() || hour > 23 || minute > 59 || second > 60 || day == 0 || day > days_in_month(year, month)? { return None; } // 使用 proleptic Gregorian 日数精确换算,并在纳秒层应用 offset。 let epoch_seconds = days_from_civil(year, month, day) .checked_mul(86_400)? .checked_add(hour * 3_600 + minute * 60 + second.min(59))?; let epoch_ns = epoch_seconds .checked_mul(1_000_000_000)? .checked_add(subsecond_ns)? .checked_sub(offset_ns)?; (epoch_ns.unsigned_abs() <= MAX_INSTANT_NS as u128).then_some(epoch_ns) } fn native_engine_error(vm: &mut H, error: &str) -> JsValue { vm.take_uncaught_value() .unwrap_or_else(|| crate::error::create_from_text(vm, error)) } fn primitive_to_bigint(vm: &mut H, raw: JsValue) -> Result { let primitive = oxide_runtime_api::to_primitive(raw, oxide_runtime_api::ToPrimitiveHint::Number, vm) .map_err(|error| native_engine_error(vm, &error))?; if primitive.is_bigint() { return Ok(vm.bigint_value(primitive).to_i128().unwrap_or(i128::MAX)); } if primitive.is_bool() { return Ok(i128::from(primitive.as_bool())); } if primitive.is_string() { let input = to_string(primitive); let trimmed = input.trim(); if trimmed.is_empty() { return Ok(0); } return trimmed.parse::().map_err(|_| { crate::error::create_syntax_error(vm, "Cannot convert string to BigInt: invalid integer literal") }); } Err(crate::error::create_type_error(vm, "Cannot convert value to a BigInt")) } fn instant_like_epoch_ns(vm: &mut H, value: JsValue) -> Result { if value.is_object() { let ptr = value.as_js_object_ptr(); if !ptr.is_null() { let obj = unsafe { &*ptr }; if obj.is_instant_obj() { return get_instant_epoch_ns(obj) .ok_or_else(|| crate::error::create_range_error(vm, "invalid Instant")); } if obj.is_function() { return Err(crate::error::create_range_error(vm, "invalid ISO 8601 string")); } } // 对象按规范走 ToString -> ParseTemporalInstantString(解析失败 RangeError)。 let input = oxide_runtime_api::to_string_full(value, vm).map_err(|error| native_engine_error(vm, &error))?; return parse_instant_string(&input) .ok_or_else(|| crate::error::create_range_error(vm, "invalid ISO 8601 string")); } // 非字符串原始值(undefined/null/boolean/number/bigint/symbol)按规范直接抛 TypeError。 if !value.is_string() { return Err(crate::error::create_type_error(vm, "cannot convert to Instant")); } let input = to_string(value); parse_instant_string(&input).ok_or_else(|| crate::error::create_range_error(vm, "invalid ISO 8601 string")) } /// `Temporal.Now.instant()`:返回当前时刻的 Temporal.Instant。 pub fn now_instant(vm: &mut H, _args: &[u8]) -> NativeResult { let now = Utc::now(); let epoch_ns = now.timestamp() as i128 * 1_000_000_000 + now.timestamp_subsec_nanos() as i128; make_instant(vm, epoch_ns) } /// `Temporal.Now.timeZoneId()`:引擎固定使用 UTC。 pub fn now_time_zone_id(vm: &mut H, _args: &[u8]) -> NativeResult { NativeResult::Ok(vm.new_string("UTC")) } /// `Temporal.Instant` 构造器:把参数按 ToBigInt 转换为纪元纳秒。 pub fn instant_constructor(vm: &mut H, args: &[u8]) -> NativeResult { let ctor_proto = vm.session().builtin_world().instant_proto.as_ptr(); if !is_ctor_call(vm, args, ctor_proto) { return NativeResult::Err(crate::error::create_type_error( vm, "Class constructor Temporal.Instant cannot be invoked without 'new'", )); } let raw = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; let epoch_ns = native_try!(primitive_to_bigint(vm, raw)); if epoch_ns.unsigned_abs() > MAX_INSTANT_NS as u128 { return NativeResult::Err(crate::error::create_range_error(vm, "Instant outside supported range")); } let epoch_value = vm.new_bigint(num_bigint::BigInt::from(epoch_ns)); initialize_temporal_receiver(vm, args, JsObject::OBJ_TYPE_INSTANT, [epoch_value]) } /// `Temporal.Instant.from(value)`:接受 Instant、ISO 字符串或可转换为字符串的对象。 pub fn instant_from(vm: &mut H, args: &[u8]) -> NativeResult { let val = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; let epoch_ns = native_try!(instant_like_epoch_ns(vm, val)); make_instant(vm, epoch_ns) } /// `Temporal.Instant.fromEpochMilliseconds(epochMilliseconds)`:从整数毫秒创建 Instant。 pub fn instant_from_epoch_milliseconds(vm: &mut H, args: &[u8]) -> NativeResult { let raw = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; let primitive = match oxide_runtime_api::to_primitive(raw, oxide_runtime_api::ToPrimitiveHint::Number, vm) { Ok(value) => value, Err(error) => return NativeResult::Err(native_engine_error(vm, &error)), }; if primitive.is_bigint() || primitive.is_symbol() { return NativeResult::Err(crate::error::create_type_error(vm, "Cannot convert value to a number")); } let epoch_ms = to_number(primitive); if !epoch_ms.is_finite() || epoch_ms.fract() != 0.0 || epoch_ms.abs() > 8_640_000_000_000_000.0 { return NativeResult::Err(crate::error::create_range_error(vm, "invalid epoch milliseconds")); } make_instant(vm, (epoch_ms as i128) * 1_000_000) } /// `Temporal.Instant.fromEpochNanoseconds(epochNanoseconds)`:从 BigInt 纳秒创建 Instant。 pub fn instant_from_epoch_nanoseconds(vm: &mut H, args: &[u8]) -> NativeResult { let raw = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; let epoch_ns = native_try!(primitive_to_bigint(vm, raw)); if epoch_ns.unsigned_abs() > MAX_INSTANT_NS as u128 { return NativeResult::Err(crate::error::create_range_error(vm, "Instant outside supported range")); } make_instant(vm, epoch_ns) } /// `Temporal.Instant.compare(one, two)`:按纪元纳秒返回 -1、0 或 1。 pub fn instant_compare(vm: &mut H, args: &[u8]) -> NativeResult { let one = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; let two = if args.len() < 3 { JsValue::undefined() } else { vm.reg(args[2]) }; let one_ns = native_try!(instant_like_epoch_ns(vm, one)); let two_ns = native_try!(instant_like_epoch_ns(vm, two)); let result = match one_ns.cmp(&two_ns) { std::cmp::Ordering::Less => -1, std::cmp::Ordering::Equal => 0, std::cmp::Ordering::Greater => 1, }; NativeResult::Ok(JsValue::int(result)) } /// `Temporal.Instant.prototype.epochSeconds` getter。 pub fn instant_epoch_seconds(vm: &mut H, args: &[u8]) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; native_try!(ensure_instant(vm, obj)); let epoch_ns = match get_instant_epoch_ns(obj) { Some(value) => value, None => return NativeResult::Err(crate::error::create_range_error(vm, "invalid Instant")), }; NativeResult::Ok(JsValue::float(epoch_ns.div_euclid(1_000_000_000) as f64)) } /// `Temporal.Instant.prototype.epochMilliseconds` getter。 pub fn instant_epoch_milliseconds(vm: &mut H, args: &[u8]) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; native_try!(ensure_instant(vm, obj)); let epoch_ns = match get_instant_epoch_ns(obj) { Some(value) => value, None => return NativeResult::Err(crate::error::create_range_error(vm, "invalid Instant")), }; NativeResult::Ok(JsValue::float(epoch_ns.div_euclid(1_000_000) as f64)) } /// `Temporal.Instant.prototype.epochMicroseconds` getter。 pub fn instant_epoch_microseconds(vm: &mut H, args: &[u8]) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; native_try!(ensure_instant(vm, obj)); let epoch_ns = match get_instant_epoch_ns(obj) { Some(value) => value, None => return NativeResult::Err(crate::error::create_range_error(vm, "invalid Instant")), }; NativeResult::Ok(JsValue::float(epoch_ns.div_euclid(1_000) as f64)) } /// `Temporal.Instant.prototype.epochNanoseconds` getter:返回精确 BigInt。 pub fn instant_epoch_nanoseconds(vm: &mut H, args: &[u8]) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; native_try!(ensure_instant(vm, obj)); let value = obj.get_prop_at(0); if value.is_bigint() { NativeResult::Ok(value) } else { match get_instant_epoch_ns(obj) { Some(epoch_ns) => NativeResult::Ok(vm.new_bigint(num_bigint::BigInt::from(epoch_ns))), None => NativeResult::Err(crate::error::create_range_error(vm, "invalid Instant")), } } } /// `Temporal.Instant.prototype.equals(other)`:比较两个 Instant 的纪元纳秒。 pub fn instant_equals(vm: &mut H, args: &[u8]) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; native_try!(ensure_instant(vm, obj)); let Some(epoch_ns) = get_instant_epoch_ns(obj) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid Instant")); }; let other = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; let other_ns = native_try!(instant_like_epoch_ns(vm, other)); NativeResult::Ok(JsValue::bool(epoch_ns == other_ns)) } fn instant_add_duration(vm: &mut H, args: &[u8], direction: i128) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; native_try!(ensure_instant(vm, obj)); let Some(epoch_ns) = get_instant_epoch_ns(obj) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid Instant")); }; let duration_like = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; let values = native_try!(duration_like_values(vm, duration_like)); if values[..4].iter().any(|value| *value != 0.0) { return NativeResult::Err(crate::error::create_range_error( vm, "Instant arithmetic does not support date units", )); } let Some(delta_ns) = duration_time_nanoseconds(&values) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid duration")); }; let Some(result_ns) = delta_ns.checked_mul(direction).and_then(|delta| epoch_ns.checked_add(delta)) else { return NativeResult::Err(crate::error::create_range_error(vm, "Instant outside supported range")); }; if result_ns.unsigned_abs() > MAX_INSTANT_NS as u128 { return NativeResult::Err(crate::error::create_range_error(vm, "Instant outside supported range")); } make_instant(vm, result_ns) } /// `Temporal.Instant.prototype.add(durationLike)`:精确增加仅含时间单位的时长。 pub fn instant_add(vm: &mut H, args: &[u8]) -> NativeResult { instant_add_duration(vm, args, 1) } /// `Temporal.Instant.prototype.subtract(durationLike)`:精确减去仅含时间单位的时长。 pub fn instant_subtract(vm: &mut H, args: &[u8]) -> NativeResult { instant_add_duration(vm, args, -1) } #[derive(Clone, Copy)] enum InstantRoundingMode { Ceil, Expand, Floor, HalfCeil, HalfEven, HalfExpand, HalfFloor, HalfTrunc, Trunc, } fn temporal_option_value( vm: &mut H, obj: &JsObject, receiver: JsValue, name: &str, ) -> Result { 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)) } fn temporal_option_number(vm: &mut H, value: JsValue) -> Result { 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)) } fn temporal_option_string(vm: &mut H, value: JsValue) -> Result { 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)) } fn instant_rounding_mode(value: &str) -> Option { match value { "ceil" => Some(InstantRoundingMode::Ceil), "expand" => Some(InstantRoundingMode::Expand), "floor" => Some(InstantRoundingMode::Floor), "halfCeil" => Some(InstantRoundingMode::HalfCeil), "halfEven" => Some(InstantRoundingMode::HalfEven), "halfExpand" => Some(InstantRoundingMode::HalfExpand), "halfFloor" => Some(InstantRoundingMode::HalfFloor), "halfTrunc" => Some(InstantRoundingMode::HalfTrunc), "trunc" => Some(InstantRoundingMode::Trunc), _ => None, } } fn instant_round_unit(value: &str) -> Option<(i128, i128)> { match value { "hour" | "hours" => Some((3_600_000_000_000, 24)), "minute" | "minutes" => Some((60_000_000_000, 1_440)), "second" | "seconds" => Some((1_000_000_000, 86_400)), "millisecond" | "milliseconds" => Some((1_000_000, 86_400_000)), "microsecond" | "microseconds" => Some((1_000, 86_400_000_000)), "nanosecond" | "nanoseconds" => Some((1, 86_400_000_000_000)), _ => None, } } /// 按 Temporal 的 `RoundNumberToIncrementAsIfPositive` 语义舍入纳秒。 fn round_instant_ns(value: i128, increment: i128, mode: InstantRoundingMode) -> Option { let quotient = value.div_euclid(increment); let remainder = value.rem_euclid(increment); if remainder == 0 { return Some(value); } let use_upper = match mode { InstantRoundingMode::Ceil | InstantRoundingMode::Expand => true, InstantRoundingMode::Floor | InstantRoundingMode::Trunc => false, _ => match (remainder * 2).cmp(&increment) { std::cmp::Ordering::Less => false, std::cmp::Ordering::Greater => true, std::cmp::Ordering::Equal => match mode { InstantRoundingMode::HalfCeil | InstantRoundingMode::HalfExpand => true, InstantRoundingMode::HalfFloor | InstantRoundingMode::HalfTrunc => false, InstantRoundingMode::HalfEven => quotient.rem_euclid(2) != 0, _ => unreachable!(), }, }, }; quotient .checked_add(i128::from(use_upper)) .and_then(|rounded| rounded.checked_mul(increment)) } /// `Temporal.Instant.prototype.round(roundTo)`:按给定单位、增量和模式精确舍入。 pub fn instant_round(vm: &mut H, args: &[u8]) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; native_try!(ensure_instant(vm, obj)); let Some(epoch_ns) = get_instant_epoch_ns(obj) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid Instant")); }; let round_to = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; let (increment_value, mode_value, unit_value) = if round_to.is_string() { (1.0, "halfExpand".to_string(), Some(to_string(round_to))) } else { if !round_to.is_object() { return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be a string or object")); } let options_ptr = round_to.as_js_object_ptr(); if options_ptr.is_null() { return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be a string or object")); } let options = unsafe { &*options_ptr }; let increment_raw = native_try!(temporal_option_value(vm, options, round_to, "roundingIncrement")); let increment = if increment_raw.is_undefined() { 1.0 } else { native_try!(temporal_option_number(vm, increment_raw)) }; let mode_raw = native_try!(temporal_option_value(vm, options, round_to, "roundingMode")); let mode = if mode_raw.is_undefined() { "halfExpand".to_string() } else { native_try!(temporal_option_string(vm, mode_raw)) }; let unit_raw = native_try!(temporal_option_value(vm, options, round_to, "smallestUnit")); let unit = if unit_raw.is_undefined() { None } else { Some(native_try!(temporal_option_string(vm, unit_raw))) }; (increment, mode, unit) }; let Some(mode) = instant_rounding_mode(&mode_value) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding mode")); }; let Some(unit_value) = unit_value else { return NativeResult::Err(crate::error::create_range_error(vm, "smallestUnit is required")); }; let Some((unit_ns, units_per_day)) = instant_round_unit(&unit_value) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallest unit")); }; if !increment_value.is_finite() { return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); } let increment = increment_value.trunc(); if !(1.0..=1_000_000_000.0).contains(&increment) { return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); } let increment = increment as i128; if units_per_day % increment != 0 { return NativeResult::Err(crate::error::create_range_error(vm, "rounding increment must divide a day")); } let Some(quantum_ns) = unit_ns.checked_mul(increment) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); }; let Some(rounded_ns) = round_instant_ns(epoch_ns, quantum_ns, mode) else { return NativeResult::Err(crate::error::create_range_error(vm, "Instant outside supported range")); }; if rounded_ns.unsigned_abs() > MAX_INSTANT_NS as u128 { return NativeResult::Err(crate::error::create_range_error(vm, "Instant outside supported range")); } make_instant(vm, rounded_ns) } fn instant_difference_unit(value: &str) -> Option<(usize, i128, i128)> { match value { "hour" | "hours" => Some((0, 3_600_000_000_000, 24)), "minute" | "minutes" => Some((1, 60_000_000_000, 60)), "second" | "seconds" => Some((2, 1_000_000_000, 60)), "millisecond" | "milliseconds" => Some((3, 1_000_000, 1_000)), "microsecond" | "microseconds" => Some((4, 1_000, 1_000)), "nanosecond" | "nanoseconds" => Some((5, 1, 1_000)), _ => None, } } /// 按普通有符号数语义舍入 Instant 差值。 fn round_instant_difference(value: i128, increment: i128, mode: InstantRoundingMode) -> Option { let negative = value < 0; let magnitude = value.checked_abs()?; let quotient = magnitude / increment; let remainder = magnitude % increment; if remainder == 0 { return Some(value); } let use_upper = match mode { InstantRoundingMode::Ceil => !negative, InstantRoundingMode::Expand => true, InstantRoundingMode::Floor => negative, InstantRoundingMode::Trunc => false, _ => match (remainder * 2).cmp(&increment) { std::cmp::Ordering::Less => false, std::cmp::Ordering::Greater => true, std::cmp::Ordering::Equal => match mode { InstantRoundingMode::HalfCeil => !negative, InstantRoundingMode::HalfEven => quotient.rem_euclid(2) != 0, InstantRoundingMode::HalfExpand => true, InstantRoundingMode::HalfFloor => negative, InstantRoundingMode::HalfTrunc => false, _ => unreachable!(), }, }, }; let rounded = quotient.checked_add(i128::from(use_upper))?.checked_mul(increment)?; Some(if negative { -rounded } else { rounded }) } fn balance_instant_difference(value: i128, largest_unit: usize) -> Option<[f64; 10]> { const SCALES: [i128; 6] = [3_600_000_000_000, 60_000_000_000, 1_000_000_000, 1_000_000, 1_000, 1]; let negative = value < 0; let mut remainder = value.checked_abs()?; let mut values = [0.0; 10]; for (index, scale) in SCALES.iter().enumerate().skip(largest_unit) { let component = remainder / scale; remainder %= scale; if component != 0 { values[index + 4] = if negative { -(component as f64) } else { component as f64 }; } } Some(values) } fn instant_difference(vm: &mut H, args: &[u8], since: bool) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; native_try!(ensure_instant(vm, obj)); let Some(epoch_ns) = get_instant_epoch_ns(obj) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid Instant")); }; // other 必须先完成 Instant 转换,之后才读取 options。 let other = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; let other_ns = native_try!(instant_like_epoch_ns(vm, other)); let options_value = if args.len() < 3 { JsValue::undefined() } else { vm.reg(args[2]) }; let (largest_value, increment_value, mode_value, smallest_value) = if options_value.is_undefined() { (None, 1.0, "trunc".to_string(), "nanosecond".to_string()) } else { if !options_value.is_object() { return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object")); } let options_ptr = options_value.as_js_object_ptr(); if options_ptr.is_null() { return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object")); } let options = unsafe { &*options_ptr }; let largest_raw = native_try!(temporal_option_value(vm, options, options_value, "largestUnit")); let largest = if largest_raw.is_undefined() { None } else { Some(native_try!(temporal_option_string(vm, largest_raw))) }; let increment_raw = native_try!(temporal_option_value(vm, options, options_value, "roundingIncrement")); let increment = if increment_raw.is_undefined() { 1.0 } else { native_try!(temporal_option_number(vm, increment_raw)) }; let mode_raw = native_try!(temporal_option_value(vm, options, options_value, "roundingMode")); let mode = if mode_raw.is_undefined() { "trunc".to_string() } else { native_try!(temporal_option_string(vm, mode_raw)) }; let smallest_raw = native_try!(temporal_option_value(vm, options, options_value, "smallestUnit")); let smallest = if smallest_raw.is_undefined() { "nanosecond".to_string() } else { native_try!(temporal_option_string(vm, smallest_raw)) }; (largest, increment, mode, smallest) }; let Some((smallest_index, smallest_ns, increment_limit)) = instant_difference_unit(&smallest_value) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallest unit")); }; let largest_index = match largest_value { Some(value) => match instant_difference_unit(&value) { Some((index, _, _)) => index, None => return NativeResult::Err(crate::error::create_range_error(vm, "invalid largest unit")), }, None => smallest_index.min(2), }; let Some(mode) = instant_rounding_mode(&mode_value) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding mode")); }; if !increment_value.is_finite() { return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); } let increment = increment_value.trunc(); if !(1.0..=1_000_000_000.0).contains(&increment) { return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); } let increment = increment as i128; if increment >= increment_limit || increment_limit % increment != 0 { return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); } if largest_index > smallest_index { return NativeResult::Err(crate::error::create_range_error(vm, "smallest unit exceeds largest unit")); } let delta = if since { epoch_ns - other_ns } else { other_ns - epoch_ns }; let Some(quantum_ns) = smallest_ns.checked_mul(increment) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); }; let Some(rounded_ns) = round_instant_difference(delta, quantum_ns, mode) else { return NativeResult::Err(crate::error::create_range_error(vm, "Instant difference is out of range")); }; let Some(values) = balance_instant_difference(rounded_ns, largest_index) else { return NativeResult::Err(crate::error::create_range_error(vm, "Instant difference is out of range")); }; make_duration(vm, values) } /// `Temporal.Instant.prototype.until(other, options)`:返回从 receiver 到 other 的精确时长。 pub fn instant_until(vm: &mut H, args: &[u8]) -> NativeResult { instant_difference(vm, args, false) } /// `Temporal.Instant.prototype.since(other, options)`:返回从 other 到 receiver 的精确时长。 pub fn instant_since(vm: &mut H, args: &[u8]) -> NativeResult { instant_difference(vm, args, true) } enum FractionalSecondDigitsInput { Auto, Number(f64), String(String), } fn parse_fractional_second_digits( vm: &mut H, input: FractionalSecondDigitsInput, ) -> Result, 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)) } } } fn parse_offset_minutes(value: &str) -> Option { 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 = i32::from(bytes[1] - b'0') * 10 + i32::from(bytes[2] - b'0'); let minutes = i32::from(bytes[4] - b'0') * 10 + i32::from(bytes[5] - b'0'); if hours > 23 || minutes > 59 { return None; } let magnitude = hours * 60 + minutes; Some(if bytes[0] == b'-' { -magnitude } else { magnitude }) } fn canonical_time_zone(value: &str) -> Option<(String, i32)> { let input = value.trim(); if input.eq_ignore_ascii_case("UTC") || input.eq_ignore_ascii_case("Z") { return Some(("UTC".to_string(), 0)); } if let Some(offset) = parse_offset_minutes(input) { return Some((input.to_string(), offset)); } // ±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 i32 * 10 + (input.as_bytes()[2] - b'0') as i32; if hour <= 23 { let sign = if input.as_bytes()[0] == b'-' { -1 } else { 1 }; return Some((input.to_string(), sign * hour * 60)); } 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 i32 * 10 + (input.as_bytes()[2] - b'0') as i32; let minute = (input.as_bytes()[3] - b'0') as i32 * 10 + (input.as_bytes()[4] - b'0') as i32; if hour <= 23 && minute <= 59 { let sign = if input.as_bytes()[0] == b'-' { -1 } else { 1 }; return Some((input.to_string(), sign * (hour * 60 + minute))); } return None; } if input.starts_with("-000000") { return None; } // 带 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(), 0)); } return parse_offset_minutes(annotation).map(|offset| (annotation.to_string(), offset)); } let time_start = input.find(['T', 't', ' '])?; let time = &input[time_start + 1..]; if time.ends_with(['Z', 'z']) { return Some(("UTC".to_string(), 0)); } let offset_start = time .char_indices() .rev() .find_map(|(index, ch)| matches!(ch, '+' | '-').then_some(index))?; let offset_id = &time[offset_start..]; parse_offset_minutes(offset_id).map(|offset| (offset_id.to_string(), offset)) } fn instant_time_zone_offset(value: &str) -> Option { canonical_time_zone(value).map(|(_, offset)| offset) } 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) } 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) } } fn format_instant_iso( epoch_ns: i128, offset_minutes: Option, include_seconds: bool, fractional_digits: Option, ) -> Option { const DAY_NS: i128 = 86_400_000_000_000; let offset_ns = i128::from(offset_minutes.unwrap_or(0)).checked_mul(60_000_000_000)?; let local_ns = epoch_ns.checked_add(offset_ns)?; let days = local_ns.div_euclid(DAY_NS); let mut time_ns = local_ns.rem_euclid(DAY_NS); let hour = time_ns / 3_600_000_000_000; time_ns %= 3_600_000_000_000; let minute = time_ns / 60_000_000_000; time_ns %= 60_000_000_000; let second = time_ns / 1_000_000_000; let subsecond = time_ns % 1_000_000_000; let (year, month, day) = civil_from_days(days); let mut output = format!("{}-{month:02}-{day:02}T{hour:02}:{minute:02}", format_iso_year(year)); if include_seconds { output.push_str(&format!(":{second:02}")); match fractional_digits { Some(0) => {} Some(digits) => { let fraction = format!("{subsecond:09}"); output.push('.'); output.push_str(&fraction[..digits]); } None if subsecond != 0 => { let fraction = format!("{subsecond:09}").trim_end_matches('0').to_string(); output.push('.'); output.push_str(&fraction); } None => {} } } if let Some(offset) = offset_minutes { let sign = if offset < 0 { '-' } else { '+' }; let magnitude = offset.abs(); output.push_str(&format!("{sign}{:02}:{:02}", magnitude / 60, magnitude % 60)); } else { output.push('Z'); } Some(output) } fn instant_default_string(vm: &mut H, args: &[u8]) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; native_try!(ensure_instant(vm, obj)); let Some(epoch_ns) = get_instant_epoch_ns(obj) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid Instant")); }; match format_instant_iso(epoch_ns, None, true, None) { Some(output) => NativeResult::Ok(vm.new_string_owned(output)), None => NativeResult::Err(crate::error::create_range_error(vm, "invalid Instant")), } } /// `Temporal.Instant.prototype.toString(options)`:按精度、舍入模式和时区输出 ISO 8601。 pub fn instant_to_string(vm: &mut H, args: &[u8]) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; native_try!(ensure_instant(vm, obj)); let epoch_ns = match get_instant_epoch_ns(obj) { Some(value) => value, None => return NativeResult::Err(crate::error::create_range_error(vm, "invalid Instant")), }; let options_value = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; let (fractional_input, mode_value, smallest_value, time_zone_raw) = if options_value.is_undefined() { (FractionalSecondDigitsInput::Auto, "trunc".to_string(), None, JsValue::undefined()) } else { if !options_value.is_object() { return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object")); } let options_ptr = options_value.as_js_object_ptr(); if options_ptr.is_null() { return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object")); } let options = unsafe { &*options_ptr }; let fractional_raw = native_try!(temporal_option_value(vm, options, options_value, "fractionalSecondDigits")); let fractional = if fractional_raw.is_undefined() { FractionalSecondDigitsInput::Auto } else if fractional_raw.is_int() || fractional_raw.is_double() { FractionalSecondDigitsInput::Number(to_number(fractional_raw)) } else { FractionalSecondDigitsInput::String(native_try!(temporal_option_string(vm, fractional_raw))) }; let mode_raw = native_try!(temporal_option_value(vm, options, options_value, "roundingMode")); let mode = if mode_raw.is_undefined() { "trunc".to_string() } else { native_try!(temporal_option_string(vm, mode_raw)) }; let smallest_raw = native_try!(temporal_option_value(vm, options, options_value, "smallestUnit")); let smallest = if smallest_raw.is_undefined() { None } else { Some(native_try!(temporal_option_string(vm, smallest_raw))) }; let time_zone = native_try!(temporal_option_value(vm, options, options_value, "timeZone")); (fractional, mode, smallest, time_zone) }; let fractional_digits = native_try!(parse_fractional_second_digits(vm, fractional_input)); let Some(mode) = instant_rounding_mode(&mode_value) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding mode")); }; let (quantum_ns, include_seconds, output_digits) = match smallest_value.as_deref() { Some("minute" | "minutes") => (60_000_000_000, false, Some(0)), Some("second" | "seconds") => (1_000_000_000, true, Some(0)), Some("millisecond" | "milliseconds") => (1_000_000, true, Some(3)), Some("microsecond" | "microseconds") => (1_000, true, Some(6)), Some("nanosecond" | "nanoseconds") => (1, true, Some(9)), Some(_) => return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallest unit")), None => match fractional_digits { Some(digits) => (10_i128.pow((9 - digits) as u32), true, Some(digits)), None => (1, true, None), }, }; let offset_minutes = if time_zone_raw.is_undefined() { None } else { if !time_zone_raw.is_string() { return NativeResult::Err(crate::error::create_type_error(vm, "invalid time zone")); } let time_zone = to_string(time_zone_raw); match instant_time_zone_offset(&time_zone) { Some(offset) => Some(offset), None => return NativeResult::Err(crate::error::create_range_error(vm, "invalid time zone")), } }; let Some(rounded_ns) = round_instant_ns(epoch_ns, quantum_ns, mode) else { return NativeResult::Err(crate::error::create_range_error(vm, "Instant outside supported range")); }; if rounded_ns.unsigned_abs() > MAX_INSTANT_NS as u128 { return NativeResult::Err(crate::error::create_range_error(vm, "Instant outside supported range")); } match format_instant_iso(rounded_ns, offset_minutes, include_seconds, output_digits) { Some(output) => NativeResult::Ok(vm.new_string_owned(output)), None => NativeResult::Err(crate::error::create_range_error(vm, "invalid Instant")), } } /// `Temporal.Instant.prototype.toJSON()`:输出默认 UTC ISO 字符串,忽略参数。 pub fn instant_to_json(vm: &mut H, args: &[u8]) -> NativeResult { instant_default_string(vm, args) } /// `Temporal.Instant.prototype.toLocaleString()`:当前使用稳定的 UTC ISO 表示。 pub fn instant_to_locale_string(vm: &mut H, args: &[u8]) -> NativeResult { instant_default_string(vm, args) } /// `Temporal.Instant.prototype.toZonedDateTimeISO(timeZone)`:以同一纪元纳秒创建 ISO 日历 ZonedDateTime。 pub fn instant_to_zoned_date_time_iso(vm: &mut H, args: &[u8]) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; native_try!(ensure_instant(vm, obj)); let Some(epoch_ns) = get_instant_epoch_ns(obj) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid Instant")); }; if args.len() < 2 { return NativeResult::Err(crate::error::create_type_error(vm, "timeZone is required")); } let raw = vm.reg(args[1]); if !raw.is_string() { return NativeResult::Err(crate::error::create_type_error(vm, "invalid time zone")); } let input = to_string(raw); let Some((time_zone_id, _)) = canonical_time_zone(&input) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid time zone")); }; make_zoned_date_time(vm, epoch_ns, &time_zone_id, "iso8601") } /// `Temporal.Instant.prototype.valueOf()`:Temporal 对象禁止转原始值。 pub fn instant_value_of(vm: &mut H, _args: &[u8]) -> NativeResult { NativeResult::Err(crate::error::create_type_error(vm, "Temporal.Instant has no valueOf")) } /// `Temporal.ZonedDateTime` 构造器:保存纪元纳秒、固定偏移或 UTC 时区以及 ISO 日历。 pub fn zoned_date_time_constructor(vm: &mut H, args: &[u8]) -> NativeResult { let ctor_proto = vm.session().builtin_world().zoned_date_time_proto.as_ptr(); if !is_ctor_call(vm, args, ctor_proto) { return NativeResult::Err(crate::error::create_type_error( vm, "Class constructor Temporal.ZonedDateTime cannot be invoked without 'new'", )); } let epoch_raw = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; let epoch_ns = native_try!(primitive_to_bigint(vm, epoch_raw)); if epoch_ns.unsigned_abs() > MAX_INSTANT_NS as u128 { return NativeResult::Err(crate::error::create_range_error(vm, "ZonedDateTime outside supported range")); } if args.len() < 3 || !vm.reg(args[2]).is_string() { return NativeResult::Err(crate::error::create_type_error(vm, "invalid time zone")); } let time_zone_input = to_string(vm.reg(args[2])); let Some((time_zone_id, _)) = canonical_time_zone(&time_zone_input) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid time zone")); }; let calendar = if args.len() >= 4 && !vm.reg(args[3]).is_undefined() { native_try!(temporal_calendar_id_strict(vm, vm.reg(args[3]))).unwrap_or_else(|| "iso8601".to_string()) } else { "iso8601".to_string() }; 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); initialize_temporal_receiver( vm, args, JsObject::OBJ_TYPE_ZONED_DATE_TIME, [epoch_value, time_zone_value, calendar_value], ) } /// `Temporal.ZonedDateTime.prototype.epochNanoseconds`:返回精确纪元纳秒。 pub fn zoned_date_time_epoch_nanoseconds(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)); NativeResult::Ok(obj.get_prop_at(0)) } /// `Temporal.ZonedDateTime.prototype.timeZoneId`:返回规范化时区标识符。 pub fn zoned_date_time_time_zone_id(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)); NativeResult::Ok(obj.get_prop_at(1)) } /// `Temporal.ZonedDateTime.prototype.calendarId`:读日历槽。 pub fn zoned_date_time_calendar_id(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)); NativeResult::Ok(obj.get_prop_at(2)) } /// 与 instant_string_without_annotations 同一次扫描并行提取末尾时区注解(不剥离)。 /// /// # 步骤 /// 1. 仿 instant_string_without_annotations 的注解扫描循环,跳过带 `=` 的 key 注解。 /// 2. 首个无 `=` 的注解按时间区形式校验(UTC 大小写 / ±HH / ±HH:MM / ±HHMM),命中即返回。 /// /// # 边界与前提 /// - 无时区注解或注解形式非法返回 None;critical(`!` 前缀)注解同样返回(带标记)。 /// - 不修改 instant_string_without_annotations 的返回;剥离与提取各自独立扫描。 /// - IANA 命名区(如 America/New_York)通过本函数校验,由调用方 canonical_time_zone 裁决。 #[allow(dead_code)] fn extract_time_zone_annotation(input: &str) -> Option<(String, bool)> { let first_annotation = input.find('[')?; let mut rest = &input[first_annotation..]; while !rest.is_empty() { let body_start = rest.strip_prefix('[')?; let close = body_start.find(']')?; let body = &body_start[..close]; rest = &body_start[close + 1..]; if body.is_empty() || (!rest.is_empty() && !rest.starts_with('[')) { return None; } let (critical, annotation) = match body.strip_prefix('!') { Some(value) => (true, value), None => (false, body), }; // 带 `=` 的是 key 注解(u-ca 等),跳过继续扫描。 if annotation.contains('=') { continue; } if !is_time_zone_annotation_value(annotation) { return None; } return Some((annotation.to_string(), critical)); } None } /// 校验时区注解值:UTC/Z 大小写折叠、±HH / ±HH:MM / ±HHMM 数值偏移。 /// 非数值形式(IANA 命名区)原样放行,交由 canonical_time_zone 裁决。 fn is_time_zone_annotation_value(annotation: &str) -> bool { if annotation.eq_ignore_ascii_case("UTC") || annotation.eq_ignore_ascii_case("Z") { return true; } if !matches!(annotation.as_bytes().first(), Some(b'+' | b'-')) { return true; } let bytes = annotation.as_bytes(); let hour_only = bytes.len() == 3 && bytes[1..3].iter().all(u8::is_ascii_digit); let hour_minute = bytes.len() == 6 && bytes[3] == b':' && bytes[1..3].iter().all(u8::is_ascii_digit) && bytes[4..6].iter().all(u8::is_ascii_digit); let hour_minute_compact = bytes.len() == 5 && bytes[1..5].iter().all(u8::is_ascii_digit); if hour_only { return (bytes[1] - b'0') * 10 + bytes[2] - b'0' <= 23; } if hour_minute { let hour = (bytes[1] - b'0') * 10 + bytes[2] - b'0'; let minute = (bytes[4] - b'0') * 10 + bytes[5] - b'0'; return hour <= 23 && minute <= 59; } if hour_minute_compact { let hour = (bytes[1] - b'0') * 10 + bytes[2] - b'0'; let minute = (bytes[3] - b'0') * 10 + bytes[4] - b'0'; return hour <= 23 && minute <= 59; } false } /// 时区注解文本:命名区原样返回,偏移区规范化为 `±HH:MM` 带冒号;critical 时 `!` 置于括号内。 fn format_time_zone_annotation(time_zone_id: &str, critical: bool) -> String { let inner = if matches!(time_zone_id.as_bytes().first(), Some(b'+') | Some(b'-')) { let offset_minutes = instant_time_zone_offset(time_zone_id).unwrap_or(0); let sign = if offset_minutes < 0 { '-' } else { '+' }; let magnitude = offset_minutes.abs(); format!("{sign}{:02}:{:02}", magnitude / 60, magnitude % 60) } else { time_zone_id.to_string() }; if critical { format!("[!{inner}]") } else { format!("[{inner}]") } } /// ZDT 字符串化核心:epoch 域已舍入后按偏移与注解选项拼 `{date}T{time}.fff{offset}[{tz}][{ca}]`。 /// /// # 边界与前提 /// - epoch_ns 须已按 quantum 舍入(调用方保证),此处仅做本地墙钟分解。 /// - offset_name/time_zone_name 取 auto/never/critical,calendar_name 取 auto/never/always/critical。 /// - 偏移段恒 `±HH:MM` 带冒号;时区注解命名区原样、偏移区规范化。 #[expect(clippy::too_many_arguments)] fn format_zoned_date_time_iso( epoch_ns: i128, offset_minutes: i32, time_zone_id: &str, calendar_id: &str, include_seconds: bool, output_digits: Option, offset_name: &str, time_zone_name: &str, calendar_name: &str, ) -> Option { const DAY_NS: i128 = 86_400_000_000_000; let offset_ns = i128::from(offset_minutes).checked_mul(60_000_000_000)?; let local_ns = epoch_ns.checked_add(offset_ns)?; let days = local_ns.div_euclid(DAY_NS); let mut time_ns = local_ns.rem_euclid(DAY_NS); let hour = time_ns / 3_600_000_000_000; time_ns %= 3_600_000_000_000; let minute = time_ns / 60_000_000_000; time_ns %= 60_000_000_000; let second = time_ns / 1_000_000_000; let subsecond = time_ns % 1_000_000_000; let (year, month, day) = civil_from_days(days); let mut output = format!("{}-{month:02}-{day:02}T{hour:02}:{minute:02}", format_iso_year(year)); if include_seconds { output.push_str(&format!(":{second:02}")); match output_digits { Some(0) => {} Some(digits) => { let fraction = format!("{subsecond:09}"); output.push('.'); output.push_str(&fraction[..digits]); } None if subsecond != 0 => { let fraction = format!("{subsecond:09}").trim_end_matches('0').to_string(); output.push('.'); output.push_str(&fraction); } None => {} } } // offset 段:auto/always 显示,never 省略,critical 段前加 !。 if offset_name != "never" { let sign = if offset_minutes < 0 { '-' } else { '+' }; let magnitude = offset_minutes.abs(); if offset_name == "critical" { output.push('!'); } output.push_str(&format!("{sign}{:02}:{:02}", magnitude / 60, magnitude % 60)); } // timeZoneName 注解:auto 显示,never 省略,critical 时 `!` 置于括号内。 if time_zone_name != "never" { output.push_str(&format_time_zone_annotation(time_zone_id, time_zone_name == "critical")); } // calendarName 注解:always/critical 显示,auto/never 省略,critical 时 `!` 置于括号内。 if calendar_name == "always" || calendar_name == "critical" { if calendar_name == "critical" { output.push_str(&format!("[!u-ca={calendar_id}]")); } else { output.push_str(&format!("[u-ca={calendar_id}]")); } } Some(output) } /// `Temporal.ZonedDateTime.prototype.toString(options)`:epoch 域舍入后按六项 options 输出 ISO 8601。 pub fn zoned_date_time_to_string(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 Some(epoch_ns) = get_instant_epoch_ns(obj) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid ZonedDateTime")); }; let time_zone_id = to_string(obj.get_prop_at(1)); let offset_minutes = native_try!(zoned_date_time_offset_minutes(vm, obj)); let calendar_id = get_calendar_id(obj, 2); let options_value = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; // 六项 options 按规范顺序先全部 Get:calendarName → timeZoneName → offset → // fractionalSecondDigits → roundingMode → smallestUnit。 let (calendar_name, time_zone_name, offset_name, fractional_input, mode_value, smallest_value) = if options_value .is_undefined() { ( "auto".to_string(), "auto".to_string(), "auto".to_string(), FractionalSecondDigitsInput::Auto, "trunc".to_string(), None, ) } else { if !options_value.is_object() { return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object")); } let options_ptr = options_value.as_js_object_ptr(); if options_ptr.is_null() { return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object")); } let options = unsafe { &*options_ptr }; let calendar_raw = native_try!(temporal_option_value(vm, options, options_value, "calendarName")); let calendar = if calendar_raw.is_undefined() { "auto".to_string() } else { native_try!(temporal_option_string(vm, calendar_raw)) }; let tz_raw = native_try!(temporal_option_value(vm, options, options_value, "timeZoneName")); let tz_name = if tz_raw.is_undefined() { "auto".to_string() } else { native_try!(temporal_option_string(vm, tz_raw)) }; let offset_raw = native_try!(temporal_option_value(vm, options, options_value, "offset")); let offset_name = if offset_raw.is_undefined() { "auto".to_string() } else { native_try!(temporal_option_string(vm, offset_raw)) }; let fractional_raw = native_try!(temporal_option_value(vm, options, options_value, "fractionalSecondDigits")); let fractional = if fractional_raw.is_undefined() { FractionalSecondDigitsInput::Auto } else if fractional_raw.is_int() || fractional_raw.is_double() { FractionalSecondDigitsInput::Number(to_number(fractional_raw)) } else { FractionalSecondDigitsInput::String(native_try!(temporal_option_string(vm, fractional_raw))) }; let mode_raw = native_try!(temporal_option_value(vm, options, options_value, "roundingMode")); let mode = if mode_raw.is_undefined() { "trunc".to_string() } else { native_try!(temporal_option_string(vm, mode_raw)) }; let smallest_raw = native_try!(temporal_option_value(vm, options, options_value, "smallestUnit")); let smallest = if smallest_raw.is_undefined() { None } else { Some(native_try!(temporal_option_string(vm, smallest_raw))) }; (calendar, tz_name, offset_name, fractional, mode, smallest) }; // 白名单校验(全部 Get 完成后统一校验)。 if !matches!(offset_name.as_str(), "auto" | "never" | "always" | "critical") { return NativeResult::Err(crate::error::create_range_error(vm, "invalid offset option")); } if !matches!(time_zone_name.as_str(), "auto" | "never" | "critical") { return NativeResult::Err(crate::error::create_range_error(vm, "invalid timeZoneName option")); } if !matches!(calendar_name.as_str(), "auto" | "never" | "always" | "critical") { return NativeResult::Err(crate::error::create_range_error(vm, "invalid calendarName option")); } let fractional_digits = native_try!(parse_fractional_second_digits(vm, fractional_input)); let Some(mode) = instant_rounding_mode(&mode_value) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding mode")); }; let (quantum_ns, include_seconds, output_digits) = match smallest_value.as_deref() { Some("minute" | "minutes") => (60_000_000_000, false, Some(0)), Some("second" | "seconds") => (1_000_000_000, true, Some(0)), Some("millisecond" | "milliseconds") => (1_000_000, true, Some(3)), Some("microsecond" | "microseconds") => (1_000, true, Some(6)), Some("nanosecond" | "nanoseconds") => (1, true, Some(9)), Some(_) => return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallest unit")), None => match fractional_digits { Some(digits) => (10_i128.pow((9 - digits) as u32), true, Some(digits)), None => (1, true, None), }, }; let Some(rounded_ns) = round_instant_ns(epoch_ns, quantum_ns, mode) else { return NativeResult::Err(crate::error::create_range_error(vm, "ZonedDateTime outside supported range")); }; if rounded_ns.unsigned_abs() > MAX_INSTANT_NS as u128 { return NativeResult::Err(crate::error::create_range_error(vm, "ZonedDateTime outside supported range")); } match format_zoned_date_time_iso( rounded_ns, offset_minutes, &time_zone_id, &calendar_id, include_seconds, output_digits, &offset_name, &time_zone_name, &calendar_name, ) { Some(output) => NativeResult::Ok(vm.new_string_owned(output)), None => NativeResult::Err(crate::error::create_range_error(vm, "invalid ZonedDateTime")), } } /// `Temporal.ZonedDateTime.prototype.toJSON()`:输出默认 toString 字符串,忽略参数。 pub fn zoned_date_time_to_json(vm: &mut H, args: &[u8]) -> NativeResult { let receiver = args.first().copied().unwrap_or(0); zoned_date_time_to_string(vm, &[receiver]) } /// `Temporal.ZonedDateTime.prototype.toLocaleString()`:当前使用稳定的默认 ISO 表示。 pub fn zoned_date_time_to_locale_string(vm: &mut H, args: &[u8]) -> NativeResult { let receiver = args.first().copied().unwrap_or(0); zoned_date_time_to_string(vm, &[receiver]) } /// `Temporal.ZonedDateTime.prototype.valueOf()`:Temporal 对象禁止转原始值。 pub fn zoned_date_time_value_of(vm: &mut H, _args: &[u8]) -> NativeResult { NativeResult::Err(crate::error::create_type_error(vm, "Temporal.ZonedDateTime has no valueOf")) } /// `Temporal.ZonedDateTime.prototype.withTimeZone(timeZone)`:返回同一 instant 换时区槽的新 ZDT。 /// /// # 步骤 /// 1. branding 校验 receiver 为 ZDT。 /// 2. 读槽 0 epoch 与槽 2 calendar(保不变),参数经 canonical_time_zone 解析新时区。 /// 3. make_zoned_date_time 重建对象,仅替换时区槽。 /// /// # 边界与前提 /// - 参数须为字符串;非字符串抛 TypeError。无法解析的时区串抛 RangeError。 /// - epoch 与 calendar 槽原样保留,仅时区 ID 变化。 pub fn zoned_date_time_with_time_zone(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 Some(epoch_ns) = get_instant_epoch_ns(obj) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid ZonedDateTime")); }; let calendar_id = get_calendar_id(obj, 2); if args.len() < 2 { return NativeResult::Err(crate::error::create_type_error(vm, "timeZone is required")); } let raw = vm.reg(args[1]); if !raw.is_string() { return NativeResult::Err(crate::error::create_type_error(vm, "invalid time zone")); } let input = to_string(raw); let Some((time_zone_id, _)) = canonical_time_zone(&input) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid time zone")); }; make_zoned_date_time(vm, epoch_ns, &time_zone_id, &calendar_id) } /// `Temporal.ZonedDateTime.prototype.equals(other)`:与另一 ZDT 按 epoch/时区/日历三槽比较。 /// /// # 边界与前提 /// - receiver 须为 ZDT,否则 TypeError。 /// - 参数仅支持 ZDT 对象:三槽(epoch/时区/日历)全等才返回 true。 /// - 非 ZDT 对象或非对象参数:S3 基础范围外,返回 false(完整比较语义待后续)。 pub fn zoned_date_time_equals(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 other = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; if !other.is_object() { return NativeResult::Ok(JsValue::bool(false)); } let other_ptr = other.as_js_object_ptr(); if other_ptr.is_null() { return NativeResult::Ok(JsValue::bool(false)); } let other_obj = unsafe { &*other_ptr }; if !other_obj.is_zoned_date_time_obj() { return NativeResult::Ok(JsValue::bool(false)); } let epoch_equal = get_instant_epoch_ns(obj) == get_instant_epoch_ns(other_obj); let zone_equal = to_string(obj.get_prop_at(1)) == to_string(other_obj.get_prop_at(1)); let calendar_equal = get_calendar_id(obj, 2) == get_calendar_id(other_obj, 2); NativeResult::Ok(JsValue::bool(epoch_equal && zone_equal && calendar_equal)) } /// ZDT 字段 getter 宏:branding 后读本地分量,按选择函数取字段。 macro_rules! zoned_date_time_parts_getter { ($name:ident, $select:expr) => { pub fn $name(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 (year, month, day, time_ns) = native_try!(zoned_date_time_plain_parts(vm, obj)); NativeResult::Ok(JsValue::float($select(year, month, day, time_ns) as f64)) } }; } /// ZDT epoch 除法 getter 宏:槽 0 按除数向下取整(负值 floor,仿 instant_epoch_*)。 macro_rules! zoned_date_time_epoch_getter { ($name:ident, $divisor:expr) => { pub fn $name(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 Some(epoch_ns) = get_instant_epoch_ns(obj) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid ZonedDateTime")); }; NativeResult::Ok(JsValue::float(epoch_ns.div_euclid($divisor) as f64)) } }; } /// ZDT 日期派生 getter 宏:本地日期转 chrono NaiveDate 后按闭包取值。 macro_rules! zoned_date_time_naive_getter { ($name:ident, $body:expr) => { pub fn $name(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 (year, month, day, _) = native_try!(zoned_date_time_plain_parts(vm, obj)); let Some(date) = NaiveDate::from_ymd_opt(year, month, day) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid date")); }; NativeResult::Ok($body(&date)) } }; } /// ZDT 常量 getter 宏:仅做 branding,返回固定值。 macro_rules! zoned_date_time_brand_only_getter { ($name:ident, $value:expr) => { pub fn $name(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)); NativeResult::Ok($value) } }; } // 墙钟字段 getter:本地日期分量直读。 zoned_date_time_parts_getter!(zoned_date_time_year, |y, _, _, _| y as f64); zoned_date_time_parts_getter!(zoned_date_time_month, |_, m, _, _| m as f64); zoned_date_time_parts_getter!(zoned_date_time_day, |_, _, d, _| d as f64); zoned_date_time_parts_getter!(zoned_date_time_hour, |_, _, _, t| plain_time_components(t).0 as f64); zoned_date_time_parts_getter!(zoned_date_time_minute, |_, _, _, t| plain_time_components(t).1 as f64); zoned_date_time_parts_getter!(zoned_date_time_second, |_, _, _, t| plain_time_components(t).2 as f64); zoned_date_time_parts_getter!(zoned_date_time_millisecond, |_, _, _, t| plain_time_components(t).3 as f64); zoned_date_time_parts_getter!(zoned_date_time_microsecond, |_, _, _, t| plain_time_components(t).4 as f64); zoned_date_time_parts_getter!(zoned_date_time_nanosecond, |_, _, _, t| plain_time_components(t).5 as f64); // epoch 除法 getter:BigInt 除以秒/毫秒/微秒,负值向下取整。 zoned_date_time_epoch_getter!(zoned_date_time_epoch_seconds, 1_000_000_000); zoned_date_time_epoch_getter!(zoned_date_time_epoch_milliseconds, 1_000_000); zoned_date_time_epoch_getter!(zoned_date_time_epoch_microseconds, 1_000); // 日期派生 getter:本地日期经 chrono NaiveDate 取周/年/月属性。 zoned_date_time_naive_getter!(zoned_date_time_day_of_week, |d: &NaiveDate| { JsValue::float((d.weekday().num_days_from_monday() + 1) as f64) }); zoned_date_time_naive_getter!(zoned_date_time_day_of_year, |d: &NaiveDate| { JsValue::float(d.ordinal() as f64) }); zoned_date_time_naive_getter!(zoned_date_time_week_of_year, |d: &NaiveDate| { JsValue::float(d.iso_week().week() as f64) }); zoned_date_time_naive_getter!(zoned_date_time_year_of_week, |d: &NaiveDate| { JsValue::float(d.iso_week().year() as f64) }); zoned_date_time_naive_getter!(zoned_date_time_days_in_month, |d: &NaiveDate| { JsValue::float(days_in_month_iso(d.year(), d.month()) as f64) }); zoned_date_time_naive_getter!(zoned_date_time_days_in_year, |d: &NaiveDate| { JsValue::float(if is_leap_year_iso(d.year()) { 366.0 } else { 365.0 }) }); zoned_date_time_naive_getter!(zoned_date_time_in_leap_year, |d: &NaiveDate| { JsValue::bool(is_leap_year_iso(d.year())) }); // 常量 getter:ISO 日历下固定值,仅做 branding。 zoned_date_time_brand_only_getter!(zoned_date_time_days_in_week, JsValue::float(7.0)); zoned_date_time_brand_only_getter!(zoned_date_time_months_in_year, JsValue::float(12.0)); zoned_date_time_brand_only_getter!(zoned_date_time_era, JsValue::undefined()); zoned_date_time_brand_only_getter!(zoned_date_time_era_year, JsValue::undefined()); /// 读 ZDT 时区偏移(分钟);槽 1 解析失败返回 RangeError。调用方须先 ensure_zoned_date_time。 fn zoned_date_time_offset_minutes(vm: &mut H, obj: &JsObject) -> Result { let time_zone_id = to_string(obj.get_prop_at(1)); instant_time_zone_offset(&time_zone_id).ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone")) } /// `Temporal.ZonedDateTime.prototype.offset`:由偏移分钟数规范化为 ±HH:MM 字符串。 pub fn zoned_date_time_offset(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 offset_minutes = native_try!(zoned_date_time_offset_minutes(vm, obj)); let sign = if offset_minutes < 0 { '-' } else { '+' }; let magnitude = offset_minutes.abs(); NativeResult::Ok(vm.new_string(&format!("{sign}{:02}:{:02}", magnitude / 60, magnitude % 60))) } /// `Temporal.ZonedDateTime.prototype.offsetNanoseconds`:偏移分钟数换算纳秒(f64 精确域内)。 pub fn zoned_date_time_offset_nanoseconds(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 offset_minutes = native_try!(zoned_date_time_offset_minutes(vm, obj)); NativeResult::Ok(JsValue::float(offset_minutes as f64 * 60_000_000_000.0)) } /// `Temporal.ZonedDateTime.prototype.monthCode`:ISO 日历下恒为 `M{month:02}` 补零格式。 pub fn zoned_date_time_month_code(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 (_, month, _, _) = native_try!(zoned_date_time_plain_parts(vm, obj)); NativeResult::Ok(vm.new_string(&format!("M{month:02}"))) } /// `Temporal.ZonedDateTime.prototype.hoursInDay`:当日与次日当地午夜差 / 小时,含 Instant 范围校验。 pub fn zoned_date_time_hours_in_day(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 (year, month, day, _) = native_try!(zoned_date_time_plain_parts(vm, obj)); let offset_minutes = native_try!(zoned_date_time_offset_minutes(vm, obj)); let today = native_try!(start_of_day_epoch_ns(year, month, day, offset_minutes) .ok_or_else(|| crate::error::create_range_error(vm, "invalid date"))); let days = days_from_civil(i128::from(year), i128::from(month), i128::from(day)); let tomorrow = native_try!(start_of_day_epoch_ns_by_days(days + 1, offset_minutes) .ok_or_else(|| crate::error::create_range_error(vm, "invalid date"))); NativeResult::Ok(JsValue::float((tomorrow - today) as f64 / 3_600_000_000_000.0)) } fn make_plain_date(vm: &mut H, year: i32, month: u32, day: u32, calendar: &str) -> NativeResult { let proto = JsValue::from_js_object(vm.session().builtin_world().plain_date_proto.as_ptr() as *mut JsObject); 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))) } fn make_plain_time(vm: &mut H, total_ns: f64) -> NativeResult { let proto = JsValue::from_js_object(vm.session().builtin_world().plain_time_proto.as_ptr() as *mut JsObject); 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))) } fn make_plain_date_time( vm: &mut H, year: i32, month: u32, day: u32, total_ns: f64, calendar: &str, ) -> NativeResult { let proto = JsValue::from_js_object(vm.session().builtin_world().plain_date_time_proto.as_ptr() as *mut JsObject); 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))) } fn make_duration(vm: &mut H, values: [f64; 10]) -> NativeResult { let proto = JsValue::from_js_object(vm.session().builtin_world().duration_proto.as_ptr() as *mut JsObject); 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))) } fn parse_duration_string(input: &str) -> Option<[f64; 10]> { let bytes = input.as_bytes(); let mut cursor = 0usize; let negative = match bytes.first().copied() { Some(b'-') => { cursor += 1; true } Some(b'+') => { cursor += 1; false } _ => false, }; if bytes.get(cursor).copied().map(|byte| byte.to_ascii_uppercase()) != Some(b'P') { return None; } cursor += 1; let mut values = [0.0; 10]; let mut in_time = false; let mut saw_unit = false; let mut saw_time_unit = false; let mut last_order = 0usize; let mut fraction_seen = false; while cursor < bytes.len() { if bytes[cursor].eq_ignore_ascii_case(&b'T') { if in_time { return None; } in_time = true; cursor += 1; continue; } let number_start = cursor; while bytes.get(cursor).is_some_and(u8::is_ascii_digit) { cursor += 1; } if cursor == number_start { return None; } let whole = input[number_start..cursor].parse::().ok()?; if !whole.is_finite() { return None; } let mut fraction = None; if matches!(bytes.get(cursor), Some(b'.' | b',')) { cursor += 1; let fraction_start = cursor; while bytes.get(cursor).is_some_and(u8::is_ascii_digit) { cursor += 1; } let digits = cursor - fraction_start; if digits == 0 || digits > 9 { return None; } let numerator = input[fraction_start..cursor].parse::().ok()?; fraction = Some((numerator, 10_u64.pow(digits as u32))); } let unit = bytes.get(cursor)?.to_ascii_uppercase(); cursor += 1; let (index, order, unit_nanos) = match (in_time, unit) { (false, b'Y') => (0, 1, None), (false, b'M') => (1, 2, None), (false, b'W') => (2, 3, None), (false, b'D') => (3, 4, None), (true, b'H') => (4, 5, Some(3_600_000_000_000_u64)), (true, b'M') => (5, 6, Some(60_000_000_000_u64)), (true, b'S') => (6, 7, Some(1_000_000_000_u64)), _ => return None, }; if order <= last_order || fraction_seen { return None; } last_order = order; values[index] = whole; saw_unit = true; saw_time_unit |= in_time; if let Some((numerator, scale)) = fraction { let unit_nanos = unit_nanos?; let mut nanos = (numerator as u128 * unit_nanos as u128 / scale as u128) as u64; if index <= 4 { values[5] += (nanos / 60_000_000_000) as f64; nanos %= 60_000_000_000; } if index <= 5 { values[6] += (nanos / 1_000_000_000) as f64; nanos %= 1_000_000_000; } values[7] += (nanos / 1_000_000) as f64; nanos %= 1_000_000; values[8] += (nanos / 1_000) as f64; values[9] += (nanos % 1_000) as f64; fraction_seen = true; } } if !saw_unit || (in_time && !saw_time_unit) { return None; } if negative { for value in &mut values { if *value != 0.0 { *value = -*value; } } } Some(values) } const DURATION_FIELD_ORDER: [(&str, usize); 10] = [ ("days", 3), ("hours", 4), ("microseconds", 8), ("milliseconds", 7), ("minutes", 5), ("months", 1), ("nanoseconds", 9), ("seconds", 6), ("weeks", 2), ("years", 0), ]; fn duration_field_number( vm: &mut H, obj: &JsObject, receiver: JsValue, name: &str, ) -> Result, JsValue> { let key_val = vm.new_string(name); let si = vm.property_key_si(key_val); let raw = match vm.ordinary_get(obj, si, receiver) { Ok(value) => value, Err(error) => return Err(native_engine_error(vm, &error)), }; if raw.is_undefined() { return Ok(None); } let primitive = match oxide_runtime_api::to_primitive(raw, oxide_runtime_api::ToPrimitiveHint::Number, vm) { Ok(value) => value, Err(error) => return Err(native_engine_error(vm, &error)), }; if primitive.is_symbol() || primitive.is_bigint() { return Err(crate::error::create_type_error(vm, "cannot convert duration field to number")); } Ok(Some(to_number(primitive))) } fn duration_partial_values(vm: &mut H, val: JsValue) -> Result<[Option; 10], JsValue> { if !val.is_object() { return Err(crate::error::create_type_error(vm, "duration-like value must be an object")); } let ptr = val.as_js_object_ptr(); if ptr.is_null() { return Err(crate::error::create_type_error(vm, "duration-like value must be an object")); } let obj = unsafe { &*ptr }; if obj.is_duration_obj() { return Ok(std::array::from_fn(|index| Some(get_double_prop(obj, index)))); } let mut values = [None; 10]; for (name, index) in DURATION_FIELD_ORDER { values[index] = duration_field_number(vm, obj, val, name)?; } if values.iter().all(Option::is_none) { return Err(crate::error::create_type_error(vm, "duration-like object has no duration fields")); } Ok(values) } fn duration_component_integer(value: f64) -> Option { if !value.is_finite() || value.fract() != 0.0 || value.abs() >= i128::MAX as f64 { return None; } Some(value as i128) } fn duration_time_nanoseconds(values: &[f64; 10]) -> Option { const SCALES: [i128; 7] = [86_400_000_000_000, 3_600_000_000_000, 60_000_000_000, 1_000_000_000, 1_000_000, 1_000, 1]; let mut total = 0_i128; for (value, scale) in values[3..].iter().zip(SCALES) { total = total.checked_add(duration_component_integer(*value)?.checked_mul(scale)?)?; } Some(total) } fn validate_duration_values(vm: &mut H, values: &[f64; 10]) -> Result<(), JsValue> { let mut sign = 0_i8; for value in values { if duration_component_integer(*value).is_none() { return Err(crate::error::create_range_error(vm, "invalid duration")); } if *value != 0.0 { let current = if value.is_sign_negative() { -1 } else { 1 }; if sign != 0 && sign != current { return Err(crate::error::create_range_error(vm, "duration fields must have the same sign")); } sign = current; } } if values[..3].iter().any(|value| value.abs() > u32::MAX as f64) { return Err(crate::error::create_range_error(vm, "duration date field is out of range")); } const MAX_TIME_NANOSECONDS: i128 = (1_i128 << 53) * 1_000_000_000; let total = duration_time_nanoseconds(values) .ok_or_else(|| crate::error::create_range_error(vm, "duration time fields are out of range"))?; if total.abs() >= MAX_TIME_NANOSECONDS { return Err(crate::error::create_range_error(vm, "duration time fields are out of range")); } Ok(()) } fn duration_like_values(vm: &mut H, val: JsValue) -> Result<[f64; 10], JsValue> { if val.is_string() { let values = parse_duration_string(&to_string(val)) .ok_or_else(|| crate::error::create_range_error(vm, "invalid duration string"))?; validate_duration_values(vm, &values)?; return Ok(values); } let partial = duration_partial_values(vm, val)?; let values = std::array::from_fn(|index| partial[index].unwrap_or(0.0)); validate_duration_values(vm, &values)?; Ok(values) } /// `Temporal.Duration` 构造器,保存十个整数时长分量。 pub fn duration_constructor(vm: &mut H, args: &[u8]) -> NativeResult { let ctor_proto = vm.session().builtin_world().duration_proto.as_ptr(); if !is_ctor_call(vm, args, ctor_proto) { return NativeResult::Err(crate::error::create_type_error( vm, "Class constructor Temporal.Duration cannot be invoked without 'new'", )); } let mut values = [0.0; 10]; for (index, value) in values.iter_mut().enumerate() { if args.len() <= index + 1 { continue; } let number = to_number(vm.reg(args[index + 1])); if !number.is_finite() || number.fract() != 0.0 { return NativeResult::Err(crate::error::create_range_error(vm, "invalid duration")); } *value = number; } native_try!(validate_duration_values(vm, &values)); initialize_temporal_receiver(vm, args, JsObject::OBJ_TYPE_DURATION, values.map(JsValue::float)) } /// `Temporal.Duration.from(value)`,复制 Duration 或读取同名字段。 pub fn duration_from(vm: &mut H, args: &[u8]) -> NativeResult { let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; let values = native_try!(duration_like_values(vm, val)); make_duration(vm, values) } fn duration_values(obj: &JsObject) -> [f64; 10] { std::array::from_fn(|index| get_double_prop(obj, index)) } fn format_duration_number(value: f64) -> String { format!("{}", value as i64) } fn format_duration_seconds(seconds: f64, milliseconds: f64, microseconds: f64, nanoseconds: f64) -> Option { let total = duration_component_integer(seconds)? .checked_mul(1_000_000_000)? .checked_add(duration_component_integer(milliseconds)?.checked_mul(1_000_000)?)? .checked_add(duration_component_integer(microseconds)?.checked_mul(1_000)?)? .checked_add(duration_component_integer(nanoseconds)?)?; if total == 0 { return None; } let negative = total < 0; let magnitude = total.abs(); let whole = magnitude / 1_000_000_000; let fraction = magnitude % 1_000_000_000; let mut result = format!("{}", whole); if fraction != 0 { let mut digits = format!("{fraction:09}"); while digits.ends_with('0') { digits.pop(); } result.push('.'); result.push_str(&digits); } if negative { result.insert(0, '-'); } Some(result) } /// `Temporal.Duration.prototype.with(partial)`,以给定字段替换当前时长分量。 pub fn duration_with(vm: &mut H, args: &[u8]) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; native_try!(ensure_duration(vm, obj)); let partial_value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; let partial = native_try!(duration_partial_values(vm, partial_value)); let mut values = duration_values(obj); for (index, value) in partial.into_iter().enumerate() { if let Some(value) = value { values[index] = value; } } native_try!(validate_duration_values(vm, &values)); make_duration(vm, values) } /// `Temporal.Duration.prototype.total(totalOf)`:按单位汇总时长。 /// 日历单位(year/month/week)需要 relativeTo(PlainDateTime/PlainDate)。 pub fn duration_total(vm: &mut H, args: &[u8]) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; native_try!(ensure_duration(vm, obj)); let total_of = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; if total_of.is_undefined() { return NativeResult::Err(crate::error::create_type_error(vm, "options argument is required")); } let (unit_raw, relative_raw) = if total_of.is_string() { (to_string(total_of), JsValue::undefined()) } else { if !total_of.is_object() { return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object")); } let options_ptr = total_of.as_js_object_ptr(); if options_ptr.is_null() { return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object")); } let options = unsafe { &*options_ptr }; let relative_raw = match temporal_option_value(vm, options, total_of, "relativeTo") { Ok(raw) => raw, Err(error) => return NativeResult::Err(error), }; let unit_raw = match temporal_option_value(vm, options, total_of, "unit") { Ok(raw) => match temporal_option_string(vm, raw) { Ok(value) => value, Err(error) => return NativeResult::Err(error), }, Err(error) => return NativeResult::Err(error), }; (unit_raw, relative_raw) }; let unit_index = match plain_date_time_unit_index(&unit_raw) { Some(index) => index, None => return NativeResult::Err(crate::error::create_range_error(vm, "invalid unit")), }; let values = duration_values(obj); // 含日历单位(year/month/week)时,days 及以上的 total 必须走 relativeTo 日历路径。 let has_calendar_units = values[0] != 0.0 || values[1] != 0.0 || values[2] != 0.0; // relativeTo:支持 PlainDateTime / PlainDate,取日期分量(时间按午夜计算,对齐 polyfill)。 // relativeTo: supports PlainDateTime / PlainDate objects; strings follow the // ToRelativeTemporalObject path (PlainDateTime first, falling back to PlainDate), // invalid/out-of-range strings throw RangeError, and non-string primitives // (number/boolean/bigint/symbol/null) throw TypeError per test262. let relative_date = if relative_raw.is_string() { let text = to_string(relative_raw); match parse_plain_date_time_string(&text) .or_else(|_| parse_plain_date_string(&text).map(|(y, m, d)| (y, m, d, 0.0))) { Ok((year, month, day, _time_ns)) => Some((i128::from(year), i128::from(month), i128::from(day))), Err(_) => { return NativeResult::Err(crate::error::create_range_error(vm, "invalid relativeTo string")); } } } else if relative_raw.is_object() { let rel_ptr = relative_raw.as_js_object_ptr(); if rel_ptr.is_null() { return NativeResult::Err(crate::error::create_type_error(vm, "invalid relativeTo")); } let rel = unsafe { &*rel_ptr }; if rel.is_plain_date_time_obj() || rel.is_plain_date_obj() { Some(( i128::from(get_double_prop(rel, 0) as i32), i128::from(get_double_prop(rel, 1) as u32), i128::from(get_double_prop(rel, 2) as u32), )) } else { None } } else if relative_raw.is_undefined() { None } else { return NativeResult::Err(crate::error::create_type_error(vm, "invalid relativeTo")); }; const UNIT_NS: [i128; 6] = [3_600_000_000_000, 60_000_000_000, 1_000_000_000, 1_000_000, 1_000, 1]; if unit_index <= 2 || (unit_index == 3 && has_calendar_units) { // 日历单位:需要 relativeTo,用纪元纳秒窗口计算分数总量。 let Some(rel_date) = relative_date else { return NativeResult::Err(crate::error::create_range_error( vm, &format!("a starting point is required for {} total", unit_raw), )); }; let Some(time_ns_base) = duration_time_nanoseconds(&values) else { return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range")); }; // days 并入时间(24 小时/天),整日回填到日期分量。 // duration_time_nanoseconds 已包含 days 分量(24 小时/天),直接取整日。 let time_ns_total = time_ns_base; let delta_days = time_ns_total / DAY_NS; let target_time = time_ns_total % DAY_NS; let mut date_parts = [0.0; 10]; date_parts[0] = values[0]; date_parts[1] = values[1]; date_parts[2] = values[2]; date_parts[3] = delta_days as f64; let target_date = add_date_duration(rel_date, &date_parts); // DifferenceISODateTime:date1=relativeTo(午夜)、date2=target。 let date1 = rel_date; let mut date2 = target_date; let mut time_ns = target_time; let time_sign = if time_ns > 0 { 1_i128 } else if time_ns < 0 { -1_i128 } else { 0 }; let date_sign = compare_iso_date(date1, date2); if date_sign != 0 && date_sign == time_sign { date2 = add_days_iso(date2, time_sign); time_ns -= time_sign * DAY_NS; } let date_values = date_until_iso(date1, date2, unit_index); let sign = { let ds = date_duration_sign(&date_values); if ds != 0 { ds } else if time_ns > 0 { 1 } else if time_ns < 0 { -1 } else { 1 } }; let origin_epoch = days_from_civil(date1.0, date1.1, date1.2) * DAY_NS; let dest_epoch = days_from_civil(target_date.0, target_date.1, target_date.2) * DAY_NS + target_time; let (r1, _, start_dur, end_dur) = nudge_window(sign, &date_values, date1, 1, unit_index, false); let epoch_of = |dur: &[f64; 10]| -> Option { if date_duration_sign(dur) == 0 { return Some(origin_epoch); } let date = add_date_duration(date1, dur); let days = days_from_civil(date.0, date.1, date.2); if days.abs() > MAX_ISO_DAY { return None; } Some(days * DAY_NS) }; let Some(start_epoch) = epoch_of(&start_dur) else { return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range")); }; let Some(end_epoch) = epoch_of(&end_dur) else { return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range")); }; let numerator = dest_epoch - start_epoch; let denominator = end_epoch - start_epoch; if denominator == 0 { return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range")); } let total = (denominator as f64 * r1 as f64 + numerator as f64 * sign as f64) / denominator as f64; return NativeResult::Ok(JsValue::float(total)); } // 均匀长度单位(day..nanosecond):直接按纳秒汇总。 let Some(time_ns_base) = duration_time_nanoseconds(&values) else { return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range")); }; // duration_time_nanoseconds 已包含 days 分量。 let total_ns = time_ns_base; let scale = if unit_index == 3 { DAY_NS } else { UNIT_NS[unit_index - 4] }; NativeResult::Ok(JsValue::float(total_ns as f64 / scale as f64)) } /// `Temporal.Duration.prototype.toString()` 的 ISO 8601 表示。 pub fn duration_to_string(vm: &mut H, args: &[u8]) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; native_try!(ensure_duration(vm, obj)); let mut values = duration_values(obj); let negative = values.iter().any(|value| *value < 0.0); for value in &mut values { *value = value.abs(); } let mut output = String::from(if negative { "-P" } else { "P" }); let date_units = [(0, "Y"), (1, "M"), (2, "W"), (3, "D")]; let mut has_date = false; for (index, suffix) in date_units { if values[index] != 0.0 { has_date = true; output.push_str(&format_duration_number(values[index])); output.push_str(suffix); } } let time_units = [(4, "H"), (5, "M")]; let has_time = values[4..].iter().any(|value| *value != 0.0); if has_time || !has_date { output.push('T'); for (index, suffix) in time_units { if values[index] != 0.0 { output.push_str(&format_duration_number(values[index])); output.push_str(suffix); } } if let Some(seconds) = format_duration_seconds(values[6], values[7], values[8], values[9]) { output.push_str(&seconds); output.push('S'); } else if output.ends_with('T') { output.push_str("0S"); } } NativeResult::Ok(vm.new_string_owned(output)) } /// `Temporal.Duration.prototype.valueOf()` 始终拒绝隐式数值转换。 pub fn duration_value_of(vm: &mut H, _args: &[u8]) -> NativeResult { NativeResult::Err(crate::error::create_type_error(vm, "Temporal.Duration has no valueOf")) } /// `Temporal.Duration.prototype.abs()`,返回所有分量的绝对值副本。 pub fn duration_abs(vm: &mut H, args: &[u8]) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; native_try!(ensure_duration(vm, obj)); let mut values = duration_values(obj); for value in &mut values { *value = value.abs(); } make_duration(vm, values) } /// `Temporal.Duration.prototype.negated()`,返回非零分量取反的副本。 pub fn duration_negated(vm: &mut H, args: &[u8]) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; native_try!(ensure_duration(vm, obj)); let mut values = duration_values(obj); for value in &mut values { if *value != 0.0 { *value = -*value; } } make_duration(vm, values) } /// `Temporal.Duration.prototype.add(other)`:分量相加并按规范平衡时间单位; /// 任一侧含日历单位(years/months/weeks)时抛 RangeError(无 relativeTo 支持)。 pub fn duration_add(vm: &mut H, args: &[u8]) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; native_try!(ensure_duration(vm, obj)); let other_val = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; let other = native_try!(duration_like_values(vm, other_val)); let receiver = duration_values(obj); if receiver[..3].iter().any(|value| *value != 0.0) || other[..3].iter().any(|value| *value != 0.0) { return NativeResult::Err(crate::error::create_range_error(vm, "cannot add durations with calendar units")); } // 最大单位取 receiver 与参数中最大的非零时间单位(days=3 最大,ns=9 最小)。 let mut largest = 9_usize; for index in 3..10 { if receiver[index] != 0.0 || other[index] != 0.0 { largest = index; break; } } if largest == 9 && receiver[9] + other[9] == 0.0 { return make_duration(vm, [0.0; 10]); } // 分量按精确整数求和(f64 分量是精确整数;超过 2^53 的和需在 i128 上保持精确,规范按数学值计算)。 let mut total_ns = 0_i128; const SUM_SCALES: [i128; 7] = [86_400_000_000_000, 3_600_000_000_000, 60_000_000_000, 1_000_000_000, 1_000_000, 1_000, 1]; for (index, scale) in (3..10).zip(SUM_SCALES) { let Some(a) = duration_component_integer(receiver[index]) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid duration")); }; let Some(b) = duration_component_integer(other[index]) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid duration")); }; let Some(component) = a.checked_add(b).and_then(|value| value.checked_mul(scale)) else { return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range")); }; let Some(updated) = total_ns.checked_add(component) else { return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range")); }; total_ns = updated; } // 从总纳秒向下按截断除法分解,进位到 largest 为止(largest 单位可无界)。 let mut values = [0.0; 10]; let mut rem = total_ns; let mut unit = 9_usize; loop { if unit == largest { values[unit] = rem as f64; break; } let base = match unit { 7..=9 => 1_000, 6 | 5 => 60, _ => 24, }; values[unit] = (rem % base) as f64; rem /= base; unit -= 1; } // 范围校验:对 𝔽 舍入后的每个分量按其纳秒刻度检查是否达到 2^53 秒上限。 const MAX_TIME_NANOSECONDS: f64 = (1_i128 << 53) as f64 * 1_000_000_000.0; const UNIT_SCALES: [f64; 7] = [ 86_400_000_000_000.0, 3_600_000_000_000.0, 60_000_000_000.0, 1_000_000_000.0, 1_000_000.0, 1_000.0, 1.0, ]; for (index, scale) in (3..10).zip(UNIT_SCALES) { if values[index] != 0.0 && values[index].abs() * scale >= MAX_TIME_NANOSECONDS { return NativeResult::Err(crate::error::create_range_error(vm, "duration time fields are out of range")); } } make_duration(vm, values) } /// `Temporal.Duration.prototype.round(roundTo)`锛氭寜鏈€灏忓崟浣嶈垗鍏ュ苟鎸夋渶澶у崟浣嶅钩琛°€? /// 绗竴鐗堟敮鎸佹棤鏃ュ巻鍗曚綅锛坹ear/month/week 闈為浂鎴栫洰鏍囦负鏃ュ巻鍗曚綅鏃惰姹?relativeTo锛屾殏鎶?RangeError锛夛紱 /// 绾弒鏃堕棿璺緞鎸?24 灏忔椂/澶╁鐞嗭紝涓?polyfill 鐨?24h-day 璇箟涓€鑷淬€? pub fn duration_round(vm: &mut H, args: &[u8]) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; native_try!(ensure_duration(vm, obj)); let round_to = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; if round_to.is_undefined() { return NativeResult::Err(crate::error::create_type_error(vm, "options parameter is required")); } let values = duration_values(obj); // 鐜版湁鏈€澶у崟浣嶏細棣栦釜闈為浂鍒嗛噺锛涘叏闆舵椂瑙嗕负 nanosecond銆? let mut existing_largest = 9_usize; for (index, value) in values.iter().enumerate() { if *value != 0.0 { existing_largest = index; break; } } // roundTo 瀛楃涓?=> { smallestUnit: 瀛楃涓?}锛涘惁鍒欏繀椤讳负瀵硅薄銆? let (largest_raw, _relative_raw, increment_raw, mode_raw, smallest_raw) = if round_to.is_string() { ( JsValue::undefined(), JsValue::undefined(), JsValue::undefined(), JsValue::undefined(), round_to, ) } else { if !round_to.is_object() { return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be an object")); } let options_ptr = round_to.as_js_object_ptr(); if options_ptr.is_null() { return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be an object")); } let options = unsafe { &*options_ptr }; let largest_raw = native_try!(temporal_option_value(vm, options, round_to, "largestUnit")); let relative_raw = native_try!(temporal_option_value(vm, options, round_to, "relativeTo")); let increment_raw = native_try!(temporal_option_value(vm, options, round_to, "roundingIncrement")); let mode_raw = native_try!(temporal_option_value(vm, options, round_to, "roundingMode")); let smallest_raw = native_try!(temporal_option_value(vm, options, round_to, "smallestUnit")); (largest_raw, relative_raw, increment_raw, mode_raw, smallest_raw) }; // largestUnit锛氬厑璁?auto"锛涚己鐪?/undefined 瑙嗕负鏈彁渚涖€? let largest_provided = !largest_raw.is_undefined(); let largest_index = if largest_raw.is_undefined() { None } else { let unit = native_try!(temporal_option_string(vm, largest_raw)); if unit == "auto" { None } else { match plain_date_time_unit_index(&unit) { Some(index) => Some(index), None => { return NativeResult::Err(crate::error::create_range_error(vm, "invalid largestUnit")); } } } }; // roundingIncrement锛歍oIntegerOrInfinity 鑸嶅叆鍚庨渶鍦?[1, 10^9] 鍐呫€? let increment_value = if increment_raw.is_undefined() { 1.0 } else { native_try!(temporal_option_number(vm, increment_raw)) }; let increment = increment_value.trunc(); if !increment.is_finite() || !(1.0..=1_000_000_000.0).contains(&increment) { return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); } let increment = increment as i128; // roundingMode锛氱己鐪?halfExpand銆? let mode = if mode_raw.is_undefined() { InstantRoundingMode::HalfExpand } else { let mode_string = native_try!(temporal_option_string(vm, mode_raw)); match instant_rounding_mode(&mode_string) { Some(mode) => mode, None => { return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding mode")); } } }; // smallestUnit锛氱己鐪?nanosecond銆? let smallest_provided = !smallest_raw.is_undefined(); let smallest_index = if smallest_raw.is_undefined() { 9 } else { let unit = native_try!(temporal_option_string(vm, smallest_raw)); match plain_date_time_unit_index(&unit) { Some(index) => index, None => { return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallestUnit")); } } }; // 榛樿鏈€澶у崟浣嶏細鐜版湁鏈€澶у崟浣嶄笌鏈€灏忓崟浣嶄腑杈冨ぇ鐨勯偅涓€€? let default_largest = if existing_largest < smallest_index { existing_largest } else { smallest_index }; let largest = largest_index.unwrap_or(default_largest); // 鑷冲皯涓€涓崟浣嶉渶瑕佹樉寮忔彁渚涳紱largest 涓嶈兘灏忎簬 smallest銆? if !smallest_provided && !largest_provided { return NativeResult::Err(crate::error::create_range_error( vm, "at least one of smallestUnit or largestUnit is required", )); } if smallest_index < largest { return NativeResult::Err(crate::error::create_range_error( vm, "largestUnit cannot be smaller than smallestUnit", )); } // 鑸嶅叆澧為噺涓婇檺锛氬崟浣嶈秺灏忓彲闄ら櫎涓婁竴绾э紱鏃ュ巻鍗曚綅锛坹ear/month/week/day锛夋棤鏁撮櫎绾︽潫銆? const MAX_INCREMENT: [i128; 10] = [0, 0, 0, 0, 24, 60, 60, 1000, 1000, 1000]; let max_increment = MAX_INCREMENT[smallest_index]; if max_increment != 0 && (increment >= max_increment || max_increment % increment != 0) { return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); } // 鏃ュ巻鍗曚綅锛坹ear/month/week锛夛細鏈増瑕佹眰 relativeTo锛屾殏鎶?RangeError銆? if values[..3].iter().any(|value| *value != 0.0) { return NativeResult::Err(crate::error::create_range_error( vm, "a starting point is required for balancing calendar units", )); } if largest < 3 || smallest_index < 3 { return NativeResult::Err(crate::error::create_range_error( vm, "a starting point is required for calendar units", )); } if increment > 1 && smallest_index == 3 && largest != smallest_index { return NativeResult::Err(crate::error::create_range_error( vm, "rounding increments of calendar units require largestUnit to equal smallestUnit", )); } // 绾弒鏃堕棿璺緞锛氭寜 24 灏忔椂/澶╁皢 days..nanoseconds 姹囨€讳负绾崇锛屾寜 smallest 鑸嶅叆鍚庡钩琛″埌 largest銆? let Some(time_ns) = duration_time_nanoseconds(&values) else { return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range")); }; const UNIT_NS: [i128; 7] = [86_400_000_000_000, 3_600_000_000_000, 60_000_000_000, 1_000_000_000, 1_000_000, 1_000, 1]; let quantum = if smallest_index == 3 { increment * UNIT_NS[0] } else { increment * UNIT_NS[smallest_index - 3] }; let Some(rounded) = round_instant_difference(time_ns, quantum, mode) else { return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range")); }; let mut result = [0.0; 10]; if smallest_index == 3 { result[3] = (rounded / UNIT_NS[0]) as f64; } else { // 涓?duration_add 鐩稿悓鐨勫悓鍙锋ā鍒嗚В锛氫粠 ns 鍚?largest 杩涗綅銆? let mut rem = rounded; let mut unit = 9_usize; loop { if unit == largest { result[unit] = rem as f64; break; } let base = match unit { 7..=9 => 1_000, 6 | 5 => 60, _ => 24, }; result[unit] = (rem % base) as f64; rem /= base; unit -= 1; } } // 鑼冨洿鏍¢獙锛氬姣忎釜鏃堕棿鍒嗛噺鎸夊叾绾崇鍒诲害妫€鏌ユ槸鍚﹁揪鍒?2^53 绉掍笂闄愩€? const MAX_TIME_NANOSECONDS: f64 = (1_i128 << 53) as f64 * 1_000_000_000.0; const UNIT_SCALES: [f64; 7] = [ 86_400_000_000_000.0, 3_600_000_000_000.0, 60_000_000_000.0, 1_000_000_000.0, 1_000_000.0, 1_000.0, 1.0, ]; for (index, scale) in (3..10).zip(UNIT_SCALES) { if result[index] != 0.0 && result[index].abs() * scale >= MAX_TIME_NANOSECONDS { return NativeResult::Err(crate::error::create_range_error(vm, "duration time fields are out of range")); } } make_duration(vm, result) } macro_rules! duration_getter { ($name:ident, $index:expr) => { pub fn $name(vm: &mut H, args: &[u8]) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; native_try!(ensure_duration(vm, obj)); NativeResult::Ok(JsValue::float(get_double_prop(obj, $index))) } }; } duration_getter!(duration_years, 0); duration_getter!(duration_months, 1); duration_getter!(duration_weeks, 2); duration_getter!(duration_days, 3); duration_getter!(duration_hours, 4); duration_getter!(duration_minutes, 5); duration_getter!(duration_seconds, 6); duration_getter!(duration_milliseconds, 7); duration_getter!(duration_microseconds, 8); duration_getter!(duration_nanoseconds, 9); /// 校验 ISO 日期分量(month 1-12、day 按月份与闰年,不依赖 chrono 年份范围)。 fn valid_iso_date(year: i32, month: u32, day: u32) -> bool { match days_in_month(i128::from(year), i128::from(month)) { Some(days) => day != 0 && day as i128 <= days, None => false, } } /// 计算 ISO 日期时间的纪元纳秒;分量须已通过 `valid_iso_date`。 fn iso_date_time_epoch_ns(year: i32, month: u32, day: u32, time_ns: f64) -> Option { 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) } /// 校验日期时间落在 PlainDateTime 表示范围(约 ±(10^8 + 1) 天,边界互斥)。 fn valid_plain_date_time_range(year: i32, month: u32, day: u32, time_ns: f64) -> bool { const MAX_ISO_DAY: i128 = 100_000_000; let limit = (MAX_ISO_DAY + 1) * 86_400_000_000_000; match iso_date_time_epoch_ns(year, month, day, time_ns) { Some(epoch_ns) => epoch_ns > -limit && epoch_ns < limit, None => false, } } /// 读两位数字(basic 格式的月/日等紧凑分量)。 fn read_iso2(t: &str, i: &mut usize, bytes: &[u8]) -> Result { if *i + 2 > bytes.len() { return Err("truncated ISO component".into()); } let two = &t[*i..*i + 2]; *i += 2; two.parse().map_err(|_| "invalid ISO component".to_string()) } /// 从 Temporal ISO 字符串中提取日期分量 `(year, month, day)`。 /// /// # 支持 /// - extended `YYYY-MM-DD` 与 basic `YYYYMMDD`,可带 `+`/`-` 符号(扩展年)。 /// - 时间部分(`T`/`t` 起)与 offset(`Z`/`±HH:MM`)与 annotations 允许存在, /// 日期提取时忽略(日历选择由调用方处理)。 /// /// # 边界 /// - 空串、年份不足 4 位、月/日超界、零年(含 `-000000` 减零年)均拒绝。 /// - 日期后的残余内容须以 `T`/`t`、offset、`[` 开头,否则拒绝。 fn parse_iso_date(s: &str) -> Result<(i32, u32, u32), String> { let t = s.trim().replace('\u{2212}', "-"); if t.is_empty() { return Err("invalid ISO string".into()); } let bytes = t.as_bytes(); let mut i = 0usize; let signed_year = i < bytes.len() && (bytes[i] == b'-' || bytes[i] == b'+'); let negative = if signed_year { let neg = bytes[i] == b'-'; i += 1; neg } else { false }; let y_start = i; while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; } let digits = &t[y_start..i]; // basic 格式(无分隔符,年月日连续):8-10 位数字,前 4-6 位年,后 4 位月日。 // 否则纯年(4-6 位),月/日由 `-MM-DD` 接续。 let (year_digits, month_day) = if digits.len() >= 8 { if !(8..=10).contains(&digits.len()) { return Err("invalid ISO date".into()); } let y_len = digits.len() - 4; if !(4..=6).contains(&y_len) { return Err("invalid ISO year".into()); } (&digits[..y_len], Some(&digits[y_len..])) } else { (digits, None) }; let expected_year_digits = if signed_year { 6 } else { 4 }; if year_digits.len() != expected_year_digits { return Err("invalid ISO year".into()); } let mut year: i64 = year_digits.parse().map_err(|_| "invalid ISO year".to_string())?; if negative { year = -year; } let (month, day) = if let Some(md) = month_day { if md.len() != 4 { return Err("invalid ISO date".into()); } let m: u32 = md[..2].parse().map_err(|_| "invalid ISO month".to_string())?; let d: u32 = md[2..].parse().map_err(|_| "invalid ISO day".to_string())?; (m, d) } else if i < bytes.len() && bytes[i] == b'-' { i += 1; let m = read_iso2(&t, &mut i, bytes)?; if i >= bytes.len() || bytes[i] != b'-' { return Err("invalid ISO date".into()); } i += 1; let d = read_iso2(&t, &mut i, bytes)?; (m, d) } else { return Err("missing month/day".into()); }; let rest = &t[i..]; if !rest.is_empty() { let ok = rest.starts_with(['T', 't', '[']); if !ok { return Err("invalid trailing content".into()); } } if month == 0 || month > 12 || day == 0 { return Err("invalid ISO date".into()); } if negative && year == 0 { return Err("invalid ISO negative zero year".into()); } Ok((year as i32, month, day)) } /// `Temporal.PlainDate` 构造器:`new PlainDate(year, month, day)`。 pub fn plain_date_constructor(vm: &mut H, args: &[u8]) -> NativeResult { let ctor_proto = vm.session().builtin_world().plain_date_proto.as_ptr(); if !is_ctor_call(vm, args, ctor_proto) { return NativeResult::Err(crate::error::create_type_error( vm, "Class constructor Temporal.PlainDate cannot be invoked without 'new'", )); } let year = if args.len() > 1 { to_number(vm.reg(args[1])).trunc() as i32 } else { 0 }; let month = if args.len() > 2 { to_number(vm.reg(args[2])).trunc() as u32 } else { 1 }; let day = if args.len() > 3 { to_number(vm.reg(args[3])).trunc() as u32 } else { 1 }; if !valid_iso_date(year, month, day) { return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO date")); } // 表示范围(ISODateWithinLimits):-271821-04-19 … +275760-09-13。 let day_count = days_from_civil(i128::from(year), i128::from(month), i128::from(day)); if !(-100_000_001..=100_000_000).contains(&day_count) { return NativeResult::Err(crate::error::create_range_error(vm, "ISO date is out of range")); } let calendar = if args.len() > 4 && !vm.reg(args[4]).is_undefined() { native_try!(temporal_calendar_id_strict(vm, vm.reg(args[4]))).unwrap_or_else(|| "iso8601".to_string()) } else { "iso8601".to_string() }; let calendar_value = vm.new_string(&calendar); initialize_temporal_receiver( vm, args, JsObject::OBJ_TYPE_PLAIN_DATE, [ JsValue::float(year as f64), JsValue::float(month as f64), JsValue::float(day as f64), calendar_value, ], ) } /// `Temporal.PlainDate.from(value, options)`:接受 ISO 日期字符串或 `{year, month, day}` 对象。 pub fn plain_date_from(vm: &mut H, args: &[u8]) -> NativeResult { let val = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; let (year, month, day, calendar) = if val.is_string() { // 规范顺序:先 ParseTemporalDateString,再 ToTemporalOverflow(options)。 let ymd = match parse_plain_date_string(&to_string(val)) { Ok(ymd) => ymd, Err(_) => { return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO 8601 date")); } }; native_try!(temporal_overflow(vm, args)); (ymd.0, ymd.1, ymd.2, None) } else { let constrain = native_try!(temporal_overflow(vm, args)); match object_date_ymd(vm, val, constrain) { Ok(ymd) => ymd, Err(error) => return NativeResult::Err(error), } }; if !valid_iso_date(year, month, day) { return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO date")); } // 表示范围(ISODateWithinLimits):-271821-04-19 … +275760-09-13。 let day_count = days_from_civil(i128::from(year), i128::from(month), i128::from(day)); if !(-100_000_001..=100_000_000).contains(&day_count) { return NativeResult::Err(crate::error::create_range_error(vm, "ISO date is out of range")); } make_plain_date(vm, year, month, day, calendar.as_deref().unwrap_or("iso8601")) } fn plain_date_ymd(vm: &mut H, args: &[u8]) -> Result<(f64, f64, f64), JsValue> { let ptr = receiver_obj(vm, args)?; let obj = unsafe { &*ptr }; ensure_plain_date(vm, obj)?; Ok((get_double_prop(obj, 0), get_double_prop(obj, 1), get_double_prop(obj, 2))) } /// `Temporal.PlainDate.prototype.year` getter。 pub fn plain_date_year(vm: &mut H, args: &[u8]) -> NativeResult { let (y, _, _) = native_try!(plain_date_ymd(vm, args)); NativeResult::Ok(JsValue::float(y)) } /// `Temporal.PlainDate.prototype.month` getter。 pub fn plain_date_month(vm: &mut H, args: &[u8]) -> NativeResult { let (_, m, _) = native_try!(plain_date_ymd(vm, args)); NativeResult::Ok(JsValue::float(m)) } /// `Temporal.PlainDate.prototype.day` getter。 pub fn plain_date_day(vm: &mut H, args: &[u8]) -> NativeResult { let (_, _, d) = native_try!(plain_date_ymd(vm, args)); NativeResult::Ok(JsValue::float(d)) } /// `Temporal.PlainDate.prototype.toString()`:输出 `YYYY-MM-DD`。 pub fn plain_date_to_string(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; NativeResult::Ok(vm.new_string(&format!("{year:04}-{month:02}-{day:02}"))) } fn valid_plain_time(hour: u32, minute: u32, second: u32, ms: u32, us: u32, ns: u32) -> bool { hour <= 23 && minute <= 59 && second <= 59 && ms <= 999 && us <= 999 && ns <= 999 } /// `Temporal.PlainTime` 构造器:`new PlainTime(h, m, s, ms, us, ns)`,缺省为 0。 pub fn plain_time_constructor(vm: &mut H, args: &[u8]) -> NativeResult { let ctor_proto = vm.session().builtin_world().plain_time_proto.as_ptr(); if !is_ctor_call(vm, args, ctor_proto) { return NativeResult::Err(crate::error::create_type_error( vm, "Class constructor Temporal.PlainTime cannot be invoked without 'new'", )); } let get = |i: usize, default: f64| -> f64 { if args.len() > i { to_number(vm.reg(args[i])).trunc() } else { default } }; let hour = get(1, 0.0) as u32; let minute = get(2, 0.0) as u32; let second = get(3, 0.0) as u32; let ms = get(4, 0.0) as u32; let us = get(5, 0.0) as u32; let ns = get(6, 0.0) as u32; if !valid_plain_time(hour, minute, second, ms, us, ns) { return NativeResult::Err(crate::error::create_range_error(vm, "invalid time component")); } let total_ns = hour as f64 * 3.6e12 + minute as f64 * 6e10 + second as f64 * 1e9 + ms as f64 * 1e6 + us as f64 * 1e3 + ns as f64; initialize_temporal_receiver(vm, args, JsObject::OBJ_TYPE_PLAIN_TIME, [JsValue::float(total_ns)]) } /// 拆解午夜后纳秒为各分量。 /// 带符号分解午夜后纳秒为时/分/秒/毫秒/微秒/纳秒(各分量向零截断,保持同号)。 fn plain_time_components_signed(total_ns: i128) -> [i128; 6] { const SCALES: [i128; 5] = [3_600_000_000_000, 60_000_000_000, 1_000_000_000, 1_000_000, 1_000]; let mut values = [0_i128; 6]; let mut remainder = total_ns; for (index, scale) in SCALES.iter().enumerate() { values[index] = remainder / scale; remainder %= scale; } values[5] = remainder; values } fn plain_time_components(total_ns: f64) -> (u32, u32, u32, u32, u32, u32) { let total = total_ns as u64; let ns = total % 1_000; let us = total / 1_000 % 1_000; let ms = total / 1_000_000 % 1_000; let second = total / 1_000_000_000 % 60; let minute = total / 60_000_000_000 % 60; let hour = total / 3_600_000_000_000 % 24; (hour as u32, minute as u32, second as u32, ms as u32, us as u32, ns as u32) } fn plain_time_get(vm: &mut H, args: &[u8], select: fn(u32, u32, u32, u32, u32, u32) -> u32) -> NativeResult { let ptr = match receiver_obj(vm, args) { Ok(p) => p, Err(e) => return NativeResult::Err(e), }; let obj = unsafe { &*ptr }; if let Err(e) = ensure_plain_time(vm, obj) { return NativeResult::Err(e); } let (h, m, s, ms, us, ns) = plain_time_components(get_double_prop(obj, 0)); NativeResult::Ok(JsValue::float(select(h, m, s, ms, us, ns) as f64)) } /// `Temporal.PlainTime.prototype.hour` getter。 pub fn plain_time_hour(vm: &mut H, args: &[u8]) -> NativeResult { plain_time_get(vm, args, |h, _, _, _, _, _| h) } /// `Temporal.PlainTime.prototype.minute` getter。 pub fn plain_time_minute(vm: &mut H, args: &[u8]) -> NativeResult { plain_time_get(vm, args, |_, m, _, _, _, _| m) } /// `Temporal.PlainTime.prototype.second` getter。 pub fn plain_time_second(vm: &mut H, args: &[u8]) -> NativeResult { plain_time_get(vm, args, |_, _, s, _, _, _| s) } /// `Temporal.PlainTime.prototype.millisecond` getter。 pub fn plain_time_millisecond(vm: &mut H, args: &[u8]) -> NativeResult { plain_time_get(vm, args, |_, _, _, ms, _, _| ms) } /// `Temporal.PlainTime.prototype.microsecond` getter。 pub fn plain_time_microsecond(vm: &mut H, args: &[u8]) -> NativeResult { plain_time_get(vm, args, |_, _, _, _, us, _| us) } /// `Temporal.PlainTime.prototype.nanosecond` getter。 pub fn plain_time_nanosecond(vm: &mut H, args: &[u8]) -> NativeResult { plain_time_get(vm, args, |_, _, _, _, _, ns| ns) } /// `Temporal.PlainTime.prototype.toString()`:`HH:MM:SS`,亚秒部分按需输出。 pub fn plain_time_to_string(vm: &mut H, args: &[u8]) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; native_try!(ensure_plain_time(vm, obj)); let (h, m, s, ms, us, ns) = plain_time_components(get_double_prop(obj, 0)); let frac = ms as u64 * 1_000_000 + us as u64 * 1_000 + ns as u64; if frac == 0 { NativeResult::Ok(vm.new_string(&format!("{h:02}:{m:02}:{s:02}"))) } else { let mut digits = format!("{frac:09}"); while digits.ends_with('0') { digits.pop(); } NativeResult::Ok(vm.new_string(&format!("{h:02}:{m:02}:{s:02}.{digits}"))) } } // ───────────────────── PlainDateTime 基础方法 ───────────────────── /// `Temporal.PlainDateTime` 构造器:保存 ISO 日期与午夜后纳秒。 pub fn plain_date_time_constructor(vm: &mut H, args: &[u8]) -> NativeResult { let ctor_proto = vm.session().builtin_world().plain_date_time_proto.as_ptr(); if !is_ctor_call(vm, args, ctor_proto) { return NativeResult::Err(crate::error::create_type_error( vm, "Class constructor Temporal.PlainDateTime cannot be invoked without 'new'", )); } let mut component = |index: usize, default: f64| -> Result { if args.len() > index { let raw = vm.reg(args[index]); temporal_option_number(vm, raw).map(f64::trunc) } else { Ok(default) } }; let year_value = native_try!(component(1, f64::NAN)); let month_value = native_try!(component(2, f64::NAN)); let day_value = native_try!(component(3, f64::NAN)); let hour_value = native_try!(component(4, 0.0)); let minute_value = native_try!(component(5, 0.0)); let second_value = native_try!(component(6, 0.0)); let millisecond_value = native_try!(component(7, 0.0)); let microsecond_value = native_try!(component(8, 0.0)); let nanosecond_value = native_try!(component(9, 0.0)); let components = [ year_value, month_value, day_value, hour_value, minute_value, second_value, millisecond_value, microsecond_value, nanosecond_value, ]; if components.iter().any(|value| !value.is_finite()) { return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time component")); } let year = year_value as i32; let month = month_value as u32; let day = day_value as u32; let hour = hour_value as u32; let minute = minute_value as u32; let second = second_value as u32; let millisecond = millisecond_value as u32; let microsecond = microsecond_value as u32; let nanosecond = nanosecond_value as u32; let non_negative = components[1..].iter().all(|value| *value >= 0.0); if !non_negative || !valid_iso_date(year, month, day) || !valid_plain_time(hour, minute, second, millisecond, microsecond, nanosecond) { return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time component")); } let calendar = if args.len() > 10 && !vm.reg(args[10]).is_undefined() { native_try!(temporal_calendar_id_strict(vm, vm.reg(args[10]))).unwrap_or_else(|| "iso8601".to_string()) } else { "iso8601".to_string() }; 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 !valid_plain_date_time_range(year, month, day, total_ns) { return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time component")); } let calendar_value = vm.new_string(&calendar); initialize_temporal_receiver( vm, args, JsObject::OBJ_TYPE_PLAIN_DATE_TIME, [ JsValue::float(year as f64), JsValue::float(month as f64), JsValue::float(day as f64), JsValue::float(total_ns), calendar_value, ], ) } 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,秒可带小数),返回剩余部分。 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..]) } fn parse_plain_date_time_string(input: &str) -> Result<(i32, u32, u32, f64), String> { parse_temporal_string_impl(input, true) } /// ParseTemporalDateString:PlainDate 字符串(时间部分可选且被忽略,仅按日期做范围校验)。 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)) } 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::>() } 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::, _>>() .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 { 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::().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)。 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。 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/空串)。 /// 供构造器日历参数与注解值判定使用。 fn parse_temporal_calendar_id_strict(input: &str) -> Result { 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"。 fn parse_temporal_calendar_string(input: &str) -> Result { 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()); } // 部分日期: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;校验月份/日期基本范围并拒绝负零年。 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 = None; let mut day: Option = 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。 fn temporal_calendar_id(vm: &mut H, value: JsValue) -> Result, 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))); } } } Err(crate::error::create_type_error(vm, "invalid calendar")) } /// ToTemporalCalendar 严格版(构造器日历参数用):字符串只走 18 项白名单,拒绝 ISO 串; /// undefined / 其他分支与宽松版一致。 fn temporal_calendar_id_strict(vm: &mut H, value: JsValue) -> Result, 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) } fn plain_date_time_object_parts( vm: &mut H, value: JsValue, obj: &JsObject, constrain: bool, ignore_time: bool, ) -> Result<(i32, u32, u32, f64, Option), 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); // 语法(well-formed):M 后两位数字,可选 L 后缀。 let digits_ok = code.len() >= 3 && code.starts_with('M') && code.as_bytes()[1..3].iter().all(u8::is_ascii_digit); let well_formed = digits_ok && (code.len() == 3 || (code.len() == 4 && code.ends_with('L'))); if !well_formed { return Err(crate::error::create_range_error(vm, "invalid monthCode")); } Some((code.clone(), code.ends_with('L'))) }; // 数值字段类型转换:Symbol/BigInt 抛 TypeError。 let mut convert_number = |_name: &str, raw: JsValue| -> Result, 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)?, ); // monthCode 适配 ISO 日历:仅 M01-M12,L 后缀不支持;无效抛 RangeError。 let month_code = match month_code { Some((code, leap)) => { if leap { return Err(crate::error::create_range_error(vm, "monthCode is not valid for ISO calendar")); } let number = code[1..3] .parse::() .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; let (year, month, day) = (year as i32, month as u32, day as u32); let (hour, minute, second) = (hour as u32, minute as u32, second as u32); let (millisecond, microsecond, nanosecond) = (millisecond as u32, microsecond as u32, nanosecond as u32); 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)) } /// 将 ZonedDateTime 按时区偏移转换为本地 PlainDateTime 分量。 fn zoned_date_time_plain_parts(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_minutes) = instant_time_zone_offset(&time_zone_id) 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_minutes) * 60_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)) } /// 当地午夜纪元纳秒(含 Instant 范围校验,spec GetStartOfDay 语义);越界返回 None。 fn start_of_day_epoch_ns(year: i32, month: u32, day: u32, offset_minutes: i32) -> Option { start_of_day_epoch_ns_by_days(days_from_civil(i128::from(year), i128::from(month), i128::from(day)), offset_minutes) } /// 按日数直接算当地午夜(hoursInDay 的明日边界复用:days+1 不经 civil 回填)。 fn start_of_day_epoch_ns_by_days(days: i128, offset_minutes: i32) -> Option { let start = days .checked_mul(86_400_000_000_000)? .checked_sub(i128::from(offset_minutes) * 60_000_000_000)?; (start.unsigned_abs() <= MAX_INSTANT_NS as u128).then_some(start) } /// 本地墙钟分量 + 时区偏移(分钟)→ 纪元纳秒(zoned_date_time_plain_parts 的逆)。 /// /// # 边界与前提 /// - (year, month, day, time_ns) 须已通过 valid_iso_date / valid_plain_time 校验(调用方保证)。 /// - 仅做 checked 溢出防护,Instant 范围校验由调用方按需执行。 #[allow(dead_code)] fn local_to_epoch_ns(year: i32, month: u32, day: u32, time_ns: f64, offset_minutes: i32) -> Option { 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_minutes) * 60_000_000_000) } fn plain_date_time_like_parts( vm: &mut H, value: JsValue, constrain: bool, ) -> Result<(i32, u32, u32, f64, Option), JsValue> { if value.is_string() { return parse_plain_date_time_string(&to_string(value)) .map(|(year, month, day, total_ns)| (year, month, day, total_ns, None)) .map_err(|_| crate::error::create_range_error(vm, "invalid ISO 8601 date-time")); } if !value.is_object() { return Err(crate::error::create_type_error(vm, "cannot convert to PlainDateTime")); } let ptr = value.as_js_object_ptr(); if ptr.is_null() { return Err(crate::error::create_type_error(vm, "cannot convert to PlainDateTime")); } let obj = unsafe { &*ptr }; if obj.is_plain_date_time_obj() { return Ok(( get_double_prop(obj, 0) as i32, get_double_prop(obj, 1) as u32, get_double_prop(obj, 2) as u32, get_double_prop(obj, 3), Some(get_calendar_id(obj, 4)), )); } if obj.is_plain_date_obj() { return Ok(( get_double_prop(obj, 0) as i32, get_double_prop(obj, 1) as u32, get_double_prop(obj, 2) as u32, 0.0, Some(get_calendar_id(obj, 3)), )); } if obj.is_zoned_date_time_obj() { return zoned_date_time_plain_parts(vm, obj) .map(|(year, month, day, total_ns)| (year, month, day, total_ns, Some(get_calendar_id(obj, 2)))); } plain_date_time_object_parts(vm, value, obj, constrain, false) } fn temporal_overflow(vm: &mut H, args: &[u8]) -> Result { 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.PlainDateTime.from(item)`:从实例、ISO 字符串或字段对象创建副本。 pub fn plain_date_time_from(vm: &mut H, args: &[u8]) -> NativeResult { let value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; if value.is_string() { // 规范顺序:先 ParseTemporalDateTimeString,再 ToTemporalOverflow(options)。 let (year, month, day, total_ns) = match parse_plain_date_time_string(&to_string(value)) { Ok(parts) => parts, Err(_) => { return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO 8601 date-time")); } }; native_try!(temporal_overflow(vm, args)); make_plain_date_time(vm, year, month, day, total_ns, "iso8601") } else { let constrain = native_try!(temporal_overflow(vm, args)); let (year, month, day, total_ns, calendar) = native_try!(plain_date_time_like_parts(vm, value, constrain)); make_plain_date_time(vm, year, month, day, total_ns, calendar.as_deref().unwrap_or("iso8601")) } } /// `Temporal.PlainDateTime.compare(one, two)`:按 ISO 日期时间字段做字典序比较。 pub fn plain_date_time_compare(vm: &mut H, args: &[u8]) -> NativeResult { let one = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; let two = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; let one = native_try!(plain_date_time_like_parts(vm, one, true)); let two = native_try!(plain_date_time_like_parts(vm, two, true)); let ordering = (one.0, one.1, one.2, one.3 as u64).cmp(&(two.0, two.1, two.2, two.3 as u64)); NativeResult::Ok(JsValue::int(match ordering { std::cmp::Ordering::Less => -1, std::cmp::Ordering::Equal => 0, std::cmp::Ordering::Greater => 1, })) } /// `Temporal.PlainDateTime.prototype.equals(other)`:比较日期时间分量是否相等。 pub fn plain_date_time_equals(vm: &mut H, args: &[u8]) -> NativeResult { let (y, m, d, ns) = match plain_date_time_parts(vm, args) { Ok(parts) => parts, Err(e) => return NativeResult::Err(e), }; let other_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; let other = match plain_date_time_like_parts(vm, other_val, true) { Ok(parts) => parts, Err(e) => return NativeResult::Err(e), }; let equal = other.0 == y && other.1 == m && other.2 == d && other.3 as u64 == ns as u64; NativeResult::Ok(JsValue::bool(equal)) } fn plain_date_time_parts(vm: &mut H, args: &[u8]) -> Result<(i32, u32, u32, f64), JsValue> { let ptr = receiver_obj(vm, args)?; let obj = unsafe { &*ptr }; ensure_plain_date_time(vm, obj)?; Ok(( get_double_prop(obj, 0) as i32, get_double_prop(obj, 1) as u32, get_double_prop(obj, 2) as u32, get_double_prop(obj, 3), )) } macro_rules! plain_date_time_date_getter { ($name:ident, $index:tt) => { pub fn $name(vm: &mut H, args: &[u8]) -> NativeResult { let parts = native_try!(plain_date_time_parts(vm, args)); NativeResult::Ok(JsValue::float(parts.$index as f64)) } }; } plain_date_time_date_getter!(plain_date_time_year, 0); plain_date_time_date_getter!(plain_date_time_month, 1); plain_date_time_date_getter!(plain_date_time_day, 2); fn plain_date_time_naive(vm: &mut H, args: &[u8]) -> Result { let (year, month, day, _) = plain_date_time_parts(vm, args)?; NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| crate::error::create_range_error(vm, "invalid date")) } /// `Temporal.PlainDateTime.prototype.dayOfWeek` getter。 pub fn plain_date_time_day_of_week(vm: &mut H, args: &[u8]) -> NativeResult { let date = native_try!(plain_date_time_naive(vm, args)); NativeResult::Ok(JsValue::float((date.weekday().num_days_from_monday() + 1) as f64)) } /// `Temporal.PlainDateTime.prototype.dayOfYear` getter。 pub fn plain_date_time_day_of_year(vm: &mut H, args: &[u8]) -> NativeResult { let date = native_try!(plain_date_time_naive(vm, args)); NativeResult::Ok(JsValue::float(date.ordinal() as f64)) } /// `Temporal.PlainDateTime.prototype.daysInMonth` getter。 pub fn plain_date_time_days_in_month(vm: &mut H, args: &[u8]) -> NativeResult { let date = native_try!(plain_date_time_naive(vm, args)); NativeResult::Ok(JsValue::float(days_in_month_iso(date.year(), date.month()) as f64)) } /// `Temporal.PlainDateTime.prototype.daysInWeek` getter。 pub fn plain_date_time_days_in_week(vm: &mut H, args: &[u8]) -> NativeResult { let _ = native_try!(plain_date_time_parts(vm, args)); NativeResult::Ok(JsValue::float(7.0)) } /// `Temporal.PlainDateTime.prototype.daysInYear` getter。 pub fn plain_date_time_days_in_year(vm: &mut H, args: &[u8]) -> NativeResult { let date = native_try!(plain_date_time_naive(vm, args)); NativeResult::Ok(JsValue::float(if is_leap_year_iso(date.year()) { 366.0 } else { 365.0 })) } /// `Temporal.PlainDateTime.prototype.monthsInYear` getter。 pub fn plain_date_time_months_in_year(vm: &mut H, args: &[u8]) -> NativeResult { let _ = native_try!(plain_date_time_parts(vm, args)); NativeResult::Ok(JsValue::float(12.0)) } /// `Temporal.PlainDateTime.prototype.inLeapYear` getter。 pub fn plain_date_time_in_leap_year(vm: &mut H, args: &[u8]) -> NativeResult { let date = native_try!(plain_date_time_naive(vm, args)); NativeResult::Ok(JsValue::bool(is_leap_year_iso(date.year()))) } /// `Temporal.PlainDateTime.prototype.weekOfYear` getter。 pub fn plain_date_time_week_of_year(vm: &mut H, args: &[u8]) -> NativeResult { let date = native_try!(plain_date_time_naive(vm, args)); NativeResult::Ok(JsValue::float(date.iso_week().week() as f64)) } /// `Temporal.PlainDateTime.prototype.yearOfWeek` getter。 pub fn plain_date_time_year_of_week(vm: &mut H, args: &[u8]) -> NativeResult { let date = native_try!(plain_date_time_naive(vm, args)); NativeResult::Ok(JsValue::float(date.iso_week().year() as f64)) } /// `Temporal.PlainDateTime.prototype.monthCode` getter。 pub fn plain_date_time_month_code(vm: &mut H, args: &[u8]) -> NativeResult { let (_, month, _, _) = native_try!(plain_date_time_parts(vm, args)); NativeResult::Ok(vm.new_string(&format!("M{month:02}"))) } /// ISO 日历没有可观察的 era 字段。 pub fn plain_date_time_era(vm: &mut H, args: &[u8]) -> NativeResult { let _ = native_try!(plain_date_time_parts(vm, args)); NativeResult::Ok(JsValue::undefined()) } /// ISO 日历没有可观察的 eraYear 字段。 pub fn plain_date_time_era_year(vm: &mut H, args: &[u8]) -> NativeResult { let _ = native_try!(plain_date_time_parts(vm, args)); NativeResult::Ok(JsValue::undefined()) } fn plain_date_time_time_get( vm: &mut H, args: &[u8], select: fn(u32, u32, u32, u32, u32, u32) -> u32, ) -> NativeResult { let (_, _, _, total_ns) = native_try!(plain_date_time_parts(vm, args)); let (hour, minute, second, millisecond, microsecond, nanosecond) = plain_time_components(total_ns); NativeResult::Ok(JsValue::float(select(hour, minute, second, millisecond, microsecond, nanosecond) as f64)) } /// `Temporal.PlainDateTime.prototype.hour` getter。 pub fn plain_date_time_hour(vm: &mut H, args: &[u8]) -> NativeResult { plain_date_time_time_get(vm, args, |hour, _, _, _, _, _| hour) } /// `Temporal.PlainDateTime.prototype.minute` getter。 pub fn plain_date_time_minute(vm: &mut H, args: &[u8]) -> NativeResult { plain_date_time_time_get(vm, args, |_, minute, _, _, _, _| minute) } /// `Temporal.PlainDateTime.prototype.second` getter。 pub fn plain_date_time_second(vm: &mut H, args: &[u8]) -> NativeResult { plain_date_time_time_get(vm, args, |_, _, second, _, _, _| second) } /// `Temporal.PlainDateTime.prototype.millisecond` getter。 pub fn plain_date_time_millisecond(vm: &mut H, args: &[u8]) -> NativeResult { plain_date_time_time_get(vm, args, |_, _, _, millisecond, _, _| millisecond) } /// `Temporal.PlainDateTime.prototype.microsecond` getter。 pub fn plain_date_time_microsecond(vm: &mut H, args: &[u8]) -> NativeResult { plain_date_time_time_get(vm, args, |_, _, _, _, microsecond, _| microsecond) } /// `Temporal.PlainDateTime.prototype.nanosecond` getter。 pub fn plain_date_time_nanosecond(vm: &mut H, args: &[u8]) -> NativeResult { plain_date_time_time_get(vm, args, |_, _, _, _, _, nanosecond| nanosecond) } /// `Temporal.PlainDateTime.prototype.calendarId`:读日历槽,兜底 ISO 8601。 pub fn plain_date_time_calendar_id(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)); NativeResult::Ok(vm.new_string(&get_calendar_id(obj, 4))) } /// 返回仅保留日期分量的新 `Temporal.PlainDate`,日历从 receiver 槽继承。 pub fn plain_date_time_to_plain_date(vm: &mut H, args: &[u8]) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; let (year, month, day, _) = native_try!(plain_date_time_parts(vm, args)); make_plain_date(vm, year, month, day, &get_calendar_id(obj, 4)) } /// 返回仅保留时间分量的新 `Temporal.PlainTime`。 pub fn plain_date_time_to_plain_time(vm: &mut H, args: &[u8]) -> NativeResult { let (_, _, _, total_ns) = native_try!(plain_date_time_parts(vm, args)); make_plain_time(vm, total_ns) } /// Temporal 对象禁止隐式转换为原始值。 pub fn plain_date_time_value_of(vm: &mut H, _args: &[u8]) -> NativeResult { NativeResult::Err(crate::error::create_type_error(vm, "Temporal.PlainDateTime has no valueOf")) } /// 格式化 PlainDateTime 本地 ISO 日期时间(无偏移),可裁秒与小数位并追加日历注解。 fn format_plain_date_time_iso( year: i32, month: u32, day: u32, time_ns: i128, include_seconds: bool, fractional_digits: Option, calendar_name: &str, ) -> String { let hour = time_ns / 3_600_000_000_000; let minute = time_ns / 60_000_000_000 % 60; let second = time_ns / 1_000_000_000 % 60; let subsecond = time_ns % 1_000_000_000; let mut output = format!("{}-{month:02}-{day:02}T{hour:02}:{minute:02}", format_iso_year(i128::from(year))); if include_seconds { output.push_str(&format!(":{second:02}")); match fractional_digits { Some(0) => {} Some(digits) => { let fraction = format!("{subsecond:09}"); output.push('.'); output.push_str(&fraction[..digits]); } None if subsecond != 0 => { output.push('.'); output.push_str(format!("{subsecond:09}").trim_end_matches('0')); } None => {} } } match calendar_name { "always" => output.push_str("[u-ca=iso8601]"), "critical" => output.push_str("[!u-ca=iso8601]"), _ => {} } output } fn plain_date_time_iso_string(vm: &mut H, args: &[u8]) -> NativeResult { let (year, month, day, total_ns) = native_try!(plain_date_time_parts(vm, args)); let output = format_plain_date_time_iso(year, month, day, total_ns as i128, true, None, "never"); NativeResult::Ok(vm.new_string_owned(output)) } /// `Temporal.PlainDateTime.prototype.toString(options)`:按精度、舍入模式与日历显示输出 ISO 8601。 pub fn plain_date_time_to_string(vm: &mut H, args: &[u8]) -> NativeResult { let (year, month, day, total_ns) = native_try!(plain_date_time_parts(vm, args)); let options_value = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; let (calendar_name, fractional_input, mode_value, smallest_value) = if options_value.is_undefined() { ("auto".to_string(), FractionalSecondDigitsInput::Auto, "trunc".to_string(), None) } else { if !options_value.is_object() { return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object")); } let options_ptr = options_value.as_js_object_ptr(); if options_ptr.is_null() { return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object")); } let options = unsafe { &*options_ptr }; let calendar_raw = native_try!(temporal_option_value(vm, options, options_value, "calendarName")); let calendar_name = if calendar_raw.is_undefined() { "auto".to_string() } else { native_try!(temporal_option_string(vm, calendar_raw)) }; let fractional_raw = native_try!(temporal_option_value(vm, options, options_value, "fractionalSecondDigits")); let fractional_input = if fractional_raw.is_undefined() { FractionalSecondDigitsInput::Auto } else if fractional_raw.is_int() || fractional_raw.is_double() { FractionalSecondDigitsInput::Number(to_number(fractional_raw)) } else { FractionalSecondDigitsInput::String(native_try!(temporal_option_string(vm, fractional_raw))) }; let mode_raw = native_try!(temporal_option_value(vm, options, options_value, "roundingMode")); let mode_value = if mode_raw.is_undefined() { "trunc".to_string() } else { native_try!(temporal_option_string(vm, mode_raw)) }; let smallest_raw = native_try!(temporal_option_value(vm, options, options_value, "smallestUnit")); let smallest_value = if smallest_raw.is_undefined() { None } else { Some(native_try!(temporal_option_string(vm, smallest_raw))) }; (calendar_name, fractional_input, mode_value, smallest_value) }; match calendar_name.as_str() { "auto" | "always" | "never" | "critical" => {} _ => return NativeResult::Err(crate::error::create_range_error(vm, "invalid calendarName")), } let fractional_digits = native_try!(parse_fractional_second_digits(vm, fractional_input)); let Some(mode) = instant_rounding_mode(&mode_value) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding mode")); }; let (quantum_ns, include_seconds, output_digits) = match smallest_value.as_deref() { Some("minute" | "minutes") => (60_000_000_000, false, Some(0)), Some("second" | "seconds") => (1_000_000_000, true, Some(0)), Some("millisecond" | "milliseconds") => (1_000_000, true, Some(3)), Some("microsecond" | "microseconds") => (1_000, true, Some(6)), Some("nanosecond" | "nanoseconds") => (1, true, Some(9)), Some(_) => return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallest unit")), None => match fractional_digits { Some(digits) => (10_i128.pow((9 - digits) as u32), true, Some(digits)), None => (1, true, None), }, }; const DAY_NS: i128 = 86_400_000_000_000; let Some(rounded_ns) = round_instant_ns(total_ns as i128, quantum_ns, mode) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time")); }; let extra_days = rounded_ns / DAY_NS; let time_ns = rounded_ns % DAY_NS; let total_days = days_from_civil(i128::from(year), i128::from(month), i128::from(day)) + extra_days; let (year, month, day) = civil_from_days(total_days); if !valid_plain_date_time_range(year as i32, month as u32, day as u32, time_ns as f64) { return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time")); } let output = format_plain_date_time_iso( year as i32, month as u32, day as u32, time_ns, include_seconds, output_digits, &calendar_name, ); NativeResult::Ok(vm.new_string_owned(output)) } /// 按 duration-like 分量对 PlainDateTime 做加减:先平衡时间(溢出为天), /// 再按年/月/周/日推进日期,最后校验范围。 fn plain_date_time_apply_duration(vm: &mut H, args: &[u8], sign: i64) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; let (year, month, day, time_ns) = match plain_date_time_parts(vm, args) { Ok(parts) => parts, Err(error) => return NativeResult::Err(error), }; let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; let values = match duration_like_values(vm, val) { Ok(values) => values, Err(error) => return NativeResult::Err(error), }; let constrain = match temporal_overflow(vm, args) { Ok(constrain) => constrain, Err(error) => return NativeResult::Err(error), }; const DAY_NS: i128 = 86_400_000_000_000; // 时间字段(hours 起)单独换算纳秒;days 由日期部分处理,避免重复计入。 let [_, _, _, _, h, min, s, ms, us, ns] = values; let time_delta = duration_component_integer(h).unwrap_or(0) * 3_600_000_000_000 + duration_component_integer(min).unwrap_or(0) * 60_000_000_000 + duration_component_integer(s).unwrap_or(0) * 1_000_000_000 + duration_component_integer(ms).unwrap_or(0) * 1_000_000 + duration_component_integer(us).unwrap_or(0) * 1_000 + duration_component_integer(ns).unwrap_or(0); let total_ns = time_ns as i128 + time_delta * sign as i128; let extra_days = total_ns.div_euclid(DAY_NS); let new_time_ns = total_ns.rem_euclid(DAY_NS); let [y, m, w, d, ..] = values; let months = (duration_component_integer(y).unwrap_or(0) * 12 + duration_component_integer(m).unwrap_or(0)) * sign as i128; let total_month = i128::from(year) * 12 + i128::from(month) - 1 + months; let ny = total_month.div_euclid(12); let nm = total_month.rem_euclid(12) + 1; let Some(max_day) = days_in_month(ny, nm) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid date")); }; let day = i128::from(day); let new_day = if day > max_day { if !constrain { return NativeResult::Err(crate::error::create_range_error(vm, "day out of range")); } max_day } else { day }; let day_delta = duration_component_integer(w).unwrap_or(0) * 7 + duration_component_integer(d).unwrap_or(0); let total_days = days_from_civil(ny, nm, new_day) + extra_days + day_delta * sign as i128; let (yy, mm, dd) = civil_from_days(total_days); if !valid_plain_date_time_range(yy as i32, mm as u32, dd as u32, new_time_ns as f64) { return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time")); } make_plain_date_time(vm, yy as i32, mm as u32, dd as u32, new_time_ns as f64, &get_calendar_id(obj, 4)) } /// PlainDateTime 差值单位层级:year=0 … nanosecond=9;"auto" 仅限 largestUnit。 fn plain_date_time_unit_index(value: &str) -> Option { match value { "year" | "years" => Some(0), "month" | "months" => Some(1), "week" | "weeks" => Some(2), "day" | "days" => Some(3), "hour" | "hours" => Some(4), "minute" | "minutes" => Some(5), "second" | "seconds" => Some(6), "millisecond" | "milliseconds" => Some(7), "microsecond" | "microseconds" => Some(8), "nanosecond" | "nanoseconds" => Some(9), _ => None, } } const DAY_NS: i128 = 86_400_000_000_000; const MAX_ISO_DAY: i128 = 100_000_000; /// 比较两个 ISO 日期,返回 -1/0/+1。 fn compare_iso_date(a: (i128, i128, i128), b: (i128, i128, i128)) -> i128 { if a.0 != b.0 { return if a.0 < b.0 { -1 } else { 1 }; } if a.1 != b.1 { return if a.1 < b.1 { -1 } else { 1 }; } if a.2 != b.2 { return if a.2 < b.2 { -1 } else { 1 }; } 0 } /// 候选日期(日以 day_override 参与比较)是否在 sign 方向上越过终点。 fn surpasses_with_day(sign: i128, candidate: (i128, i128, i128), day_override: i128, end: (i128, i128, i128)) -> bool { let cmp = compare_iso_date((candidate.0, candidate.1, day_override), end); if sign > 0 { cmp > 0 } else { cmp < 0 } } /// 日期加天数。 fn add_days_iso(date: (i128, i128, i128), days: i128) -> (i128, i128, i128) { let (y, m, d) = civil_from_days(days_from_civil(date.0, date.1, date.2) + days); (y, m, d) } /// ISO8601 日期差分解,对齐 polyfill 的 dateUntil/untilCalendar。 /// largest:0=year 1=month 2=week 3=day。 fn date_until_iso(start: (i128, i128, i128), end: (i128, i128, i128), largest: usize) -> [f64; 10] { let mut values = [0.0; 10]; if largest >= 2 { let mut days = days_from_civil(end.0, end.1, end.2) - days_from_civil(start.0, start.1, start.2); if largest == 2 { values[2] = (days / 7) as f64; days %= 7; } values[3] = days as f64; return values; } let sign = compare_iso_date(end, start); if sign == 0 { return values; } let diff_years = end.0 - start.0; let diff_days = end.2 - start.2; let diff_in_year_sign = if end.1 > start.1 { 1 } else if end.1 < start.1 { -1 } else if diff_days > 0 { 1 } else if diff_days < 0 { -1 } else { 0 }; // 终点的月-日早于起点的月-日时,年份差需沿 sign 方向修正 1。 let mut years = if diff_in_year_sign * sign < 0 { diff_years - sign } else { diff_years }; let mut months = 0_i128; if largest == 1 { months = years * 12; years = 0; } let intermediate = add_months_i128(start.0, start.1, start.2, years * 12 + months); // 闰日校正:intermediate 已越过终点时年份回退 1。 if surpasses_with_day(sign, intermediate, start.2, end) { years -= sign; } // 从 start 按锚定日逐月推进(月末钳制);current 始终是未越界的真实日期, // 锚定日覆盖只用于越界比较,避免伪日期(如 2 月 29 日)污染天数计算。 let mut current = add_months_i128(start.0, start.1, start.2, years * 12 + months); loop { months += sign; let candidate = add_months_i128(start.0, start.1, start.2, years * 12 + months); let mut cmp = candidate; cmp.2 = start.2; if surpasses_with_day(sign, cmp, start.2, end) { break; } current = candidate; } months -= sign; let days = days_from_civil(end.0, end.1, end.2) - days_from_civil(current.0, current.1, current.2); values[0] = years as f64; values[1] = months as f64; values[3] = days as f64; values } /// 向零截断到 increment 的倍数。 fn round_to_increment_trunc(value: i128, increment: i128) -> i128 { (value / increment) * increment } /// 时长日期部分符号(首个非零分量决定)。 fn date_duration_sign(values: &[f64; 10]) -> i128 { for value in values[0..4].iter().copied() { if value != 0.0 { return if value > 0.0 { 1 } else { -1 }; } } 0 } /// 在日期上叠加时长(年/月/周/日)。 fn add_date_duration(date: (i128, i128, i128), values: &[f64; 10]) -> (i128, i128, i128) { let years = values[0] as i128; let months = values[1] as i128; let weeks = values[2] as i128; let days = values[3] as i128; let d = add_months_i128(date.0, date.1, date.2, years * 12 + months); add_days_iso(d, weeks * 7 + days) } /// 无符号舍入模式(ApplyUnsignedRoundingMode 用的模式集合)。 #[derive(Clone, Copy)] enum UnsignedMode { Zero, Infinity, HalfEven, HalfInfinity, HalfZero, } /// ApplyUnsignedRoundingMode:在 r1/r2 间选择(均为非负量)。 fn apply_unsigned_rounding( r1: i128, r2: i128, cmp: std::cmp::Ordering, even: bool, mode: InstantRoundingMode, negative: bool, ) -> i128 { let unsigned_mode = match mode { InstantRoundingMode::Ceil => { if negative { UnsignedMode::Zero } else { UnsignedMode::Infinity } } InstantRoundingMode::Floor => { if negative { UnsignedMode::Infinity } else { UnsignedMode::Zero } } InstantRoundingMode::Expand => UnsignedMode::Infinity, InstantRoundingMode::Trunc => UnsignedMode::Zero, InstantRoundingMode::HalfCeil => { if negative { UnsignedMode::HalfZero } else { UnsignedMode::HalfInfinity } } InstantRoundingMode::HalfFloor => { if negative { UnsignedMode::HalfInfinity } else { UnsignedMode::HalfZero } } InstantRoundingMode::HalfEven => UnsignedMode::HalfEven, InstantRoundingMode::HalfExpand => UnsignedMode::HalfInfinity, InstantRoundingMode::HalfTrunc => UnsignedMode::HalfZero, }; match unsigned_mode { UnsignedMode::Zero => r1, UnsignedMode::Infinity => r2, UnsignedMode::HalfEven => match cmp { std::cmp::Ordering::Less => r1, std::cmp::Ordering::Equal => { if even { r1 } else { r2 } } std::cmp::Ordering::Greater => r2, }, UnsignedMode::HalfInfinity => { if cmp == std::cmp::Ordering::Less { r1 } else { r2 } } UnsignedMode::HalfZero => { if cmp == std::cmp::Ordering::Greater { r2 } else { r1 } } } } /// ComputeNudgeWindow:smallestUnit 为 day/week/month/year 时的舍入窗口。 /// unit:0=year 1=month 2=week 3=day。 fn nudge_window( sign: i128, values: &[f64; 10], date1: (i128, i128, i128), increment: i128, unit: usize, shift: bool, ) -> (i128, i128, [f64; 10], [f64; 10]) { let years = values[0] as i128; let months = values[1] as i128; let weeks = values[2] as i128; let days = values[3] as i128; let (r1, r2) = match unit { 0 => { let y = round_to_increment_trunc(years, increment); let r1 = if !shift { y } else { y + increment * sign }; (r1, r1 + increment * sign) } 1 => { let m = round_to_increment_trunc(months, increment); let r1 = if !shift { m } else { m + increment * sign }; (r1, r1 + increment * sign) } 2 => { let weeks_start = add_months_i128(date1.0, date1.1, date1.2, years * 12 + months); let weeks_end = add_days_iso(weeks_start, days); let w = date_until_iso(weeks_start, weeks_end, 2)[2] as i128; let total = weeks + w; let r1 = round_to_increment_trunc(total, increment); let r1 = if !shift { r1 } else { r1 + increment * sign }; (r1, r1 + increment * sign) } _ => { let d = round_to_increment_trunc(days, increment); let r1 = if !shift { d } else { d + increment * sign }; (r1, r1 + increment * sign) } }; let mut start = [0.0; 10]; let mut end = [0.0; 10]; match unit { 0 => { start[0] = r1 as f64; end[0] = r2 as f64; } 1 => { start[0] = years as f64; start[1] = r1 as f64; end[0] = years as f64; end[1] = r2 as f64; } 2 => { start[0] = years as f64; start[1] = months as f64; start[2] = r1 as f64; end[0] = years as f64; end[1] = months as f64; end[2] = r2 as f64; } _ => { start[0] = years as f64; start[1] = months as f64; start[2] = weeks as f64; start[3] = r1 as f64; end[0] = years as f64; end[1] = months as f64; end[2] = weeks as f64; end[3] = r2 as f64; } } (r1, r2, start, end) } /// NudgeToCalendarUnit:日历单位用纪元纳秒边界取整。 #[allow(clippy::too_many_arguments)] fn nudge_to_calendar_unit( sign: i128, values: &[f64; 10], origin_epoch: i128, dest_epoch: i128, date1: (i128, i128, i128), time1_ns: i128, increment: i128, unit: usize, mode: InstantRoundingMode, ) -> Result<([f64; 10], i128, bool), ()> { let epoch_of = |dur: &[f64; 10]| -> Result { if date_duration_sign(dur) == 0 { return Ok(origin_epoch); } let date = add_date_duration(date1, dur); let days = days_from_civil(date.0, date.1, date.2); if days.abs() > MAX_ISO_DAY { return Err(()); } Ok(days * DAY_NS + time1_ns) }; let mut did_expand = false; let (mut r1, mut r2, mut start_dur, mut end_dur) = nudge_window(sign, values, date1, increment, unit, false); let mut start_epoch = epoch_of(&start_dur)?; let mut end_epoch = epoch_of(&end_dur)?; let in_window = if sign > 0 { dest_epoch >= start_epoch && dest_epoch <= end_epoch } else { dest_epoch <= start_epoch && dest_epoch >= end_epoch }; if !in_window { (r1, r2, start_dur, end_dur) = nudge_window(sign, values, date1, increment, unit, true); start_epoch = epoch_of(&start_dur)?; end_epoch = epoch_of(&end_dur)?; did_expand = true; let in_window = if sign > 0 { dest_epoch >= start_epoch && dest_epoch <= end_epoch } else { dest_epoch <= start_epoch && dest_epoch >= end_epoch }; if !in_window { return Err(()); } } let numerator = dest_epoch - start_epoch; let denominator = end_epoch - start_epoch; let even = (r1.abs() / increment) % 2 == 0; let rounded_unit = if numerator == 0 { r1.abs() } else if numerator == denominator { r2.abs() } else { let cmp = (numerator * 2).abs().cmp(&denominator.abs()); apply_unsigned_rounding(r1.abs(), r2.abs(), cmp, even, mode, sign < 0) }; did_expand = did_expand || rounded_unit == r2.abs(); let duration = if rounded_unit == r2.abs() { end_dur } else { start_dur }; let nudged = if did_expand { end_epoch } else { start_epoch }; Ok((duration, nudged, did_expand)) } /// BubbleRelativeDuration:舍入越过小单位边界时向更大单位进位(到 largest 为止)。 fn bubble_relative_duration( sign: i128, mut values: [f64; 10], nudged_epoch: i128, date1: (i128, i128, i128), time1_ns: i128, largest: usize, start_unit: usize, ) -> Result<[f64; 10], ()> { if start_unit == 0 { return Ok(values); } let mut unit = start_unit - 1; loop { if unit >= largest { if unit == 2 && largest != 2 { // weeks 不向 months 进位,跳过。 if unit == 0 { return Ok(values); } unit -= 1; continue; } let mut end_dur = values; match unit { 0 => { end_dur[0] = (values[0] as i128 + sign) as f64; end_dur[1] = 0.0; end_dur[2] = 0.0; end_dur[3] = 0.0; } 1 => { end_dur[1] = (values[1] as i128 + sign) as f64; end_dur[2] = 0.0; end_dur[3] = 0.0; } 2 => { end_dur[2] = (values[2] as i128 + sign) as f64; end_dur[3] = 0.0; } _ => unreachable!(), } end_dur[4..10].fill(0.0); let end_date = add_date_duration(date1, &end_dur); // Bubble 边界只用于比较,不做 ISO 范围校验(对齐 bugzilla 2036259)。 let end_epoch = days_from_civil(end_date.0, end_date.1, end_date.2) * DAY_NS + time1_ns; let reached_end = if sign > 0 { nudged_epoch >= end_epoch } else { nudged_epoch <= end_epoch }; if reached_end { values = end_dur; } else { return Ok(values); } if unit == 0 { return Ok(values); } unit -= 1; } else { return Ok(values); } } } /// 在年月上推进指定月数并保持日(超出目标月末时截断)。 fn add_months_i128(year: i128, month: i128, day: i128, months: i128) -> (i128, i128, i128) { let total = year * 12 + (month - 1) + months; let ny = total.div_euclid(12); let nm = total.rem_euclid(12) + 1; let max_day = days_in_month(ny, nm).unwrap_or(31); (ny, nm, day.min(max_day)) } /// `Temporal.PlainDateTime.prototype.until/since(other, options)`:按最大/最小单位 /// 差值设置(GetDifferenceSettings 产物:单位层级、增量、舍入模式)。 struct DifferenceSettings { largest_index: usize, smallest_index: usize, increment: i128, mode: InstantRoundingMode, } /// PlainDate 差值单位层级:0=year 1=month 2=week 3=day(不含时间单位)。 fn plain_date_unit_index(value: &str) -> Option { match value { "year" | "years" => Some(0), "month" | "months" => Some(1), "week" | "weeks" => Some(2), "day" | "days" => Some(3), _ => None, } } /// 解析差值选项(对齐 GetDifferenceSettings):读取顺序 largestUnit → /// roundingIncrement → roundingMode → smallestUnit。date_only 时单位限定 /// year/month/week/day,smallestUnit 缺省 "day"(含时间时缺省 "nanosecond")。 fn parse_difference_settings( vm: &mut H, options_value: JsValue, date_only: bool, ) -> Result { let unit_index = |value: &str| -> Option { if date_only { plain_date_unit_index(value) } else { plain_date_time_unit_index(value) } }; let default_smallest = if date_only { "day" } else { "nanosecond" }; let (largest_raw, increment_value, mode_value, smallest_raw) = if options_value.is_undefined() { (None, 1.0, "trunc".to_string(), default_smallest.to_string()) } else { if !options_value.is_object() { return Err(crate::error::create_type_error(vm, "options must be an object")); } let options_ptr = options_value.as_js_object_ptr(); if options_ptr.is_null() { return Err(crate::error::create_type_error(vm, "options must be an object")); } let options = unsafe { &*options_ptr }; let largest_raw = match temporal_option_value(vm, options, options_value, "largestUnit") { Ok(raw) if raw.is_undefined() => None, Ok(raw) => Some(temporal_option_string(vm, raw)?), Err(error) => return Err(error), }; let increment_raw = match temporal_option_value(vm, options, options_value, "roundingIncrement") { Ok(raw) if raw.is_undefined() => 1.0, Ok(raw) => temporal_option_number(vm, raw)?, Err(error) => return Err(error), }; let mode_raw = match temporal_option_value(vm, options, options_value, "roundingMode") { Ok(raw) if raw.is_undefined() => "trunc".to_string(), Ok(raw) => temporal_option_string(vm, raw)?, Err(error) => return Err(error), }; let smallest_raw = match temporal_option_value(vm, options, options_value, "smallestUnit") { Ok(raw) if raw.is_undefined() => default_smallest.to_string(), Ok(raw) => temporal_option_string(vm, raw)?, Err(error) => return Err(error), }; (largest_raw, increment_raw, mode_raw, smallest_raw) }; let smallest_index = match unit_index(&smallest_raw) { Some(index) => index, None => return Err(crate::error::create_range_error(vm, "invalid smallestUnit")), }; // auto/缺省:LargerOfTwoTemporalUnits('day', smallestUnit)。 // 索引 0=year…3=day…9=nanosecond,更大单位取更小索引,故为 min(3, smallest)。 let largest_index = match largest_raw { Some(value) if value == "auto" => smallest_index.min(3), Some(value) => match unit_index(&value) { Some(index) => index, None => return Err(crate::error::create_range_error(vm, "invalid largestUnit")), }, None => smallest_index.min(3), }; if largest_index > smallest_index { return Err(crate::error::create_range_error(vm, "smallestUnit exceeds largestUnit")); } let Some(mode) = instant_rounding_mode(&mode_value) else { return Err(crate::error::create_range_error(vm, "invalid roundingMode")); }; if !increment_value.is_finite() { return Err(crate::error::create_range_error(vm, "invalid roundingIncrement")); } let increment = increment_value.trunc(); if !(1.0..=1_000_000_000.0).contains(&increment) { return Err(crate::error::create_range_error(vm, "invalid roundingIncrement")); } let increment = increment as i128; const UNIT_LIMITS: [i128; 6] = [24, 60, 60, 1_000, 1_000, 1_000]; if smallest_index >= 4 { let limit = UNIT_LIMITS[smallest_index - 4]; if increment >= limit || limit % increment != 0 { return Err(crate::error::create_range_error(vm, "invalid roundingIncrement")); } } Ok(DifferenceSettings { largest_index, smallest_index, increment, mode, }) } /// 差值核心:internal = end - start,按设置取整;since 用 NegateRoundingMode /// 的舍入模式并在最后整体取反(不调换两端,调换会改变 0.5 边界所在的年长)。 #[allow(clippy::too_many_arguments)] fn difference_core( vm: &mut H, start: (i128, i128, i128), start_time_ns: i128, end: (i128, i128, i128), end_time_ns: i128, settings: DifferenceSettings, since: bool, ) -> NativeResult { let DifferenceSettings { largest_index, smallest_index, increment, mut mode, } = settings; if since { mode = match mode { InstantRoundingMode::Ceil => InstantRoundingMode::Floor, InstantRoundingMode::Floor => InstantRoundingMode::Ceil, InstantRoundingMode::HalfCeil => InstantRoundingMode::HalfFloor, InstantRoundingMode::HalfFloor => InstantRoundingMode::HalfCeil, other => other, }; } const UNIT_NS: [i128; 6] = [3_600_000_000_000, 60_000_000_000, 1_000_000_000, 1_000_000, 1_000, 1]; const DAY_NS: i128 = 86_400_000_000_000; // 规范语义:internal = other - receiver;since 用 NegateRoundingMode 的舍入模式,最后整体取反。 // 不调换两端(调换会改变 0.5 边界所在的年长,破坏对称性)。 let origin_epoch = days_from_civil(start.0, start.1, start.2) * DAY_NS + start_time_ns; let dest_epoch = days_from_civil(end.0, end.1, end.2) * DAY_NS + end_time_ns; let date1 = start; let time1_ns = start_time_ns; let mut date2 = end; let mut time_ns = end_time_ns - time1_ns; let time_sign = if time_ns > 0 { 1_i128 } else if time_ns < 0 { -1_i128 } else { 0 }; let date_sign = compare_iso_date(date1, date2); // 日期差与时间差符号一致时,从日期差借一天给时间差,使时间落回一天内。 if date_sign != 0 && date_sign == time_sign { date2 = add_days_iso(date2, time_sign); time_ns -= time_sign * DAY_NS; } // 日期部分:largestUnit 为时间单位时按 day 分解,再把 days 并入时间。 let date_largest = largest_index.min(3); let mut date_values = date_until_iso(date1, date2, date_largest); let mut date_days = date_values[3] as i128; if largest_index >= 4 { time_ns += date_days * DAY_NS; date_values[3] = 0.0; date_days = 0; } let sign = { let date_sign = date_duration_sign(&date_values); if date_sign != 0 { date_sign } else if time_ns > 0 { 1 } else if time_ns < 0 { -1 } else { 1 } }; let mut values = if smallest_index == 9 && increment == 1 { // 不要求舍入:时间按最大单位拆回。 let mut values = date_values; if largest_index >= 4 { let Some(time_values) = balance_instant_difference(time_ns, largest_index - 4) else { return NativeResult::Err(crate::error::create_range_error(vm, "difference is out of range")); }; values[4..10].copy_from_slice(&time_values[4..10]); } else { let day_part = time_ns / DAY_NS; let rem = time_ns % DAY_NS; values[3] += day_part as f64; let time_values = plain_time_components_signed(rem); for i in 0..6 { values[4 + i] = time_values[i] as f64; } } values } else if smallest_index >= 3 { // NudgeToDayOrTime:合并天与时间为总纳秒后按单位取整(day 为均匀单位,同样走此路径)。 let total_ns = time_ns + date_days * DAY_NS; let quantum = if smallest_index == 3 { DAY_NS * increment } else { UNIT_NS[smallest_index - 4] * increment }; let Some(rounded_ns) = round_instant_difference(total_ns, quantum, mode) else { return NativeResult::Err(crate::error::create_range_error(vm, "difference is out of range")); }; let whole_days = rounded_ns / DAY_NS; let rem = rounded_ns % DAY_NS; let old_whole = total_ns / DAY_NS; let did_expand_days = (whole_days - old_whole).signum() == total_ns.signum(); let mut values = date_values; if largest_index >= 4 { let Some(time_values) = balance_instant_difference(rounded_ns, largest_index - 4) else { return NativeResult::Err(crate::error::create_range_error(vm, "difference is out of range")); }; values[4..10].copy_from_slice(&time_values[4..10]); } else { values[3] = whole_days as f64; let time_values = plain_time_components_signed(rem); for i in 0..6 { values[4 + i] = time_values[i] as f64; } } if did_expand_days { let nudged = dest_epoch + (rounded_ns - total_ns); match bubble_relative_duration(sign, values, nudged, date1, time1_ns, largest_index, 3) { Ok(bubbled) => values = bubbled, Err(()) => { return NativeResult::Err(crate::error::create_range_error(vm, "difference is out of range")) } } } values } else { // NudgeToCalendarUnit:year/month/week 用纪元纳秒窗口取整。 let (mut values, nudged_epoch, did_expand) = match nudge_to_calendar_unit( sign, &date_values, origin_epoch, dest_epoch, date1, time1_ns, increment, smallest_index, mode, ) { Ok(result) => result, Err(()) => return NativeResult::Err(crate::error::create_range_error(vm, "difference is out of range")), }; if did_expand && smallest_index != 2 { match bubble_relative_duration(sign, values, nudged_epoch, date1, time1_ns, largest_index, smallest_index) { Ok(bubbled) => values = bubbled, Err(()) => { return NativeResult::Err(crate::error::create_range_error(vm, "difference is out of range")) } } } values }; if since { for value in &mut values { if *value != 0.0 { *value = -*value; } } } make_duration(vm, values) } /// 计算差值并舍入。until 返回 other 减 receiver,since 返回反向。 fn plain_date_time_difference(vm: &mut H, args: &[u8], since: bool) -> NativeResult { let (sy, sm, sd, st) = match plain_date_time_parts(vm, args) { Ok(parts) => parts, Err(error) => return NativeResult::Err(error), }; let other = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; let (oy, om, od, ot, _) = match plain_date_time_like_parts(vm, other, true) { Ok(parts) => parts, Err(error) => return NativeResult::Err(error), }; let options_value = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; let settings = match parse_difference_settings(vm, options_value, false) { Ok(settings) => settings, Err(error) => return NativeResult::Err(error), }; difference_core( vm, (i128::from(sy), i128::from(sm), i128::from(sd)), st as i128, (i128::from(oy), i128::from(om), i128::from(od)), ot as i128, settings, since, ) } /// `Temporal.PlainDateTime.prototype.until(other, options)`。 pub fn plain_date_time_until(vm: &mut H, args: &[u8]) -> NativeResult { plain_date_time_difference(vm, args, false) } /// `Temporal.PlainDateTime.prototype.since(other, options)`。 pub fn plain_date_time_since(vm: &mut H, args: &[u8]) -> NativeResult { plain_date_time_difference(vm, args, true) } /// `Temporal.PlainDateTime.prototype.add(durationLike, options)`。 pub fn plain_date_time_add(vm: &mut H, args: &[u8]) -> NativeResult { plain_date_time_apply_duration(vm, args, 1) } /// `Temporal.PlainDateTime.prototype.subtract(durationLike, options)`。 pub fn plain_date_time_subtract(vm: &mut H, args: &[u8]) -> NativeResult { plain_date_time_apply_duration(vm, args, -1) } /// `Temporal.PlainDateTime.prototype.toJSON()`:输出默认 ISO 日期时间。 pub fn plain_date_time_to_json(vm: &mut H, args: &[u8]) -> NativeResult { plain_date_time_iso_string(vm, args) } // ───────────────────── PlainDate 扩展方法 ───────────────────── /// 从 PlainDate receiver 读 year/month/day 并构造 chrono NaiveDate(供日期计算)。 fn plain_date_naive(vm: &mut H, args: &[u8]) -> Result { let (y, m, d) = plain_date_ymd(vm, args)?; NaiveDate::from_ymd_opt(y as i32, m as u32, d as u32) .ok_or_else(|| crate::error::create_range_error(vm, "invalid date")) } /// 从 PlainDate receiver 读 year/month/day 并按 ISO 范围校验,不依赖 chrono 年份范围。 fn plain_date_ymd_checked(vm: &mut H, args: &[u8]) -> Result<(i32, u32, u32), JsValue> { let (y, m, d) = plain_date_ymd(vm, args)?; if y.is_nan() || m.is_nan() || d.is_nan() { return Err(crate::error::create_type_error(vm, "called on incompatible receiver")); } let ymd = (y.trunc() as i32, m.trunc() as u32, d.trunc() as u32); if !valid_iso_date(ymd.0, ymd.1, ymd.2) { return Err(crate::error::create_range_error(vm, "invalid ISO date")); } // 表示范围(PlainDate):-271821-04-19(第 -100,000,001 天)… +275760-09-13(第 +100,000,000 天), // 超出 chrono NaiveDate 年份范围(约 ±26 万年)的日期也在此被拒绝而非后续 panic。 let day_count = days_from_civil(i128::from(ymd.0), i128::from(ymd.1), i128::from(ymd.2)); if !(-100_000_001..=100_000_000).contains(&day_count) { return Err(crate::error::create_range_error(vm, "ISO date is out of range")); } Ok(ymd) } /// 从对象式日期字段(`{year, month, day}`)读三字段与日历;PlainDate/PlainDateTime 实例 /// 直接读内部槽;缺字段返回 None。 fn object_date_ymd( vm: &mut H, val: JsValue, constrain: bool, ) -> Result<(i32, u32, u32, Option), JsValue> { if !val.is_object() { return Err(crate::error::create_type_error(vm, "cannot convert to PlainDate")); } let ptr = val.as_js_object_ptr(); if ptr.is_null() { return Err(crate::error::create_type_error(vm, "cannot convert to PlainDate")); } let obj = unsafe { &*ptr }; if obj.is_plain_date_time_obj() { return Ok(( get_double_prop(obj, 0) as i32, get_double_prop(obj, 1) as u32, get_double_prop(obj, 2) as u32, Some(get_calendar_id(obj, 4)), )); } if obj.is_plain_date_obj() { return Ok(( get_double_prop(obj, 0) as i32, get_double_prop(obj, 1) as u32, get_double_prop(obj, 2) as u32, Some(get_calendar_id(obj, 3)), )); } if obj.is_zoned_date_time_obj() { let (year, month, day, _) = zoned_date_time_plain_parts(vm, obj)?; return Ok((year, month, day, Some(get_calendar_id(obj, 2)))); } let (year, month, day, _, calendar) = plain_date_time_object_parts(vm, val, obj, constrain, true)?; Ok((year, month, day, calendar)) } fn date_like_ymd(vm: &mut H, val: JsValue) -> Result<(i32, u32, u32, Option), JsValue> { let ymd = if val.is_string() { let (y, m, d) = parse_plain_date_string(&to_string(val)) .map_err(|_| crate::error::create_range_error(vm, "invalid ISO 8601 date"))?; (y, m, d, None) } else { object_date_ymd(vm, val, true)? }; if !valid_iso_date(ymd.0, ymd.1, ymd.2) { return Err(crate::error::create_range_error(vm, "invalid ISO date")); } // 表示范围(PlainDate):-271821-04-19(第 -100,000,001 天)… +275760-09-13(第 +100,000,000 天), // 超出 chrono NaiveDate 年份范围(约 ±26 万年)的日期也在此被拒绝而非后续 panic。 let day_count = days_from_civil(i128::from(ymd.0), i128::from(ymd.1), i128::from(ymd.2)); if !(-100_000_001..=100_000_000).contains(&day_count) { return Err(crate::error::create_range_error(vm, "ISO date is out of range")); } Ok(ymd) } /// `Temporal.PlainDate.prototype.dayOfWeek` getter:ISO 周几(周一 1 … 周日 7)。 pub fn plain_date_day_of_week(vm: &mut H, args: &[u8]) -> NativeResult { let date = match plain_date_naive(vm, args) { Ok(d) => d, Err(e) => return NativeResult::Err(e), }; NativeResult::Ok(JsValue::float((date.weekday().num_days_from_monday() + 1) as f64)) } /// `Temporal.PlainDate.prototype.dayOfYear` getter:年内第几天(1 起)。 pub fn plain_date_day_of_year(vm: &mut H, args: &[u8]) -> NativeResult { let date = match plain_date_naive(vm, args) { Ok(d) => d, Err(e) => return NativeResult::Err(e), }; NativeResult::Ok(JsValue::float(date.ordinal() as f64)) } /// 计算给定年月的天数(含闰年)。 fn days_in_month_iso(year: i32, month: u32) -> u32 { NaiveDate::from_ymd_opt(year, month + 1, 1) .and_then(|next| next.checked_sub_days(Days::new(1))) .map(|d| d.day()) .unwrap_or(31) } /// `Temporal.PlainDate.prototype.daysInMonth` getter。 pub fn plain_date_days_in_month(vm: &mut H, args: &[u8]) -> NativeResult { let date = match plain_date_naive(vm, args) { Ok(d) => d, Err(e) => return NativeResult::Err(e), }; NativeResult::Ok(JsValue::float(days_in_month_iso(date.year(), date.month()) as f64)) } /// `Temporal.PlainDate.prototype.daysInWeek` getter:恒 7。 pub fn plain_date_days_in_week(vm: &mut H, args: &[u8]) -> NativeResult { let _ = native_try!(plain_date_ymd(vm, args)); NativeResult::Ok(JsValue::float(7.0)) } fn is_leap_year_iso(year: i32) -> bool { NaiveDate::from_ymd_opt(year, 2, 29).is_some() } /// `Temporal.PlainDate.prototype.daysInYear` getter:366(闰年)或 365。 pub fn plain_date_days_in_year(vm: &mut H, args: &[u8]) -> NativeResult { let date = match plain_date_naive(vm, args) { Ok(d) => d, Err(e) => return NativeResult::Err(e), }; NativeResult::Ok(JsValue::float(if is_leap_year_iso(date.year()) { 366.0 } else { 365.0 })) } /// `Temporal.PlainDate.prototype.monthsInYear` getter:恒 12(ISO 日历)。 pub fn plain_date_months_in_year(vm: &mut H, args: &[u8]) -> NativeResult { let _ = native_try!(plain_date_ymd(vm, args)); NativeResult::Ok(JsValue::float(12.0)) } /// `Temporal.PlainDate.prototype.inLeapYear` getter。 pub fn plain_date_in_leap_year(vm: &mut H, args: &[u8]) -> NativeResult { let date = match plain_date_naive(vm, args) { Ok(d) => d, Err(e) => return NativeResult::Err(e), }; NativeResult::Ok(JsValue::bool(is_leap_year_iso(date.year()))) } /// `Temporal.PlainDate.prototype.weekOfYear` getter:ISO 周数。 pub fn plain_date_week_of_year(vm: &mut H, args: &[u8]) -> NativeResult { let date = match plain_date_naive(vm, args) { Ok(d) => d, Err(e) => return NativeResult::Err(e), }; NativeResult::Ok(JsValue::float(date.iso_week().week() as f64)) } /// `Temporal.PlainDate.prototype.yearOfWeek` getter:ISO 周所属年。 pub fn plain_date_year_of_week(vm: &mut H, args: &[u8]) -> NativeResult { let date = match plain_date_naive(vm, args) { Ok(d) => d, Err(e) => return NativeResult::Err(e), }; NativeResult::Ok(JsValue::float(date.iso_week().year() as f64)) } /// `Temporal.PlainDate.prototype.monthCode` getter:`M01`..`M12`。 pub fn plain_date_month_code(vm: &mut H, args: &[u8]) -> NativeResult { let (_, m, _) = match plain_date_ymd(vm, args) { Ok(x) => x, Err(e) => return NativeResult::Err(e), }; NativeResult::Ok(vm.new_string(&format!("M{m:02}"))) } /// `Temporal.PlainDate.prototype.era` getter:ISO 日历无纪元,恒 undefined。 pub fn plain_date_era(vm: &mut H, args: &[u8]) -> NativeResult { let _ = native_try!(plain_date_ymd(vm, args)); NativeResult::Ok(JsValue::undefined()) } /// `Temporal.PlainDate.prototype.eraYear` getter:ISO 日历无纪元,恒 undefined。 pub fn plain_date_era_year(vm: &mut H, args: &[u8]) -> NativeResult { let _ = native_try!(plain_date_ymd(vm, args)); NativeResult::Ok(JsValue::undefined()) } /// `Temporal.PlainDate.prototype.calendarId`:读日历槽,兜底 ISO 8601。 pub fn plain_date_calendar_id(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)); NativeResult::Ok(vm.new_string(&get_calendar_id(obj, 3))) } /// `Temporal.PlainDate.prototype.equals(other)`:比较年月日是否相等。 pub fn plain_date_equals(vm: &mut H, args: &[u8]) -> NativeResult { let (y, m, d) = match plain_date_ymd(vm, args) { Ok(x) => x, Err(e) => return NativeResult::Err(e), }; let other_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; let other = match date_like_ymd(vm, other_val) { Ok(x) => x, Err(e) => return NativeResult::Err(e), }; let equal = other.0 as f64 == y && other.1 as f64 == m && other.2 as f64 == d; NativeResult::Ok(JsValue::bool(equal)) } /// `Temporal.PlainDate.compare(a, b)`:静态比较,返回 -1/0/1。 pub fn plain_date_compare(vm: &mut H, args: &[u8]) -> NativeResult { let a_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; let b_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; let a = match date_like_ymd(vm, a_val) { Ok(x) => x, Err(e) => return NativeResult::Err(e), }; let b = match date_like_ymd(vm, b_val) { Ok(x) => x, Err(e) => return NativeResult::Err(e), }; let cmp = (a.0, a.1, a.2).cmp(&(b.0, b.1, b.2)); NativeResult::Ok(JsValue::float(match cmp { std::cmp::Ordering::Less => -1.0, std::cmp::Ordering::Equal => 0.0, std::cmp::Ordering::Greater => 1.0, })) } /// `Temporal.PlainDate.prototype.valueOf()`:Temporal 对象禁止转原始值。 pub fn plain_date_value_of(vm: &mut H, _args: &[u8]) -> NativeResult { NativeResult::Err(crate::error::create_type_error(vm, "Temporal.PlainDate has no valueOf")) } /// `Temporal.PlainDate.prototype.toJSON()`:输出 ISO 日期串(同 toString)。 pub fn plain_date_to_json(vm: &mut H, args: &[u8]) -> NativeResult { let ptr = match receiver_obj(vm, args) { Ok(p) => p, Err(e) => return NativeResult::Err(e), }; let obj = unsafe { &*ptr }; if let Err(e) = ensure_plain_date(vm, obj) { return NativeResult::Err(e); } 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; NativeResult::Ok(vm.new_string(&format!("{year:04}-{month:02}-{day:02}"))) } /// 按 duration-like 日期字段对日期做加减(Temporal 大单位运算,月份不足日时取月末)。 fn date_apply_duration(vm: &mut H, args: &[u8], sign: i64) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; let date = match plain_date_naive(vm, args) { Ok(d) => d, Err(e) => return NativeResult::Err(e), }; let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; let values = match duration_like_values(vm, val) { Ok(values) => values, Err(error) => return NativeResult::Err(error), }; let [y, m, w, d, h, min, s, ms, us, ns] = values; let mut result = date; let total_months = ((y * 12.0 + m) * sign as f64) as i64; if total_months != 0 { if let Some(nd) = add_signed_months(result, total_months) { result = nd; } else { return NativeResult::Err(crate::error::create_range_error(vm, "date out of range")); } } let time_days = (h / 24.0 + min / 1_440.0 + s / 86_400.0 + ms / 86_400_000.0 + us / 86_400_000_000.0 + ns / 86_400_000_000_000.0) .trunc(); let total_days = ((w * 7.0 + d + time_days) * sign as f64) as i64; if total_days != 0 { let next = if total_days >= 0 { result.checked_add_days(Days::new(total_days as u64)) } else { result.checked_sub_days(Days::new(total_days.unsigned_abs())) }; if let Some(nd) = next { result = nd; } else { return NativeResult::Err(crate::error::create_range_error(vm, "date out of range")); } } make_plain_date(vm, result.year(), result.month(), result.day(), &get_calendar_id(obj, 3)) } /// `Temporal.PlainDate.prototype.add(durationLike)`。 pub fn plain_date_add(vm: &mut H, args: &[u8]) -> NativeResult { date_apply_duration(vm, args, 1) } /// `Temporal.PlainDate.prototype.subtract(durationLike)`。 pub fn plain_date_subtract(vm: &mut H, args: &[u8]) -> NativeResult { date_apply_duration(vm, args, -1) } fn add_signed_months(date: NaiveDate, months: i64) -> Option { let total = date.year() as i64 * 12 + date.month0() as i64 + months; let year = total.div_euclid(12); let month = total.rem_euclid(12) as u32 + 1; let year = i32::try_from(year).ok()?; let day = date.day().min(days_in_month_iso(year, month)); NaiveDate::from_ymd_opt(year, month, day) } /// `Temporal.PlainDate.prototype.until(other, options)`:date-only 单位 /// (year/month/week/day)差值 + 舍入语义。 pub fn plain_date_until(vm: &mut H, args: &[u8]) -> NativeResult { let start = match plain_date_ymd_checked(vm, args) { Ok(date) => date, Err(error) => return NativeResult::Err(error), }; let other_value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; let other = match date_like_ymd(vm, other_value) { Ok(value) => value, Err(error) => return NativeResult::Err(error), }; let options_value = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; let settings = match parse_difference_settings(vm, options_value, true) { Ok(settings) => settings, Err(error) => return NativeResult::Err(error), }; difference_core( vm, (i128::from(start.0), i128::from(start.1), i128::from(start.2)), 0, (i128::from(other.0), i128::from(other.1), i128::from(other.2)), 0, settings, false, ) } /// `Temporal.PlainDate.prototype.since(other, options)`:date-only 单位差值 + 舍入。 pub fn plain_date_since(vm: &mut H, args: &[u8]) -> NativeResult { let start = match plain_date_ymd_checked(vm, args) { Ok(date) => date, Err(error) => return NativeResult::Err(error), }; let other_value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; let other = match date_like_ymd(vm, other_value) { Ok(value) => value, Err(error) => return NativeResult::Err(error), }; let options_value = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; let settings = match parse_difference_settings(vm, options_value, true) { Ok(settings) => settings, Err(error) => return NativeResult::Err(error), }; difference_core( vm, (i128::from(start.0), i128::from(start.1), i128::from(start.2)), 0, (i128::from(other.0), i128::from(other.1), i128::from(other.2)), 0, settings, true, ) } #[cfg(test)] mod tests { use super::*; #[test] fn builtin_calendar_id_whitelist() { // 18 项白名单全部命中且返回规范小写形式。 for id in CALENDAR_ID_WHITELIST { assert_eq!(is_builtin_calendar_id(id), Some(id), "白名单项 {id} 应命中自身"); } // ASCII 大小写折叠:仅首字母大写与全大写变体命中。 assert_eq!(is_builtin_calendar_id("IsO8601"), Some("iso8601")); assert_eq!(is_builtin_calendar_id("HEBREW"), Some("hebrew")); assert_eq!(is_builtin_calendar_id("Gregory"), Some("gregory")); // 点 I(U+0130)不属于 ASCII 折叠域,必须拒绝,否则误判为 iso8601。 assert_eq!(is_builtin_calendar_id("\u{0130}SO8601"), None); // 白名单外与类日期串拒绝。 assert_eq!(is_builtin_calendar_id("notacal"), None); assert_eq!(is_builtin_calendar_id("1111-11-11"), None); assert_eq!(is_builtin_calendar_id("11111111"), None); } #[test] fn strict_calendar_id_parser() { // 严格解析:只收白名单 ID,ISO 串 / 未知值 / 空串全拒。 assert_eq!(parse_temporal_calendar_id_strict("hebrew"), Ok("hebrew".to_string())); assert_eq!(parse_temporal_calendar_id_strict("IsO8601"), Ok("iso8601".to_string())); assert_eq!(parse_temporal_calendar_id_strict(" gregory "), Ok("gregory".to_string())); assert_eq!( parse_temporal_calendar_id_strict("1997-12-04[u-ca=iso8601]"), Err("invalid calendar".to_string()) ); assert_eq!(parse_temporal_calendar_id_strict("11111111"), Err("invalid calendar".to_string())); assert_eq!(parse_temporal_calendar_id_strict("\u{0130}SO8601"), Err("invalid calendar".to_string())); assert_eq!(parse_temporal_calendar_id_strict(""), Err("invalid calendar".to_string())); } #[test] fn loose_calendar_string_parser() { // 宽松解析(property bag 路径):白名单 ID 返回规范小写,ISO 串路径恒为 iso8601。 assert_eq!(parse_temporal_calendar_string("gregory"), Ok("gregory".to_string())); assert_eq!(parse_temporal_calendar_string("iSo8601"), Ok("iso8601".to_string())); assert_eq!(parse_temporal_calendar_string("1997-12-04"), Ok("iso8601".to_string())); assert_eq!(parse_temporal_calendar_string("1997-12-04[u-ca=iso8601]"), Ok("iso8601".to_string())); assert_eq!(parse_temporal_calendar_string(""), Err("invalid calendar".to_string())); // 注解值未过白名单 / 裸未知标识符,两种路径都拒绝。 assert!(parse_temporal_calendar_string("[u-ca=notacal]").is_err()); assert!(parse_temporal_calendar_string("notacal").is_err()); } #[test] fn annotation_suffix_calendar_whitelist() { // 首个 u-ca 注解值过 18 项白名单:白名单内放行(含关键标记),白名单外拒绝。 assert!(validate_temporal_annotation_suffix("[u-ca=hebrew]").is_ok()); assert!(validate_temporal_annotation_suffix("[!u-ca=hebrew]").is_ok()); assert!(validate_temporal_annotation_suffix("[u-ca=iSo8601]").is_ok()); assert!(validate_temporal_annotation_suffix("[u-ca=notacal]").is_err()); assert!(validate_temporal_annotation_suffix("[u-ca=1111-11-11]").is_err()); // 点 I 变体不做 Unicode 折叠,拒绝。 assert!(validate_temporal_annotation_suffix("[u-ca=\u{0130}SO8601]").is_err()); // 第二及后续 u-ca 注解被忽略,不参与校验;含关键标记的重复日历报错。 assert!(validate_temporal_annotation_suffix("[u-ca=iso8601][u-ca=discord]").is_ok()); assert!(validate_temporal_annotation_suffix("[u-ca=iso8601][!u-ca=iso8601]").is_err()); } #[test] fn calendar_id_slot_fallback() { // 字符串槽原值返回由 VM 层构造器/getter 测试覆盖;此处验证 undefined 与非 string 槽兜底 iso8601。 let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::undefined()); obj.set_prop_at(3, JsValue::undefined()); assert_eq!(get_calendar_id(&obj, 3), "iso8601"); obj.set_prop_at(3, JsValue::int(7)); assert_eq!(get_calendar_id(&obj, 3), "iso8601"); } #[test] fn start_of_day_epoch_ns_instant_range() { // 当地午夜回推 epoch 越 Instant 界(±MAX)返回 None,供 startOfDay/hoursInDay RangeError 依据。 // -100000001 天 + 1h < -MAX → 越界。 assert_eq!(start_of_day_epoch_ns(-271_821, 4, 19, -60), None); // -100000000 天(-271821-04-20)UTC 当地午夜恰为 -MAX,在边界内。 assert_eq!(start_of_day_epoch_ns(-271_821, 4, 20, 0), Some(-8_640_000_000_000_000_000_000)); // 同日期 +1h 偏移使当地午夜 -MAX - 1h 越界。 assert_eq!(start_of_day_epoch_ns(-271_821, 4, 20, 60), None); // 明日边界越界:+100000001 天(UTC)。 assert_eq!(start_of_day_epoch_ns_by_days(100_000_001, 0), None); // 正常日期返回当地午夜。 assert_eq!(start_of_day_epoch_ns(1970, 1, 1, 0), Some(0)); assert_eq!(start_of_day_epoch_ns(1970, 1, 1, 60), Some(-3_600_000_000_000)); } #[test] fn format_time_zone_annotation_normalizes_offset() { // 命名区原样;偏移区规范化为 ±HH:MM 带冒号;critical 时 `!` 置于括号内。 assert_eq!(format_time_zone_annotation("UTC", false), "[UTC]"); assert_eq!(format_time_zone_annotation("+01:00", false), "[+01:00]"); assert_eq!(format_time_zone_annotation("+01", false), "[+01:00]"); assert_eq!(format_time_zone_annotation("-05:00", false), "[-05:00]"); assert_eq!(format_time_zone_annotation("UTC", true), "[!UTC]"); assert_eq!(format_time_zone_annotation("+01", true), "[!+01:00]"); } #[test] fn format_zoned_date_time_iso_annotations() { // 偏移/时区/日历注解各取值组合,验证段序 `{offset}[{tz}][{ca}]` 与 critical 前缀。 let base = format_zoned_date_time_iso(0, 60, "+01:00", "iso8601", true, None, "auto", "auto", "auto"); assert_eq!(base, Some("1970-01-01T01:00:00+01:00[+01:00]".to_string())); // offset never 省略偏移段,时区注解仍显示。 let no_offset = format_zoned_date_time_iso(0, 60, "+01:00", "iso8601", true, None, "never", "auto", "auto"); assert_eq!(no_offset, Some("1970-01-01T01:00:00[+01:00]".to_string())); // calendarName always 追加日历注解。 let ca_always = format_zoned_date_time_iso(0, 60, "+01:00", "iso8601", true, None, "auto", "auto", "always"); assert_eq!(ca_always, Some("1970-01-01T01:00:00+01:00[+01:00][u-ca=iso8601]".to_string())); // timeZoneName/calendarName critical 均 `!` 置于括号内。 let critical = format_zoned_date_time_iso(0, 60, "+01:00", "iso8601", true, None, "auto", "critical", "critical"); assert_eq!(critical, Some("1970-01-01T01:00:00+01:00[!+01:00][!u-ca=iso8601]".to_string())); // offset critical 偏移段前加 !。 let offset_critical = format_zoned_date_time_iso(0, 60, "+01:00", "iso8601", true, None, "critical", "auto", "auto"); assert_eq!(offset_critical, Some("1970-01-01T01:00:00!+01:00[+01:00]".to_string())); } #[test] fn format_zoned_date_time_iso_epoch_rounding_cross_midnight() { // 2000-01-01 前 1ns 已舍入到 2000-01-01 整点,8 位小数输出。 let rounded = round_instant_ns(946_684_799_999_999_999, 100, InstantRoundingMode::HalfExpand).unwrap(); let output = format_zoned_date_time_iso(rounded, 0, "UTC", "iso8601", true, Some(8), "auto", "auto", "auto"); assert_eq!(output, Some("2000-01-01T00:00:00.00000000+00:00[UTC]".to_string())); } #[test] fn local_to_epoch_ns_roundtrip() { // 与 parse_instant_string 互逆对拍:同一时刻的本地分量 + 偏移换算回 epoch 一致。 assert_eq!(parse_instant_string("2024-01-01T00:00:00+01:00"), local_to_epoch_ns(2024, 1, 1, 0.0, 60),); assert_eq!( parse_instant_string("1969-07-16T13:32:01.234567891Z"), local_to_epoch_ns(1969, 7, 16, 48_721_234_567_891.0, 0), ); // startOfDay 最小边界:-271821-04-20 加 1h 再回推 1h 偏移回到 -MAX。 assert_eq!( local_to_epoch_ns(-271821, 4, 20, 3_600_000_000_000.0, 60), Some(-8_640_000_000_000_000_000_000), ); } #[test] fn canonical_time_zone_4_digit_offset() { // ±HHMM 无冒号形式归一:ID 保留原串,offset 分钟数正确换算。 assert_eq!(canonical_time_zone("+0000"), Some(("+0000".to_string(), 0))); assert_eq!(canonical_time_zone("-0530"), Some(("-0530".to_string(), -330))); assert_eq!(canonical_time_zone("+2330"), Some(("+2330".to_string(), 1410))); // 非法分钟/小时拒绝。 assert_eq!(canonical_time_zone("+2400"), None); assert_eq!(canonical_time_zone("+0060"), None); assert_eq!(canonical_time_zone("+123"), None); // 长度不符 } #[test] fn extract_time_zone_annotation_forms() { // UTC / critical / 数值偏移各形式提取;u-ca 注解跳过,时区注解在前后均能取到。 assert_eq!(extract_time_zone_annotation("1976-11-18T15:23:30[UTC]"), Some(("UTC".to_string(), false))); assert_eq!(extract_time_zone_annotation("1976-11-18T15:23:30[!UTC]"), Some(("UTC".to_string(), true))); assert_eq!( extract_time_zone_annotation("1976-11-18T15:23:30+01:00[+01:00]"), Some(("+01:00".to_string(), false)) ); assert_eq!(extract_time_zone_annotation("1976-11-18T15:23:30[+01]"), Some(("+01".to_string(), false))); assert_eq!( extract_time_zone_annotation("1976-11-18T15:23:30[+0100]"), Some(("+0100".to_string(), false)) ); assert_eq!( extract_time_zone_annotation("1976-11-18T15:23:30[u-ca=iso8601][UTC]"), Some(("UTC".to_string(), false)) ); assert_eq!( extract_time_zone_annotation("1976-11-18T15:23:30[UTC][u-ca=iso8601]"), Some(("UTC".to_string(), false)) ); // 无注解 / 非法偏移注解返回 None。 assert_eq!(extract_time_zone_annotation("1976-11-18T15:23:30Z"), None); assert_eq!(extract_time_zone_annotation("1976-11-18T15:23:30[+24:00]"), None); assert_eq!(extract_time_zone_annotation("1976-11-18T15:23:30[]"), None); } }