diff --git a/crates/oxide_builtins/src/temporal/mod.rs b/crates/oxide_builtins/src/temporal/mod.rs index 0fbe8a81..dde2bba0 100644 --- a/crates/oxide_builtins/src/temporal/mod.rs +++ b/crates/oxide_builtins/src/temporal/mod.rs @@ -1,7 +1,5 @@ -use chrono::{Datelike, NaiveDate}; - use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; -use oxide_runtime_api::{to_number, to_string, NativeResult, VmHost}; +use oxide_runtime_api::{to_string, NativeResult, VmHost}; use oxide_types::object::JsObject; use oxide_types::value::JsValue; @@ -10,6 +8,7 @@ mod difference; mod duration; mod instant; mod plain_date; +mod plain_date_time; mod plain_time; mod zoned_date_time; @@ -18,6 +17,7 @@ use difference::*; pub use duration::*; pub use instant::*; pub use plain_date::*; +pub use plain_date_time::*; pub use plain_time::*; pub use zoned_date_time::*; @@ -64,602 +64,6 @@ fn make_duration(vm: &mut H, values: [f64; 10]) -> NativeResult { NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj))) } -// ───────────────────── PlainDateTime 基础方法 ───────────────────── - -/// `Temporal.PlainDateTime` 构造器:保存 ISO 日期与午夜后纳秒。 -pub fn plain_date_time_constructor(vm: &mut H, args: &[u8]) -> NativeResult { - let ctor_proto = vm.session().builtin_world().plain_date_time_proto.as_ptr(); - if !is_ctor_call(vm, args, ctor_proto) { - return NativeResult::Err(crate::error::create_type_error( - vm, - "Class constructor Temporal.PlainDateTime cannot be invoked without 'new'", - )); - } - - let mut component = |index: usize, default: f64| -> Result { - if args.len() > index { - let raw = vm.reg(args[index]); - // 时间分量(缺省臂为有限值):显式 undefined 与缺参同义,取缺省值; - // 日期分量无缺省臂(缺省值为 NaN),undefined 仍经 NaN 统一抛 - // RangeError。 - if raw.is_undefined() && default.is_finite() { - return Ok(default); - } - temporal_option_number(vm, raw).map(f64::trunc) - } else { - Ok(default) - } - }; - let year_value = native_try!(component(1, f64::NAN)); - let month_value = native_try!(component(2, f64::NAN)); - let day_value = native_try!(component(3, f64::NAN)); - let hour_value = native_try!(component(4, 0.0)); - let minute_value = native_try!(component(5, 0.0)); - let second_value = native_try!(component(6, 0.0)); - let millisecond_value = native_try!(component(7, 0.0)); - let microsecond_value = native_try!(component(8, 0.0)); - let nanosecond_value = native_try!(component(9, 0.0)); - let components = [ - year_value, - month_value, - day_value, - hour_value, - minute_value, - second_value, - millisecond_value, - microsecond_value, - nanosecond_value, - ]; - if components.iter().any(|value| !value.is_finite()) { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time component")); - } - - let year = year_value as i32; - let month = month_value as u32; - let day = day_value as u32; - let hour = hour_value as u32; - let minute = minute_value as u32; - let second = second_value as u32; - let millisecond = millisecond_value as u32; - let microsecond = microsecond_value as u32; - let nanosecond = nanosecond_value as u32; - let non_negative = components[1..].iter().all(|value| *value >= 0.0); - if !non_negative - || !valid_iso_date(year, month, day) - || !valid_plain_time(hour, minute, second, millisecond, microsecond, nanosecond) - { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time component")); - } - - let calendar = if args.len() > 10 && !vm.reg(args[10]).is_undefined() { - native_try!(temporal_calendar_id_strict(vm, vm.reg(args[10]))).unwrap_or_else(|| "iso8601".to_string()) - } else { - "iso8601".to_string() - }; - - let total_ns = hour as f64 * 3_600_000_000_000.0 - + minute as f64 * 60_000_000_000.0 - + second as f64 * 1_000_000_000.0 - + millisecond as f64 * 1_000_000.0 - + microsecond as f64 * 1_000.0 - + nanosecond as f64; - if !valid_plain_date_time_range(year, month, day, total_ns) { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time component")); - } - let calendar_value = vm.new_string(&calendar); - initialize_temporal_receiver( - vm, - args, - JsObject::OBJ_TYPE_PLAIN_DATE_TIME, - [ - JsValue::float(year as f64), - JsValue::float(month as f64), - JsValue::float(day as f64), - JsValue::float(total_ns), - calendar_value, - ], - ) -} - -fn parse_plain_date_time_string(input: &str) -> Result<(i32, u32, u32, f64), String> { - parse_temporal_string_impl(input, true) -} - -fn plain_date_time_like_parts( - vm: &mut H, value: JsValue, constrain: bool, -) -> Result<(i32, u32, u32, f64, Option), JsValue> { - if value.is_string() { - return parse_plain_date_time_string(&to_string(value)) - .map(|(year, month, day, total_ns)| (year, month, day, total_ns, None)) - .map_err(|_| crate::error::create_range_error(vm, "invalid ISO 8601 date-time")); - } - if !value.is_object() { - return Err(crate::error::create_type_error(vm, "cannot convert to PlainDateTime")); - } - let ptr = value.as_js_object_ptr(); - if ptr.is_null() { - return Err(crate::error::create_type_error(vm, "cannot convert to PlainDateTime")); - } - let obj = unsafe { &*ptr }; - if obj.is_plain_date_time_obj() { - return Ok(( - get_double_prop(obj, 0) as i32, - get_double_prop(obj, 1) as u32, - get_double_prop(obj, 2) as u32, - get_double_prop(obj, 3), - Some(get_calendar_id(obj, 4)), - )); - } - if obj.is_plain_date_obj() { - return Ok(( - get_double_prop(obj, 0) as i32, - get_double_prop(obj, 1) as u32, - get_double_prop(obj, 2) as u32, - 0.0, - Some(get_calendar_id(obj, 3)), - )); - } - if obj.is_zoned_date_time_obj() { - return zoned_date_time_plain_parts(vm, obj) - .map(|(year, month, day, total_ns)| (year, month, day, total_ns, Some(get_calendar_id(obj, 2)))); - } - plain_date_time_object_parts(vm, value, obj, constrain, false) -} - -/// `Temporal.PlainDateTime.from(item)`:从实例、ISO 字符串或字段对象创建副本。 -pub fn plain_date_time_from(vm: &mut H, args: &[u8]) -> NativeResult { - let value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - if value.is_string() { - // 规范顺序:先 ParseTemporalDateTimeString,再 ToTemporalOverflow(options)。 - let (year, month, day, total_ns) = match parse_plain_date_time_string(&to_string(value)) { - Ok(parts) => parts, - Err(_) => { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO 8601 date-time")); - } - }; - native_try!(temporal_overflow(vm, args)); - make_plain_date_time(vm, year, month, day, total_ns, "iso8601") - } else { - let constrain = native_try!(temporal_overflow(vm, args)); - let (year, month, day, total_ns, calendar) = native_try!(plain_date_time_like_parts(vm, value, constrain)); - make_plain_date_time(vm, year, month, day, total_ns, calendar.as_deref().unwrap_or("iso8601")) - } -} - -/// `Temporal.PlainDateTime.compare(one, two)`:按 ISO 日期时间字段做字典序比较。 -pub fn plain_date_time_compare(vm: &mut H, args: &[u8]) -> NativeResult { - let one = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let two = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; - let one = native_try!(plain_date_time_like_parts(vm, one, true)); - let two = native_try!(plain_date_time_like_parts(vm, two, true)); - let ordering = (one.0, one.1, one.2, one.3 as u64).cmp(&(two.0, two.1, two.2, two.3 as u64)); - NativeResult::Ok(JsValue::int(match ordering { - std::cmp::Ordering::Less => -1, - std::cmp::Ordering::Equal => 0, - std::cmp::Ordering::Greater => 1, - })) -} - -/// `Temporal.PlainDateTime.prototype.equals(other)`:比较日期时间分量是否相等。 -pub fn plain_date_time_equals(vm: &mut H, args: &[u8]) -> NativeResult { - let (y, m, d, ns) = match plain_date_time_parts(vm, args) { - Ok(parts) => parts, - Err(e) => return NativeResult::Err(e), - }; - let other_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let other = match plain_date_time_like_parts(vm, other_val, true) { - Ok(parts) => parts, - Err(e) => return NativeResult::Err(e), - }; - let equal = other.0 == y && other.1 == m && other.2 == d && other.3 as u64 == ns as u64; - NativeResult::Ok(JsValue::bool(equal)) -} - -fn plain_date_time_parts(vm: &mut H, args: &[u8]) -> Result<(i32, u32, u32, f64), JsValue> { - let ptr = receiver_obj(vm, args)?; - let obj = unsafe { &*ptr }; - ensure_plain_date_time(vm, obj)?; - Ok(( - get_double_prop(obj, 0) as i32, - get_double_prop(obj, 1) as u32, - get_double_prop(obj, 2) as u32, - get_double_prop(obj, 3), - )) -} - -macro_rules! plain_date_time_date_getter { - ($name:ident, $index:tt) => { - pub fn $name(vm: &mut H, args: &[u8]) -> NativeResult { - let parts = native_try!(plain_date_time_parts(vm, args)); - NativeResult::Ok(JsValue::float(parts.$index as f64)) - } - }; -} - -plain_date_time_date_getter!(plain_date_time_year, 0); -plain_date_time_date_getter!(plain_date_time_month, 1); -plain_date_time_date_getter!(plain_date_time_day, 2); - -fn plain_date_time_naive(vm: &mut H, args: &[u8]) -> Result { - let (year, month, day, _) = plain_date_time_parts(vm, args)?; - NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| crate::error::create_range_error(vm, "invalid date")) -} - -/// `Temporal.PlainDateTime.prototype.dayOfWeek` getter。 -pub fn plain_date_time_day_of_week(vm: &mut H, args: &[u8]) -> NativeResult { - let date = native_try!(plain_date_time_naive(vm, args)); - NativeResult::Ok(JsValue::float((date.weekday().num_days_from_monday() + 1) as f64)) -} - -/// `Temporal.PlainDateTime.prototype.dayOfYear` getter。 -pub fn plain_date_time_day_of_year(vm: &mut H, args: &[u8]) -> NativeResult { - let date = native_try!(plain_date_time_naive(vm, args)); - NativeResult::Ok(JsValue::float(date.ordinal() as f64)) -} - -/// `Temporal.PlainDateTime.prototype.daysInMonth` getter。 -pub fn plain_date_time_days_in_month(vm: &mut H, args: &[u8]) -> NativeResult { - let date = native_try!(plain_date_time_naive(vm, args)); - NativeResult::Ok(JsValue::float(days_in_month_iso(date.year(), date.month()) as f64)) -} - -/// `Temporal.PlainDateTime.prototype.daysInWeek` getter。 -pub fn plain_date_time_days_in_week(vm: &mut H, args: &[u8]) -> NativeResult { - let _ = native_try!(plain_date_time_parts(vm, args)); - NativeResult::Ok(JsValue::float(7.0)) -} - -/// `Temporal.PlainDateTime.prototype.daysInYear` getter。 -pub fn plain_date_time_days_in_year(vm: &mut H, args: &[u8]) -> NativeResult { - let date = native_try!(plain_date_time_naive(vm, args)); - NativeResult::Ok(JsValue::float(if is_leap_year_iso(date.year()) { 366.0 } else { 365.0 })) -} - -/// `Temporal.PlainDateTime.prototype.monthsInYear` getter。 -pub fn plain_date_time_months_in_year(vm: &mut H, args: &[u8]) -> NativeResult { - let _ = native_try!(plain_date_time_parts(vm, args)); - NativeResult::Ok(JsValue::float(12.0)) -} - -/// `Temporal.PlainDateTime.prototype.inLeapYear` getter。 -pub fn plain_date_time_in_leap_year(vm: &mut H, args: &[u8]) -> NativeResult { - let date = native_try!(plain_date_time_naive(vm, args)); - NativeResult::Ok(JsValue::bool(is_leap_year_iso(date.year()))) -} - -/// `Temporal.PlainDateTime.prototype.weekOfYear` getter。 -pub fn plain_date_time_week_of_year(vm: &mut H, args: &[u8]) -> NativeResult { - let date = native_try!(plain_date_time_naive(vm, args)); - NativeResult::Ok(JsValue::float(date.iso_week().week() as f64)) -} - -/// `Temporal.PlainDateTime.prototype.yearOfWeek` getter。 -pub fn plain_date_time_year_of_week(vm: &mut H, args: &[u8]) -> NativeResult { - let date = native_try!(plain_date_time_naive(vm, args)); - NativeResult::Ok(JsValue::float(date.iso_week().year() as f64)) -} - -/// `Temporal.PlainDateTime.prototype.monthCode` getter。 -pub fn plain_date_time_month_code(vm: &mut H, args: &[u8]) -> NativeResult { - let (_, month, _, _) = native_try!(plain_date_time_parts(vm, args)); - NativeResult::Ok(vm.new_string(&format!("M{month:02}"))) -} - -/// ISO 日历没有可观察的 era 字段。 -pub fn plain_date_time_era(vm: &mut H, args: &[u8]) -> NativeResult { - let _ = native_try!(plain_date_time_parts(vm, args)); - NativeResult::Ok(JsValue::undefined()) -} - -/// ISO 日历没有可观察的 eraYear 字段。 -pub fn plain_date_time_era_year(vm: &mut H, args: &[u8]) -> NativeResult { - let _ = native_try!(plain_date_time_parts(vm, args)); - NativeResult::Ok(JsValue::undefined()) -} - -fn plain_date_time_time_get( - vm: &mut H, args: &[u8], select: fn(u32, u32, u32, u32, u32, u32) -> u32, -) -> NativeResult { - let (_, _, _, total_ns) = native_try!(plain_date_time_parts(vm, args)); - let (hour, minute, second, millisecond, microsecond, nanosecond) = plain_time_components(total_ns); - NativeResult::Ok(JsValue::float(select(hour, minute, second, millisecond, microsecond, nanosecond) as f64)) -} - -/// `Temporal.PlainDateTime.prototype.hour` getter。 -pub fn plain_date_time_hour(vm: &mut H, args: &[u8]) -> NativeResult { - plain_date_time_time_get(vm, args, |hour, _, _, _, _, _| hour) -} - -/// `Temporal.PlainDateTime.prototype.minute` getter。 -pub fn plain_date_time_minute(vm: &mut H, args: &[u8]) -> NativeResult { - plain_date_time_time_get(vm, args, |_, minute, _, _, _, _| minute) -} - -/// `Temporal.PlainDateTime.prototype.second` getter。 -pub fn plain_date_time_second(vm: &mut H, args: &[u8]) -> NativeResult { - plain_date_time_time_get(vm, args, |_, _, second, _, _, _| second) -} - -/// `Temporal.PlainDateTime.prototype.millisecond` getter。 -pub fn plain_date_time_millisecond(vm: &mut H, args: &[u8]) -> NativeResult { - plain_date_time_time_get(vm, args, |_, _, _, millisecond, _, _| millisecond) -} - -/// `Temporal.PlainDateTime.prototype.microsecond` getter。 -pub fn plain_date_time_microsecond(vm: &mut H, args: &[u8]) -> NativeResult { - plain_date_time_time_get(vm, args, |_, _, _, _, microsecond, _| microsecond) -} - -/// `Temporal.PlainDateTime.prototype.nanosecond` getter。 -pub fn plain_date_time_nanosecond(vm: &mut H, args: &[u8]) -> NativeResult { - plain_date_time_time_get(vm, args, |_, _, _, _, _, nanosecond| nanosecond) -} - -/// `Temporal.PlainDateTime.prototype.calendarId`:读日历槽,兜底 ISO 8601。 -pub fn plain_date_time_calendar_id(vm: &mut H, args: &[u8]) -> NativeResult { - let ptr = native_try!(receiver_obj(vm, args)); - let obj = unsafe { &*ptr }; - native_try!(ensure_plain_date_time(vm, obj)); - NativeResult::Ok(vm.new_string(&get_calendar_id(obj, 4))) -} - -/// 返回仅保留日期分量的新 `Temporal.PlainDate`,日历从 receiver 槽继承。 -pub fn plain_date_time_to_plain_date(vm: &mut H, args: &[u8]) -> NativeResult { - let ptr = native_try!(receiver_obj(vm, args)); - let obj = unsafe { &*ptr }; - let (year, month, day, _) = native_try!(plain_date_time_parts(vm, args)); - make_plain_date(vm, year, month, day, &get_calendar_id(obj, 4)) -} - -/// 返回仅保留时间分量的新 `Temporal.PlainTime`。 -pub fn plain_date_time_to_plain_time(vm: &mut H, args: &[u8]) -> NativeResult { - let (_, _, _, total_ns) = native_try!(plain_date_time_parts(vm, args)); - make_plain_time(vm, total_ns) -} - -/// Temporal 对象禁止隐式转换为原始值。 -pub fn plain_date_time_value_of(vm: &mut H, _args: &[u8]) -> NativeResult { - NativeResult::Err(crate::error::create_type_error(vm, "Temporal.PlainDateTime has no valueOf")) -} - -/// 格式化 PlainDateTime 本地 ISO 日期时间(无偏移),可裁秒与小数位并追加日历注解。 -fn format_plain_date_time_iso( - year: i32, month: u32, day: u32, time_ns: i128, include_seconds: bool, fractional_digits: Option, - calendar_name: &str, -) -> String { - let hour = time_ns / 3_600_000_000_000; - let minute = time_ns / 60_000_000_000 % 60; - let second = time_ns / 1_000_000_000 % 60; - let subsecond = time_ns % 1_000_000_000; - let mut output = format!("{}-{month:02}-{day:02}T{hour:02}:{minute:02}", format_iso_year(i128::from(year))); - if include_seconds { - output.push_str(&format!(":{second:02}")); - match fractional_digits { - Some(0) => {} - Some(digits) => { - let fraction = format!("{subsecond:09}"); - output.push('.'); - output.push_str(&fraction[..digits]); - } - None if subsecond != 0 => { - output.push('.'); - output.push_str(format!("{subsecond:09}").trim_end_matches('0')); - } - None => {} - } - } - match calendar_name { - "always" => output.push_str("[u-ca=iso8601]"), - "critical" => output.push_str("[!u-ca=iso8601]"), - _ => {} - } - output -} - -fn plain_date_time_iso_string(vm: &mut H, args: &[u8]) -> NativeResult { - let (year, month, day, total_ns) = native_try!(plain_date_time_parts(vm, args)); - let output = format_plain_date_time_iso(year, month, day, total_ns as i128, true, None, "never"); - NativeResult::Ok(vm.new_string_owned(output)) -} - -/// `Temporal.PlainDateTime.prototype.toString(options)`:按精度、舍入模式与日历显示输出 ISO 8601。 -pub fn plain_date_time_to_string(vm: &mut H, args: &[u8]) -> NativeResult { - let (year, month, day, total_ns) = native_try!(plain_date_time_parts(vm, args)); - let options_value = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; - let (calendar_name, fractional_input, mode_value, smallest_value) = if options_value.is_undefined() { - ("auto".to_string(), FractionalSecondDigitsInput::Auto, "trunc".to_string(), None) - } else { - if !options_value.is_object() { - return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object")); - } - let options_ptr = options_value.as_js_object_ptr(); - if options_ptr.is_null() { - return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object")); - } - let options = unsafe { &*options_ptr }; - let calendar_raw = native_try!(temporal_option_value(vm, options, options_value, "calendarName")); - let calendar_name = if calendar_raw.is_undefined() { - "auto".to_string() - } else { - native_try!(temporal_option_string(vm, calendar_raw)) - }; - let fractional_raw = native_try!(temporal_option_value(vm, options, options_value, "fractionalSecondDigits")); - let fractional_input = if fractional_raw.is_undefined() { - FractionalSecondDigitsInput::Auto - } else if fractional_raw.is_int() || fractional_raw.is_double() { - FractionalSecondDigitsInput::Number(to_number(fractional_raw)) - } else { - FractionalSecondDigitsInput::String(native_try!(temporal_option_string(vm, fractional_raw))) - }; - let mode_raw = native_try!(temporal_option_value(vm, options, options_value, "roundingMode")); - let mode_value = if mode_raw.is_undefined() { - "trunc".to_string() - } else { - native_try!(temporal_option_string(vm, mode_raw)) - }; - let smallest_raw = native_try!(temporal_option_value(vm, options, options_value, "smallestUnit")); - let smallest_value = if smallest_raw.is_undefined() { - None - } else { - Some(native_try!(temporal_option_string(vm, smallest_raw))) - }; - (calendar_name, fractional_input, mode_value, smallest_value) - }; - - match calendar_name.as_str() { - "auto" | "always" | "never" | "critical" => {} - _ => return NativeResult::Err(crate::error::create_range_error(vm, "invalid calendarName")), - } - let fractional_digits = native_try!(parse_fractional_second_digits(vm, fractional_input)); - let Some(mode) = instant_rounding_mode(&mode_value) else { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding mode")); - }; - let (quantum_ns, include_seconds, output_digits) = match smallest_value.as_deref() { - Some("minute" | "minutes") => (60_000_000_000, false, Some(0)), - Some("second" | "seconds") => (1_000_000_000, true, Some(0)), - Some("millisecond" | "milliseconds") => (1_000_000, true, Some(3)), - Some("microsecond" | "microseconds") => (1_000, true, Some(6)), - Some("nanosecond" | "nanoseconds") => (1, true, Some(9)), - Some(_) => return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallest unit")), - None => match fractional_digits { - Some(digits) => (10_i128.pow((9 - digits) as u32), true, Some(digits)), - None => (1, true, None), - }, - }; - const DAY_NS: i128 = 86_400_000_000_000; - let Some(rounded_ns) = round_instant_ns(total_ns as i128, quantum_ns, mode) else { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time")); - }; - let extra_days = rounded_ns / DAY_NS; - let time_ns = rounded_ns % DAY_NS; - let total_days = days_from_civil(i128::from(year), i128::from(month), i128::from(day)) + extra_days; - let (year, month, day) = civil_from_days(total_days); - if !valid_plain_date_time_range(year as i32, month as u32, day as u32, time_ns as f64) { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time")); - } - let output = format_plain_date_time_iso( - year as i32, - month as u32, - day as u32, - time_ns, - include_seconds, - output_digits, - &calendar_name, - ); - NativeResult::Ok(vm.new_string_owned(output)) -} - -/// 按 duration-like 分量对 PlainDateTime 做加减:先平衡时间(溢出为天), -/// 再按年/月/周/日推进日期,最后校验范围。 -fn plain_date_time_apply_duration(vm: &mut H, args: &[u8], sign: i64) -> NativeResult { - let ptr = native_try!(receiver_obj(vm, args)); - let obj = unsafe { &*ptr }; - let (year, month, day, time_ns) = match plain_date_time_parts(vm, args) { - Ok(parts) => parts, - Err(error) => return NativeResult::Err(error), - }; - let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let values = match duration_like_values(vm, val) { - Ok(values) => values, - Err(error) => return NativeResult::Err(error), - }; - let constrain = match temporal_overflow(vm, args) { - Ok(constrain) => constrain, - Err(error) => return NativeResult::Err(error), - }; - const DAY_NS: i128 = 86_400_000_000_000; - // 时间字段(hours 起)单独换算纳秒;days 由日期部分处理,避免重复计入。 - let [_, _, _, _, h, min, s, ms, us, ns] = values; - let time_delta = duration_component_integer(h).unwrap_or(0) * 3_600_000_000_000 - + duration_component_integer(min).unwrap_or(0) * 60_000_000_000 - + duration_component_integer(s).unwrap_or(0) * 1_000_000_000 - + duration_component_integer(ms).unwrap_or(0) * 1_000_000 - + duration_component_integer(us).unwrap_or(0) * 1_000 - + duration_component_integer(ns).unwrap_or(0); - let total_ns = time_ns as i128 + time_delta * sign as i128; - let extra_days = total_ns.div_euclid(DAY_NS); - let new_time_ns = total_ns.rem_euclid(DAY_NS); - - let [y, m, w, d, ..] = values; - let months = - (duration_component_integer(y).unwrap_or(0) * 12 + duration_component_integer(m).unwrap_or(0)) * sign as i128; - let total_month = i128::from(year) * 12 + i128::from(month) - 1 + months; - let ny = total_month.div_euclid(12); - let nm = total_month.rem_euclid(12) + 1; - let Some(max_day) = days_in_month(ny, nm) else { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid date")); - }; - let day = i128::from(day); - let new_day = if day > max_day { - if !constrain { - return NativeResult::Err(crate::error::create_range_error(vm, "day out of range")); - } - max_day - } else { - day - }; - let day_delta = duration_component_integer(w).unwrap_or(0) * 7 + duration_component_integer(d).unwrap_or(0); - let total_days = days_from_civil(ny, nm, new_day) + extra_days + day_delta * sign as i128; - let (yy, mm, dd) = civil_from_days(total_days); - if !valid_plain_date_time_range(yy as i32, mm as u32, dd as u32, new_time_ns as f64) { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time")); - } - make_plain_date_time(vm, yy as i32, mm as u32, dd as u32, new_time_ns as f64, &get_calendar_id(obj, 4)) -} - -/// 计算差值并舍入。until 返回 other 减 receiver,since 返回反向。 -fn plain_date_time_difference(vm: &mut H, args: &[u8], since: bool) -> NativeResult { - let (sy, sm, sd, st) = match plain_date_time_parts(vm, args) { - Ok(parts) => parts, - Err(error) => return NativeResult::Err(error), - }; - let other = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let (oy, om, od, ot, _) = match plain_date_time_like_parts(vm, other, true) { - Ok(parts) => parts, - Err(error) => return NativeResult::Err(error), - }; - let options_value = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; - let settings = match parse_difference_settings(vm, options_value, false, 3) { - Ok(settings) => settings, - Err(error) => return NativeResult::Err(error), - }; - difference_core( - vm, - (i128::from(sy), i128::from(sm), i128::from(sd)), - st as i128, - (i128::from(oy), i128::from(om), i128::from(od)), - ot as i128, - settings, - since, - 9, - ) -} - -/// `Temporal.PlainDateTime.prototype.until(other, options)`。 -pub fn plain_date_time_until(vm: &mut H, args: &[u8]) -> NativeResult { - plain_date_time_difference(vm, args, false) -} - -/// `Temporal.PlainDateTime.prototype.since(other, options)`。 -pub fn plain_date_time_since(vm: &mut H, args: &[u8]) -> NativeResult { - plain_date_time_difference(vm, args, true) -} - -/// `Temporal.PlainDateTime.prototype.add(durationLike, options)`。 -pub fn plain_date_time_add(vm: &mut H, args: &[u8]) -> NativeResult { - plain_date_time_apply_duration(vm, args, 1) -} - -/// `Temporal.PlainDateTime.prototype.subtract(durationLike, options)`。 -pub fn plain_date_time_subtract(vm: &mut H, args: &[u8]) -> NativeResult { - plain_date_time_apply_duration(vm, args, -1) -} - -/// `Temporal.PlainDateTime.prototype.toJSON()`:输出默认 ISO 日期时间。 -pub fn plain_date_time_to_json(vm: &mut H, args: &[u8]) -> NativeResult { - plain_date_time_iso_string(vm, args) -} - // Temporal.PlainMonthDay / Temporal.PlainYearMonth 对象地基: // 数字分量转换、槽对象构造、构造器与 getter/toString/toJSON。 diff --git a/crates/oxide_builtins/src/temporal/plain_date_time.rs b/crates/oxide_builtins/src/temporal/plain_date_time.rs new file mode 100644 index 00000000..852c3e97 --- /dev/null +++ b/crates/oxide_builtins/src/temporal/plain_date_time.rs @@ -0,0 +1,613 @@ +//! Temporal.PlainDateTime:ISO 日期时间解析、构造、getter、日历属性与加减/差值。 + +use chrono::{Datelike, NaiveDate}; +use oxide_runtime_api::{to_number, to_string, NativeResult, VmHost}; +use oxide_types::object::JsObject; +use oxide_types::value::JsValue; + +use super::common::native_try; +use super::{ + civil_from_days, days_from_civil, days_in_month, days_in_month_iso, difference_core, duration_component_integer, + duration_like_values, ensure_plain_date_time, format_iso_year, get_calendar_id, get_double_prop, + initialize_temporal_receiver, instant_rounding_mode, is_ctor_call, is_leap_year_iso, make_plain_date, + make_plain_date_time, make_plain_time, parse_difference_settings, parse_fractional_second_digits, + parse_temporal_string_impl, plain_date_time_object_parts, plain_time_components, receiver_obj, round_instant_ns, + temporal_calendar_id_strict, temporal_option_number, temporal_option_string, temporal_option_value, + temporal_overflow, valid_iso_date, valid_plain_date_time_range, valid_plain_time, zoned_date_time_plain_parts, + FractionalSecondDigitsInput, +}; +// ───────────────────── PlainDateTime 基础方法 ───────────────────── + +/// `Temporal.PlainDateTime` 构造器:保存 ISO 日期与午夜后纳秒。 +pub fn plain_date_time_constructor(vm: &mut H, args: &[u8]) -> NativeResult { + let ctor_proto = vm.session().builtin_world().plain_date_time_proto.as_ptr(); + if !is_ctor_call(vm, args, ctor_proto) { + return NativeResult::Err(crate::error::create_type_error( + vm, + "Class constructor Temporal.PlainDateTime cannot be invoked without 'new'", + )); + } + + let mut component = |index: usize, default: f64| -> Result { + if args.len() > index { + let raw = vm.reg(args[index]); + // 时间分量(缺省臂为有限值):显式 undefined 与缺参同义,取缺省值; + // 日期分量无缺省臂(缺省值为 NaN),undefined 仍经 NaN 统一抛 + // RangeError。 + if raw.is_undefined() && default.is_finite() { + return Ok(default); + } + temporal_option_number(vm, raw).map(f64::trunc) + } else { + Ok(default) + } + }; + let year_value = native_try!(component(1, f64::NAN)); + let month_value = native_try!(component(2, f64::NAN)); + let day_value = native_try!(component(3, f64::NAN)); + let hour_value = native_try!(component(4, 0.0)); + let minute_value = native_try!(component(5, 0.0)); + let second_value = native_try!(component(6, 0.0)); + let millisecond_value = native_try!(component(7, 0.0)); + let microsecond_value = native_try!(component(8, 0.0)); + let nanosecond_value = native_try!(component(9, 0.0)); + let components = [ + year_value, + month_value, + day_value, + hour_value, + minute_value, + second_value, + millisecond_value, + microsecond_value, + nanosecond_value, + ]; + if components.iter().any(|value| !value.is_finite()) { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time component")); + } + + let year = year_value as i32; + let month = month_value as u32; + let day = day_value as u32; + let hour = hour_value as u32; + let minute = minute_value as u32; + let second = second_value as u32; + let millisecond = millisecond_value as u32; + let microsecond = microsecond_value as u32; + let nanosecond = nanosecond_value as u32; + let non_negative = components[1..].iter().all(|value| *value >= 0.0); + if !non_negative + || !valid_iso_date(year, month, day) + || !valid_plain_time(hour, minute, second, millisecond, microsecond, nanosecond) + { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time component")); + } + + let calendar = if args.len() > 10 && !vm.reg(args[10]).is_undefined() { + native_try!(temporal_calendar_id_strict(vm, vm.reg(args[10]))).unwrap_or_else(|| "iso8601".to_string()) + } else { + "iso8601".to_string() + }; + + let total_ns = hour as f64 * 3_600_000_000_000.0 + + minute as f64 * 60_000_000_000.0 + + second as f64 * 1_000_000_000.0 + + millisecond as f64 * 1_000_000.0 + + microsecond as f64 * 1_000.0 + + nanosecond as f64; + if !valid_plain_date_time_range(year, month, day, total_ns) { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time component")); + } + let calendar_value = vm.new_string(&calendar); + initialize_temporal_receiver( + vm, + args, + JsObject::OBJ_TYPE_PLAIN_DATE_TIME, + [ + JsValue::float(year as f64), + JsValue::float(month as f64), + JsValue::float(day as f64), + JsValue::float(total_ns), + calendar_value, + ], + ) +} + +pub(crate) fn parse_plain_date_time_string(input: &str) -> Result<(i32, u32, u32, f64), String> { + parse_temporal_string_impl(input, true) +} + +fn plain_date_time_like_parts( + vm: &mut H, value: JsValue, constrain: bool, +) -> Result<(i32, u32, u32, f64, Option), JsValue> { + if value.is_string() { + return parse_plain_date_time_string(&to_string(value)) + .map(|(year, month, day, total_ns)| (year, month, day, total_ns, None)) + .map_err(|_| crate::error::create_range_error(vm, "invalid ISO 8601 date-time")); + } + if !value.is_object() { + return Err(crate::error::create_type_error(vm, "cannot convert to PlainDateTime")); + } + let ptr = value.as_js_object_ptr(); + if ptr.is_null() { + return Err(crate::error::create_type_error(vm, "cannot convert to PlainDateTime")); + } + let obj = unsafe { &*ptr }; + if obj.is_plain_date_time_obj() { + return Ok(( + get_double_prop(obj, 0) as i32, + get_double_prop(obj, 1) as u32, + get_double_prop(obj, 2) as u32, + get_double_prop(obj, 3), + Some(get_calendar_id(obj, 4)), + )); + } + if obj.is_plain_date_obj() { + return Ok(( + get_double_prop(obj, 0) as i32, + get_double_prop(obj, 1) as u32, + get_double_prop(obj, 2) as u32, + 0.0, + Some(get_calendar_id(obj, 3)), + )); + } + if obj.is_zoned_date_time_obj() { + return zoned_date_time_plain_parts(vm, obj) + .map(|(year, month, day, total_ns)| (year, month, day, total_ns, Some(get_calendar_id(obj, 2)))); + } + plain_date_time_object_parts(vm, value, obj, constrain, false) +} + +/// `Temporal.PlainDateTime.from(item)`:从实例、ISO 字符串或字段对象创建副本。 +pub fn plain_date_time_from(vm: &mut H, args: &[u8]) -> NativeResult { + let value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + if value.is_string() { + // 规范顺序:先 ParseTemporalDateTimeString,再 ToTemporalOverflow(options)。 + let (year, month, day, total_ns) = match parse_plain_date_time_string(&to_string(value)) { + Ok(parts) => parts, + Err(_) => { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO 8601 date-time")); + } + }; + native_try!(temporal_overflow(vm, args)); + make_plain_date_time(vm, year, month, day, total_ns, "iso8601") + } else { + let constrain = native_try!(temporal_overflow(vm, args)); + let (year, month, day, total_ns, calendar) = native_try!(plain_date_time_like_parts(vm, value, constrain)); + make_plain_date_time(vm, year, month, day, total_ns, calendar.as_deref().unwrap_or("iso8601")) + } +} + +/// `Temporal.PlainDateTime.compare(one, two)`:按 ISO 日期时间字段做字典序比较。 +pub fn plain_date_time_compare(vm: &mut H, args: &[u8]) -> NativeResult { + let one = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let two = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; + let one = native_try!(plain_date_time_like_parts(vm, one, true)); + let two = native_try!(plain_date_time_like_parts(vm, two, true)); + let ordering = (one.0, one.1, one.2, one.3 as u64).cmp(&(two.0, two.1, two.2, two.3 as u64)); + NativeResult::Ok(JsValue::int(match ordering { + std::cmp::Ordering::Less => -1, + std::cmp::Ordering::Equal => 0, + std::cmp::Ordering::Greater => 1, + })) +} + +/// `Temporal.PlainDateTime.prototype.equals(other)`:比较日期时间分量是否相等。 +pub fn plain_date_time_equals(vm: &mut H, args: &[u8]) -> NativeResult { + let (y, m, d, ns) = match plain_date_time_parts(vm, args) { + Ok(parts) => parts, + Err(e) => return NativeResult::Err(e), + }; + let other_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let other = match plain_date_time_like_parts(vm, other_val, true) { + Ok(parts) => parts, + Err(e) => return NativeResult::Err(e), + }; + let equal = other.0 == y && other.1 == m && other.2 == d && other.3 as u64 == ns as u64; + NativeResult::Ok(JsValue::bool(equal)) +} + +fn plain_date_time_parts(vm: &mut H, args: &[u8]) -> Result<(i32, u32, u32, f64), JsValue> { + let ptr = receiver_obj(vm, args)?; + let obj = unsafe { &*ptr }; + ensure_plain_date_time(vm, obj)?; + Ok(( + get_double_prop(obj, 0) as i32, + get_double_prop(obj, 1) as u32, + get_double_prop(obj, 2) as u32, + get_double_prop(obj, 3), + )) +} + +macro_rules! plain_date_time_date_getter { + ($name:ident, $index:tt) => { + pub fn $name(vm: &mut H, args: &[u8]) -> NativeResult { + let parts = native_try!(plain_date_time_parts(vm, args)); + NativeResult::Ok(JsValue::float(parts.$index as f64)) + } + }; +} + +plain_date_time_date_getter!(plain_date_time_year, 0); +plain_date_time_date_getter!(plain_date_time_month, 1); +plain_date_time_date_getter!(plain_date_time_day, 2); + +fn plain_date_time_naive(vm: &mut H, args: &[u8]) -> Result { + let (year, month, day, _) = plain_date_time_parts(vm, args)?; + NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| crate::error::create_range_error(vm, "invalid date")) +} + +/// `Temporal.PlainDateTime.prototype.dayOfWeek` getter。 +pub fn plain_date_time_day_of_week(vm: &mut H, args: &[u8]) -> NativeResult { + let date = native_try!(plain_date_time_naive(vm, args)); + NativeResult::Ok(JsValue::float((date.weekday().num_days_from_monday() + 1) as f64)) +} + +/// `Temporal.PlainDateTime.prototype.dayOfYear` getter。 +pub fn plain_date_time_day_of_year(vm: &mut H, args: &[u8]) -> NativeResult { + let date = native_try!(plain_date_time_naive(vm, args)); + NativeResult::Ok(JsValue::float(date.ordinal() as f64)) +} + +/// `Temporal.PlainDateTime.prototype.daysInMonth` getter。 +pub fn plain_date_time_days_in_month(vm: &mut H, args: &[u8]) -> NativeResult { + let date = native_try!(plain_date_time_naive(vm, args)); + NativeResult::Ok(JsValue::float(days_in_month_iso(date.year(), date.month()) as f64)) +} + +/// `Temporal.PlainDateTime.prototype.daysInWeek` getter。 +pub fn plain_date_time_days_in_week(vm: &mut H, args: &[u8]) -> NativeResult { + let _ = native_try!(plain_date_time_parts(vm, args)); + NativeResult::Ok(JsValue::float(7.0)) +} + +/// `Temporal.PlainDateTime.prototype.daysInYear` getter。 +pub fn plain_date_time_days_in_year(vm: &mut H, args: &[u8]) -> NativeResult { + let date = native_try!(plain_date_time_naive(vm, args)); + NativeResult::Ok(JsValue::float(if is_leap_year_iso(date.year()) { 366.0 } else { 365.0 })) +} + +/// `Temporal.PlainDateTime.prototype.monthsInYear` getter。 +pub fn plain_date_time_months_in_year(vm: &mut H, args: &[u8]) -> NativeResult { + let _ = native_try!(plain_date_time_parts(vm, args)); + NativeResult::Ok(JsValue::float(12.0)) +} + +/// `Temporal.PlainDateTime.prototype.inLeapYear` getter。 +pub fn plain_date_time_in_leap_year(vm: &mut H, args: &[u8]) -> NativeResult { + let date = native_try!(plain_date_time_naive(vm, args)); + NativeResult::Ok(JsValue::bool(is_leap_year_iso(date.year()))) +} + +/// `Temporal.PlainDateTime.prototype.weekOfYear` getter。 +pub fn plain_date_time_week_of_year(vm: &mut H, args: &[u8]) -> NativeResult { + let date = native_try!(plain_date_time_naive(vm, args)); + NativeResult::Ok(JsValue::float(date.iso_week().week() as f64)) +} + +/// `Temporal.PlainDateTime.prototype.yearOfWeek` getter。 +pub fn plain_date_time_year_of_week(vm: &mut H, args: &[u8]) -> NativeResult { + let date = native_try!(plain_date_time_naive(vm, args)); + NativeResult::Ok(JsValue::float(date.iso_week().year() as f64)) +} + +/// `Temporal.PlainDateTime.prototype.monthCode` getter。 +pub fn plain_date_time_month_code(vm: &mut H, args: &[u8]) -> NativeResult { + let (_, month, _, _) = native_try!(plain_date_time_parts(vm, args)); + NativeResult::Ok(vm.new_string(&format!("M{month:02}"))) +} + +/// ISO 日历没有可观察的 era 字段。 +pub fn plain_date_time_era(vm: &mut H, args: &[u8]) -> NativeResult { + let _ = native_try!(plain_date_time_parts(vm, args)); + NativeResult::Ok(JsValue::undefined()) +} + +/// ISO 日历没有可观察的 eraYear 字段。 +pub fn plain_date_time_era_year(vm: &mut H, args: &[u8]) -> NativeResult { + let _ = native_try!(plain_date_time_parts(vm, args)); + NativeResult::Ok(JsValue::undefined()) +} + +fn plain_date_time_time_get( + vm: &mut H, args: &[u8], select: fn(u32, u32, u32, u32, u32, u32) -> u32, +) -> NativeResult { + let (_, _, _, total_ns) = native_try!(plain_date_time_parts(vm, args)); + let (hour, minute, second, millisecond, microsecond, nanosecond) = plain_time_components(total_ns); + NativeResult::Ok(JsValue::float(select(hour, minute, second, millisecond, microsecond, nanosecond) as f64)) +} + +/// `Temporal.PlainDateTime.prototype.hour` getter。 +pub fn plain_date_time_hour(vm: &mut H, args: &[u8]) -> NativeResult { + plain_date_time_time_get(vm, args, |hour, _, _, _, _, _| hour) +} + +/// `Temporal.PlainDateTime.prototype.minute` getter。 +pub fn plain_date_time_minute(vm: &mut H, args: &[u8]) -> NativeResult { + plain_date_time_time_get(vm, args, |_, minute, _, _, _, _| minute) +} + +/// `Temporal.PlainDateTime.prototype.second` getter。 +pub fn plain_date_time_second(vm: &mut H, args: &[u8]) -> NativeResult { + plain_date_time_time_get(vm, args, |_, _, second, _, _, _| second) +} + +/// `Temporal.PlainDateTime.prototype.millisecond` getter。 +pub fn plain_date_time_millisecond(vm: &mut H, args: &[u8]) -> NativeResult { + plain_date_time_time_get(vm, args, |_, _, _, millisecond, _, _| millisecond) +} + +/// `Temporal.PlainDateTime.prototype.microsecond` getter。 +pub fn plain_date_time_microsecond(vm: &mut H, args: &[u8]) -> NativeResult { + plain_date_time_time_get(vm, args, |_, _, _, _, microsecond, _| microsecond) +} + +/// `Temporal.PlainDateTime.prototype.nanosecond` getter。 +pub fn plain_date_time_nanosecond(vm: &mut H, args: &[u8]) -> NativeResult { + plain_date_time_time_get(vm, args, |_, _, _, _, _, nanosecond| nanosecond) +} + +/// `Temporal.PlainDateTime.prototype.calendarId`:读日历槽,兜底 ISO 8601。 +pub fn plain_date_time_calendar_id(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_plain_date_time(vm, obj)); + NativeResult::Ok(vm.new_string(&get_calendar_id(obj, 4))) +} + +/// 返回仅保留日期分量的新 `Temporal.PlainDate`,日历从 receiver 槽继承。 +pub fn plain_date_time_to_plain_date(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + let (year, month, day, _) = native_try!(plain_date_time_parts(vm, args)); + make_plain_date(vm, year, month, day, &get_calendar_id(obj, 4)) +} + +/// 返回仅保留时间分量的新 `Temporal.PlainTime`。 +pub fn plain_date_time_to_plain_time(vm: &mut H, args: &[u8]) -> NativeResult { + let (_, _, _, total_ns) = native_try!(plain_date_time_parts(vm, args)); + make_plain_time(vm, total_ns) +} + +/// Temporal 对象禁止隐式转换为原始值。 +pub fn plain_date_time_value_of(vm: &mut H, _args: &[u8]) -> NativeResult { + NativeResult::Err(crate::error::create_type_error(vm, "Temporal.PlainDateTime has no valueOf")) +} + +/// 格式化 PlainDateTime 本地 ISO 日期时间(无偏移),可裁秒与小数位并追加日历注解。 +fn format_plain_date_time_iso( + year: i32, month: u32, day: u32, time_ns: i128, include_seconds: bool, fractional_digits: Option, + calendar_name: &str, +) -> String { + let hour = time_ns / 3_600_000_000_000; + let minute = time_ns / 60_000_000_000 % 60; + let second = time_ns / 1_000_000_000 % 60; + let subsecond = time_ns % 1_000_000_000; + let mut output = format!("{}-{month:02}-{day:02}T{hour:02}:{minute:02}", format_iso_year(i128::from(year))); + if include_seconds { + output.push_str(&format!(":{second:02}")); + match fractional_digits { + Some(0) => {} + Some(digits) => { + let fraction = format!("{subsecond:09}"); + output.push('.'); + output.push_str(&fraction[..digits]); + } + None if subsecond != 0 => { + output.push('.'); + output.push_str(format!("{subsecond:09}").trim_end_matches('0')); + } + None => {} + } + } + match calendar_name { + "always" => output.push_str("[u-ca=iso8601]"), + "critical" => output.push_str("[!u-ca=iso8601]"), + _ => {} + } + output +} + +fn plain_date_time_iso_string(vm: &mut H, args: &[u8]) -> NativeResult { + let (year, month, day, total_ns) = native_try!(plain_date_time_parts(vm, args)); + let output = format_plain_date_time_iso(year, month, day, total_ns as i128, true, None, "never"); + NativeResult::Ok(vm.new_string_owned(output)) +} + +/// `Temporal.PlainDateTime.prototype.toString(options)`:按精度、舍入模式与日历显示输出 ISO 8601。 +pub fn plain_date_time_to_string(vm: &mut H, args: &[u8]) -> NativeResult { + let (year, month, day, total_ns) = native_try!(plain_date_time_parts(vm, args)); + let options_value = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; + let (calendar_name, fractional_input, mode_value, smallest_value) = if options_value.is_undefined() { + ("auto".to_string(), FractionalSecondDigitsInput::Auto, "trunc".to_string(), None) + } else { + if !options_value.is_object() { + return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object")); + } + let options_ptr = options_value.as_js_object_ptr(); + if options_ptr.is_null() { + return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object")); + } + let options = unsafe { &*options_ptr }; + let calendar_raw = native_try!(temporal_option_value(vm, options, options_value, "calendarName")); + let calendar_name = if calendar_raw.is_undefined() { + "auto".to_string() + } else { + native_try!(temporal_option_string(vm, calendar_raw)) + }; + let fractional_raw = native_try!(temporal_option_value(vm, options, options_value, "fractionalSecondDigits")); + let fractional_input = if fractional_raw.is_undefined() { + FractionalSecondDigitsInput::Auto + } else if fractional_raw.is_int() || fractional_raw.is_double() { + FractionalSecondDigitsInput::Number(to_number(fractional_raw)) + } else { + FractionalSecondDigitsInput::String(native_try!(temporal_option_string(vm, fractional_raw))) + }; + let mode_raw = native_try!(temporal_option_value(vm, options, options_value, "roundingMode")); + let mode_value = if mode_raw.is_undefined() { + "trunc".to_string() + } else { + native_try!(temporal_option_string(vm, mode_raw)) + }; + let smallest_raw = native_try!(temporal_option_value(vm, options, options_value, "smallestUnit")); + let smallest_value = if smallest_raw.is_undefined() { + None + } else { + Some(native_try!(temporal_option_string(vm, smallest_raw))) + }; + (calendar_name, fractional_input, mode_value, smallest_value) + }; + + match calendar_name.as_str() { + "auto" | "always" | "never" | "critical" => {} + _ => return NativeResult::Err(crate::error::create_range_error(vm, "invalid calendarName")), + } + let fractional_digits = native_try!(parse_fractional_second_digits(vm, fractional_input)); + let Some(mode) = instant_rounding_mode(&mode_value) else { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding mode")); + }; + let (quantum_ns, include_seconds, output_digits) = match smallest_value.as_deref() { + Some("minute" | "minutes") => (60_000_000_000, false, Some(0)), + Some("second" | "seconds") => (1_000_000_000, true, Some(0)), + Some("millisecond" | "milliseconds") => (1_000_000, true, Some(3)), + Some("microsecond" | "microseconds") => (1_000, true, Some(6)), + Some("nanosecond" | "nanoseconds") => (1, true, Some(9)), + Some(_) => return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallest unit")), + None => match fractional_digits { + Some(digits) => (10_i128.pow((9 - digits) as u32), true, Some(digits)), + None => (1, true, None), + }, + }; + const DAY_NS: i128 = 86_400_000_000_000; + let Some(rounded_ns) = round_instant_ns(total_ns as i128, quantum_ns, mode) else { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time")); + }; + let extra_days = rounded_ns / DAY_NS; + let time_ns = rounded_ns % DAY_NS; + let total_days = days_from_civil(i128::from(year), i128::from(month), i128::from(day)) + extra_days; + let (year, month, day) = civil_from_days(total_days); + if !valid_plain_date_time_range(year as i32, month as u32, day as u32, time_ns as f64) { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time")); + } + let output = format_plain_date_time_iso( + year as i32, + month as u32, + day as u32, + time_ns, + include_seconds, + output_digits, + &calendar_name, + ); + NativeResult::Ok(vm.new_string_owned(output)) +} + +/// 按 duration-like 分量对 PlainDateTime 做加减:先平衡时间(溢出为天), +/// 再按年/月/周/日推进日期,最后校验范围。 +fn plain_date_time_apply_duration(vm: &mut H, args: &[u8], sign: i64) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + let (year, month, day, time_ns) = match plain_date_time_parts(vm, args) { + Ok(parts) => parts, + Err(error) => return NativeResult::Err(error), + }; + let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let values = match duration_like_values(vm, val) { + Ok(values) => values, + Err(error) => return NativeResult::Err(error), + }; + let constrain = match temporal_overflow(vm, args) { + Ok(constrain) => constrain, + Err(error) => return NativeResult::Err(error), + }; + const DAY_NS: i128 = 86_400_000_000_000; + // 时间字段(hours 起)单独换算纳秒;days 由日期部分处理,避免重复计入。 + let [_, _, _, _, h, min, s, ms, us, ns] = values; + let time_delta = duration_component_integer(h).unwrap_or(0) * 3_600_000_000_000 + + duration_component_integer(min).unwrap_or(0) * 60_000_000_000 + + duration_component_integer(s).unwrap_or(0) * 1_000_000_000 + + duration_component_integer(ms).unwrap_or(0) * 1_000_000 + + duration_component_integer(us).unwrap_or(0) * 1_000 + + duration_component_integer(ns).unwrap_or(0); + let total_ns = time_ns as i128 + time_delta * sign as i128; + let extra_days = total_ns.div_euclid(DAY_NS); + let new_time_ns = total_ns.rem_euclid(DAY_NS); + + let [y, m, w, d, ..] = values; + let months = + (duration_component_integer(y).unwrap_or(0) * 12 + duration_component_integer(m).unwrap_or(0)) * sign as i128; + let total_month = i128::from(year) * 12 + i128::from(month) - 1 + months; + let ny = total_month.div_euclid(12); + let nm = total_month.rem_euclid(12) + 1; + let Some(max_day) = days_in_month(ny, nm) else { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid date")); + }; + let day = i128::from(day); + let new_day = if day > max_day { + if !constrain { + return NativeResult::Err(crate::error::create_range_error(vm, "day out of range")); + } + max_day + } else { + day + }; + let day_delta = duration_component_integer(w).unwrap_or(0) * 7 + duration_component_integer(d).unwrap_or(0); + let total_days = days_from_civil(ny, nm, new_day) + extra_days + day_delta * sign as i128; + let (yy, mm, dd) = civil_from_days(total_days); + if !valid_plain_date_time_range(yy as i32, mm as u32, dd as u32, new_time_ns as f64) { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time")); + } + make_plain_date_time(vm, yy as i32, mm as u32, dd as u32, new_time_ns as f64, &get_calendar_id(obj, 4)) +} + +/// 计算差值并舍入。until 返回 other 减 receiver,since 返回反向。 +fn plain_date_time_difference(vm: &mut H, args: &[u8], since: bool) -> NativeResult { + let (sy, sm, sd, st) = match plain_date_time_parts(vm, args) { + Ok(parts) => parts, + Err(error) => return NativeResult::Err(error), + }; + let other = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let (oy, om, od, ot, _) = match plain_date_time_like_parts(vm, other, true) { + Ok(parts) => parts, + Err(error) => return NativeResult::Err(error), + }; + let options_value = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; + let settings = match parse_difference_settings(vm, options_value, false, 3) { + Ok(settings) => settings, + Err(error) => return NativeResult::Err(error), + }; + difference_core( + vm, + (i128::from(sy), i128::from(sm), i128::from(sd)), + st as i128, + (i128::from(oy), i128::from(om), i128::from(od)), + ot as i128, + settings, + since, + 9, + ) +} + +/// `Temporal.PlainDateTime.prototype.until(other, options)`。 +pub fn plain_date_time_until(vm: &mut H, args: &[u8]) -> NativeResult { + plain_date_time_difference(vm, args, false) +} + +/// `Temporal.PlainDateTime.prototype.since(other, options)`。 +pub fn plain_date_time_since(vm: &mut H, args: &[u8]) -> NativeResult { + plain_date_time_difference(vm, args, true) +} + +/// `Temporal.PlainDateTime.prototype.add(durationLike, options)`。 +pub fn plain_date_time_add(vm: &mut H, args: &[u8]) -> NativeResult { + plain_date_time_apply_duration(vm, args, 1) +} + +/// `Temporal.PlainDateTime.prototype.subtract(durationLike, options)`。 +pub fn plain_date_time_subtract(vm: &mut H, args: &[u8]) -> NativeResult { + plain_date_time_apply_duration(vm, args, -1) +} + +/// `Temporal.PlainDateTime.prototype.toJSON()`:输出默认 ISO 日期时间。 +pub fn plain_date_time_to_json(vm: &mut H, args: &[u8]) -> NativeResult { + plain_date_time_iso_string(vm, args) +}