feat: implement Temporal duration and plain date differences

This commit is contained in:
爱特每羊队
2026-08-11 12:38:31 +08:00
parent 1a7cef45d9
commit d7a63a0d54
6 changed files with 664 additions and 68 deletions
+494 -46
View File
@@ -1,4 +1,4 @@
use chrono::{DateTime, Datelike, Days, Months, NaiveDate, SecondsFormat, Utc};
use chrono::{DateTime, Datelike, Days, NaiveDate, SecondsFormat, Utc};
use oxide_kernel::shape_forge::EMPTY_SHAPE_ID;
use oxide_runtime_api::{to_number, to_string, NativeResult, VmHost};
@@ -49,6 +49,13 @@ fn ensure_plain_time<H: VmHost>(vm: &mut H, obj: &JsObject) -> Result<(), JsValu
Ok(())
}
fn ensure_duration<H: VmHost>(vm: &mut H, obj: &JsObject) -> Result<(), JsValue> {
if !obj.is_duration_obj() {
return Err(crate::error::create_type_error(vm, "called on incompatible receiver"));
}
Ok(())
}
fn receiver_obj<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<*mut JsObject, JsValue> {
let raw = vm.reg(if args.is_empty() { 0 } else { args[0] });
if !raw.is_object() {
@@ -123,11 +130,7 @@ pub fn instant_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
"Class constructor Temporal.Instant cannot be invoked without 'new'",
));
}
let epoch_ns = if args.len() < 2 {
0.0
} else {
to_number(vm.reg(args[1]))
};
let epoch_ns = if args.len() < 2 { 0.0 } else { to_number(vm.reg(args[1])) };
make_instant(vm, epoch_ns as i128)
}
@@ -228,8 +231,7 @@ pub fn instant_value_of<H: VmHost>(vm: &mut H, _args: &[u8]) -> NativeResult {
}
fn make_plain_date<H: VmHost>(vm: &mut H, year: i32, month: u32, day: u32) -> NativeResult {
let proto =
JsValue::from_js_object(vm.session().builtin_world().plain_date_proto.as_ptr() as *mut JsObject);
let proto = JsValue::from_js_object(vm.session().builtin_world().plain_date_proto.as_ptr() as *mut JsObject);
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto);
obj.type_tag = JsObject::OBJ_TYPE_PLAIN_DATE;
obj.set_prop_at(0, JsValue::float(year as f64));
@@ -238,6 +240,326 @@ fn make_plain_date<H: VmHost>(vm: &mut H, year: i32, month: u32, day: u32) -> Na
NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj)))
}
fn make_duration<H: VmHost>(vm: &mut H, values: [f64; 10]) -> NativeResult {
let proto = JsValue::from_js_object(vm.session().builtin_world().duration_proto.as_ptr() as *mut JsObject);
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto);
obj.type_tag = JsObject::OBJ_TYPE_DURATION;
for (index, value) in values.into_iter().enumerate() {
obj.set_prop_at(index, JsValue::float(value));
}
NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj)))
}
fn parse_duration_string(input: &str) -> Option<[f64; 10]> {
let bytes = input.as_bytes();
let mut cursor = 0usize;
let negative = match bytes.first().copied() {
Some(b'-') => {
cursor += 1;
true
}
Some(b'+') => {
cursor += 1;
false
}
_ => false,
};
if bytes.get(cursor) != Some(&b'P') {
return None;
}
cursor += 1;
let mut values = [0.0; 10];
let mut in_time = false;
let mut saw_unit = false;
let mut saw_time_unit = false;
let mut last_order = 0usize;
let mut fraction_seen = false;
while cursor < bytes.len() {
if bytes[cursor] == b'T' {
if in_time {
return None;
}
in_time = true;
cursor += 1;
continue;
}
let number_start = cursor;
while bytes.get(cursor).is_some_and(u8::is_ascii_digit) {
cursor += 1;
}
if cursor == number_start {
return None;
}
let whole = input[number_start..cursor].parse::<f64>().ok()?;
if !whole.is_finite() {
return None;
}
let mut fraction = None;
if matches!(bytes.get(cursor), Some(b'.' | b',')) {
cursor += 1;
let fraction_start = cursor;
while bytes.get(cursor).is_some_and(u8::is_ascii_digit) {
cursor += 1;
}
let digits = cursor - fraction_start;
if digits == 0 || digits > 9 {
return None;
}
let numerator = input[fraction_start..cursor].parse::<u64>().ok()?;
fraction = Some((numerator, 10_u64.pow(digits as u32)));
}
let unit = *bytes.get(cursor)?;
cursor += 1;
let (index, order, unit_nanos) = match (in_time, unit) {
(false, b'Y') => (0, 1, None),
(false, b'M') => (1, 2, None),
(false, b'W') => (2, 3, None),
(false, b'D') => (3, 4, None),
(true, b'H') => (4, 5, Some(3_600_000_000_000_u64)),
(true, b'M') => (5, 6, Some(60_000_000_000_u64)),
(true, b'S') => (6, 7, Some(1_000_000_000_u64)),
_ => return None,
};
if order <= last_order || (fraction_seen && (whole != 0.0 || fraction.is_some_and(|f| f.0 != 0))) {
return None;
}
last_order = order;
values[index] = whole;
saw_unit = true;
saw_time_unit |= in_time;
if let Some((numerator, scale)) = fraction {
let unit_nanos = unit_nanos?;
let mut nanos = (numerator as u128 * unit_nanos as u128 / scale as u128) as u64;
if index <= 4 {
values[5] += (nanos / 60_000_000_000) as f64;
nanos %= 60_000_000_000;
}
if index <= 5 {
values[6] += (nanos / 1_000_000_000) as f64;
nanos %= 1_000_000_000;
}
values[7] += (nanos / 1_000_000) as f64;
nanos %= 1_000_000;
values[8] += (nanos / 1_000) as f64;
values[9] += (nanos % 1_000) as f64;
fraction_seen = true;
}
}
if !saw_unit || (in_time && !saw_time_unit) {
return None;
}
if negative {
for value in &mut values {
if *value != 0.0 {
*value = -*value;
}
}
}
Some(values)
}
fn duration_like_values<H: VmHost>(vm: &mut H, val: JsValue) -> Result<[f64; 10], JsValue> {
if val.is_string() {
return parse_duration_string(&to_string(val))
.ok_or_else(|| crate::error::create_range_error(vm, "invalid duration string"));
}
if !val.is_object() {
return Err(crate::error::create_type_error(vm, "cannot convert value to Duration"));
}
let ptr = val.as_js_object_ptr();
if ptr.is_null() {
return Err(crate::error::create_type_error(vm, "cannot convert value to Duration"));
}
let obj = unsafe { &*ptr };
let names = [
"years",
"months",
"weeks",
"days",
"hours",
"minutes",
"seconds",
"milliseconds",
"microseconds",
"nanoseconds",
];
let mut values = [0.0; 10];
for (index, name) in names.iter().enumerate() {
let value = if obj.is_duration_obj() {
get_double_prop(obj, index)
} else {
read_prop_number(vm, obj, val, name)
};
if value.is_nan() {
continue;
}
if !value.is_finite() || value.fract() != 0.0 {
return Err(crate::error::create_range_error(vm, "invalid duration"));
}
values[index] = value;
}
Ok(values)
}
/// `Temporal.Duration` 构造器,保存十个整数时长分量。
pub fn duration_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ctor_proto = vm.session().builtin_world().duration_proto.as_ptr();
if !is_ctor_call(vm, args, ctor_proto) {
return NativeResult::Err(crate::error::create_type_error(
vm,
"Class constructor Temporal.Duration cannot be invoked without 'new'",
));
}
let mut values = [0.0; 10];
for (index, value) in values.iter_mut().enumerate() {
if args.len() <= index + 1 {
continue;
}
let number = to_number(vm.reg(args[index + 1]));
if !number.is_finite() || number.fract() != 0.0 {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid duration"));
}
*value = number;
}
make_duration(vm, values)
}
/// `Temporal.Duration.from(value)`,复制 Duration 或读取同名字段。
pub fn duration_from<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let values = native_try!(duration_like_values(vm, val));
make_duration(vm, values)
}
fn duration_values(obj: &JsObject) -> [f64; 10] {
std::array::from_fn(|index| get_double_prop(obj, index))
}
fn format_duration_number(value: f64) -> String {
format!("{}", value as i64)
}
fn format_duration_seconds(seconds: f64, milliseconds: f64, microseconds: f64, nanoseconds: f64) -> Option<String> {
let subsecond = milliseconds * 1_000_000.0 + microseconds * 1_000.0 + nanoseconds;
if seconds == 0.0 && subsecond == 0.0 {
return None;
}
let negative = seconds < 0.0 || (seconds == 0.0 && subsecond < 0.0);
let whole = seconds.abs() as i64;
let fraction = subsecond.abs() as u64;
let mut result = format!("{}", whole);
if fraction != 0 {
let mut digits = format!("{fraction:09}");
while digits.ends_with('0') {
digits.pop();
}
result.push('.');
result.push_str(&digits);
}
if negative {
result.insert(0, '-');
}
Some(result)
}
/// `Temporal.Duration.prototype.toString()` 的 ISO 8601 表示。
pub fn duration_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_duration(vm, obj));
let mut values = duration_values(obj);
let negative = values.iter().any(|value| *value < 0.0);
for value in &mut values {
*value = value.abs();
}
let mut output = String::from(if negative { "-P" } else { "P" });
let date_units = [(0, "Y"), (1, "M"), (2, "W"), (3, "D")];
let mut has_date = false;
for (index, suffix) in date_units {
if values[index] != 0.0 {
has_date = true;
output.push_str(&format_duration_number(values[index]));
output.push_str(suffix);
}
}
let time_units = [(4, "H"), (5, "M")];
let has_time = values[4..].iter().any(|value| *value != 0.0);
if has_time || !has_date {
output.push('T');
for (index, suffix) in time_units {
if values[index] != 0.0 {
output.push_str(&format_duration_number(values[index]));
output.push_str(suffix);
}
}
if let Some(seconds) = format_duration_seconds(values[6], values[7], values[8], values[9]) {
output.push_str(&seconds);
output.push('S');
} else if output.ends_with('T') {
output.push_str("0S");
}
}
NativeResult::Ok(vm.new_string(&output))
}
/// `Temporal.Duration.prototype.valueOf()` 始终拒绝隐式数值转换。
pub fn duration_value_of<H: VmHost>(vm: &mut H, _args: &[u8]) -> NativeResult {
NativeResult::Err(crate::error::create_type_error(vm, "Temporal.Duration has no valueOf"))
}
/// `Temporal.Duration.prototype.abs()`,返回所有分量的绝对值副本。
pub fn duration_abs<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_duration(vm, obj));
let mut values = duration_values(obj);
for value in &mut values {
*value = value.abs();
}
make_duration(vm, values)
}
/// `Temporal.Duration.prototype.negated()`,返回非零分量取反的副本。
pub fn duration_negated<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_duration(vm, obj));
let mut values = duration_values(obj);
for value in &mut values {
if *value != 0.0 {
*value = -*value;
}
}
make_duration(vm, values)
}
macro_rules! duration_getter {
($name:ident, $index:expr) => {
pub fn $name<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_duration(vm, obj));
NativeResult::Ok(JsValue::float(get_double_prop(obj, $index)))
}
};
}
duration_getter!(duration_years, 0);
duration_getter!(duration_months, 1);
duration_getter!(duration_weeks, 2);
duration_getter!(duration_days, 3);
duration_getter!(duration_hours, 4);
duration_getter!(duration_minutes, 5);
duration_getter!(duration_seconds, 6);
duration_getter!(duration_milliseconds, 7);
duration_getter!(duration_microseconds, 8);
duration_getter!(duration_nanoseconds, 9);
/// 校验 ISO 日期分量(month 1-12、day 按月份与闰年)。
fn valid_iso_date(year: i32, month: u32, day: u32) -> bool {
NaiveDate::from_ymd_opt(year, month, day).is_some()
@@ -399,11 +721,7 @@ fn plain_date_ymd<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<(f64, f64, f64),
let ptr = receiver_obj(vm, args)?;
let obj = unsafe { &*ptr };
ensure_plain_date(vm, obj)?;
Ok((
get_double_prop(obj, 0),
get_double_prop(obj, 1),
get_double_prop(obj, 2),
))
Ok((get_double_prop(obj, 0), get_double_prop(obj, 1), get_double_prop(obj, 2)))
}
/// `Temporal.PlainDate.prototype.year` getter。
@@ -435,10 +753,16 @@ pub fn plain_date_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
NativeResult::Ok(vm.new_string(&format!("{year:04}-{month:02}-{day:02}")))
}
fn make_plain_time<H: VmHost>(vm: &mut H, hour: u32, minute: u32, second: u32, ms: u32, us: u32, ns: u32) -> NativeResult {
let total_ns = hour as f64 * 3.6e12 + minute as f64 * 6e10 + second as f64 * 1e9 + ms as f64 * 1e6 + us as f64 * 1e3 + ns as f64;
let proto =
JsValue::from_js_object(vm.session().builtin_world().plain_time_proto.as_ptr() as *mut JsObject);
fn make_plain_time<H: VmHost>(
vm: &mut H, hour: u32, minute: u32, second: u32, ms: u32, us: u32, ns: u32,
) -> NativeResult {
let total_ns = hour as f64 * 3.6e12
+ minute as f64 * 6e10
+ second as f64 * 1e9
+ ms as f64 * 1e6
+ us as f64 * 1e3
+ ns as f64;
let proto = JsValue::from_js_object(vm.session().builtin_world().plain_time_proto.as_ptr() as *mut JsObject);
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto);
obj.type_tag = JsObject::OBJ_TYPE_PLAIN_TIME;
obj.set_prop_at(0, JsValue::float(total_ns));
@@ -565,7 +889,11 @@ fn object_ymd<H: VmHost>(vm: &mut H, val: JsValue) -> Option<(i32, u32, u32)> {
if !val.is_object() {
return None;
}
let obj = unsafe { &*val.as_js_object_ptr() };
let ptr = val.as_js_object_ptr();
if ptr.is_null() {
return None;
}
let obj = unsafe { &*ptr };
if obj.is_plain_date_obj() {
return Some((
get_double_prop(obj, 0) as i32,
@@ -582,6 +910,77 @@ fn object_ymd<H: VmHost>(vm: &mut H, val: JsValue) -> Option<(i32, u32, u32)> {
Some((y.trunc() as i32, m.trunc() as u32, d.trunc() as u32))
}
fn date_like_ymd<H: VmHost>(vm: &mut H, val: JsValue) -> Result<(i32, u32, u32), JsValue> {
let ymd = if val.is_string() {
parse_iso_date(&to_string(val)).map_err(|_| crate::error::create_range_error(vm, "invalid ISO 8601 date"))?
} else {
object_ymd(vm, val).ok_or_else(|| crate::error::create_type_error(vm, "cannot convert to PlainDate"))?
};
if !valid_iso_date(ymd.0, ymd.1, ymd.2) {
return Err(crate::error::create_range_error(vm, "invalid ISO date"));
}
Ok(ymd)
}
fn read_prop_text<H: VmHost>(vm: &mut H, obj: &JsObject, receiver: JsValue, name: &str) -> Option<String> {
let key_val = vm.new_string(name);
let si = vm.property_key_si(key_val);
vm.ordinary_get(obj, si, receiver)
.ok()
.filter(|value| value.is_string())
.map(to_string)
}
fn largest_unit<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<String, JsValue> {
if args.len() <= 2 {
return Ok("days".to_string());
}
let options = vm.reg(args[2]);
if options.is_nullish() {
return Ok("days".to_string());
}
if !options.is_object() {
return Err(crate::error::create_type_error(vm, "options must be an object"));
}
let ptr = options.as_js_object_ptr();
if ptr.is_null() {
return Err(crate::error::create_type_error(vm, "options must be an object"));
}
let unit = read_prop_text(vm, unsafe { &*ptr }, options, "largestUnit").unwrap_or_else(|| "days".to_string());
let unit = unit.to_ascii_lowercase();
let unit = unit.strip_suffix('s').unwrap_or(&unit);
match unit {
"year" | "month" | "week" | "day" => Ok(unit.to_string()),
"auto" => Ok("day".to_string()),
_ => Err(crate::error::create_range_error(vm, "invalid largestUnit")),
}
}
fn date_difference(start: NaiveDate, end: NaiveDate, unit: &str) -> [f64; 10] {
let total_days = end.signed_duration_since(start).num_days();
if unit == "day" {
return [0.0, 0.0, 0.0, total_days as f64, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
}
if unit == "week" {
return [0.0, 0.0, (total_days / 7) as f64, (total_days % 7) as f64, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
}
let direction = if end >= start { 1_i64 } else { -1_i64 };
let mut total_months = (end.year() as i64 - start.year() as i64) * 12 + end.month0() as i64 - start.month0() as i64;
let mut candidate = add_signed_months(start, total_months).unwrap_or(start);
if (direction > 0 && candidate > end) || (direction < 0 && candidate < end) {
total_months -= direction;
candidate = add_signed_months(start, total_months).unwrap_or(start);
}
let remainder_days = end.signed_duration_since(candidate).num_days();
let (years, months) = if unit == "year" {
(total_months / 12, total_months % 12)
} else {
(0, total_months)
};
[years as f64, months as f64, 0.0, remainder_days as f64, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
}
/// `Temporal.PlainDate.prototype.dayOfWeek` getter:ISO 周几(周一 1 … 周日 7)。
pub fn plain_date_day_of_week<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let date = match plain_date_naive(vm, args) {
@@ -770,46 +1169,42 @@ pub fn plain_date_to_json<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
NativeResult::Ok(vm.new_string(&format!("{year:04}-{month:02}-{day:02}")))
}
/// 从 duration-like 值读日期单位字段(year/month/week/day);非对象或字段缺失视为 0。
fn duration_like_date_fields<H: VmHost>(vm: &mut H, val: JsValue) -> (f64, f64, f64, f64) {
if !val.is_object() {
return (0.0, 0.0, 0.0, 0.0);
}
let obj = unsafe { &*val.as_js_object_ptr() };
let y = read_prop_number(vm, obj, val, "years");
let m = read_prop_number(vm, obj, val, "months");
let w = read_prop_number(vm, obj, val, "weeks");
let d = read_prop_number(vm, obj, val, "days");
(
if y.is_nan() { 0.0 } else { y.trunc() },
if m.is_nan() { 0.0 } else { m.trunc() },
if w.is_nan() { 0.0 } else { w.trunc() },
if d.is_nan() { 0.0 } else { d.trunc() },
)
}
/// 按 duration-like 日期字段对日期做加减(Temporal 大单位运算,月份不足日时取月末)。
fn date_apply_duration<H: VmHost>(
vm: &mut H, args: &[u8], sign: i64,
) -> NativeResult {
fn date_apply_duration<H: VmHost>(vm: &mut H, args: &[u8], sign: i64) -> NativeResult {
let date = match plain_date_naive(vm, args) {
Ok(d) => d,
Err(e) => return NativeResult::Err(e),
};
let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let (y, m, w, d) = duration_like_date_fields(vm, val);
let values = match duration_like_values(vm, val) {
Ok(values) => values,
Err(error) => return NativeResult::Err(error),
};
let [y, m, w, d, h, min, s, ms, us, ns] = values;
let mut result = date;
let total_months = (y * 12.0 + m) * sign as f64;
if total_months != 0.0 {
if let Some(nd) = result.checked_add_months(Months::new(total_months as u32)) {
let total_months = ((y * 12.0 + m) * sign as f64) as i64;
if total_months != 0 {
if let Some(nd) = add_signed_months(result, total_months) {
result = nd;
} else {
return NativeResult::Err(crate::error::create_range_error(vm, "date out of range"));
}
}
let total_days = (w * 7.0 + d) * sign as f64;
if total_days != 0.0 {
if let Some(nd) = result.checked_add_days(Days::new(total_days as u64)) {
let time_days = (h / 24.0
+ min / 1_440.0
+ s / 86_400.0
+ ms / 86_400_000.0
+ us / 86_400_000_000.0
+ ns / 86_400_000_000_000.0)
.trunc();
let total_days = ((w * 7.0 + d + time_days) * sign as f64) as i64;
if total_days != 0 {
let next = if total_days >= 0 {
result.checked_add_days(Days::new(total_days as u64))
} else {
result.checked_sub_days(Days::new(total_days.unsigned_abs()))
};
if let Some(nd) = next {
result = nd;
} else {
return NativeResult::Err(crate::error::create_range_error(vm, "date out of range"));
@@ -827,3 +1222,56 @@ pub fn plain_date_add<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
pub fn plain_date_subtract<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
date_apply_duration(vm, args, -1)
}
fn add_signed_months(date: NaiveDate, months: i64) -> Option<NaiveDate> {
let total = date.year() as i64 * 12 + date.month0() as i64 + months;
let year = total.div_euclid(12);
let month = total.rem_euclid(12) as u32 + 1;
let year = i32::try_from(year).ok()?;
let day = date.day().min(days_in_month_iso(year, month));
NaiveDate::from_ymd_opt(year, month, day)
}
/// `Temporal.PlainDate.prototype.until(other)`,按默认天单位返回差值。
pub fn plain_date_until<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let start = match plain_date_naive(vm, args) {
Ok(date) => date,
Err(error) => return NativeResult::Err(error),
};
let other_value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let (year, month, day) = match date_like_ymd(vm, other_value) {
Ok(value) => value,
Err(error) => return NativeResult::Err(error),
};
let other = NaiveDate::from_ymd_opt(year, month, day).expect("date_like_ymd validates ISO date");
let unit = match largest_unit(vm, args) {
Ok(unit) => unit,
Err(error) => return NativeResult::Err(error),
};
make_duration(vm, date_difference(start, other, &unit))
}
/// `Temporal.PlainDate.prototype.since(other)`,按默认天单位返回差值。
pub fn plain_date_since<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let start = match plain_date_naive(vm, args) {
Ok(date) => date,
Err(error) => return NativeResult::Err(error),
};
let other_value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let (year, month, day) = match date_like_ymd(vm, other_value) {
Ok(value) => value,
Err(error) => return NativeResult::Err(error),
};
let other = NaiveDate::from_ymd_opt(year, month, day).expect("date_like_ymd validates ISO date");
let unit = match largest_unit(vm, args) {
Ok(unit) => unit,
Err(error) => return NativeResult::Err(error),
};
let mut values = date_difference(start, other, &unit);
for value in &mut values {
if *value != 0.0 {
*value = -*value;
}
}
make_duration(vm, values)
}
+30 -4
View File
@@ -251,6 +251,8 @@ pub struct BuiltinWorld {
pub plain_date_proto: P<JsObject>,
pub plain_time_constructor: P<JsObject>,
pub plain_time_proto: P<JsObject>,
pub duration_constructor: P<JsObject>,
pub duration_proto: P<JsObject>,
pub bigint_constructor: P<JsObject>,
pub bigint_proto: P<JsObject>,
pub stub_objects: Vec<P<JsObject>>,
@@ -504,10 +506,11 @@ fn wire_builtin_world_links(world: &BuiltinWorld) {
wire_ctor_proto(&world.instant_constructor, &world.instant_proto);
wire_ctor_proto(&world.plain_date_constructor, &world.plain_date_proto);
wire_ctor_proto(&world.plain_time_constructor, &world.plain_time_proto);
wire_ctor_proto(&world.duration_constructor, &world.duration_proto);
wire_ctor_proto(&world.bigint_constructor, &world.bigint_proto);
let obj_proto_val = JsValue::from_js_object(world.object_proto.as_ptr() as *mut JsObject);
let non_object_protos: [&P<JsObject>; 18] = [
let non_object_protos: [&P<JsObject>; 19] = [
&world.array_proto,
&world.function_proto,
&world.string_proto,
@@ -525,6 +528,7 @@ fn wire_builtin_world_links(world: &BuiltinWorld) {
&world.instant_proto,
&world.plain_date_proto,
&world.plain_time_proto,
&world.duration_proto,
&world.bigint_proto,
];
for proto in &non_object_protos {
@@ -660,6 +664,8 @@ impl BuiltinWorld {
BuiltinId::PlainDateProto => &self.plain_date_proto,
BuiltinId::PlainTimeConstructor => &self.plain_time_constructor,
BuiltinId::PlainTimeProto => &self.plain_time_proto,
BuiltinId::DurationConstructor => &self.duration_constructor,
BuiltinId::DurationProto => &self.duration_proto,
BuiltinId::BigIntConstructor => &self.bigint_constructor,
BuiltinId::BigIntProto => &self.bigint_proto,
}
@@ -710,6 +716,7 @@ impl BuiltinWorld {
make_named_pair(string_forge, shape_forge, labels, "PlainDate");
let (plain_time_proto, plain_time_constructor) =
make_named_pair(string_forge, shape_forge, labels, "PlainTime");
let (duration_proto, duration_constructor) = make_named_pair(string_forge, shape_forge, labels, "Duration");
let (bigint_proto, bigint_constructor) = make_named_pair(string_forge, shape_forge, labels, "BigInt");
let stub_objects = Vec::new();
@@ -791,6 +798,8 @@ impl BuiltinWorld {
plain_date_proto,
plain_time_constructor,
plain_time_proto,
duration_constructor,
duration_proto,
bigint_constructor,
bigint_proto,
stub_objects,
@@ -967,13 +976,24 @@ impl BuiltinWorld {
biguint64array_proto: current.biguint64array_proto.clone(),
}
};
let (temporal_object, temporal_now_object, instant_proto, instant_constructor, plain_date_proto, plain_date_constructor, plain_time_proto, plain_time_constructor) = if dirty.temporal {
let (instant_proto, instant_constructor) =
make_named_pair(string_forge, shape_forge, labels, "Instant");
let (
temporal_object,
temporal_now_object,
instant_proto,
instant_constructor,
plain_date_proto,
plain_date_constructor,
plain_time_proto,
plain_time_constructor,
duration_proto,
duration_constructor,
) = if dirty.temporal {
let (instant_proto, instant_constructor) = make_named_pair(string_forge, shape_forge, labels, "Instant");
let (plain_date_proto, plain_date_constructor) =
make_named_pair(string_forge, shape_forge, labels, "PlainDate");
let (plain_time_proto, plain_time_constructor) =
make_named_pair(string_forge, shape_forge, labels, "PlainTime");
let (duration_proto, duration_constructor) = make_named_pair(string_forge, shape_forge, labels, "Duration");
(
P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())),
P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())),
@@ -983,6 +1003,8 @@ impl BuiltinWorld {
plain_date_constructor,
plain_time_proto,
plain_time_constructor,
duration_proto,
duration_constructor,
)
} else {
(
@@ -994,6 +1016,8 @@ impl BuiltinWorld {
current.plain_date_constructor.clone(),
current.plain_time_proto.clone(),
current.plain_time_constructor.clone(),
current.duration_proto.clone(),
current.duration_constructor.clone(),
)
};
let (bigint_proto, bigint_constructor) = if dirty.stubs {
@@ -1081,6 +1105,8 @@ impl BuiltinWorld {
plain_date_proto,
plain_time_constructor,
plain_time_proto,
duration_constructor,
duration_proto,
bigint_constructor,
bigint_proto,
stub_objects,
+8 -2
View File
@@ -225,7 +225,7 @@ pub struct KernelSession {
/// 维护注意:每个新增的 `BuiltinWorld` 对象字段都必须加到这里以及
/// `KernelSession::dirty_since_snapshot()`,以便选择性重置重建正确的
/// builtin 家族。
pub const NUM_BUILTINS: usize = 78;
pub const NUM_BUILTINS: usize = 80;
/// 内置对象枚举 id,与 `BuiltinWorld` 中的存储槽一一对应。
///
@@ -312,6 +312,8 @@ pub enum BuiltinId {
PlainTimeProto = 75,
BigIntConstructor = 76,
BigIntProto = 77,
DurationConstructor = 78,
DurationProto = 79,
}
impl BuiltinId {
@@ -395,6 +397,8 @@ impl BuiltinId {
BuiltinId::PlainTimeProto,
BuiltinId::BigIntConstructor,
BuiltinId::BigIntProto,
BuiltinId::DurationConstructor,
BuiltinId::DurationProto,
];
}
@@ -627,7 +631,9 @@ impl KernelSession {
|| gen(BuiltinId::PlainDateConstructor) != snap(BuiltinId::PlainDateConstructor)
|| gen(BuiltinId::PlainDateProto) != snap(BuiltinId::PlainDateProto)
|| gen(BuiltinId::PlainTimeConstructor) != snap(BuiltinId::PlainTimeConstructor)
|| gen(BuiltinId::PlainTimeProto) != snap(BuiltinId::PlainTimeProto),
|| gen(BuiltinId::PlainTimeProto) != snap(BuiltinId::PlainTimeProto)
|| gen(BuiltinId::DurationConstructor) != snap(BuiltinId::DurationConstructor)
|| gen(BuiltinId::DurationProto) != snap(BuiltinId::DurationProto),
stubs: world.stub_objects.len() != snapshot.stub_objects_len
|| stub_generations_dirty
|| gen(BuiltinId::BigIntConstructor) != snap(BuiltinId::BigIntConstructor)
+9 -6
View File
@@ -443,6 +443,8 @@ impl JsObject {
pub const OBJ_TYPE_PLAIN_DATE: u8 = 15;
/// Temporal.PlainTime 对象:午夜后纳秒存于 prop 0。
pub const OBJ_TYPE_PLAIN_TIME: u8 = 16;
/// Temporal.Duration 对象:十个时长分量依次存于 prop 0-9。
pub const OBJ_TYPE_DURATION: u8 = 17;
/// `is_session_epoch` 字段中的 session 标记位。
pub const SESSION_EPOCH_BIT: u8 = 0x01;
/// `is_session_epoch` 字段中的 GC 标记位。
@@ -523,6 +525,11 @@ impl JsObject {
pub fn is_plain_time_obj(&self) -> bool {
self.type_tag == Self::OBJ_TYPE_PLAIN_TIME
}
/// 是否 Temporal.Duration 对象。
#[inline]
pub fn is_duration_obj(&self) -> bool {
self.type_tag == Self::OBJ_TYPE_DURATION
}
/// 构造无属性、可扩展的空对象(`new Object()` 的基础对象)。
pub fn new_empty(shape_id: ShapeId, proto: JsValue) -> Self {
@@ -897,13 +904,9 @@ impl JsObject {
if self.is_array() {
let count = self.array_prop_count as usize;
if pos < count {
return self
.array_elements_meta_vec()
.and_then(|vec| vec.get(pos).copied().flatten());
return self.array_elements_meta_vec().and_then(|vec| vec.get(pos).copied().flatten());
}
return self
.prop_meta_vec()
.and_then(|vec| vec.get(pos - count).copied().flatten());
return self.prop_meta_vec().and_then(|vec| vec.get(pos - count).copied().flatten());
}
self.prop_meta_vec().and_then(|vec| vec.get(pos).copied().flatten())
}
+66 -8
View File
@@ -5,7 +5,7 @@ use oxide_kernel::kernel::{KernelCore, KernelSession};
use oxide_types::object::JsObject;
use oxide_types::value::JsValue;
/// 把 Temporal 命名空间及其 Now/Instant/PlainDate/PlainTime 子对象绑定到 global。
/// 把 Temporal 命名空间及其子对象绑定到 global。
pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &mut JsObject) {
let world = session.builtin_world();
let temporal_ptr = world.temporal_object.as_ptr() as *mut JsObject;
@@ -20,11 +20,7 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
core,
&[
("instant", oxide_builtins::temporal::now_instant::<crate::vm::Vm> as *const (), 0),
(
"timeZoneId",
oxide_builtins::temporal::now_time_zone_id::<crate::vm::Vm> as *const (),
0,
),
("timeZoneId", oxide_builtins::temporal::now_time_zone_id::<crate::vm::Vm> as *const (), 0),
],
);
@@ -218,12 +214,18 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
plain_date_proto,
core,
&[
("toString", oxide_builtins::temporal::plain_date_to_string::<crate::vm::Vm> as *const (), 0),
(
"toString",
oxide_builtins::temporal::plain_date_to_string::<crate::vm::Vm> as *const (),
0,
),
("toJSON", oxide_builtins::temporal::plain_date_to_json::<crate::vm::Vm> as *const (), 0),
("valueOf", oxide_builtins::temporal::plain_date_value_of::<crate::vm::Vm> as *const (), 0),
("equals", oxide_builtins::temporal::plain_date_equals::<crate::vm::Vm> as *const (), 1),
("add", oxide_builtins::temporal::plain_date_add::<crate::vm::Vm> as *const (), 1),
("subtract", oxide_builtins::temporal::plain_date_subtract::<crate::vm::Vm> as *const (), 1),
("until", oxide_builtins::temporal::plain_date_until::<crate::vm::Vm> as *const (), 1),
("since", oxide_builtins::temporal::plain_date_since::<crate::vm::Vm> as *const (), 1),
],
);
@@ -283,7 +285,62 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
world,
plain_time_proto,
core,
&[("toString", oxide_builtins::temporal::plain_time_to_string::<crate::vm::Vm> as *const (), 0)],
&[(
"toString",
oxide_builtins::temporal::plain_time_to_string::<crate::vm::Vm> as *const (),
0,
)],
);
// Temporal.Duration:构造器、from、分量 getter 与 ISO 字符串转换。
let duration_ctor_ptr = world.duration_constructor.as_ptr() as *mut JsObject;
let duration_ctor = unsafe { &mut *duration_ctor_ptr };
configure_native_constructor(
duration_ctor,
oxide_builtins::temporal::duration_constructor::<crate::vm::Vm> as *const (),
10,
);
apply_binding_table(
world,
duration_ctor,
core,
&[("from", oxide_builtins::temporal::duration_from::<crate::vm::Vm> as *const (), 1)],
);
let duration_proto_ptr = world.duration_proto.as_ptr() as *mut JsObject;
let duration_proto = unsafe { &mut *duration_proto_ptr };
for (name, getter) in [
("years", oxide_builtins::temporal::duration_years::<crate::vm::Vm> as *const ()),
("months", oxide_builtins::temporal::duration_months::<crate::vm::Vm> as *const ()),
("weeks", oxide_builtins::temporal::duration_weeks::<crate::vm::Vm> as *const ()),
("days", oxide_builtins::temporal::duration_days::<crate::vm::Vm> as *const ()),
("hours", oxide_builtins::temporal::duration_hours::<crate::vm::Vm> as *const ()),
("minutes", oxide_builtins::temporal::duration_minutes::<crate::vm::Vm> as *const ()),
("seconds", oxide_builtins::temporal::duration_seconds::<crate::vm::Vm> as *const ()),
(
"milliseconds",
oxide_builtins::temporal::duration_milliseconds::<crate::vm::Vm> as *const (),
),
(
"microseconds",
oxide_builtins::temporal::duration_microseconds::<crate::vm::Vm> as *const (),
),
(
"nanoseconds",
oxide_builtins::temporal::duration_nanoseconds::<crate::vm::Vm> as *const (),
),
] {
bind_accessor_getter(core, session, duration_proto, name, getter);
}
apply_binding_table(
world,
duration_proto,
core,
&[
("abs", oxide_builtins::temporal::duration_abs::<crate::vm::Vm> as *const (), 0),
("negated", oxide_builtins::temporal::duration_negated::<crate::vm::Vm> as *const (), 0),
("toString", oxide_builtins::temporal::duration_to_string::<crate::vm::Vm> as *const (), 0),
("valueOf", oxide_builtins::temporal::duration_value_of::<crate::vm::Vm> as *const (), 0),
],
);
// 把子对象挂到 Temporal 命名空间对象上,再把 Temporal 挂到 global。
@@ -291,5 +348,6 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
bind_global_value(core, temporal, "Instant", JsValue::from_js_object(instant_ctor_ptr));
bind_global_value(core, temporal, "PlainDate", JsValue::from_js_object(plain_date_ctor_ptr));
bind_global_value(core, temporal, "PlainTime", JsValue::from_js_object(plain_time_ctor_ptr));
bind_global_value(core, temporal, "Duration", JsValue::from_js_object(duration_ctor_ptr));
bind_global_value(core, global, "Temporal", JsValue::from_js_object(temporal_ptr));
}
@@ -112,8 +112,11 @@ fn instant_value_of_throws() {
#[test]
fn instant_method_on_non_instant_throws() {
let mut vm = Vm::new();
let r = eval(&mut vm, "try { Temporal.Instant.prototype.toString.call({}) } catch (e) { e.constructor.name }")
.unwrap();
let r = eval(
&mut vm,
"try { Temporal.Instant.prototype.toString.call({}) } catch (e) { e.constructor.name }",
)
.unwrap();
assert_eq!(str_val(&vm, r), "TypeError");
}
@@ -188,6 +191,58 @@ fn plain_date_method_on_non_date_throws() {
assert_eq!(str_val(&vm, r), "TypeError");
}
#[test]
fn plain_date_add_and_subtract_support_negative_offsets() {
let mut vm = Vm::new();
let r = eval(&mut vm, "new Temporal.PlainDate(2024, 1, 31).add({months:1}).toString()").unwrap();
assert_eq!(str_val(&vm, r), "2024-02-29");
let r = eval(&mut vm, "new Temporal.PlainDate(2024, 1, 5).subtract({days:10}).toString()").unwrap();
assert_eq!(str_val(&vm, r), "2023-12-26");
}
#[test]
fn plain_date_until_and_since_return_duration() {
let mut vm = Vm::new();
let r = eval(&mut vm, "new Temporal.PlainDate(2024, 1, 1).until('2024-01-06').toString()").unwrap();
assert_eq!(str_val(&vm, r), "P5D");
assert_eq!(
num(
&mut vm,
"new Temporal.PlainDate(2024, 1, 6).since(new Temporal.PlainDate(2024, 1, 11)).days",
),
-5.0
);
assert_eq!(
num(
&mut vm,
"new Temporal.PlainDate(1997, 7, 16).since(new Temporal.PlainDate(2021, 7, 15), {largestUnit:'years'}).days",
),
-29.0
);
}
#[test]
fn duration_constructor_exposes_components() {
let mut vm = Vm::new();
let r = eval(&mut vm, "new Temporal.Duration(1,2,3,4,5,6,7,8,9,10).toString()").unwrap();
assert_eq!(str_val(&vm, r), "P1Y2M3W4DT5H6M7.00800901S");
assert_eq!(num(&mut vm, "new Temporal.Duration(1,2,3,4,5,6,7,8,9,10).nanoseconds"), 10.0);
let r = eval(&mut vm, "Temporal.Duration.from('-PT24.567890123H').toString()").unwrap();
assert_eq!(str_val(&vm, r), "-PT24H34M4.4044428S");
assert_eq!(num(&mut vm, "Temporal.Duration.from('P43Y').years"), 43.0);
let r = eval(&mut vm, "new Temporal.Duration(1,2,3,4).negated().abs().toString()").unwrap();
assert_eq!(str_val(&vm, r), "P1Y2M3W4D");
}
#[test]
fn plain_date_balances_duration_time_units_into_days() {
let mut vm = Vm::new();
let r = eval(&mut vm, "new Temporal.PlainDate(2000,5,2).add('P1DT24H1440M86400S').toString()").unwrap();
assert_eq!(str_val(&vm, r), "2000-05-06");
let r = eval(&mut vm, "new Temporal.PlainDate(2000,5,2).add('-PT24.567890123H').toString()").unwrap();
assert_eq!(str_val(&vm, r), "2000-05-01");
}
// -- Temporal.PlainTime --
#[test]