mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
1467 lines
64 KiB
Rust
1467 lines
64 KiB
Rust
//! Temporal.Duration:ISO 字符串与 property bag 解析、构造、getter、total/round、
|
||
//! 加减比较与格式化。
|
||
|
||
use oxide_runtime_api::{to_number, to_string, NativeResult, ProtoKind, VmHost};
|
||
use oxide_types::object::JsObject;
|
||
use oxide_types::value::JsValue;
|
||
|
||
use super::common::native_try;
|
||
use super::common::{
|
||
canonical_time_zone, civil_from_days, days_from_civil, ensure_duration, get_calendar_id, get_double_prop,
|
||
get_instant_epoch_ns, initialize_temporal_receiver, is_ctor_call, local_to_epoch_ns, month_code_well_formed,
|
||
native_engine_error, parse_round_options, receiver_obj, resolve_date_time_parts, temporal_calendar_id,
|
||
temporal_option_number, temporal_option_string, temporal_option_value, temporal_string_strict,
|
||
zoned_date_time_plain_parts, DateTimeParts,
|
||
};
|
||
use super::difference::{
|
||
add_date_duration, add_days_iso, balance_instant_difference, compare_iso_date, date_duration_sign, date_until_iso,
|
||
nudge_iso_difference, nudge_window, plain_date_time_unit_index, round_instant_difference, DifferenceSettings,
|
||
InternalDuration, IsoDateTime, DAY_NS, MAX_ISO_DAY,
|
||
};
|
||
use super::time_zone::zone_offset_seconds;
|
||
use super::zoned_date_time::{
|
||
add_zoned_datetime, difference_zoned_datetime_with_rounding_vm, difference_zoned_datetime_with_total_vm,
|
||
disambiguate_possible_epoch_nanoseconds, extract_time_zone_annotation, get_possible_epoch_nanoseconds,
|
||
map_add_zoned_error, parse_any_offset_seconds, valid_offset_fraction, zoned_date_time_string_parts, ZonedPair,
|
||
};
|
||
use super::{make_duration, parse_plain_date_time_string};
|
||
|
||
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).copied().map(|byte| byte.to_ascii_uppercase()) != 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].eq_ignore_ascii_case(&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)?.to_ascii_uppercase();
|
||
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 {
|
||
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)
|
||
}
|
||
|
||
const DURATION_FIELD_ORDER: [(&str, usize); 10] = [
|
||
("days", 3),
|
||
("hours", 4),
|
||
("microseconds", 8),
|
||
("milliseconds", 7),
|
||
("minutes", 5),
|
||
("months", 1),
|
||
("nanoseconds", 9),
|
||
("seconds", 6),
|
||
("weeks", 2),
|
||
("years", 0),
|
||
];
|
||
|
||
fn duration_field_number<H: VmHost>(
|
||
vm: &mut H, obj: &JsObject, receiver: JsValue, name: &str,
|
||
) -> Result<Option<f64>, JsValue> {
|
||
let key_val = vm.new_string(name);
|
||
let si = vm.property_key_si(key_val);
|
||
let raw = match vm.ordinary_get(obj, si, receiver) {
|
||
Ok(value) => value,
|
||
Err(error) => return Err(native_engine_error(vm, &error)),
|
||
};
|
||
if raw.is_undefined() {
|
||
return Ok(None);
|
||
}
|
||
let primitive = match oxide_runtime_api::to_primitive(raw, oxide_runtime_api::ToPrimitiveHint::Number, vm) {
|
||
Ok(value) => value,
|
||
Err(error) => return Err(native_engine_error(vm, &error)),
|
||
};
|
||
if primitive.is_symbol() || primitive.is_bigint() {
|
||
return Err(crate::error::create_type_error(vm, "cannot convert duration field to number"));
|
||
}
|
||
Ok(Some(to_number(primitive)))
|
||
}
|
||
|
||
fn duration_partial_values<H: VmHost>(vm: &mut H, val: JsValue) -> Result<[Option<f64>; 10], JsValue> {
|
||
if !val.is_object() {
|
||
return Err(crate::error::create_type_error(vm, "duration-like value must be an object"));
|
||
}
|
||
let ptr = val.as_js_object_ptr();
|
||
if ptr.is_null() {
|
||
return Err(crate::error::create_type_error(vm, "duration-like value must be an object"));
|
||
}
|
||
let obj = unsafe { &*ptr };
|
||
if obj.is_duration_obj() {
|
||
return Ok(std::array::from_fn(|index| Some(get_double_prop(obj, index))));
|
||
}
|
||
|
||
let mut values = [None; 10];
|
||
for (name, index) in DURATION_FIELD_ORDER {
|
||
values[index] = duration_field_number(vm, obj, val, name)?;
|
||
}
|
||
if values.iter().all(Option::is_none) {
|
||
return Err(crate::error::create_type_error(vm, "duration-like object has no duration fields"));
|
||
}
|
||
Ok(values)
|
||
}
|
||
|
||
pub(crate) fn duration_component_integer(value: f64) -> Option<i128> {
|
||
if !value.is_finite() || value.fract() != 0.0 || value.abs() >= i128::MAX as f64 {
|
||
return None;
|
||
}
|
||
Some(value as i128)
|
||
}
|
||
|
||
pub(crate) fn duration_time_nanoseconds(values: &[f64; 10]) -> Option<i128> {
|
||
const SCALES: [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 mut total = 0_i128;
|
||
for (value, scale) in values[3..].iter().zip(SCALES) {
|
||
total = total.checked_add(duration_component_integer(*value)?.checked_mul(scale)?)?;
|
||
}
|
||
Some(total)
|
||
}
|
||
|
||
/// 时长时间部分(时到纳秒,不含日分量)的纳秒合计。
|
||
pub(crate) fn duration_time_only_nanoseconds(values: &[f64; 10]) -> Option<i128> {
|
||
const SCALES: [i128; 6] = [3_600_000_000_000, 60_000_000_000, 1_000_000_000, 1_000_000, 1_000, 1];
|
||
let mut total = 0_i128;
|
||
for (value, scale) in values[4..].iter().zip(SCALES) {
|
||
total = total.checked_add(duration_component_integer(*value)?.checked_mul(scale)?)?;
|
||
}
|
||
Some(total)
|
||
}
|
||
|
||
fn validate_duration_values<H: VmHost>(vm: &mut H, values: &[f64; 10]) -> Result<(), JsValue> {
|
||
let mut sign = 0_i8;
|
||
for value in values {
|
||
if duration_component_integer(*value).is_none() {
|
||
return Err(crate::error::create_range_error(vm, "invalid duration"));
|
||
}
|
||
if *value != 0.0 {
|
||
let current = if value.is_sign_negative() { -1 } else { 1 };
|
||
if sign != 0 && sign != current {
|
||
return Err(crate::error::create_range_error(vm, "duration fields must have the same sign"));
|
||
}
|
||
sign = current;
|
||
}
|
||
}
|
||
if values[..3].iter().any(|value| value.abs() > u32::MAX as f64) {
|
||
return Err(crate::error::create_range_error(vm, "duration date field is out of range"));
|
||
}
|
||
const MAX_TIME_NANOSECONDS: i128 = (1_i128 << 53) * 1_000_000_000;
|
||
let total = duration_time_nanoseconds(values)
|
||
.ok_or_else(|| crate::error::create_range_error(vm, "duration time fields are out of range"))?;
|
||
if total.abs() >= MAX_TIME_NANOSECONDS {
|
||
return Err(crate::error::create_range_error(vm, "duration time fields are out of range"));
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
pub(crate) fn duration_like_values<H: VmHost>(vm: &mut H, val: JsValue) -> Result<[f64; 10], JsValue> {
|
||
if val.is_string() {
|
||
let values = parse_duration_string(&to_string(val))
|
||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid duration string"))?;
|
||
validate_duration_values(vm, &values)?;
|
||
return Ok(values);
|
||
}
|
||
let partial = duration_partial_values(vm, val)?;
|
||
let values = std::array::from_fn(|index| partial[index].unwrap_or(0.0));
|
||
validate_duration_values(vm, &values)?;
|
||
Ok(values)
|
||
}
|
||
|
||
/// `Temporal.Duration` 构造器,保存十个整数时长分量。
|
||
pub fn duration_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||
let ctor_proto = vm.builtin_proto(ProtoKind::DurationProto);
|
||
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;
|
||
}
|
||
native_try!(validate_duration_values(vm, &values));
|
||
initialize_temporal_receiver(vm, args, JsObject::OBJ_TYPE_DURATION, values.map(JsValue::float))
|
||
}
|
||
|
||
/// `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))
|
||
}
|
||
|
||
/// relativeTo 归一后的相对点载荷:Wall(墙历日期)或 Zoned(epoch + 时区 + 日历)。
|
||
enum RelativePoint {
|
||
Wall {
|
||
date: (i128, i128, i128),
|
||
/// 当前仅 iso8601 日历,zoned 路径不消费日历。
|
||
#[allow(dead_code)]
|
||
calendar: String,
|
||
},
|
||
Zoned {
|
||
epoch_ns: i128,
|
||
time_zone: String,
|
||
calendar: String,
|
||
},
|
||
}
|
||
|
||
/// 相对点的墙历日期。
|
||
///
|
||
/// # 步骤
|
||
/// 1. Wall → 直接返回日期。
|
||
/// 2. Zoned → 区偏移(epoch 依赖查找)+ civil_from_days 反推墙历日期。
|
||
///
|
||
/// # 边界与前提
|
||
/// - Zoned 支仅用于 compare 的两支纯时间落回路径(24h 语义与规范 plain 路径一致)。
|
||
fn relative_point_wall_date<H: VmHost>(vm: &mut H, point: &RelativePoint) -> Result<(i128, i128, i128), JsValue> {
|
||
match point {
|
||
RelativePoint::Wall { date, .. } => Ok(*date),
|
||
RelativePoint::Zoned { epoch_ns, time_zone, .. } => {
|
||
let offset_seconds = zone_offset_seconds(time_zone, epoch_ns.div_euclid(1_000_000_000) as i64)
|
||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))?;
|
||
let wall_ns = epoch_ns + i128::from(offset_seconds) * 1_000_000_000;
|
||
Ok(civil_from_days(wall_ns.div_euclid(DAY_NS)))
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 归一 options.relativeTo 为相对点(Wall 墙历日期 / Zoned epoch+时区+日历)。
|
||
///
|
||
/// # 步骤
|
||
/// 1. string 先检测 `[tz]` 注解:含注解走 zoned 解析(offset-vs-zone 校验,reject 语义)
|
||
/// 得 Zoned 载荷;无注解走 plain 解析(纯日期、日期时间、带偏移的 plain 串)得 Wall 载荷。
|
||
/// 2. PlainDateTime / PlainDate 对象 → 直读 prop 0/1/2,Wall 载荷。
|
||
/// 3. ZonedDateTime → Zoned 载荷(zoned_date_time_plain_parts 范围检查后直读三槽)。
|
||
/// 4. 其他对象(含 Instant)按 property bag 解析:字段按规范字典序单读(calendar 先,
|
||
/// 非 iso8601 日历读 era/eraYear),逐字段 Get 后立即转换,constrain;
|
||
/// timeZone 缺省 → Wall 载荷;在场 → InterpretISODateTimeOffset(wall/option 两行为)
|
||
/// 得 Zoned 载荷。
|
||
/// 5. undefined → None;其余原始值 → TypeError。
|
||
///
|
||
/// # 边界与前提
|
||
/// - 调用方须已把 raw 从 options 取出(读序由调用方保证)。
|
||
/// - bag 支 offset 语法校验在字典序位(nanosecond 后)立即完成,先于后续字段转换;
|
||
/// offset-vs-zone 匹配校验在字段转换之后;timeZone 在场时经候选消歧/精确匹配产 Zoned 载荷。
|
||
fn duration_relative_to_date<H: VmHost>(vm: &mut H, relative_raw: JsValue) -> Result<Option<RelativePoint>, JsValue> {
|
||
if relative_raw.is_undefined() {
|
||
return Ok(None);
|
||
}
|
||
if relative_raw.is_string() {
|
||
let text = to_string(relative_raw);
|
||
// 含 [tz] 注解走 zoned 路径(offset-vs-zone 校验,reject 语义):
|
||
// 按 instant 解析,时区与日历直取解析产物。
|
||
if extract_time_zone_annotation(&text).is_some() {
|
||
let (epoch_ns, time_zone_id, _calendar) = zoned_date_time_string_parts(vm, &text, "reject")?;
|
||
return Ok(Some(RelativePoint::Zoned {
|
||
epoch_ns,
|
||
time_zone: time_zone_id,
|
||
calendar: "iso8601".to_string(),
|
||
}));
|
||
}
|
||
// 无注解走 plain 路径:纯日期、日期时间、带偏移的 plain 串(偏移不作时区标识符)。
|
||
if let Ok((year, month, day, _time_ns)) = parse_plain_date_time_string(&text) {
|
||
return Ok(Some(RelativePoint::Wall {
|
||
date: (i128::from(year), i128::from(month), i128::from(day)),
|
||
calendar: "iso8601".to_string(),
|
||
}));
|
||
}
|
||
return Err(crate::error::create_range_error(vm, "invalid relativeTo"));
|
||
}
|
||
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() {
|
||
let calendar = get_calendar_id(obj, if obj.is_plain_date_obj() { 3 } else { 4 });
|
||
return Ok(Some(RelativePoint::Wall {
|
||
date: (
|
||
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),
|
||
),
|
||
calendar,
|
||
}));
|
||
}
|
||
if obj.is_zoned_date_time_obj() {
|
||
// 保留 zoned_date_time_plain_parts 范围检查(墙历日期时间越界直接抛错)。
|
||
zoned_date_time_plain_parts(vm, obj)?;
|
||
let Some(epoch_ns) = get_instant_epoch_ns(obj) else {
|
||
return Err(crate::error::create_range_error(vm, "invalid ZonedDateTime"));
|
||
};
|
||
return Ok(Some(RelativePoint::Zoned {
|
||
epoch_ns,
|
||
time_zone: to_string(obj.get_prop_at(1)),
|
||
calendar: get_calendar_id(obj, 2),
|
||
}));
|
||
}
|
||
// property bag:字段按规范字典序单读(calendar 先),逐字段 Get 后立即转换。
|
||
// calendar 经宽松版接受:白名单/ISO 串(非法 RangeError)、Temporal 实例读日历槽、其余 TypeError。
|
||
let calendar_raw = temporal_option_value(vm, obj, relative_raw, "calendar")?;
|
||
let calendar = temporal_calendar_id(vm, calendar_raw)?;
|
||
// calendar 未提供(undefined)时默认 iso8601,era/eraYear 不读;仅具体非 iso8601 日历才读。
|
||
let non_iso_calendar = calendar.as_deref().map(|c| c != "iso8601").unwrap_or(false);
|
||
|
||
let day_raw = temporal_option_value(vm, obj, relative_raw, "day")?;
|
||
let day = bag_number_field(vm, day_raw)?;
|
||
// era/eraYear 仅非 iso8601 日历读取(规范扩展字段);iso8601 袋不读,读到即违反读序。
|
||
let era_raw = if non_iso_calendar {
|
||
temporal_option_value(vm, obj, relative_raw, "era")?
|
||
} else {
|
||
JsValue::undefined()
|
||
};
|
||
let era = bag_string_field(vm, era_raw)?;
|
||
let era_year_raw = if non_iso_calendar {
|
||
temporal_option_value(vm, obj, relative_raw, "eraYear")?
|
||
} else {
|
||
JsValue::undefined()
|
||
};
|
||
let era_year = bag_number_field(vm, era_year_raw)?;
|
||
let hour_raw = temporal_option_value(vm, obj, relative_raw, "hour")?;
|
||
let hour = bag_number_field(vm, hour_raw)?.unwrap_or(0.0);
|
||
let microsecond_raw = temporal_option_value(vm, obj, relative_raw, "microsecond")?;
|
||
let microsecond = bag_number_field(vm, microsecond_raw)?.unwrap_or(0.0);
|
||
let millisecond_raw = temporal_option_value(vm, obj, relative_raw, "millisecond")?;
|
||
let millisecond = bag_number_field(vm, millisecond_raw)?.unwrap_or(0.0);
|
||
let minute_raw = temporal_option_value(vm, obj, relative_raw, "minute")?;
|
||
let minute = bag_number_field(vm, minute_raw)?.unwrap_or(0.0);
|
||
let month_raw = temporal_option_value(vm, obj, relative_raw, "month")?;
|
||
let month = bag_number_field(vm, month_raw)?;
|
||
// monthCode 经 ToPrimitive(String):非串 TypeError,语法不合法 RangeError。
|
||
let month_code_raw = temporal_option_value(vm, obj, relative_raw, "monthCode")?;
|
||
let month_code = if month_code_raw.is_undefined() {
|
||
None
|
||
} else {
|
||
let code = temporal_string_strict(vm, month_code_raw)?;
|
||
if !month_code_well_formed(&code) {
|
||
return Err(crate::error::create_range_error(vm, "invalid monthCode"));
|
||
}
|
||
Some(code)
|
||
};
|
||
let nanosecond_raw = temporal_option_value(vm, obj, relative_raw, "nanosecond")?;
|
||
let nanosecond = bag_number_field(vm, nanosecond_raw)?.unwrap_or(0.0);
|
||
// offset 经 ToPrimitive(String) 后语法校验(RangeError),先于后续字段转换。
|
||
let offset_raw = temporal_option_value(vm, obj, relative_raw, "offset")?;
|
||
let bag_offset_seconds = if offset_raw.is_undefined() {
|
||
None
|
||
} else {
|
||
let offset_input = temporal_string_strict(vm, offset_raw)?;
|
||
let (seconds, _has_sub_minute) = parse_any_offset_seconds(&offset_input)
|
||
.filter(|_| valid_offset_fraction(&offset_input))
|
||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid offset"))?;
|
||
Some(seconds)
|
||
};
|
||
let second_raw = temporal_option_value(vm, obj, relative_raw, "second")?;
|
||
let second = bag_number_field(vm, second_raw)?.unwrap_or(0.0);
|
||
// timeZone 保持原始类型检查:非字符串(对象/符号/null/数字)→ TypeError;字符串非合法时区 → RangeError。
|
||
let time_zone_raw = temporal_option_value(vm, obj, relative_raw, "timeZone")?;
|
||
let time_zone_id = if time_zone_raw.is_undefined() {
|
||
None
|
||
} else {
|
||
if !time_zone_raw.is_string() {
|
||
return Err(crate::error::create_type_error(vm, "invalid time zone"));
|
||
}
|
||
let tz_input = to_string(time_zone_raw);
|
||
Some(canonical_time_zone(&tz_input).ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))?)
|
||
};
|
||
let year_raw = temporal_option_value(vm, obj, relative_raw, "year")?;
|
||
let year = bag_number_field(vm, year_raw)?;
|
||
|
||
// 缺失必填字段先抛 TypeError(先于任何 RangeError 值校验)。
|
||
if day.is_none() {
|
||
return Err(crate::error::create_type_error(vm, "day is required"));
|
||
}
|
||
if month.is_none() && month_code.is_none() {
|
||
return Err(crate::error::create_type_error(vm, "month is required"));
|
||
}
|
||
|
||
// era/eraYear 合并(非 iso8601 日历,gregory 最小语义):三者不全(仅 era 或仅 eraYear)
|
||
// TypeError;eraYear 0 归 1、负值换 era 续数;ad → year=eraYear、bc → year=1-eraYear;
|
||
// year 与 era/eraYear 同场指向不同年 RangeError。
|
||
let year = merge_bag_era_year(vm, year, era, era_year)?;
|
||
|
||
let (year, month, day, time_ns, calendar) = resolve_date_time_parts(
|
||
vm,
|
||
calendar,
|
||
DateTimeParts {
|
||
year: Some(year),
|
||
month,
|
||
month_code,
|
||
day,
|
||
hour,
|
||
minute,
|
||
second,
|
||
millisecond,
|
||
microsecond,
|
||
nanosecond,
|
||
},
|
||
true,
|
||
false,
|
||
)?;
|
||
let calendar = calendar.unwrap_or_else(|| "iso8601".to_string());
|
||
// timeZone 缺省:Wall 载荷(墙历路径)。
|
||
let Some(time_zone) = time_zone_id else {
|
||
return Ok(Some(RelativePoint::Wall {
|
||
date: (i128::from(year), i128::from(month), i128::from(day)),
|
||
calendar,
|
||
}));
|
||
};
|
||
// timeZone 在场:InterpretISODateTimeOffset(袋支恒 match-exactly)。
|
||
// offset 在场(~option~):候选 epoch 点 zone 偏移与输入偏移秒级精确相等(reject 语义),
|
||
// 再从候选集选命中候选(candidateOffset = utcEpochNs - candidate,秒级含亚分钟)。
|
||
// offset 缺省(~wall~):compatible 消歧(重叠取 earlier、间隙平移取 later 侧)。
|
||
// 固定偏移区退化:候选恒 1、偏移恒常量,与 local_to_epoch_ns 数值恒等。
|
||
let wall_ns = local_to_epoch_ns(year, month, day, time_ns, 0)
|
||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid date-time"))?;
|
||
if let Some(offset) = bag_offset_seconds {
|
||
let zone_offset = zone_offset_seconds(&time_zone, wall_ns.div_euclid(1_000_000_000) as i64)
|
||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))?;
|
||
if zone_offset != offset {
|
||
return Err(crate::error::create_range_error(vm, "offset and time zone disagree"));
|
||
}
|
||
}
|
||
let candidates = get_possible_epoch_nanoseconds(&time_zone, year, month, day, time_ns as i128)
|
||
.map_err(|error| map_add_zoned_error(vm, error))?;
|
||
let epoch_ns = if let Some(offset) = bag_offset_seconds {
|
||
candidates
|
||
.iter()
|
||
.find(|candidate| (wall_ns - *candidate) / 1_000_000_000 == i128::from(offset))
|
||
.copied()
|
||
.ok_or_else(|| crate::error::create_range_error(vm, "offset and time zone disagree"))?
|
||
} else {
|
||
disambiguate_possible_epoch_nanoseconds(
|
||
&time_zone,
|
||
(i128::from(year), i128::from(month), i128::from(day)),
|
||
time_ns as i128,
|
||
"compatible",
|
||
&candidates,
|
||
)
|
||
.map_err(|error| map_add_zoned_error(vm, error))?
|
||
};
|
||
Ok(Some(RelativePoint::Zoned { epoch_ns, time_zone, calendar }))
|
||
}
|
||
|
||
/// 袋支数值字段:undefined → None,其余 ToPrimitive(Number) 截断(Symbol/BigInt TypeError)。
|
||
fn bag_number_field<H: VmHost>(vm: &mut H, raw: JsValue) -> Result<Option<f64>, JsValue> {
|
||
if raw.is_undefined() {
|
||
Ok(None)
|
||
} else {
|
||
temporal_option_number(vm, raw).map(|number| Some(number.trunc()))
|
||
}
|
||
}
|
||
|
||
/// 袋支字符串字段:undefined → None,其余 ToPrimitive(String)(非串 TypeError)。
|
||
fn bag_string_field<H: VmHost>(vm: &mut H, raw: JsValue) -> Result<Option<String>, JsValue> {
|
||
if raw.is_undefined() {
|
||
Ok(None)
|
||
} else {
|
||
temporal_string_strict(vm, raw).map(Some)
|
||
}
|
||
}
|
||
|
||
/// era/eraYear 合并到 year(gregory 最小语义):
|
||
/// 仅 era 或仅 eraYear 在场 → TypeError;era 非 ad/bc(大小写不敏感)→ RangeError;
|
||
/// eraYear 0 归 1,负值换 era 续数(bc 的 -1 → ad 2 式);ad → year=eraYear、bc → year=1-eraYear。
|
||
fn merge_bag_era_year<H: VmHost>(
|
||
vm: &mut H, year: Option<f64>, era: Option<String>, era_year: Option<f64>,
|
||
) -> Result<f64, JsValue> {
|
||
match (year, era, era_year) {
|
||
(Some(year), Some(era), Some(era_year)) => {
|
||
let merged = compute_era_year(vm, &era, era_year)?;
|
||
if merged != year {
|
||
return Err(crate::error::create_range_error(vm, "era and year disagree"));
|
||
}
|
||
Ok(merged)
|
||
}
|
||
(Some(year), None, None) => Ok(year),
|
||
(None, Some(era), Some(era_year)) => compute_era_year(vm, &era, era_year),
|
||
(None, None, None) => Err(crate::error::create_type_error(vm, "year is required")),
|
||
_ => Err(crate::error::create_type_error(vm, "era and eraYear must both be present")),
|
||
}
|
||
}
|
||
|
||
fn compute_era_year<H: VmHost>(vm: &mut H, era: &str, era_year: f64) -> Result<f64, JsValue> {
|
||
let era = era.to_ascii_lowercase();
|
||
if era != "ad" && era != "bc" {
|
||
return Err(crate::error::create_range_error(vm, "invalid era"));
|
||
}
|
||
let mut era_is_ad = era == "ad";
|
||
let mut era_year = era_year;
|
||
if era_year == 0.0 {
|
||
era_year = 1.0;
|
||
}
|
||
if era_year < 0.0 {
|
||
era_is_ad = !era_is_ad;
|
||
era_year = 1.0 - era_year;
|
||
}
|
||
Ok(if era_is_ad { era_year } else { 1.0 - era_year })
|
||
}
|
||
|
||
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 total = duration_component_integer(seconds)?
|
||
.checked_mul(1_000_000_000)?
|
||
.checked_add(duration_component_integer(milliseconds)?.checked_mul(1_000_000)?)?
|
||
.checked_add(duration_component_integer(microseconds)?.checked_mul(1_000)?)?
|
||
.checked_add(duration_component_integer(nanoseconds)?)?;
|
||
if total == 0 {
|
||
return None;
|
||
}
|
||
let negative = total < 0;
|
||
let magnitude = total.abs();
|
||
let whole = magnitude / 1_000_000_000;
|
||
let fraction = magnitude % 1_000_000_000;
|
||
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.with(partial)`,以给定字段替换当前时长分量。
|
||
pub fn duration_with<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 partial_value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
|
||
let partial = native_try!(duration_partial_values(vm, partial_value));
|
||
let mut values = duration_values(obj);
|
||
for (index, value) in partial.into_iter().enumerate() {
|
||
if let Some(value) = value {
|
||
values[index] = value;
|
||
}
|
||
}
|
||
native_try!(validate_duration_values(vm, &values));
|
||
make_duration(vm, values)
|
||
}
|
||
|
||
/// `Temporal.Duration.prototype.total(totalOf)`:按单位汇总时长。
|
||
/// 日历单位(year/month/week)需要 relativeTo(PlainDateTime/PlainDate)。
|
||
pub fn duration_total<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 total_of = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
|
||
if total_of.is_undefined() {
|
||
return NativeResult::Err(crate::error::create_type_error(vm, "options argument is required"));
|
||
}
|
||
let (unit_raw, relative_raw) = if total_of.is_string() {
|
||
(to_string(total_of), JsValue::undefined())
|
||
} else {
|
||
if !total_of.is_object() {
|
||
return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object"));
|
||
}
|
||
let options_ptr = total_of.as_js_object_ptr();
|
||
if options_ptr.is_null() {
|
||
return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object"));
|
||
}
|
||
let options = unsafe { &*options_ptr };
|
||
let relative_raw = match temporal_option_value(vm, options, total_of, "relativeTo") {
|
||
Ok(raw) => raw,
|
||
Err(error) => return NativeResult::Err(error),
|
||
};
|
||
let unit_raw = match temporal_option_value(vm, options, total_of, "unit") {
|
||
Ok(raw) => match temporal_option_string(vm, raw) {
|
||
Ok(value) => value,
|
||
Err(error) => return NativeResult::Err(error),
|
||
},
|
||
Err(error) => return NativeResult::Err(error),
|
||
};
|
||
(unit_raw, relative_raw)
|
||
};
|
||
let unit_index = match plain_date_time_unit_index(&unit_raw) {
|
||
Some(index) => index,
|
||
None => return NativeResult::Err(crate::error::create_range_error(vm, "invalid unit")),
|
||
};
|
||
let values = duration_values(obj);
|
||
// 含日历单位(year/month/week)时,days 及以上的 total 必须走 relativeTo 日历路径。
|
||
let has_calendar_units = values[0] != 0.0 || values[1] != 0.0 || values[2] != 0.0;
|
||
|
||
// relativeTo:统一经 duration_relative_to_date 归一(支持 string/PD/PDT/ZDT/bag)。
|
||
let relative_date = match duration_relative_to_date(vm, relative_raw) {
|
||
Ok(relative) => relative,
|
||
Err(error) => return NativeResult::Err(error),
|
||
};
|
||
|
||
// Zoned 载荷:AddZonedDateTime → DifferenceZonedDateTimeWithTotal(门与单位无关)。
|
||
if let Some(RelativePoint::Zoned { epoch_ns, time_zone, calendar }) = &relative_date {
|
||
let target = native_try!(add_zoned_datetime(vm, *epoch_ns, time_zone, calendar, &values, true));
|
||
let pair = ZonedPair {
|
||
ns1: *epoch_ns,
|
||
ns2: target,
|
||
time_zone,
|
||
calendar,
|
||
};
|
||
let total = native_try!(difference_zoned_datetime_with_total_vm(vm, pair, unit_index));
|
||
return NativeResult::Ok(JsValue::float(total));
|
||
}
|
||
|
||
const UNIT_NS: [i128; 6] = [3_600_000_000_000, 60_000_000_000, 1_000_000_000, 1_000_000, 1_000, 1];
|
||
if unit_index <= 2 || (unit_index == 3 && has_calendar_units) {
|
||
// 日历单位:需要 relativeTo,用纪元纳秒窗口计算分数总量。
|
||
let Some(point) = relative_date else {
|
||
return NativeResult::Err(crate::error::create_range_error(
|
||
vm,
|
||
&format!("a starting point is required for {} total", unit_raw),
|
||
));
|
||
};
|
||
let rel_date = native_try!(relative_point_wall_date(vm, &point));
|
||
let Some(time_ns_base) = duration_time_nanoseconds(&values) else {
|
||
return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range"));
|
||
};
|
||
// days 并入时间(24 小时/天),整日回填到日期分量。
|
||
// duration_time_nanoseconds 已包含 days 分量(24 小时/天),直接取整日。
|
||
let time_ns_total = time_ns_base;
|
||
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 target_date = add_date_duration(rel_date, &date_parts);
|
||
|
||
// DifferenceISODateTime:date1=relativeTo(午夜)、date2=target。
|
||
let date1 = rel_date;
|
||
let mut date2 = target_date;
|
||
let mut time_ns = target_time;
|
||
let time_sign = if time_ns > 0 {
|
||
1_i128
|
||
} else if time_ns < 0 {
|
||
-1_i128
|
||
} else {
|
||
0
|
||
};
|
||
let date_sign = compare_iso_date(date1, date2);
|
||
if date_sign != 0 && date_sign == time_sign {
|
||
date2 = add_days_iso(date2, time_sign);
|
||
time_ns -= time_sign * DAY_NS;
|
||
}
|
||
let date_values = date_until_iso(date1, date2, unit_index);
|
||
let sign = {
|
||
let ds = date_duration_sign(&date_values);
|
||
if ds != 0 {
|
||
ds
|
||
} else if time_ns > 0 {
|
||
1
|
||
} else if time_ns < 0 {
|
||
-1
|
||
} else {
|
||
1
|
||
}
|
||
};
|
||
let origin_epoch = days_from_civil(date1.0, date1.1, date1.2) * DAY_NS;
|
||
let dest_epoch = days_from_civil(target_date.0, target_date.1, target_date.2) * DAY_NS + target_time;
|
||
let (r1, _, start_dur, end_dur) = nudge_window(sign, &date_values, date1, 1, unit_index, false);
|
||
let epoch_of = |dur: &[f64; 10]| -> Option<i128> {
|
||
if date_duration_sign(dur) == 0 {
|
||
return Some(origin_epoch);
|
||
}
|
||
let date = add_date_duration(date1, dur);
|
||
let days = days_from_civil(date.0, date.1, date.2);
|
||
if days.abs() > MAX_ISO_DAY {
|
||
return None;
|
||
}
|
||
Some(days * DAY_NS)
|
||
};
|
||
let Some(start_epoch) = epoch_of(&start_dur) else {
|
||
return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range"));
|
||
};
|
||
let Some(end_epoch) = epoch_of(&end_dur) else {
|
||
return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range"));
|
||
};
|
||
let numerator = dest_epoch - start_epoch;
|
||
let denominator = end_epoch - start_epoch;
|
||
if denominator == 0 {
|
||
return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range"));
|
||
}
|
||
let total = (denominator as f64 * r1 as f64 + numerator as f64 * sign as f64) / denominator as f64;
|
||
return NativeResult::Ok(JsValue::float(total));
|
||
}
|
||
|
||
// 均匀长度单位(day..nanosecond):直接按纳秒汇总。
|
||
let Some(time_ns_base) = duration_time_nanoseconds(&values) else {
|
||
return NativeResult::Err(crate::error::create_range_error(vm, "duration is out of range"));
|
||
};
|
||
// duration_time_nanoseconds 已包含 days 分量。
|
||
let total_ns = time_ns_base;
|
||
let scale = if unit_index == 3 { DAY_NS } else { UNIT_NS[unit_index - 4] };
|
||
NativeResult::Ok(JsValue::float(total_ns as f64 / scale as f64))
|
||
}
|
||
|
||
/// `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_owned(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)
|
||
}
|
||
|
||
/// `Temporal.Duration.prototype.add(other)`:分量相加并按规范平衡时间单位;
|
||
/// 任一侧含日历单位(years/months/weeks)时抛 RangeError(无 relativeTo 支持)。
|
||
pub fn duration_add<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 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. 平衡后存储时间分量(含 days,𝔽 舍入)的纳秒加权和校验 2^53 秒上限。
|
||
///
|
||
/// # 边界与前提
|
||
/// - add/subtract 无 relativeTo 选项,任一侧含 years/months/weeks 即抛 RangeError。
|
||
/// - 全零与 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 Err(crate::error::create_range_error(vm, "cannot add durations with calendar units"));
|
||
}
|
||
// 最大单位取 receiver 与参数中最大的非零时间单位(days=3 最大,ns=9 最小)。
|
||
let mut largest = 9_usize;
|
||
for index in 3..10 {
|
||
if receiver[index] != 0.0 || other[index] != 0.0 {
|
||
largest = index;
|
||
break;
|
||
}
|
||
}
|
||
if largest == 9 && receiver[9] + other[9] == 0.0 {
|
||
return Ok([0.0; 10]);
|
||
}
|
||
// 分量按精确整数求和(f64 分量是精确整数;超过 2^53 的和需在 i128 上保持精确,规范按数学值计算)。
|
||
let mut total_ns = 0_i128;
|
||
const SUM_SCALES: [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];
|
||
for (index, scale) in (3..10).zip(SUM_SCALES) {
|
||
let Some(a) = duration_component_integer(receiver[index]) else {
|
||
return Err(crate::error::create_range_error(vm, "invalid duration"));
|
||
};
|
||
let Some(b) = duration_component_integer(other[index]) else {
|
||
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 Err(crate::error::create_range_error(vm, "duration is out of range"));
|
||
};
|
||
let Some(updated) = total_ns.checked_add(component) else {
|
||
return Err(crate::error::create_range_error(vm, "duration is out of range"));
|
||
};
|
||
total_ns = updated;
|
||
}
|
||
// 从总纳秒向下按截断除法分解,进位到 largest 为止(largest 单位可无界)。
|
||
let mut values = [0.0; 10];
|
||
let mut rem = total_ns;
|
||
let mut unit = 9_usize;
|
||
loop {
|
||
if unit == largest {
|
||
values[unit] = rem as f64;
|
||
break;
|
||
}
|
||
let base = match unit {
|
||
7..=9 => 1_000,
|
||
6 | 5 => 60,
|
||
_ => 24,
|
||
};
|
||
values[unit] = (rem % base) as f64;
|
||
rem /= base;
|
||
unit -= 1;
|
||
}
|
||
// 范围校验:判据作用在存储分量(平衡后 𝔽 舍入结果)的纳秒加权和上,而非输入
|
||
// 精确和——内部槽位按 float64 可表示整数存储,转换可有损,舍入后分量可能越界;
|
||
// 加权和同时覆盖逐分量检查的盲区(总和落在 [2^53 s, 2^53 s + 86400 s) 而各分量
|
||
// 均低于上限的缝隙带)。
|
||
const MAX_TIME_NANOSECONDS: i128 = (1_i128 << 53) * 1_000_000_000;
|
||
let Some(stored_ns) = duration_time_nanoseconds(&values) else {
|
||
return Err(crate::error::create_range_error(vm, "invalid duration"));
|
||
};
|
||
if stored_ns.abs() >= MAX_TIME_NANOSECONDS {
|
||
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.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))
|
||
}
|
||
|
||
/// DefaultTemporalLargestUnit(规范 7.5.17):首个非零分量索引(全零视为 nanosecond)。
|
||
pub(crate) fn default_temporal_largest_unit(values: &[f64; 10]) -> usize {
|
||
for (index, value) in values.iter().enumerate() {
|
||
if *value != 0.0 {
|
||
return index;
|
||
}
|
||
}
|
||
9
|
||
}
|
||
|
||
/// TemporalDurationFromInternal(规范 7.5.8):日期部分直传,时间部分按 largestUnit 平衡。
|
||
///
|
||
/// # 步骤
|
||
/// 1. largestUnit 为日期类(day 及以上):时间全量分解,days 折回日期部分。
|
||
/// 2. largestUnit 为时间类:从该单位向下分解。
|
||
pub(crate) fn internal_duration_to_values(internal: &InternalDuration, largest: usize) -> [f64; 10] {
|
||
let mut values = [0.0; 10];
|
||
values[0..4].copy_from_slice(&internal.date);
|
||
let sign = if internal.time_ns < 0 { -1 } else { 1 };
|
||
if largest <= 3 {
|
||
let mut remainder = internal.time_ns.unsigned_abs();
|
||
let hours = remainder / 3_600_000_000_000;
|
||
remainder %= 3_600_000_000_000;
|
||
let minutes = remainder / 60_000_000_000;
|
||
remainder %= 60_000_000_000;
|
||
let seconds = remainder / 1_000_000_000;
|
||
remainder %= 1_000_000_000;
|
||
let milliseconds = remainder / 1_000_000;
|
||
remainder %= 1_000_000;
|
||
let microseconds = remainder / 1_000;
|
||
let nanoseconds = remainder % 1_000;
|
||
let days = hours / 24;
|
||
values[3] = internal.date[3] + sign as f64 * days as f64;
|
||
values[4] = (hours % 24) as f64 * sign as f64;
|
||
values[5] = minutes as f64 * sign as f64;
|
||
values[6] = seconds as f64 * sign as f64;
|
||
values[7] = milliseconds as f64 * sign as f64;
|
||
values[8] = microseconds as f64 * sign as f64;
|
||
values[9] = nanoseconds as f64 * sign as f64;
|
||
} else {
|
||
let time_values = balance_instant_difference(internal.time_ns, largest - 4).unwrap_or([0.0; 10]);
|
||
values[4..10].copy_from_slice(&time_values[4..10]);
|
||
}
|
||
values
|
||
}
|
||
|
||
/// `Temporal.Duration.compare(one, two, options)`:按相对点比较两个时长。
|
||
///
|
||
/// # 步骤
|
||
/// 1. one/two 各自 duration_like_values 归一(读序 one → two → options)。
|
||
/// 2. options.relativeTo 经 duration_relative_to_date 归一(含 ZDT/bag 分支)。
|
||
/// 3. 无 relativeTo 且无日历单位:按 duration_time_nanoseconds 直接比大小(fast path)。
|
||
/// 4. 无 relativeTo 但含日历单位:抛 RangeError,日历单位无法脱离具体相对点折算。
|
||
/// 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:相等时长无论是否含日历单位,应用任意相对点结果都相同,
|
||
// 无需读取 options.relativeTo。
|
||
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(point) => {
|
||
// Zoned 载荷且任一支最大单位为日期类:epoch 级比较(门是 OR)。
|
||
// 两支均纯时间时落回 24h 墙钟路径(与规范 plain 路径一致)。
|
||
if let RelativePoint::Zoned { epoch_ns, time_zone, calendar } = &point {
|
||
let largest1 = default_temporal_largest_unit(&one);
|
||
let largest2 = default_temporal_largest_unit(&two);
|
||
if largest1 <= 3 || largest2 <= 3 {
|
||
let after1 = native_try!(add_zoned_datetime(vm, *epoch_ns, time_zone, calendar, &one, true));
|
||
let after2 = native_try!(add_zoned_datetime(vm, *epoch_ns, time_zone, calendar, &two, true));
|
||
return NativeResult::Ok(JsValue::int(match after1.cmp(&after2) {
|
||
std::cmp::Ordering::Less => -1,
|
||
std::cmp::Ordering::Equal => 0,
|
||
std::cmp::Ordering::Greater => 1,
|
||
}));
|
||
}
|
||
}
|
||
// 有 relativeTo:各 duration 应用后先比日期再比时间。
|
||
let rel = native_try!(relative_point_wall_date(vm, &point));
|
||
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 };
|
||
native_try!(ensure_duration(vm, obj));
|
||
let round_to = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) };
|
||
if round_to.is_undefined() {
|
||
return NativeResult::Err(crate::error::create_type_error(vm, "options parameter is required"));
|
||
}
|
||
|
||
let values = duration_values(obj);
|
||
// 现有最大单位:首个非零分量;全零时视为 nanosecond。
|
||
let mut existing_largest = 9_usize;
|
||
for (index, value) in values.iter().enumerate() {
|
||
if *value != 0.0 {
|
||
existing_largest = index;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// roundTo 字符串 => { smallestUnit: 字符串 };否则必须为对象。
|
||
// largestUnit/relativeTo 先于 increment 读取(读序钉死);其余三字段走统一 helper。
|
||
let (largest_raw, relative_raw) = if round_to.is_string() {
|
||
(JsValue::undefined(), JsValue::undefined())
|
||
} else {
|
||
if !round_to.is_object() {
|
||
return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be an object"));
|
||
}
|
||
let options_ptr = round_to.as_js_object_ptr();
|
||
if options_ptr.is_null() {
|
||
return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be an object"));
|
||
}
|
||
let options = unsafe { &*options_ptr };
|
||
(
|
||
native_try!(temporal_option_value(vm, options, round_to, "largestUnit")),
|
||
native_try!(temporal_option_value(vm, options, round_to, "relativeTo")),
|
||
)
|
||
};
|
||
let (increment, mode, smallest_unit) = native_try!(parse_round_options(vm, round_to));
|
||
|
||
// largestUnit:允许 "auto";缺失/undefined 视为未提供。
|
||
let largest_provided = !largest_raw.is_undefined();
|
||
let largest_index = if largest_raw.is_undefined() {
|
||
None
|
||
} else {
|
||
let unit = native_try!(temporal_option_string(vm, largest_raw));
|
||
if unit == "auto" {
|
||
None
|
||
} else {
|
||
match plain_date_time_unit_index(&unit) {
|
||
Some(index) => Some(index),
|
||
None => {
|
||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid largestUnit"));
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
// smallestUnit:缺失时 nanosecond。
|
||
let smallest_provided = smallest_unit.is_some();
|
||
let smallest_index = match smallest_unit {
|
||
Some(unit) => match plain_date_time_unit_index(&unit) {
|
||
Some(index) => index,
|
||
None => return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallestUnit")),
|
||
},
|
||
None => 9,
|
||
};
|
||
|
||
// 默认最大单位:现有最大单位与最小单位中较大(索引较小)的那个。
|
||
let default_largest = if existing_largest < smallest_index {
|
||
existing_largest
|
||
} else {
|
||
smallest_index
|
||
};
|
||
let largest = largest_index.unwrap_or(default_largest);
|
||
|
||
// 至少一个单位需要显式提供;largest 不能小于 smallest。
|
||
if !smallest_provided && !largest_provided {
|
||
return NativeResult::Err(crate::error::create_range_error(
|
||
vm,
|
||
"at least one of smallestUnit or largestUnit is required",
|
||
));
|
||
}
|
||
if smallest_index < largest {
|
||
return NativeResult::Err(crate::error::create_range_error(
|
||
vm,
|
||
"largestUnit cannot be smaller than smallestUnit",
|
||
));
|
||
}
|
||
|
||
// 舍入增量上限:单位越小可除以上一级;日历单位(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"));
|
||
}
|
||
// 日历单位的 increment 要求 largestUnit 与 smallestUnit 相同(防止窗口舍入歧义)。
|
||
if increment > 1 && smallest_index == 3 && largest != smallest_index {
|
||
return NativeResult::Err(crate::error::create_range_error(
|
||
vm,
|
||
"rounding increments of calendar units require largestUnit to equal smallestUnit",
|
||
));
|
||
}
|
||
|
||
// relativeTo 仅在日历路径需要时读取(解析失败直接抛错)。
|
||
let relative_date = match duration_relative_to_date(vm, relative_raw) {
|
||
Ok(relative) => relative,
|
||
Err(error) => return NativeResult::Err(error),
|
||
};
|
||
|
||
// 日历单位存在或目标为日历单位时走 relativeTo 路径,否则走纯时间路径。
|
||
// Zoned 相对点无条件走 zoned 路径(DST 感知的日长,24h 路径失准)。
|
||
let needs_relative = values[..3].iter().any(|value| *value != 0.0)
|
||
|| largest < 3
|
||
|| smallest_index < 3
|
||
|| matches!(&relative_date, Some(RelativePoint::Zoned { .. }));
|
||
// Zoned 载荷:AddZonedDateTime → DifferenceZonedDateTimeWithRounding(门与单位无关)。
|
||
if let Some(RelativePoint::Zoned { epoch_ns, time_zone, calendar }) = &relative_date {
|
||
let target = native_try!(add_zoned_datetime(vm, *epoch_ns, time_zone, calendar, &values, true));
|
||
let pair = ZonedPair {
|
||
ns1: *epoch_ns,
|
||
ns2: target,
|
||
time_zone,
|
||
calendar,
|
||
};
|
||
let settings = DifferenceSettings {
|
||
largest_index: largest,
|
||
smallest_index,
|
||
increment,
|
||
mode,
|
||
};
|
||
let internal = native_try!(difference_zoned_datetime_with_rounding_vm(vm, pair, settings));
|
||
// largestUnit 为日期类时降为 hour。
|
||
let largest = if largest <= 3 { 4 } else { largest };
|
||
let result = internal_duration_to_values(&internal, largest);
|
||
return make_duration(vm, result);
|
||
}
|
||
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;
|
||
}
|
||
}
|
||
// 范围校验:判据作用在存储分量(平衡后 𝔽 舍入结果)的纳秒加权和上,与
|
||
// add/subtract 同式;加权和覆盖逐分量检查的盲区(总和落在 [2^53 s, 2^53 s +
|
||
// 86400 s) 而各分量均低于上限的缝隙带)。
|
||
const MAX_TIME_NANOSECONDS: i128 = (1_i128 << 53) * 1_000_000_000;
|
||
let Some(stored_ns) = duration_time_nanoseconds(&result) else {
|
||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid duration"));
|
||
};
|
||
if stored_ns.abs() >= 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(point) = relative_date else {
|
||
return NativeResult::Err(crate::error::create_range_error(
|
||
vm,
|
||
"a starting point is required for rounding calendar units",
|
||
));
|
||
};
|
||
let rel = native_try!(relative_point_wall_date(vm, &point));
|
||
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 start = IsoDateTime { date: rel, time_ns: 0 };
|
||
let end_dt = IsoDateTime {
|
||
date: end,
|
||
time_ns: target_time,
|
||
};
|
||
let result = match nudge_iso_difference(vm, start, end_dt, settings, false, 9) {
|
||
Ok(values) => values,
|
||
Err(error) => return NativeResult::Err(error),
|
||
};
|
||
make_duration(vm, result)
|
||
}
|
||
|
||
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);
|