feat: implement Temporal Instant round
This commit is contained in:
@@ -624,6 +624,173 @@ pub fn instant_subtract<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
|||||||
instant_add_duration(vm, args, -1)
|
instant_add_duration(vm, args, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum InstantRoundingMode {
|
||||||
|
Ceil,
|
||||||
|
Expand,
|
||||||
|
Floor,
|
||||||
|
HalfCeil,
|
||||||
|
HalfEven,
|
||||||
|
HalfExpand,
|
||||||
|
HalfFloor,
|
||||||
|
HalfTrunc,
|
||||||
|
Trunc,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn temporal_option_value<H: VmHost>(
|
||||||
|
vm: &mut H, obj: &JsObject, receiver: JsValue, name: &str,
|
||||||
|
) -> Result<JsValue, JsValue> {
|
||||||
|
let key = vm.new_string(name);
|
||||||
|
let si = vm.property_key_si(key);
|
||||||
|
vm.ordinary_get(obj, si, receiver)
|
||||||
|
.map_err(|error| native_engine_error(vm, &error))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn temporal_option_number<H: VmHost>(vm: &mut H, value: JsValue) -> Result<f64, JsValue> {
|
||||||
|
let primitive = oxide_runtime_api::to_primitive(value, oxide_runtime_api::ToPrimitiveHint::Number, vm)
|
||||||
|
.map_err(|error| native_engine_error(vm, &error))?;
|
||||||
|
if primitive.is_symbol() || primitive.is_bigint() {
|
||||||
|
return Err(crate::error::create_type_error(vm, "cannot convert option to number"));
|
||||||
|
}
|
||||||
|
Ok(to_number(primitive))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn temporal_option_string<H: VmHost>(vm: &mut H, value: JsValue) -> Result<String, JsValue> {
|
||||||
|
let primitive = oxide_runtime_api::to_primitive(value, oxide_runtime_api::ToPrimitiveHint::String, vm)
|
||||||
|
.map_err(|error| native_engine_error(vm, &error))?;
|
||||||
|
if primitive.is_symbol() {
|
||||||
|
return Err(crate::error::create_type_error(vm, "cannot convert option to string"));
|
||||||
|
}
|
||||||
|
Ok(to_string(primitive))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn instant_rounding_mode(value: &str) -> Option<InstantRoundingMode> {
|
||||||
|
match value {
|
||||||
|
"ceil" => Some(InstantRoundingMode::Ceil),
|
||||||
|
"expand" => Some(InstantRoundingMode::Expand),
|
||||||
|
"floor" => Some(InstantRoundingMode::Floor),
|
||||||
|
"halfCeil" => Some(InstantRoundingMode::HalfCeil),
|
||||||
|
"halfEven" => Some(InstantRoundingMode::HalfEven),
|
||||||
|
"halfExpand" => Some(InstantRoundingMode::HalfExpand),
|
||||||
|
"halfFloor" => Some(InstantRoundingMode::HalfFloor),
|
||||||
|
"halfTrunc" => Some(InstantRoundingMode::HalfTrunc),
|
||||||
|
"trunc" => Some(InstantRoundingMode::Trunc),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn instant_round_unit(value: &str) -> Option<(i128, i128)> {
|
||||||
|
match value {
|
||||||
|
"hour" | "hours" => Some((3_600_000_000_000, 24)),
|
||||||
|
"minute" | "minutes" => Some((60_000_000_000, 1_440)),
|
||||||
|
"second" | "seconds" => Some((1_000_000_000, 86_400)),
|
||||||
|
"millisecond" | "milliseconds" => Some((1_000_000, 86_400_000)),
|
||||||
|
"microsecond" | "microseconds" => Some((1_000, 86_400_000_000)),
|
||||||
|
"nanosecond" | "nanoseconds" => Some((1, 86_400_000_000_000)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 按 Temporal 的 `RoundNumberToIncrementAsIfPositive` 语义舍入纳秒。
|
||||||
|
fn round_instant_ns(value: i128, increment: i128, mode: InstantRoundingMode) -> Option<i128> {
|
||||||
|
let quotient = value.div_euclid(increment);
|
||||||
|
let remainder = value.rem_euclid(increment);
|
||||||
|
if remainder == 0 {
|
||||||
|
return Some(value);
|
||||||
|
}
|
||||||
|
let use_upper = match mode {
|
||||||
|
InstantRoundingMode::Ceil | InstantRoundingMode::Expand => true,
|
||||||
|
InstantRoundingMode::Floor | 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 | InstantRoundingMode::HalfExpand => true,
|
||||||
|
InstantRoundingMode::HalfFloor | InstantRoundingMode::HalfTrunc => false,
|
||||||
|
InstantRoundingMode::HalfEven => quotient.rem_euclid(2) != 0,
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
quotient
|
||||||
|
.checked_add(i128::from(use_upper))
|
||||||
|
.and_then(|rounded| rounded.checked_mul(increment))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Temporal.Instant.prototype.round(roundTo)`:按给定单位、增量和模式精确舍入。
|
||||||
|
pub fn instant_round<H: VmHost>(vm: &mut H, args: &[u8]) -> 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"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let round_to = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) };
|
||||||
|
let (increment_value, mode_value, unit_value) = if round_to.is_string() {
|
||||||
|
(1.0, "halfExpand".to_string(), Some(to_string(round_to)))
|
||||||
|
} else {
|
||||||
|
if !round_to.is_object() {
|
||||||
|
return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be a string or object"));
|
||||||
|
}
|
||||||
|
let options_ptr = round_to.as_js_object_ptr();
|
||||||
|
if options_ptr.is_null() {
|
||||||
|
return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be a string or object"));
|
||||||
|
}
|
||||||
|
let options = unsafe { &*options_ptr };
|
||||||
|
let increment_raw = native_try!(temporal_option_value(vm, options, round_to, "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, round_to, "roundingMode"));
|
||||||
|
let mode = if mode_raw.is_undefined() {
|
||||||
|
"halfExpand".to_string()
|
||||||
|
} else {
|
||||||
|
native_try!(temporal_option_string(vm, mode_raw))
|
||||||
|
};
|
||||||
|
let unit_raw = native_try!(temporal_option_value(vm, options, round_to, "smallestUnit"));
|
||||||
|
let unit = if unit_raw.is_undefined() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(native_try!(temporal_option_string(vm, unit_raw)))
|
||||||
|
};
|
||||||
|
(increment, mode, unit)
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(mode) = instant_rounding_mode(&mode_value) else {
|
||||||
|
return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding mode"));
|
||||||
|
};
|
||||||
|
let Some(unit_value) = unit_value else {
|
||||||
|
return NativeResult::Err(crate::error::create_range_error(vm, "smallestUnit is required"));
|
||||||
|
};
|
||||||
|
let Some((unit_ns, units_per_day)) = instant_round_unit(&unit_value) else {
|
||||||
|
return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallest unit"));
|
||||||
|
};
|
||||||
|
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 units_per_day % increment != 0 {
|
||||||
|
return NativeResult::Err(crate::error::create_range_error(vm, "rounding increment must divide a day"));
|
||||||
|
}
|
||||||
|
let Some(quantum_ns) = unit_ns.checked_mul(increment) else {
|
||||||
|
return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment"));
|
||||||
|
};
|
||||||
|
let Some(rounded_ns) = round_instant_ns(epoch_ns, quantum_ns, mode) else {
|
||||||
|
return NativeResult::Err(crate::error::create_range_error(vm, "Instant outside supported range"));
|
||||||
|
};
|
||||||
|
if rounded_ns.unsigned_abs() > MAX_INSTANT_NS as u128 {
|
||||||
|
return NativeResult::Err(crate::error::create_range_error(vm, "Instant outside supported range"));
|
||||||
|
}
|
||||||
|
make_instant(vm, rounded_ns)
|
||||||
|
}
|
||||||
|
|
||||||
/// `Temporal.Instant.prototype.toString()`:输出 ISO 8601 UTC(如 `2024-01-01T00:00:00Z`)。
|
/// `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 {
|
pub fn instant_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||||
let ptr = native_try!(receiver_obj(vm, args));
|
let ptr = native_try!(receiver_obj(vm, args));
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
|
|||||||
&[
|
&[
|
||||||
("add", oxide_builtins::temporal::instant_add::<crate::vm::Vm> as *const (), 1),
|
("add", oxide_builtins::temporal::instant_add::<crate::vm::Vm> as *const (), 1),
|
||||||
("equals", oxide_builtins::temporal::instant_equals::<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),
|
||||||
("subtract", oxide_builtins::temporal::instant_subtract::<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),
|
("toString", oxide_builtins::temporal::instant_to_string::<crate::vm::Vm> as *const (), 0),
|
||||||
("valueOf", oxide_builtins::temporal::instant_value_of::<crate::vm::Vm> as *const (), 0),
|
("valueOf", oxide_builtins::temporal::instant_value_of::<crate::vm::Vm> as *const (), 0),
|
||||||
|
|||||||
@@ -180,6 +180,84 @@ fn instant_arithmetic_rejects_date_units_and_out_of_range_results() {
|
|||||||
assert!(r.as_bool());
|
assert!(r.as_bool());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn instant_round_supports_all_rounding_directions() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let r = eval(
|
||||||
|
&mut vm,
|
||||||
|
"let i = new Temporal.Instant(-1500n);
|
||||||
|
i.round({smallestUnit:'microsecond', roundingMode:'floor'}).epochNanoseconds === -2000n
|
||||||
|
&& i.round({smallestUnit:'microsecond', roundingMode:'ceil'}).epochNanoseconds === -1000n
|
||||||
|
&& i.round({smallestUnit:'microsecond', roundingMode:'halfFloor'}).epochNanoseconds === -2000n
|
||||||
|
&& i.round({smallestUnit:'microsecond', roundingMode:'halfCeil'}).epochNanoseconds === -1000n
|
||||||
|
&& i.round({smallestUnit:'microsecond', roundingMode:'halfEven'}).epochNanoseconds === -2000n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(r.as_bool());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn instant_round_validates_increment_and_accepts_shorthand() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let r = eval(
|
||||||
|
&mut vm,
|
||||||
|
"let i = new Temporal.Instant(123456789n);
|
||||||
|
i.round('microseconds').epochNanoseconds === 123457000n
|
||||||
|
&& i.round({smallestUnit:'nanosecond', roundingIncrement:2.5, roundingMode:'expand'}).epochNanoseconds === 123456790n
|
||||||
|
&& (() => { try { i.round({smallestUnit:'hour', roundingIncrement:7}); return false } catch (e) { return e instanceof RangeError } })()",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(r.as_bool());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn instant_round_accepts_plural_units_inside_callbacks() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let r = eval(
|
||||||
|
&mut vm,
|
||||||
|
"let i = new Temporal.Instant(1000000000123456789n), ok = true;
|
||||||
|
['hour','minute','second','millisecond','microsecond','nanosecond'].forEach(unit => {
|
||||||
|
ok = ok && i.round({smallestUnit:unit}).equals(i.round({smallestUnit:`${unit}s`}));
|
||||||
|
});
|
||||||
|
ok",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(r.as_bool());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn instant_round_handles_shorthand_callback_parameters() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let r = eval(
|
||||||
|
&mut vm,
|
||||||
|
"let i = new Temporal.Instant(1n), shorthand = true;
|
||||||
|
['hour'].forEach(smallestUnit => {
|
||||||
|
shorthand = i.round({smallestUnit}).equals(i.round(smallestUnit));
|
||||||
|
});
|
||||||
|
shorthand",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(r.as_bool());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn instant_round_handles_nested_destructured_callback_captures() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let r = eval(
|
||||||
|
&mut vm,
|
||||||
|
"let i = new Temporal.Instant(1n);
|
||||||
|
let pairs = [['hour', [1, 2]]], captured = true;
|
||||||
|
pairs.forEach(([unit, increments]) => {
|
||||||
|
increments.forEach(increment => {
|
||||||
|
captured = captured && i.round({smallestUnit: unit, roundingIncrement: increment}) instanceof Temporal.Instant;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
captured",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(r.as_bool());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn instant_epoch_factories_validate_input_and_range() {
|
fn instant_epoch_factories_validate_input_and_range() {
|
||||||
let mut vm = Vm::new();
|
let mut vm = Vm::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user