feat: 新增 PlainMonthDay 算术与 Instant 转换方法

This commit is contained in:
2026-10-15 13:32:44 +00:00
parent 53def9d0af
commit 44b1a6bb03
6 changed files with 359 additions and 28 deletions
@@ -2,6 +2,7 @@
use chrono::Utc;
use num_traits::ToPrimitive;
use oxide_kernel::shape_forge::EMPTY_SHAPE_ID;
use oxide_runtime_api::{to_number, to_string, NativeResult, ProtoKind, VmHost};
use oxide_types::object::JsObject;
use oxide_types::value::JsValue;
@@ -937,6 +938,71 @@ pub fn instant_to_zoned_date_time_iso<H: VmHost>(vm: &mut H, args: &[u8]) -> Nat
make_zoned_date_time(vm, epoch_ns, &time_zone_id, "iso8601")
}
/// ToNumeric 语义的纪元纳秒取值:number 须为有限整数(否则 RangeError),
/// BigInt 直读 i128;其余原始值 → TypeError。
fn to_numeric_epoch_ns<H: VmHost>(vm: &mut H, value: JsValue) -> Result<i128, JsValue> {
if value.is_number() {
let number = to_number(value);
if !number.is_finite() || number.fract() != 0.0 {
return Err(crate::error::create_range_error(vm, "invalid epochNanoseconds"));
}
return Ok(number as i128);
}
if value.is_bigint() {
return Ok(vm.bigint_value(value).to_i128().unwrap_or(i128::MAX));
}
Err(crate::error::create_type_error(vm, "invalid epochNanoseconds"))
}
/// `Temporal.Instant.prototype.with(properties)`:properties.epochNanoseconds
/// (number 或 BigInt)须过 Instant 范围,返回新 Instant;options 不读取。
///
/// # 步骤
/// 1. receiver branding(TypeError)。
/// 2. properties 非对象 → TypeError。
/// 3. Get epochNanoseconds:undefined → TypeError;ToNumeric 取值。
/// 4. 绝对值超 MAX_INSTANT_NS → RangeError;make_instant 重建。
pub fn instant_with<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_instant(vm, obj));
let properties = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) };
if !properties.is_object() {
return NativeResult::Err(crate::error::create_type_error(vm, "properties must be an object"));
}
let pptr = properties.as_js_object_ptr();
if pptr.is_null() {
return NativeResult::Err(crate::error::create_type_error(vm, "properties must be an object"));
}
let properties_obj = unsafe { &*pptr };
let raw = native_try!(temporal_option_value(vm, properties_obj, properties, "epochNanoseconds"));
if raw.is_undefined() {
return NativeResult::Err(crate::error::create_type_error(vm, "epochNanoseconds is required"));
}
let epoch_ns = native_try!(to_numeric_epoch_ns(vm, raw));
if epoch_ns.unsigned_abs() > MAX_INSTANT_NS as u128 {
return NativeResult::Err(crate::error::create_range_error(vm, "Instant outside supported range"));
}
make_instant(vm, epoch_ns)
}
/// `Temporal.Instant.prototype.toDate()`:纪元纳秒按毫秒取整(floor)后
/// 产出 Date 对象(同一时刻,亚毫秒精度丢失,与 epochMilliseconds getter 同口径)。
pub fn instant_to_date<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_instant(vm, obj));
let Some(epoch_ns) = get_instant_epoch_ns(obj) else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid Instant"));
};
let epoch_ms = epoch_ns.div_euclid(1_000_000) as f64;
let mut date =
JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(vm.builtin_proto(ProtoKind::DateProto)));
date.type_tag = JsObject::OBJ_TYPE_DATE;
date.set_prop_at(0, JsValue::float(epoch_ms));
NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(date)))
}
/// `Temporal.Instant.prototype.valueOf()`:Temporal 对象禁止转原始值。
pub fn instant_value_of<H: VmHost>(vm: &mut H, _args: &[u8]) -> NativeResult {
NativeResult::Err(crate::error::create_type_error(vm, "Temporal.Instant has no valueOf"))
@@ -6,12 +6,12 @@ use oxide_types::value::JsValue;
use super::common::native_try;
use super::{
calendar_annotation, days_from_civil, days_in_month_iso, ensure_plain_month_day, format_iso_year, get_calendar_id,
get_double_prop, initialize_temporal_receiver, is_ctor_call, make_plain_date, make_plain_month_day,
month_day_bag_calendar_id, read_iso2, receiver_obj, reject_partial_object_with_calendar_or_time_zone,
strip_iso_offset, temporal_calendar_id_strict, temporal_number_component, temporal_option_string,
temporal_option_value, temporal_overflow, temporal_to_show_calendar, valid_iso_date, valid_plain_time,
ShowCalendar,
calendar_annotation, days_from_civil, days_in_month_iso, duration_like_values, ensure_plain_month_day,
format_iso_year, get_calendar_id, get_double_prop, initialize_temporal_receiver, is_ctor_call,
make_plain_date, make_plain_month_day, month_day_bag_calendar_id, parse_round_options, read_iso2, receiver_obj,
reject_partial_object_with_calendar_or_time_zone, strip_iso_offset, temporal_calendar_id_strict,
temporal_number_component, temporal_option_string, temporal_option_value, temporal_overflow,
temporal_to_show_calendar, valid_iso_date, valid_plain_time, ShowCalendar,
};
/// 读 PlainMonthDay 的 月/日/参考年 三元组(槽 0-2),含 receiver 校验。
@@ -746,3 +746,109 @@ pub fn plain_month_day_to_plain_date<H: VmHost>(vm: &mut H, args: &[u8]) -> Nati
let calendar = get_calendar_id(obj, 3);
make_plain_date(vm, year, month, day, &calendar)
}
// ==================== PlainMonthDay add/subtract/round ====================
/// `Temporal.PlainMonthDay.prototype.add/subtract(durationLike [, options])` 核心。
///
/// # 步骤
/// 1. receiver branding(TypeError)。
/// 2. ToTemporalDuration(串 / bag / 实例;TypeError、RangeError 各自语义)。
/// 3. 读 overflow(缺省 constrain)。
/// 4. years / weeks / days / 时间分量任一非零 → RangeError(PMD 无年与日分量)。
/// 5. (年, 月) 按参考年(槽 2)折算绝对月数后平衡,日按 overflow 规整
/// (constrain 钳到目标年月长,reject 非法即抛)。
/// 6. 结果 (年, 月, 日) 过 ISODateWithinLimits(day 级)→ 越界 RangeError。
///
/// # 边界与副作用
/// - 结果参考年为算术后的年,日历标识取自 receiver 槽 3。
/// - subtract 与 add 共用本函数,sign 参数取反月分量。
fn plain_month_day_apply_duration<H: VmHost>(vm: &mut H, args: &[u8], sign: i64) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_month_day(vm, obj));
let month = get_double_prop(obj, 0) as i64;
let day = get_double_prop(obj, 1) as i64;
let ref_year = get_double_prop(obj, 2) as i64;
let calendar = get_calendar_id(obj, 3);
let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let values = native_try!(duration_like_values(vm, val));
let constrain = native_try!(temporal_overflow(vm, args));
// PMD 无年分量:years 与 weeks/days/时间分量均须为零。
if values[0] != 0.0 || values[2..].iter().any(|value| *value != 0.0) {
return NativeResult::Err(crate::error::create_range_error(
vm,
"month-day cannot add years, weeks, days, or time units",
));
}
// BalanceISOYearMonth:月折算绝对月数后平衡(days 恒 0,日不参与)。
let months = sign * values[1] as i64;
let absolute = ref_year * 12 + month - 1 + months;
let year = absolute.div_euclid(12);
let month = absolute.rem_euclid(12) + 1;
// RegulateISODate:constrain 钳日到目标年月长,reject 非法即抛。
let day = if constrain {
day.min(days_in_month_iso(year as i32, month as u32) as i64)
} else {
if !valid_iso_date(year as i32, month as u32, day as u32) {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO date"));
}
day
};
// ISODateWithinLimits(day 级):-100_000_001..=100_000_000。
let day_count = days_from_civil(i128::from(year), i128::from(month), i128::from(day));
if !(-100_000_001..=100_000_000).contains(&day_count) {
return NativeResult::Err(crate::error::create_range_error(vm, "month-day is out of range"));
}
make_plain_month_day(vm, month as u32, day as u32, year as i32, &calendar)
}
/// `Temporal.PlainMonthDay.prototype.add(durationLike)`。
pub fn plain_month_day_add<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_month_day_apply_duration(vm, args, 1)
}
/// `Temporal.PlainMonthDay.prototype.subtract(durationLike)`。
pub fn plain_month_day_subtract<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_month_day_apply_duration(vm, args, -1)
}
/// `Temporal.PlainMonthDay.prototype.round(roundTo)`:smallestUnit 仅 "day",
/// 增量须为 1(日已是整数,舍入为恒等),日钳到参考年月长后过表示范围。
///
/// # 步骤
/// 1. receiver 品牌校验;读槽 0-3 入本地(参考年与日历槽继承 receiver)。
/// 2. roundTo 经 parse_round_options(undefined → TypeError;字符串简写;对象固定读序)。
/// 3. smallestUnit 缺失 → RangeError;非 day → RangeError;增量 > 1 → RangeError。
/// 4. 日钳到参考年月长(constrain 语义);valid_iso_date 与 day 级表示范围检查。
/// 5. make_plain_month_day 重建(参考年与日历槽继承 receiver)。
pub fn plain_month_day_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_month_day(vm, obj));
let month = get_double_prop(obj, 0) as u32;
let day = get_double_prop(obj, 1) as u32;
let ref_year = get_double_prop(obj, 2) as i32;
let calendar = get_calendar_id(obj, 3);
let round_to = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let (increment, _mode, unit_value) = native_try!(parse_round_options(vm, round_to));
let Some(unit_value) = unit_value else {
return NativeResult::Err(crate::error::create_range_error(vm, "smallestUnit is required"));
};
if unit_value != "day" && unit_value != "days" {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallest unit"));
}
if increment > 1 {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment"));
}
// 日已是整数且增量恒 1,舍入为恒等;钳到参考年月长。
let day = day.min(days_in_month_iso(ref_year, month));
if !valid_iso_date(ref_year, month, day) {
return NativeResult::Err(crate::error::create_range_error(vm, "month-day is out of range"));
}
let day_count = days_from_civil(i128::from(ref_year), i128::from(month), i128::from(day));
if !(-100_000_001..=100_000_000).contains(&day_count) {
return NativeResult::Err(crate::error::create_range_error(vm, "month-day is out of range"));
}
make_plain_month_day(vm, month, day, ref_year, &calendar)
}