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

4113 lines
187 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use chrono::{Datelike, Days, NaiveDate};
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;
mod common;
mod difference;
mod duration;
mod instant;
mod zoned_date_time;
use common::*;
use difference::*;
pub use duration::*;
pub use instant::*;
pub use zoned_date_time::*;
/// 时间串的日期歧义判定:形如 YYYY-MM / MMDD / YYYYMM / MM-DD 且作为日期合法 → 歧义。
///
/// # 边界与前提
/// - 2 月按闰年处理(0229 判歧义、0230 不判)。
/// - 月/日非法(13、00、2 月 30 日等)不算歧义,按时间解析。
fn is_ambiguous_date_string(s: &str) -> bool {
let bytes = s.as_bytes();
let all_digits = |range: std::ops::Range<usize>| {
bytes.get(range.clone()).is_some_and(|part| part.iter().all(u8::is_ascii_digit))
};
let two_digits = |at: usize| -> Option<i32> {
if at + 1 >= bytes.len() {
None
} else {
Some((bytes[at] - b'0') as i32 * 10 + (bytes[at + 1] - b'0') as i32)
}
};
let day_in_month = |month: i32, day: i32| -> bool {
// 2 月按闰年(29 天)判定歧义。
let max = match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => 29,
_ => return false,
};
day >= 1 && day <= max
};
match s.len() {
4 if all_digits(0..4) => {
let (month, day) = (two_digits(0).unwrap(), two_digits(2).unwrap());
day_in_month(month, day)
}
6 if all_digits(0..6) => (1..=12).contains(&two_digits(4).unwrap()),
5 if bytes[2] == b'-' && all_digits(0..2) && all_digits(3..5) => {
let (month, day) = (two_digits(0).unwrap(), two_digits(3).unwrap());
day_in_month(month, day)
}
7 if bytes[4] == b'-' && all_digits(0..4) && all_digits(5..7) => (1..=12).contains(&two_digits(5).unwrap()),
_ => false,
}
}
/// 解析时间主体(时[:分[:秒[.小数]]] 或 HHMM / HHMMSS),闰秒按前一秒。
fn parse_plain_clock(clock: &str) -> Option<f64> {
let (clock, fraction) = match clock.find(['.', ',']) {
Some(index) => (&clock[..index], Some(&clock[index + 1..])),
None => (clock, None),
};
let subsecond = match fraction {
Some(digits) => {
if digits.is_empty() || digits.len() > 9 || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
let mut padded = digits.to_string();
padded.extend(std::iter::repeat('0').take(9 - digits.len()));
padded.parse::<u32>().ok()?
}
None => 0,
};
let (hour, minute, second) = if clock.contains(':') {
let fields: Vec<&str> = clock.split(':').collect();
if fields.is_empty()
|| fields.len() > 3
|| fields
.iter()
.any(|field| field.len() != 2 || !field.bytes().all(|byte| byte.is_ascii_digit()))
{
return None;
}
if fraction.is_some() && fields.len() < 3 {
return None;
}
(
fields[0].parse::<u32>().ok()?,
fields.get(1).map_or(Ok(0), |field| field.parse()).ok()?,
fields.get(2).map_or(Ok(0), |field| field.parse()).ok()?,
)
} else {
if !clock.bytes().all(|byte| byte.is_ascii_digit())
|| clock.len() % 2 != 0
|| clock.len() > 6
|| clock.is_empty()
{
return None;
}
if fraction.is_some() && clock.len() < 6 {
return None;
}
let read2 = |at: usize| (clock.as_bytes()[at] - b'0') as u32 * 10 + (clock.as_bytes()[at + 1] - b'0') as u32;
(
read2(0),
if clock.len() >= 4 { read2(2) } else { 0 },
if clock.len() >= 6 { read2(4) } else { 0 },
)
};
let second = if second == 60 { 59 } else { second };
if hour > 23 || minute > 59 || second > 59 {
return None;
}
Some(
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,
)
}
/// ParseTemporalTimeString:时间串 → 当日纳秒(offset/注解被忽略、Z 拒绝)。
///
/// # 边界与前提
/// - Z/z designator → None;纯日期串 → None(不隐式午夜)。
/// - 日期+时间("1976-11-18T12:34...")→ 取 T 后时间部分;空格分隔需带日期部分。
/// - 无 T 的数字/连字符形式先做日期歧义判定:日期合法 → None(须 T 前缀);
/// 日期非法 → 按时间解析(HHMM-UU 形式,offset 被忽略)。
/// - 小数秒 ≤9 位;offset 小数秒 ≤9 位;闰秒按前一秒。
fn parse_plain_time_string(input: &str) -> Option<f64> {
let text = input.trim();
if text.contains('\u{2212}') {
return None;
}
let body = instant_string_without_annotations(text)?;
if body.contains(['Z', 'z']) {
return None;
}
let sep = body.find(['T', 't', ' ']);
let (date_part, time_part, has_t) = match sep {
Some(0) => ("", &body[1..], matches!(body.as_bytes().first(), Some(b'T' | b't'))),
Some(index) => (&body[..index], &body[index + 1..], matches!(body.as_bytes()[index], b'T' | b't')),
None => ("", body, false),
};
if !has_t && sep == Some(0) {
// 前导空格不能替代 T 前缀(无日期部分的时间串)。
return None;
}
if !date_part.is_empty() {
// 完整日期部分必须可解析(负零年等由 parse_iso_date 拒绝)。
if parse_iso_date(date_part).is_err() {
return None;
}
return parse_plain_time_spec(time_part);
}
if !has_t && !time_part.contains(':') && is_ambiguous_date_string(time_part) {
return None;
}
parse_plain_time_spec(time_part)
}
/// 时间主体 + 尾部 offset(剥离并忽略):offset 语法非法返回 None。
fn parse_plain_time_spec(s: &str) -> Option<f64> {
let offset_start = s
.char_indices()
.rev()
.find_map(|(index, ch)| matches!(ch, '+' | '-').then_some(index));
let (clock, offset) = match offset_start {
Some(index) => (&s[..index], &s[index..]),
None => (s, ""),
};
if !offset.is_empty() && (parse_any_offset_minutes(offset).is_none() || !valid_offset_fraction(offset)) {
return None;
}
parse_plain_clock(clock)
}
/// ToTemporalTime:PlainTime 对象 / ZDT / PlainDateTime / 字符串 / property bag → 当日纳秒。
///
/// # 步骤
/// 1. undefined → 0(午夜);字符串走 parse_plain_time_string(失败 RangeError)。
/// 2. PlainTime 读槽 0;ZDT 用其自身时区取本地时间;PlainDateTime 读时间槽。
/// 3. 其余对象按 ToTemporalTimeRecord 读时间字段(无字段 TypeError)。
///
/// # 边界与前提
/// - 非字符串原始值(number/bigint/null/boolean)→ TypeError;Symbol → TypeError。
/// - bag 字段缺省 0(完整模式),越界值按 constrain 钳制(second=60 → 59)。
fn plain_time_like_ns<H: VmHost>(vm: &mut H, value: JsValue) -> Result<f64, JsValue> {
if value.is_undefined() {
return Ok(0.0);
}
if value.is_string() {
return parse_plain_time_string(&to_string(value))
.ok_or_else(|| crate::error::create_range_error(vm, "invalid ISO 8601 time"));
}
if value.is_object() {
let ptr = value.as_js_object_ptr();
if !ptr.is_null() {
let obj = unsafe { &*ptr };
if obj.is_plain_time_obj() {
return Ok(get_double_prop(obj, 0));
}
if obj.is_zoned_date_time_obj() {
return zoned_date_time_plain_parts(vm, obj).map(|(_, _, _, time_ns)| time_ns);
}
if obj.is_plain_date_time_obj() {
return Ok(get_double_prop(obj, 3));
}
return plain_time_bag_ns(vm, value, obj);
}
}
Err(crate::error::create_type_error(vm, "cannot convert to PlainTime"))
}
/// ToTemporalTimeRecord(完整模式):按字母序读时间字段,缺省 0,无字段 TypeError。
fn plain_time_bag_ns<H: VmHost>(vm: &mut H, value: JsValue, obj: &JsObject) -> Result<f64, JsValue> {
let hour_raw = temporal_option_value(vm, obj, value, "hour")?;
let microsecond_raw = temporal_option_value(vm, obj, value, "microsecond")?;
let millisecond_raw = temporal_option_value(vm, obj, value, "millisecond")?;
let minute_raw = temporal_option_value(vm, obj, value, "minute")?;
let nanosecond_raw = temporal_option_value(vm, obj, value, "nanosecond")?;
let second_raw = temporal_option_value(vm, obj, value, "second")?;
if [hour_raw, microsecond_raw, millisecond_raw, minute_raw, nanosecond_raw, second_raw]
.iter()
.all(|raw| raw.is_undefined())
{
return Err(crate::error::create_type_error(vm, "no time units present"));
}
let mut convert = |raw: JsValue| -> Result<f64, JsValue> {
if raw.is_undefined() {
Ok(0.0)
} else {
let number = temporal_option_number(vm, raw)?;
if !number.is_finite() {
return Err(crate::error::create_range_error(vm, "invalid time component"));
}
Ok(number.trunc())
}
};
let hour = convert(hour_raw)?;
let microsecond = convert(microsecond_raw)?;
let millisecond = convert(millisecond_raw)?;
let minute = convert(minute_raw)?;
let nanosecond = convert(nanosecond_raw)?;
let second = convert(second_raw)?;
// RegulateTime(constrain):越界钳制(second=60 → 59)。
let hour = hour.clamp(0.0, 23.0);
let minute = minute.clamp(0.0, 59.0);
let second = second.clamp(0.0, 59.0);
let millisecond = millisecond.clamp(0.0, 999.0);
let microsecond = microsecond.clamp(0.0, 999.0);
let nanosecond = nanosecond.clamp(0.0, 999.0);
Ok(hour * 3_600_000_000_000.0
+ minute * 60_000_000_000.0
+ second * 1_000_000_000.0
+ millisecond * 1_000_000.0
+ microsecond * 1_000.0
+ nanosecond)
}
fn make_plain_date<H: VmHost>(vm: &mut H, year: i32, month: u32, day: u32, calendar: &str) -> NativeResult {
let proto = JsValue::from_js_object(vm.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<H: VmHost>(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<H: VmHost>(
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<H: VmHost>(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)))
}
/// 校验 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<i128> {
let days = days_from_civil(i128::from(year), i128::from(month), i128::from(day));
days.checked_mul(86_400_000_000_000)?.checked_add(time_ns as i128)
}
/// 校验日期时间落在 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<u32, String> {
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<H: VmHost>(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'",
));
}
// 分量转换序:year → month → day;缺参归 undefined 经 number-only 路径
// 统一落 RangeError(日期分量无缺省值,缺参不是合法分量)。
let year = native_try!(temporal_number_component(
vm,
if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() },
)) as i32;
let month = native_try!(temporal_number_component(
vm,
if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() },
)) as u32;
let day = native_try!(temporal_number_component(
vm,
if args.len() > 3 { vm.reg(args[3]) } else { JsValue::undefined() },
)) as u32;
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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_date(vm, obj));
let year = get_double_prop(obj, 0) as i32;
let month = get_double_prop(obj, 1) as u32;
let day = get_double_prop(obj, 2) as u32;
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<H: VmHost>(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'",
));
}
// 时间分量:缺参或显式 undefined 取默认 0(undefined 是合法分量,与 PD
// 缺参抛错相反);其余经 number-only 路径(Symbol/BigInt → TypeError,
// NaN/±Inf/不可解析串 → RangeError)。
let mut get = |i: usize| -> Result<f64, JsValue> {
let raw = if args.len() > i { vm.reg(args[i]) } else { JsValue::undefined() };
if raw.is_undefined() {
return Ok(0.0);
}
temporal_number_component(vm, raw)
};
// 负值在 u32 转换前检查:截断后为负的整分量(含负分数整数化为负)须先抛错,
// 不能让 f64→u32 饱和把其静默归 0 而逃过范围检查。
let hour_value = native_try!(get(1));
let minute_value = native_try!(get(2));
let second_value = native_try!(get(3));
let ms_value = native_try!(get(4));
let us_value = native_try!(get(5));
let ns_value = native_try!(get(6));
if [hour_value, minute_value, second_value, ms_value, us_value, ns_value]
.iter()
.any(|value| *value < 0.0)
{
return NativeResult::Err(crate::error::create_range_error(vm, "invalid time component"));
}
let hour = hour_value as u32;
let minute = minute_value as u32;
let second = second_value as u32;
let ms = ms_value as u32;
let us = us_value as u32;
let ns = ns_value 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<H: VmHost>(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<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_time_get(vm, args, |h, _, _, _, _, _| h)
}
/// `Temporal.PlainTime.prototype.minute` getter。
pub fn plain_time_minute<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_time_get(vm, args, |_, m, _, _, _, _| m)
}
/// `Temporal.PlainTime.prototype.second` getter。
pub fn plain_time_second<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_time_get(vm, args, |_, _, s, _, _, _| s)
}
/// `Temporal.PlainTime.prototype.millisecond` getter。
pub fn plain_time_millisecond<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_time_get(vm, args, |_, _, _, ms, _, _| ms)
}
/// `Temporal.PlainTime.prototype.microsecond` getter。
pub fn plain_time_microsecond<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_time_get(vm, args, |_, _, _, _, us, _| us)
}
/// `Temporal.PlainTime.prototype.nanosecond` getter。
pub fn plain_time_nanosecond<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_time_get(vm, args, |_, _, _, _, _, ns| ns)
}
/// `Temporal.PlainTime.prototype.with(temporalTimeLike [, options])`:替换
/// receiver 的指定时间分量,返回新 PlainTime。
///
/// # 步骤
/// 1. branding + ToTemporalTimeLike(非对象 / Temporal 实例 / calendar、
/// timeZone 有定义 → TypeError;字段按字典序读 hour → microsecond →
/// millisecond → minute → nanosecond → second,非数字 → TypeError,
/// NaN/±Inf → RangeError)。
/// 2. 分量全 undefined → TypeError。
/// 3. options → overflow(缺省 constrain)。
/// 4. RegulateTime:constrain 钳制;reject 先校验范围再截断(负 / 超界分量
/// 立即抛 RangeError)。
/// 5. 合并分量算午夜后纳秒,构造新 PlainTime。
///
/// # 边界与前提
/// - 字段读取先于选项解析(options-wrong-type 先报字段错误,与 ZDT with 同口径)。
/// - receiver 分量取自午夜后纳秒槽;partial 中 undefined 的分量保留 receiver 值。
pub fn plain_time_with<H: VmHost>(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 value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
native_try!(reject_partial_object_with_calendar_or_time_zone(vm, value));
// 字段读取按字典序,undefined 不覆盖;number-only 路径(Symbol/BigInt →
// TypeError,NaN/±Inf → RangeError)。
let bag = unsafe { &*value.as_js_object_ptr() };
let read = |vm: &mut H, name: &str| -> Result<Option<f64>, JsValue> {
let raw = temporal_option_value(vm, bag, value, name)?;
if raw.is_undefined() {
Ok(None)
} else {
Ok(Some(temporal_number_component(vm, raw)?))
}
};
let hour = native_try!(read(vm, "hour"));
let microsecond = native_try!(read(vm, "microsecond"));
let millisecond = native_try!(read(vm, "millisecond"));
let minute = native_try!(read(vm, "minute"));
let nanosecond = native_try!(read(vm, "nanosecond"));
let second = native_try!(read(vm, "second"));
if [hour, microsecond, millisecond, minute, nanosecond, second]
.iter()
.all(|c| c.is_none())
{
return NativeResult::Err(crate::error::create_type_error(vm, "no properties present"));
}
// options:字段读取完成后解析 overflow。
let constrain = native_try!(temporal_overflow(vm, args));
// RegulateTime:partial 有定义的分量覆盖 receiver 分量。
let (recv_hour, recv_minute, recv_second, recv_ms, recv_us, recv_ns) =
plain_time_components(get_double_prop(obj, 0));
let merged = [
hour.unwrap_or(recv_hour as f64),
minute.unwrap_or(recv_minute as f64),
second.unwrap_or(recv_second as f64),
millisecond.unwrap_or(recv_ms as f64),
microsecond.unwrap_or(recv_us as f64),
nanosecond.unwrap_or(recv_ns as f64),
];
let values = if constrain {
[
merged[0].clamp(0.0, 23.0),
merged[1].clamp(0.0, 59.0),
merged[2].clamp(0.0, 59.0),
merged[3].clamp(0.0, 999.0),
merged[4].clamp(0.0, 999.0),
merged[5].clamp(0.0, 999.0),
]
} else {
// reject 语义须先做范围校验再截断:负分量或超上限分量立即抛错,
// 不允许 f64→u32 饱和把负值静默归 0 而逃过范围检查。
let limits = [23.0, 59.0, 59.0, 999.0, 999.0, 999.0];
if merged.iter().zip(limits.iter()).any(|(v, limit)| *v < 0.0 || *v > *limit) {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid time component"));
}
merged
};
let total_ns = values[0] * 3_600_000_000_000.0
+ values[1] * 60_000_000_000.0
+ values[2] * 1_000_000_000.0
+ values[3] * 1_000_000.0
+ values[4] * 1_000.0
+ values[5];
make_plain_time(vm, total_ns)
}
/// 按是否含秒与小数位格式化当日纳秒为 `HH:MM:SS[.frac]`。
///
/// # 边界与前提
/// - `include_seconds=false` 时忽略小数位,仅输出 `HH:MM`。
/// - `Some(0)` 不输出小数;`Some(digits)` 固定补足位数;`None` 按实际非零亚秒去尾零。
fn format_plain_time_iso(time_ns: i128, include_seconds: bool, fractional_digits: Option<usize>) -> 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!("{hour:02}:{minute:02}");
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 => {}
}
}
output
}
/// `Temporal.PlainTime.prototype.toString(options)`:按精度、舍入模式与最小单位输出 ISO 8601 时间。
pub fn plain_time_to_string<H: VmHost>(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 total_ns = get_double_prop(obj, 0) as i128;
let options_value = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) };
let (fractional_input, mode_value, smallest_value) = if options_value.is_undefined() {
(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 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)))
};
(fractional, mode, smallest)
};
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),
},
};
// 舍入后可能跨到次日(rounding-cross-midnight),取模保持 0-24 域。
const DAY_NS: i128 = 86_400_000_000_000;
let Some(rounded_ns) = round_instant_ns(total_ns, quantum_ns, mode) else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid time"));
};
let time_ns = rounded_ns.rem_euclid(DAY_NS);
let output = format_plain_time_iso(time_ns, include_seconds, output_digits);
NativeResult::Ok(vm.new_string_owned(output))
}
/// `Temporal.PlainTime.prototype.toJSON()`:输出默认 ISO 时间字符串,忽略参数。
pub fn plain_time_to_json<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_time_default_string(vm, args)
}
/// `Temporal.PlainTime.prototype.toLocaleString()`:当前使用稳定的默认 ISO 时间表示。
pub fn plain_time_to_locale_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_time_default_string(vm, args)
}
/// 输出无 options 的默认 PlainTime 串(全秒 + 非零亚秒)。
fn plain_time_default_string<H: VmHost>(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 total_ns = get_double_prop(obj, 0) as i128;
NativeResult::Ok(vm.new_string_owned(format_plain_time_iso(total_ns, true, None)))
}
/// `Temporal.PlainTime.prototype.valueOf()`:Temporal 对象禁止转原始值。
pub fn plain_time_value_of<H: VmHost>(vm: &mut H, _args: &[u8]) -> NativeResult {
NativeResult::Err(crate::error::create_type_error(vm, "Temporal.PlainTime has no valueOf"))
}
/// `Temporal.PlainTime.from(item, options)`:从字符串、PlainTime/ZDT/PlainDateTime 实例或 property bag 构造。
pub fn plain_time_from<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
if value.is_string() {
// 规范顺序:先 ParseTemporalTimeString,再 ToTemporalOverflow(options)。
let total_ns = match parse_plain_time_string(&to_string(value)) {
Some(ns) => ns,
None => {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO 8601 time"));
}
};
native_try!(temporal_overflow(vm, args));
make_plain_time(vm, total_ns)
} else {
// 对象分支:实例复制 / property bag 统一走 plain_time_like_ns(含缺省 0 与约束钳制)。
native_try!(temporal_overflow(vm, args));
let total_ns = native_try!(plain_time_like_ns(vm, value));
make_plain_time(vm, total_ns)
}
}
/// `Temporal.PlainTime.prototype.equals(other)`:各分量全等返回 true,否则 false。
///
/// # 边界与前提
/// - other 经 `plain_time_like_ns` 归一(实例/字符串/property bag),无法转换抛 TypeError/RangeError。
pub fn plain_time_equals<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = match receiver_obj(vm, args) {
Ok(p) => p,
Err(error) => return NativeResult::Err(error),
};
let obj = unsafe { &*ptr };
if let Err(error) = ensure_plain_time(vm, obj) {
return NativeResult::Err(error);
}
let receiver_ns = get_double_prop(obj, 0);
let other_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let other_ns = match plain_time_like_ns(vm, other_val) {
Ok(ns) => ns,
Err(error) => return NativeResult::Err(error),
};
NativeResult::Ok(JsValue::bool(receiver_ns as i128 == other_ns as i128))
}
/// `Temporal.PlainTime.compare(one, two)`:静态比较,按午夜后纳秒返回 -1、0 或 1。
pub fn plain_time_compare<H: VmHost>(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_ns = match plain_time_like_ns(vm, one) {
Ok(ns) => ns,
Err(error) => return NativeResult::Err(error),
};
let two_ns = match plain_time_like_ns(vm, two) {
Ok(ns) => ns,
Err(error) => return NativeResult::Err(error),
};
let result = match (one_ns as i128).cmp(&(two_ns as i128)) {
std::cmp::Ordering::Less => -1,
std::cmp::Ordering::Equal => 0,
std::cmp::Ordering::Greater => 1,
};
NativeResult::Ok(JsValue::int(result))
}
// ───────────────────── PlainTime until / since ─────────────────────
/// until/since 核心:receiver 与 other 归一为午夜后纳秒,经 `difference_core` 计算差值。
///
/// # 步骤
/// 1. branding receiver 取槽 0 ns,other 经 `plain_time_like_ns` 归一。
/// 2. `parse_difference_settings` 解析 options(default_largest=4=hour)。
/// 3. 日期固定为 epoch(0,0,0),调 `difference_core` 输出 Duration。
///
/// # 边界与前提
/// - since 复用 same 语义并整体取反(difference_core 内部处理),不调换两端。
fn plain_time_difference<H: VmHost>(vm: &mut H, args: &[u8], since: bool) -> NativeResult {
let ptr = match receiver_obj(vm, args) {
Ok(p) => p,
Err(error) => return NativeResult::Err(error),
};
let obj = unsafe { &*ptr };
if let Err(error) = ensure_plain_time(vm, obj) {
return NativeResult::Err(error);
}
let receiver_ns = get_double_prop(obj, 0);
let other_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let other_ns = match plain_time_like_ns(vm, other_val) {
Ok(ns) => ns,
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, 4) {
Ok(settings) => settings,
Err(error) => return NativeResult::Err(error),
};
difference_core(vm, (0, 0, 0), receiver_ns as i128, (0, 0, 0), other_ns as i128, settings, since, 9)
}
/// `Temporal.PlainTime.prototype.until(other, options)`。
pub fn plain_time_until<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_time_difference(vm, args, false)
}
/// `Temporal.PlainTime.prototype.since(other, options)`。
pub fn plain_time_since<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_time_difference(vm, args, true)
}
// ───────────────────── PlainTime add / subtract ─────────────────────
/// add/subtract 核心:receiver 午夜后纳秒叠加时长的"时间域"增量。
///
/// # 步骤
/// 1. branding receiver 取槽 0 ns。
/// 2. `duration_like_values` 归一 duration,`temporal_overflow` 解析 options(读序:duration → options)。
/// 3. 仅取 hours 起的时间字段换算纳秒增量(日期字段 days 及以上规范忽略)。
/// 4. 按 sign 叠加后 `rem_euclid(DAY_NS)` 保持 0-24 域 → `make_plain_time`。
///
/// # 边界与前提
/// - duration 全 0 → 值不变的新对象(blank-duration 语义)。
/// - duration 含日期字段(y/m/w/d)不报错,规范对 PlainTime 直接忽略。
fn plain_time_apply_duration<H: VmHost>(vm: &mut H, args: &[u8], sign: i64) -> NativeResult {
let ptr = match receiver_obj(vm, args) {
Ok(p) => p,
Err(error) => return NativeResult::Err(error),
};
let obj = unsafe { &*ptr };
if let Err(error) = ensure_plain_time(vm, obj) {
return NativeResult::Err(error);
}
let time_ns = get_double_prop(obj, 0) as i128;
let duration_like = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let values = match duration_like_values(vm, duration_like) {
Ok(values) => values,
Err(error) => return NativeResult::Err(error),
};
if let Err(error) = temporal_overflow(vm, args) {
return NativeResult::Err(error);
}
const DAY_NS: i128 = 86_400_000_000_000;
// 时间字段(hours 起)单独换算纳秒;日期字段 days 及以上对 PlainTime 无意义直接忽略。
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);
// 时间溢出跨午夜:rem_euclid 保持 0-24 域。
let new_time_ns = (time_ns + time_delta * sign as i128).rem_euclid(DAY_NS);
make_plain_time(vm, new_time_ns as f64)
}
/// `Temporal.PlainTime.prototype.add(durationLike, options)`。
pub fn plain_time_add<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_time_apply_duration(vm, args, 1)
}
/// `Temporal.PlainTime.prototype.subtract(durationLike, options)`。
pub fn plain_time_subtract<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_time_apply_duration(vm, args, -1)
}
// ───────────────────── PlainTime round ─────────────────────
/// PlainTime round 的最小单位表:小时..纳秒(不含 day)。
/// 返回 (单位纳秒, 最大增量)。最大增量 = MaximumTemporalDurationRoundingIncrement:
/// hour→24、minute→60、second→60、millisecond→1000、microsecond→1000、nanosecond→1000。
fn plain_time_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, 60)),
"second" | "seconds" => Some((1_000_000_000, 60)),
"millisecond" | "milliseconds" => Some((1_000_000, 1_000)),
"microsecond" | "microseconds" => Some((1_000, 1_000)),
"nanosecond" | "nanoseconds" => Some((1, 1_000)),
_ => None,
}
}
/// `Temporal.PlainTime.prototype.round(roundTo)`:按最小单位、增量和模式在"当日"域内舍入。
///
/// # 步骤
/// 1. roundTo 解析:undefined → TypeError;字符串 → {smallestUnit: 串};对象 → 依次 Get
/// roundingIncrement → roundingMode → smallestUnit(读序对齐 order-of-operations)。
/// 2. branding receiver 取槽 0 ns。
/// 3. 单位表查 smallestUnit(hour..nanosecond;None → RangeError "invalid smallest unit")。
/// 4. roundingIncrement 校验:1..=1e9 且 **真因子**(increment < max_increment 且
/// max_increment % increment == 0,否则 RangeError)。
/// 5. mode = instant_rounding_mode(None → RangeError);缺省 "halfExpand"。
/// 6. round_instant_ns(time_ns, unit_ns*increment, mode),结果 rem_euclid(DAY_NS) 保持 0-24 域。
///
/// # 边界与前提
/// - 舍入跨午夜(如 23:59:59.9 舍入到秒)→ 取模回 00:00,符合 rounding-cross-midnight 语义。
pub fn plain_time_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let round_to = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
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 ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_time(vm, obj));
let time_ns = get_double_prop(obj, 0) as i128;
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, max_increment)) = plain_time_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;
// 真因子校验:增量须严格小于最大增量且能整除(MaximumTemporalDurationRoundingIncrement
// 语义;instant_round 只查 % 的缺陷不在此沿用)。
if increment >= max_increment || max_increment % 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(time_ns, quantum_ns, mode) else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid time"));
};
const DAY_NS: i128 = 86_400_000_000_000;
let time_ns = rounded_ns.rem_euclid(DAY_NS);
make_plain_time(vm, time_ns as f64)
}
// ───────────────────── PlainDateTime 基础方法 ─────────────────────
/// `Temporal.PlainDateTime` 构造器:保存 ISO 日期与午夜后纳秒。
pub fn plain_date_time_constructor<H: VmHost>(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<f64, JsValue> {
if args.len() > index {
let raw = vm.reg(args[index]);
// 时间分量(缺省臂为有限值):显式 undefined 与缺参同义,取缺省值;
// 日期分量无缺省臂(缺省值为 NaN),undefined 仍经 NaN 统一抛
// RangeError。
if raw.is_undefined() && default.is_finite() {
return Ok(default);
}
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 parse_plain_date_time_string(input: &str) -> Result<(i32, u32, u32, f64), String> {
parse_temporal_string_impl(input, true)
}
fn plain_date_time_like_parts<H: VmHost>(
vm: &mut H, value: JsValue, constrain: bool,
) -> Result<(i32, u32, u32, f64, Option<String>), 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)
}
/// `Temporal.PlainDateTime.from(item)`:从实例、ISO 字符串或字段对象创建副本。
pub fn plain_date_time_from<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<NaiveDate, JsValue> {
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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(
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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_date_time(vm, obj));
NativeResult::Ok(vm.new_string(&get_calendar_id(obj, 4)))
}
/// 返回仅保留日期分量的新 `Temporal.PlainDate`,日历从 receiver 槽继承。
pub fn plain_date_time_to_plain_date<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<usize>,
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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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))
}
/// 计算差值并舍入。until 返回 other 减 receiver,since 返回反向。
fn plain_date_time_difference<H: VmHost>(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, 3) {
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,
9,
)
}
/// `Temporal.PlainDateTime.prototype.until(other, options)`。
pub fn plain_date_time_until<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<NaiveDate, JsValue> {
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<H: VmHost>(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<H: VmHost>(
vm: &mut H, val: JsValue, constrain: bool,
) -> Result<(i32, u32, u32, Option<String>), 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<H: VmHost>(vm: &mut H, val: JsValue) -> Result<(i32, u32, u32, Option<String>), 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<H: VmHost>(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<H: VmHost>(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 {
if let Some(next) = NaiveDate::from_ymd_opt(year, month + 1, 1) {
if let Some(prev) = next.checked_sub_days(Days::new(1)) {
return prev.day();
}
}
// 年越 chrono 表示范围(PMD from 的 bag year 可任意 i32):直接按 ISO 规则。
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
_ => {
// 闰年判定须用数学取模(负年),与 chrono 域内结果一致。
if year.rem_euclid(4) == 0 && (year.rem_euclid(100) != 0 || year.rem_euclid(400) == 0) {
29
} else {
28
}
}
}
}
/// `Temporal.PlainDate.prototype.daysInMonth` getter。
pub fn plain_date_days_in_month<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_date(vm, obj));
NativeResult::Ok(vm.new_string(&get_calendar_id(obj, 3)))
}
/// `Temporal.PlainDate.prototype.equals(other)`:比较年月日是否相等。
pub fn plain_date_equals<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
date_apply_duration(vm, args, 1)
}
/// `Temporal.PlainDate.prototype.subtract(durationLike)`。
pub fn plain_date_subtract<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
date_apply_duration(vm, args, -1)
}
fn add_signed_months(date: NaiveDate, months: i64) -> Option<NaiveDate> {
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<H: VmHost>(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, 3) {
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,
9,
)
}
/// `Temporal.PlainDate.prototype.since(other, options)`:date-only 单位差值 + 舍入。
pub fn plain_date_since<H: VmHost>(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, 3) {
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,
9,
)
}
// Temporal.PlainMonthDay / Temporal.PlainYearMonth 对象地基:
// 数字分量转换、槽对象构造、构造器与 getter/toString/toJSON。
/// 构造 PlainMonthDay 实例对象(槽 0-3 = 月/日/参考年/日历 ID)。
/// from/toPlainDate 等返回新对象的成员使用;构造器走 receiver 初始化路径。
fn make_plain_month_day<H: VmHost>(vm: &mut H, month: u32, day: u32, ref_year: i32, calendar: &str) -> NativeResult {
let proto = JsValue::from_js_object(vm.session().builtin_world().plain_month_day_proto.as_ptr() as *mut JsObject);
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto);
obj.type_tag = JsObject::OBJ_TYPE_PLAIN_MONTH_DAY;
obj.set_prop_at(0, JsValue::float(month as f64));
obj.set_prop_at(1, JsValue::float(day as f64));
obj.set_prop_at(2, JsValue::float(ref_year as f64));
obj.set_prop_at(3, vm.new_string(calendar));
NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj)))
}
/// 构造 PlainYearMonth 实例对象(槽 0-3 = 年/月/参考日/日历 ID)。
/// from 等返回新对象的成员使用;构造器走 receiver 初始化路径。
fn make_plain_year_month<H: VmHost>(vm: &mut H, year: i32, month: u32, ref_day: u32, calendar: &str) -> NativeResult {
let proto = JsValue::from_js_object(vm.session().builtin_world().plain_year_month_proto.as_ptr() as *mut JsObject);
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto);
obj.type_tag = JsObject::OBJ_TYPE_PLAIN_YEAR_MONTH;
obj.set_prop_at(0, JsValue::float(year as f64));
obj.set_prop_at(1, JsValue::float(month as f64));
obj.set_prop_at(2, JsValue::float(ref_day as f64));
obj.set_prop_at(3, vm.new_string(calendar));
NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj)))
}
fn ensure_plain_month_day<H: VmHost>(vm: &mut H, obj: &JsObject) -> Result<(), JsValue> {
if !obj.is_plain_month_day_obj() {
return Err(crate::error::create_type_error(vm, "called on incompatible receiver"));
}
Ok(())
}
fn ensure_plain_year_month<H: VmHost>(vm: &mut H, obj: &JsObject) -> Result<(), JsValue> {
if !obj.is_plain_year_month_obj() {
return Err(crate::error::create_type_error(vm, "called on incompatible receiver"));
}
Ok(())
}
/// 读 PlainMonthDay 的 月/日/参考年 三元组(槽 0-2),含 receiver 校验。
fn plain_month_day_mdy<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<(f64, f64, f64), JsValue> {
let ptr = receiver_obj(vm, args)?;
let obj = unsafe { &*ptr };
ensure_plain_month_day(vm, obj)?;
Ok((get_double_prop(obj, 0), get_double_prop(obj, 1), get_double_prop(obj, 2)))
}
/// 读 PlainYearMonth 的 年/月/参考日 三元组(槽 0-2),含 receiver 校验。
fn plain_year_month_ymd<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<(f64, f64, f64), JsValue> {
let ptr = receiver_obj(vm, args)?;
let obj = unsafe { &*ptr };
ensure_plain_year_month(vm, obj)?;
Ok((get_double_prop(obj, 0), get_double_prop(obj, 1), get_double_prop(obj, 2)))
}
/// 构造器日历参数解析(ToTemporalCalendarSlotValue,缺省 "iso8601"):
/// undefined → 缺省;字符串 → 严格白名单(非法 RangeError);Temporal 日期实例 →
/// 日历槽直读(不触发属性 getter);函数对象 → 缺省(无日历行为);
/// 其他对象与原始值 → TypeError。
fn temporal_constructor_calendar_id<H: VmHost>(vm: &mut H, value: JsValue) -> Result<String, JsValue> {
if value.is_undefined() {
return Ok("iso8601".to_string());
}
if value.is_string() {
return temporal_calendar_id_strict(vm, value)
.map(|calendar| calendar.unwrap_or_else(|| "iso8601".to_string()));
}
if value.is_object() {
let ptr = value.as_js_object_ptr();
if !ptr.is_null() {
let obj = unsafe { &*ptr };
if obj.is_function() {
return Ok("iso8601".to_string());
}
let slot = if obj.is_plain_date_obj() {
3
} else if obj.is_plain_date_time_obj() {
4
} else if obj.is_zoned_date_time_obj() {
2
} else if obj.is_plain_month_day_obj() || obj.is_plain_year_month_obj() {
3
} else {
return Err(crate::error::create_type_error(vm, "invalid calendar"));
};
return Ok(get_calendar_id(obj, slot));
}
}
Err(crate::error::create_type_error(vm, "invalid calendar"))
}
/// `Temporal.PlainMonthDay` 构造器:`new PlainMonthDay(month, day[, calendar[, referenceISOYear]])`。
pub fn plain_month_day_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ctor_proto = vm.session().builtin_world().plain_month_day_proto.as_ptr();
if !is_ctor_call(vm, args, ctor_proto) {
return NativeResult::Err(crate::error::create_type_error(
vm,
"Class constructor Temporal.PlainMonthDay cannot be invoked without 'new'",
));
}
// 转换序:month → day → calendar → referenceISOYear(后两者有缺省值)。
let month = native_try!(temporal_number_component(
vm,
if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() },
));
let day = native_try!(temporal_number_component(
vm,
if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() },
));
let calendar_raw = if args.len() > 3 { vm.reg(args[3]) } else { JsValue::undefined() };
let calendar = native_try!(temporal_constructor_calendar_id(vm, calendar_raw));
let ref_year = if args.len() > 4 && !vm.reg(args[4]).is_undefined() {
native_try!(temporal_number_component(vm, vm.reg(args[4]))) as i32
} else {
1972
};
if !valid_iso_date(ref_year, month as u32, day as u32) {
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(ref_year), i128::from(month as i32), i128::from(day as i32));
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_value = vm.new_string(&calendar);
initialize_temporal_receiver(
vm,
args,
JsObject::OBJ_TYPE_PLAIN_MONTH_DAY,
[
JsValue::float(month),
JsValue::float(day),
JsValue::float(ref_year as f64),
calendar_value,
],
)
}
/// `Temporal.PlainYearMonth` 构造器:`new PlainYearMonth(year, month[, calendar[, referenceISODay]])`。
pub fn plain_year_month_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ctor_proto = vm.session().builtin_world().plain_year_month_proto.as_ptr();
if !is_ctor_call(vm, args, ctor_proto) {
return NativeResult::Err(crate::error::create_type_error(
vm,
"Class constructor Temporal.PlainYearMonth cannot be invoked without 'new'",
));
}
// 转换序:year → month → calendar → referenceISODay(后两者有缺省值)。
let year = native_try!(temporal_number_component(
vm,
if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() },
)) as i32;
let month = native_try!(temporal_number_component(
vm,
if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() },
));
let calendar_raw = if args.len() > 3 { vm.reg(args[3]) } else { JsValue::undefined() };
let calendar = native_try!(temporal_constructor_calendar_id(vm, calendar_raw));
let ref_day = if args.len() > 4 && !vm.reg(args[4]).is_undefined() {
native_try!(temporal_number_component(vm, vm.reg(args[4])))
} else {
1.0
};
let month_i = month as i32;
if !(1..=12).contains(&month_i) {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO date"));
}
let ref_day_i = ref_day as i32;
if ref_day_i < 1 || ref_day_i > days_in_month_iso(year, month_i as u32) as i32 {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO date"));
}
if !iso_year_month_within_limits(year, month_i as u32) {
return NativeResult::Err(crate::error::create_range_error(vm, "ISO date is out of range"));
}
let calendar_value = vm.new_string(&calendar);
initialize_temporal_receiver(
vm,
args,
JsObject::OBJ_TYPE_PLAIN_YEAR_MONTH,
[
JsValue::float(year as f64),
JsValue::float(month),
JsValue::float(ref_day),
calendar_value,
],
)
}
/// `Temporal.PlainMonthDay.prototype.day` getter(槽 1)。
pub fn plain_month_day_day<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let (_, day, _) = native_try!(plain_month_day_mdy(vm, args));
NativeResult::Ok(JsValue::float(day))
}
/// `Temporal.PlainMonthDay.prototype.monthCode` getter:`M01`..`M12`。
pub fn plain_month_day_month_code<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let (month, _, _) = native_try!(plain_month_day_mdy(vm, args));
NativeResult::Ok(vm.new_string(&format!("M{:02}", month as u32)))
}
/// `Temporal.PlainMonthDay.prototype.calendarId` getter:读日历槽(槽 3)。
pub fn plain_month_day_calendar_id<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_month_day(vm, obj));
NativeResult::Ok(vm.new_string(&get_calendar_id(obj, 3)))
}
/// `Temporal.PlainYearMonth.prototype.year` getter(槽 0)。
pub fn plain_year_month_year<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let (year, _, _) = native_try!(plain_year_month_ymd(vm, args));
NativeResult::Ok(JsValue::float(year))
}
/// `Temporal.PlainYearMonth.prototype.month` getter(槽 1)。
pub fn plain_year_month_month<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let (_, month, _) = native_try!(plain_year_month_ymd(vm, args));
NativeResult::Ok(JsValue::float(month))
}
/// `Temporal.PlainYearMonth.prototype.monthCode` getter:`M01`..`M12`。
pub fn plain_year_month_month_code<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let (_, month, _) = native_try!(plain_year_month_ymd(vm, args));
NativeResult::Ok(vm.new_string(&format!("M{:02}", month as u32)))
}
/// `Temporal.PlainYearMonth.prototype.calendarId` getter:读日历槽(槽 3)。
pub fn plain_year_month_calendar_id<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_year_month(vm, obj));
NativeResult::Ok(vm.new_string(&get_calendar_id(obj, 3)))
}
/// `Temporal.PlainYearMonth.prototype.daysInMonth` getter:按年月的 ISO 月长。
pub fn plain_year_month_days_in_month<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let (year, month, _) = native_try!(plain_year_month_ymd(vm, args));
NativeResult::Ok(JsValue::float(days_in_month_iso(year as i32, month as u32) as f64))
}
/// `Temporal.PlainYearMonth.prototype.daysInYear` getter:366(闰年)或 365。
pub fn plain_year_month_days_in_year<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let (year, _, _) = native_try!(plain_year_month_ymd(vm, args));
NativeResult::Ok(JsValue::float(if is_leap_year_iso(year as i32) { 366.0 } else { 365.0 }))
}
/// `Temporal.PlainYearMonth.prototype.monthsInYear` getter:恒 12(ISO 日历)。
pub fn plain_year_month_months_in_year<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let _ = native_try!(plain_year_month_ymd(vm, args));
NativeResult::Ok(JsValue::float(12.0))
}
/// `Temporal.PlainYearMonth.prototype.inLeapYear` getter。
pub fn plain_year_month_in_leap_year<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let (year, _, _) = native_try!(plain_year_month_ymd(vm, args));
NativeResult::Ok(JsValue::bool(is_leap_year_iso(year as i32)))
}
/// `Temporal.PlainYearMonth.prototype.era` getter:ISO 日历无纪元,恒 undefined。
pub fn plain_year_month_era<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let _ = native_try!(plain_year_month_ymd(vm, args));
NativeResult::Ok(JsValue::undefined())
}
/// `Temporal.PlainYearMonth.prototype.eraYear` getter:ISO 日历无纪元,恒 undefined。
pub fn plain_year_month_era_year<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let _ = native_try!(plain_year_month_ymd(vm, args));
NativeResult::Ok(JsValue::undefined())
}
/// PlainMonthDay ISO 串:always/critical 或非 iso8601 日历补参考年与日历注解;
/// iso8601 日历的 auto/never 形保持裸 `MM-DD`。
fn plain_month_day_iso_string(obj: &JsObject, show: ShowCalendar) -> String {
let month = get_double_prop(obj, 0) as i32;
let day = get_double_prop(obj, 1) as i32;
let calendar = get_calendar_id(obj, 3);
let with_year = matches!(show, ShowCalendar::Always | ShowCalendar::Critical) || calendar != "iso8601";
if !with_year {
return format!("{month:02}-{day:02}");
}
let ref_year = get_double_prop(obj, 2) as i32;
let annotation = calendar_annotation(&calendar, show);
format!("{}-{month:02}-{day:02}{annotation}", format_iso_year(ref_year as i128))
}
/// PlainMonthDay 默认形串(auto 形),含 receiver 校验,不读 options。
fn plain_month_day_default_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_month_day(vm, obj));
NativeResult::Ok(vm.new_string_owned(plain_month_day_iso_string(obj, ShowCalendar::Auto)))
}
/// `Temporal.PlainMonthDay.prototype.toString([options])`:
/// 默认 `MM-DD`;非 iso8601 日历补参考年与 `[u-ca=…]` 注解,
/// always/critical 另按注解表补 `[u-ca=…]`/`[!u-ca=…]`。
pub fn plain_month_day_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_month_day(vm, obj));
let show = native_try!(temporal_to_show_calendar(vm, args));
NativeResult::Ok(vm.new_string_owned(plain_month_day_iso_string(obj, show)))
}
/// `Temporal.PlainMonthDay.prototype.toJSON()`:默认形串,忽略参数。
pub fn plain_month_day_to_json<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_month_day_default_string(vm, args)
}
/// `Temporal.PlainMonthDay.prototype.toLocaleString()`:locale/options 无引擎行为,
/// 恒输出默认形串。
pub fn plain_month_day_to_locale_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_month_day_default_string(vm, args)
}
/// `Temporal.PlainMonthDay.prototype.valueOf()`:Temporal 对象无值表示,恒 TypeError。
pub fn plain_month_day_value_of<H: VmHost>(vm: &mut H, _args: &[u8]) -> NativeResult {
NativeResult::Err(crate::error::create_type_error(vm, "Temporal.PlainMonthDay has no valueOf"))
}
/// PlainYearMonth ISO 串:always/critical 或非 iso8601 日历补参考日与日历注解;
/// iso8601 日历的 auto/never 形保持裸 `±YYYY-MM`。
fn plain_year_month_iso_string(obj: &JsObject, show: ShowCalendar) -> String {
let year = get_double_prop(obj, 0) as i32;
let month = get_double_prop(obj, 1) as i32;
let calendar = get_calendar_id(obj, 3);
let with_day = matches!(show, ShowCalendar::Always | ShowCalendar::Critical) || calendar != "iso8601";
if !with_day {
return format!("{}-{month:02}", format_iso_year(year as i128));
}
let ref_day = get_double_prop(obj, 2) as i32;
let annotation = calendar_annotation(&calendar, show);
format!("{}-{month:02}-{ref_day:02}{annotation}", format_iso_year(year as i128))
}
/// PlainYearMonth 默认形串(auto 形),含 receiver 校验,不读 options。
fn plain_year_month_default_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_year_month(vm, obj));
NativeResult::Ok(vm.new_string_owned(plain_year_month_iso_string(obj, ShowCalendar::Auto)))
}
/// `Temporal.PlainYearMonth.prototype.toString([options])`:
/// 默认 `±YYYY-MM`;非 iso8601 日历补参考日与 `[u-ca=…]` 注解,
/// always/critical 另按注解表补 `[u-ca=…]`/`[!u-ca=…]`。
pub fn plain_year_month_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_year_month(vm, obj));
let show = native_try!(temporal_to_show_calendar(vm, args));
NativeResult::Ok(vm.new_string_owned(plain_year_month_iso_string(obj, show)))
}
/// `Temporal.PlainYearMonth.prototype.toJSON()`:默认形串,忽略参数。
pub fn plain_year_month_to_json<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_year_month_default_string(vm, args)
}
/// `Temporal.PlainYearMonth.prototype.toLocaleString()`:locale/options 无引擎行为,
/// 恒输出默认形串。
pub fn plain_year_month_to_locale_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_year_month_default_string(vm, args)
}
/// `Temporal.PlainYearMonth.prototype.valueOf()`:Temporal 对象无值表示,恒 TypeError。
pub fn plain_year_month_value_of<H: VmHost>(vm: &mut H, _args: &[u8]) -> NativeResult {
NativeResult::Err(crate::error::create_type_error(vm, "Temporal.PlainYearMonth has no valueOf"))
}
// ==================== PlainMonthDay.from ====================
/// PlainMonthDay 串注解校验:与 validate_temporal_annotation_suffix 同构,
/// 但 u-ca 值只接受 iso8601(大小写不敏感),其余内置日历 ID 拒绝。
fn validate_month_day_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 !value.eq_ignore_ascii_case("iso8601") {
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());
} else {
time_zone_seen = true;
}
suffix = &suffix[end + 1..];
}
Ok(())
}
/// PlainMonthDay 串的时间部分校验(值丢弃):偏移剥离须恰好耗尽剩余文本;
/// 接受 T/t/空格分隔、小时可缺分钟秒、秒可 60(leap second)、小数秒 ≤9 位;
/// 禁止小数时/分、UTC 设计符在调用侧先行拒绝。
fn validate_month_day_time_suffix(input: &str) -> Result<(), String> {
let time_end = input
.char_indices()
.find_map(|(index, ch)| matches!(ch, '+' | '-').then_some(index))
.unwrap_or(input.len());
if time_end < input.len() && !strip_iso_offset(&input[time_end..])?.is_empty() {
return Err("invalid ISO offset".into());
}
let time = &input[..time_end];
if time.is_empty() {
return Err("missing ISO time".into());
}
let (clock, fraction) = match time.find(['.', ',']) {
Some(index) => (&time[..index], Some(&time[index + 1..])),
None => (time, None),
};
let fields = if clock.contains(':') {
clock.split(':').collect::<Vec<_>>()
} else {
if clock.len() % 2 != 0 || clock.len() > 6 {
return Err("invalid ISO time".into());
}
clock
.as_bytes()
.chunks(2)
.map(std::str::from_utf8)
.collect::<Result<Vec<_>, _>>()
.map_err(|_| "invalid ISO time".to_string())?
};
if fields.is_empty() || fields.len() > 3 || fields.iter().any(|field| field.len() != 2) {
return Err("invalid ISO time".into());
}
if fraction.is_some() && fields.len() < 3 {
return Err("fractional hours and minutes are not valid".into());
}
let parse_field = |index: usize| -> Result<u32, String> {
fields
.get(index)
.map_or(Ok(0), |field| field.parse().map_err(|_| "invalid ISO time".to_string()))
};
let hour = parse_field(0)?;
let minute = parse_field(1)?;
let mut second = parse_field(2)?;
if second == 60 {
second = 59;
}
let subsecond = match fraction {
Some(digits) if !digits.is_empty() && digits.len() <= 9 && digits.bytes().all(|byte| byte.is_ascii_digit()) => {
let mut padded = digits.to_owned();
padded.extend(std::iter::repeat('0').take(9 - digits.len()));
padded.parse::<u32>().map_err(|_| "invalid ISO fraction".to_string())?
}
Some(_) => return Err("invalid ISO fraction".into()),
None => 0,
};
if !valid_plain_time(hour, minute, second, subsecond / 1_000_000, subsecond / 1_000 % 1_000, subsecond % 1_000) {
return Err("invalid ISO time".into());
}
Ok(())
}
/// "MM-DD" 或 "MMDD" 形态的月日解析。
fn parse_month_day_md(input: &str) -> Result<(u32, u32), String> {
let bytes = input.as_bytes();
let (m_s, d_s) = if bytes.len() == 4 && bytes.iter().all(|b| b.is_ascii_digit()) {
(&input[..2], &input[2..])
} else if bytes.len() == 5
&& bytes[2] == b'-'
&& bytes[..2].iter().all(|b| b.is_ascii_digit())
&& bytes[3..].iter().all(|b| b.is_ascii_digit())
{
(&input[..2], &input[3..])
} else {
return Err("invalid ISO month-day".into());
};
let month: u32 = m_s.parse().map_err(|_| "invalid ISO month".to_string())?;
let day: u32 = d_s.parse().map_err(|_| "invalid ISO day".to_string())?;
Ok((month, day))
}
/// PlainMonthDay 串的日期部分:MM-DD / MMDD / --MM-DD / --MMDD / 完整日期(扩展或紧凑)。
/// 年份仅用于负零年拒绝(-000000 → 错),无范围检查;返回 (month, day)。
fn parse_month_day_date_part(input: &str) -> Result<(u32, u32), String> {
if input.is_empty() {
return Err("invalid ISO date".into());
}
if let Some(rest) = input.strip_prefix("--") {
let (month, day) = parse_month_day_md(rest)?;
return finish_month_day_range(month, day);
}
let bytes = input.as_bytes();
let signed = matches!(bytes[0], b'-' | b'+');
let negative = bytes[0] == b'-';
let mut i = usize::from(signed);
let y_start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
let digits = &input[y_start..i];
if i < bytes.len() && bytes[i] == b'-' {
// 扩展形:年份 + -MM-DD;无符号两位引导是 MM-DD 形态。
if digits.len() == 2 && !signed {
let (month, day) = parse_month_day_md(input)?;
return finish_month_day_range(month, day);
}
let expected = usize::from(signed) * 6 + 4 * (1 - usize::from(signed));
if digits.len() != expected {
return Err("invalid ISO year".into());
}
i += 1;
let month = read_iso2(input, &mut i, bytes)?;
if i >= bytes.len() || bytes[i] != b'-' {
return Err("invalid ISO date".into());
}
i += 1;
let day = read_iso2(input, &mut i, bytes)?;
if i != bytes.len() {
return Err("invalid trailing content".into());
}
if negative && digits == "000000" {
return Err("invalid ISO negative zero year".into());
}
return finish_month_day_range(month, day);
}
if i != bytes.len() {
return Err("invalid trailing content".into());
}
// 紧凑纯数字:4 位 = MMDD(无符号);否则 (4-6 位年) + 4 位月日。
let len = digits.len();
if !(len == 4 && !signed || len == 10 && signed || (!signed && (8..=10).contains(&len))) {
return Err("invalid ISO date".into());
}
let tail = &input[y_start..];
let y_len = tail.len() - 4;
let (month, day) = parse_month_day_md(&tail[y_len..])?;
if y_len == 6 && negative && &tail[..6] == "000000" {
return Err("invalid ISO negative zero year".into());
}
finish_month_day_range(month, day)
}
/// 月日取值边界:月 1..=12、日 1..=31(串路径无参考年,不做月长检查)。
fn finish_month_day_range(month: u32, day: u32) -> Result<(u32, u32), String> {
if month == 0 || month > 12 || day == 0 || day > 31 {
return Err("invalid ISO date".into());
}
Ok((month, day))
}
/// 解析 PlainMonthDay ISO 串("MM-DD"/"MMDD"/"--MM-DD"/完整日期/完整 datetime)。
/// 注解与时间部分按规则校验;参考年恒 1972(串中年份丢弃)。
fn parse_month_day_string(input: &str) -> Result<(u32, u32), String> {
let trimmed = input.trim();
if trimmed.contains('\u{2212}') {
return Err("variant minus sign is not valid for PlainMonthDay".into());
}
let text = trimmed.to_owned();
let annotation_start = text.find('[').unwrap_or(text.len());
validate_month_day_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 PlainMonthDay".into());
}
let separator = text.find(['T', 't', ' ']);
let (date_part, time_part) = match separator {
Some(index) => (&text[..index], &text[index + 1..]),
None => (text, ""),
};
if !time_part.is_empty() {
validate_month_day_time_suffix(time_part)?;
}
parse_month_day_date_part(date_part)
}
/// 年月串边界:月 1..=12;日(若有)1..=31 且过语法级月日静态检查
/// (2 月 30/31、30 天月份 31 拒;无参考年,2 月 29 不拒)。
fn finish_year_month_range(year: i32, month: u32, day: Option<u32>) -> Result<(i32, u32, Option<u32>), String> {
if !(1..=12).contains(&month) {
return Err("invalid ISO month".into());
}
if let Some(day) = day {
if !(1..=31).contains(&day) || (month == 2 && day > 29) || (matches!(month, 4 | 6 | 9 | 11) && day == 31) {
return Err("invalid ISO date".into());
}
}
Ok((year, month, day))
}
/// PlainYearMonth 串的日期部分:扩展形(无符号 4 位年或带符号 6 位年)+ -MM[-DD],
/// 紧凑形(无符号 6/8 位、带符号 8/10 位纯数字);日部分可缺(年-月形);
/// 负零年(-000000)拒;返回 (year, month, day)。
fn parse_year_month_date_part(input: &str) -> Result<(i32, u32, Option<u32>), String> {
if input.is_empty() {
return Err("invalid ISO date".into());
}
let bytes = input.as_bytes();
let signed = matches!(bytes[0], b'-' | b'+');
let negative = bytes[0] == b'-';
let mut i = usize::from(signed);
let y_start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
let digits = &input[y_start..i];
if negative && digits == "000000" {
return Err("invalid ISO negative zero year".into());
}
if i < bytes.len() && bytes[i] == b'-' {
// 扩展形:无符号 4 位年或带符号 6 位年,随后 -MM[-DD]。
let expected = usize::from(signed) * 6 + 4 * (1 - usize::from(signed));
if digits.len() != expected {
return Err("invalid ISO year".into());
}
let year = digits.parse::<i32>().map_err(|_| "invalid ISO year".to_string())?;
let year = if negative { -year } else { year };
i += 1;
let month = read_iso2(input, &mut i, bytes)?;
if i >= bytes.len() {
return finish_year_month_range(year, month, None);
}
if bytes[i] != b'-' {
return Err("invalid trailing content".into());
}
i += 1;
let day = read_iso2(input, &mut i, bytes)?;
if i != bytes.len() {
return Err("invalid trailing content".into());
}
return finish_year_month_range(year, month, Some(day));
}
if i != bytes.len() {
return Err("invalid trailing content".into());
}
// 紧凑纯数字:无符号 6(年+月)/ 8(年+月+日);带符号 8 / 10(6 位年起步)。
let len = digits.len();
if !(matches!(len, 6 | 8) && !signed || matches!(len, 8 | 10) && signed) {
return Err("invalid ISO date".into());
}
let month_start = if signed { 6 } else { 4 };
let year = digits[..month_start]
.parse::<i32>()
.map_err(|_| "invalid ISO year".to_string())?;
let year = if negative { -year } else { year };
let month = digits[month_start..month_start + 2]
.parse::<u32>()
.map_err(|_| "invalid ISO month".to_string())?;
let day = if digits.len() > month_start + 2 {
Some(
digits[month_start + 2..]
.parse::<u32>()
.map_err(|_| "invalid ISO day".to_string())?,
)
} else {
None
};
finish_year_month_range(year, month, day)
}
/// 解析 PlainYearMonth ISO 串(年-月 / 年-月-日 / 完整 datetime 形态)。
/// 返回 (year, month):日部分仅参与语法级校验,参考日恒 1(不入结果)。
fn parse_year_month_string(input: &str) -> Result<(i32, u32), String> {
let trimmed = input.trim();
if trimmed.contains('\u{2212}') {
return Err("variant minus sign is not valid for PlainYearMonth".into());
}
let text = trimmed.to_owned();
let annotation_start = text.find('[').unwrap_or(text.len());
validate_month_day_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 PlainYearMonth".into());
}
let separator = text.find(['T', 't', ' ']);
let (date_part, time_part) = match separator {
Some(index) => (&text[..index], &text[index + 1..]),
None => (text, ""),
};
if !time_part.is_empty() {
validate_month_day_time_suffix(time_part)?;
}
let (year, month, _) = parse_year_month_date_part(date_part)?;
Ok((year, month))
}
/// `Temporal.PlainMonthDay.from(item[, options])`:ISO 串 / PMD 实例 / PD 实例 / 字段对象。
/// 参考年恒 1972(串中年份与 bag year 均不入结果,year 仅供 overflow 月长判断);
/// options.overflow 在全部字段 Get 之后读取。
pub fn plain_month_day_from<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
if value.is_string() {
// 规范顺序:先解析(错误即抛,不触 options),再读 overflow(结果不受其影响)。
let (month, day) = native_try!(parse_month_day_string(&to_string(value))
.map_err(|_| crate::error::create_range_error(vm, "invalid ISO 8601 month-day string")));
native_try!(temporal_overflow(vm, args));
return make_plain_month_day(vm, month, day, 1972, "iso8601");
}
if !value.is_object() {
return NativeResult::Err(crate::error::create_type_error(vm, "from() argument must be a string or an object"));
}
let ptr = value.as_js_object_ptr();
if ptr.is_null() {
return NativeResult::Err(crate::error::create_type_error(vm, "from() argument must be a string or an object"));
}
let obj = unsafe { &*ptr };
// 实例快路径:槽直读,不触发属性。
if obj.is_plain_month_day_obj() {
// 实例复制:槽直读(refYear/日历保留),overflow 读取但结果不受其影响。
native_try!(temporal_overflow(vm, args));
let calendar = get_calendar_id(obj, 3);
return make_plain_month_day(
vm,
get_double_prop(obj, 0) as u32,
get_double_prop(obj, 1) as u32,
get_double_prop(obj, 2) as i32,
&calendar,
);
}
if obj.is_plain_date_obj() {
// PlainDate 实例:年月日槽直读,overflow 按其年应用,参考年 1972。
let constrain = native_try!(temporal_overflow(vm, args));
let year = get_double_prop(obj, 0) as i32;
let month = get_double_prop(obj, 1) as u32;
let mut day = get_double_prop(obj, 2) as u32;
let calendar = get_calendar_id(obj, 3);
if constrain {
day = day.min(days_in_month_iso(year, month));
}
return make_plain_month_day(vm, month, day, 1972, &calendar);
}
// 字段对象:Get 序 calendar → day → month → monthCode → year → era → eraYear,
// 全部读原始值(不转换);overflow 在全部 Get 之后读取。
let calendar_raw = native_try!(temporal_option_value(vm, obj, value, "calendar"));
let calendar = native_try!(month_day_bag_calendar_id(vm, calendar_raw));
let day_raw = native_try!(temporal_option_value(vm, obj, value, "day"));
let month_raw = native_try!(temporal_option_value(vm, obj, value, "month"));
let month_code_raw = native_try!(temporal_option_value(vm, obj, value, "monthCode"));
let month_code = match month_code_raw.is_undefined() {
true => None,
false => Some(native_try!(temporal_option_string(vm, month_code_raw))),
};
let year_raw = native_try!(temporal_option_value(vm, obj, value, "year"));
let era_raw = native_try!(temporal_option_value(vm, obj, value, "era"));
let era_year_raw = native_try!(temporal_option_value(vm, obj, value, "eraYear"));
let constrain = native_try!(temporal_overflow(vm, args));
// era 族:双现 → RangeError;恰一个 → 忽略(ISO 无纪元体系)。
if !era_raw.is_undefined() && !era_year_raw.is_undefined() {
return NativeResult::Err(crate::error::create_range_error(vm, "era and eraYear cannot both be present"));
}
// 月来源:双给 → RangeError;双缺 → TypeError(先于日存在性)。
if !month_raw.is_undefined() && month_code.is_some() {
return NativeResult::Err(crate::error::create_range_error(vm, "month and monthCode cannot both be present"));
}
if month_raw.is_undefined() && month_code.is_none() {
return NativeResult::Err(crate::error::create_type_error(vm, "month or monthCode is required"));
}
if day_raw.is_undefined() {
return NativeResult::Err(crate::error::create_type_error(vm, "day is required"));
}
// monthCode 两段校验:第一段语法("M"+两位数字,可选再 +"L")先于一切数值转换;
// 第二段适配(闰月后缀、1..12 范围)在 year 转换之后。
let (month_code_num, leap_month) = match &month_code {
Some(text) => {
let b = text.as_bytes();
let well_formed = b[0] == b'M'
&& b.len() >= 3
&& b[1].is_ascii_digit()
&& b[2].is_ascii_digit()
&& (b.len() == 3 || (b.len() == 4 && b[3] == b'L'));
if !well_formed {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid monthCode"));
}
(Some(u32::from(b[1] - b'0') * 10 + u32::from(b[2] - b'0')), b.len() == 4)
}
None => (None, false),
};
// year 转换(TypeError/RangeError)在 monthCode 语法之后。
let year = match year_raw.is_undefined() {
true => None,
false => Some(native_try!(temporal_number_component(vm, year_raw)) as i32),
};
let month_f = match month_raw.is_undefined() {
true => None,
false => Some(native_try!(temporal_number_component(vm, month_raw))),
};
// monthCode 第二段适配:闰月后缀或月值越界 → RangeError。
if let Some(code) = month_code_num {
if leap_month || !(1..=12).contains(&code) {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid monthCode"));
}
}
// 月定值:monthCode 与数值 month 并存须一致(冲突 → RangeError);
// 数值 month 负值恒 RangeError,>12 时 constrain 钳 12、reject 抛错。
let month = if let Some(code) = month_code_num {
if let Some(f) = month_f {
if f as u32 != code {
return NativeResult::Err(crate::error::create_range_error(vm, "month and monthCode conflict"));
}
}
code
} else {
let f = month_f.expect("month or monthCode is required checked above");
if f < 1.0 {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid month"));
}
if f > 12.0 {
if constrain {
12
} else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid month"));
}
} else {
f as u32
}
};
let day_f = native_try!(temporal_number_component(vm, day_raw));
if day_f < 1.0 {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid day"));
}
// overflow 应用:月长按 year(bag 缺省 1972)判断;constrain 钳制,reject 抛错。
let overflow_year = year.unwrap_or(1972);
let days = days_in_month_iso(overflow_year, month) as i32;
let day_i = day_f as i32;
let day = if day_i > days {
if constrain {
days
} else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid day"));
}
} else {
day_i
};
make_plain_month_day(vm, month, day as u32, 1972, &calendar)
}
/// `Temporal.PlainYearMonth.from(item[, options])`:ISO 串 / PYM 实例 / PD 实例 / 字段对象。
/// 串路径按 (年, 月) 查表示范围(日不参与);实例分支槽直读(PYM 复制保留参考日,
/// PD 参考日恒 1);bag 路径 year 必填、day 从不读取(参考日恒 1)、
/// monthCode 先 ToPrimitive 再两段校验(语法先于 year 转换、适配在 year 转换后)。
pub fn plain_year_month_from<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
if value.is_string() {
// 规范顺序:先解析(坏串 RangeError 不触 options),再读 overflow,再查 (年, 月) 范围。
let (year, month) = native_try!(parse_year_month_string(&to_string(value))
.map_err(|_| crate::error::create_range_error(vm, "invalid ISO 8601 year-month string")));
native_try!(temporal_overflow(vm, args));
if !iso_year_month_within_limits(year, month) {
return NativeResult::Err(crate::error::create_range_error(vm, "ISO year-month is out of range"));
}
return make_plain_year_month(vm, year, month, 1, "iso8601");
}
if !value.is_object() {
return NativeResult::Err(crate::error::create_type_error(vm, "from() argument must be a string or an object"));
}
let ptr = value.as_js_object_ptr();
if ptr.is_null() {
return NativeResult::Err(crate::error::create_type_error(vm, "from() argument must be a string or an object"));
}
let obj = unsafe { &*ptr };
// 实例快路径:槽直读,不触发属性。
if obj.is_plain_year_month_obj() {
// 实例复制:overflow 先读(坏 options 先抛)后复制槽(参考日/日历保留)。
native_try!(temporal_overflow(vm, args));
let calendar = get_calendar_id(obj, 3);
return make_plain_year_month(
vm,
get_double_prop(obj, 0) as i32,
get_double_prop(obj, 1) as u32,
get_double_prop(obj, 2) as u32,
&calendar,
);
}
if obj.is_plain_date_obj() {
// PlainDate 实例:年/月槽直读,参考日恒 1(日不读),日历槽直读。
native_try!(temporal_overflow(vm, args));
let year = get_double_prop(obj, 0) as i32;
let month = get_double_prop(obj, 1) as u32;
let calendar = get_calendar_id(obj, 3);
return make_plain_year_month(vm, year, month, 1, &calendar);
}
// 字段对象:Get 序 calendar → year → month → monthCode → era → eraYear(day 不读);
// overflow 在全部 Get 之后读取。
let calendar_raw = native_try!(temporal_option_value(vm, obj, value, "calendar"));
let calendar = native_try!(month_day_bag_calendar_id(vm, calendar_raw));
let year_raw = native_try!(temporal_option_value(vm, obj, value, "year"));
let month_raw = native_try!(temporal_option_value(vm, obj, value, "month"));
let month_code_raw = native_try!(temporal_option_value(vm, obj, value, "monthCode"));
let era_raw = native_try!(temporal_option_value(vm, obj, value, "era"));
let era_year_raw = native_try!(temporal_option_value(vm, obj, value, "eraYear"));
let constrain = native_try!(temporal_overflow(vm, args));
// era 族:双现 → RangeError;恰一个 → 忽略(ISO 无纪元体系)。
if !era_raw.is_undefined() && !era_year_raw.is_undefined() {
return NativeResult::Err(crate::error::create_range_error(vm, "era and eraYear cannot both be present"));
}
// 存在性:year 必填(先于 monthCode 语法);month|monthCode 至少其一。
if year_raw.is_undefined() {
return NativeResult::Err(crate::error::create_type_error(vm, "year is required"));
}
if month_raw.is_undefined() && month_code_raw.is_undefined() {
return NativeResult::Err(crate::error::create_type_error(vm, "month or monthCode is required"));
}
// monthCode ToPrimitive 语义:字符串直用;对象(含函数)经 valueOf/toString;
// 转换结果非字符串(number/bigint/boolean/null/Symbol 等)→ TypeError。
let month_code = if month_code_raw.is_undefined() {
None
} else if month_code_raw.is_string() {
Some(to_string(month_code_raw))
} else if month_code_raw.is_object() {
let primitive =
match oxide_runtime_api::to_primitive(month_code_raw, oxide_runtime_api::ToPrimitiveHint::String, vm) {
Ok(primitive) => primitive,
Err(error) => return NativeResult::Err(native_engine_error(vm, &error)),
};
if !primitive.is_string() {
return NativeResult::Err(crate::error::create_type_error(vm, "invalid monthCode"));
}
Some(to_string(primitive))
} else {
return NativeResult::Err(crate::error::create_type_error(vm, "invalid monthCode"));
};
// monthCode 第一段语法("M"+两位数字,可选再 +"L")先于一切数值转换。
let (month_code_num, leap_month) = match &month_code {
Some(text) => {
let b = text.as_bytes();
let well_formed = b[0] == b'M'
&& b.len() >= 3
&& b[1].is_ascii_digit()
&& b[2].is_ascii_digit()
&& (b.len() == 3 || (b.len() == 4 && b[3] == b'L'));
if !well_formed {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid monthCode"));
}
(Some(u32::from(b[1] - b'0') * 10 + u32::from(b[2] - b'0')), b.len() == 4)
}
None => (None, false),
};
// year 转换(TypeError/RangeError)在 monthCode 语法之后。
let year = native_try!(temporal_number_component(vm, year_raw)) as i32;
let month_f = match month_raw.is_undefined() {
true => None,
false => Some(native_try!(temporal_number_component(vm, month_raw))),
};
// monthCode 第二段适配:闰月后缀或月值越界 → RangeError。
if let Some(code) = month_code_num {
if leap_month || !(1..=12).contains(&code) {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid monthCode"));
}
}
// 月定值:monthCode 与数值 month 并存须一致(冲突 → RangeError);
// 数值 month <1 恒 RangeError,>12 时 constrain 钳 12、reject 抛错。
let month = if let Some(code) = month_code_num {
if let Some(f) = month_f {
if f as u32 != code {
return NativeResult::Err(crate::error::create_range_error(vm, "month and monthCode conflict"));
}
}
code
} else {
let f = month_f.expect("month or monthCode is required checked above");
if f < 1.0 {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid month"));
}
if f > 12.0 {
if constrain {
12
} else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid month"));
}
} else {
f as u32
}
};
// (年, 月) 表示范围。
if !iso_year_month_within_limits(year, month) {
return NativeResult::Err(crate::error::create_range_error(vm, "ISO year-month is out of range"));
}
make_plain_year_month(vm, year, month, 1, &calendar)
}
/// ToTemporalYearMonth 四分支共享件(compare 等无 options 参数的成员复用):
/// 串(解析 + (年, 月) 范围,参考日恒 1)/ PYM 实例(槽直读,参考日保留)/
/// PD 实例(年月槽直读,参考日恒 1)/ 字段 bag(year 必填、day 从不读取、参考日恒 1、
/// monthCode 两段校验同 from);constrain 决定 month >12 钳制或抛错。
fn year_month_like_parts<H: VmHost>(
vm: &mut H, value: JsValue, constrain: bool,
) -> Result<(i32, u32, u32, String), JsValue> {
if value.is_string() {
let (year, month) = parse_year_month_string(&to_string(value))
.map_err(|_| crate::error::create_range_error(vm, "invalid ISO 8601 year-month string"))?;
if !iso_year_month_within_limits(year, month) {
return Err(crate::error::create_range_error(vm, "ISO year-month is out of range"));
}
return Ok((year, month, 1, "iso8601".to_string()));
}
if !value.is_object() {
return Err(crate::error::create_type_error(vm, "argument must be a string or an object"));
}
let ptr = value.as_js_object_ptr();
if ptr.is_null() {
return Err(crate::error::create_type_error(vm, "argument must be a string or an object"));
}
let obj = unsafe { &*ptr };
// 实例快路径:槽直读,不触发属性。
if obj.is_plain_year_month_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_calendar_id(obj, 3),
));
}
if obj.is_plain_date_obj() {
return Ok((get_double_prop(obj, 0) as i32, get_double_prop(obj, 1) as u32, 1, get_calendar_id(obj, 3)));
}
// 字段对象:Get 序 calendar → year → month → monthCode → era → eraYear(day 不读)。
let calendar_raw = temporal_option_value(vm, obj, value, "calendar")?;
let calendar = month_day_bag_calendar_id(vm, calendar_raw)?;
let year_raw = temporal_option_value(vm, obj, value, "year")?;
let month_raw = temporal_option_value(vm, obj, value, "month")?;
let month_code_raw = temporal_option_value(vm, obj, value, "monthCode")?;
let era_raw = temporal_option_value(vm, obj, value, "era")?;
let era_year_raw = temporal_option_value(vm, obj, value, "eraYear")?;
// era 族:双现 → RangeError;恰一个 → 忽略(ISO 无纪元体系)。
if !era_raw.is_undefined() && !era_year_raw.is_undefined() {
return Err(crate::error::create_range_error(vm, "era and eraYear cannot both be present"));
}
// 存在性:year 必填(先于 monthCode 语法);month|monthCode 至少其一。
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 or monthCode is required"));
}
// monthCode ToPrimitive 语义:字符串直用;对象(含函数)经 valueOf/toString;
// 转换结果非字符串(number/bigint/boolean/null/Symbol 等)→ TypeError。
let month_code = if month_code_raw.is_undefined() {
None
} else if month_code_raw.is_string() {
Some(to_string(month_code_raw))
} else if month_code_raw.is_object() {
let primitive =
match oxide_runtime_api::to_primitive(month_code_raw, oxide_runtime_api::ToPrimitiveHint::String, vm) {
Ok(primitive) => primitive,
Err(error) => return Err(native_engine_error(vm, &error)),
};
if !primitive.is_string() {
return Err(crate::error::create_type_error(vm, "invalid monthCode"));
}
Some(to_string(primitive))
} else {
return Err(crate::error::create_type_error(vm, "invalid monthCode"));
};
// monthCode 第一段语法("M"+两位数字,可选再 +"L")先于一切数值转换。
let (month_code_num, leap_month) = match &month_code {
Some(text) => {
let b = text.as_bytes();
let well_formed = b[0] == b'M'
&& b.len() >= 3
&& b[1].is_ascii_digit()
&& b[2].is_ascii_digit()
&& (b.len() == 3 || (b.len() == 4 && b[3] == b'L'));
if !well_formed {
return Err(crate::error::create_range_error(vm, "invalid monthCode"));
}
(Some(u32::from(b[1] - b'0') * 10 + u32::from(b[2] - b'0')), b.len() == 4)
}
None => (None, false),
};
// year 转换(TypeError/RangeError)在 monthCode 语法之后。
let year = temporal_number_component(vm, year_raw)? as i32;
let month_f = match month_raw.is_undefined() {
true => None,
false => Some(temporal_number_component(vm, month_raw)?),
};
// monthCode 第二段适配:闰月后缀或月值越界 → RangeError。
if let Some(code) = month_code_num {
if leap_month || !(1..=12).contains(&code) {
return Err(crate::error::create_range_error(vm, "invalid monthCode"));
}
}
// 月定值:monthCode 与数值 month 并存须一致(冲突 → RangeError);
// 数值 month <1 恒 RangeError,>12 时 constrain 钳 12、reject 抛错。
let month = if let Some(code) = month_code_num {
if let Some(f) = month_f {
if f as u32 != code {
return Err(crate::error::create_range_error(vm, "month and monthCode conflict"));
}
}
code
} else {
let f = month_f.expect("month or monthCode is required checked above");
if f < 1.0 {
return Err(crate::error::create_range_error(vm, "invalid month"));
}
if f > 12.0 {
if constrain {
12
} else {
return Err(crate::error::create_range_error(vm, "invalid month"));
}
} else {
f as u32
}
};
// (年, 月) 表示范围。
if !iso_year_month_within_limits(year, month) {
return Err(crate::error::create_range_error(vm, "ISO year-month is out of range"));
}
Ok((year, month, 1, calendar))
}
/// `Temporal.PlainYearMonth.compare(one, two)`:静态比较,返回 -1/0/1。
/// 比较 (年, 月, 参考日) 三元字典序;两参均经 ToTemporalYearMonth。
pub fn plain_year_month_compare<H: VmHost>(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 = native_try!(year_month_like_parts(vm, a_val, true));
let b = native_try!(year_month_like_parts(vm, b_val, true));
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.PlainYearMonth.prototype.equals(other)`:比较 (年, 月, 参考日) 三元与日历标识;
/// other 经 ToTemporalYearMonth(串 / 字段对象 / PD 实例 / PYM 实例)转换。
pub fn plain_year_month_equals<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_year_month(vm, obj));
let other = match plain_year_month_from(vm, args) {
NativeResult::Ok(val) => val,
NativeResult::Err(err) => return NativeResult::Err(err),
NativeResult::TailCall { .. } => unreachable!("工厂路径不产生尾调用"),
};
let other_ptr = other.as_js_object_ptr();
if other_ptr.is_null() {
return NativeResult::Err(crate::error::create_type_error(vm, "incompatible receiver"));
}
let o = unsafe { &*other_ptr };
let equal = get_double_prop(obj, 0) == get_double_prop(o, 0)
&& get_double_prop(obj, 1) == get_double_prop(o, 1)
&& get_double_prop(obj, 2) == get_double_prop(o, 2)
&& get_calendar_id(obj, 3) == get_calendar_id(o, 3);
NativeResult::Ok(JsValue::bool(equal))
}
/// add/subtract 的 (年, 月) 可表示性检查,按日 = 1 计算(ISODateWithinLimits)。
/// 与 ISOYearMonthWithinLimits 的差异仅在 -271821 年:该年 4 月 1 日早于最早
/// 可表示的日期时间,故 4 月及以前越界(5 月起在界内);+275760 年须 9 月及以前。
fn iso_year_month_day1_within_limits(year: i64, month: i64) -> bool {
if !(-271_821_i64..=275_760_i64).contains(&year) {
return false;
}
!((year == -271_821 && month < 5) || (year == 275_760 && month > 9))
}
/// `Temporal.PlainYearMonth.prototype.add/subtract(durationLike [, options])` 核心。
///
/// # 步骤
/// 1. receiver branding(TypeError)。
/// 2. ToTemporalDuration(串 / bag / 实例;TypeError、RangeError 各自语义)。
/// 3. 读 overflow(规范顺序先于后续算法校验;ISO 日历下取值不可观测,仅校验)。
/// 4. weeks / days / 时间分量任一非零 → RangeError。
/// 5. receiver (年, 月) 按日 1 检查可表示性 → RangeError。
/// 6. 年、月分量折算绝对月数后相加再平衡(div/rem_euclid)。
/// 7. 结果 (年, 月) 按日 1 检查可表示性 → RangeError。
///
/// # 边界与副作用
/// - 结果参考日恒 1,日历标识取自 receiver 槽 3。
/// - subtract 与 add 共用本函数,sign 参数取反时长分量。
fn plain_year_month_apply_duration<H: VmHost>(vm: &mut H, args: &[u8], sign: i64) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_year_month(vm, obj));
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 _overflow = native_try!(temporal_overflow(vm, args));
if values[2..].iter().any(|value| *value != 0.0) {
return NativeResult::Err(crate::error::create_range_error(
vm,
"year-month cannot add weeks, days, or time units",
));
}
let year = get_double_prop(obj, 0) as i64;
let month = get_double_prop(obj, 1) as i64;
if !iso_year_month_day1_within_limits(year, month) {
return NativeResult::Err(crate::error::create_range_error(vm, "year-month is out of range"));
}
let months = (values[0] as i64) * 12 + values[1] as i64;
let absolute = year * 12 + month - 1 + sign * months;
let result_year = absolute.div_euclid(12);
let result_month = absolute.rem_euclid(12) + 1;
if !iso_year_month_day1_within_limits(result_year, result_month) {
return NativeResult::Err(crate::error::create_range_error(vm, "year-month is out of range"));
}
make_plain_year_month(vm, result_year as i32, result_month as u32, 1, &get_calendar_id(obj, 3))
}
/// `Temporal.PlainYearMonth.prototype.add(durationLike)`。
pub fn plain_year_month_add<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_year_month_apply_duration(vm, args, 1)
}
/// `Temporal.PlainYearMonth.prototype.subtract(durationLike)`。
pub fn plain_year_month_subtract<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_year_month_apply_duration(vm, args, -1)
}
/// `Temporal.PlainYearMonth.prototype.with(yearMonthLike[, options])`:覆盖 year/month/monthCode;
/// undefined 不覆盖;calendar/timeZone 键拒绝;无识别字段 → TypeError。
///
/// # 步骤
/// 1. receiver branding(TypeError)。
/// 2. reject calendar/timeZone 键与 Temporal 实例(TypeError)。
/// 3. Get 序 month → monthCode → year(字典序);全 undefined → TypeError。
/// 4. monthCode 第一段语法校验与字段数值转换先于 options 读取。
/// 5. 读 overflow(缺省 constrain);monthCode 第二段适配(闰月后缀/1..12)与
/// month/monthCode 冲突、>12 钳制在此。
/// 6. 缺省分量取 receiver 槽(年 0 / 月 1);(年, 月) 过 ISOYearMonthWithinLimits。
///
/// # 边界与副作用
/// - 结果参考日恒 1,日历标识取自 receiver 槽 3。
/// - 年月级范围检查(非按日 1),-271821-04 可经 with 产出。
pub fn plain_year_month_with<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_year_month(vm, obj));
let like = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
native_try!(reject_partial_object_with_calendar_or_time_zone(vm, like));
let lptr = like.as_js_object_ptr();
if lptr.is_null() {
return NativeResult::Err(crate::error::create_type_error(vm, "invalid argument"));
}
let like_obj = unsafe { &*lptr };
// Get 序(字典序):month → monthCode → year;无识别字段 → TypeError。
let month_raw = native_try!(temporal_option_value(vm, like_obj, like, "month"));
let month_code_raw = native_try!(temporal_option_value(vm, like_obj, like, "monthCode"));
let month_code = match month_code_raw.is_undefined() {
true => None,
false => Some(native_try!(temporal_option_string(vm, month_code_raw))),
};
let year_raw = native_try!(temporal_option_value(vm, like_obj, like, "year"));
if [month_raw, month_code_raw, year_raw].iter().all(|raw| raw.is_undefined()) {
return NativeResult::Err(crate::error::create_type_error(vm, "no properties present"));
}
// monthCode 第一段语法("M"+两位数字,可选再 +"L")先于一切数值转换。
let (month_code_num, leap_month) = match &month_code {
Some(text) => {
let b = text.as_bytes();
let well_formed = b[0] == b'M'
&& b.len() >= 3
&& b[1].is_ascii_digit()
&& b[2].is_ascii_digit()
&& (b.len() == 3 || (b.len() == 4 && b[3] == b'L'));
if !well_formed {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid monthCode"));
}
(Some(u32::from(b[1] - b'0') * 10 + u32::from(b[2] - b'0')), b.len() == 4)
}
None => (None, false),
};
// 部分字段先于 options 完整转换(RangeError/TypeError 先于 options 形态错误)。
let year = match year_raw.is_undefined() {
true => None,
false => Some(native_try!(temporal_number_component(vm, year_raw)) as i32),
};
let month_f = match month_raw.is_undefined() {
true => None,
false => Some(native_try!(temporal_number_component(vm, month_raw))),
};
if let Some(f) = month_f {
if f < 1.0 {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid month"));
}
}
let constrain = native_try!(temporal_overflow(vm, args));
if let Some(code) = month_code_num {
if leap_month || !(1..=12).contains(&code) {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid monthCode"));
}
}
// 月定值:monthCode 与数值 month 并存须一致(冲突 → RangeError);
// 数值 month >12 时 constrain 钳 12、reject 抛错;缺省取 receiver 月(槽 1)。
let month = if let Some(code) = month_code_num {
if let Some(f) = month_f {
if f as u32 != code {
return NativeResult::Err(crate::error::create_range_error(vm, "month and monthCode conflict"));
}
}
code
} else {
match month_f {
Some(f) => {
if f > 12.0 {
if constrain {
12
} else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid month"));
}
} else {
f as u32
}
}
None => get_double_prop(obj, 1) as u32,
}
};
let year = year.unwrap_or_else(|| get_double_prop(obj, 0) as i32);
// (年, 月) 可表示范围(年月级,非按日 1)。
if !iso_year_month_within_limits(year, month) {
return NativeResult::Err(crate::error::create_range_error(vm, "ISO year-month is out of range"));
}
let calendar = get_calendar_id(obj, 3);
make_plain_year_month(vm, year, month, 1, &calendar)
}
/// `Temporal.PlainYearMonth.prototype.toPlainDate(dayLike)`:dayLike 须为对象,
/// 只 Get `day`(year/month/monthCode/calendar 从不读取);options.overflow 绝不读取;
/// 合并 (年, 月, 日) 后 constrain 钳 day 至 [1, 月长],day 级可表示范围检查后
/// 产出 PlainDate(receiver 日历)。
pub fn plain_year_month_to_plain_date<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_year_month(vm, obj));
let item = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
if !item.is_object() {
return NativeResult::Err(crate::error::create_type_error(vm, "invalid argument"));
}
let iptr = item.as_js_object_ptr();
if iptr.is_null() {
return NativeResult::Err(crate::error::create_type_error(vm, "invalid argument"));
}
let item_obj = unsafe { &*iptr };
let day_raw = native_try!(temporal_option_value(vm, item_obj, item, "day"));
if day_raw.is_undefined() {
return NativeResult::Err(crate::error::create_type_error(vm, "day is required"));
}
let day = native_try!(temporal_number_component(vm, day_raw));
let year = get_double_prop(obj, 0) as i32;
let month = get_double_prop(obj, 1) as u32;
// constrain:day 钳到 [1, 月长]。
let day = (day as i32).clamp(1, days_in_month_iso(year, month) as i32) as u32;
// day 级表示范围:-100_000_001..=100_000_000(年月级合法而 day 级越界在此抛)。
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, "date is out of range"));
}
let calendar = get_calendar_id(obj, 3);
make_plain_date(vm, year, month, day, &calendar)
}
/// `Temporal.PlainYearMonth.prototype.until(other[, options])`。
pub fn plain_year_month_until<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_year_month_difference(vm, args, false)
}
/// `Temporal.PlainYearMonth.prototype.since(other[, options])`。
pub fn plain_year_month_since<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_year_month_difference(vm, args, true)
}
/// until/since 差值核心(DifferenceTemporalPlainYearMonth)。
///
/// # 步骤
/// 1. receiver branding(TypeError)。
/// 2. other 经 ToTemporalYearMonth(串 / bag / PD / PYM 实例;day 不读)。
/// 3. 日历相等(不等 RangeError;在 options 读取之前)。
/// 4. options 对象 + 差值设置(单位表仅 year/month,smallestUnit 缺省 month,
/// largestUnit 缺省/auto 为 year)。
/// 5. (年, 月, 参考日) 三元全等 → 空 Duration(先于可表示性检查)。
/// 6. 两端按日 1 查可表示性(ISODateWithinLimits,day 级)→ RangeError。
/// 7. 既有差值机件(nudge 窗口端点同样带 day 级范围检查);since 整体取反。
///
/// # 边界与副作用
/// - 参考日只参与第 5 步的全等判断;差值本身恒按日 1 计算。
/// - 第 2 步的字符串/袋路径为年月级范围检查(自 ToTemporalYearMonth),
/// day 级补查在第 6 步,两步错误类相同(RangeError),仅抛错时点不同。
fn plain_year_month_difference<H: VmHost>(vm: &mut H, args: &[u8], since: bool) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_year_month(vm, obj));
let other_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let (oy, om, od, ocal) = native_try!(year_month_like_parts(vm, other_val, true));
// 日历相等(ToTemporalYearMonth 之后、options 读取之前)。
if get_calendar_id(obj, 3) != ocal {
return NativeResult::Err(crate::error::create_range_error(vm, "calendars must be equal"));
}
let options_value = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() };
let settings = match parse_year_month_difference_settings(vm, options_value) {
Ok(settings) => settings,
Err(error) => return NativeResult::Err(error),
};
// (年, 月, 参考日) 三元全等 → 空 Duration(先于可表示性检查)。
let ry = get_double_prop(obj, 0) as i128;
let rm = get_double_prop(obj, 1) as i128;
let rd = get_double_prop(obj, 2) as i128;
if compare_iso_date((ry, rm, rd), (i128::from(oy), i128::from(om), i128::from(od))) == 0 {
return make_duration(vm, [0.0; 10]);
}
// 两端按日 1 的可表示性检查(ISODateWithinLimits,day 级)。
if days_from_civil(ry, rm, 1).abs() > MAX_ISO_DAY
|| days_from_civil(i128::from(oy), i128::from(om), 1).abs() > MAX_ISO_DAY
{
return NativeResult::Err(crate::error::create_range_error(vm, "date is out of range"));
}
difference_core(vm, (ry, rm, 1), 0, (i128::from(oy), i128::from(om), 1), 0, settings, since, 1)
}
/// `Temporal.PlainMonthDay.prototype.equals(other)`:比较 (月, 日, 参考年) 三元与日历标识;
/// other 经 ToTemporalMonthDay(串 / 字段对象 / 实例)转换。
pub fn plain_month_day_equals<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_month_day(vm, obj));
let other = match plain_month_day_from(vm, args) {
NativeResult::Ok(val) => val,
NativeResult::Err(err) => return NativeResult::Err(err),
NativeResult::TailCall { .. } => unreachable!("工厂路径不产生尾调用"),
};
let other_ptr = other.as_js_object_ptr();
if other_ptr.is_null() {
return NativeResult::Err(crate::error::create_type_error(vm, "incompatible receiver"));
}
let o = unsafe { &*other_ptr };
let equal = get_double_prop(obj, 0) == get_double_prop(o, 0)
&& get_double_prop(obj, 1) == get_double_prop(o, 1)
&& get_double_prop(obj, 2) == get_double_prop(o, 2)
&& get_calendar_id(obj, 3) == get_calendar_id(o, 3);
NativeResult::Ok(JsValue::bool(equal))
}
/// `Temporal.PlainMonthDay.prototype.with(monthDayLike[, options])`:覆盖 day/month/monthCode;
/// year 只参与 overflow 月长判断;undefined 不覆盖;calendar/timeZone 键拒绝;
/// 无识别字段 TypeError;overflow 读自 options(缺省 constrain)。
/// 结果保留 receiver 日历,参考年恒 1972。
pub fn plain_month_day_with<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_month_day(vm, obj));
let like = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
native_try!(reject_partial_object_with_calendar_or_time_zone(vm, like));
let lptr = like.as_js_object_ptr();
if lptr.is_null() {
return NativeResult::Err(crate::error::create_type_error(vm, "invalid argument"));
}
let like_obj = unsafe { &*lptr };
// Get 序(字典序):day → month → monthCode → year;无识别字段 → TypeError。
let day_raw = native_try!(temporal_option_value(vm, like_obj, like, "day"));
let month_raw = native_try!(temporal_option_value(vm, like_obj, like, "month"));
let month_code_raw = native_try!(temporal_option_value(vm, like_obj, like, "monthCode"));
let month_code = match month_code_raw.is_undefined() {
true => None,
false => Some(native_try!(temporal_option_string(vm, month_code_raw))),
};
let year_raw = native_try!(temporal_option_value(vm, like_obj, like, "year"));
if [day_raw, month_raw, month_code_raw, year_raw]
.iter()
.all(|raw| raw.is_undefined())
{
return NativeResult::Err(crate::error::create_type_error(vm, "no properties present"));
}
// monthCode 两段校验:语法("M"+两位数字,可选 +"L")先于 year 转换;
// 适配(闰月后缀、1..12 范围)与冲突检查在 options 读取之后。
let (month_code_num, leap_month) = match &month_code {
Some(text) => {
let b = text.as_bytes();
let well_formed = b[0] == b'M'
&& b.len() >= 3
&& b[1].is_ascii_digit()
&& b[2].is_ascii_digit()
&& (b.len() == 3 || (b.len() == 4 && b[3] == b'L'));
if !well_formed {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid monthCode"));
}
(Some(u32::from(b[1] - b'0') * 10 + u32::from(b[2] - b'0')), b.len() == 4)
}
None => (None, false),
};
// 部分字段先于 options 完整转换(RangeError/TypeError 先于 options 形态错误)。
let year = match year_raw.is_undefined() {
true => None,
false => Some(native_try!(temporal_number_component(vm, year_raw)) as i32),
};
let month_f = match month_raw.is_undefined() {
true => None,
false => Some(native_try!(temporal_number_component(vm, month_raw))),
};
let day_f = match day_raw.is_undefined() {
true => None,
false => Some(native_try!(temporal_number_component(vm, day_raw))),
};
if let Some(f) = month_f {
if f < 1.0 {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid month"));
}
}
if let Some(f) = day_f {
if f < 1.0 {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid day"));
}
}
let constrain = native_try!(temporal_overflow(vm, args));
if let Some(code) = month_code_num {
if leap_month || !(1..=12).contains(&code) {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid monthCode"));
}
}
// 月定值:receiver 月(槽 0)为缺省;monthCode 与数值 month 并存须一致;
// 数值 month 负值恒 RangeError,>12 时 constrain 钳 12、reject 抛错。
let month = if let Some(code) = month_code_num {
if let Some(f) = month_f {
if f as u32 != code {
return NativeResult::Err(crate::error::create_range_error(vm, "month and monthCode conflict"));
}
}
code
} else {
match month_f {
Some(f) => {
if f < 1.0 {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid month"));
}
if f > 12.0 {
if constrain {
12
} else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid month"));
}
} else {
f as u32
}
}
None => get_double_prop(obj, 0) as u32,
}
};
// 日:receiver 日(槽 1)为缺省(partial 日已先行转换并过 <1 检查)。
let day_f = day_f.unwrap_or_else(|| get_double_prop(obj, 1));
// overflow 应用:月长按 bag year(缺省 1972)判断;constrain 钳制,reject 抛错。
let overflow_year = year.unwrap_or(1972);
let days = days_in_month_iso(overflow_year, month) as i32;
let day_i = day_f as i32;
let day = if day_i > days {
if constrain {
days
} else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid day"));
}
} else {
day_i
};
let calendar = get_calendar_id(obj, 3);
make_plain_month_day(vm, month, day as u32, 1972, &calendar)
}
/// `Temporal.PlainMonthDay.prototype.toPlainDate(yearLike)`:yearLike 为 number 或 {year};
/// 只 Get item 的 year;overflow 恒 constrain 且不读 options;
/// constrain 钳 day、day 级范围检查后产出 PlainDate(receiver 日历)。
pub fn plain_month_day_to_plain_date<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_month_day(vm, obj));
let item = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
if !item.is_object() {
return NativeResult::Err(crate::error::create_type_error(vm, "invalid argument"));
}
let iptr = item.as_js_object_ptr();
if iptr.is_null() {
return NativeResult::Err(crate::error::create_type_error(vm, "invalid argument"));
}
let item_obj = unsafe { &*iptr };
let year_raw = native_try!(temporal_option_value(vm, item_obj, item, "year"));
if year_raw.is_undefined() {
return NativeResult::Err(crate::error::create_type_error(vm, "year is required"));
}
let year = native_try!(temporal_number_component(vm, year_raw)) as i32;
let month = get_double_prop(obj, 0) as u32;
// constrain:day 钳到 (year, month) 月长。
let day = (get_double_prop(obj, 1) as u32).min(days_in_month_iso(year, month));
// day 级表示范围:-100_000_001..=100_000_000。
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, "date is out of range"));
}
let calendar = get_calendar_id(obj, 3);
make_plain_date(vm, year, month, day, &calendar)
}
#[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 zoned_date_time_string_wall_day_range_boundary() {
// ZDT 字符串的墙钟日范围(CheckISODaysRange)边界:±10^8 天内合法,
// 第 ±100000001 天(-271821-04-19 / +275760-09-14)即使 epoch 换算回界内也拒绝。
assert!(days_from_civil(-271_821, 4, 19).abs() > 100_000_000);
assert!(days_from_civil(-271_821, 4, 20).abs() <= 100_000_000);
assert!(days_from_civil(275_760, 9, 14).abs() > 100_000_000);
assert!(days_from_civil(275_760, 9, 13).abs() <= 100_000_000);
// 解析路径一致性:边界内字符串可解析,越界墙钟经偏移拉回界内也不放行。
assert_eq!(
parse_instant_string("+275760-09-13T01:00+01:00[+01:00]"),
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);
}
#[test]
fn parse_plain_time_string_cases() {
// 明确时间:T 前缀 / 冒号 / 非法日期数字串 / 带 offset 与注解。
assert_eq!(parse_plain_time_string("T00:30"), Some(1_800_000_000_000.0));
assert_eq!(parse_plain_time_string("T0030"), Some(1_800_000_000_000.0));
assert_eq!(parse_plain_time_string("12:34:56.987654321+00:00"), Some(45_296_987_654_321.0));
assert_eq!(parse_plain_time_string("12:34:56.987654321+00:00[UTC]"), Some(45_296_987_654_321.0));
assert_eq!(parse_plain_time_string("1976-11-18T12:34:56.987654321+00:00"), Some(45_296_987_654_321.0));
assert_eq!(parse_plain_time_string("1976-11-18 12:34:56.987654321"), Some(45_296_987_654_321.0));
assert_eq!(parse_plain_time_string("1314"), Some(47_640_000_000_000.0)); // 13:14
assert_eq!(parse_plain_time_string("0631"), Some(23_460_000_000_000.0)); // 06:31
assert_eq!(parse_plain_time_string("2021-13"), Some(73_260_000_000_000.0)); // 20:21 + offset -13 忽略
// 歧义日期 → None(须 T 前缀)。
assert_eq!(parse_plain_time_string("2019-10-01"), None);
assert_eq!(parse_plain_time_string("1214"), None); // MMDD 合法
assert_eq!(parse_plain_time_string("0229"), None); // 闰年 2 月 29 判歧义
assert_eq!(parse_plain_time_string("1130"), None);
assert_eq!(parse_plain_time_string("12-14"), None); // MM-DD 合法
assert_eq!(parse_plain_time_string("202112"), None); // YYYYMM 合法
assert_eq!(parse_plain_time_string("2021-12"), None); // YYYY-MM 合法
assert_eq!(parse_plain_time_string("2021-12[-12:00]"), None);
assert_eq!(parse_plain_time_string("202112[UTC]"), None);
assert_eq!(parse_plain_time_string("1214[u-ca=iso8601]"), None);
// T 前缀后歧义消除;空格不能替代 T。
assert!(parse_plain_time_string("T2021-12").is_some());
assert_eq!(parse_plain_time_string(" 2021-12"), None);
// Z designator / 越界 / 多小数 / 负零年。
assert_eq!(parse_plain_time_string("09:00:00Z"), None);
assert_eq!(parse_plain_time_string("2019-10-01T09:00:00Z"), None);
assert_eq!(parse_plain_time_string("24:00"), None);
assert_eq!(parse_plain_time_string("12:34:56.1234567890"), None);
assert_eq!(parse_plain_time_string("-000000-12-07T03:24:30"), None);
// 闰秒按前一秒。
assert_eq!(parse_plain_time_string("2016-12-31T23:59:60"), Some(86_399_000_000_000.0));
}
#[test]
fn parse_plain_time_string_offsets_and_fractions() {
// offset 小数秒 ≤9 位合法,>9 位拒绝;逗号小数接受。
assert_eq!(
parse_plain_time_string("12:34:56.987654321+00:00:00.000000000"),
Some(45_296_987_654_321.0)
);
assert_eq!(parse_plain_time_string("12:34:56.987654321+00:00:00,0"), Some(45_296_987_654_321.0));
assert_eq!(parse_plain_time_string("00:00:00.1234567891"), None);
assert_eq!(parse_plain_time_string("00+00:00:00.1234567891"), None);
// 日期+offset 但无时间部分 → 拒绝。
assert_eq!(parse_plain_time_string("2022-09-15Z"), None);
assert_eq!(parse_plain_time_string("2022-09-15+00:00"), None);
}
#[test]
fn zoned_date_time_round_unit_table() {
// day 含独立条目,每单位 (ns, 更高一级数量) 与 spec 对齐;day 更高一级数量为 1。
assert_eq!(zoned_date_time_round_unit("day"), Some((86_400_000_000_000, 1)));
assert_eq!(zoned_date_time_round_unit("days"), Some((86_400_000_000_000, 1)));
assert_eq!(zoned_date_time_round_unit("hour"), Some((3_600_000_000_000, 24)));
assert_eq!(zoned_date_time_round_unit("minute"), Some((60_000_000_000, 60)));
assert_eq!(zoned_date_time_round_unit("second"), Some((1_000_000_000, 60)));
assert_eq!(zoned_date_time_round_unit("millisecond"), Some((1_000_000, 1_000)));
assert_eq!(zoned_date_time_round_unit("microsecond"), Some((1_000, 1_000)));
assert_eq!(zoned_date_time_round_unit("nanosecond"), Some((1, 1_000)));
// year/month/week 及拼写错误不在单位表。
assert_eq!(zoned_date_time_round_unit("years"), None);
assert_eq!(zoned_date_time_round_unit("months"), None);
assert_eq!(zoned_date_time_round_unit("weeks"), None);
assert_eq!(zoned_date_time_round_unit("hourz"), None);
}
#[test]
fn zoned_date_time_round_else_path_epoch() {
// 217175010123456789n +01:00:本地 time_ns = 55_410_123_456_789。
// hour/4 quantum 14_400e9 → rounded_time 57_600e9 → local_to_epoch_ns 回推 217177200000000000。
let quantum = 3_600_000_000_000 * 4;
let rounded = round_instant_ns(55_410_123_456_789, quantum, InstantRoundingMode::HalfExpand).unwrap();
assert_eq!(rounded, 57_600_000_000_000);
// 本地墙钟日回推:offset 60 分,local_to_epoch_ns 得目标 epoch。
let epoch = local_to_epoch_ns(1976, 11, 18, rounded as f64, 60).unwrap();
assert_eq!(epoch, 217_177_200_000_000_000);
}
#[test]
fn zoned_date_time_round_day_path_epoch() {
// 同日本地午夜 startNs(2513 天,offset 60 分),dayProgress 舍到次日 → 217206000000000000。
let start_ns = start_of_day_epoch_ns(1976, 11, 18, 60).unwrap();
assert_eq!(start_ns, 217_119_600_000_000_000);
let day_progress = 217_175_010_123_456_789 - start_ns;
let rounded = round_instant_ns(day_progress, 86_400_000_000_000, InstantRoundingMode::HalfExpand).unwrap();
assert_eq!(rounded, 86_400_000_000_000);
assert_eq!(start_ns + rounded, 217_206_000_000_000_000);
}
#[test]
fn format_plain_time_iso_seconds_and_fractions() {
// 无小数(亚秒为 0):仅 HH:MM:SS。
assert_eq!(format_plain_time_iso(45_296_000_000_000, true, None), "12:34:56");
// 固定小数位补足 9 位。
assert_eq!(format_plain_time_iso(45_296_987_654_321, true, Some(9)), "12:34:56.987654321");
// 固定 3 位截断亚秒。
assert_eq!(format_plain_time_iso(45_296_987_654_321, true, Some(3)), "12:34:56.987");
// 固定 0 位不输出小数。
assert_eq!(format_plain_time_iso(45_296_987_654_321, true, Some(0)), "12:34:56");
// 自动模式去尾零(.500 归一为 .5,整秒无小数)。
assert_eq!(format_plain_time_iso(45_296_500_000_000, true, None), "12:34:56.5");
assert_eq!(format_plain_time_iso(45_296_000_000_000, true, None), "12:34:56");
// 不含秒:仅 HH:MM。
assert_eq!(format_plain_time_iso(45_296_000_000_000, false, Some(0)), "12:34");
assert_eq!(format_plain_time_iso(45_296_987_654_321, false, None), "12:34");
}
#[test]
fn plain_time_rounding_cross_midnight() {
// 23:59:59.9 以 second 舍入(halfExpand)→ 24:00:00 → 取模回 00:00:00。
let total_ns = 86_399_900_000_000_i128;
let quantum = 1_000_000_000_i128;
let rounded = round_instant_ns(total_ns, quantum, InstantRoundingMode::HalfExpand).unwrap();
assert_eq!(rounded, 86_400_000_000_000);
const DAY_NS: i128 = 86_400_000_000_000;
assert_eq!(rounded.rem_euclid(DAY_NS), 0);
assert_eq!(format_plain_time_iso(rounded.rem_euclid(DAY_NS), true, Some(0)), "00:00:00");
}
#[test]
fn plain_time_round_increment_real_factor() {
// 真因子校验:增量须严格小于最大增量且能整除(MaximumTemporalDurationRoundingIncrement)。
// hour 合法增量:[1,2,3,4,6,8,12],24(= 最大增量)与 11(不整除)均拒。
let (_, max_increment) = plain_time_round_unit("hour").unwrap();
for increment in [1, 2, 3, 4, 6, 8, 12] {
assert!(increment < max_increment && max_increment % increment == 0, "hour {increment} 应合法");
}
for increment in [11, 24] {
assert!(increment >= max_increment || max_increment % increment != 0, "hour {increment} 应拒绝");
}
// minute 合法增量:整除 60 且严格小于 60;60(= 最大增量)与 29(不整除)均拒。
let (_, max_increment) = plain_time_round_unit("minute").unwrap();
for increment in [1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30] {
assert!(increment < max_increment && max_increment % increment == 0, "minute {increment} 应合法");
}
for increment in [29, 60] {
assert!(increment >= max_increment || max_increment % increment != 0, "minute {increment} 应拒绝");
}
// millisecond 最大增量 1000:1000 拒,29 拒。
let (_, max_increment) = plain_time_round_unit("millisecond").unwrap();
assert!(max_increment == 1000);
for increment in [29, 1000] {
assert!(increment >= max_increment || max_increment % increment != 0, "ms {increment} 应拒绝");
}
}
#[test]
fn plain_time_apply_duration_ignores_date_units() {
// 时间域加总仅取 hours 起字段;days 及以上对 PlainTime 忽略(与 instant_round 日期单位报错不同)。
let mut values = [0.0; 10];
values[3] = 5.0; // days
values[4] = 2.0; // hours
values[5] = 30.0; // minutes
let time_delta = duration_component_integer(values[4]).unwrap() * 3_600_000_000_000
+ duration_component_integer(values[5]).unwrap() * 60_000_000_000;
assert_eq!(time_delta, 9_000_000_000_000); // 2h30m
// 忽略 days:time_delta 不含 DAY_NS 分量。
assert_eq!(time_delta % 86_400_000_000_000, 9_000_000_000_000);
}
}