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
Generated
+21 -21
View File
@@ -1160,7 +1160,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_builtins" name = "oxide_builtins"
version = "0.19.13" version = "0.19.14"
dependencies = [ dependencies = [
"chrono", "chrono",
"fast-float", "fast-float",
@@ -1185,7 +1185,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_bytecode" name = "oxide_bytecode"
version = "0.19.13" version = "0.19.14"
dependencies = [ dependencies = [
"num-bigint", "num-bigint",
"oxide_log", "oxide_log",
@@ -1193,7 +1193,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_cfg" name = "oxide_cfg"
version = "0.19.13" version = "0.19.14"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_emit", "oxide_emit",
@@ -1204,7 +1204,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_cli" name = "oxide_cli"
version = "0.19.13" version = "0.19.14"
dependencies = [ dependencies = [
"ansi_term", "ansi_term",
"bumpalo", "bumpalo",
@@ -1228,7 +1228,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_code_cache" name = "oxide_code_cache"
version = "0.19.13" version = "0.19.14"
dependencies = [ dependencies = [
"lru", "lru",
"oxide_bytecode", "oxide_bytecode",
@@ -1237,7 +1237,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_compiler" name = "oxide_compiler"
version = "0.19.13" version = "0.19.14"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_cfg", "oxide_cfg",
@@ -1258,7 +1258,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_dce" name = "oxide_dce"
version = "0.19.13" version = "0.19.14"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_cfg", "oxide_cfg",
@@ -1272,7 +1272,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_emit" name = "oxide_emit"
version = "0.19.13" version = "0.19.14"
dependencies = [ dependencies = [
"num-bigint", "num-bigint",
"oxc_allocator", "oxc_allocator",
@@ -1288,7 +1288,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_engine" name = "oxide_engine"
version = "0.19.13" version = "0.19.14"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_compiler", "oxide_compiler",
@@ -1300,7 +1300,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_fold" name = "oxide_fold"
version = "0.19.13" version = "0.19.14"
dependencies = [ dependencies = [
"num-bigint", "num-bigint",
"oxide_bytecode", "oxide_bytecode",
@@ -1312,7 +1312,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_ir" name = "oxide_ir"
version = "0.19.13" version = "0.19.14"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_log", "oxide_log",
@@ -1321,7 +1321,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_kernel" name = "oxide_kernel"
version = "0.19.13" version = "0.19.14"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"dashmap", "dashmap",
@@ -1335,7 +1335,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_liveness" name = "oxide_liveness"
version = "0.19.13" version = "0.19.14"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_cfg", "oxide_cfg",
@@ -1347,7 +1347,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_log" name = "oxide_log"
version = "0.19.13" version = "0.19.14"
dependencies = [ dependencies = [
"tracing", "tracing",
"tracing-appender", "tracing-appender",
@@ -1356,11 +1356,11 @@ dependencies = [
[[package]] [[package]]
name = "oxide_net" name = "oxide_net"
version = "0.19.13" version = "0.19.14"
[[package]] [[package]]
name = "oxide_parser" name = "oxide_parser"
version = "0.19.13" version = "0.19.14"
dependencies = [ dependencies = [
"oxc_allocator", "oxc_allocator",
"oxc_ast", "oxc_ast",
@@ -1375,7 +1375,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_regalloc" name = "oxide_regalloc"
version = "0.19.13" version = "0.19.14"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_cfg", "oxide_cfg",
@@ -1391,7 +1391,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_runtime_api" name = "oxide_runtime_api"
version = "0.19.13" version = "0.19.14"
dependencies = [ dependencies = [
"num-bigint", "num-bigint",
"num-traits", "num-traits",
@@ -1404,7 +1404,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_test262" name = "oxide_test262"
version = "0.19.13" version = "0.19.14"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_compiler", "oxide_compiler",
@@ -1424,7 +1424,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_types" name = "oxide_types"
version = "0.19.13" version = "0.19.14"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"dashmap", "dashmap",
@@ -1437,7 +1437,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_vm" name = "oxide_vm"
version = "0.19.13" version = "0.19.14"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"chrono", "chrono",
+1 -1
View File
@@ -25,7 +25,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "0.19.13" version = "0.19.14"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
rust-version = "1.80" rust-version = "1.80"
@@ -2,6 +2,7 @@
use chrono::Utc; use chrono::Utc;
use num_traits::ToPrimitive; 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_runtime_api::{to_number, to_string, NativeResult, ProtoKind, VmHost};
use oxide_types::object::JsObject; use oxide_types::object::JsObject;
use oxide_types::value::JsValue; 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") 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 对象禁止转原始值。 /// `Temporal.Instant.prototype.valueOf()`:Temporal 对象禁止转原始值。
pub fn instant_value_of<H: VmHost>(vm: &mut H, _args: &[u8]) -> NativeResult { 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")) 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::common::native_try;
use super::{ use super::{
calendar_annotation, days_from_civil, days_in_month_iso, ensure_plain_month_day, format_iso_year, get_calendar_id, calendar_annotation, days_from_civil, days_in_month_iso, duration_like_values, ensure_plain_month_day,
get_double_prop, initialize_temporal_receiver, is_ctor_call, make_plain_date, make_plain_month_day, format_iso_year, get_calendar_id, get_double_prop, initialize_temporal_receiver, is_ctor_call,
month_day_bag_calendar_id, read_iso2, receiver_obj, reject_partial_object_with_calendar_or_time_zone, make_plain_date, make_plain_month_day, month_day_bag_calendar_id, parse_round_options, read_iso2, receiver_obj,
strip_iso_offset, temporal_calendar_id_strict, temporal_number_component, temporal_option_string, reject_partial_object_with_calendar_or_time_zone, strip_iso_offset, temporal_calendar_id_strict,
temporal_option_value, temporal_overflow, temporal_to_show_calendar, valid_iso_date, valid_plain_time, temporal_number_component, temporal_option_string, temporal_option_value, temporal_overflow,
ShowCalendar, temporal_to_show_calendar, valid_iso_date, valid_plain_time, ShowCalendar,
}; };
/// 读 PlainMonthDay 的 月/日/参考年 三元组(槽 0-2),含 receiver 校验。 /// 读 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); let calendar = get_calendar_id(obj, 3);
make_plain_date(vm, year, month, day, &calendar) 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)
}
@@ -149,6 +149,8 @@ pub fn bind_temporal(env: BindEnv<'_>, global: &mut JsObject) {
), ),
("until", oxide_builtins::temporal::instant_until::<crate::vm::Vm> as *const (), 1), ("until", oxide_builtins::temporal::instant_until::<crate::vm::Vm> as *const (), 1),
("valueOf", oxide_builtins::temporal::instant_value_of::<crate::vm::Vm> as *const (), 0), ("valueOf", oxide_builtins::temporal::instant_value_of::<crate::vm::Vm> as *const (), 0),
("with", oxide_builtins::temporal::instant_with::<crate::vm::Vm> as *const (), 1),
("toDate", oxide_builtins::temporal::instant_to_date::<crate::vm::Vm> as *const (), 0),
], ],
); );
bind_well_known_data_property( bind_well_known_data_property(
@@ -1063,6 +1065,13 @@ pub fn bind_temporal(env: BindEnv<'_>, global: &mut JsObject) {
oxide_builtins::temporal::plain_month_day_to_plain_date::<crate::vm::Vm> as *const (), oxide_builtins::temporal::plain_month_day_to_plain_date::<crate::vm::Vm> as *const (),
1, 1,
), ),
("add", oxide_builtins::temporal::plain_month_day_add::<crate::vm::Vm> as *const (), 1),
(
"subtract",
oxide_builtins::temporal::plain_month_day_subtract::<crate::vm::Vm> as *const (),
1,
),
("round", oxide_builtins::temporal::plain_month_day_round::<crate::vm::Vm> as *const (), 1),
], ],
); );
bind_well_known_data_property( bind_well_known_data_property(
@@ -412,6 +412,61 @@ fn instant_from_invalid_throws_range_error() {
assert_eq!(str_val(&vm, r), "RangeError"); assert_eq!(str_val(&vm, r), "RangeError");
} }
#[test]
fn instant_with_epoch_nanoseconds() {
let mut vm = Vm::new();
// with:epochNanoseconds 按 ToNumeric 取值(number 须有限整数、BigInt 直读);
// 越界 → RangeError;缺失/非对象 → TypeError;非整数/NaN → RangeError;
// 边界值 ±MAX_INSTANT_NS 合法。
let r = eval(
&mut vm,
"(() => {
const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } };
const i = Temporal.Instant.fromEpochNanoseconds(0n);
return [
i.with({epochNanoseconds: 123456789n}).epochNanoseconds,
i.with({epochNanoseconds: 5000000}).epochNanoseconds,
kind(() => i.with({epochNanoseconds: 8640000000000000000001n})),
kind(() => i.with({epochNanoseconds: -8640000000000000000001n})),
i.with({epochNanoseconds: 8640000000000000000000n}).epochNanoseconds,
i.with({epochNanoseconds: -8640000000000000000000n}).epochNanoseconds,
kind(() => i.with({})),
kind(() => i.with(5)),
kind(() => i.with({epochNanoseconds: 1.5})),
kind(() => i.with({epochNanoseconds: NaN})),
kind(() => i.with({epochNanoseconds: '1'})),
].join('|');
})()",
)
.unwrap();
assert_eq!(
str_val(&vm, r),
"123456789|5000000|RangeError|RangeError|8640000000000000000000|-8640000000000000000000|TypeError|TypeError|RangeError|RangeError|TypeError"
);
}
#[test]
fn instant_to_date() {
let mut vm = Vm::new();
// toDate:纳秒按毫秒向下取整(与 epochMilliseconds getter 同口径),
// 负值向 -Inf 取整;结果是真实 Date(instanceof + getTime)。
let r = eval(
&mut vm,
"(() => {
const d = Temporal.Instant.fromEpochNanoseconds(1234567890123456789n).toDate();
return [
d instanceof Date,
d.getTime(),
Temporal.Instant.fromEpochNanoseconds(-1n).toDate().getTime(),
Temporal.Instant.fromEpochNanoseconds(-1000001n).toDate().getTime(),
Temporal.Instant.fromEpochNanoseconds(1000000000n).toDate().getTime(),
].join('|');
})()",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true|1234567890123|-1|-2|1000");
}
// -- Temporal.PlainDate -- // -- Temporal.PlainDate --
#[test] #[test]
@@ -3166,6 +3221,101 @@ fn plain_month_day_to_plain_date_matrix() {
); );
} }
#[test]
fn plain_month_day_add_balance_and_clamp() {
let mut vm = Vm::new();
// add:月按(参考年, 月)平衡,负年经 div_euclid 处理;日钳到目标年月长
// (constrain 缺省,reject 非法即抛);years/weeks/days/时间分量 → RangeError;
// 无参 → TypeError;day 级表示范围 ±100_000_00x。
let r = eval(
&mut vm,
"(() => {
const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } };
const md = Temporal.PlainMonthDay.from('01-31');
return [
md.add('P1M').toString(),
kind(() => md.add('P1M', {overflow: 'reject'})),
Temporal.PlainMonthDay.from('02-29').add('P1M').toString(),
Temporal.PlainMonthDay.from('12-15').add('P1M').toString(),
Temporal.PlainMonthDay.from('01-15').add('P13M').toString(),
Temporal.PlainMonthDay.from('01-15').add({months: -1}).toString(),
new Temporal.PlainMonthDay(1, 15, 'iso8601', -1).add({months: -1}).toString(),
Temporal.PlainMonthDay.from('01-15').add({months: 1}).toString(),
kind(() => Temporal.PlainMonthDay.from('01-15').add('P1Y')),
kind(() => Temporal.PlainMonthDay.from('01-15').add('P1W')),
kind(() => Temporal.PlainMonthDay.from('01-15').add('P1D')),
kind(() => Temporal.PlainMonthDay.from('01-15').add('PT1H')),
kind(() => Temporal.PlainMonthDay.from('01-15').add()),
kind(() => new Temporal.PlainMonthDay(10, 31, 'iso8601', 275760).add('P1M')),
].join('|');
})()",
)
.unwrap();
assert_eq!(
str_val(&vm, r),
"02-29|RangeError|03-29|01-15|02-15|12-15|12-15|02-15|RangeError|RangeError|RangeError|RangeError|TypeError|RangeError"
);
}
#[test]
fn plain_month_day_subtract_balance_and_clamp() {
let mut vm = Vm::new();
// subtract:月分量反向(sign 取反);日钳到目标年月长;
// years/weeks/days/时间分量 → RangeError;day 级表示范围下界。
let r = eval(
&mut vm,
"(() => {
const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } };
return [
Temporal.PlainMonthDay.from('03-31').subtract('P1M').toString(),
kind(() => Temporal.PlainMonthDay.from('03-31').subtract('P1M', {overflow: 'reject'})),
Temporal.PlainMonthDay.from('01-15').subtract('P13M').toString(),
new Temporal.PlainMonthDay(1, 15, 'iso8601', -1).subtract('P1M').toString(),
kind(() => Temporal.PlainMonthDay.from('01-15').subtract('P1Y')),
kind(() => Temporal.PlainMonthDay.from('01-15').subtract('P1D')),
kind(() => Temporal.PlainMonthDay.from('01-15').subtract('PT1H')),
kind(() => new Temporal.PlainMonthDay(3, 10, 'iso8601', -271821).subtract('P1M')),
].join('|');
})()",
)
.unwrap();
assert_eq!(
str_val(&vm, r),
"02-29|RangeError|12-15|12-15|RangeError|RangeError|RangeError|RangeError"
);
}
#[test]
fn plain_month_day_round_identity_and_validation() {
let mut vm = Vm::new();
// round:日已是整数且增量恒 1,舍入为恒等;日钳到参考年月长
// (from('02-31') 串路径不校验日,可造出超长按日);smallestUnit 须 day
// (缺失/其他 → RangeError);增量 > 1 → RangeError;非串/对象 → TypeError。
let r = eval(
&mut vm,
"(() => {
const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } };
const leap = Temporal.PlainMonthDay.from('02-29');
return [
leap.round('day').toString(),
leap.round({smallestUnit: 'days'}).toString(),
Temporal.PlainMonthDay.from('02-31').round('day').toString(),
kind(() => leap.round('month')),
kind(() => leap.round({})),
kind(() => leap.round({smallestUnit: 'day', roundingIncrement: 2})),
kind(() => leap.round(5)),
kind(() => leap.round()),
leap.round('day').day,
].join('|');
})()",
)
.unwrap();
assert_eq!(
str_val(&vm, r),
"02-29|02-29|02-29|RangeError|RangeError|RangeError|TypeError|TypeError|29"
);
}
#[test] #[test]
fn plain_year_month_from_string_matrix() { fn plain_year_month_from_string_matrix() {
let mut vm = Vm::new(); let mut vm = Vm::new();