From 7f012c21195204d5f680f3f9570e76f8b8d5dd67 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Thu, 17 Sep 2026 10:22:45 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20temporal=20=E5=B9=B4=E6=9C=88?= =?UTF-8?q?=E4=B8=8E=E6=9C=88=E6=97=A5=E6=97=8F=E6=8B=86=E5=87=BA=E5=8F=8C?= =?UTF-8?q?=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 | 1579 +---------------- .../src/temporal/plain_month_day.rs | 730 ++++++++ .../src/temporal/plain_year_month.rs | 875 +++++++++ 3 files changed, 1610 insertions(+), 1574 deletions(-) create mode 100644 crates/oxide_builtins/src/temporal/plain_month_day.rs create mode 100644 crates/oxide_builtins/src/temporal/plain_year_month.rs diff --git a/crates/oxide_builtins/src/temporal/mod.rs b/crates/oxide_builtins/src/temporal/mod.rs index dde2bba0..50462398 100644 --- a/crates/oxide_builtins/src/temporal/mod.rs +++ b/crates/oxide_builtins/src/temporal/mod.rs @@ -1,5 +1,5 @@ use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; -use oxide_runtime_api::{to_string, NativeResult, VmHost}; +use oxide_runtime_api::{NativeResult, VmHost}; use oxide_types::object::JsObject; use oxide_types::value::JsValue; @@ -9,7 +9,9 @@ mod duration; mod instant; mod plain_date; mod plain_date_time; +mod plain_month_day; mod plain_time; +mod plain_year_month; mod zoned_date_time; use common::*; @@ -18,7 +20,9 @@ pub use duration::*; pub use instant::*; pub use plain_date::*; pub use plain_date_time::*; +pub use plain_month_day::*; pub use plain_time::*; +pub use plain_year_month::*; pub use zoned_date_time::*; fn make_plain_date(vm: &mut H, year: i32, month: u32, day: u32, calendar: &str) -> NativeResult { @@ -107,1579 +111,6 @@ fn ensure_plain_year_month(vm: &mut H, obj: &JsObject) -> Result<(), Ok(()) } -/// 读 PlainMonthDay 的 月/日/参考年 三元组(槽 0-2),含 receiver 校验。 -fn plain_month_day_mdy(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(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(vm: &mut H, value: JsValue) -> Result { - 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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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::>() - } else { - if clock.len() % 2 != 0 || clock.len() > 6 { - return Err("invalid ISO time".into()); - } - clock - .as_bytes() - .chunks(2) - .map(std::str::from_utf8) - .collect::, _>>() - .map_err(|_| "invalid ISO time".to_string())? - }; - if fields.is_empty() || fields.len() > 3 || fields.iter().any(|field| field.len() != 2) { - return Err("invalid ISO time".into()); - } - if fraction.is_some() && fields.len() < 3 { - return Err("fractional hours and minutes are not valid".into()); - } - let parse_field = |index: usize| -> Result { - fields - .get(index) - .map_or(Ok(0), |field| field.parse().map_err(|_| "invalid ISO time".to_string())) - }; - let hour = parse_field(0)?; - let minute = parse_field(1)?; - let mut second = parse_field(2)?; - if second == 60 { - second = 59; - } - let subsecond = match fraction { - Some(digits) if !digits.is_empty() && digits.len() <= 9 && digits.bytes().all(|byte| byte.is_ascii_digit()) => { - let mut padded = digits.to_owned(); - padded.extend(std::iter::repeat('0').take(9 - digits.len())); - padded.parse::().map_err(|_| "invalid ISO fraction".to_string())? - } - Some(_) => return Err("invalid ISO fraction".into()), - None => 0, - }; - 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) -> Result<(i32, u32, Option), 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), 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::().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::() - .map_err(|_| "invalid ISO year".to_string())?; - let year = if negative { -year } else { year }; - let month = digits[month_start..month_start + 2] - .parse::() - .map_err(|_| "invalid ISO month".to_string())?; - let day = if digits.len() > month_start + 2 { - Some( - digits[month_start + 2..] - .parse::() - .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(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(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( - 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(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(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(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(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(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(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(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(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(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(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(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(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(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::*; diff --git a/crates/oxide_builtins/src/temporal/plain_month_day.rs b/crates/oxide_builtins/src/temporal/plain_month_day.rs new file mode 100644 index 00000000..363c55bb --- /dev/null +++ b/crates/oxide_builtins/src/temporal/plain_month_day.rs @@ -0,0 +1,730 @@ +//! Temporal.PlainMonthDay:ISO 月日解析、构造、getter 与 with/toPlainDate。 + +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::{ + calendar_annotation, days_from_civil, days_in_month_iso, ensure_plain_month_day, format_iso_year, get_calendar_id, + get_double_prop, initialize_temporal_receiver, is_ctor_call, make_plain_date, make_plain_month_day, + month_day_bag_calendar_id, read_iso2, receiver_obj, reject_partial_object_with_calendar_or_time_zone, + strip_iso_offset, temporal_calendar_id_strict, temporal_number_component, temporal_option_string, + temporal_option_value, temporal_overflow, temporal_to_show_calendar, valid_iso_date, valid_plain_time, + ShowCalendar, +}; + +/// 读 PlainMonthDay 的 月/日/参考年 三元组(槽 0-2),含 receiver 校验。 +fn plain_month_day_mdy(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))) +} + +/// 构造器日历参数解析(ToTemporalCalendarSlotValue,缺省 "iso8601"): +/// undefined → 缺省;字符串 → 严格白名单(非法 RangeError);Temporal 日期实例 → +/// 日历槽直读(不触发属性 getter);函数对象 → 缺省(无日历行为); +/// 其他对象与原始值 → TypeError。 +pub(crate) fn temporal_constructor_calendar_id(vm: &mut H, value: JsValue) -> Result { + 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(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.PlainMonthDay.prototype.day` getter(槽 1)。 +pub fn plain_month_day_day(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(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(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))) +} + +/// 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(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(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(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(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(vm: &mut H, _args: &[u8]) -> NativeResult { + NativeResult::Err(crate::error::create_type_error(vm, "Temporal.PlainMonthDay has no valueOf")) +} + +// ==================== PlainMonthDay.from ==================== + +/// PlainMonthDay 串注解校验:与 validate_temporal_annotation_suffix 同构, +/// 但 u-ca 值只接受 iso8601(大小写不敏感),其余内置日历 ID 拒绝。 +pub(crate) 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 设计符在调用侧先行拒绝。 +pub(crate) 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::>() + } else { + if clock.len() % 2 != 0 || clock.len() > 6 { + return Err("invalid ISO time".into()); + } + clock + .as_bytes() + .chunks(2) + .map(std::str::from_utf8) + .collect::, _>>() + .map_err(|_| "invalid ISO time".to_string())? + }; + if fields.is_empty() || fields.len() > 3 || fields.iter().any(|field| field.len() != 2) { + return Err("invalid ISO time".into()); + } + if fraction.is_some() && fields.len() < 3 { + return Err("fractional hours and minutes are not valid".into()); + } + let parse_field = |index: usize| -> Result { + fields + .get(index) + .map_or(Ok(0), |field| field.parse().map_err(|_| "invalid ISO time".to_string())) + }; + let hour = parse_field(0)?; + let minute = parse_field(1)?; + let mut second = parse_field(2)?; + if second == 60 { + second = 59; + } + let subsecond = match fraction { + Some(digits) if !digits.is_empty() && digits.len() <= 9 && digits.bytes().all(|byte| byte.is_ascii_digit()) => { + let mut padded = digits.to_owned(); + padded.extend(std::iter::repeat('0').take(9 - digits.len())); + padded.parse::().map_err(|_| "invalid ISO fraction".to_string())? + } + Some(_) => return Err("invalid ISO fraction".into()), + None => 0, + }; + 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) +} + +/// `Temporal.PlainMonthDay.from(item[, options])`:ISO 串 / PMD 实例 / PD 实例 / 字段对象。 +/// 参考年恒 1972(串中年份与 bag year 均不入结果,year 仅供 overflow 月长判断); +/// options.overflow 在全部字段 Get 之后读取。 +pub fn plain_month_day_from(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.PlainMonthDay.prototype.equals(other)`:比较 (月, 日, 参考年) 三元与日历标识; +/// other 经 ToTemporalMonthDay(串 / 字段对象 / 实例)转换。 +pub fn plain_month_day_equals(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(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(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) +} diff --git a/crates/oxide_builtins/src/temporal/plain_year_month.rs b/crates/oxide_builtins/src/temporal/plain_year_month.rs new file mode 100644 index 00000000..4ab562ae --- /dev/null +++ b/crates/oxide_builtins/src/temporal/plain_year_month.rs @@ -0,0 +1,875 @@ +//! Temporal.PlainYearMonth:ISO 年月解析、构造、getter、日历属性与 with/加减差值。 + +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::{ + calendar_annotation, compare_iso_date, days_from_civil, days_in_month_iso, difference_core, duration_like_values, + ensure_plain_year_month, format_iso_year, get_calendar_id, get_double_prop, initialize_temporal_receiver, + is_ctor_call, is_leap_year_iso, iso_year_month_within_limits, make_duration, make_plain_date, + make_plain_year_month, month_day_bag_calendar_id, native_engine_error, parse_year_month_difference_settings, + read_iso2, receiver_obj, reject_partial_object_with_calendar_or_time_zone, temporal_constructor_calendar_id, + temporal_number_component, temporal_option_string, temporal_option_value, temporal_overflow, + temporal_to_show_calendar, validate_month_day_annotation_suffix, validate_month_day_time_suffix, ShowCalendar, + MAX_ISO_DAY, +}; + +/// 读 PlainYearMonth 的 年/月/参考日 三元组(槽 0-2),含 receiver 校验。 +fn plain_year_month_ymd(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))) +} + +/// `Temporal.PlainYearMonth` 构造器:`new PlainYearMonth(year, month[, calendar[, referenceISODay]])`。 +pub fn plain_year_month_constructor(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.PlainYearMonth.prototype.year` getter(槽 0)。 +pub fn plain_year_month_year(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(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(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(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(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(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(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(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(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(vm: &mut H, args: &[u8]) -> NativeResult { + let _ = native_try!(plain_year_month_ymd(vm, args)); + NativeResult::Ok(JsValue::undefined()) +} + +/// 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(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(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(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(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(vm: &mut H, _args: &[u8]) -> NativeResult { + NativeResult::Err(crate::error::create_type_error(vm, "Temporal.PlainYearMonth has no valueOf")) +} + +/// 年月串边界:月 1..=12;日(若有)1..=31 且过语法级月日静态检查 +/// (2 月 30/31、30 天月份 31 拒;无参考年,2 月 29 不拒)。 +fn finish_year_month_range(year: i32, month: u32, day: Option) -> Result<(i32, u32, Option), 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), 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::().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::() + .map_err(|_| "invalid ISO year".to_string())?; + let year = if negative { -year } else { year }; + let month = digits[month_start..month_start + 2] + .parse::() + .map_err(|_| "invalid ISO month".to_string())?; + let day = if digits.len() > month_start + 2 { + Some( + digits[month_start + 2..] + .parse::() + .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.PlainYearMonth.from(item[, options])`:ISO 串 / PYM 实例 / PD 实例 / 字段对象。 +/// 串路径按 (年, 月) 查表示范围(日不参与);实例分支槽直读(PYM 复制保留参考日, +/// PD 参考日恒 1);bag 路径 year 必填、day 从不读取(参考日恒 1)、 +/// monthCode 先 ToPrimitive 再两段校验(语法先于 year 转换、适配在 year 转换后)。 +pub fn plain_year_month_from(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( + 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(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(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(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(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(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(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(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(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(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(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) +}