feat: implement Temporal Instant differences
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user