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
@@ -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 任一
+17 -52
View File
@@ -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,
};
// 默认最大单位:现有最大单位与最小单位中较大(索引较小)的那个。
+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,
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 {