From e1132b98ff64df17a272d06b1df99f9eba2ae7df Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Thu, 17 Sep 2026 09:53:05 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20temporal=20PlainDate=20=E6=97=8F?= =?UTF-8?q?=E6=8B=86=E5=87=BA=E7=8B=AC=E7=AB=8B=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/temporal/mod.rs | 635 +---------------- .../oxide_builtins/src/temporal/plain_date.rs | 647 ++++++++++++++++++ 2 files changed, 650 insertions(+), 632 deletions(-) create mode 100644 crates/oxide_builtins/src/temporal/plain_date.rs diff --git a/crates/oxide_builtins/src/temporal/mod.rs b/crates/oxide_builtins/src/temporal/mod.rs index b92cfef1..8891974a 100644 --- a/crates/oxide_builtins/src/temporal/mod.rs +++ b/crates/oxide_builtins/src/temporal/mod.rs @@ -1,4 +1,4 @@ -use chrono::{Datelike, Days, NaiveDate}; +use chrono::{Datelike, NaiveDate}; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_runtime_api::{to_number, to_string, NativeResult, VmHost}; @@ -9,12 +9,14 @@ mod common; mod difference; mod duration; mod instant; +mod plain_date; mod zoned_date_time; use common::*; use difference::*; pub use duration::*; pub use instant::*; +pub use plain_date::*; pub use zoned_date_time::*; /// 时间串的日期歧义判定:形如 YYYY-MM / MMDD / YYYYMM / MM-DD 且作为日期合法 → 歧义。 @@ -306,244 +308,6 @@ fn make_duration(vm: &mut H, values: [f64; 10]) -> NativeResult { 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 { - let days = days_from_civil(i128::from(year), i128::from(month), i128::from(day)); - days.checked_mul(86_400_000_000_000)?.checked_add(time_ns as i128) -} - -/// 校验日期时间落在 PlainDateTime 表示范围(约 ±(10^8 + 1) 天,边界互斥)。 -fn valid_plain_date_time_range(year: i32, month: u32, day: u32, time_ns: f64) -> bool { - const MAX_ISO_DAY: i128 = 100_000_000; - let limit = (MAX_ISO_DAY + 1) * 86_400_000_000_000; - match iso_date_time_epoch_ns(year, month, day, time_ns) { - Some(epoch_ns) => epoch_ns > -limit && epoch_ns < limit, - None => false, - } -} - -/// 读两位数字(basic 格式的月/日等紧凑分量)。 -fn read_iso2(t: &str, i: &mut usize, bytes: &[u8]) -> Result { - if *i + 2 > bytes.len() { - return Err("truncated ISO component".into()); - } - let two = &t[*i..*i + 2]; - *i += 2; - two.parse().map_err(|_| "invalid ISO component".to_string()) -} - -/// 从 Temporal ISO 字符串中提取日期分量 `(year, month, day)`。 -/// -/// # 支持 -/// - extended `YYYY-MM-DD` 与 basic `YYYYMMDD`,可带 `+`/`-` 符号(扩展年)。 -/// - 时间部分(`T`/`t` 起)与 offset(`Z`/`±HH:MM`)与 annotations 允许存在, -/// 日期提取时忽略(日历选择由调用方处理)。 -/// -/// # 边界 -/// - 空串、年份不足 4 位、月/日超界、零年(含 `-000000` 减零年)均拒绝。 -/// - 日期后的残余内容须以 `T`/`t`、offset、`[` 开头,否则拒绝。 -fn parse_iso_date(s: &str) -> Result<(i32, u32, u32), String> { - let t = s.trim().replace('\u{2212}', "-"); - if t.is_empty() { - return Err("invalid ISO string".into()); - } - let bytes = t.as_bytes(); - let mut i = 0usize; - let signed_year = i < bytes.len() && (bytes[i] == b'-' || bytes[i] == b'+'); - let negative = if signed_year { - let neg = bytes[i] == b'-'; - i += 1; - neg - } else { - false - }; - let y_start = i; - while i < bytes.len() && bytes[i].is_ascii_digit() { - i += 1; - } - let digits = &t[y_start..i]; - // basic 格式(无分隔符,年月日连续):8-10 位数字,前 4-6 位年,后 4 位月日。 - // 否则纯年(4-6 位),月/日由 `-MM-DD` 接续。 - let (year_digits, month_day) = if digits.len() >= 8 { - if !(8..=10).contains(&digits.len()) { - return Err("invalid ISO date".into()); - } - let y_len = digits.len() - 4; - if !(4..=6).contains(&y_len) { - return Err("invalid ISO year".into()); - } - (&digits[..y_len], Some(&digits[y_len..])) - } else { - (digits, None) - }; - let expected_year_digits = if signed_year { 6 } else { 4 }; - if year_digits.len() != expected_year_digits { - return Err("invalid ISO year".into()); - } - let mut year: i64 = year_digits.parse().map_err(|_| "invalid ISO year".to_string())?; - if negative { - year = -year; - } - let (month, day) = if let Some(md) = month_day { - if md.len() != 4 { - return Err("invalid ISO date".into()); - } - let m: u32 = md[..2].parse().map_err(|_| "invalid ISO month".to_string())?; - let d: u32 = md[2..].parse().map_err(|_| "invalid ISO day".to_string())?; - (m, d) - } else if i < bytes.len() && bytes[i] == b'-' { - i += 1; - let m = read_iso2(&t, &mut i, bytes)?; - if i >= bytes.len() || bytes[i] != b'-' { - return Err("invalid ISO date".into()); - } - i += 1; - let d = read_iso2(&t, &mut i, bytes)?; - (m, d) - } else { - return Err("missing month/day".into()); - }; - let rest = &t[i..]; - if !rest.is_empty() { - let ok = rest.starts_with(['T', 't', '[']); - if !ok { - return Err("invalid trailing content".into()); - } - } - if month == 0 || month > 12 || day == 0 { - return Err("invalid ISO date".into()); - } - if negative && year == 0 { - return Err("invalid ISO negative zero year".into()); - } - Ok((year as i32, month, day)) -} - -/// `Temporal.PlainDate` 构造器:`new PlainDate(year, month, day)`。 -pub fn plain_date_constructor(vm: &mut H, args: &[u8]) -> NativeResult { - let ctor_proto = vm.session().builtin_world().plain_date_proto.as_ptr(); - if !is_ctor_call(vm, args, ctor_proto) { - return NativeResult::Err(crate::error::create_type_error( - vm, - "Class constructor Temporal.PlainDate cannot be invoked without 'new'", - )); - } - // 分量转换序: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(vm: &mut H, args: &[u8]) -> NativeResult { - let val = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; - let (year, month, day, calendar) = if val.is_string() { - // 规范顺序:先 ParseTemporalDateString,再 ToTemporalOverflow(options)。 - let ymd = match parse_plain_date_string(&to_string(val)) { - Ok(ymd) => ymd, - Err(_) => { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO 8601 date")); - } - }; - native_try!(temporal_overflow(vm, args)); - (ymd.0, ymd.1, ymd.2, None) - } else { - let constrain = native_try!(temporal_overflow(vm, args)); - match object_date_ymd(vm, val, constrain) { - Ok(ymd) => ymd, - Err(error) => return NativeResult::Err(error), - } - }; - if !valid_iso_date(year, month, day) { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO date")); - } - // 表示范围(ISODateWithinLimits):-271821-04-19 … +275760-09-13。 - let day_count = days_from_civil(i128::from(year), i128::from(month), i128::from(day)); - if !(-100_000_001..=100_000_000).contains(&day_count) { - return NativeResult::Err(crate::error::create_range_error(vm, "ISO date is out of range")); - } - make_plain_date(vm, year, month, day, calendar.as_deref().unwrap_or("iso8601")) -} - -fn plain_date_ymd(vm: &mut H, args: &[u8]) -> Result<(f64, f64, f64), JsValue> { - let ptr = receiver_obj(vm, args)?; - let obj = unsafe { &*ptr }; - ensure_plain_date(vm, obj)?; - Ok((get_double_prop(obj, 0), get_double_prop(obj, 1), get_double_prop(obj, 2))) -} - -/// `Temporal.PlainDate.prototype.year` getter。 -pub fn plain_date_year(vm: &mut H, args: &[u8]) -> NativeResult { - let (y, _, _) = native_try!(plain_date_ymd(vm, args)); - NativeResult::Ok(JsValue::float(y)) -} - -/// `Temporal.PlainDate.prototype.month` getter。 -pub fn plain_date_month(vm: &mut H, args: &[u8]) -> NativeResult { - let (_, m, _) = native_try!(plain_date_ymd(vm, args)); - NativeResult::Ok(JsValue::float(m)) -} - -/// `Temporal.PlainDate.prototype.day` getter。 -pub fn plain_date_day(vm: &mut H, args: &[u8]) -> NativeResult { - let (_, _, d) = native_try!(plain_date_ymd(vm, args)); - NativeResult::Ok(JsValue::float(d)) -} - -/// `Temporal.PlainDate.prototype.toString()`:输出 `YYYY-MM-DD`。 -pub fn plain_date_to_string(vm: &mut H, args: &[u8]) -> NativeResult { - let ptr = native_try!(receiver_obj(vm, args)); - let obj = unsafe { &*ptr }; - native_try!(ensure_plain_date(vm, obj)); - let year = get_double_prop(obj, 0) as i32; - let month = get_double_prop(obj, 1) as u32; - let day = get_double_prop(obj, 2) as u32; - NativeResult::Ok(vm.new_string(&format!("{year:04}-{month:02}-{day:02}"))) -} - fn valid_plain_time(hour: u32, minute: u32, second: u32, ms: u32, us: u32, ns: u32) -> bool { hour <= 23 && minute <= 59 && second <= 59 && ms <= 999 && us <= 999 && ns <= 999 } @@ -1737,399 +1501,6 @@ pub fn plain_date_time_to_json(vm: &mut H, args: &[u8]) -> NativeResu plain_date_time_iso_string(vm, args) } -// ───────────────────── PlainDate 扩展方法 ───────────────────── - -/// 从 PlainDate receiver 读 year/month/day 并构造 chrono NaiveDate(供日期计算)。 -fn plain_date_naive(vm: &mut H, args: &[u8]) -> Result { - let (y, m, d) = plain_date_ymd(vm, args)?; - NaiveDate::from_ymd_opt(y as i32, m as u32, d as u32) - .ok_or_else(|| crate::error::create_range_error(vm, "invalid date")) -} - -/// 从 PlainDate receiver 读 year/month/day 并按 ISO 范围校验,不依赖 chrono 年份范围。 -fn plain_date_ymd_checked(vm: &mut H, args: &[u8]) -> Result<(i32, u32, u32), JsValue> { - let (y, m, d) = plain_date_ymd(vm, args)?; - if y.is_nan() || m.is_nan() || d.is_nan() { - return Err(crate::error::create_type_error(vm, "called on incompatible receiver")); - } - let ymd = (y.trunc() as i32, m.trunc() as u32, d.trunc() as u32); - if !valid_iso_date(ymd.0, ymd.1, ymd.2) { - return Err(crate::error::create_range_error(vm, "invalid ISO date")); - } - // 表示范围(PlainDate):-271821-04-19(第 -100,000,001 天)… +275760-09-13(第 +100,000,000 天), - // 超出 chrono NaiveDate 年份范围(约 ±26 万年)的日期也在此被拒绝而非后续 panic。 - let day_count = days_from_civil(i128::from(ymd.0), i128::from(ymd.1), i128::from(ymd.2)); - if !(-100_000_001..=100_000_000).contains(&day_count) { - return Err(crate::error::create_range_error(vm, "ISO date is out of range")); - } - Ok(ymd) -} - -/// 从对象式日期字段(`{year, month, day}`)读三字段与日历;PlainDate/PlainDateTime 实例 -/// 直接读内部槽;缺字段返回 None。 -fn object_date_ymd( - vm: &mut H, val: JsValue, constrain: bool, -) -> Result<(i32, u32, u32, Option), JsValue> { - if !val.is_object() { - return Err(crate::error::create_type_error(vm, "cannot convert to PlainDate")); - } - let ptr = val.as_js_object_ptr(); - if ptr.is_null() { - return Err(crate::error::create_type_error(vm, "cannot convert to PlainDate")); - } - let obj = unsafe { &*ptr }; - if obj.is_plain_date_time_obj() { - return Ok(( - get_double_prop(obj, 0) as i32, - get_double_prop(obj, 1) as u32, - get_double_prop(obj, 2) as u32, - Some(get_calendar_id(obj, 4)), - )); - } - if obj.is_plain_date_obj() { - return Ok(( - get_double_prop(obj, 0) as i32, - get_double_prop(obj, 1) as u32, - get_double_prop(obj, 2) as u32, - Some(get_calendar_id(obj, 3)), - )); - } - if obj.is_zoned_date_time_obj() { - let (year, month, day, _) = zoned_date_time_plain_parts(vm, obj)?; - return Ok((year, month, day, Some(get_calendar_id(obj, 2)))); - } - let (year, month, day, _, calendar) = plain_date_time_object_parts(vm, val, obj, constrain, true)?; - Ok((year, month, day, calendar)) -} - -fn date_like_ymd(vm: &mut H, val: JsValue) -> Result<(i32, u32, u32, Option), JsValue> { - let ymd = if val.is_string() { - let (y, m, d) = parse_plain_date_string(&to_string(val)) - .map_err(|_| crate::error::create_range_error(vm, "invalid ISO 8601 date"))?; - (y, m, d, None) - } else { - object_date_ymd(vm, val, true)? - }; - if !valid_iso_date(ymd.0, ymd.1, ymd.2) { - return Err(crate::error::create_range_error(vm, "invalid ISO date")); - } - // 表示范围(PlainDate):-271821-04-19(第 -100,000,001 天)… +275760-09-13(第 +100,000,000 天), - // 超出 chrono NaiveDate 年份范围(约 ±26 万年)的日期也在此被拒绝而非后续 panic。 - let day_count = days_from_civil(i128::from(ymd.0), i128::from(ymd.1), i128::from(ymd.2)); - if !(-100_000_001..=100_000_000).contains(&day_count) { - return Err(crate::error::create_range_error(vm, "ISO date is out of range")); - } - Ok(ymd) -} - -/// `Temporal.PlainDate.prototype.dayOfWeek` getter:ISO 周几(周一 1 … 周日 7)。 -pub fn plain_date_day_of_week(vm: &mut H, args: &[u8]) -> NativeResult { - let date = match plain_date_naive(vm, args) { - Ok(d) => d, - Err(e) => return NativeResult::Err(e), - }; - NativeResult::Ok(JsValue::float((date.weekday().num_days_from_monday() + 1) as f64)) -} - -/// `Temporal.PlainDate.prototype.dayOfYear` getter:年内第几天(1 起)。 -pub fn plain_date_day_of_year(vm: &mut H, args: &[u8]) -> NativeResult { - let date = match plain_date_naive(vm, args) { - Ok(d) => d, - Err(e) => return NativeResult::Err(e), - }; - NativeResult::Ok(JsValue::float(date.ordinal() as f64)) -} - -/// 计算给定年月的天数(含闰年)。 -fn days_in_month_iso(year: i32, month: u32) -> u32 { - 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(vm: &mut H, args: &[u8]) -> NativeResult { - let date = match plain_date_naive(vm, args) { - Ok(d) => d, - Err(e) => return NativeResult::Err(e), - }; - NativeResult::Ok(JsValue::float(days_in_month_iso(date.year(), date.month()) as f64)) -} - -/// `Temporal.PlainDate.prototype.daysInWeek` getter:恒 7。 -pub fn plain_date_days_in_week(vm: &mut H, args: &[u8]) -> NativeResult { - let _ = native_try!(plain_date_ymd(vm, args)); - NativeResult::Ok(JsValue::float(7.0)) -} - -fn is_leap_year_iso(year: i32) -> bool { - NaiveDate::from_ymd_opt(year, 2, 29).is_some() -} - -/// `Temporal.PlainDate.prototype.daysInYear` getter:366(闰年)或 365。 -pub fn plain_date_days_in_year(vm: &mut H, args: &[u8]) -> NativeResult { - let date = match plain_date_naive(vm, args) { - Ok(d) => d, - Err(e) => return NativeResult::Err(e), - }; - NativeResult::Ok(JsValue::float(if is_leap_year_iso(date.year()) { 366.0 } else { 365.0 })) -} - -/// `Temporal.PlainDate.prototype.monthsInYear` getter:恒 12(ISO 日历)。 -pub fn plain_date_months_in_year(vm: &mut H, args: &[u8]) -> NativeResult { - let _ = native_try!(plain_date_ymd(vm, args)); - NativeResult::Ok(JsValue::float(12.0)) -} - -/// `Temporal.PlainDate.prototype.inLeapYear` getter。 -pub fn plain_date_in_leap_year(vm: &mut H, args: &[u8]) -> NativeResult { - let date = match plain_date_naive(vm, args) { - Ok(d) => d, - Err(e) => return NativeResult::Err(e), - }; - NativeResult::Ok(JsValue::bool(is_leap_year_iso(date.year()))) -} - -/// `Temporal.PlainDate.prototype.weekOfYear` getter:ISO 周数。 -pub fn plain_date_week_of_year(vm: &mut H, args: &[u8]) -> NativeResult { - let date = match plain_date_naive(vm, args) { - Ok(d) => d, - Err(e) => return NativeResult::Err(e), - }; - NativeResult::Ok(JsValue::float(date.iso_week().week() as f64)) -} - -/// `Temporal.PlainDate.prototype.yearOfWeek` getter:ISO 周所属年。 -pub fn plain_date_year_of_week(vm: &mut H, args: &[u8]) -> NativeResult { - let date = match plain_date_naive(vm, args) { - Ok(d) => d, - Err(e) => return NativeResult::Err(e), - }; - NativeResult::Ok(JsValue::float(date.iso_week().year() as f64)) -} - -/// `Temporal.PlainDate.prototype.monthCode` getter:`M01`..`M12`。 -pub fn plain_date_month_code(vm: &mut H, args: &[u8]) -> NativeResult { - let (_, m, _) = match plain_date_ymd(vm, args) { - Ok(x) => x, - Err(e) => return NativeResult::Err(e), - }; - NativeResult::Ok(vm.new_string(&format!("M{m:02}"))) -} - -/// `Temporal.PlainDate.prototype.era` getter:ISO 日历无纪元,恒 undefined。 -pub fn plain_date_era(vm: &mut H, args: &[u8]) -> NativeResult { - let _ = native_try!(plain_date_ymd(vm, args)); - NativeResult::Ok(JsValue::undefined()) -} - -/// `Temporal.PlainDate.prototype.eraYear` getter:ISO 日历无纪元,恒 undefined。 -pub fn plain_date_era_year(vm: &mut H, args: &[u8]) -> NativeResult { - let _ = native_try!(plain_date_ymd(vm, args)); - NativeResult::Ok(JsValue::undefined()) -} - -/// `Temporal.PlainDate.prototype.calendarId`:读日历槽,兜底 ISO 8601。 -pub fn plain_date_calendar_id(vm: &mut H, args: &[u8]) -> NativeResult { - let ptr = native_try!(receiver_obj(vm, args)); - let obj = unsafe { &*ptr }; - native_try!(ensure_plain_date(vm, obj)); - NativeResult::Ok(vm.new_string(&get_calendar_id(obj, 3))) -} - -/// `Temporal.PlainDate.prototype.equals(other)`:比较年月日是否相等。 -pub fn plain_date_equals(vm: &mut H, args: &[u8]) -> NativeResult { - let (y, m, d) = match plain_date_ymd(vm, args) { - Ok(x) => x, - Err(e) => return NativeResult::Err(e), - }; - let other_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let other = match date_like_ymd(vm, other_val) { - Ok(x) => x, - Err(e) => return NativeResult::Err(e), - }; - let equal = other.0 as f64 == y && other.1 as f64 == m && other.2 as f64 == d; - NativeResult::Ok(JsValue::bool(equal)) -} - -/// `Temporal.PlainDate.compare(a, b)`:静态比较,返回 -1/0/1。 -pub fn plain_date_compare(vm: &mut H, args: &[u8]) -> NativeResult { - let a_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let b_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; - let a = match date_like_ymd(vm, a_val) { - Ok(x) => x, - Err(e) => return NativeResult::Err(e), - }; - let b = match date_like_ymd(vm, b_val) { - Ok(x) => x, - Err(e) => return NativeResult::Err(e), - }; - let cmp = (a.0, a.1, a.2).cmp(&(b.0, b.1, b.2)); - NativeResult::Ok(JsValue::float(match cmp { - std::cmp::Ordering::Less => -1.0, - std::cmp::Ordering::Equal => 0.0, - std::cmp::Ordering::Greater => 1.0, - })) -} - -/// `Temporal.PlainDate.prototype.valueOf()`:Temporal 对象禁止转原始值。 -pub fn plain_date_value_of(vm: &mut H, _args: &[u8]) -> NativeResult { - NativeResult::Err(crate::error::create_type_error(vm, "Temporal.PlainDate has no valueOf")) -} - -/// `Temporal.PlainDate.prototype.toJSON()`:输出 ISO 日期串(同 toString)。 -pub fn plain_date_to_json(vm: &mut H, args: &[u8]) -> NativeResult { - let ptr = match receiver_obj(vm, args) { - Ok(p) => p, - Err(e) => return NativeResult::Err(e), - }; - let obj = unsafe { &*ptr }; - if let Err(e) = ensure_plain_date(vm, obj) { - return NativeResult::Err(e); - } - let year = get_double_prop(obj, 0) as i32; - let month = get_double_prop(obj, 1) as u32; - let day = get_double_prop(obj, 2) as u32; - NativeResult::Ok(vm.new_string(&format!("{year:04}-{month:02}-{day:02}"))) -} - -/// 按 duration-like 日期字段对日期做加减(Temporal 大单位运算,月份不足日时取月末)。 -fn date_apply_duration(vm: &mut H, args: &[u8], sign: i64) -> NativeResult { - let ptr = native_try!(receiver_obj(vm, args)); - let obj = unsafe { &*ptr }; - let date = match plain_date_naive(vm, args) { - Ok(d) => d, - Err(e) => return NativeResult::Err(e), - }; - let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let values = match duration_like_values(vm, val) { - Ok(values) => values, - Err(error) => return NativeResult::Err(error), - }; - let [y, m, w, d, h, min, s, ms, us, ns] = values; - let mut result = date; - let total_months = ((y * 12.0 + m) * sign as f64) as i64; - if total_months != 0 { - if let Some(nd) = add_signed_months(result, total_months) { - result = nd; - } else { - return NativeResult::Err(crate::error::create_range_error(vm, "date out of range")); - } - } - let time_days = (h / 24.0 - + min / 1_440.0 - + s / 86_400.0 - + ms / 86_400_000.0 - + us / 86_400_000_000.0 - + ns / 86_400_000_000_000.0) - .trunc(); - let total_days = ((w * 7.0 + d + time_days) * sign as f64) as i64; - if total_days != 0 { - let next = if total_days >= 0 { - result.checked_add_days(Days::new(total_days as u64)) - } else { - result.checked_sub_days(Days::new(total_days.unsigned_abs())) - }; - if let Some(nd) = next { - result = nd; - } else { - return NativeResult::Err(crate::error::create_range_error(vm, "date out of range")); - } - } - make_plain_date(vm, result.year(), result.month(), result.day(), &get_calendar_id(obj, 3)) -} - -/// `Temporal.PlainDate.prototype.add(durationLike)`。 -pub fn plain_date_add(vm: &mut H, args: &[u8]) -> NativeResult { - date_apply_duration(vm, args, 1) -} - -/// `Temporal.PlainDate.prototype.subtract(durationLike)`。 -pub fn plain_date_subtract(vm: &mut H, args: &[u8]) -> NativeResult { - date_apply_duration(vm, args, -1) -} - -fn add_signed_months(date: NaiveDate, months: i64) -> Option { - let total = date.year() as i64 * 12 + date.month0() as i64 + months; - let year = total.div_euclid(12); - let month = total.rem_euclid(12) as u32 + 1; - let year = i32::try_from(year).ok()?; - let day = date.day().min(days_in_month_iso(year, month)); - NaiveDate::from_ymd_opt(year, month, day) -} - -/// `Temporal.PlainDate.prototype.until(other, options)`:date-only 单位 -/// (year/month/week/day)差值 + 舍入语义。 -pub fn plain_date_until(vm: &mut H, args: &[u8]) -> NativeResult { - let start = match plain_date_ymd_checked(vm, args) { - Ok(date) => date, - Err(error) => return NativeResult::Err(error), - }; - let other_value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let other = match date_like_ymd(vm, other_value) { - Ok(value) => value, - Err(error) => return NativeResult::Err(error), - }; - let options_value = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; - let settings = match parse_difference_settings(vm, options_value, true, 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(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。 diff --git a/crates/oxide_builtins/src/temporal/plain_date.rs b/crates/oxide_builtins/src/temporal/plain_date.rs new file mode 100644 index 00000000..49588c60 --- /dev/null +++ b/crates/oxide_builtins/src/temporal/plain_date.rs @@ -0,0 +1,647 @@ +//! Temporal.PlainDate:ISO 日期解析与校验、构造、getter、日历属性与加减/差值。 + +use chrono::{Datelike, Days, NaiveDate}; +use oxide_runtime_api::{to_string, NativeResult, VmHost}; +use oxide_types::object::JsObject; +use oxide_types::value::JsValue; + +use super::common::native_try; +use super::common::{ + days_from_civil, days_in_month, ensure_plain_date, get_calendar_id, get_double_prop, initialize_temporal_receiver, + is_ctor_call, parse_plain_date_string, plain_date_time_object_parts, receiver_obj, temporal_calendar_id_strict, + temporal_number_component, temporal_overflow, zoned_date_time_plain_parts, +}; +use super::difference::{difference_core, parse_difference_settings}; +use super::duration::duration_like_values; +use super::make_plain_date; + +/// 校验 ISO 日期分量(month 1-12、day 按月份与闰年,不依赖 chrono 年份范围)。 +pub(crate) fn valid_iso_date(year: i32, month: u32, day: u32) -> bool { + match days_in_month(i128::from(year), i128::from(month)) { + Some(days) => day != 0 && day as i128 <= days, + None => false, + } +} + +/// 计算 ISO 日期时间的纪元纳秒;分量须已通过 `valid_iso_date`。 +fn iso_date_time_epoch_ns(year: i32, month: u32, day: u32, time_ns: f64) -> Option { + let days = days_from_civil(i128::from(year), i128::from(month), i128::from(day)); + days.checked_mul(86_400_000_000_000)?.checked_add(time_ns as i128) +} + +/// 校验日期时间落在 PlainDateTime 表示范围(约 ±(10^8 + 1) 天,边界互斥)。 +pub(crate) 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 格式的月/日等紧凑分量)。 +pub(crate) fn read_iso2(t: &str, i: &mut usize, bytes: &[u8]) -> Result { + if *i + 2 > bytes.len() { + return Err("truncated ISO component".into()); + } + let two = &t[*i..*i + 2]; + *i += 2; + two.parse().map_err(|_| "invalid ISO component".to_string()) +} + +/// 从 Temporal ISO 字符串中提取日期分量 `(year, month, day)`。 +/// +/// # 支持 +/// - extended `YYYY-MM-DD` 与 basic `YYYYMMDD`,可带 `+`/`-` 符号(扩展年)。 +/// - 时间部分(`T`/`t` 起)与 offset(`Z`/`±HH:MM`)与 annotations 允许存在, +/// 日期提取时忽略(日历选择由调用方处理)。 +/// +/// # 边界 +/// - 空串、年份不足 4 位、月/日超界、零年(含 `-000000` 减零年)均拒绝。 +/// - 日期后的残余内容须以 `T`/`t`、offset、`[` 开头,否则拒绝。 +pub(crate) fn parse_iso_date(s: &str) -> Result<(i32, u32, u32), String> { + let t = s.trim().replace('\u{2212}', "-"); + if t.is_empty() { + return Err("invalid ISO string".into()); + } + let bytes = t.as_bytes(); + let mut i = 0usize; + let signed_year = i < bytes.len() && (bytes[i] == b'-' || bytes[i] == b'+'); + let negative = if signed_year { + let neg = bytes[i] == b'-'; + i += 1; + neg + } else { + false + }; + let y_start = i; + while i < bytes.len() && bytes[i].is_ascii_digit() { + i += 1; + } + let digits = &t[y_start..i]; + // basic 格式(无分隔符,年月日连续):8-10 位数字,前 4-6 位年,后 4 位月日。 + // 否则纯年(4-6 位),月/日由 `-MM-DD` 接续。 + let (year_digits, month_day) = if digits.len() >= 8 { + if !(8..=10).contains(&digits.len()) { + return Err("invalid ISO date".into()); + } + let y_len = digits.len() - 4; + if !(4..=6).contains(&y_len) { + return Err("invalid ISO year".into()); + } + (&digits[..y_len], Some(&digits[y_len..])) + } else { + (digits, None) + }; + let expected_year_digits = if signed_year { 6 } else { 4 }; + if year_digits.len() != expected_year_digits { + return Err("invalid ISO year".into()); + } + let mut year: i64 = year_digits.parse().map_err(|_| "invalid ISO year".to_string())?; + if negative { + year = -year; + } + let (month, day) = if let Some(md) = month_day { + if md.len() != 4 { + return Err("invalid ISO date".into()); + } + let m: u32 = md[..2].parse().map_err(|_| "invalid ISO month".to_string())?; + let d: u32 = md[2..].parse().map_err(|_| "invalid ISO day".to_string())?; + (m, d) + } else if i < bytes.len() && bytes[i] == b'-' { + i += 1; + let m = read_iso2(&t, &mut i, bytes)?; + if i >= bytes.len() || bytes[i] != b'-' { + return Err("invalid ISO date".into()); + } + i += 1; + let d = read_iso2(&t, &mut i, bytes)?; + (m, d) + } else { + return Err("missing month/day".into()); + }; + let rest = &t[i..]; + if !rest.is_empty() { + let ok = rest.starts_with(['T', 't', '[']); + if !ok { + return Err("invalid trailing content".into()); + } + } + if month == 0 || month > 12 || day == 0 { + return Err("invalid ISO date".into()); + } + if negative && year == 0 { + return Err("invalid ISO negative zero year".into()); + } + Ok((year as i32, month, day)) +} + +/// `Temporal.PlainDate` 构造器:`new PlainDate(year, month, day)`。 +pub fn plain_date_constructor(vm: &mut H, args: &[u8]) -> NativeResult { + let ctor_proto = vm.session().builtin_world().plain_date_proto.as_ptr(); + if !is_ctor_call(vm, args, ctor_proto) { + return NativeResult::Err(crate::error::create_type_error( + vm, + "Class constructor Temporal.PlainDate cannot be invoked without 'new'", + )); + } + // 分量转换序: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(vm: &mut H, args: &[u8]) -> NativeResult { + let val = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; + let (year, month, day, calendar) = if val.is_string() { + // 规范顺序:先 ParseTemporalDateString,再 ToTemporalOverflow(options)。 + let ymd = match parse_plain_date_string(&to_string(val)) { + Ok(ymd) => ymd, + Err(_) => { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO 8601 date")); + } + }; + native_try!(temporal_overflow(vm, args)); + (ymd.0, ymd.1, ymd.2, None) + } else { + let constrain = native_try!(temporal_overflow(vm, args)); + match object_date_ymd(vm, val, constrain) { + Ok(ymd) => ymd, + Err(error) => return NativeResult::Err(error), + } + }; + if !valid_iso_date(year, month, day) { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO date")); + } + // 表示范围(ISODateWithinLimits):-271821-04-19 … +275760-09-13。 + let day_count = days_from_civil(i128::from(year), i128::from(month), i128::from(day)); + if !(-100_000_001..=100_000_000).contains(&day_count) { + return NativeResult::Err(crate::error::create_range_error(vm, "ISO date is out of range")); + } + make_plain_date(vm, year, month, day, calendar.as_deref().unwrap_or("iso8601")) +} + +fn plain_date_ymd(vm: &mut H, args: &[u8]) -> Result<(f64, f64, f64), JsValue> { + let ptr = receiver_obj(vm, args)?; + let obj = unsafe { &*ptr }; + ensure_plain_date(vm, obj)?; + Ok((get_double_prop(obj, 0), get_double_prop(obj, 1), get_double_prop(obj, 2))) +} + +/// `Temporal.PlainDate.prototype.year` getter。 +pub fn plain_date_year(vm: &mut H, args: &[u8]) -> NativeResult { + let (y, _, _) = native_try!(plain_date_ymd(vm, args)); + NativeResult::Ok(JsValue::float(y)) +} + +/// `Temporal.PlainDate.prototype.month` getter。 +pub fn plain_date_month(vm: &mut H, args: &[u8]) -> NativeResult { + let (_, m, _) = native_try!(plain_date_ymd(vm, args)); + NativeResult::Ok(JsValue::float(m)) +} + +/// `Temporal.PlainDate.prototype.day` getter。 +pub fn plain_date_day(vm: &mut H, args: &[u8]) -> NativeResult { + let (_, _, d) = native_try!(plain_date_ymd(vm, args)); + NativeResult::Ok(JsValue::float(d)) +} + +/// `Temporal.PlainDate.prototype.toString()`:输出 `YYYY-MM-DD`。 +pub fn plain_date_to_string(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_plain_date(vm, obj)); + let year = get_double_prop(obj, 0) as i32; + let month = get_double_prop(obj, 1) as u32; + let day = get_double_prop(obj, 2) as u32; + NativeResult::Ok(vm.new_string(&format!("{year:04}-{month:02}-{day:02}"))) +} + +// ───────────────────── PlainDate 扩展方法 ───────────────────── + +/// 从 PlainDate receiver 读 year/month/day 并构造 chrono NaiveDate(供日期计算)。 +fn plain_date_naive(vm: &mut H, args: &[u8]) -> Result { + let (y, m, d) = plain_date_ymd(vm, args)?; + NaiveDate::from_ymd_opt(y as i32, m as u32, d as u32) + .ok_or_else(|| crate::error::create_range_error(vm, "invalid date")) +} + +/// 从 PlainDate receiver 读 year/month/day 并按 ISO 范围校验,不依赖 chrono 年份范围。 +fn plain_date_ymd_checked(vm: &mut H, args: &[u8]) -> Result<(i32, u32, u32), JsValue> { + let (y, m, d) = plain_date_ymd(vm, args)?; + if y.is_nan() || m.is_nan() || d.is_nan() { + return Err(crate::error::create_type_error(vm, "called on incompatible receiver")); + } + let ymd = (y.trunc() as i32, m.trunc() as u32, d.trunc() as u32); + if !valid_iso_date(ymd.0, ymd.1, ymd.2) { + return Err(crate::error::create_range_error(vm, "invalid ISO date")); + } + // 表示范围(PlainDate):-271821-04-19(第 -100,000,001 天)… +275760-09-13(第 +100,000,000 天), + // 超出 chrono NaiveDate 年份范围(约 ±26 万年)的日期也在此被拒绝而非后续 panic。 + let day_count = days_from_civil(i128::from(ymd.0), i128::from(ymd.1), i128::from(ymd.2)); + if !(-100_000_001..=100_000_000).contains(&day_count) { + return Err(crate::error::create_range_error(vm, "ISO date is out of range")); + } + Ok(ymd) +} + +/// 从对象式日期字段(`{year, month, day}`)读三字段与日历;PlainDate/PlainDateTime 实例 +/// 直接读内部槽;缺字段返回 None。 +fn object_date_ymd( + vm: &mut H, val: JsValue, constrain: bool, +) -> Result<(i32, u32, u32, Option), JsValue> { + if !val.is_object() { + return Err(crate::error::create_type_error(vm, "cannot convert to PlainDate")); + } + let ptr = val.as_js_object_ptr(); + if ptr.is_null() { + return Err(crate::error::create_type_error(vm, "cannot convert to PlainDate")); + } + let obj = unsafe { &*ptr }; + if obj.is_plain_date_time_obj() { + return Ok(( + get_double_prop(obj, 0) as i32, + get_double_prop(obj, 1) as u32, + get_double_prop(obj, 2) as u32, + Some(get_calendar_id(obj, 4)), + )); + } + if obj.is_plain_date_obj() { + return Ok(( + get_double_prop(obj, 0) as i32, + get_double_prop(obj, 1) as u32, + get_double_prop(obj, 2) as u32, + Some(get_calendar_id(obj, 3)), + )); + } + if obj.is_zoned_date_time_obj() { + let (year, month, day, _) = zoned_date_time_plain_parts(vm, obj)?; + return Ok((year, month, day, Some(get_calendar_id(obj, 2)))); + } + let (year, month, day, _, calendar) = plain_date_time_object_parts(vm, val, obj, constrain, true)?; + Ok((year, month, day, calendar)) +} + +fn date_like_ymd(vm: &mut H, val: JsValue) -> Result<(i32, u32, u32, Option), JsValue> { + let ymd = if val.is_string() { + let (y, m, d) = parse_plain_date_string(&to_string(val)) + .map_err(|_| crate::error::create_range_error(vm, "invalid ISO 8601 date"))?; + (y, m, d, None) + } else { + object_date_ymd(vm, val, true)? + }; + if !valid_iso_date(ymd.0, ymd.1, ymd.2) { + return Err(crate::error::create_range_error(vm, "invalid ISO date")); + } + // 表示范围(PlainDate):-271821-04-19(第 -100,000,001 天)… +275760-09-13(第 +100,000,000 天), + // 超出 chrono NaiveDate 年份范围(约 ±26 万年)的日期也在此被拒绝而非后续 panic。 + let day_count = days_from_civil(i128::from(ymd.0), i128::from(ymd.1), i128::from(ymd.2)); + if !(-100_000_001..=100_000_000).contains(&day_count) { + return Err(crate::error::create_range_error(vm, "ISO date is out of range")); + } + Ok(ymd) +} + +/// `Temporal.PlainDate.prototype.dayOfWeek` getter:ISO 周几(周一 1 … 周日 7)。 +pub fn plain_date_day_of_week(vm: &mut H, args: &[u8]) -> NativeResult { + let date = match plain_date_naive(vm, args) { + Ok(d) => d, + Err(e) => return NativeResult::Err(e), + }; + NativeResult::Ok(JsValue::float((date.weekday().num_days_from_monday() + 1) as f64)) +} + +/// `Temporal.PlainDate.prototype.dayOfYear` getter:年内第几天(1 起)。 +pub fn plain_date_day_of_year(vm: &mut H, args: &[u8]) -> NativeResult { + let date = match plain_date_naive(vm, args) { + Ok(d) => d, + Err(e) => return NativeResult::Err(e), + }; + NativeResult::Ok(JsValue::float(date.ordinal() as f64)) +} + +/// 计算给定年月的天数(含闰年)。 +pub(crate) 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(vm: &mut H, args: &[u8]) -> NativeResult { + let date = match plain_date_naive(vm, args) { + Ok(d) => d, + Err(e) => return NativeResult::Err(e), + }; + NativeResult::Ok(JsValue::float(days_in_month_iso(date.year(), date.month()) as f64)) +} + +/// `Temporal.PlainDate.prototype.daysInWeek` getter:恒 7。 +pub fn plain_date_days_in_week(vm: &mut H, args: &[u8]) -> NativeResult { + let _ = native_try!(plain_date_ymd(vm, args)); + NativeResult::Ok(JsValue::float(7.0)) +} + +pub(crate) fn is_leap_year_iso(year: i32) -> bool { + NaiveDate::from_ymd_opt(year, 2, 29).is_some() +} + +/// `Temporal.PlainDate.prototype.daysInYear` getter:366(闰年)或 365。 +pub fn plain_date_days_in_year(vm: &mut H, args: &[u8]) -> NativeResult { + let date = match plain_date_naive(vm, args) { + Ok(d) => d, + Err(e) => return NativeResult::Err(e), + }; + NativeResult::Ok(JsValue::float(if is_leap_year_iso(date.year()) { 366.0 } else { 365.0 })) +} + +/// `Temporal.PlainDate.prototype.monthsInYear` getter:恒 12(ISO 日历)。 +pub fn plain_date_months_in_year(vm: &mut H, args: &[u8]) -> NativeResult { + let _ = native_try!(plain_date_ymd(vm, args)); + NativeResult::Ok(JsValue::float(12.0)) +} + +/// `Temporal.PlainDate.prototype.inLeapYear` getter。 +pub fn plain_date_in_leap_year(vm: &mut H, args: &[u8]) -> NativeResult { + let date = match plain_date_naive(vm, args) { + Ok(d) => d, + Err(e) => return NativeResult::Err(e), + }; + NativeResult::Ok(JsValue::bool(is_leap_year_iso(date.year()))) +} + +/// `Temporal.PlainDate.prototype.weekOfYear` getter:ISO 周数。 +pub fn plain_date_week_of_year(vm: &mut H, args: &[u8]) -> NativeResult { + let date = match plain_date_naive(vm, args) { + Ok(d) => d, + Err(e) => return NativeResult::Err(e), + }; + NativeResult::Ok(JsValue::float(date.iso_week().week() as f64)) +} + +/// `Temporal.PlainDate.prototype.yearOfWeek` getter:ISO 周所属年。 +pub fn plain_date_year_of_week(vm: &mut H, args: &[u8]) -> NativeResult { + let date = match plain_date_naive(vm, args) { + Ok(d) => d, + Err(e) => return NativeResult::Err(e), + }; + NativeResult::Ok(JsValue::float(date.iso_week().year() as f64)) +} + +/// `Temporal.PlainDate.prototype.monthCode` getter:`M01`..`M12`。 +pub fn plain_date_month_code(vm: &mut H, args: &[u8]) -> NativeResult { + let (_, m, _) = match plain_date_ymd(vm, args) { + Ok(x) => x, + Err(e) => return NativeResult::Err(e), + }; + NativeResult::Ok(vm.new_string(&format!("M{m:02}"))) +} + +/// `Temporal.PlainDate.prototype.era` getter:ISO 日历无纪元,恒 undefined。 +pub fn plain_date_era(vm: &mut H, args: &[u8]) -> NativeResult { + let _ = native_try!(plain_date_ymd(vm, args)); + NativeResult::Ok(JsValue::undefined()) +} + +/// `Temporal.PlainDate.prototype.eraYear` getter:ISO 日历无纪元,恒 undefined。 +pub fn plain_date_era_year(vm: &mut H, args: &[u8]) -> NativeResult { + let _ = native_try!(plain_date_ymd(vm, args)); + NativeResult::Ok(JsValue::undefined()) +} + +/// `Temporal.PlainDate.prototype.calendarId`:读日历槽,兜底 ISO 8601。 +pub fn plain_date_calendar_id(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_plain_date(vm, obj)); + NativeResult::Ok(vm.new_string(&get_calendar_id(obj, 3))) +} + +/// `Temporal.PlainDate.prototype.equals(other)`:比较年月日是否相等。 +pub fn plain_date_equals(vm: &mut H, args: &[u8]) -> NativeResult { + let (y, m, d) = match plain_date_ymd(vm, args) { + Ok(x) => x, + Err(e) => return NativeResult::Err(e), + }; + let other_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let other = match date_like_ymd(vm, other_val) { + Ok(x) => x, + Err(e) => return NativeResult::Err(e), + }; + let equal = other.0 as f64 == y && other.1 as f64 == m && other.2 as f64 == d; + NativeResult::Ok(JsValue::bool(equal)) +} + +/// `Temporal.PlainDate.compare(a, b)`:静态比较,返回 -1/0/1。 +pub fn plain_date_compare(vm: &mut H, args: &[u8]) -> NativeResult { + let a_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let b_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; + let a = match date_like_ymd(vm, a_val) { + Ok(x) => x, + Err(e) => return NativeResult::Err(e), + }; + let b = match date_like_ymd(vm, b_val) { + Ok(x) => x, + Err(e) => return NativeResult::Err(e), + }; + let cmp = (a.0, a.1, a.2).cmp(&(b.0, b.1, b.2)); + NativeResult::Ok(JsValue::float(match cmp { + std::cmp::Ordering::Less => -1.0, + std::cmp::Ordering::Equal => 0.0, + std::cmp::Ordering::Greater => 1.0, + })) +} + +/// `Temporal.PlainDate.prototype.valueOf()`:Temporal 对象禁止转原始值。 +pub fn plain_date_value_of(vm: &mut H, _args: &[u8]) -> NativeResult { + NativeResult::Err(crate::error::create_type_error(vm, "Temporal.PlainDate has no valueOf")) +} + +/// `Temporal.PlainDate.prototype.toJSON()`:输出 ISO 日期串(同 toString)。 +pub fn plain_date_to_json(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = match receiver_obj(vm, args) { + Ok(p) => p, + Err(e) => return NativeResult::Err(e), + }; + let obj = unsafe { &*ptr }; + if let Err(e) = ensure_plain_date(vm, obj) { + return NativeResult::Err(e); + } + let year = get_double_prop(obj, 0) as i32; + let month = get_double_prop(obj, 1) as u32; + let day = get_double_prop(obj, 2) as u32; + NativeResult::Ok(vm.new_string(&format!("{year:04}-{month:02}-{day:02}"))) +} + +/// 按 duration-like 日期字段对日期做加减(Temporal 大单位运算,月份不足日时取月末)。 +fn date_apply_duration(vm: &mut H, args: &[u8], sign: i64) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + let date = match plain_date_naive(vm, args) { + Ok(d) => d, + Err(e) => return NativeResult::Err(e), + }; + let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let values = match duration_like_values(vm, val) { + Ok(values) => values, + Err(error) => return NativeResult::Err(error), + }; + let [y, m, w, d, h, min, s, ms, us, ns] = values; + let mut result = date; + let total_months = ((y * 12.0 + m) * sign as f64) as i64; + if total_months != 0 { + if let Some(nd) = add_signed_months(result, total_months) { + result = nd; + } else { + return NativeResult::Err(crate::error::create_range_error(vm, "date out of range")); + } + } + let time_days = (h / 24.0 + + min / 1_440.0 + + s / 86_400.0 + + ms / 86_400_000.0 + + us / 86_400_000_000.0 + + ns / 86_400_000_000_000.0) + .trunc(); + let total_days = ((w * 7.0 + d + time_days) * sign as f64) as i64; + if total_days != 0 { + let next = if total_days >= 0 { + result.checked_add_days(Days::new(total_days as u64)) + } else { + result.checked_sub_days(Days::new(total_days.unsigned_abs())) + }; + if let Some(nd) = next { + result = nd; + } else { + return NativeResult::Err(crate::error::create_range_error(vm, "date out of range")); + } + } + make_plain_date(vm, result.year(), result.month(), result.day(), &get_calendar_id(obj, 3)) +} + +/// `Temporal.PlainDate.prototype.add(durationLike)`。 +pub fn plain_date_add(vm: &mut H, args: &[u8]) -> NativeResult { + date_apply_duration(vm, args, 1) +} + +/// `Temporal.PlainDate.prototype.subtract(durationLike)`。 +pub fn plain_date_subtract(vm: &mut H, args: &[u8]) -> NativeResult { + date_apply_duration(vm, args, -1) +} + +fn add_signed_months(date: NaiveDate, months: i64) -> Option { + let total = date.year() as i64 * 12 + date.month0() as i64 + months; + let year = total.div_euclid(12); + let month = total.rem_euclid(12) as u32 + 1; + let year = i32::try_from(year).ok()?; + let day = date.day().min(days_in_month_iso(year, month)); + NaiveDate::from_ymd_opt(year, month, day) +} + +/// `Temporal.PlainDate.prototype.until(other, options)`:date-only 单位 +/// (year/month/week/day)差值 + 舍入语义。 +pub fn plain_date_until(vm: &mut H, args: &[u8]) -> NativeResult { + let start = match plain_date_ymd_checked(vm, args) { + Ok(date) => date, + Err(error) => return NativeResult::Err(error), + }; + let other_value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let other = match date_like_ymd(vm, other_value) { + Ok(value) => value, + Err(error) => return NativeResult::Err(error), + }; + let options_value = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; + let settings = match parse_difference_settings(vm, options_value, true, 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(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, + ) +}