feat: Temporal Instant 差值实现

This commit is contained in:
2026-08-11 18:22:21 +08:00
parent 4e2d00fd0f
commit 59da42fea9
3 changed files with 199 additions and 0 deletions
+160
View File
@@ -791,6 +791,166 @@ pub fn instant_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
make_instant(vm, rounded_ns)
}
fn instant_difference_unit(value: &str) -> Option<(usize, i128, i128)> {
match value {
"hour" | "hours" => Some((0, 3_600_000_000_000, 24)),
"minute" | "minutes" => Some((1, 60_000_000_000, 60)),
"second" | "seconds" => Some((2, 1_000_000_000, 60)),
"millisecond" | "milliseconds" => Some((3, 1_000_000, 1_000)),
"microsecond" | "microseconds" => Some((4, 1_000, 1_000)),
"nanosecond" | "nanoseconds" => Some((5, 1, 1_000)),
_ => None,
}
}
/// 按普通有符号数语义舍入 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 };
native_try!(ensure_instant(vm, obj));
let Some(epoch_ns) = get_instant_epoch_ns(obj) else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid Instant"));
};
// other 必须先完成 Instant 转换,之后才读取 options。
let other = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) };
let other_ns = native_try!(instant_like_epoch_ns(vm, other));
let options_value = if args.len() < 3 { JsValue::undefined() } else { vm.reg(args[2]) };
let (largest_value, increment_value, mode_value, smallest_value) = if options_value.is_undefined() {
(None, 1.0, "trunc".to_string(), "nanosecond".to_string())
} else {
if !options_value.is_object() {
return NativeResult::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 NativeResult::Err(crate::error::create_type_error(vm, "options must be an object"));
}
let options = unsafe { &*options_ptr };
let largest_raw = native_try!(temporal_option_value(vm, options, options_value, "largestUnit"));
let largest = if largest_raw.is_undefined() {
None
} else {
Some(native_try!(temporal_option_string(vm, largest_raw)))
};
let increment_raw = native_try!(temporal_option_value(vm, options, options_value, "roundingIncrement"));
let increment = if increment_raw.is_undefined() {
1.0
} else {
native_try!(temporal_option_number(vm, increment_raw))
};
let mode_raw = native_try!(temporal_option_value(vm, options, options_value, "roundingMode"));
let mode = if mode_raw.is_undefined() {
"trunc".to_string()
} else {
native_try!(temporal_option_string(vm, mode_raw))
};
let smallest_raw = native_try!(temporal_option_value(vm, options, options_value, "smallestUnit"));
let smallest = if smallest_raw.is_undefined() {
"nanosecond".to_string()
} else {
native_try!(temporal_option_string(vm, smallest_raw))
};
(largest, increment, mode, smallest)
};
let Some((smallest_index, smallest_ns, increment_limit)) = instant_difference_unit(&smallest_value) else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallest unit"));
};
let largest_index = match largest_value {
Some(value) => match instant_difference_unit(&value) {
Some((index, _, _)) => index,
None => return NativeResult::Err(crate::error::create_range_error(vm, "invalid largest unit")),
},
None => smallest_index.min(2),
};
let Some(mode) = instant_rounding_mode(&mode_value) else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding mode"));
};
if !increment_value.is_finite() {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment"));
}
let increment = increment_value.trunc();
if !(1.0..=1_000_000_000.0).contains(&increment) {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment"));
}
let increment = increment as i128;
if increment >= increment_limit || increment_limit % increment != 0 {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment"));
}
if largest_index > smallest_index {
return NativeResult::Err(crate::error::create_range_error(vm, "smallest unit exceeds largest unit"));
}
let delta = if since { epoch_ns - other_ns } else { other_ns - epoch_ns };
let Some(quantum_ns) = smallest_ns.checked_mul(increment) else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment"));
};
let Some(rounded_ns) = round_instant_difference(delta, quantum_ns, mode) else {
return NativeResult::Err(crate::error::create_range_error(vm, "Instant difference is out of range"));
};
let Some(values) = balance_instant_difference(rounded_ns, largest_index) else {
return NativeResult::Err(crate::error::create_range_error(vm, "Instant difference is out of range"));
};
make_duration(vm, values)
}
/// `Temporal.Instant.prototype.until(other, options)`:返回从 receiver 到 other 的精确时长。
pub fn instant_until<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
instant_difference(vm, args, false)
}
/// `Temporal.Instant.prototype.since(other, options)`:返回从 other 到 receiver 的精确时长。
pub fn instant_since<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
instant_difference(vm, args, true)
}
/// `Temporal.Instant.prototype.toString()`:输出 ISO 8601 UTC(如 `2024-01-01T00:00:00Z`)。
pub fn instant_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
@@ -89,8 +89,10 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
("add", oxide_builtins::temporal::instant_add::<crate::vm::Vm> as *const (), 1),
("equals", oxide_builtins::temporal::instant_equals::<crate::vm::Vm> as *const (), 1),
("round", oxide_builtins::temporal::instant_round::<crate::vm::Vm> as *const (), 1),
("since", oxide_builtins::temporal::instant_since::<crate::vm::Vm> as *const (), 1),
("subtract", oxide_builtins::temporal::instant_subtract::<crate::vm::Vm> as *const (), 1),
("toString", oxide_builtins::temporal::instant_to_string::<crate::vm::Vm> as *const (), 0),
("until", oxide_builtins::temporal::instant_until::<crate::vm::Vm> as *const (), 1),
("valueOf", oxide_builtins::temporal::instant_value_of::<crate::vm::Vm> as *const (), 0),
],
);
@@ -258,6 +258,43 @@ fn instant_round_handles_nested_destructured_callback_captures() {
assert!(r.as_bool());
}
#[test]
fn instant_until_and_since_balance_exact_differences() {
let mut vm = Vm::new();
let r = eval(
&mut vm,
"let earlier = new Temporal.Instant(1000000000000000000n);
let later = new Temporal.Instant(1000090061987654321n);
let exact = earlier.until(later);
let hours = later.since(earlier, { largestUnit: 'hours' });
exact.seconds === 90061 && exact.milliseconds === 987
&& exact.microseconds === 654 && exact.nanoseconds === 321
&& hours.hours === 25 && hours.minutes === 1 && hours.seconds === 1
&& hours.milliseconds === 987 && hours.microseconds === 654 && hours.nanoseconds === 321",
)
.unwrap();
assert!(r.as_bool());
}
#[test]
fn instant_difference_rounds_signed_values_and_validates_units() {
let mut vm = Vm::new();
let r = eval(
&mut vm,
"let zero = new Temporal.Instant(0n), value = new Temporal.Instant(7199000000000n);
let positive = zero.until(value, { largestUnit: 'hours', smallestUnit: 'minutes', roundingMode: 'expand' });
let negative = zero.since(value, { largestUnit: 'hours', smallestUnit: 'minutes', roundingMode: 'expand' });
positive.hours === 2 && positive.minutes === 0
&& negative.hours === -2 && negative.minutes === 0
&& (() => { try { zero.until(value, { largestUnit: 'seconds', smallestUnit: 'hours' }); return false }
catch (e) { return e instanceof RangeError } })()
&& (() => { try { zero.until(value, { smallestUnit: 'minutes', roundingIncrement: 60 }); return false }
catch (e) { return e instanceof RangeError } })()",
)
.unwrap();
assert!(r.as_bool());
}
#[test]
fn instant_epoch_factories_validate_input_and_range() {
let mut vm = Vm::new();