mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-24 10:41:11 +08:00
feat: Temporal.Duration 补全——compare/subtract/equals/sign/blank/toJSON/toLocaleString + relativeTo 相对日历对齐
This commit is contained in:
@@ -3477,6 +3477,94 @@ fn duration_values(obj: &JsObject) -> [f64; 10] {
|
||||
std::array::from_fn(|index| get_double_prop(obj, index))
|
||||
}
|
||||
|
||||
/// 归一 options.relativeTo 为相对日期 `(y, m, d)`。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. string → parse_plain_date_time_string,失败回退 parse_plain_date_string(PDT 优先、PD 回退);失败 RangeError。
|
||||
/// 2. PlainDateTime / PlainDate 对象 → 直读 prop 0/1/2。
|
||||
/// 3. ZonedDateTime → zoned_date_time_plain_parts 取本地 (y,m,d)。
|
||||
/// 4. Instant → epoch 纳秒按 UTC 分解(div_euclid(DAY_NS) → civil_from_days)。
|
||||
/// 5. 其他对象按 property bag 解析(ToTemporalDateTime,constrain)。
|
||||
/// 6. undefined → None;其余原始值 → TypeError。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 调用方须已把 raw 从 options 取出(读序由调用方保证)。
|
||||
/// - 本函数只取日期分量;时间分量由调用方的 duration 分量另行加。
|
||||
fn duration_relative_to_date<H: VmHost>(
|
||||
vm: &mut H, relative_raw: JsValue,
|
||||
) -> Result<Option<(i128, i128, i128)>, JsValue> {
|
||||
if relative_raw.is_undefined() {
|
||||
return Ok(None);
|
||||
}
|
||||
if relative_raw.is_string() {
|
||||
let text = to_string(relative_raw);
|
||||
// 先按 PlainDateTime/PlainDate 解析(纯日期、日期时间、带 offset/注解的 plain 串)。
|
||||
if let Ok((year, month, day, _time_ns)) = parse_plain_date_time_string(&text) {
|
||||
return Ok(Some((i128::from(year), i128::from(month), i128::from(day))));
|
||||
}
|
||||
// ZonedDateTime-like(含 Z 或时区注解):按 instant 解析 + 注解时区反推墙钟日期。
|
||||
let (epoch_ns, time_zone_id, _calendar) = zoned_date_time_string_parts(vm, &text, "reject")?;
|
||||
let offset_minutes = instant_time_zone_offset(&time_zone_id).unwrap_or(0);
|
||||
let wall_ns = epoch_ns + i128::from(offset_minutes) * 60_000_000_000;
|
||||
let (year, month, day) = civil_from_days(wall_ns.div_euclid(DAY_NS));
|
||||
return Ok(Some((year, month, day)));
|
||||
}
|
||||
if !relative_raw.is_object() {
|
||||
return Err(crate::error::create_type_error(vm, "invalid relativeTo"));
|
||||
}
|
||||
let ptr = relative_raw.as_js_object_ptr();
|
||||
if ptr.is_null() {
|
||||
return Err(crate::error::create_type_error(vm, "invalid relativeTo"));
|
||||
}
|
||||
let obj = unsafe { &*ptr };
|
||||
if obj.is_plain_date_time_obj() || obj.is_plain_date_obj() {
|
||||
return Ok(Some((
|
||||
i128::from(get_double_prop(obj, 0) as i32),
|
||||
i128::from(get_double_prop(obj, 1) as u32),
|
||||
i128::from(get_double_prop(obj, 2) as u32),
|
||||
)));
|
||||
}
|
||||
if obj.is_zoned_date_time_obj() {
|
||||
let (year, month, day, _time_ns) = zoned_date_time_plain_parts(vm, obj)?;
|
||||
return Ok(Some((i128::from(year), i128::from(month), i128::from(day))));
|
||||
}
|
||||
if obj.is_instant_obj() {
|
||||
let Some(epoch_ns) = get_instant_epoch_ns(obj) else {
|
||||
return Err(crate::error::create_range_error(vm, "invalid Instant"));
|
||||
};
|
||||
let (year, month, day) = civil_from_days(epoch_ns.div_euclid(DAY_NS));
|
||||
return Ok(Some((year, month, day)));
|
||||
}
|
||||
// property bag:按 ToRelativeTemporalObject 依次校验 calendar/timeZone/offset 再读字段。
|
||||
let calendar_raw = temporal_option_value(vm, obj, relative_raw, "calendar")?;
|
||||
if !calendar_raw.is_undefined() {
|
||||
return Err(crate::error::create_type_error(vm, "invalid relativeTo"));
|
||||
}
|
||||
let time_zone_raw = temporal_option_value(vm, obj, relative_raw, "timeZone")?;
|
||||
if !time_zone_raw.is_undefined() {
|
||||
// 非字符串(对象/符号/null/数字)→ TypeError;字符串非合法时区 → RangeError。
|
||||
if !time_zone_raw.is_string() {
|
||||
return Err(crate::error::create_type_error(vm, "invalid time zone"));
|
||||
}
|
||||
if canonical_time_zone(&to_string(time_zone_raw)).is_none() {
|
||||
return Err(crate::error::create_range_error(vm, "invalid time zone"));
|
||||
}
|
||||
}
|
||||
let offset_raw = temporal_option_value(vm, obj, relative_raw, "offset")?;
|
||||
if !offset_raw.is_undefined() {
|
||||
// 非字符串 → TypeError;字符串格式非法(含亚秒偏移)→ RangeError。
|
||||
if !offset_raw.is_string() {
|
||||
return Err(crate::error::create_type_error(vm, "invalid offset"));
|
||||
}
|
||||
let offset_input = to_string(offset_raw);
|
||||
if parse_any_offset_minutes(&offset_input).is_none() || !valid_offset_fraction(&offset_input) {
|
||||
return Err(crate::error::create_range_error(vm, "invalid offset"));
|
||||
}
|
||||
}
|
||||
let (year, month, day, _time_ns, _calendar) = plain_date_time_object_parts(vm, relative_raw, obj, true, false)?;
|
||||
Ok(Some((i128::from(year), i128::from(month), i128::from(day))))
|
||||
}
|
||||
|
||||
fn format_duration_number(value: f64) -> String {
|
||||
format!("{}", value as i64)
|
||||
}
|
||||
@@ -3568,40 +3656,10 @@ pub fn duration_total<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
// 含日历单位(year/month/week)时,days 及以上的 total 必须走 relativeTo 日历路径。
|
||||
let has_calendar_units = values[0] != 0.0 || values[1] != 0.0 || values[2] != 0.0;
|
||||
|
||||
// relativeTo:支持 PlainDateTime / PlainDate,取日期分量(时间按午夜计算,对齐 polyfill)。
|
||||
// relativeTo: supports PlainDateTime / PlainDate objects; strings follow the
|
||||
// ToRelativeTemporalObject path (PlainDateTime first, falling back to PlainDate),
|
||||
// invalid/out-of-range strings throw RangeError, and non-string primitives
|
||||
// (number/boolean/bigint/symbol/null) throw TypeError per test262.
|
||||
let relative_date = if relative_raw.is_string() {
|
||||
let text = to_string(relative_raw);
|
||||
match parse_plain_date_time_string(&text)
|
||||
.or_else(|_| parse_plain_date_string(&text).map(|(y, m, d)| (y, m, d, 0.0)))
|
||||
{
|
||||
Ok((year, month, day, _time_ns)) => Some((i128::from(year), i128::from(month), i128::from(day))),
|
||||
Err(_) => {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid relativeTo string"));
|
||||
}
|
||||
}
|
||||
} else if relative_raw.is_object() {
|
||||
let rel_ptr = relative_raw.as_js_object_ptr();
|
||||
if rel_ptr.is_null() {
|
||||
return NativeResult::Err(crate::error::create_type_error(vm, "invalid relativeTo"));
|
||||
}
|
||||
let rel = unsafe { &*rel_ptr };
|
||||
if rel.is_plain_date_time_obj() || rel.is_plain_date_obj() {
|
||||
Some((
|
||||
i128::from(get_double_prop(rel, 0) as i32),
|
||||
i128::from(get_double_prop(rel, 1) as u32),
|
||||
i128::from(get_double_prop(rel, 2) as u32),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if relative_raw.is_undefined() {
|
||||
None
|
||||
} else {
|
||||
return NativeResult::Err(crate::error::create_type_error(vm, "invalid relativeTo"));
|
||||
// relativeTo:统一经 duration_relative_to_date 归一(支持 string/PD/PDT/ZDT/Instant/bag)。
|
||||
let relative_date = match duration_relative_to_date(vm, relative_raw) {
|
||||
Ok(relative) => relative,
|
||||
Err(error) => return NativeResult::Err(error),
|
||||
};
|
||||
|
||||
const UNIT_NS: [i128; 6] = [3_600_000_000_000, 60_000_000_000, 1_000_000_000, 1_000_000, 1_000, 1];
|
||||
@@ -3776,8 +3834,26 @@ pub fn duration_add<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let other_val = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) };
|
||||
let other = native_try!(duration_like_values(vm, other_val));
|
||||
let receiver = duration_values(obj);
|
||||
let values = native_try!(add_duration_values(vm, &receiver, &other));
|
||||
make_duration(vm, values)
|
||||
}
|
||||
|
||||
/// 两个纯时间 duration(无 year/month/week)按分量精确求和并按最大非零单位平衡。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. 最大单位取两侧最大的非零时间单位(days=3 最大,ns=9 最小)。
|
||||
/// 2. 分量在 i128 上按纳秒精确求和(f64 分量是精确整数,数学值不受 2^53 截断)。
|
||||
/// 3. 从总纳秒向下按同号截断分解,进位到最大单位为止。
|
||||
/// 4. 逐分量按纳秒刻度校验 2^53 秒上限。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 任一侧含日历单位抛 RangeError(本批无 relativeTo 支持)。
|
||||
/// - 全零与 nanosecond 分量和为 0 时直接返回零时长。
|
||||
fn add_duration_values<H: VmHost>(
|
||||
vm: &mut H, receiver: &[f64; 10], other: &[f64; 10],
|
||||
) -> Result<[f64; 10], JsValue> {
|
||||
if receiver[..3].iter().any(|value| *value != 0.0) || other[..3].iter().any(|value| *value != 0.0) {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "cannot add durations with calendar units"));
|
||||
return Err(crate::error::create_range_error(vm, "cannot add durations with calendar units"));
|
||||
}
|
||||
// 最大单位取 receiver 与参数中最大的非零时间单位(days=3 最大,ns=9 最小)。
|
||||
let mut largest = 9_usize;
|
||||
@@ -3788,7 +3864,7 @@ pub fn duration_add<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
}
|
||||
}
|
||||
if largest == 9 && receiver[9] + other[9] == 0.0 {
|
||||
return make_duration(vm, [0.0; 10]);
|
||||
return Ok([0.0; 10]);
|
||||
}
|
||||
// 分量按精确整数求和(f64 分量是精确整数;超过 2^53 的和需在 i128 上保持精确,规范按数学值计算)。
|
||||
let mut total_ns = 0_i128;
|
||||
@@ -3796,16 +3872,16 @@ pub fn duration_add<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
[86_400_000_000_000, 3_600_000_000_000, 60_000_000_000, 1_000_000_000, 1_000_000, 1_000, 1];
|
||||
for (index, scale) in (3..10).zip(SUM_SCALES) {
|
||||
let Some(a) = duration_component_integer(receiver[index]) else {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid duration"));
|
||||
return Err(crate::error::create_range_error(vm, "invalid duration"));
|
||||
};
|
||||
let Some(b) = duration_component_integer(other[index]) else {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid duration"));
|
||||
return Err(crate::error::create_range_error(vm, "invalid duration"));
|
||||
};
|
||||
let Some(component) = a.checked_add(b).and_then(|value| value.checked_mul(scale)) else {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range"));
|
||||
return Err(crate::error::create_range_error(vm, "duration is out of range"));
|
||||
};
|
||||
let Some(updated) = total_ns.checked_add(component) else {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range"));
|
||||
return Err(crate::error::create_range_error(vm, "duration is out of range"));
|
||||
};
|
||||
total_ns = updated;
|
||||
}
|
||||
@@ -3840,14 +3916,199 @@ pub fn duration_add<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
];
|
||||
for (index, scale) in (3..10).zip(UNIT_SCALES) {
|
||||
if values[index] != 0.0 && values[index].abs() * scale >= MAX_TIME_NANOSECONDS {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "duration time fields are out of range"));
|
||||
return Err(crate::error::create_range_error(vm, "duration time fields are out of range"));
|
||||
}
|
||||
}
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
/// `Temporal.Duration.prototype.subtract(other)`:等于 add(other.negated())。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 继承 add 的无日历单位限制:任一侧含 year/month/week 抛 RangeError。
|
||||
/// - 参数归一与 add 一致(字符串/对象/负值均支持)。
|
||||
pub fn duration_subtract<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 other_val = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) };
|
||||
let other = native_try!(duration_like_values(vm, other_val));
|
||||
let receiver = duration_values(obj);
|
||||
let negated = std::array::from_fn(|index| if other[index] != 0.0 { -other[index] } else { 0.0 });
|
||||
let values = native_try!(add_duration_values(vm, &receiver, &negated));
|
||||
make_duration(vm, values)
|
||||
}
|
||||
/// `Temporal.Duration.prototype.round(roundTo)`锛氭寜鏈€灏忓崟浣嶈垗鍏ュ苟鎸夋渶澶у崟浣嶅钩琛°€?
|
||||
/// 绗竴鐗堟敮鎸佹棤鏃ュ巻鍗曚綅锛坹ear/month/week 闈為浂鎴栫洰鏍囦负鏃ュ巻鍗曚綅鏃惰姹?relativeTo锛屾殏鎶?RangeError锛夛紱
|
||||
/// 绾弒鏃堕棿璺緞鎸?24 灏忔椂/澶╁鐞嗭紝涓?polyfill 鐨?24h-day 璇箟涓€鑷淬€?
|
||||
|
||||
/// `Temporal.Duration.prototype.equals(other)`:逐分量比较两个时长。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - receiver 须为 Duration(branding TypeError)。
|
||||
/// - 参数经 duration_like_values 归一(无效字符串/混合符号抛 RangeError)。
|
||||
/// - 全分量相等返回 true;仅数值相等比较,不涉及日历语义。
|
||||
pub fn duration_equals<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 other_val = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) };
|
||||
let other = native_try!(duration_like_values(vm, other_val));
|
||||
let receiver = duration_values(obj);
|
||||
let equal = (0..10).all(|index| receiver[index] == other[index]);
|
||||
NativeResult::Ok(JsValue::bool(equal))
|
||||
}
|
||||
|
||||
/// `Temporal.Duration.prototype.sign`:首个非零分量的符号(-1/0/+1)。
|
||||
pub fn duration_sign<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));
|
||||
for value in duration_values(obj) {
|
||||
if value != 0.0 {
|
||||
return NativeResult::Ok(JsValue::int(if value.is_sign_negative() { -1 } else { 1 }));
|
||||
}
|
||||
}
|
||||
NativeResult::Ok(JsValue::int(0))
|
||||
}
|
||||
|
||||
/// `Temporal.Duration.prototype.blank`:所有分量是否全零。
|
||||
pub fn duration_blank<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::bool(duration_values(obj).iter().all(|value| *value == 0.0)))
|
||||
}
|
||||
|
||||
/// `Temporal.Duration.prototype.toJSON()`:输出默认 ISO 字符串,忽略参数。
|
||||
pub fn duration_to_json<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let receiver = args.first().copied().unwrap_or(0);
|
||||
duration_to_string(vm, &[receiver])
|
||||
}
|
||||
|
||||
/// `Temporal.Duration.prototype.toLocaleString()`:默认 locale 下返回 ISO 字符串(无 Intl 依赖)。
|
||||
pub fn duration_to_locale_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let receiver = args.first().copied().unwrap_or(0);
|
||||
duration_to_string(vm, &[receiver])
|
||||
}
|
||||
|
||||
/// 把 duration 应用相对点,返回目标 `(date, time_ns)`。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. days 及时间分量按 24h/day 汇总为纳秒,跨天部分拆成 delta_days 与一天内 target_time。
|
||||
/// 2. 日历分量(year/month/week)与 delta_days 经 add_date_duration 加到相对点。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 时间分量超出 i128 纳秒表示范围时抛 RangeError。
|
||||
/// - 返回的 time_ns 带符号(负 duration 时可为负)。
|
||||
fn duration_apply_to_relative<H: VmHost>(
|
||||
vm: &mut H, rel: (i128, i128, i128), values: &[f64; 10],
|
||||
) -> Result<((i128, i128, i128), i128), JsValue> {
|
||||
let Some(time_ns_total) = duration_time_nanoseconds(values) else {
|
||||
return Err(crate::error::create_range_error(vm, "duration is out of range"));
|
||||
};
|
||||
let delta_days = time_ns_total / DAY_NS;
|
||||
let target_time = time_ns_total % DAY_NS;
|
||||
let mut date_parts = [0.0; 10];
|
||||
date_parts[0] = values[0];
|
||||
date_parts[1] = values[1];
|
||||
date_parts[2] = values[2];
|
||||
date_parts[3] = delta_days as f64;
|
||||
let end = add_date_duration(rel, &date_parts);
|
||||
// 目标日期须落在 ISO 日期范围内(约 ±10^8 天),否则抛 RangeError。
|
||||
if days_from_civil(end.0, end.1, end.2).abs() > MAX_ISO_DAY {
|
||||
return Err(crate::error::create_range_error(vm, "relativeTo plus duration is out of range"));
|
||||
}
|
||||
Ok((end, target_time))
|
||||
}
|
||||
|
||||
/// `Temporal.Duration.compare(one, two, options)`:按相对点比较两个时长。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. one/two 各自 duration_like_values 归一(读序 one → two → options)。
|
||||
/// 2. options.relativeTo 经 duration_relative_to_date 归一(含 ZDT/Instant/bag 分支)。
|
||||
/// 3. 无 relativeTo 且无日历单位:按 duration_time_nanoseconds 直接比大小(fast path)。
|
||||
/// 4. 无 relativeTo 但含日历单位:RangeError(calendar-possibly-required 语义)。
|
||||
/// 5. 有 relativeTo:每个 duration 应用相对点得 (date, time),先比日期再比时间(字典序)。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 含日历单位必须提供 relativeTo,否则 RangeError。
|
||||
/// - 时间分量按 24h/day 折算;比较的是应用后目标时刻,非分量数值。
|
||||
pub fn duration_compare<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let one_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
|
||||
let two_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() };
|
||||
let one = native_try!(duration_like_values(vm, one_val));
|
||||
let two = native_try!(duration_like_values(vm, two_val));
|
||||
// 全分量相等直接返回 0(含日历单位时也无需 relativeTo,对齐 instances-identical)。
|
||||
if one == two {
|
||||
return NativeResult::Ok(JsValue::int(0));
|
||||
}
|
||||
let options_value = if args.len() > 3 { vm.reg(args[3]) } else { JsValue::undefined() };
|
||||
let relative_raw = if options_value.is_undefined() {
|
||||
JsValue::undefined()
|
||||
} 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 };
|
||||
match temporal_option_value(vm, options, options_value, "relativeTo") {
|
||||
Ok(raw) => raw,
|
||||
Err(error) => return NativeResult::Err(error),
|
||||
}
|
||||
};
|
||||
let relative = native_try!(duration_relative_to_date(vm, relative_raw));
|
||||
|
||||
let ordering = match relative {
|
||||
None => {
|
||||
// 无 relativeTo:含日历单位抛 RangeError,否则按归一纳秒比大小。
|
||||
if one[..3].iter().any(|value| *value != 0.0) || two[..3].iter().any(|value| *value != 0.0) {
|
||||
return NativeResult::Err(crate::error::create_range_error(
|
||||
vm,
|
||||
"relativeTo is required for calendar units",
|
||||
));
|
||||
}
|
||||
let one_ns = native_try!(duration_time_nanoseconds(&one).ok_or_else(|| {
|
||||
crate::error::create_range_error(vm, "duration is out of range")
|
||||
}));
|
||||
let two_ns = native_try!(duration_time_nanoseconds(&two).ok_or_else(|| {
|
||||
crate::error::create_range_error(vm, "duration is out of range")
|
||||
}));
|
||||
one_ns.cmp(&two_ns)
|
||||
}
|
||||
Some(rel) => {
|
||||
// 有 relativeTo:各 duration 应用后先比日期再比时间。
|
||||
let (date1, time1) = native_try!(duration_apply_to_relative(vm, rel, &one));
|
||||
let (date2, time2) = native_try!(duration_apply_to_relative(vm, rel, &two));
|
||||
let date_cmp = compare_iso_date(date1, date2);
|
||||
if date_cmp != 0 {
|
||||
if date_cmp > 0 {
|
||||
std::cmp::Ordering::Greater
|
||||
} else {
|
||||
std::cmp::Ordering::Less
|
||||
}
|
||||
} else {
|
||||
time1.cmp(&time2)
|
||||
}
|
||||
}
|
||||
};
|
||||
NativeResult::Ok(JsValue::int(match ordering {
|
||||
std::cmp::Ordering::Less => -1,
|
||||
std::cmp::Ordering::Equal => 0,
|
||||
std::cmp::Ordering::Greater => 1,
|
||||
}))
|
||||
}
|
||||
/// `Temporal.Duration.prototype.round(roundTo)`:按最小单位舍入并按最大单位平衡。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. 解析 roundTo(字符串简写或对象)→ largestUnit/smallestUnit/roundingIncrement/roundingMode/relativeTo。
|
||||
/// 2. relativeTo 经 duration_relative_to_date 归一;仅日历单位路径需要它。
|
||||
/// 3. 纯时间路径(无 year/month/week 且 largest/smallest ≥ day)按 24h/day 汇总舍入。
|
||||
/// 4. 日历单位路径把 duration 应用 relativeTo 后走 nudge_iso_difference 平衡舍入。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 含日历单位(year/month/week 非零)或目标单位为日历单位时 relativeTo 缺失抛 RangeError。
|
||||
/// - 时间分量按 24 小时/天折算,与 polyfill 的 24h-day 语义一致。
|
||||
pub fn duration_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let ptr = native_try!(receiver_obj(vm, args));
|
||||
let obj = unsafe { &*ptr };
|
||||
@@ -3858,7 +4119,7 @@ pub fn duration_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
}
|
||||
|
||||
let values = duration_values(obj);
|
||||
// 鐜版湁鏈€澶у崟浣嶏細棣栦釜闈為浂鍒嗛噺锛涘叏闆舵椂瑙嗕负 nanosecond銆?
|
||||
// 现有最大单位:首个非零分量;全零时视为 nanosecond。
|
||||
let mut existing_largest = 9_usize;
|
||||
for (index, value) in values.iter().enumerate() {
|
||||
if *value != 0.0 {
|
||||
@@ -3867,8 +4128,8 @@ pub fn duration_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
}
|
||||
}
|
||||
|
||||
// roundTo 瀛楃涓?=> { smallestUnit: 瀛楃涓?}锛涘惁鍒欏繀椤讳负瀵硅薄銆?
|
||||
let (largest_raw, _relative_raw, increment_raw, mode_raw, smallest_raw) = if round_to.is_string() {
|
||||
// roundTo 字符串 => { smallestUnit: 字符串 };否则必须为对象。
|
||||
let (largest_raw, relative_raw, increment_raw, mode_raw, smallest_raw) = if round_to.is_string() {
|
||||
(
|
||||
JsValue::undefined(),
|
||||
JsValue::undefined(),
|
||||
@@ -3893,7 +4154,7 @@ pub fn duration_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
(largest_raw, relative_raw, increment_raw, mode_raw, smallest_raw)
|
||||
};
|
||||
|
||||
// largestUnit锛氬厑璁?auto"锛涚己鐪?/undefined 瑙嗕负鏈彁渚涖€?
|
||||
// largestUnit:允许 "auto";缺失/undefined 视为未提供。
|
||||
let largest_provided = !largest_raw.is_undefined();
|
||||
let largest_index = if largest_raw.is_undefined() {
|
||||
None
|
||||
@@ -3911,7 +4172,7 @@ pub fn duration_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
}
|
||||
};
|
||||
|
||||
// roundingIncrement锛歍oIntegerOrInfinity 鑸嶅叆鍚庨渶鍦?[1, 10^9] 鍐呫€?
|
||||
// roundingIncrement:ToIntegerOrInfinity 舍入后需在 [1, 10^9] 内。
|
||||
let increment_value = if increment_raw.is_undefined() {
|
||||
1.0
|
||||
} else {
|
||||
@@ -3923,7 +4184,7 @@ pub fn duration_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
}
|
||||
let increment = increment as i128;
|
||||
|
||||
// roundingMode锛氱己鐪?halfExpand銆?
|
||||
// roundingMode:缺失时 halfExpand。
|
||||
let mode = if mode_raw.is_undefined() {
|
||||
InstantRoundingMode::HalfExpand
|
||||
} else {
|
||||
@@ -3936,7 +4197,7 @@ pub fn duration_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
}
|
||||
};
|
||||
|
||||
// smallestUnit锛氱己鐪?nanosecond銆?
|
||||
// smallestUnit:缺失时 nanosecond。
|
||||
let smallest_provided = !smallest_raw.is_undefined();
|
||||
let smallest_index = if smallest_raw.is_undefined() {
|
||||
9
|
||||
@@ -3950,7 +4211,7 @@ pub fn duration_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
}
|
||||
};
|
||||
|
||||
// 榛樿鏈€澶у崟浣嶏細鐜版湁鏈€澶у崟浣嶄笌鏈€灏忓崟浣嶄腑杈冨ぇ鐨勯偅涓€€?
|
||||
// 默认最大单位:现有最大单位与最小单位中较大(索引较小)的那个。
|
||||
let default_largest = if existing_largest < smallest_index {
|
||||
existing_largest
|
||||
} else {
|
||||
@@ -3958,7 +4219,7 @@ pub fn duration_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
};
|
||||
let largest = largest_index.unwrap_or(default_largest);
|
||||
|
||||
// 鑷冲皯涓€涓崟浣嶉渶瑕佹樉寮忔彁渚涳紱largest 涓嶈兘灏忎簬 smallest銆?
|
||||
// 至少一个单位需要显式提供;largest 不能小于 smallest。
|
||||
if !smallest_provided && !largest_provided {
|
||||
return NativeResult::Err(crate::error::create_range_error(
|
||||
vm,
|
||||
@@ -3972,26 +4233,13 @@ pub fn duration_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
));
|
||||
}
|
||||
|
||||
// 鑸嶅叆澧為噺涓婇檺锛氬崟浣嶈秺灏忓彲闄ら櫎涓婁竴绾э紱鏃ュ巻鍗曚綅锛坹ear/month/week/day锛夋棤鏁撮櫎绾︽潫銆?
|
||||
// 舍入增量上限:单位越小可除以上一级;日历单位(year/month/week/day)无整除约束。
|
||||
const MAX_INCREMENT: [i128; 10] = [0, 0, 0, 0, 24, 60, 60, 1000, 1000, 1000];
|
||||
let max_increment = MAX_INCREMENT[smallest_index];
|
||||
if max_increment != 0 && (increment >= max_increment || max_increment % increment != 0) {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment"));
|
||||
}
|
||||
|
||||
// 鏃ュ巻鍗曚綅锛坹ear/month/week锛夛細鏈増瑕佹眰 relativeTo锛屾殏鎶?RangeError銆?
|
||||
if values[..3].iter().any(|value| *value != 0.0) {
|
||||
return NativeResult::Err(crate::error::create_range_error(
|
||||
vm,
|
||||
"a starting point is required for balancing calendar units",
|
||||
));
|
||||
}
|
||||
if largest < 3 || smallest_index < 3 {
|
||||
return NativeResult::Err(crate::error::create_range_error(
|
||||
vm,
|
||||
"a starting point is required for calendar units",
|
||||
));
|
||||
}
|
||||
// 日历单位的 increment 要求 largestUnit 与 smallestUnit 相同(防止窗口舍入歧义)。
|
||||
if increment > 1 && smallest_index == 3 && largest != smallest_index {
|
||||
return NativeResult::Err(crate::error::create_range_error(
|
||||
vm,
|
||||
@@ -3999,58 +4247,98 @@ pub fn duration_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
));
|
||||
}
|
||||
|
||||
// 绾弒鏃堕棿璺緞锛氭寜 24 灏忔椂/澶╁皢 days..nanoseconds 姹囨€讳负绾崇锛屾寜 smallest 鑸嶅叆鍚庡钩琛″埌 largest銆?
|
||||
let Some(time_ns) = duration_time_nanoseconds(&values) else {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range"));
|
||||
// relativeTo 仅在日历路径需要时读取(解析失败直接抛错)。
|
||||
let relative_date = match duration_relative_to_date(vm, relative_raw) {
|
||||
Ok(relative) => relative,
|
||||
Err(error) => return NativeResult::Err(error),
|
||||
};
|
||||
const UNIT_NS: [i128; 7] =
|
||||
[86_400_000_000_000, 3_600_000_000_000, 60_000_000_000, 1_000_000_000, 1_000_000, 1_000, 1];
|
||||
let quantum = if smallest_index == 3 {
|
||||
increment * UNIT_NS[0]
|
||||
} else {
|
||||
increment * UNIT_NS[smallest_index - 3]
|
||||
};
|
||||
let Some(rounded) = round_instant_difference(time_ns, quantum, mode) else {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range"));
|
||||
};
|
||||
let mut result = [0.0; 10];
|
||||
if smallest_index == 3 {
|
||||
result[3] = (rounded / UNIT_NS[0]) as f64;
|
||||
} else {
|
||||
// 涓?duration_add 鐩稿悓鐨勫悓鍙锋ā鍒嗚В锛氫粠 ns 鍚?largest 杩涗綅銆?
|
||||
let mut rem = rounded;
|
||||
let mut unit = 9_usize;
|
||||
loop {
|
||||
if unit == largest {
|
||||
result[unit] = rem as f64;
|
||||
break;
|
||||
|
||||
// 日历单位存在或目标为日历单位时走 relativeTo 路径,否则走纯时间路径。
|
||||
let needs_relative = values[..3].iter().any(|value| *value != 0.0) || largest < 3 || smallest_index < 3;
|
||||
if !needs_relative {
|
||||
// 纯时间路径:按 24 小时/天将 days..nanoseconds 汇总为纳秒,按 smallest 舍入后平衡到 largest。
|
||||
let Some(time_ns) = duration_time_nanoseconds(&values) else {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range"));
|
||||
};
|
||||
const UNIT_NS: [i128; 7] =
|
||||
[86_400_000_000_000, 3_600_000_000_000, 60_000_000_000, 1_000_000_000, 1_000_000, 1_000, 1];
|
||||
let quantum = if smallest_index == 3 {
|
||||
increment * UNIT_NS[0]
|
||||
} else {
|
||||
increment * UNIT_NS[smallest_index - 3]
|
||||
};
|
||||
let Some(rounded) = round_instant_difference(time_ns, quantum, mode) else {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range"));
|
||||
};
|
||||
let mut result = [0.0; 10];
|
||||
if smallest_index == 3 {
|
||||
result[3] = (rounded / UNIT_NS[0]) as f64;
|
||||
} else {
|
||||
// 与 duration_add 相同的同号模分解:从 ns 向 largest 进位。
|
||||
let mut rem = rounded;
|
||||
let mut unit = 9_usize;
|
||||
loop {
|
||||
if unit == largest {
|
||||
result[unit] = rem as f64;
|
||||
break;
|
||||
}
|
||||
let base = match unit {
|
||||
7..=9 => 1_000,
|
||||
6 | 5 => 60,
|
||||
_ => 24,
|
||||
};
|
||||
result[unit] = (rem % base) as f64;
|
||||
rem /= base;
|
||||
unit -= 1;
|
||||
}
|
||||
let base = match unit {
|
||||
7..=9 => 1_000,
|
||||
6 | 5 => 60,
|
||||
_ => 24,
|
||||
};
|
||||
result[unit] = (rem % base) as f64;
|
||||
rem /= base;
|
||||
unit -= 1;
|
||||
}
|
||||
}
|
||||
// 鑼冨洿鏍¢獙锛氬姣忎釜鏃堕棿鍒嗛噺鎸夊叾绾崇鍒诲害妫€鏌ユ槸鍚﹁揪鍒?2^53 绉掍笂闄愩€?
|
||||
const MAX_TIME_NANOSECONDS: f64 = (1_i128 << 53) as f64 * 1_000_000_000.0;
|
||||
const UNIT_SCALES: [f64; 7] = [
|
||||
86_400_000_000_000.0,
|
||||
3_600_000_000_000.0,
|
||||
60_000_000_000.0,
|
||||
1_000_000_000.0,
|
||||
1_000_000.0,
|
||||
1_000.0,
|
||||
1.0,
|
||||
];
|
||||
for (index, scale) in (3..10).zip(UNIT_SCALES) {
|
||||
if result[index] != 0.0 && result[index].abs() * scale >= MAX_TIME_NANOSECONDS {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "duration time fields are out of range"));
|
||||
// 范围校验:对每个时间分量按纳秒刻度检查是否达到 2^53 秒上限。
|
||||
const MAX_TIME_NANOSECONDS: f64 = (1_i128 << 53) as f64 * 1_000_000_000.0;
|
||||
const UNIT_SCALES: [f64; 7] = [
|
||||
86_400_000_000_000.0,
|
||||
3_600_000_000_000.0,
|
||||
60_000_000_000.0,
|
||||
1_000_000_000.0,
|
||||
1_000_000.0,
|
||||
1_000.0,
|
||||
1.0,
|
||||
];
|
||||
for (index, scale) in (3..10).zip(UNIT_SCALES) {
|
||||
if result[index] != 0.0 && result[index].abs() * scale >= MAX_TIME_NANOSECONDS {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "duration time fields are out of range"));
|
||||
}
|
||||
}
|
||||
return make_duration(vm, result);
|
||||
}
|
||||
|
||||
// 日历路径:需要 relativeTo,把 duration 应用后按 nudge_iso_difference 舍入平衡。
|
||||
let Some(rel) = relative_date else {
|
||||
return NativeResult::Err(crate::error::create_range_error(
|
||||
vm,
|
||||
"a starting point is required for rounding calendar units",
|
||||
));
|
||||
};
|
||||
let Some(time_ns_total) = duration_time_nanoseconds(&values) else {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range"));
|
||||
};
|
||||
let delta_days = time_ns_total / DAY_NS;
|
||||
let target_time = time_ns_total % DAY_NS;
|
||||
let mut date_parts = [0.0; 10];
|
||||
date_parts[0] = values[0];
|
||||
date_parts[1] = values[1];
|
||||
date_parts[2] = values[2];
|
||||
date_parts[3] = delta_days as f64;
|
||||
let end = add_date_duration(rel, &date_parts);
|
||||
let settings = DifferenceSettings {
|
||||
largest_index: largest,
|
||||
smallest_index,
|
||||
increment,
|
||||
mode,
|
||||
};
|
||||
let result = match nudge_iso_difference(vm, rel, 0, end, target_time, settings, false) {
|
||||
Ok(values) => values,
|
||||
Err(error) => return NativeResult::Err(error),
|
||||
};
|
||||
make_duration(vm, result)
|
||||
}
|
||||
|
||||
@@ -6495,6 +6783,30 @@ fn difference_core<H: VmHost>(
|
||||
vm: &mut H, start: (i128, i128, i128), start_time_ns: i128, end: (i128, i128, i128), end_time_ns: i128,
|
||||
settings: DifferenceSettings, since: bool,
|
||||
) -> NativeResult {
|
||||
let values = match nudge_iso_difference(vm, start, start_time_ns, end, end_time_ns, settings, since) {
|
||||
Ok(values) => values,
|
||||
Err(error) => return NativeResult::Err(error),
|
||||
};
|
||||
make_duration(vm, values)
|
||||
}
|
||||
|
||||
/// 对 `end - start` 的 ISO 日期时间差按设置取整并平衡到最大单位。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. 日期差按 largestUnit 分解(时间单位时 days 并入时间)。
|
||||
/// 2. 不要求舍入(smallest=nanosecond 且 increment=1)时直接按最大单位拆回。
|
||||
/// 3. smallest 为 day/时间单位走 NudgeToDayOrTime;为 year/month/week 走 NudgeToCalendarUnit。
|
||||
/// 4. since 用 NegateRoundingMode 舍入并在最后整体取反。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - Duration.round 的日历单位路径与本函数共用:round 把 duration 应用 relativeTo 后
|
||||
/// 以同样的 start/end 差输入本函数,输出即 balance 后的舍入结果。
|
||||
/// - 时间分量为带符号 i128;start/end 日与时间各自独立,符号由差值推导。
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn nudge_iso_difference<H: VmHost>(
|
||||
vm: &mut H, start: (i128, i128, i128), start_time_ns: i128, end: (i128, i128, i128), end_time_ns: i128,
|
||||
settings: DifferenceSettings, since: bool,
|
||||
) -> Result<[f64; 10], JsValue> {
|
||||
let DifferenceSettings {
|
||||
largest_index,
|
||||
smallest_index,
|
||||
@@ -6564,7 +6876,7 @@ fn difference_core<H: VmHost>(
|
||||
let mut values = date_values;
|
||||
if largest_index >= 4 {
|
||||
let Some(time_values) = balance_instant_difference(time_ns, largest_index - 4) else {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "difference is out of range"));
|
||||
return Err(crate::error::create_range_error(vm, "difference is out of range"));
|
||||
};
|
||||
values[4..10].copy_from_slice(&time_values[4..10]);
|
||||
} else {
|
||||
@@ -6586,7 +6898,7 @@ fn difference_core<H: VmHost>(
|
||||
UNIT_NS[smallest_index - 4] * increment
|
||||
};
|
||||
let Some(rounded_ns) = round_instant_difference(total_ns, quantum, mode) else {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "difference is out of range"));
|
||||
return Err(crate::error::create_range_error(vm, "difference is out of range"));
|
||||
};
|
||||
let whole_days = rounded_ns / DAY_NS;
|
||||
let rem = rounded_ns % DAY_NS;
|
||||
@@ -6595,7 +6907,7 @@ fn difference_core<H: VmHost>(
|
||||
let mut values = date_values;
|
||||
if largest_index >= 4 {
|
||||
let Some(time_values) = balance_instant_difference(rounded_ns, largest_index - 4) else {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "difference is out of range"));
|
||||
return Err(crate::error::create_range_error(vm, "difference is out of range"));
|
||||
};
|
||||
values[4..10].copy_from_slice(&time_values[4..10]);
|
||||
} else {
|
||||
@@ -6609,9 +6921,7 @@ fn difference_core<H: VmHost>(
|
||||
let nudged = dest_epoch + (rounded_ns - total_ns);
|
||||
match bubble_relative_duration(sign, values, nudged, date1, time1_ns, largest_index, 3) {
|
||||
Ok(bubbled) => values = bubbled,
|
||||
Err(()) => {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "difference is out of range"))
|
||||
}
|
||||
Err(()) => return Err(crate::error::create_range_error(vm, "difference is out of range")),
|
||||
}
|
||||
}
|
||||
values
|
||||
@@ -6629,13 +6939,13 @@ fn difference_core<H: VmHost>(
|
||||
mode,
|
||||
) {
|
||||
Ok(result) => result,
|
||||
Err(()) => return NativeResult::Err(crate::error::create_range_error(vm, "difference is out of range")),
|
||||
Err(()) => return Err(crate::error::create_range_error(vm, "difference is out of range")),
|
||||
};
|
||||
if did_expand && smallest_index != 2 {
|
||||
match bubble_relative_duration(sign, values, nudged_epoch, date1, time1_ns, largest_index, smallest_index) {
|
||||
Ok(bubbled) => values = bubbled,
|
||||
Err(()) => {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "difference is out of range"))
|
||||
return Err(crate::error::create_range_error(vm, "difference is out of range"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6648,7 +6958,7 @@ fn difference_core<H: VmHost>(
|
||||
}
|
||||
}
|
||||
}
|
||||
make_duration(vm, values)
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
/// 计算差值并舍入。until 返回 other 减 receiver,since 返回反向。
|
||||
|
||||
@@ -891,7 +891,14 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
|
||||
world,
|
||||
duration_ctor,
|
||||
core,
|
||||
&[("from", oxide_builtins::temporal::duration_from::<crate::vm::Vm> as *const (), 1)],
|
||||
&[
|
||||
("from", oxide_builtins::temporal::duration_from::<crate::vm::Vm> as *const (), 1),
|
||||
(
|
||||
"compare",
|
||||
oxide_builtins::temporal::duration_compare::<crate::vm::Vm> as *const (),
|
||||
2,
|
||||
),
|
||||
],
|
||||
);
|
||||
let duration_proto_ptr = world.duration_proto.as_ptr() as *mut JsObject;
|
||||
let duration_proto = unsafe { &mut *duration_proto_ptr };
|
||||
@@ -915,6 +922,8 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
|
||||
"nanoseconds",
|
||||
oxide_builtins::temporal::duration_nanoseconds::<crate::vm::Vm> as *const (),
|
||||
),
|
||||
("sign", oxide_builtins::temporal::duration_sign::<crate::vm::Vm> as *const ()),
|
||||
("blank", oxide_builtins::temporal::duration_blank::<crate::vm::Vm> as *const ()),
|
||||
] {
|
||||
bind_accessor_getter(core, session, duration_proto, name, getter);
|
||||
}
|
||||
@@ -926,13 +935,35 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
|
||||
("abs", oxide_builtins::temporal::duration_abs::<crate::vm::Vm> as *const (), 0),
|
||||
("negated", oxide_builtins::temporal::duration_negated::<crate::vm::Vm> as *const (), 0),
|
||||
("add", oxide_builtins::temporal::duration_add::<crate::vm::Vm> as *const (), 1),
|
||||
(
|
||||
"subtract",
|
||||
oxide_builtins::temporal::duration_subtract::<crate::vm::Vm> as *const (),
|
||||
1,
|
||||
),
|
||||
("equals", oxide_builtins::temporal::duration_equals::<crate::vm::Vm> as *const (), 1),
|
||||
("round", oxide_builtins::temporal::duration_round::<crate::vm::Vm> as *const (), 1),
|
||||
("with", oxide_builtins::temporal::duration_with::<crate::vm::Vm> as *const (), 1),
|
||||
("total", oxide_builtins::temporal::duration_total::<crate::vm::Vm> as *const (), 1),
|
||||
("toString", oxide_builtins::temporal::duration_to_string::<crate::vm::Vm> as *const (), 0),
|
||||
("toJSON", oxide_builtins::temporal::duration_to_json::<crate::vm::Vm> as *const (), 0),
|
||||
(
|
||||
"toLocaleString",
|
||||
oxide_builtins::temporal::duration_to_locale_string::<crate::vm::Vm> as *const (),
|
||||
0,
|
||||
),
|
||||
("valueOf", oxide_builtins::temporal::duration_value_of::<crate::vm::Vm> as *const (), 0),
|
||||
],
|
||||
);
|
||||
bind_well_known_data_property(
|
||||
core,
|
||||
duration_proto,
|
||||
9,
|
||||
JsValue::perm_string(
|
||||
core.perm_interner()
|
||||
.string_ptr(core.perm_interner().intern("Temporal.Duration").0),
|
||||
),
|
||||
PropAttributes::new(false, false, true),
|
||||
);
|
||||
|
||||
// 把子对象挂到 Temporal 命名空间对象上,再把 Temporal 挂到 global。
|
||||
bind_global_value(core, temporal, "Now", JsValue::from_js_object(now_ptr));
|
||||
|
||||
@@ -2435,3 +2435,179 @@ fn zoned_date_time_add_error_paths() {
|
||||
.unwrap();
|
||||
assert_eq!(str_val(&vm, r), "true|true|true|true|true");
|
||||
}
|
||||
|
||||
// -- Temporal.Duration 本体补全(69.2) --
|
||||
|
||||
#[test]
|
||||
fn duration_compare_time_only_fast_path() {
|
||||
let mut vm = Vm::new();
|
||||
// 纯时间单位无 relativeTo:按归一纳秒比大小(days 折 24h)。
|
||||
assert_eq!(num(&mut vm, "Temporal.Duration.compare('P1D', 'PT24H')"), 0.0);
|
||||
assert_eq!(num(&mut vm, "Temporal.Duration.compare('P1D', 'PT23H')"), 1.0);
|
||||
assert_eq!(num(&mut vm, "Temporal.Duration.compare('PT23H', 'P1D')"), -1.0);
|
||||
assert_eq!(num(&mut vm, "Temporal.Duration.compare('P2D', 'PT48H')"), 0.0);
|
||||
assert_eq!(num(&mut vm, "Temporal.Duration.compare({days:1}, {hours:24})"), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_compare_calendar_units_require_relative() {
|
||||
let mut vm = Vm::new();
|
||||
// 无 relativeTo 且含日历单位 → RangeError。
|
||||
let r = eval(
|
||||
&mut vm,
|
||||
"try { Temporal.Duration.compare({years:1},{months:12}); 'no-throw' } catch (e) { e.constructor.name }",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(str_val(&vm, r), "RangeError");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_compare_relative_calendar_units() {
|
||||
let mut vm = Vm::new();
|
||||
// relativeto-month 对拍:P1M vs P30D 取决于相对点所在月份长度。
|
||||
assert_eq!(
|
||||
num(&mut vm, "Temporal.Duration.compare('P1M','P30D',{relativeTo:'2018-04-01'})"),
|
||||
0.0
|
||||
);
|
||||
assert_eq!(
|
||||
num(&mut vm, "Temporal.Duration.compare('P1M','P30D',{relativeTo:'2018-03-01'})"),
|
||||
1.0
|
||||
);
|
||||
assert_eq!(
|
||||
num(&mut vm, "Temporal.Duration.compare('P1M','P30D',{relativeTo:'2018-02-01'})"),
|
||||
-1.0
|
||||
);
|
||||
// string 与 PlainDate 对象等价。
|
||||
assert_eq!(
|
||||
num(
|
||||
&mut vm,
|
||||
"Temporal.Duration.compare('P1M','P30D',{relativeTo: Temporal.PlainDate.from('2018-04-01')})"
|
||||
),
|
||||
0.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_compare_relative_zdt_and_instant() {
|
||||
let mut vm = Vm::new();
|
||||
// 1970-04-01T00:00Z:P1M(到 5/1 为 30 天)vs P30D → 0。
|
||||
assert_eq!(
|
||||
num(
|
||||
&mut vm,
|
||||
"Temporal.Duration.compare('P1M','P30D',{relativeTo: new Temporal.ZonedDateTime(7776000000000000n,'UTC')})"
|
||||
),
|
||||
0.0
|
||||
);
|
||||
// Instant relativeTo(UTC 分解)同结果。
|
||||
assert_eq!(
|
||||
num(
|
||||
&mut vm,
|
||||
"Temporal.Duration.compare('P1M','P30D',{relativeTo: Temporal.Instant.fromEpochNanoseconds(7776000000000000n)})"
|
||||
),
|
||||
0.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_subtract_equals_add_negated() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(
|
||||
&mut vm,
|
||||
"const a = new Temporal.Duration(0,0,0,5,3);
|
||||
const b = {days:2,hours:1};
|
||||
a.subtract(b).toString() + '|' + a.add(Temporal.Duration.from(b).negated()).toString()",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(str_val(&vm, r), "P3DT2H|P3DT2H");
|
||||
// 含日历单位 → RangeError(继承 add 限制)。
|
||||
let r = eval(
|
||||
&mut vm,
|
||||
"try { new Temporal.Duration(1,0,0,0).subtract({days:1}); 'no-throw' } catch (e) { e.constructor.name }",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(str_val(&vm, r), "RangeError");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_equals_compares_all_components() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(
|
||||
&mut vm,
|
||||
"new Temporal.Duration(1,2,3,4).equals('P1Y2M3W4D') + '|' +
|
||||
new Temporal.Duration(1,2,3,4).equals('P1Y2M3W4DT1S') + '|' +
|
||||
new Temporal.Duration().equals('PT0S') + '|' +
|
||||
(() => { try { Temporal.Duration.prototype.equals.call({}, 'P1D'); return 'no-throw'; }
|
||||
catch (e) { return e instanceof TypeError; } })()",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(str_val(&vm, r), "true|false|true|true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_sign_and_blank() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(
|
||||
&mut vm,
|
||||
"new Temporal.Duration().sign + '|' + new Temporal.Duration().blank + '|' +
|
||||
new Temporal.Duration(-1,0,0,0).sign + '|' + new Temporal.Duration(-1,0,0,0).blank + '|' +
|
||||
Temporal.Duration.from('PT1H').sign",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(str_val(&vm, r), "0|true|-1|false|1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_to_json_and_locale_string_and_tag() {
|
||||
let mut vm = Vm::new();
|
||||
let r = eval(
|
||||
&mut vm,
|
||||
"const d = new Temporal.Duration(1,2,3,4,5);
|
||||
d.toJSON() + '|' + d.toLocaleString() + '|' + d[Symbol.toStringTag] + '|' +
|
||||
(d.toJSON() === d.toString()) + '|' + (d.toLocaleString('zh', {}) === d.toString())",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(str_val(&vm, r), "P1Y2M3W4DT5H|P1Y2M3W4DT5H|Temporal.Duration|true|true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_round_relative_calendar_units() {
|
||||
let mut vm = Vm::new();
|
||||
// P1M 相对 2018-04-01 舍到 day:4/1 + P1M = 5/1,round day → P1M。
|
||||
// P11M round {largestUnit:'year'}:smallest 默认 nanosecond,无日历舍入 → P11M。
|
||||
// 缺 relativeTo 的日历单位舍入 → RangeError。
|
||||
let r = eval(
|
||||
&mut vm,
|
||||
"Temporal.Duration.from('P1M').round({smallestUnit:'day', relativeTo:'2018-04-01'}).toString() + '|' +
|
||||
Temporal.Duration.from('P11M').round({largestUnit:'year', relativeTo:'2021-01-01'}).toString() + '|' +
|
||||
(() => { try { Temporal.Duration.from('P1M').round({smallestUnit:'day'}); return 'no-throw'; }
|
||||
catch (e) { return e instanceof RangeError; } })()",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(str_val(&vm, r), "P1M|P11M|true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_round_relative_smallest_calendar_unit() {
|
||||
let mut vm = Vm::new();
|
||||
// P11M round {largestUnit:'year', smallestUnit:'year'}:2021-01-01 下 11 个月 < 1 年,
|
||||
// halfExpand 折 334/365 → 舍到 1 年。
|
||||
let r = eval(
|
||||
&mut vm,
|
||||
"Temporal.Duration.from('P11M').round({largestUnit:'year', smallestUnit:'year', relativeTo:'2021-01-01'}).toString() + '|' +
|
||||
Temporal.Duration.from('P11M').round({largestUnit:'year', smallestUnit:'month', relativeTo:'2021-01-01'}).toString()",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(str_val(&vm, r), "P1Y|P11M");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_total_relative_zdt() {
|
||||
let mut vm = Vm::new();
|
||||
// ZDT relativeTo 取本地日期:1970-04-01 下 P1M total months → 1。
|
||||
let r = eval(
|
||||
&mut vm,
|
||||
"Temporal.Duration.from('P1M').total({unit:'months', relativeTo: new Temporal.ZonedDateTime(7776000000000000n,'UTC')})",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(r.as_double(), 1.0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user