diff --git a/crates/oxide_builtins/src/temporal/difference.rs b/crates/oxide_builtins/src/temporal/difference.rs new file mode 100644 index 00000000..8c56b046 --- /dev/null +++ b/crates/oxide_builtins/src/temporal/difference.rs @@ -0,0 +1,854 @@ +//! until/since/add/subtract 共享差量层:差值设置解析(GetDifferenceSettings 族)、 +//! nudge/舍入(窗口取整、日历单位进位)、ISO 日期差量计算与单位表。 + +use oxide_runtime_api::{NativeResult, VmHost}; +use oxide_types::value::JsValue; + +use super::common::{ + civil_from_days, days_from_civil, days_in_month, temporal_option_number, temporal_option_string, + temporal_option_value, +}; +use super::{instant_rounding_mode, make_duration, plain_time_components_signed, InstantRoundingMode}; + +/// 按普通有符号数语义舍入 Instant 差值。 +pub(crate) fn round_instant_difference(value: i128, increment: i128, mode: InstantRoundingMode) -> Option { + let negative = value < 0; + let magnitude = value.checked_abs()?; + let quotient = magnitude / increment; + let remainder = magnitude % increment; + if remainder == 0 { + return Some(value); + } + let use_upper = match mode { + InstantRoundingMode::Ceil => !negative, + InstantRoundingMode::Expand => true, + InstantRoundingMode::Floor => negative, + InstantRoundingMode::Trunc => false, + _ => match (remainder * 2).cmp(&increment) { + std::cmp::Ordering::Less => false, + std::cmp::Ordering::Greater => true, + std::cmp::Ordering::Equal => match mode { + InstantRoundingMode::HalfCeil => !negative, + InstantRoundingMode::HalfEven => quotient.rem_euclid(2) != 0, + InstantRoundingMode::HalfExpand => true, + InstantRoundingMode::HalfFloor => negative, + InstantRoundingMode::HalfTrunc => false, + _ => unreachable!(), + }, + }, + }; + let rounded = quotient.checked_add(i128::from(use_upper))?.checked_mul(increment)?; + Some(if negative { -rounded } else { rounded }) +} + +pub(crate) fn balance_instant_difference(value: i128, largest_unit: usize) -> Option<[f64; 10]> { + const SCALES: [i128; 6] = [3_600_000_000_000, 60_000_000_000, 1_000_000_000, 1_000_000, 1_000, 1]; + let negative = value < 0; + let mut remainder = value.checked_abs()?; + let mut values = [0.0; 10]; + for (index, scale) in SCALES.iter().enumerate().skip(largest_unit) { + let component = remainder / scale; + remainder %= scale; + if component != 0 { + values[index + 4] = if negative { -(component as f64) } else { component as f64 }; + } + } + Some(values) +} + +/// PlainDateTime 差值单位层级:year=0 … nanosecond=9;"auto" 仅限 largestUnit。 +pub(crate) fn plain_date_time_unit_index(value: &str) -> Option { + match value { + "year" | "years" => Some(0), + "month" | "months" => Some(1), + "week" | "weeks" => Some(2), + "day" | "days" => Some(3), + "hour" | "hours" => Some(4), + "minute" | "minutes" => Some(5), + "second" | "seconds" => Some(6), + "millisecond" | "milliseconds" => Some(7), + "microsecond" | "microseconds" => Some(8), + "nanosecond" | "nanoseconds" => Some(9), + _ => None, + } +} + +pub(crate) const DAY_NS: i128 = 86_400_000_000_000; + +pub(crate) const MAX_ISO_DAY: i128 = 100_000_000; + +/// 比较两个 ISO 日期,返回 -1/0/+1。 +pub(crate) fn compare_iso_date(a: (i128, i128, i128), b: (i128, i128, i128)) -> i128 { + if a.0 != b.0 { + return if a.0 < b.0 { -1 } else { 1 }; + } + if a.1 != b.1 { + return if a.1 < b.1 { -1 } else { 1 }; + } + if a.2 != b.2 { + return if a.2 < b.2 { -1 } else { 1 }; + } + 0 +} + +/// 候选日期(日以 day_override 参与比较)是否在 sign 方向上越过终点。 +fn surpasses_with_day(sign: i128, candidate: (i128, i128, i128), day_override: i128, end: (i128, i128, i128)) -> bool { + let cmp = compare_iso_date((candidate.0, candidate.1, day_override), end); + if sign > 0 { + cmp > 0 + } else { + cmp < 0 + } +} + +/// 日期加天数。 +pub(crate) fn add_days_iso(date: (i128, i128, i128), days: i128) -> (i128, i128, i128) { + let (y, m, d) = civil_from_days(days_from_civil(date.0, date.1, date.2) + days); + (y, m, d) +} + +/// ISO8601 日期差分解,对齐 polyfill 的 dateUntil/untilCalendar。 +/// largest:0=year 1=month 2=week 3=day。 +pub(crate) fn date_until_iso(start: (i128, i128, i128), end: (i128, i128, i128), largest: usize) -> [f64; 10] { + let mut values = [0.0; 10]; + if largest >= 2 { + let mut days = days_from_civil(end.0, end.1, end.2) - days_from_civil(start.0, start.1, start.2); + if largest == 2 { + values[2] = (days / 7) as f64; + days %= 7; + } + values[3] = days as f64; + return values; + } + let sign = compare_iso_date(end, start); + if sign == 0 { + return values; + } + let diff_years = end.0 - start.0; + let diff_days = end.2 - start.2; + let diff_in_year_sign = if end.1 > start.1 { + 1 + } else if end.1 < start.1 { + -1 + } else if diff_days > 0 { + 1 + } else if diff_days < 0 { + -1 + } else { + 0 + }; + // 终点的月-日早于起点的月-日时,年份差需沿 sign 方向修正 1。 + let mut years = if diff_in_year_sign * sign < 0 { diff_years - sign } else { diff_years }; + let mut months = 0_i128; + if largest == 1 { + months = years * 12; + years = 0; + } + let intermediate = add_months_i128(start.0, start.1, start.2, years * 12 + months); + // 闰日校正:intermediate 已越过终点时年份回退 1。 + if surpasses_with_day(sign, intermediate, start.2, end) { + years -= sign; + } + // 从 start 按锚定日逐月推进(月末钳制);current 始终是未越界的真实日期, + // 锚定日覆盖只用于越界比较,避免伪日期(如 2 月 29 日)污染天数计算。 + let mut current = add_months_i128(start.0, start.1, start.2, years * 12 + months); + loop { + months += sign; + let candidate = add_months_i128(start.0, start.1, start.2, years * 12 + months); + let mut cmp = candidate; + cmp.2 = start.2; + if surpasses_with_day(sign, cmp, start.2, end) { + break; + } + current = candidate; + } + months -= sign; + let days = days_from_civil(end.0, end.1, end.2) - days_from_civil(current.0, current.1, current.2); + values[0] = years as f64; + values[1] = months as f64; + values[3] = days as f64; + values +} + +/// 向零截断到 increment 的倍数。 +fn round_to_increment_trunc(value: i128, increment: i128) -> i128 { + (value / increment) * increment +} + +/// 时长日期部分符号(首个非零分量决定)。 +pub(crate) fn date_duration_sign(values: &[f64; 10]) -> i128 { + for value in values[0..4].iter().copied() { + if value != 0.0 { + return if value > 0.0 { 1 } else { -1 }; + } + } + 0 +} + +/// 在日期上叠加时长(年/月/周/日)。 +pub(crate) fn add_date_duration(date: (i128, i128, i128), values: &[f64; 10]) -> (i128, i128, i128) { + let years = values[0] as i128; + let months = values[1] as i128; + let weeks = values[2] as i128; + let days = values[3] as i128; + let d = add_months_i128(date.0, date.1, date.2, years * 12 + months); + add_days_iso(d, weeks * 7 + days) +} + +/// 无符号舍入模式(ApplyUnsignedRoundingMode 用的模式集合)。 +#[derive(Clone, Copy)] +enum UnsignedMode { + Zero, + Infinity, + HalfEven, + HalfInfinity, + HalfZero, +} + +/// ApplyUnsignedRoundingMode:在 r1/r2 间选择(均为非负量)。 +fn apply_unsigned_rounding( + r1: i128, r2: i128, cmp: std::cmp::Ordering, even: bool, mode: InstantRoundingMode, negative: bool, +) -> i128 { + let unsigned_mode = match mode { + InstantRoundingMode::Ceil => { + if negative { + UnsignedMode::Zero + } else { + UnsignedMode::Infinity + } + } + InstantRoundingMode::Floor => { + if negative { + UnsignedMode::Infinity + } else { + UnsignedMode::Zero + } + } + InstantRoundingMode::Expand => UnsignedMode::Infinity, + InstantRoundingMode::Trunc => UnsignedMode::Zero, + InstantRoundingMode::HalfCeil => { + if negative { + UnsignedMode::HalfZero + } else { + UnsignedMode::HalfInfinity + } + } + InstantRoundingMode::HalfFloor => { + if negative { + UnsignedMode::HalfInfinity + } else { + UnsignedMode::HalfZero + } + } + InstantRoundingMode::HalfEven => UnsignedMode::HalfEven, + InstantRoundingMode::HalfExpand => UnsignedMode::HalfInfinity, + InstantRoundingMode::HalfTrunc => UnsignedMode::HalfZero, + }; + match unsigned_mode { + UnsignedMode::Zero => r1, + UnsignedMode::Infinity => r2, + UnsignedMode::HalfEven => match cmp { + std::cmp::Ordering::Less => r1, + std::cmp::Ordering::Equal => { + if even { + r1 + } else { + r2 + } + } + std::cmp::Ordering::Greater => r2, + }, + UnsignedMode::HalfInfinity => { + if cmp == std::cmp::Ordering::Less { + r1 + } else { + r2 + } + } + UnsignedMode::HalfZero => { + if cmp == std::cmp::Ordering::Greater { + r2 + } else { + r1 + } + } + } +} + +/// ComputeNudgeWindow:smallestUnit 为 day/week/month/year 时的舍入窗口。 +/// unit:0=year 1=month 2=week 3=day。 +pub(crate) fn nudge_window( + sign: i128, values: &[f64; 10], date1: (i128, i128, i128), increment: i128, unit: usize, shift: bool, +) -> (i128, i128, [f64; 10], [f64; 10]) { + let years = values[0] as i128; + let months = values[1] as i128; + let weeks = values[2] as i128; + let days = values[3] as i128; + let (r1, r2) = match unit { + 0 => { + let y = round_to_increment_trunc(years, increment); + let r1 = if !shift { y } else { y + increment * sign }; + (r1, r1 + increment * sign) + } + 1 => { + let m = round_to_increment_trunc(months, increment); + let r1 = if !shift { m } else { m + increment * sign }; + (r1, r1 + increment * sign) + } + 2 => { + let weeks_start = add_months_i128(date1.0, date1.1, date1.2, years * 12 + months); + let weeks_end = add_days_iso(weeks_start, days); + let w = date_until_iso(weeks_start, weeks_end, 2)[2] as i128; + let total = weeks + w; + let r1 = round_to_increment_trunc(total, increment); + let r1 = if !shift { r1 } else { r1 + increment * sign }; + (r1, r1 + increment * sign) + } + _ => { + let d = round_to_increment_trunc(days, increment); + let r1 = if !shift { d } else { d + increment * sign }; + (r1, r1 + increment * sign) + } + }; + let mut start = [0.0; 10]; + let mut end = [0.0; 10]; + match unit { + 0 => { + start[0] = r1 as f64; + end[0] = r2 as f64; + } + 1 => { + start[0] = years as f64; + start[1] = r1 as f64; + end[0] = years as f64; + end[1] = r2 as f64; + } + 2 => { + start[0] = years as f64; + start[1] = months as f64; + start[2] = r1 as f64; + end[0] = years as f64; + end[1] = months as f64; + end[2] = r2 as f64; + } + _ => { + start[0] = years as f64; + start[1] = months as f64; + start[2] = weeks as f64; + start[3] = r1 as f64; + end[0] = years as f64; + end[1] = months as f64; + end[2] = weeks as f64; + end[3] = r2 as f64; + } + } + (r1, r2, start, end) +} + +/// NudgeToCalendarUnit:日历单位用纪元纳秒边界取整。 +#[allow(clippy::too_many_arguments)] +fn nudge_to_calendar_unit( + sign: i128, values: &[f64; 10], origin_epoch: i128, dest_epoch: i128, date1: (i128, i128, i128), time1_ns: i128, + increment: i128, unit: usize, mode: InstantRoundingMode, +) -> Result<([f64; 10], i128, bool), ()> { + let epoch_of = |dur: &[f64; 10]| -> Result { + if date_duration_sign(dur) == 0 { + return Ok(origin_epoch); + } + let date = add_date_duration(date1, dur); + let days = days_from_civil(date.0, date.1, date.2); + if days.abs() > MAX_ISO_DAY { + return Err(()); + } + Ok(days * DAY_NS + time1_ns) + }; + let mut did_expand = false; + let (mut r1, mut r2, mut start_dur, mut end_dur) = nudge_window(sign, values, date1, increment, unit, false); + let mut start_epoch = epoch_of(&start_dur)?; + let mut end_epoch = epoch_of(&end_dur)?; + let in_window = if sign > 0 { + dest_epoch >= start_epoch && dest_epoch <= end_epoch + } else { + dest_epoch <= start_epoch && dest_epoch >= end_epoch + }; + if !in_window { + (r1, r2, start_dur, end_dur) = nudge_window(sign, values, date1, increment, unit, true); + start_epoch = epoch_of(&start_dur)?; + end_epoch = epoch_of(&end_dur)?; + did_expand = true; + let in_window = if sign > 0 { + dest_epoch >= start_epoch && dest_epoch <= end_epoch + } else { + dest_epoch <= start_epoch && dest_epoch >= end_epoch + }; + if !in_window { + return Err(()); + } + } + let numerator = dest_epoch - start_epoch; + let denominator = end_epoch - start_epoch; + let even = (r1.abs() / increment) % 2 == 0; + let rounded_unit = if numerator == 0 { + r1.abs() + } else if numerator == denominator { + r2.abs() + } else { + let cmp = (numerator * 2).abs().cmp(&denominator.abs()); + apply_unsigned_rounding(r1.abs(), r2.abs(), cmp, even, mode, sign < 0) + }; + did_expand = did_expand || rounded_unit == r2.abs(); + let duration = if rounded_unit == r2.abs() { end_dur } else { start_dur }; + let nudged = if did_expand { end_epoch } else { start_epoch }; + Ok((duration, nudged, did_expand)) +} + +/// BubbleRelativeDuration:舍入越过小单位边界时向更大单位进位(到 largest 为止)。 +fn bubble_relative_duration( + sign: i128, mut values: [f64; 10], nudged_epoch: i128, date1: (i128, i128, i128), time1_ns: i128, largest: usize, + start_unit: usize, +) -> Result<[f64; 10], ()> { + if start_unit == 0 { + return Ok(values); + } + let mut unit = start_unit - 1; + loop { + if unit >= largest { + if unit == 2 && largest != 2 { + // weeks 不向 months 进位,跳过。 + if unit == 0 { + return Ok(values); + } + unit -= 1; + continue; + } + let mut end_dur = values; + match unit { + 0 => { + end_dur[0] = (values[0] as i128 + sign) as f64; + end_dur[1] = 0.0; + end_dur[2] = 0.0; + end_dur[3] = 0.0; + } + 1 => { + end_dur[1] = (values[1] as i128 + sign) as f64; + end_dur[2] = 0.0; + end_dur[3] = 0.0; + } + 2 => { + end_dur[2] = (values[2] as i128 + sign) as f64; + end_dur[3] = 0.0; + } + _ => unreachable!(), + } + end_dur[4..10].fill(0.0); + let end_date = add_date_duration(date1, &end_dur); + // Bubble 边界只用于比较,不做 ISO 范围校验(对齐 bugzilla 2036259)。 + let end_epoch = days_from_civil(end_date.0, end_date.1, end_date.2) * DAY_NS + time1_ns; + let reached_end = if sign > 0 { nudged_epoch >= end_epoch } else { nudged_epoch <= end_epoch }; + if reached_end { + values = end_dur; + } else { + return Ok(values); + } + if unit == 0 { + return Ok(values); + } + unit -= 1; + } else { + return Ok(values); + } + } +} + +/// 在年月上推进指定月数并保持日(超出目标月末时截断)。 +fn add_months_i128(year: i128, month: i128, day: i128, months: i128) -> (i128, i128, i128) { + let total = year * 12 + (month - 1) + months; + let ny = total.div_euclid(12); + let nm = total.rem_euclid(12) + 1; + let max_day = days_in_month(ny, nm).unwrap_or(31); + (ny, nm, day.min(max_day)) +} + +/// `Temporal.PlainDateTime.prototype.until/since(other, options)`:按最大/最小单位 +/// 差值设置(GetDifferenceSettings 产物:单位层级、增量、舍入模式)。 +pub(crate) struct DifferenceSettings { + pub(crate) largest_index: usize, + pub(crate) smallest_index: usize, + pub(crate) increment: i128, + pub(crate) mode: InstantRoundingMode, +} + +/// PlainDate 差值单位层级:0=year 1=month 2=week 3=day(不含时间单位)。 +fn plain_date_unit_index(value: &str) -> Option { + match value { + "year" | "years" => Some(0), + "month" | "months" => Some(1), + "week" | "weeks" => Some(2), + "day" | "days" => Some(3), + _ => None, + } +} + +/// PlainYearMonth 差值单位层级:仅 0=year 1=month(week/day 及时间单位均不合法)。 +fn plain_year_month_unit_index(value: &str) -> Option { + match value { + "year" | "years" => Some(0), + "month" | "months" => Some(1), + _ => None, + } +} + +/// 解析差值选项(对齐 GetDifferenceSettings):读取顺序 largestUnit → +/// roundingIncrement → roundingMode → smallestUnit。date_only 时单位限定 +/// year/month/week/day,smallestUnit 缺省 "day"(含时间时缺省 "nanosecond")。 +/// default_largest 为 largestUnit 缺省/auto 时相对 smallestUnit 的取小上限 +/// (PDT/PD 传 3=day,ZDT 传 4=hour)。 +pub(crate) fn parse_difference_settings( + vm: &mut H, options_value: JsValue, date_only: bool, default_largest: usize, +) -> Result { + let unit_index = |value: &str| -> Option { + if date_only { + plain_date_unit_index(value) + } else { + plain_date_time_unit_index(value) + } + }; + let default_smallest = if date_only { "day" } else { "nanosecond" }; + let (largest_raw, increment_value, mode_value, smallest_raw) = if options_value.is_undefined() { + (None, 1.0, "trunc".to_string(), default_smallest.to_string()) + } else { + if !options_value.is_object() { + return Err(crate::error::create_type_error(vm, "options must be an object")); + } + let options_ptr = options_value.as_js_object_ptr(); + if options_ptr.is_null() { + return Err(crate::error::create_type_error(vm, "options must be an object")); + } + let options = unsafe { &*options_ptr }; + let largest_raw = match temporal_option_value(vm, options, options_value, "largestUnit") { + Ok(raw) if raw.is_undefined() => None, + Ok(raw) => Some(temporal_option_string(vm, raw)?), + Err(error) => return Err(error), + }; + let increment_raw = match temporal_option_value(vm, options, options_value, "roundingIncrement") { + Ok(raw) if raw.is_undefined() => 1.0, + Ok(raw) => temporal_option_number(vm, raw)?, + Err(error) => return Err(error), + }; + let mode_raw = match temporal_option_value(vm, options, options_value, "roundingMode") { + Ok(raw) if raw.is_undefined() => "trunc".to_string(), + Ok(raw) => temporal_option_string(vm, raw)?, + Err(error) => return Err(error), + }; + let smallest_raw = match temporal_option_value(vm, options, options_value, "smallestUnit") { + Ok(raw) if raw.is_undefined() => default_smallest.to_string(), + Ok(raw) => temporal_option_string(vm, raw)?, + Err(error) => return Err(error), + }; + (largest_raw, increment_raw, mode_raw, smallest_raw) + }; + + let smallest_index = match unit_index(&smallest_raw) { + Some(index) => index, + None => return Err(crate::error::create_range_error(vm, "invalid smallestUnit")), + }; + // auto/缺省:LargerOfTwoTemporalUnits(default_largest, smallestUnit)。 + // 索引 0=year…3=day…9=nanosecond,更大单位取更小索引,故为 min(default_largest, smallest)。 + let largest_index = match largest_raw { + Some(value) if value == "auto" => smallest_index.min(default_largest), + Some(value) => match unit_index(&value) { + Some(index) => index, + None => return Err(crate::error::create_range_error(vm, "invalid largestUnit")), + }, + None => smallest_index.min(default_largest), + }; + if largest_index > smallest_index { + return Err(crate::error::create_range_error(vm, "smallestUnit exceeds largestUnit")); + } + let Some(mode) = instant_rounding_mode(&mode_value) else { + return Err(crate::error::create_range_error(vm, "invalid roundingMode")); + }; + if !increment_value.is_finite() { + return Err(crate::error::create_range_error(vm, "invalid roundingIncrement")); + } + let increment = increment_value.trunc(); + if !(1.0..=1_000_000_000.0).contains(&increment) { + return Err(crate::error::create_range_error(vm, "invalid roundingIncrement")); + } + let increment = increment as i128; + const UNIT_LIMITS: [i128; 6] = [24, 60, 60, 1_000, 1_000, 1_000]; + if smallest_index >= 4 { + let limit = UNIT_LIMITS[smallest_index - 4]; + if increment >= limit || limit % increment != 0 { + return Err(crate::error::create_range_error(vm, "invalid roundingIncrement")); + } + } + Ok(DifferenceSettings { + largest_index, + smallest_index, + increment, + mode, + }) +} + +/// 解析 PlainYearMonth 差值选项(对齐 GetDifferenceSettings,unitGroup=date): +/// 单位表仅 year/month(week/day/时间单位不合法);smallestUnit 缺省 "month", +/// largestUnit 缺省/auto 为 LargerOfTwo(year, smallest) = year。 +/// 读取顺序 largestUnit → roundingIncrement → roundingMode → smallestUnit(字典序)。 +pub(crate) fn parse_year_month_difference_settings( + vm: &mut H, options_value: JsValue, +) -> Result { + let default_smallest = "month"; + let default_largest = 0usize; // year + let (largest_raw, increment_value, mode_value, smallest_raw) = if options_value.is_undefined() { + (None, 1.0, "trunc".to_string(), default_smallest.to_string()) + } else { + if !options_value.is_object() { + return Err(crate::error::create_type_error(vm, "options must be an object")); + } + let options_ptr = options_value.as_js_object_ptr(); + if options_ptr.is_null() { + return Err(crate::error::create_type_error(vm, "options must be an object")); + } + let options = unsafe { &*options_ptr }; + let largest_raw = match temporal_option_value(vm, options, options_value, "largestUnit") { + Ok(raw) if raw.is_undefined() => None, + Ok(raw) => Some(temporal_option_string(vm, raw)?), + Err(error) => return Err(error), + }; + let increment_raw = match temporal_option_value(vm, options, options_value, "roundingIncrement") { + Ok(raw) if raw.is_undefined() => 1.0, + Ok(raw) => temporal_option_number(vm, raw)?, + Err(error) => return Err(error), + }; + let mode_raw = match temporal_option_value(vm, options, options_value, "roundingMode") { + Ok(raw) if raw.is_undefined() => "trunc".to_string(), + Ok(raw) => temporal_option_string(vm, raw)?, + Err(error) => return Err(error), + }; + let smallest_raw = match temporal_option_value(vm, options, options_value, "smallestUnit") { + Ok(raw) if raw.is_undefined() => default_smallest.to_string(), + Ok(raw) => temporal_option_string(vm, raw)?, + Err(error) => return Err(error), + }; + (largest_raw, increment_raw, mode_raw, smallest_raw) + }; + + let smallest_index = match plain_year_month_unit_index(&smallest_raw) { + Some(index) => index, + None => return Err(crate::error::create_range_error(vm, "invalid smallestUnit")), + }; + // auto/缺省:LargerOfTwoTemporalUnits(year, smallestUnit)。更大单位取更小索引。 + let largest_index = match largest_raw { + Some(value) if value == "auto" => smallest_index.min(default_largest), + Some(value) => match plain_year_month_unit_index(&value) { + Some(index) => index, + None => return Err(crate::error::create_range_error(vm, "invalid largestUnit")), + }, + None => smallest_index.min(default_largest), + }; + if largest_index > smallest_index { + return Err(crate::error::create_range_error(vm, "smallestUnit exceeds largestUnit")); + } + let Some(mode) = instant_rounding_mode(&mode_value) else { + return Err(crate::error::create_range_error(vm, "invalid roundingMode")); + }; + if !increment_value.is_finite() { + return Err(crate::error::create_range_error(vm, "invalid roundingIncrement")); + } + let increment = increment_value.trunc(); + if !(1.0..=1_000_000_000.0).contains(&increment) { + return Err(crate::error::create_range_error(vm, "invalid roundingIncrement")); + } + Ok(DifferenceSettings { + largest_index, + smallest_index, + increment: increment as i128, + mode, + }) +} + +/// 差值核心:internal = end - start,按设置取整;since 用 NegateRoundingMode +/// 的舍入模式并在最后整体取反(不调换两端,调换会改变 0.5 边界所在的年长)。 +/// no_rounding = 单位索引空间中"最细单位"的索引:该单位且 increment=1 时 +/// 舍入为恒等变换,跳过 nudge(边界处避免窗口端点的无效范围抛错)。 +#[allow(clippy::too_many_arguments)] +pub(crate) fn difference_core( + vm: &mut H, start: (i128, i128, i128), start_time_ns: i128, end: (i128, i128, i128), end_time_ns: i128, + settings: DifferenceSettings, since: bool, no_rounding: usize, +) -> NativeResult { + let values = match nudge_iso_difference(vm, start, start_time_ns, end, end_time_ns, settings, since, no_rounding) { + Ok(values) => values, + Err(error) => return NativeResult::Err(error), + }; + make_duration(vm, values) +} + +/// 对 `end - start` 的 ISO 日期时间差按设置取整并平衡到最大单位。 +/// +/// # 步骤 +/// 1. 日期差按 largestUnit 分解(时间单位时 days 并入时间)。 +/// 2. 不要求舍入(smallest=nanosecond 且 increment=1)时直接按最大单位拆回。 +/// 3. smallest 为 day/时间单位走 NudgeToDayOrTime;为 year/month/week 走 NudgeToCalendarUnit。 +/// 4. since 用 NegateRoundingMode 舍入并在最后整体取反。 +/// +/// # 边界与前提 +/// - Duration.round 的日历单位路径与本函数共用:round 把 duration 应用 relativeTo 后 +/// 以同样的 start/end 差输入本函数,输出即 balance 后的舍入结果。 +/// - 时间分量为带符号 i128;start/end 日与时间各自独立,符号由差值推导。 +#[allow(clippy::too_many_arguments)] +pub(crate) fn nudge_iso_difference( + vm: &mut H, start: (i128, i128, i128), start_time_ns: i128, end: (i128, i128, i128), end_time_ns: i128, + settings: DifferenceSettings, since: bool, no_rounding: usize, +) -> Result<[f64; 10], JsValue> { + let DifferenceSettings { + largest_index, + smallest_index, + increment, + mut mode, + } = settings; + if since { + mode = match mode { + InstantRoundingMode::Ceil => InstantRoundingMode::Floor, + InstantRoundingMode::Floor => InstantRoundingMode::Ceil, + InstantRoundingMode::HalfCeil => InstantRoundingMode::HalfFloor, + InstantRoundingMode::HalfFloor => InstantRoundingMode::HalfCeil, + other => other, + }; + } + const UNIT_NS: [i128; 6] = [3_600_000_000_000, 60_000_000_000, 1_000_000_000, 1_000_000, 1_000, 1]; + const DAY_NS: i128 = 86_400_000_000_000; + + // 规范语义:internal = other - receiver;since 用 NegateRoundingMode 的舍入模式,最后整体取反。 + // 不调换两端(调换会改变 0.5 边界所在的年长,破坏对称性)。 + let origin_epoch = days_from_civil(start.0, start.1, start.2) * DAY_NS + start_time_ns; + let dest_epoch = days_from_civil(end.0, end.1, end.2) * DAY_NS + end_time_ns; + + let date1 = start; + let time1_ns = start_time_ns; + let mut date2 = end; + let mut time_ns = end_time_ns - time1_ns; + let time_sign = if time_ns > 0 { + 1_i128 + } else if time_ns < 0 { + -1_i128 + } else { + 0 + }; + let date_sign = compare_iso_date(date1, date2); + // 日期差与时间差符号一致时,从日期差借一天给时间差,使时间落回一天内。 + if date_sign != 0 && date_sign == time_sign { + date2 = add_days_iso(date2, time_sign); + time_ns -= time_sign * DAY_NS; + } + + // 日期部分:largestUnit 为时间单位时按 day 分解,再把 days 并入时间。 + let date_largest = largest_index.min(3); + let mut date_values = date_until_iso(date1, date2, date_largest); + let mut date_days = date_values[3] as i128; + if largest_index >= 4 { + time_ns += date_days * DAY_NS; + date_values[3] = 0.0; + date_days = 0; + } + + let sign = { + let date_sign = date_duration_sign(&date_values); + if date_sign != 0 { + date_sign + } else if time_ns > 0 { + 1 + } else if time_ns < 0 { + -1 + } else { + 1 + } + }; + + let mut values = if smallest_index == no_rounding && increment == 1 { + // 不要求舍入:时间按最大单位拆回。 + let mut values = date_values; + if largest_index >= 4 { + let Some(time_values) = balance_instant_difference(time_ns, largest_index - 4) else { + return Err(crate::error::create_range_error(vm, "difference is out of range")); + }; + values[4..10].copy_from_slice(&time_values[4..10]); + } else { + let day_part = time_ns / DAY_NS; + let rem = time_ns % DAY_NS; + values[3] += day_part as f64; + let time_values = plain_time_components_signed(rem); + for i in 0..6 { + values[4 + i] = time_values[i] as f64; + } + } + values + } else if smallest_index >= 3 { + // NudgeToDayOrTime:合并天与时间为总纳秒后按单位取整(day 为均匀单位,同样走此路径)。 + let total_ns = time_ns + date_days * DAY_NS; + let quantum = if smallest_index == 3 { + DAY_NS * increment + } else { + UNIT_NS[smallest_index - 4] * increment + }; + let Some(rounded_ns) = round_instant_difference(total_ns, quantum, mode) else { + return Err(crate::error::create_range_error(vm, "difference is out of range")); + }; + let whole_days = rounded_ns / DAY_NS; + let rem = rounded_ns % DAY_NS; + let old_whole = total_ns / DAY_NS; + let did_expand_days = (whole_days - old_whole).signum() == total_ns.signum(); + let mut values = date_values; + if largest_index >= 4 { + let Some(time_values) = balance_instant_difference(rounded_ns, largest_index - 4) else { + return Err(crate::error::create_range_error(vm, "difference is out of range")); + }; + values[4..10].copy_from_slice(&time_values[4..10]); + } else { + values[3] = whole_days as f64; + let time_values = plain_time_components_signed(rem); + for i in 0..6 { + values[4 + i] = time_values[i] as f64; + } + } + if did_expand_days { + let nudged = dest_epoch + (rounded_ns - total_ns); + match bubble_relative_duration(sign, values, nudged, date1, time1_ns, largest_index, 3) { + Ok(bubbled) => values = bubbled, + Err(()) => return Err(crate::error::create_range_error(vm, "difference is out of range")), + } + } + values + } else { + // NudgeToCalendarUnit:year/month/week 用纪元纳秒窗口取整。 + let (mut values, nudged_epoch, did_expand) = match nudge_to_calendar_unit( + sign, + &date_values, + origin_epoch, + dest_epoch, + date1, + time1_ns, + increment, + smallest_index, + mode, + ) { + Ok(result) => result, + Err(()) => return Err(crate::error::create_range_error(vm, "difference is out of range")), + }; + if did_expand && smallest_index != 2 { + match bubble_relative_duration(sign, values, nudged_epoch, date1, time1_ns, largest_index, smallest_index) { + Ok(bubbled) => values = bubbled, + Err(()) => return Err(crate::error::create_range_error(vm, "difference is out of range")), + } + } + values + }; + if since { + for value in &mut values { + if *value != 0.0 { + *value = -*value; + } + } + } + Ok(values) +} diff --git a/crates/oxide_builtins/src/temporal/mod.rs b/crates/oxide_builtins/src/temporal/mod.rs index 778baae7..7402ce2d 100644 --- a/crates/oxide_builtins/src/temporal/mod.rs +++ b/crates/oxide_builtins/src/temporal/mod.rs @@ -7,8 +7,10 @@ use oxide_types::object::JsObject; use oxide_types::value::JsValue; mod common; +mod difference; use common::*; +use difference::*; fn instant_string_without_annotations(input: &str) -> Option<&str> { let Some(first_annotation) = input.find('[') else { @@ -613,52 +615,6 @@ fn instant_difference_unit(value: &str) -> Option<(usize, i128, i128)> { } } -/// 按普通有符号数语义舍入 Instant 差值。 -fn round_instant_difference(value: i128, increment: i128, mode: InstantRoundingMode) -> Option { - let negative = value < 0; - let magnitude = value.checked_abs()?; - let quotient = magnitude / increment; - let remainder = magnitude % increment; - if remainder == 0 { - return Some(value); - } - let use_upper = match mode { - InstantRoundingMode::Ceil => !negative, - InstantRoundingMode::Expand => true, - InstantRoundingMode::Floor => negative, - InstantRoundingMode::Trunc => false, - _ => match (remainder * 2).cmp(&increment) { - std::cmp::Ordering::Less => false, - std::cmp::Ordering::Greater => true, - std::cmp::Ordering::Equal => match mode { - InstantRoundingMode::HalfCeil => !negative, - InstantRoundingMode::HalfEven => quotient.rem_euclid(2) != 0, - InstantRoundingMode::HalfExpand => true, - InstantRoundingMode::HalfFloor => negative, - InstantRoundingMode::HalfTrunc => false, - _ => unreachable!(), - }, - }, - }; - let rounded = quotient.checked_add(i128::from(use_upper))?.checked_mul(increment)?; - Some(if negative { -rounded } else { rounded }) -} - -fn balance_instant_difference(value: i128, largest_unit: usize) -> Option<[f64; 10]> { - const SCALES: [i128; 6] = [3_600_000_000_000, 60_000_000_000, 1_000_000_000, 1_000_000, 1_000, 1]; - let negative = value < 0; - let mut remainder = value.checked_abs()?; - let mut values = [0.0; 10]; - for (index, scale) in SCALES.iter().enumerate().skip(largest_unit) { - let component = remainder / scale; - remainder %= scale; - if component != 0 { - values[index + 4] = if negative { -(component as f64) } else { component as f64 }; - } - } - Some(values) -} - fn instant_difference(vm: &mut H, args: &[u8], since: bool) -> NativeResult { let ptr = native_try!(receiver_obj(vm, args)); let obj = unsafe { &*ptr }; @@ -5351,803 +5307,6 @@ fn plain_date_time_apply_duration(vm: &mut H, args: &[u8], sign: i64) make_plain_date_time(vm, yy as i32, mm as u32, dd as u32, new_time_ns as f64, &get_calendar_id(obj, 4)) } -/// PlainDateTime 差值单位层级:year=0 … nanosecond=9;"auto" 仅限 largestUnit。 -fn plain_date_time_unit_index(value: &str) -> Option { - match value { - "year" | "years" => Some(0), - "month" | "months" => Some(1), - "week" | "weeks" => Some(2), - "day" | "days" => Some(3), - "hour" | "hours" => Some(4), - "minute" | "minutes" => Some(5), - "second" | "seconds" => Some(6), - "millisecond" | "milliseconds" => Some(7), - "microsecond" | "microseconds" => Some(8), - "nanosecond" | "nanoseconds" => Some(9), - _ => None, - } -} - -const DAY_NS: i128 = 86_400_000_000_000; - -const MAX_ISO_DAY: i128 = 100_000_000; - -/// 比较两个 ISO 日期,返回 -1/0/+1。 -fn compare_iso_date(a: (i128, i128, i128), b: (i128, i128, i128)) -> i128 { - if a.0 != b.0 { - return if a.0 < b.0 { -1 } else { 1 }; - } - if a.1 != b.1 { - return if a.1 < b.1 { -1 } else { 1 }; - } - if a.2 != b.2 { - return if a.2 < b.2 { -1 } else { 1 }; - } - 0 -} - -/// 候选日期(日以 day_override 参与比较)是否在 sign 方向上越过终点。 -fn surpasses_with_day(sign: i128, candidate: (i128, i128, i128), day_override: i128, end: (i128, i128, i128)) -> bool { - let cmp = compare_iso_date((candidate.0, candidate.1, day_override), end); - if sign > 0 { - cmp > 0 - } else { - cmp < 0 - } -} - -/// 日期加天数。 -fn add_days_iso(date: (i128, i128, i128), days: i128) -> (i128, i128, i128) { - let (y, m, d) = civil_from_days(days_from_civil(date.0, date.1, date.2) + days); - (y, m, d) -} - -/// ISO8601 日期差分解,对齐 polyfill 的 dateUntil/untilCalendar。 -/// largest:0=year 1=month 2=week 3=day。 -fn date_until_iso(start: (i128, i128, i128), end: (i128, i128, i128), largest: usize) -> [f64; 10] { - let mut values = [0.0; 10]; - if largest >= 2 { - let mut days = days_from_civil(end.0, end.1, end.2) - days_from_civil(start.0, start.1, start.2); - if largest == 2 { - values[2] = (days / 7) as f64; - days %= 7; - } - values[3] = days as f64; - return values; - } - let sign = compare_iso_date(end, start); - if sign == 0 { - return values; - } - let diff_years = end.0 - start.0; - let diff_days = end.2 - start.2; - let diff_in_year_sign = if end.1 > start.1 { - 1 - } else if end.1 < start.1 { - -1 - } else if diff_days > 0 { - 1 - } else if diff_days < 0 { - -1 - } else { - 0 - }; - // 终点的月-日早于起点的月-日时,年份差需沿 sign 方向修正 1。 - let mut years = if diff_in_year_sign * sign < 0 { diff_years - sign } else { diff_years }; - let mut months = 0_i128; - if largest == 1 { - months = years * 12; - years = 0; - } - let intermediate = add_months_i128(start.0, start.1, start.2, years * 12 + months); - // 闰日校正:intermediate 已越过终点时年份回退 1。 - if surpasses_with_day(sign, intermediate, start.2, end) { - years -= sign; - } - // 从 start 按锚定日逐月推进(月末钳制);current 始终是未越界的真实日期, - // 锚定日覆盖只用于越界比较,避免伪日期(如 2 月 29 日)污染天数计算。 - let mut current = add_months_i128(start.0, start.1, start.2, years * 12 + months); - loop { - months += sign; - let candidate = add_months_i128(start.0, start.1, start.2, years * 12 + months); - let mut cmp = candidate; - cmp.2 = start.2; - if surpasses_with_day(sign, cmp, start.2, end) { - break; - } - current = candidate; - } - months -= sign; - let days = days_from_civil(end.0, end.1, end.2) - days_from_civil(current.0, current.1, current.2); - values[0] = years as f64; - values[1] = months as f64; - values[3] = days as f64; - values -} - -/// 向零截断到 increment 的倍数。 -fn round_to_increment_trunc(value: i128, increment: i128) -> i128 { - (value / increment) * increment -} - -/// 时长日期部分符号(首个非零分量决定)。 -fn date_duration_sign(values: &[f64; 10]) -> i128 { - for value in values[0..4].iter().copied() { - if value != 0.0 { - return if value > 0.0 { 1 } else { -1 }; - } - } - 0 -} - -/// 在日期上叠加时长(年/月/周/日)。 -fn add_date_duration(date: (i128, i128, i128), values: &[f64; 10]) -> (i128, i128, i128) { - let years = values[0] as i128; - let months = values[1] as i128; - let weeks = values[2] as i128; - let days = values[3] as i128; - let d = add_months_i128(date.0, date.1, date.2, years * 12 + months); - add_days_iso(d, weeks * 7 + days) -} - -/// 无符号舍入模式(ApplyUnsignedRoundingMode 用的模式集合)。 -#[derive(Clone, Copy)] -enum UnsignedMode { - Zero, - Infinity, - HalfEven, - HalfInfinity, - HalfZero, -} - -/// ApplyUnsignedRoundingMode:在 r1/r2 间选择(均为非负量)。 -fn apply_unsigned_rounding( - r1: i128, r2: i128, cmp: std::cmp::Ordering, even: bool, mode: InstantRoundingMode, negative: bool, -) -> i128 { - let unsigned_mode = match mode { - InstantRoundingMode::Ceil => { - if negative { - UnsignedMode::Zero - } else { - UnsignedMode::Infinity - } - } - InstantRoundingMode::Floor => { - if negative { - UnsignedMode::Infinity - } else { - UnsignedMode::Zero - } - } - InstantRoundingMode::Expand => UnsignedMode::Infinity, - InstantRoundingMode::Trunc => UnsignedMode::Zero, - InstantRoundingMode::HalfCeil => { - if negative { - UnsignedMode::HalfZero - } else { - UnsignedMode::HalfInfinity - } - } - InstantRoundingMode::HalfFloor => { - if negative { - UnsignedMode::HalfInfinity - } else { - UnsignedMode::HalfZero - } - } - InstantRoundingMode::HalfEven => UnsignedMode::HalfEven, - InstantRoundingMode::HalfExpand => UnsignedMode::HalfInfinity, - InstantRoundingMode::HalfTrunc => UnsignedMode::HalfZero, - }; - match unsigned_mode { - UnsignedMode::Zero => r1, - UnsignedMode::Infinity => r2, - UnsignedMode::HalfEven => match cmp { - std::cmp::Ordering::Less => r1, - std::cmp::Ordering::Equal => { - if even { - r1 - } else { - r2 - } - } - std::cmp::Ordering::Greater => r2, - }, - UnsignedMode::HalfInfinity => { - if cmp == std::cmp::Ordering::Less { - r1 - } else { - r2 - } - } - UnsignedMode::HalfZero => { - if cmp == std::cmp::Ordering::Greater { - r2 - } else { - r1 - } - } - } -} - -/// ComputeNudgeWindow:smallestUnit 为 day/week/month/year 时的舍入窗口。 -/// unit:0=year 1=month 2=week 3=day。 -fn nudge_window( - sign: i128, values: &[f64; 10], date1: (i128, i128, i128), increment: i128, unit: usize, shift: bool, -) -> (i128, i128, [f64; 10], [f64; 10]) { - let years = values[0] as i128; - let months = values[1] as i128; - let weeks = values[2] as i128; - let days = values[3] as i128; - let (r1, r2) = match unit { - 0 => { - let y = round_to_increment_trunc(years, increment); - let r1 = if !shift { y } else { y + increment * sign }; - (r1, r1 + increment * sign) - } - 1 => { - let m = round_to_increment_trunc(months, increment); - let r1 = if !shift { m } else { m + increment * sign }; - (r1, r1 + increment * sign) - } - 2 => { - let weeks_start = add_months_i128(date1.0, date1.1, date1.2, years * 12 + months); - let weeks_end = add_days_iso(weeks_start, days); - let w = date_until_iso(weeks_start, weeks_end, 2)[2] as i128; - let total = weeks + w; - let r1 = round_to_increment_trunc(total, increment); - let r1 = if !shift { r1 } else { r1 + increment * sign }; - (r1, r1 + increment * sign) - } - _ => { - let d = round_to_increment_trunc(days, increment); - let r1 = if !shift { d } else { d + increment * sign }; - (r1, r1 + increment * sign) - } - }; - let mut start = [0.0; 10]; - let mut end = [0.0; 10]; - match unit { - 0 => { - start[0] = r1 as f64; - end[0] = r2 as f64; - } - 1 => { - start[0] = years as f64; - start[1] = r1 as f64; - end[0] = years as f64; - end[1] = r2 as f64; - } - 2 => { - start[0] = years as f64; - start[1] = months as f64; - start[2] = r1 as f64; - end[0] = years as f64; - end[1] = months as f64; - end[2] = r2 as f64; - } - _ => { - start[0] = years as f64; - start[1] = months as f64; - start[2] = weeks as f64; - start[3] = r1 as f64; - end[0] = years as f64; - end[1] = months as f64; - end[2] = weeks as f64; - end[3] = r2 as f64; - } - } - (r1, r2, start, end) -} - -/// NudgeToCalendarUnit:日历单位用纪元纳秒边界取整。 -#[allow(clippy::too_many_arguments)] -fn nudge_to_calendar_unit( - sign: i128, values: &[f64; 10], origin_epoch: i128, dest_epoch: i128, date1: (i128, i128, i128), time1_ns: i128, - increment: i128, unit: usize, mode: InstantRoundingMode, -) -> Result<([f64; 10], i128, bool), ()> { - let epoch_of = |dur: &[f64; 10]| -> Result { - if date_duration_sign(dur) == 0 { - return Ok(origin_epoch); - } - let date = add_date_duration(date1, dur); - let days = days_from_civil(date.0, date.1, date.2); - if days.abs() > MAX_ISO_DAY { - return Err(()); - } - Ok(days * DAY_NS + time1_ns) - }; - let mut did_expand = false; - let (mut r1, mut r2, mut start_dur, mut end_dur) = nudge_window(sign, values, date1, increment, unit, false); - let mut start_epoch = epoch_of(&start_dur)?; - let mut end_epoch = epoch_of(&end_dur)?; - let in_window = if sign > 0 { - dest_epoch >= start_epoch && dest_epoch <= end_epoch - } else { - dest_epoch <= start_epoch && dest_epoch >= end_epoch - }; - if !in_window { - (r1, r2, start_dur, end_dur) = nudge_window(sign, values, date1, increment, unit, true); - start_epoch = epoch_of(&start_dur)?; - end_epoch = epoch_of(&end_dur)?; - did_expand = true; - let in_window = if sign > 0 { - dest_epoch >= start_epoch && dest_epoch <= end_epoch - } else { - dest_epoch <= start_epoch && dest_epoch >= end_epoch - }; - if !in_window { - return Err(()); - } - } - let numerator = dest_epoch - start_epoch; - let denominator = end_epoch - start_epoch; - let even = (r1.abs() / increment) % 2 == 0; - let rounded_unit = if numerator == 0 { - r1.abs() - } else if numerator == denominator { - r2.abs() - } else { - let cmp = (numerator * 2).abs().cmp(&denominator.abs()); - apply_unsigned_rounding(r1.abs(), r2.abs(), cmp, even, mode, sign < 0) - }; - did_expand = did_expand || rounded_unit == r2.abs(); - let duration = if rounded_unit == r2.abs() { end_dur } else { start_dur }; - let nudged = if did_expand { end_epoch } else { start_epoch }; - Ok((duration, nudged, did_expand)) -} - -/// BubbleRelativeDuration:舍入越过小单位边界时向更大单位进位(到 largest 为止)。 -fn bubble_relative_duration( - sign: i128, mut values: [f64; 10], nudged_epoch: i128, date1: (i128, i128, i128), time1_ns: i128, largest: usize, - start_unit: usize, -) -> Result<[f64; 10], ()> { - if start_unit == 0 { - return Ok(values); - } - let mut unit = start_unit - 1; - loop { - if unit >= largest { - if unit == 2 && largest != 2 { - // weeks 不向 months 进位,跳过。 - if unit == 0 { - return Ok(values); - } - unit -= 1; - continue; - } - let mut end_dur = values; - match unit { - 0 => { - end_dur[0] = (values[0] as i128 + sign) as f64; - end_dur[1] = 0.0; - end_dur[2] = 0.0; - end_dur[3] = 0.0; - } - 1 => { - end_dur[1] = (values[1] as i128 + sign) as f64; - end_dur[2] = 0.0; - end_dur[3] = 0.0; - } - 2 => { - end_dur[2] = (values[2] as i128 + sign) as f64; - end_dur[3] = 0.0; - } - _ => unreachable!(), - } - end_dur[4..10].fill(0.0); - let end_date = add_date_duration(date1, &end_dur); - // Bubble 边界只用于比较,不做 ISO 范围校验(对齐 bugzilla 2036259)。 - let end_epoch = days_from_civil(end_date.0, end_date.1, end_date.2) * DAY_NS + time1_ns; - let reached_end = if sign > 0 { nudged_epoch >= end_epoch } else { nudged_epoch <= end_epoch }; - if reached_end { - values = end_dur; - } else { - return Ok(values); - } - if unit == 0 { - return Ok(values); - } - unit -= 1; - } else { - return Ok(values); - } - } -} - -/// 在年月上推进指定月数并保持日(超出目标月末时截断)。 -fn add_months_i128(year: i128, month: i128, day: i128, months: i128) -> (i128, i128, i128) { - let total = year * 12 + (month - 1) + months; - let ny = total.div_euclid(12); - let nm = total.rem_euclid(12) + 1; - let max_day = days_in_month(ny, nm).unwrap_or(31); - (ny, nm, day.min(max_day)) -} - -/// `Temporal.PlainDateTime.prototype.until/since(other, options)`:按最大/最小单位 -/// 差值设置(GetDifferenceSettings 产物:单位层级、增量、舍入模式)。 -struct DifferenceSettings { - largest_index: usize, - smallest_index: usize, - increment: i128, - mode: InstantRoundingMode, -} - -/// PlainDate 差值单位层级:0=year 1=month 2=week 3=day(不含时间单位)。 -fn plain_date_unit_index(value: &str) -> Option { - match value { - "year" | "years" => Some(0), - "month" | "months" => Some(1), - "week" | "weeks" => Some(2), - "day" | "days" => Some(3), - _ => None, - } -} - -/// PlainYearMonth 差值单位层级:仅 0=year 1=month(week/day 及时间单位均不合法)。 -fn plain_year_month_unit_index(value: &str) -> Option { - match value { - "year" | "years" => Some(0), - "month" | "months" => Some(1), - _ => None, - } -} - -/// 解析差值选项(对齐 GetDifferenceSettings):读取顺序 largestUnit → -/// roundingIncrement → roundingMode → smallestUnit。date_only 时单位限定 -/// year/month/week/day,smallestUnit 缺省 "day"(含时间时缺省 "nanosecond")。 -/// default_largest 为 largestUnit 缺省/auto 时相对 smallestUnit 的取小上限 -/// (PDT/PD 传 3=day,ZDT 传 4=hour)。 -fn parse_difference_settings( - vm: &mut H, options_value: JsValue, date_only: bool, default_largest: usize, -) -> Result { - let unit_index = |value: &str| -> Option { - if date_only { - plain_date_unit_index(value) - } else { - plain_date_time_unit_index(value) - } - }; - let default_smallest = if date_only { "day" } else { "nanosecond" }; - let (largest_raw, increment_value, mode_value, smallest_raw) = if options_value.is_undefined() { - (None, 1.0, "trunc".to_string(), default_smallest.to_string()) - } else { - if !options_value.is_object() { - return Err(crate::error::create_type_error(vm, "options must be an object")); - } - let options_ptr = options_value.as_js_object_ptr(); - if options_ptr.is_null() { - return Err(crate::error::create_type_error(vm, "options must be an object")); - } - let options = unsafe { &*options_ptr }; - let largest_raw = match temporal_option_value(vm, options, options_value, "largestUnit") { - Ok(raw) if raw.is_undefined() => None, - Ok(raw) => Some(temporal_option_string(vm, raw)?), - Err(error) => return Err(error), - }; - let increment_raw = match temporal_option_value(vm, options, options_value, "roundingIncrement") { - Ok(raw) if raw.is_undefined() => 1.0, - Ok(raw) => temporal_option_number(vm, raw)?, - Err(error) => return Err(error), - }; - let mode_raw = match temporal_option_value(vm, options, options_value, "roundingMode") { - Ok(raw) if raw.is_undefined() => "trunc".to_string(), - Ok(raw) => temporal_option_string(vm, raw)?, - Err(error) => return Err(error), - }; - let smallest_raw = match temporal_option_value(vm, options, options_value, "smallestUnit") { - Ok(raw) if raw.is_undefined() => default_smallest.to_string(), - Ok(raw) => temporal_option_string(vm, raw)?, - Err(error) => return Err(error), - }; - (largest_raw, increment_raw, mode_raw, smallest_raw) - }; - - let smallest_index = match unit_index(&smallest_raw) { - Some(index) => index, - None => return Err(crate::error::create_range_error(vm, "invalid smallestUnit")), - }; - // auto/缺省:LargerOfTwoTemporalUnits(default_largest, smallestUnit)。 - // 索引 0=year…3=day…9=nanosecond,更大单位取更小索引,故为 min(default_largest, smallest)。 - let largest_index = match largest_raw { - Some(value) if value == "auto" => smallest_index.min(default_largest), - Some(value) => match unit_index(&value) { - Some(index) => index, - None => return Err(crate::error::create_range_error(vm, "invalid largestUnit")), - }, - None => smallest_index.min(default_largest), - }; - if largest_index > smallest_index { - return Err(crate::error::create_range_error(vm, "smallestUnit exceeds largestUnit")); - } - let Some(mode) = instant_rounding_mode(&mode_value) else { - return Err(crate::error::create_range_error(vm, "invalid roundingMode")); - }; - if !increment_value.is_finite() { - return Err(crate::error::create_range_error(vm, "invalid roundingIncrement")); - } - let increment = increment_value.trunc(); - if !(1.0..=1_000_000_000.0).contains(&increment) { - return Err(crate::error::create_range_error(vm, "invalid roundingIncrement")); - } - let increment = increment as i128; - const UNIT_LIMITS: [i128; 6] = [24, 60, 60, 1_000, 1_000, 1_000]; - if smallest_index >= 4 { - let limit = UNIT_LIMITS[smallest_index - 4]; - if increment >= limit || limit % increment != 0 { - return Err(crate::error::create_range_error(vm, "invalid roundingIncrement")); - } - } - Ok(DifferenceSettings { - largest_index, - smallest_index, - increment, - mode, - }) -} - -/// 解析 PlainYearMonth 差值选项(对齐 GetDifferenceSettings,unitGroup=date): -/// 单位表仅 year/month(week/day/时间单位不合法);smallestUnit 缺省 "month", -/// largestUnit 缺省/auto 为 LargerOfTwo(year, smallest) = year。 -/// 读取顺序 largestUnit → roundingIncrement → roundingMode → smallestUnit(字典序)。 -fn parse_year_month_difference_settings( - vm: &mut H, options_value: JsValue, -) -> Result { - let default_smallest = "month"; - let default_largest = 0usize; // year - let (largest_raw, increment_value, mode_value, smallest_raw) = if options_value.is_undefined() { - (None, 1.0, "trunc".to_string(), default_smallest.to_string()) - } else { - if !options_value.is_object() { - return Err(crate::error::create_type_error(vm, "options must be an object")); - } - let options_ptr = options_value.as_js_object_ptr(); - if options_ptr.is_null() { - return Err(crate::error::create_type_error(vm, "options must be an object")); - } - let options = unsafe { &*options_ptr }; - let largest_raw = match temporal_option_value(vm, options, options_value, "largestUnit") { - Ok(raw) if raw.is_undefined() => None, - Ok(raw) => Some(temporal_option_string(vm, raw)?), - Err(error) => return Err(error), - }; - let increment_raw = match temporal_option_value(vm, options, options_value, "roundingIncrement") { - Ok(raw) if raw.is_undefined() => 1.0, - Ok(raw) => temporal_option_number(vm, raw)?, - Err(error) => return Err(error), - }; - let mode_raw = match temporal_option_value(vm, options, options_value, "roundingMode") { - Ok(raw) if raw.is_undefined() => "trunc".to_string(), - Ok(raw) => temporal_option_string(vm, raw)?, - Err(error) => return Err(error), - }; - let smallest_raw = match temporal_option_value(vm, options, options_value, "smallestUnit") { - Ok(raw) if raw.is_undefined() => default_smallest.to_string(), - Ok(raw) => temporal_option_string(vm, raw)?, - Err(error) => return Err(error), - }; - (largest_raw, increment_raw, mode_raw, smallest_raw) - }; - - let smallest_index = match plain_year_month_unit_index(&smallest_raw) { - Some(index) => index, - None => return Err(crate::error::create_range_error(vm, "invalid smallestUnit")), - }; - // auto/缺省:LargerOfTwoTemporalUnits(year, smallestUnit)。更大单位取更小索引。 - let largest_index = match largest_raw { - Some(value) if value == "auto" => smallest_index.min(default_largest), - Some(value) => match plain_year_month_unit_index(&value) { - Some(index) => index, - None => return Err(crate::error::create_range_error(vm, "invalid largestUnit")), - }, - None => smallest_index.min(default_largest), - }; - if largest_index > smallest_index { - return Err(crate::error::create_range_error(vm, "smallestUnit exceeds largestUnit")); - } - let Some(mode) = instant_rounding_mode(&mode_value) else { - return Err(crate::error::create_range_error(vm, "invalid roundingMode")); - }; - if !increment_value.is_finite() { - return Err(crate::error::create_range_error(vm, "invalid roundingIncrement")); - } - let increment = increment_value.trunc(); - if !(1.0..=1_000_000_000.0).contains(&increment) { - return Err(crate::error::create_range_error(vm, "invalid roundingIncrement")); - } - Ok(DifferenceSettings { - largest_index, - smallest_index, - increment: increment as i128, - mode, - }) -} - -/// 差值核心:internal = end - start,按设置取整;since 用 NegateRoundingMode -/// 的舍入模式并在最后整体取反(不调换两端,调换会改变 0.5 边界所在的年长)。 -/// no_rounding = 单位索引空间中"最细单位"的索引:该单位且 increment=1 时 -/// 舍入为恒等变换,跳过 nudge(边界处避免窗口端点的无效范围抛错)。 -#[allow(clippy::too_many_arguments)] -fn difference_core( - vm: &mut H, start: (i128, i128, i128), start_time_ns: i128, end: (i128, i128, i128), end_time_ns: i128, - settings: DifferenceSettings, since: bool, no_rounding: usize, -) -> NativeResult { - let values = match nudge_iso_difference(vm, start, start_time_ns, end, end_time_ns, settings, since, no_rounding) { - Ok(values) => values, - Err(error) => return NativeResult::Err(error), - }; - make_duration(vm, values) -} - -/// 对 `end - start` 的 ISO 日期时间差按设置取整并平衡到最大单位。 -/// -/// # 步骤 -/// 1. 日期差按 largestUnit 分解(时间单位时 days 并入时间)。 -/// 2. 不要求舍入(smallest=nanosecond 且 increment=1)时直接按最大单位拆回。 -/// 3. smallest 为 day/时间单位走 NudgeToDayOrTime;为 year/month/week 走 NudgeToCalendarUnit。 -/// 4. since 用 NegateRoundingMode 舍入并在最后整体取反。 -/// -/// # 边界与前提 -/// - Duration.round 的日历单位路径与本函数共用:round 把 duration 应用 relativeTo 后 -/// 以同样的 start/end 差输入本函数,输出即 balance 后的舍入结果。 -/// - 时间分量为带符号 i128;start/end 日与时间各自独立,符号由差值推导。 -#[allow(clippy::too_many_arguments)] -fn nudge_iso_difference( - vm: &mut H, start: (i128, i128, i128), start_time_ns: i128, end: (i128, i128, i128), end_time_ns: i128, - settings: DifferenceSettings, since: bool, no_rounding: usize, -) -> Result<[f64; 10], JsValue> { - let DifferenceSettings { - largest_index, - smallest_index, - increment, - mut mode, - } = settings; - if since { - mode = match mode { - InstantRoundingMode::Ceil => InstantRoundingMode::Floor, - InstantRoundingMode::Floor => InstantRoundingMode::Ceil, - InstantRoundingMode::HalfCeil => InstantRoundingMode::HalfFloor, - InstantRoundingMode::HalfFloor => InstantRoundingMode::HalfCeil, - other => other, - }; - } - const UNIT_NS: [i128; 6] = [3_600_000_000_000, 60_000_000_000, 1_000_000_000, 1_000_000, 1_000, 1]; - const DAY_NS: i128 = 86_400_000_000_000; - - // 规范语义:internal = other - receiver;since 用 NegateRoundingMode 的舍入模式,最后整体取反。 - // 不调换两端(调换会改变 0.5 边界所在的年长,破坏对称性)。 - let origin_epoch = days_from_civil(start.0, start.1, start.2) * DAY_NS + start_time_ns; - let dest_epoch = days_from_civil(end.0, end.1, end.2) * DAY_NS + end_time_ns; - - let date1 = start; - let time1_ns = start_time_ns; - let mut date2 = end; - let mut time_ns = end_time_ns - time1_ns; - let time_sign = if time_ns > 0 { - 1_i128 - } else if time_ns < 0 { - -1_i128 - } else { - 0 - }; - let date_sign = compare_iso_date(date1, date2); - // 日期差与时间差符号一致时,从日期差借一天给时间差,使时间落回一天内。 - if date_sign != 0 && date_sign == time_sign { - date2 = add_days_iso(date2, time_sign); - time_ns -= time_sign * DAY_NS; - } - - // 日期部分:largestUnit 为时间单位时按 day 分解,再把 days 并入时间。 - let date_largest = largest_index.min(3); - let mut date_values = date_until_iso(date1, date2, date_largest); - let mut date_days = date_values[3] as i128; - if largest_index >= 4 { - time_ns += date_days * DAY_NS; - date_values[3] = 0.0; - date_days = 0; - } - - let sign = { - let date_sign = date_duration_sign(&date_values); - if date_sign != 0 { - date_sign - } else if time_ns > 0 { - 1 - } else if time_ns < 0 { - -1 - } else { - 1 - } - }; - - let mut values = if smallest_index == no_rounding && increment == 1 { - // 不要求舍入:时间按最大单位拆回。 - let mut values = date_values; - if largest_index >= 4 { - let Some(time_values) = balance_instant_difference(time_ns, largest_index - 4) else { - return Err(crate::error::create_range_error(vm, "difference is out of range")); - }; - values[4..10].copy_from_slice(&time_values[4..10]); - } else { - let day_part = time_ns / DAY_NS; - let rem = time_ns % DAY_NS; - values[3] += day_part as f64; - let time_values = plain_time_components_signed(rem); - for i in 0..6 { - values[4 + i] = time_values[i] as f64; - } - } - values - } else if smallest_index >= 3 { - // NudgeToDayOrTime:合并天与时间为总纳秒后按单位取整(day 为均匀单位,同样走此路径)。 - let total_ns = time_ns + date_days * DAY_NS; - let quantum = if smallest_index == 3 { - DAY_NS * increment - } else { - UNIT_NS[smallest_index - 4] * increment - }; - let Some(rounded_ns) = round_instant_difference(total_ns, quantum, mode) else { - return Err(crate::error::create_range_error(vm, "difference is out of range")); - }; - let whole_days = rounded_ns / DAY_NS; - let rem = rounded_ns % DAY_NS; - let old_whole = total_ns / DAY_NS; - let did_expand_days = (whole_days - old_whole).signum() == total_ns.signum(); - let mut values = date_values; - if largest_index >= 4 { - let Some(time_values) = balance_instant_difference(rounded_ns, largest_index - 4) else { - return Err(crate::error::create_range_error(vm, "difference is out of range")); - }; - values[4..10].copy_from_slice(&time_values[4..10]); - } else { - values[3] = whole_days as f64; - let time_values = plain_time_components_signed(rem); - for i in 0..6 { - values[4 + i] = time_values[i] as f64; - } - } - if did_expand_days { - let nudged = dest_epoch + (rounded_ns - total_ns); - match bubble_relative_duration(sign, values, nudged, date1, time1_ns, largest_index, 3) { - Ok(bubbled) => values = bubbled, - Err(()) => return Err(crate::error::create_range_error(vm, "difference is out of range")), - } - } - values - } else { - // NudgeToCalendarUnit:year/month/week 用纪元纳秒窗口取整。 - let (mut values, nudged_epoch, did_expand) = match nudge_to_calendar_unit( - sign, - &date_values, - origin_epoch, - dest_epoch, - date1, - time1_ns, - increment, - smallest_index, - mode, - ) { - Ok(result) => result, - Err(()) => return Err(crate::error::create_range_error(vm, "difference is out of range")), - }; - if did_expand && smallest_index != 2 { - match bubble_relative_duration(sign, values, nudged_epoch, date1, time1_ns, largest_index, smallest_index) { - Ok(bubbled) => values = bubbled, - Err(()) => return Err(crate::error::create_range_error(vm, "difference is out of range")), - } - } - values - }; - if since { - for value in &mut values { - if *value != 0.0 { - *value = -*value; - } - } - } - Ok(values) -} - /// 计算差值并舍入。until 返回 other 减 receiver,since 返回反向。 fn plain_date_time_difference(vm: &mut H, args: &[u8], since: bool) -> NativeResult { let (sy, sm, sd, st) = match plain_date_time_parts(vm, args) {