mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-27 04:01:11 +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;
|
use oxide_types::value::JsValue;
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
|
mod difference;
|
||||||
|
|
||||||
use common::*;
|
use common::*;
|
||||||
|
use difference::*;
|
||||||
|
|
||||||
fn instant_string_without_annotations(input: &str) -> Option<&str> {
|
fn instant_string_without_annotations(input: &str) -> Option<&str> {
|
||||||
let Some(first_annotation) = input.find('[') else {
|
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 {
|
fn instant_difference<H: VmHost>(vm: &mut H, args: &[u8], since: bool) -> NativeResult {
|
||||||
let ptr = native_try!(receiver_obj(vm, args));
|
let ptr = native_try!(receiver_obj(vm, args));
|
||||||
let obj = unsafe { &*ptr };
|
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))
|
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 返回反向。
|
/// 计算差值并舍入。until 返回 other 减 receiver,since 返回反向。
|
||||||
fn plain_date_time_difference<H: VmHost>(vm: &mut H, args: &[u8], since: bool) -> NativeResult {
|
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) {
|
let (sy, sm, sd, st) = match plain_date_time_parts(vm, args) {
|
||||||
|
|||||||
Reference in New Issue
Block a user