mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
refactor: temporal 差量层拆出独立模块
This commit is contained in:
@@ -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<i128> {
|
||||
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<usize> {
|
||||
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<i128, ()> {
|
||||
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<usize> {
|
||||
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<usize> {
|
||||
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<H: VmHost>(
|
||||
vm: &mut H, options_value: JsValue, date_only: bool, default_largest: usize,
|
||||
) -> Result<DifferenceSettings, JsValue> {
|
||||
let unit_index = |value: &str| -> Option<usize> {
|
||||
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<H: VmHost>(
|
||||
vm: &mut H, options_value: JsValue,
|
||||
) -> Result<DifferenceSettings, JsValue> {
|
||||
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<H: VmHost>(
|
||||
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<H: VmHost>(
|
||||
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)
|
||||
}
|
||||
@@ -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<i128> {
|
||||
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<H: VmHost>(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<H: VmHost>(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<usize> {
|
||||
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<i128, ()> {
|
||||
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<usize> {
|
||||
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<usize> {
|
||||
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<H: VmHost>(
|
||||
vm: &mut H, options_value: JsValue, date_only: bool, default_largest: usize,
|
||||
) -> Result<DifferenceSettings, JsValue> {
|
||||
let unit_index = |value: &str| -> Option<usize> {
|
||||
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<H: VmHost>(
|
||||
vm: &mut H, options_value: JsValue,
|
||||
) -> Result<DifferenceSettings, JsValue> {
|
||||
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<H: VmHost>(
|
||||
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<H: VmHost>(
|
||||
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<H: VmHost>(vm: &mut H, args: &[u8], since: bool) -> NativeResult {
|
||||
let (sy, sm, sd, st) = match plain_date_time_parts(vm, args) {
|
||||
|
||||
Reference in New Issue
Block a user