From 96e24c84c14a0afaa2f8623cfe84312694cf18fb Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Wed, 14 Oct 2026 10:14:58 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Temporal=20=E4=B8=89=E6=97=8F=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20round=20=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 42 +-- Cargo.toml | 2 +- crates/oxide_builtins/src/temporal/common.rs | 63 ++++ .../oxide_builtins/src/temporal/duration.rs | 69 ++--- crates/oxide_builtins/src/temporal/instant.rs | 49 +-- .../oxide_builtins/src/temporal/plain_date.rs | 104 ++++++- .../src/temporal/plain_date_time.rs | 103 ++++++- .../oxide_builtins/src/temporal/plain_time.rs | 46 +-- .../src/temporal/plain_year_month.rs | 72 ++++- .../src/temporal/zoned_date_time.rs | 46 +-- crates/oxide_vm/src/bindings/bind_temporal.rs | 11 + .../oxide_vm/tests/builtins_temporal_tests.rs | 284 ++++++++++++++++++ 12 files changed, 673 insertions(+), 218 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c5c025bf..d7268d36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,7 +1160,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.19.11" +version = "0.19.12" dependencies = [ "chrono", "fast-float", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.19.11" +version = "0.19.12" dependencies = [ "num-bigint", "oxide_log", @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.19.11" +version = "0.19.12" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1204,7 +1204,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.19.11" +version = "0.19.12" dependencies = [ "ansi_term", "bumpalo", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.19.11" +version = "0.19.12" dependencies = [ "lru", "oxide_bytecode", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.19.11" +version = "0.19.12" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1258,7 +1258,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.19.11" +version = "0.19.12" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1272,7 +1272,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.19.11" +version = "0.19.12" dependencies = [ "num-bigint", "oxc_allocator", @@ -1288,7 +1288,7 @@ dependencies = [ [[package]] name = "oxide_engine" -version = "0.19.11" +version = "0.19.12" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1300,7 +1300,7 @@ dependencies = [ [[package]] name = "oxide_fold" -version = "0.19.11" +version = "0.19.12" dependencies = [ "num-bigint", "oxide_bytecode", @@ -1312,7 +1312,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.19.11" +version = "0.19.12" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1321,7 +1321,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.19.11" +version = "0.19.12" dependencies = [ "bumpalo", "dashmap", @@ -1335,7 +1335,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.19.11" +version = "0.19.12" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1347,7 +1347,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.19.11" +version = "0.19.12" dependencies = [ "tracing", "tracing-appender", @@ -1356,11 +1356,11 @@ dependencies = [ [[package]] name = "oxide_net" -version = "0.19.11" +version = "0.19.12" [[package]] name = "oxide_parser" -version = "0.19.11" +version = "0.19.12" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1375,7 +1375,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.19.11" +version = "0.19.12" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1391,7 +1391,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.19.11" +version = "0.19.12" dependencies = [ "num-bigint", "num-traits", @@ -1404,7 +1404,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.19.11" +version = "0.19.12" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1424,7 +1424,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.19.11" +version = "0.19.12" dependencies = [ "bumpalo", "dashmap", @@ -1437,7 +1437,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.19.11" +version = "0.19.12" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index a0761b14..ea19fb1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ members = [ ] [workspace.package] -version = "0.19.11" +version = "0.19.12" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_builtins/src/temporal/common.rs b/crates/oxide_builtins/src/temporal/common.rs index 8252666c..453ea66f 100644 --- a/crates/oxide_builtins/src/temporal/common.rs +++ b/crates/oxide_builtins/src/temporal/common.rs @@ -20,6 +20,7 @@ use oxide_runtime_api::{to_number, to_string, NativeResult, ProtoKind, VmHost}; use oxide_types::object::JsObject; use oxide_types::value::JsValue; +use super::instant::{instant_rounding_mode, InstantRoundingMode}; use super::time_zone::{tz_canonical_name, zone_offset_seconds}; use super::{ parse_iso_date, parse_plain_date_time_string, parse_plain_time_string, valid_iso_date, valid_plain_date_time_range, @@ -231,6 +232,68 @@ pub(crate) fn temporal_string_strict(vm: &mut H, value: JsValue) -> R Ok(to_string(primitive)) } +/// round 的 options 袋统一校验:roundTo 解析加 increment/mode/smallestUnit 读取与校验。 +/// +/// # 步骤 +/// 1. roundTo 字符串简写 → (1, halfExpand, Some(串));对象 → 按固定读序 +/// roundingIncrement → roundingMode → smallestUnit 依次取值,全部读完再统一校验。 +/// 2. increment:undefined → 1.0;temporal_option_number;有限性;1..=1e9;截断取整。 +/// 3. mode:undefined → "halfExpand";temporal_option_string;instant_rounding_mode +/// 查表(9 模式,None → RangeError)。 +/// 4. smallestUnit:undefined → None;temporal_option_string。 +/// +/// # 边界与前提 +/// - 非字符串非对象 → TypeError "roundTo must be a string or object"。 +/// - 单位表查表与真因子校验由调用方负责(单位表因类而异)。 +pub(crate) fn parse_round_options( + vm: &mut H, round_to: JsValue, +) -> Result<(i128, InstantRoundingMode, Option), JsValue> { + let (increment_raw, mode_raw, unit_raw) = if round_to.is_string() { + (JsValue::undefined(), JsValue::undefined(), round_to) + } else { + if !round_to.is_object() { + return Err(crate::error::create_type_error(vm, "roundTo must be a string or object")); + } + let options_ptr = round_to.as_js_object_ptr(); + if options_ptr.is_null() { + return Err(crate::error::create_type_error(vm, "roundTo must be a string or object")); + } + let options = unsafe { &*options_ptr }; + ( + temporal_option_value(vm, options, round_to, "roundingIncrement")?, + temporal_option_value(vm, options, round_to, "roundingMode")?, + temporal_option_value(vm, options, round_to, "smallestUnit")?, + ) + }; + let increment_value = if increment_raw.is_undefined() { + 1.0 + } else { + temporal_option_number(vm, increment_raw)? + }; + if !increment_value.is_finite() { + return Err(crate::error::create_range_error(vm, "invalid rounding increment")); + } + let increment = increment_value.trunc(); + if !(1.0..=1_000_000_000.0).contains(&increment) { + return Err(crate::error::create_range_error(vm, "invalid rounding increment")); + } + let mode_value = if mode_raw.is_undefined() { + "halfExpand".to_string() + } else { + temporal_option_string(vm, mode_raw)? + }; + let mode = match instant_rounding_mode(&mode_value) { + Some(mode) => mode, + None => return Err(crate::error::create_range_error(vm, "invalid rounding mode")), + }; + let smallest_unit = if unit_raw.is_undefined() { + None + } else { + Some(temporal_option_string(vm, unit_raw)?) + }; + Ok((increment as i128, mode, smallest_unit)) +} + /// 校验 `with` 族方法的 partial 参数并排除日历/时区载体。 /// /// 非对象、已初始化的 Temporal 日期时间类实例,或 calendar、timeZone 任一 diff --git a/crates/oxide_builtins/src/temporal/duration.rs b/crates/oxide_builtins/src/temporal/duration.rs index d3ebf636..c2d20f07 100644 --- a/crates/oxide_builtins/src/temporal/duration.rs +++ b/crates/oxide_builtins/src/temporal/duration.rs @@ -9,15 +9,15 @@ use super::common::native_try; use super::common::{ canonical_time_zone, civil_from_days, days_from_civil, ensure_duration, get_calendar_id, get_double_prop, get_instant_epoch_ns, initialize_temporal_receiver, is_ctor_call, local_to_epoch_ns, month_code_well_formed, - native_engine_error, receiver_obj, resolve_date_time_parts, temporal_calendar_id, temporal_option_number, - temporal_option_string, temporal_option_value, temporal_string_strict, zoned_date_time_plain_parts, DateTimeParts, + native_engine_error, parse_round_options, receiver_obj, resolve_date_time_parts, temporal_calendar_id, + temporal_option_number, temporal_option_string, temporal_option_value, temporal_string_strict, + zoned_date_time_plain_parts, DateTimeParts, }; use super::difference::{ add_date_duration, add_days_iso, balance_instant_difference, compare_iso_date, date_duration_sign, date_until_iso, nudge_iso_difference, nudge_window, plain_date_time_unit_index, round_instant_difference, DifferenceSettings, InternalDuration, IsoDateTime, DAY_NS, MAX_ISO_DAY, }; -use super::instant::{instant_rounding_mode, InstantRoundingMode}; use super::time_zone::zone_offset_seconds; use super::zoned_date_time::{ add_zoned_datetime, difference_zoned_datetime_with_rounding_vm, difference_zoned_datetime_with_total_vm, @@ -1240,14 +1240,9 @@ pub fn duration_round(vm: &mut H, args: &[u8]) -> NativeResult { } // roundTo 字符串 => { smallestUnit: 字符串 };否则必须为对象。 - let (largest_raw, relative_raw, increment_raw, mode_raw, smallest_raw) = if round_to.is_string() { - ( - JsValue::undefined(), - JsValue::undefined(), - JsValue::undefined(), - JsValue::undefined(), - round_to, - ) + // largestUnit/relativeTo 先于 increment 读取(读序钉死);其余三字段走统一 helper。 + let (largest_raw, relative_raw) = if round_to.is_string() { + (JsValue::undefined(), JsValue::undefined()) } else { if !round_to.is_object() { return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be an object")); @@ -1257,13 +1252,12 @@ pub fn duration_round(vm: &mut H, args: &[u8]) -> NativeResult { return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be an object")); } let options = unsafe { &*options_ptr }; - let largest_raw = native_try!(temporal_option_value(vm, options, round_to, "largestUnit")); - let relative_raw = native_try!(temporal_option_value(vm, options, round_to, "relativeTo")); - let increment_raw = native_try!(temporal_option_value(vm, options, round_to, "roundingIncrement")); - let mode_raw = native_try!(temporal_option_value(vm, options, round_to, "roundingMode")); - let smallest_raw = native_try!(temporal_option_value(vm, options, round_to, "smallestUnit")); - (largest_raw, relative_raw, increment_raw, mode_raw, smallest_raw) + ( + native_try!(temporal_option_value(vm, options, round_to, "largestUnit")), + native_try!(temporal_option_value(vm, options, round_to, "relativeTo")), + ) }; + let (increment, mode, smallest_unit) = native_try!(parse_round_options(vm, round_to)); // largestUnit:允许 "auto";缺失/undefined 视为未提供。 let largest_provided = !largest_raw.is_undefined(); @@ -1283,43 +1277,14 @@ pub fn duration_round(vm: &mut H, args: &[u8]) -> NativeResult { } }; - // roundingIncrement:ToIntegerOrInfinity 舍入后需在 [1, 10^9] 内。 - let increment_value = if increment_raw.is_undefined() { - 1.0 - } else { - native_try!(temporal_option_number(vm, increment_raw)) - }; - let increment = increment_value.trunc(); - if !increment.is_finite() || !(1.0..=1_000_000_000.0).contains(&increment) { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); - } - let increment = increment as i128; - - // roundingMode:缺失时 halfExpand。 - let mode = if mode_raw.is_undefined() { - InstantRoundingMode::HalfExpand - } else { - let mode_string = native_try!(temporal_option_string(vm, mode_raw)); - match instant_rounding_mode(&mode_string) { - Some(mode) => mode, - None => { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding mode")); - } - } - }; - // smallestUnit:缺失时 nanosecond。 - let smallest_provided = !smallest_raw.is_undefined(); - let smallest_index = if smallest_raw.is_undefined() { - 9 - } else { - let unit = native_try!(temporal_option_string(vm, smallest_raw)); - match plain_date_time_unit_index(&unit) { + let smallest_provided = smallest_unit.is_some(); + let smallest_index = match smallest_unit { + Some(unit) => match plain_date_time_unit_index(&unit) { Some(index) => index, - None => { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallestUnit")); - } - } + None => return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallestUnit")), + }, + None => 9, }; // 默认最大单位:现有最大单位与最小单位中较大(索引较小)的那个。 diff --git a/crates/oxide_builtins/src/temporal/instant.rs b/crates/oxide_builtins/src/temporal/instant.rs index d7abdfd0..043de55f 100644 --- a/crates/oxide_builtins/src/temporal/instant.rs +++ b/crates/oxide_builtins/src/temporal/instant.rs @@ -12,8 +12,8 @@ use super::{ balance_instant_difference, canonical_time_zone, civil_from_days, days_from_civil, days_in_month, duration_like_values, duration_time_nanoseconds, ensure_instant, format_iso_year, format_offset_seconds_text, get_instant_epoch_ns, initialize_temporal_receiver, is_ctor_call, make_duration, make_instant, - make_zoned_date_time, native_engine_error, parse_digits, parse_fractional_second_digits, receiver_obj, - round_instant_difference, temporal_option_number, temporal_option_string, temporal_option_value, + make_zoned_date_time, native_engine_error, parse_digits, parse_fractional_second_digits, parse_round_options, + receiver_obj, round_instant_difference, temporal_option_number, temporal_option_string, temporal_option_value, FractionalSecondDigitsInput, MAX_INSTANT_NS, }; @@ -551,55 +551,14 @@ pub fn instant_round(vm: &mut H, args: &[u8]) -> NativeResult { }; let round_to = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; - let (increment_value, mode_value, unit_value) = if round_to.is_string() { - (1.0, "halfExpand".to_string(), Some(to_string(round_to))) - } else { - if !round_to.is_object() { - return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be a string or object")); - } - let options_ptr = round_to.as_js_object_ptr(); - if options_ptr.is_null() { - return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be a string or object")); - } - let options = unsafe { &*options_ptr }; - let increment_raw = native_try!(temporal_option_value(vm, options, round_to, "roundingIncrement")); - let increment = if increment_raw.is_undefined() { - 1.0 - } else { - native_try!(temporal_option_number(vm, increment_raw)) - }; - let mode_raw = native_try!(temporal_option_value(vm, options, round_to, "roundingMode")); - let mode = if mode_raw.is_undefined() { - "halfExpand".to_string() - } else { - native_try!(temporal_option_string(vm, mode_raw)) - }; - let unit_raw = native_try!(temporal_option_value(vm, options, round_to, "smallestUnit")); - let unit = if unit_raw.is_undefined() { - None - } else { - Some(native_try!(temporal_option_string(vm, unit_raw))) - }; - (increment, mode, unit) - }; - - let Some(mode) = instant_rounding_mode(&mode_value) else { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding mode")); - }; + let (increment, mode, unit_value) = native_try!(parse_round_options(vm, round_to)); let Some(unit_value) = unit_value else { return NativeResult::Err(crate::error::create_range_error(vm, "smallestUnit is required")); }; let Some((unit_ns, units_per_day)) = instant_round_unit(&unit_value) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallest unit")); }; - if !increment_value.is_finite() { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); - } - let increment = increment_value.trunc(); - if !(1.0..=1_000_000_000.0).contains(&increment) { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); - } - let increment = increment as i128; + // 整除校验用每更高一级单位数量(Instant 语义:增量须整除一天)。 if units_per_day % increment != 0 { return NativeResult::Err(crate::error::create_range_error(vm, "rounding increment must divide a day")); } diff --git a/crates/oxide_builtins/src/temporal/plain_date.rs b/crates/oxide_builtins/src/temporal/plain_date.rs index f3ab42f7..647223e4 100644 --- a/crates/oxide_builtins/src/temporal/plain_date.rs +++ b/crates/oxide_builtins/src/temporal/plain_date.rs @@ -7,15 +7,18 @@ use oxide_types::value::JsValue; use super::common::native_try; use super::common::{ - days_from_civil, days_in_month, ensure_plain_date, get_calendar_id, get_double_prop, initialize_temporal_receiver, - is_ctor_call, parse_plain_date_string, plain_date_time_object_parts, receiver_obj, - reject_partial_object_with_calendar_or_time_zone, resolve_date_time_parts, temporal_calendar_id, - temporal_calendar_id_strict, temporal_number_component, temporal_option_string, temporal_option_value, - temporal_overflow, zoned_date_time_plain_parts, DateTimeParts, + civil_from_days, days_from_civil, days_in_month, ensure_plain_date, get_calendar_id, get_double_prop, + initialize_temporal_receiver, is_ctor_call, parse_plain_date_string, parse_round_options, + plain_date_time_object_parts, receiver_obj, reject_partial_object_with_calendar_or_time_zone, + resolve_date_time_parts, temporal_calendar_id, temporal_calendar_id_strict, temporal_number_component, + temporal_option_string, temporal_option_value, temporal_overflow, zoned_date_time_plain_parts, DateTimeParts, }; use super::difference::{difference_core, parse_difference_settings, IsoDateTime}; use super::duration::duration_like_values; -use super::{make_plain_date, make_plain_date_time, make_plain_month_day, make_plain_year_month, plain_time_like_ns}; +use super::{ + make_plain_date, make_plain_date_time, make_plain_month_day, make_plain_year_month, plain_time_like_ns, + round_instant_ns, +}; /// 校验 ISO 日期分量(month 1-12、day 按月份与闰年,不依赖 chrono 年份范围)。 pub(crate) fn valid_iso_date(year: i32, month: u32, day: u32) -> bool { @@ -779,6 +782,95 @@ pub fn plain_date_subtract(vm: &mut H, args: &[u8]) -> NativeResult { date_apply_duration(vm, args, -1) } +/// PlainDate round 的最小单位表:year/month/week/day。 +/// 返回 (单位日数, 最大增量);日历单位无整除约束(最大增量 0)。 +pub(crate) fn plain_date_round_unit(value: &str) -> Option<(i128, i128)> { + match value { + "year" | "years" => Some((0, 0)), + "month" | "months" => Some((0, 0)), + "week" | "weeks" => Some((7, 0)), + "day" | "days" => Some((1, 0)), + _ => None, + } +} + +/// `Temporal.PlainDate.prototype.round(roundTo)`:日历分量平衡到最小单位。 +/// +/// # 步骤 +/// 1. receiver 品牌校验;读槽 0-3 入本地(日历槽继承 receiver)。 +/// 2. roundTo 经 parse_round_options(undefined → TypeError;字符串简写;对象固定读序)。 +/// 3. 单位表查 smallestUnit(year/month/week/day;None → RangeError);缺失 → RangeError。 +/// 4. day/week:日数按 increment × 单位日数舍入;month/year:总月数/年数按 increment 舍入。 +/// 5. month → 日钳制到目标月末;year → 月/日钳制到 1/1。 +/// 6. valid_iso_date 加 ISODateWithinLimits(±10^8 天,下界 -10^8-1)越界抛 RangeError。 +/// 7. make_plain_date 重建(日历槽继承 receiver)。 +/// +/// # 边界与前提 +/// - 日历单位(year/month/week)的 increment 无真因子约束。 +/// - 日数换算走 days_from_civil/civil_from_days(proleptic Gregorian,精确)。 +pub fn plain_date_round(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_plain_date(vm, obj)); + let year = get_double_prop(obj, 0) as i32; + let month = get_double_prop(obj, 1) as u32; + let day = get_double_prop(obj, 2) as u32; + let calendar = get_calendar_id(obj, 3); + + let round_to = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let (increment, mode, unit_value) = native_try!(parse_round_options(vm, round_to)); + let Some(unit_value) = unit_value else { + return NativeResult::Err(crate::error::create_range_error(vm, "smallestUnit is required")); + }; + let (unit_days, max_increment) = match plain_date_round_unit(&unit_value) { + Some(entry) => entry, + None => return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallest unit")), + }; + if max_increment != 0 && (increment >= max_increment || max_increment % increment != 0) { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); + } + + let (year, month, day) = match unit_value.as_str() { + "day" | "days" | "week" | "weeks" => { + let days = days_from_civil(i128::from(year), i128::from(month), i128::from(day)); + let Some(quantum) = unit_days.checked_mul(increment) else { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); + }; + let Some(rounded_days) = round_instant_ns(days, quantum, mode) else { + return NativeResult::Err(crate::error::create_range_error(vm, "date is out of range")); + }; + let (year, month, day) = civil_from_days(rounded_days); + (year as i32, month as u32, day as u32) + } + "month" | "months" => { + let total_months = i128::from(year) * 12 + i128::from(month) - 1; + let Some(rounded_months) = round_instant_ns(total_months, increment, mode) else { + return NativeResult::Err(crate::error::create_range_error(vm, "date is out of range")); + }; + let year = rounded_months.div_euclid(12); + let month = rounded_months.rem_euclid(12) + 1; + let day = days_in_month(year, month).unwrap_or(31); + (year as i32, month as u32, day as u32) + } + "year" | "years" => { + let Some(rounded_year) = round_instant_ns(i128::from(year), increment, mode) else { + return NativeResult::Err(crate::error::create_range_error(vm, "date is out of range")); + }; + (rounded_year as i32, 1, 1) + } + _ => unreachable!(), + }; + if !valid_iso_date(year, month, day) { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO date")); + } + // 表示范围(ISODateWithinLimits):-271821-04-19 … +275760-09-13。 + let day_count = days_from_civil(i128::from(year), i128::from(month), i128::from(day)); + if !(-100_000_001..=100_000_000).contains(&day_count) { + return NativeResult::Err(crate::error::create_range_error(vm, "ISO date is out of range")); + } + make_plain_date(vm, year, month, day, &calendar) +} + fn add_signed_months(date: NaiveDate, months: i64) -> Option { let total = date.year() as i64 * 12 + date.month0() as i64 + months; let year = total.div_euclid(12); diff --git a/crates/oxide_builtins/src/temporal/plain_date_time.rs b/crates/oxide_builtins/src/temporal/plain_date_time.rs index bd4245fd..db8606b1 100644 --- a/crates/oxide_builtins/src/temporal/plain_date_time.rs +++ b/crates/oxide_builtins/src/temporal/plain_date_time.rs @@ -11,11 +11,12 @@ use super::{ 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, plain_time_like_ns, receiver_obj, - reject_partial_object_with_calendar_or_time_zone, resolve_date_time_parts, round_instant_ns, temporal_calendar_id, - temporal_calendar_id_strict, temporal_number_component, 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, DateTimeParts, FractionalSecondDigitsInput, IsoDateTime, + parse_round_options, parse_temporal_string_impl, plain_date_time_object_parts, plain_time_components, + plain_time_like_ns, receiver_obj, reject_partial_object_with_calendar_or_time_zone, resolve_date_time_parts, + round_instant_ns, temporal_calendar_id, temporal_calendar_id_strict, temporal_number_component, + 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, DateTimeParts, + FractionalSecondDigitsInput, IsoDateTime, }; // ───────────────────── PlainDateTime 基础方法 ───────────────────── @@ -827,6 +828,98 @@ pub fn plain_date_time_subtract(vm: &mut H, args: &[u8]) -> NativeRes plain_date_time_apply_duration(vm, args, -1) } +/// PlainDateTime round 的最小单位表:day + 6 个时间单位(不含 year/month/week)。 +/// 返回 (单位纳秒, 最大增量);day 的最大增量 = 1(仅 increment 1 合法)。 +pub(crate) fn plain_date_time_round_unit(value: &str) -> Option<(i128, i128)> { + match value { + "day" | "days" => Some((86_400_000_000_000, 1)), + "hour" | "hours" => Some((3_600_000_000_000, 24)), + "minute" | "minutes" => Some((60_000_000_000, 60)), + "second" | "seconds" => Some((1_000_000_000, 60)), + "millisecond" | "milliseconds" => Some((1_000_000, 1_000)), + "microsecond" | "microseconds" => Some((1_000, 1_000)), + "nanosecond" | "nanoseconds" => Some((1, 1_000)), + _ => None, + } +} + +/// `Temporal.PlainDateTime.prototype.round(roundTo)`:日历分量平衡加纳秒算术。 +/// +/// # 步骤 +/// 1. receiver 品牌校验;读槽 0-4 入本地(日历槽继承 receiver)。 +/// 2. roundTo 经 parse_round_options(undefined → TypeError;字符串简写;对象固定读序)。 +/// 3. 单位表查 smallestUnit(day..nanosecond;None → RangeError);缺失 → RangeError。 +/// 4. 真因子校验:day 仅 increment=1 合法(闭区间);时间单位增量须严格小于最大增量且整除。 +/// 5. day:整日纳秒(日数 × 24h + 时间槽)按 increment × 24h 舍入 → 日期进位回填,时间槽清零。 +/// 6. 时间单位:时间槽按单位纳秒 × increment 舍入;≥24h 进位到日(日期 +1,时间槽减 24h)。 +/// 7. valid_iso_date 加 valid_plain_date_time_range(严格不等式)越界抛 RangeError。 +/// 8. make_plain_date_time 重建(日历槽继承 receiver)。 +/// +/// # 边界与前提 +/// - 舍入跨午夜(如 23:59:59.9 舍入到秒 → 次日 00:00)合法,同 rounding-cross-midnight 语义。 +/// - 进位至多一天(quantum < 24h 且时间槽 < 24h)。 +pub fn plain_date_time_round(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)); + let year = get_double_prop(obj, 0) as i32; + let month = get_double_prop(obj, 1) as u32; + let day = get_double_prop(obj, 2) as u32; + let time_ns = get_double_prop(obj, 3) as i128; + let calendar = get_calendar_id(obj, 4); + + let round_to = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let (increment, mode, unit_value) = native_try!(parse_round_options(vm, round_to)); + let Some(unit_value) = unit_value else { + return NativeResult::Err(crate::error::create_range_error(vm, "smallestUnit is required")); + }; + let Some((unit_ns, max_increment)) = plain_date_time_round_unit(&unit_value) else { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallest unit")); + }; + const DAY_NS: i128 = 86_400_000_000_000; + // 真因子校验:day 为闭区间(上界 1,仅 increment=1 合法);时间单位增量须严格小于 + // 上一级单位数量且整除。 + let increment_valid = if unit_ns == DAY_NS { + increment == 1 + } else { + increment < max_increment && max_increment % increment == 0 + }; + if !increment_valid { + return NativeResult::Err(crate::error::create_range_error(vm, "rounding increment must divide a day")); + } + let (year, month, day, time_ns) = if unit_ns == DAY_NS { + // day:整日纳秒(日数 × 24h + 时间槽)舍入,日期进位回填,时间槽清零。 + let days = days_from_civil(i128::from(year), i128::from(month), i128::from(day)); + let Some(total_ns) = days.checked_mul(DAY_NS).and_then(|value| value.checked_add(time_ns)) else { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time")); + }; + let Some(rounded_ns) = round_instant_ns(total_ns, DAY_NS * increment, mode) else { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time")); + }; + let (year, month, day) = civil_from_days(rounded_ns.div_euclid(DAY_NS)); + (year as i32, month as u32, day as u32, rounded_ns.rem_euclid(DAY_NS)) + } else { + // 时间单位:时间槽舍入,≥24h 进位到日(至多一天)。 + let Some(quantum_ns) = unit_ns.checked_mul(increment) else { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); + }; + let Some(rounded_ns) = round_instant_ns(time_ns, quantum_ns, mode) else { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid time")); + }; + if rounded_ns >= DAY_NS { + let days = days_from_civil(i128::from(year), i128::from(month), i128::from(day)) + 1; + let (year, month, day) = civil_from_days(days); + (year as i32, month as u32, day as u32, rounded_ns - DAY_NS) + } else { + (year, month, day, rounded_ns) + } + }; + if !valid_iso_date(year, month, day) || !valid_plain_date_time_range(year, month, day, time_ns as f64) { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time")); + } + make_plain_date_time(vm, year, month, day, time_ns as f64, &calendar) +} + /// `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) diff --git a/crates/oxide_builtins/src/temporal/plain_time.rs b/crates/oxide_builtins/src/temporal/plain_time.rs index 1be7832d..8002f277 100644 --- a/crates/oxide_builtins/src/temporal/plain_time.rs +++ b/crates/oxide_builtins/src/temporal/plain_time.rs @@ -9,7 +9,8 @@ use super::{ difference_core, duration_component_integer, duration_like_values, ensure_plain_time, get_double_prop, initialize_temporal_receiver, instant_rounding_mode, instant_string_without_annotations, is_ctor_call, make_plain_time, parse_any_offset_seconds, parse_difference_settings, parse_fractional_second_digits, - parse_iso_date, receiver_obj, reject_partial_object_with_calendar_or_time_zone, round_instant_ns, + parse_iso_date, parse_round_options, receiver_obj, reject_partial_object_with_calendar_or_time_zone, + round_instant_ns, temporal_number_component, temporal_option_number, temporal_option_string, temporal_option_value, temporal_overflow, valid_offset_fraction, zoned_date_time_plain_parts, FractionalSecondDigitsInput, IsoDateTime, }; @@ -815,60 +816,19 @@ pub(crate) fn plain_time_round_unit(value: &str) -> Option<(i128, i128)> { /// - 舍入跨午夜(如 23:59:59.9 舍入到秒)→ 取模回 00:00,符合 rounding-cross-midnight 语义。 pub fn plain_time_round(vm: &mut H, args: &[u8]) -> NativeResult { let round_to = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let (increment_value, mode_value, unit_value) = if round_to.is_string() { - (1.0, "halfExpand".to_string(), Some(to_string(round_to))) - } else { - if !round_to.is_object() { - return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be a string or object")); - } - let options_ptr = round_to.as_js_object_ptr(); - if options_ptr.is_null() { - return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be a string or object")); - } - let options = unsafe { &*options_ptr }; - let increment_raw = native_try!(temporal_option_value(vm, options, round_to, "roundingIncrement")); - let increment = if increment_raw.is_undefined() { - 1.0 - } else { - native_try!(temporal_option_number(vm, increment_raw)) - }; - let mode_raw = native_try!(temporal_option_value(vm, options, round_to, "roundingMode")); - let mode = if mode_raw.is_undefined() { - "halfExpand".to_string() - } else { - native_try!(temporal_option_string(vm, mode_raw)) - }; - let unit_raw = native_try!(temporal_option_value(vm, options, round_to, "smallestUnit")); - let unit = if unit_raw.is_undefined() { - None - } else { - Some(native_try!(temporal_option_string(vm, unit_raw))) - }; - (increment, mode, unit) - }; + let (increment, mode, unit_value) = native_try!(parse_round_options(vm, round_to)); let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; native_try!(ensure_plain_time(vm, obj)); let time_ns = get_double_prop(obj, 0) as i128; - let Some(mode) = instant_rounding_mode(&mode_value) else { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding mode")); - }; let Some(unit_value) = unit_value else { return NativeResult::Err(crate::error::create_range_error(vm, "smallestUnit is required")); }; let Some((unit_ns, max_increment)) = plain_time_round_unit(&unit_value) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallest unit")); }; - if !increment_value.is_finite() { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); - } - let increment = increment_value.trunc(); - if !(1.0..=1_000_000_000.0).contains(&increment) { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); - } - let increment = increment as i128; // 真因子校验:增量须严格小于最大增量且能整除(MaximumTemporalDurationRoundingIncrement // 语义;instant_round 只查 % 的缺陷不在此沿用)。 if increment >= max_increment || max_increment % increment != 0 { diff --git a/crates/oxide_builtins/src/temporal/plain_year_month.rs b/crates/oxide_builtins/src/temporal/plain_year_month.rs index 8a61573d..81d6fe3d 100644 --- a/crates/oxide_builtins/src/temporal/plain_year_month.rs +++ b/crates/oxide_builtins/src/temporal/plain_year_month.rs @@ -9,8 +9,9 @@ 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, + make_plain_year_month, month_day_bag_calendar_id, native_engine_error, parse_round_options, + parse_year_month_difference_settings, read_iso2, receiver_obj, + reject_partial_object_with_calendar_or_time_zone, round_instant_ns, 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, IsoDateTime, ShowCalendar, MAX_ISO_DAY, @@ -688,6 +689,73 @@ pub fn plain_year_month_subtract(vm: &mut H, args: &[u8]) -> NativeRe plain_year_month_apply_duration(vm, args, -1) } +/// PlainYearMonth round 的最小单位表:year/month。 +/// 返回 (单位月数, 最大增量);日历单位无整除约束(最大增量 0)。 +pub(crate) fn plain_year_month_round_unit(value: &str) -> Option<(i128, i128)> { + match value { + "year" | "years" => Some((12, 0)), + "month" | "months" => Some((1, 0)), + _ => None, + } +} + +/// `Temporal.PlainYearMonth.prototype.round(roundTo)`:日历分量平衡。 +/// +/// # 步骤 +/// 1. receiver 品牌校验;读槽 0-3 入本地(参考日槽 2 与日历槽继承 receiver)。 +/// 2. roundTo 经 parse_round_options(undefined → TypeError;字符串简写;对象固定读序)。 +/// 3. 单位表查 smallestUnit(year/month;None → RangeError);缺失 → RangeError。 +/// 4. month:总月数(年 × 12 + 月 − 1)按 increment 舍入 → 年/月回填。 +/// 5. year:年数按 increment 舍入 → 月保持不变。 +/// 6. iso_year_month_within_limits 越界抛 RangeError。 +/// 7. make_plain_year_month 重建(参考日与日历槽继承 receiver)。 +/// +/// # 边界与前提 +/// - 日历单位(year/month)的 increment 无真因子约束。 +pub fn plain_year_month_round(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 year = get_double_prop(obj, 0) as i32; + let month = get_double_prop(obj, 1) as u32; + let ref_day = get_double_prop(obj, 2) as u32; + let calendar = get_calendar_id(obj, 3); + + let round_to = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let (increment, mode, unit_value) = native_try!(parse_round_options(vm, round_to)); + let Some(unit_value) = unit_value else { + return NativeResult::Err(crate::error::create_range_error(vm, "smallestUnit is required")); + }; + let (_, max_increment) = match plain_year_month_round_unit(&unit_value) { + Some(entry) => entry, + None => return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallest unit")), + }; + if max_increment != 0 && (increment >= max_increment || max_increment % increment != 0) { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); + } + + let (year, month) = match unit_value.as_str() { + "year" | "years" => { + let Some(rounded_year) = round_instant_ns(i128::from(year), increment, mode) else { + return NativeResult::Err(crate::error::create_range_error(vm, "year-month is out of range")); + }; + (rounded_year as i32, month) + } + "month" | "months" => { + let total_months = i128::from(year) * 12 + i128::from(month) - 1; + let Some(rounded_months) = round_instant_ns(total_months, increment, mode) else { + return NativeResult::Err(crate::error::create_range_error(vm, "year-month is out of range")); + }; + (rounded_months.div_euclid(12) as i32, rounded_months.rem_euclid(12) as u32 + 1) + } + _ => unreachable!(), + }; + if !iso_year_month_within_limits(year, month) { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid year-month")); + } + make_plain_year_month(vm, year, month, ref_day, &calendar) +} + /// `Temporal.PlainYearMonth.prototype.with(yearMonthLike[, options])`:覆盖 year/month/monthCode; /// undefined 不覆盖;calendar/timeZone 键拒绝;无识别字段 → TypeError。 /// diff --git a/crates/oxide_builtins/src/temporal/zoned_date_time.rs b/crates/oxide_builtins/src/temporal/zoned_date_time.rs index 76d05f5a..491fff88 100644 --- a/crates/oxide_builtins/src/temporal/zoned_date_time.rs +++ b/crates/oxide_builtins/src/temporal/zoned_date_time.rs @@ -11,7 +11,8 @@ use super::common::{ canonical_time_zone, civil_from_days, days_from_civil, days_in_month, ensure_zoned_date_time, fixed_offset_seconds, format_iso_year, format_offset_seconds_text, get_calendar_id, get_double_prop, get_instant_epoch_ns, initialize_temporal_receiver, is_ctor_call, local_to_epoch_ns, make_instant, make_zoned_date_time, parse_digits, - parse_fractional_second_digits, parse_temporal_string_impl, plain_date_time_object_parts, receiver_obj, + parse_fractional_second_digits, parse_round_options, parse_temporal_string_impl, plain_date_time_object_parts, + receiver_obj, reject_partial_object_with_calendar_or_time_zone, temporal_calendar_id, temporal_calendar_id_strict, temporal_option_number, temporal_option_string, temporal_option_value, temporal_overflow, temporal_string_strict, zoned_date_time_plain_parts, FractionalSecondDigitsInput, MAX_INSTANT_NS, @@ -1436,55 +1437,14 @@ pub fn zoned_date_time_round(vm: &mut H, args: &[u8]) -> NativeResult // roundTo 解析:字符串简写直接取用,对象选项按 roundingIncrement、roundingMode、 // smallestUnit 的固定读取顺序取值。 let round_to = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; - let (increment_value, mode_value, unit_value) = if round_to.is_string() { - (1.0, "halfExpand".to_string(), Some(to_string(round_to))) - } else { - if !round_to.is_object() { - return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be a string or object")); - } - let options_ptr = round_to.as_js_object_ptr(); - if options_ptr.is_null() { - return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be a string or object")); - } - let options = unsafe { &*options_ptr }; - let increment_raw = native_try!(temporal_option_value(vm, options, round_to, "roundingIncrement")); - let increment = if increment_raw.is_undefined() { - 1.0 - } else { - native_try!(temporal_option_number(vm, increment_raw)) - }; - let mode_raw = native_try!(temporal_option_value(vm, options, round_to, "roundingMode")); - let mode = if mode_raw.is_undefined() { - "halfExpand".to_string() - } else { - native_try!(temporal_option_string(vm, mode_raw)) - }; - let unit_raw = native_try!(temporal_option_value(vm, options, round_to, "smallestUnit")); - let unit = if unit_raw.is_undefined() { - None - } else { - Some(native_try!(temporal_option_string(vm, unit_raw))) - }; - (increment, mode, unit) - }; + let (increment, mode, unit_value) = native_try!(parse_round_options(vm, round_to)); - let Some(mode) = instant_rounding_mode(&mode_value) else { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding mode")); - }; let Some(unit_value) = unit_value else { return NativeResult::Err(crate::error::create_range_error(vm, "smallestUnit is required")); }; let Some((unit_ns, units_per_day)) = zoned_date_time_round_unit(&unit_value) else { return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallest unit")); }; - if !increment_value.is_finite() { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); - } - let increment = increment_value.trunc(); - if !(1.0..=1_000_000_000.0).contains(&increment) { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); - } - let increment = increment as i128; // 真因子校验:时间单位增量须小于更高一级单位数量且整除(hour/24、minute/60、second/60 拒绝); // day 特殊:更高一级数量=1,仅 increment==1 合法。 let increment_valid = if unit_ns == DAY_NS { diff --git a/crates/oxide_vm/src/bindings/bind_temporal.rs b/crates/oxide_vm/src/bindings/bind_temporal.rs index f47a1a6c..46aeb271 100644 --- a/crates/oxide_vm/src/bindings/bind_temporal.rs +++ b/crates/oxide_vm/src/bindings/bind_temporal.rs @@ -588,6 +588,7 @@ pub fn bind_temporal(env: BindEnv<'_>, global: &mut JsObject) { oxide_builtins::temporal::plain_date_to_plain_year_month:: as *const (), 0, ), + ("round", oxide_builtins::temporal::plain_date_round:: as *const (), 1), ], ); @@ -844,6 +845,11 @@ pub fn bind_temporal(env: BindEnv<'_>, global: &mut JsObject) { oxide_builtins::temporal::plain_date_time_with_plain_time:: as *const (), 0, ), + ( + "round", + oxide_builtins::temporal::plain_date_time_round:: as *const (), + 1, + ), ], ); bind_well_known_data_property( @@ -1139,6 +1145,11 @@ pub fn bind_temporal(env: BindEnv<'_>, global: &mut JsObject) { ), ("until", oxide_builtins::temporal::plain_year_month_until:: as *const (), 1), ("since", oxide_builtins::temporal::plain_year_month_since:: as *const (), 1), + ( + "round", + oxide_builtins::temporal::plain_year_month_round:: as *const (), + 1, + ), ], ); bind_well_known_data_property( diff --git a/crates/oxide_vm/tests/builtins_temporal_tests.rs b/crates/oxide_vm/tests/builtins_temporal_tests.rs index 09a97ec2..f2732f9f 100644 --- a/crates/oxide_vm/tests/builtins_temporal_tests.rs +++ b/crates/oxide_vm/tests/builtins_temporal_tests.rs @@ -4334,3 +4334,287 @@ fn plain_date_to_plain_month_day_and_year_month() { "M11|18|hebrew|1972-11-18[u-ca=hebrew]|1976|11|hebrew|1976-11-01[u-ca=hebrew]" ); } + +// -- Temporal round 族(PD/PDT/PYM 新 round 与统一 options 袋)-- + +#[test] +fn plain_date_round_calendar_units() { + let mut vm = Vm::new(); + // day:精确在日边界 → 原样。 + let r = eval(&mut vm, "new Temporal.PlainDate(1976, 11, 18).round('day').toString()").unwrap(); + assert_eq!(str_val(&vm, r), "1976-11-18"); + // week:1976-11-18 恰在周边界(epoch 锚定)→ 原样;次日舍入回边界。 + let r = eval(&mut vm, "new Temporal.PlainDate(1976, 11, 18).round('week').toString()").unwrap(); + assert_eq!(str_val(&vm, r), "1976-11-18"); + let r = eval(&mut vm, "new Temporal.PlainDate(1976, 11, 19).round('week').toString()").unwrap(); + assert_eq!(str_val(&vm, r), "1976-11-18"); + let r = eval(&mut vm, "new Temporal.PlainDate(1976, 11, 22).round('week').toString()").unwrap(); + assert_eq!(str_val(&vm, r), "1976-11-25"); + // month:日钳制到目标月末(11 月 30 天)。 + let r = eval(&mut vm, "new Temporal.PlainDate(1976, 11, 18).round('month').toString()").unwrap(); + assert_eq!(str_val(&vm, r), "1976-11-30"); + // month 增量:1976-10 恰在 3 月边界 → 原样(日钳制到 10 月末)。 + let r = eval( + &mut vm, + "new Temporal.PlainDate(1976, 10, 18).round({ smallestUnit: 'month', roundingIncrement: 3 }).toString()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "1976-10-31"); + // 1976-11 越界(总月数余 1)→ 舍入回 1976-10。 + let r = eval( + &mut vm, + "new Temporal.PlainDate(1976, 11, 18).round({ smallestUnit: 'month', roundingIncrement: 3 }).toString()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "1976-10-31"); + // 闰年 2 月末:2000-02-29 舍入到月 → 2000-02-29。 + let r = eval(&mut vm, "new Temporal.PlainDate(2000, 2, 29).round('month').toString()").unwrap(); + assert_eq!(str_val(&vm, r), "2000-02-29"); + // year:月/日钳制到 1/1。 + let r = eval(&mut vm, "new Temporal.PlainDate(1976, 11, 18).round('year').toString()").unwrap(); + assert_eq!(str_val(&vm, r), "1976-01-01"); + // year 增量:1977 按 4 舍入(1977 % 4 = 1 < 2)→ 1976-01-01。 + let r = eval( + &mut vm, + "new Temporal.PlainDate(1977, 6, 15).round({ smallestUnit: 'year', roundingIncrement: 4 }).toString()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "1976-01-01"); +} + +#[test] +fn plain_date_round_errors_and_calendar() { + let mut vm = Vm::new(); + // 缺参 / undefined → TypeError(roundTo 非串非对象)。 + let r = eval(&mut vm, "try { new Temporal.PlainDate(1976, 11, 18).round() } catch (e) { e.constructor.name }") + .unwrap(); + assert_eq!(str_val(&vm, r), "TypeError"); + // 对象缺 smallestUnit → RangeError。 + let r = eval(&mut vm, "try { new Temporal.PlainDate(1976, 11, 18).round({}) } catch (e) { e.constructor.name }") + .unwrap(); + assert_eq!(str_val(&vm, r), "RangeError"); + // 非法最小单位(PD 限 year/month/week/day)→ RangeError。 + let r = eval( + &mut vm, + "try { new Temporal.PlainDate(1976, 11, 18).round({ smallestUnit: 'hour' }) } catch (e) { e.constructor.name }", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "RangeError"); + // NaN 增量 → RangeError。 + let r = eval( + &mut vm, + "try { new Temporal.PlainDate(1976, 11, 18).round({ smallestUnit: 'week', roundingIncrement: NaN }) } catch (e) { e.constructor.name }", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "RangeError"); + // 日历槽继承 receiver。 + let r = eval( + &mut vm, + "new Temporal.PlainDate(1976, 11, 18, 'hebrew').round('month').calendarId", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "hebrew"); +} + +#[test] +fn plain_date_time_round_time_units() { + let mut vm = Vm::new(); + // second:0.6s 舍入到秒 → 进位(halfExpand 默认)。 + let r = eval( + &mut vm, + "new Temporal.PlainDateTime(1976, 11, 18, 14, 23, 30, 600, 0, 0).round({ smallestUnit: 'second' }).toString()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "1976-11-18T14:23:31"); + // nanosecond 增量 + floor:123456789ns → 123456700ns。 + let r = eval( + &mut vm, + "new Temporal.PlainDateTime(1976, 11, 18, 14, 23, 30, 123, 456, 789).round({ smallestUnit: 'nanosecond', roundingIncrement: 100, roundingMode: 'floor' }).toString()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "1976-11-18T14:23:30.1234567"); + // hour:23:59:59.999999999 舍入到小时 → 进位到次日 00:00。 + let r = eval( + &mut vm, + "new Temporal.PlainDateTime(1976, 11, 18, 23, 59, 59, 999, 999, 999).round({ smallestUnit: 'hour' }).toString()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "1976-11-19T00:00:00"); + // day:14:23:30 舍入到日(>12:00)→ 次日;floor → 当日。 + let r = eval( + &mut vm, + "new Temporal.PlainDateTime(1976, 11, 18, 14, 23, 30).round({ smallestUnit: 'day' }).toString()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "1976-11-19T00:00:00"); + let r = eval( + &mut vm, + "new Temporal.PlainDateTime(1976, 11, 18, 11, 23, 30).round({ smallestUnit: 'day', roundingMode: 'floor' }).toString()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "1976-11-18T00:00:00"); + // 跨年进位:2000-12-31T23:30 舍入到日 → 2001-01-01。 + let r = eval( + &mut vm, + "new Temporal.PlainDateTime(2000, 12, 31, 23, 30).round({ smallestUnit: 'day' }).toString()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "2001-01-01T00:00:00"); +} + +#[test] +fn plain_date_time_round_limits_and_errors() { + let mut vm = Vm::new(); + // 表示范围边界(严格不等式):min 舍入落边界下抛、max 舍入上越界抛。 + let r = eval( + &mut vm, + "(() => { const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } }; + const min = new Temporal.PlainDateTime(-271821, 4, 19, 0, 0, 0, 0, 0, 1); + const max = new Temporal.PlainDateTime(275760, 9, 13, 23, 59, 59, 999, 999, 999); + return [ + kind(() => min.round({ smallestUnit: 'day', roundingMode: 'floor' })), + kind(() => max.round({ smallestUnit: 'hour', roundingMode: 'ceil' })), + ].join('|'); })()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "RangeError|RangeError"); + // 缺参 → TypeError;对象缺 smallestUnit → RangeError。 + let r = eval( + &mut vm, + "(() => { const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } }; + const dt = new Temporal.PlainDateTime(1976, 11, 18, 14, 23, 30); + return [kind(() => dt.round()), kind(() => dt.round({}))].join('|'); })()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "TypeError|RangeError"); + // 非法最小单位(PDT 限 day..nanosecond,year/month/week 不合法)→ RangeError。 + let r = eval( + &mut vm, + "(() => { const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } }; + const dt = new Temporal.PlainDateTime(1976, 11, 18, 14, 23, 30); + return [ + kind(() => dt.round({ smallestUnit: 'year' })), + kind(() => dt.round({ smallestUnit: 'week' })), + ].join('|'); })()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "RangeError|RangeError"); + // 真因子校验:hour 增量 24 拒绝;day 仅增量 1 合法。 + let r = eval( + &mut vm, + "(() => { const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } }; + const dt = new Temporal.PlainDateTime(1976, 11, 18, 14, 23, 30); + return [ + kind(() => dt.round({ smallestUnit: 'hour', roundingIncrement: 24 })), + kind(() => dt.round({ smallestUnit: 'day', roundingIncrement: 2 })), + ].join('|'); })()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "RangeError|RangeError"); + // 日历槽继承 receiver。 + let r = eval( + &mut vm, + "new Temporal.PlainDateTime(1976, 11, 18, 12, 0, 0, 0, 0, 0, 'hebrew').round('minute').calendarId", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "hebrew"); +} + +#[test] +fn plain_year_month_round_units() { + let mut vm = Vm::new(); + // month:1976-11 恰在 1 月边界 → 原样。 + let r = eval(&mut vm, "new Temporal.PlainYearMonth(1976, 11).round('month').toString()").unwrap(); + assert_eq!(str_val(&vm, r), "1976-11"); + // month 增量:1976-12 按 3 舍入(余 2 > 1.5)→ 1977-01。 + let r = eval( + &mut vm, + "new Temporal.PlainYearMonth(1976, 12).round({ smallestUnit: 'month', roundingIncrement: 3 }).toString()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "1977-01"); + // year:1976 恰在 4 年边界 → 月不变(11)。 + let r = eval( + &mut vm, + "new Temporal.PlainYearMonth(1976, 11).round({ smallestUnit: 'year', roundingIncrement: 4 }).toString()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "1976-11"); + // year 增量:1977 按 4 舍入(余 1 < 2)→ 1976,月保持 11。 + let r = eval( + &mut vm, + "new Temporal.PlainYearMonth(1977, 11).round({ smallestUnit: 'year', roundingIncrement: 4 }).toString()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "1976-11"); +} + +#[test] +fn plain_year_month_round_errors_and_calendar() { + let mut vm = Vm::new(); + // 缺参 → TypeError;对象缺 smallestUnit → RangeError;非法单位(week)→ RangeError。 + let r = eval( + &mut vm, + "(() => { const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } }; + const ym = new Temporal.PlainYearMonth(1976, 11); + return [ + kind(() => ym.round()), + kind(() => ym.round({})), + kind(() => ym.round({ smallestUnit: 'week' })), + ].join('|'); })()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "TypeError|RangeError|RangeError"); + // 范围边界:-271821-03 在表示范围外(下界 -271821-04)→ RangeError。 + let r = eval( + &mut vm, + "try { new Temporal.PlainYearMonth(-271821, 3).round({ smallestUnit: 'month', roundingMode: 'floor' }) } catch (e) { e.constructor.name }", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "RangeError"); + // 日历槽继承 receiver。 + let r = eval( + &mut vm, + "new Temporal.PlainYearMonth(1976, 11, 'hebrew').round('year').calendarId", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "hebrew"); +} + +#[test] +fn round_options_bag_shorthand_and_modes() { + let mut vm = Vm::new(); + // 字符串简写与对象形态等价(PD/PDT/PYM 三族)。 + let r = eval( + &mut vm, + "(() => { + const pd = new Temporal.PlainDate(1976, 11, 18); + const s = pd.round('month').toString(); + const o = pd.round({ smallestUnit: 'month' }).toString(); + const pds = new Temporal.PlainDateTime(1976, 11, 18, 14, 23, 30).round('day').toString(); + const pdo = new Temporal.PlainDateTime(1976, 11, 18, 14, 23, 30).round({ smallestUnit: 'day' }).toString(); + const yms = new Temporal.PlainYearMonth(1976, 12).round('year').toString(); + const ymo = new Temporal.PlainYearMonth(1976, 12).round({ smallestUnit: 'year' }).toString(); + return [s, o, pds, pdo, yms, ymo].join('|'); + })()", + ) + .unwrap(); + assert_eq!( + str_val(&vm, r), + "1976-11-30|1976-11-30|1976-11-19T00:00:00|1976-11-19T00:00:00|1976-12|1976-12" + ); + // 9 种舍入模式全过(PDT 时间单位路径)。 + let r = eval( + &mut vm, + "(() => { + const dt = new Temporal.PlainDateTime(1976, 11, 18, 12, 30, 0); + const modes = ['ceil','expand','floor','halfCeil','halfEven','halfExpand','halfFloor','halfTrunc','trunc']; + return modes.map((m) => dt.round({ smallestUnit: 'hour', roundingMode: m }).toString()).join('|'); + })()", + ) + .unwrap(); + assert_eq!( + str_val(&vm, r), + "1976-11-18T13:00:00|1976-11-18T13:00:00|1976-11-18T12:00:00|1976-11-18T13:00:00|1976-11-18T12:00:00|1976-11-18T13:00:00|1976-11-18T12:00:00|1976-11-18T12:00:00|1976-11-18T12:00:00" + ); +}