mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-25 03:01:12 +08:00
4113 lines
187 KiB
Rust
4113 lines
187 KiB
Rust
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);
|
||
}
|
||
}
|