mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
feat: Temporal 三族新增 round 方法
This commit is contained in:
Generated
+21
-21
@@ -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",
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.19.11"
|
||||
version = "0.19.12"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
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::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<H: VmHost>(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<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 参数并排除日历/时区载体。
|
||||
///
|
||||
/// 非对象、已初始化的 Temporal 日期时间类实例,或 calendar、timeZone 任一
|
||||
|
||||
@@ -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<H: VmHost>(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<H: VmHost>(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<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。
|
||||
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,
|
||||
};
|
||||
|
||||
// 默认最大单位:现有最大单位与最小单位中较大(索引较小)的那个。
|
||||
|
||||
@@ -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<H: VmHost>(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"));
|
||||
}
|
||||
|
||||
@@ -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<H: VmHost>(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<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> {
|
||||
let total = date.year() as i64 * 12 + date.month0() as i64 + months;
|
||||
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,
|
||||
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<H: VmHost>(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<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 日期时间。
|
||||
pub fn plain_date_time_to_json<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
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,
|
||||
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<H: VmHost>(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 {
|
||||
|
||||
@@ -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<H: VmHost>(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<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;
|
||||
/// 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,
|
||||
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<H: VmHost>(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 {
|
||||
|
||||
@@ -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 (),
|
||||
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 (),
|
||||
0,
|
||||
),
|
||||
(
|
||||
"round",
|
||||
oxide_builtins::temporal::plain_date_time_round::<crate::vm::Vm> 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::<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(
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user