feat: Temporal 三族新增 round 方法

This commit is contained in:
2026-10-14 10:14:58 +00:00
parent 8a4959f8b1
commit 96e24c84c1
12 changed files with 673 additions and 218 deletions
Generated
+21 -21
View File
@@ -1160,7 +1160,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_builtins" name = "oxide_builtins"
version = "0.19.11" version = "0.19.12"
dependencies = [ dependencies = [
"chrono", "chrono",
"fast-float", "fast-float",
@@ -1185,7 +1185,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_bytecode" name = "oxide_bytecode"
version = "0.19.11" version = "0.19.12"
dependencies = [ dependencies = [
"num-bigint", "num-bigint",
"oxide_log", "oxide_log",
@@ -1193,7 +1193,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_cfg" name = "oxide_cfg"
version = "0.19.11" version = "0.19.12"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_emit", "oxide_emit",
@@ -1204,7 +1204,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_cli" name = "oxide_cli"
version = "0.19.11" version = "0.19.12"
dependencies = [ dependencies = [
"ansi_term", "ansi_term",
"bumpalo", "bumpalo",
@@ -1228,7 +1228,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_code_cache" name = "oxide_code_cache"
version = "0.19.11" version = "0.19.12"
dependencies = [ dependencies = [
"lru", "lru",
"oxide_bytecode", "oxide_bytecode",
@@ -1237,7 +1237,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_compiler" name = "oxide_compiler"
version = "0.19.11" version = "0.19.12"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_cfg", "oxide_cfg",
@@ -1258,7 +1258,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_dce" name = "oxide_dce"
version = "0.19.11" version = "0.19.12"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_cfg", "oxide_cfg",
@@ -1272,7 +1272,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_emit" name = "oxide_emit"
version = "0.19.11" version = "0.19.12"
dependencies = [ dependencies = [
"num-bigint", "num-bigint",
"oxc_allocator", "oxc_allocator",
@@ -1288,7 +1288,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_engine" name = "oxide_engine"
version = "0.19.11" version = "0.19.12"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_compiler", "oxide_compiler",
@@ -1300,7 +1300,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_fold" name = "oxide_fold"
version = "0.19.11" version = "0.19.12"
dependencies = [ dependencies = [
"num-bigint", "num-bigint",
"oxide_bytecode", "oxide_bytecode",
@@ -1312,7 +1312,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_ir" name = "oxide_ir"
version = "0.19.11" version = "0.19.12"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_log", "oxide_log",
@@ -1321,7 +1321,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_kernel" name = "oxide_kernel"
version = "0.19.11" version = "0.19.12"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"dashmap", "dashmap",
@@ -1335,7 +1335,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_liveness" name = "oxide_liveness"
version = "0.19.11" version = "0.19.12"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_cfg", "oxide_cfg",
@@ -1347,7 +1347,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_log" name = "oxide_log"
version = "0.19.11" version = "0.19.12"
dependencies = [ dependencies = [
"tracing", "tracing",
"tracing-appender", "tracing-appender",
@@ -1356,11 +1356,11 @@ dependencies = [
[[package]] [[package]]
name = "oxide_net" name = "oxide_net"
version = "0.19.11" version = "0.19.12"
[[package]] [[package]]
name = "oxide_parser" name = "oxide_parser"
version = "0.19.11" version = "0.19.12"
dependencies = [ dependencies = [
"oxc_allocator", "oxc_allocator",
"oxc_ast", "oxc_ast",
@@ -1375,7 +1375,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_regalloc" name = "oxide_regalloc"
version = "0.19.11" version = "0.19.12"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_cfg", "oxide_cfg",
@@ -1391,7 +1391,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_runtime_api" name = "oxide_runtime_api"
version = "0.19.11" version = "0.19.12"
dependencies = [ dependencies = [
"num-bigint", "num-bigint",
"num-traits", "num-traits",
@@ -1404,7 +1404,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_test262" name = "oxide_test262"
version = "0.19.11" version = "0.19.12"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_compiler", "oxide_compiler",
@@ -1424,7 +1424,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_types" name = "oxide_types"
version = "0.19.11" version = "0.19.12"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"dashmap", "dashmap",
@@ -1437,7 +1437,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_vm" name = "oxide_vm"
version = "0.19.11" version = "0.19.12"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"chrono", "chrono",
+1 -1
View File
@@ -25,7 +25,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "0.19.11" version = "0.19.12"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
rust-version = "1.80" rust-version = "1.80"
@@ -20,6 +20,7 @@ use oxide_runtime_api::{to_number, to_string, NativeResult, ProtoKind, VmHost};
use oxide_types::object::JsObject; use oxide_types::object::JsObject;
use oxide_types::value::JsValue; use oxide_types::value::JsValue;
use super::instant::{instant_rounding_mode, InstantRoundingMode};
use super::time_zone::{tz_canonical_name, zone_offset_seconds}; use super::time_zone::{tz_canonical_name, zone_offset_seconds};
use super::{ use super::{
parse_iso_date, parse_plain_date_time_string, parse_plain_time_string, valid_iso_date, valid_plain_date_time_range, 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<H: VmHost>(vm: &mut H, value: JsValue) -> R
Ok(to_string(primitive)) 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<H: VmHost>(
vm: &mut H, round_to: JsValue,
) -> Result<(i128, InstantRoundingMode, Option<String>), 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 参数并排除日历/时区载体。 /// 校验 `with` 族方法的 partial 参数并排除日历/时区载体。
/// ///
/// 非对象、已初始化的 Temporal 日期时间类实例,或 calendar、timeZone 任一 /// 非对象、已初始化的 Temporal 日期时间类实例,或 calendar、timeZone 任一
+17 -52
View File
@@ -9,15 +9,15 @@ use super::common::native_try;
use super::common::{ use super::common::{
canonical_time_zone, civil_from_days, days_from_civil, ensure_duration, get_calendar_id, get_double_prop, 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, 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, native_engine_error, parse_round_options, receiver_obj, resolve_date_time_parts, temporal_calendar_id,
temporal_option_string, temporal_option_value, temporal_string_strict, zoned_date_time_plain_parts, DateTimeParts, temporal_option_number, temporal_option_string, temporal_option_value, temporal_string_strict,
zoned_date_time_plain_parts, DateTimeParts,
}; };
use super::difference::{ use super::difference::{
add_date_duration, add_days_iso, balance_instant_difference, compare_iso_date, date_duration_sign, date_until_iso, 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, nudge_iso_difference, nudge_window, plain_date_time_unit_index, round_instant_difference, DifferenceSettings,
InternalDuration, IsoDateTime, DAY_NS, MAX_ISO_DAY, InternalDuration, IsoDateTime, DAY_NS, MAX_ISO_DAY,
}; };
use super::instant::{instant_rounding_mode, InstantRoundingMode};
use super::time_zone::zone_offset_seconds; use super::time_zone::zone_offset_seconds;
use super::zoned_date_time::{ use super::zoned_date_time::{
add_zoned_datetime, difference_zoned_datetime_with_rounding_vm, difference_zoned_datetime_with_total_vm, add_zoned_datetime, difference_zoned_datetime_with_rounding_vm, difference_zoned_datetime_with_total_vm,
@@ -1240,14 +1240,9 @@ pub fn duration_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
} }
// roundTo 字符串 => { smallestUnit: 字符串 };否则必须为对象。 // roundTo 字符串 => { smallestUnit: 字符串 };否则必须为对象。
let (largest_raw, relative_raw, increment_raw, mode_raw, smallest_raw) = if round_to.is_string() { // largestUnit/relativeTo 先于 increment 读取(读序钉死);其余三字段走统一 helper。
( let (largest_raw, relative_raw) = if round_to.is_string() {
JsValue::undefined(), (JsValue::undefined(), JsValue::undefined())
JsValue::undefined(),
JsValue::undefined(),
JsValue::undefined(),
round_to,
)
} else { } else {
if !round_to.is_object() { if !round_to.is_object() {
return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be an object")); return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be an object"));
@@ -1257,13 +1252,12 @@ pub fn duration_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be an object")); return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be an object"));
} }
let options = unsafe { &*options_ptr }; 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")); native_try!(temporal_option_value(vm, options, round_to, "largestUnit")),
let increment_raw = native_try!(temporal_option_value(vm, options, round_to, "roundingIncrement")); native_try!(temporal_option_value(vm, options, round_to, "relativeTo")),
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)
}; };
let (increment, mode, smallest_unit) = native_try!(parse_round_options(vm, round_to));
// largestUnit:允许 "auto";缺失/undefined 视为未提供。 // largestUnit:允许 "auto";缺失/undefined 视为未提供。
let largest_provided = !largest_raw.is_undefined(); let largest_provided = !largest_raw.is_undefined();
@@ -1283,43 +1277,14 @@ pub fn duration_round<H: VmHost>(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。 // smallestUnit:缺失时 nanosecond。
let smallest_provided = !smallest_raw.is_undefined(); let smallest_provided = smallest_unit.is_some();
let smallest_index = if smallest_raw.is_undefined() { let smallest_index = match smallest_unit {
9 Some(unit) => match plain_date_time_unit_index(&unit) {
} else {
let unit = native_try!(temporal_option_string(vm, smallest_raw));
match plain_date_time_unit_index(&unit) {
Some(index) => index, Some(index) => index,
None => { None => return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallestUnit")),
return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallestUnit")); },
} None => 9,
}
}; };
// 默认最大单位:现有最大单位与最小单位中较大(索引较小)的那个。 // 默认最大单位:现有最大单位与最小单位中较大(索引较小)的那个。
+4 -45
View File
@@ -12,8 +12,8 @@ use super::{
balance_instant_difference, canonical_time_zone, civil_from_days, days_from_civil, days_in_month, 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, 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, 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, make_zoned_date_time, native_engine_error, parse_digits, parse_fractional_second_digits, parse_round_options,
round_instant_difference, temporal_option_number, temporal_option_string, temporal_option_value, receiver_obj, round_instant_difference, temporal_option_number, temporal_option_string, temporal_option_value,
FractionalSecondDigitsInput, MAX_INSTANT_NS, FractionalSecondDigitsInput, MAX_INSTANT_NS,
}; };
@@ -551,55 +551,14 @@ pub fn instant_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}; };
let round_to = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; 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() { let (increment, mode, unit_value) = native_try!(parse_round_options(vm, round_to));
(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 Some(unit_value) = unit_value else { let Some(unit_value) = unit_value else {
return NativeResult::Err(crate::error::create_range_error(vm, "smallestUnit is required")); 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 { 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")); return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallest unit"));
}; };
if !increment_value.is_finite() { // 整除校验用每更高一级单位数量(Instant 语义:增量须整除一天)。
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;
if units_per_day % increment != 0 { if units_per_day % increment != 0 {
return NativeResult::Err(crate::error::create_range_error(vm, "rounding increment must divide a day")); return NativeResult::Err(crate::error::create_range_error(vm, "rounding increment must divide a day"));
} }
@@ -7,15 +7,18 @@ use oxide_types::value::JsValue;
use super::common::native_try; use super::common::native_try;
use super::common::{ use super::common::{
days_from_civil, days_in_month, ensure_plain_date, get_calendar_id, get_double_prop, initialize_temporal_receiver, civil_from_days, days_from_civil, days_in_month, ensure_plain_date, get_calendar_id, get_double_prop,
is_ctor_call, parse_plain_date_string, plain_date_time_object_parts, receiver_obj, initialize_temporal_receiver, is_ctor_call, parse_plain_date_string, parse_round_options,
reject_partial_object_with_calendar_or_time_zone, resolve_date_time_parts, temporal_calendar_id, plain_date_time_object_parts, receiver_obj, reject_partial_object_with_calendar_or_time_zone,
temporal_calendar_id_strict, temporal_number_component, temporal_option_string, temporal_option_value, resolve_date_time_parts, temporal_calendar_id, temporal_calendar_id_strict, temporal_number_component,
temporal_overflow, zoned_date_time_plain_parts, DateTimeParts, 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::difference::{difference_core, parse_difference_settings, IsoDateTime};
use super::duration::duration_like_values; 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 年份范围)。 /// 校验 ISO 日期分量(month 1-12、day 按月份与闰年,不依赖 chrono 年份范围)。
pub(crate) fn valid_iso_date(year: i32, month: u32, day: u32) -> bool { pub(crate) fn valid_iso_date(year: i32, month: u32, day: u32) -> bool {
@@ -779,6 +782,95 @@ pub fn plain_date_subtract<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
date_apply_duration(vm, args, -1) 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<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_date(vm, obj));
let year = get_double_prop(obj, 0) as i32;
let month = get_double_prop(obj, 1) as u32;
let day = get_double_prop(obj, 2) as u32;
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<NaiveDate> { fn add_signed_months(date: NaiveDate, months: i64) -> Option<NaiveDate> {
let total = date.year() as i64 * 12 + date.month0() as i64 + months; let total = date.year() as i64 * 12 + date.month0() as i64 + months;
let year = total.div_euclid(12); let year = total.div_euclid(12);
@@ -11,11 +11,12 @@ use super::{
duration_like_values, ensure_plain_date_time, format_iso_year, get_calendar_id, get_double_prop, 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, 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, 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, parse_round_options, parse_temporal_string_impl, plain_date_time_object_parts, plain_time_components,
reject_partial_object_with_calendar_or_time_zone, resolve_date_time_parts, round_instant_ns, temporal_calendar_id, plain_time_like_ns, receiver_obj, reject_partial_object_with_calendar_or_time_zone, resolve_date_time_parts,
temporal_calendar_id_strict, temporal_number_component, temporal_option_number, temporal_option_string, round_instant_ns, temporal_calendar_id, temporal_calendar_id_strict, temporal_number_component,
temporal_option_value, temporal_overflow, valid_iso_date, valid_plain_date_time_range, valid_plain_time, temporal_option_number, temporal_option_string, temporal_option_value, temporal_overflow, valid_iso_date,
zoned_date_time_plain_parts, DateTimeParts, FractionalSecondDigitsInput, IsoDateTime, valid_plain_date_time_range, valid_plain_time, zoned_date_time_plain_parts, DateTimeParts,
FractionalSecondDigitsInput, IsoDateTime,
}; };
// ───────────────────── PlainDateTime 基础方法 ───────────────────── // ───────────────────── PlainDateTime 基础方法 ─────────────────────
@@ -827,6 +828,98 @@ pub fn plain_date_time_subtract<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeRes
plain_date_time_apply_duration(vm, args, -1) 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<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_date_time(vm, obj));
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 日期时间。 /// `Temporal.PlainDateTime.prototype.toJSON()`:输出默认 ISO 日期时间。
pub fn plain_date_time_to_json<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult { pub fn plain_date_time_to_json<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_date_time_iso_string(vm, args) plain_date_time_iso_string(vm, args)
@@ -9,7 +9,8 @@ use super::{
difference_core, duration_component_integer, duration_like_values, ensure_plain_time, get_double_prop, 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, 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, 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_number_component, temporal_option_number, temporal_option_string, temporal_option_value,
temporal_overflow, valid_offset_fraction, zoned_date_time_plain_parts, FractionalSecondDigitsInput, IsoDateTime, 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 语义。 /// - 舍入跨午夜(如 23:59:59.9 舍入到秒)→ 取模回 00:00,符合 rounding-cross-midnight 语义。
pub fn plain_time_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult { pub fn plain_time_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let round_to = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; let 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() { let (increment, mode, unit_value) = native_try!(parse_round_options(vm, round_to));
(1.0, "halfExpand".to_string(), Some(to_string(round_to)))
} else {
if !round_to.is_object() {
return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be a string or object"));
}
let options_ptr = round_to.as_js_object_ptr();
if options_ptr.is_null() {
return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be a string or object"));
}
let options = unsafe { &*options_ptr };
let increment_raw = native_try!(temporal_option_value(vm, options, round_to, "roundingIncrement"));
let increment = if increment_raw.is_undefined() {
1.0
} else {
native_try!(temporal_option_number(vm, increment_raw))
};
let mode_raw = native_try!(temporal_option_value(vm, options, round_to, "roundingMode"));
let mode = if mode_raw.is_undefined() {
"halfExpand".to_string()
} else {
native_try!(temporal_option_string(vm, mode_raw))
};
let unit_raw = native_try!(temporal_option_value(vm, options, round_to, "smallestUnit"));
let unit = if unit_raw.is_undefined() {
None
} else {
Some(native_try!(temporal_option_string(vm, unit_raw)))
};
(increment, mode, unit)
};
let ptr = native_try!(receiver_obj(vm, args)); let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr }; let obj = unsafe { &*ptr };
native_try!(ensure_plain_time(vm, obj)); native_try!(ensure_plain_time(vm, obj));
let time_ns = get_double_prop(obj, 0) as i128; 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 { let Some(unit_value) = unit_value else {
return NativeResult::Err(crate::error::create_range_error(vm, "smallestUnit is required")); 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 { 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")); 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 // 真因子校验:增量须严格小于最大增量且能整除(MaximumTemporalDurationRoundingIncrement
// 语义;instant_round 只查 % 的缺陷不在此沿用)。 // 语义;instant_round 只查 % 的缺陷不在此沿用)。
if increment >= max_increment || max_increment % increment != 0 { if increment >= max_increment || max_increment % increment != 0 {
@@ -9,8 +9,9 @@ use super::{
calendar_annotation, compare_iso_date, days_from_civil, days_in_month_iso, difference_core, duration_like_values, 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, 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, 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, make_plain_year_month, month_day_bag_calendar_id, native_engine_error, parse_round_options,
read_iso2, receiver_obj, reject_partial_object_with_calendar_or_time_zone, temporal_constructor_calendar_id, 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_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, temporal_to_show_calendar, validate_month_day_annotation_suffix, validate_month_day_time_suffix, IsoDateTime,
ShowCalendar, MAX_ISO_DAY, ShowCalendar, MAX_ISO_DAY,
@@ -688,6 +689,73 @@ pub fn plain_year_month_subtract<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeRe
plain_year_month_apply_duration(vm, args, -1) 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<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_year_month(vm, obj));
let 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; /// `Temporal.PlainYearMonth.prototype.with(yearMonthLike[, options])`:覆盖 year/month/monthCode;
/// undefined 不覆盖;calendar/timeZone 键拒绝;无识别字段 → TypeError。 /// undefined 不覆盖;calendar/timeZone 键拒绝;无识别字段 → TypeError。
/// ///
@@ -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, 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, 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, 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, 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, temporal_option_number, temporal_option_string, temporal_option_value, temporal_overflow, temporal_string_strict,
zoned_date_time_plain_parts, FractionalSecondDigitsInput, MAX_INSTANT_NS, zoned_date_time_plain_parts, FractionalSecondDigitsInput, MAX_INSTANT_NS,
@@ -1436,55 +1437,14 @@ pub fn zoned_date_time_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
// roundTo 解析:字符串简写直接取用,对象选项按 roundingIncrement、roundingMode、 // roundTo 解析:字符串简写直接取用,对象选项按 roundingIncrement、roundingMode、
// smallestUnit 的固定读取顺序取值。 // smallestUnit 的固定读取顺序取值。
let round_to = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; 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() { let (increment, mode, unit_value) = native_try!(parse_round_options(vm, round_to));
(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 Some(unit_value) = unit_value else { let Some(unit_value) = unit_value else {
return NativeResult::Err(crate::error::create_range_error(vm, "smallestUnit is required")); 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 { 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")); 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 拒绝); // 真因子校验:时间单位增量须小于更高一级单位数量且整除(hour/24、minute/60、second/60 拒绝);
// day 特殊:更高一级数量=1,仅 increment==1 合法。 // day 特殊:更高一级数量=1,仅 increment==1 合法。
let increment_valid = if unit_ns == DAY_NS { let increment_valid = if unit_ns == DAY_NS {
@@ -588,6 +588,7 @@ pub fn bind_temporal(env: BindEnv<'_>, global: &mut JsObject) {
oxide_builtins::temporal::plain_date_to_plain_year_month::<crate::vm::Vm> as *const (), oxide_builtins::temporal::plain_date_to_plain_year_month::<crate::vm::Vm> as *const (),
0, 0,
), ),
("round", oxide_builtins::temporal::plain_date_round::<crate::vm::Vm> 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::<crate::vm::Vm> as *const (), oxide_builtins::temporal::plain_date_time_with_plain_time::<crate::vm::Vm> as *const (),
0, 0,
), ),
(
"round",
oxide_builtins::temporal::plain_date_time_round::<crate::vm::Vm> as *const (),
1,
),
], ],
); );
bind_well_known_data_property( 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::<crate::vm::Vm> as *const (), 1), ("until", oxide_builtins::temporal::plain_year_month_until::<crate::vm::Vm> as *const (), 1),
("since", oxide_builtins::temporal::plain_year_month_since::<crate::vm::Vm> as *const (), 1), ("since", oxide_builtins::temporal::plain_year_month_since::<crate::vm::Vm> as *const (), 1),
(
"round",
oxide_builtins::temporal::plain_year_month_round::<crate::vm::Vm> as *const (),
1,
),
], ],
); );
bind_well_known_data_property( bind_well_known_data_property(
@@ -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]" "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"
);
}