feat: implement Temporal duration and plain date differences
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user