feat: Temporal PlainDate 与 PlainDateTime 增加换槽与纯拷贝方法

This commit is contained in:
2026-10-14 09:29:40 +00:00
parent db57f55085
commit 7dbb22a165
6 changed files with 354 additions and 31 deletions
@@ -9,13 +9,13 @@ use super::common::native_try;
use super::common::{
days_from_civil, days_in_month, ensure_plain_date, get_calendar_id, get_double_prop, initialize_temporal_receiver,
is_ctor_call, parse_plain_date_string, plain_date_time_object_parts, receiver_obj,
reject_partial_object_with_calendar_or_time_zone, resolve_date_time_parts, temporal_calendar_id_strict,
temporal_number_component, temporal_option_string, temporal_option_value, temporal_overflow,
zoned_date_time_plain_parts, DateTimeParts,
reject_partial_object_with_calendar_or_time_zone, resolve_date_time_parts, temporal_calendar_id,
temporal_calendar_id_strict, temporal_number_component, temporal_option_string, temporal_option_value,
temporal_overflow, zoned_date_time_plain_parts, DateTimeParts,
};
use super::difference::{difference_core, parse_difference_settings, IsoDateTime};
use super::duration::duration_like_values;
use super::make_plain_date;
use super::{make_plain_date, make_plain_date_time, make_plain_month_day, make_plain_year_month, plain_time_like_ns};
/// 校验 ISO 日期分量(month 1-12、day 按月份与闰年,不依赖 chrono 年份范围)。
pub(crate) fn valid_iso_date(year: i32, month: u32, day: u32) -> bool {
@@ -599,6 +599,89 @@ pub fn plain_date_with<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
make_plain_date(vm, year, month, day, &get_calendar_id(obj, 3))
}
/// `Temporal.PlainDate.prototype.withCalendar(calendar)`:只换日历槽,日期分量不变。
///
/// # 步骤
/// 1. receiver 品牌校验;缺参 / undefined 抛 TypeError。
/// 2. 参数经 temporal_calendar_id(宽松版):字符串走白名单或 ISO 日期(-时间)串,
/// 六类 Temporal 对象直读日历槽(不触发 getter),其他对象 / 原始值抛 TypeError。
/// 3. 读 receiver 槽 0-2 重建,日历槽替换。
///
/// # 边界与前提
/// - 非法日历串 → RangeError;日历 ID 大小写折叠。
pub fn plain_date_with_calendar<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_date(vm, obj));
let calendar_value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
if calendar_value.is_undefined() {
return NativeResult::Err(crate::error::create_type_error(vm, "calendar is required"));
}
let calendar = native_try!(temporal_calendar_id(vm, calendar_value)).unwrap_or_else(|| "iso8601".to_string());
make_plain_date(
vm,
get_double_prop(obj, 0) as i32,
get_double_prop(obj, 1) as u32,
get_double_prop(obj, 2) as u32,
&calendar,
)
}
/// `Temporal.PlainDate.prototype.toPlainDateTime(timeLike)`:日期分量 + 时间槽组合。
///
/// # 步骤
/// 1. receiver 品牌校验;读槽 0-2 与日历槽 3 入本地(plain_time_like_ns 的 bag
/// 路径有用户调用窗口,receiver 槽须先于窗口读入)。
/// 2. plain_time_like_ns 取当日纳秒(undefined → 午夜)。
/// 3. valid_plain_date_time_range 严格不等式越界抛 RangeError;make_plain_date_time 重建。
///
/// # 边界与前提
/// - 组合越 PlainDateTime 表示范围(min 午夜抛、min 加 1 纳秒过、max 全满过)→ RangeError。
/// - 日历槽继承 receiver。
pub fn plain_date_to_plain_date_time<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_date(vm, obj));
let year = get_double_prop(obj, 0) as i32;
let month = get_double_prop(obj, 1) as u32;
let day = get_double_prop(obj, 2) as u32;
let calendar = get_calendar_id(obj, 3);
let time_like = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let time_ns = native_try!(plain_time_like_ns(vm, time_like));
if !valid_plain_date_time_range(year, month, day, time_ns) {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time"));
}
make_plain_date_time(vm, year, month, day, time_ns, &calendar)
}
/// `Temporal.PlainDate.prototype.toPlainMonthDay()`:纯拷贝月/日,参考年固定 1972。
pub fn plain_date_to_plain_month_day<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_date(vm, obj));
make_plain_month_day(
vm,
get_double_prop(obj, 1) as u32,
get_double_prop(obj, 2) as u32,
1972,
&get_calendar_id(obj, 3),
)
}
/// `Temporal.PlainDate.prototype.toPlainYearMonth()`:纯拷贝年/月,参考日固定 1。
pub fn plain_date_to_plain_year_month<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_date(vm, obj));
make_plain_year_month(
vm,
get_double_prop(obj, 0) as i32,
get_double_prop(obj, 1) as u32,
1,
&get_calendar_id(obj, 3),
)
}
/// `Temporal.PlainDate.compare(a, b)`:静态比较,返回 -1/0/1。
pub fn plain_date_compare<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let a_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
@@ -11,11 +11,11 @@ use super::{
duration_like_values, ensure_plain_date_time, format_iso_year, get_calendar_id, get_double_prop,
initialize_temporal_receiver, instant_rounding_mode, is_ctor_call, is_leap_year_iso, make_plain_date,
make_plain_date_time, make_plain_time, parse_difference_settings, parse_fractional_second_digits,
parse_temporal_string_impl, plain_date_time_object_parts, plain_time_components, receiver_obj,
reject_partial_object_with_calendar_or_time_zone, resolve_date_time_parts, round_instant_ns,
temporal_calendar_id_strict, temporal_number_component, temporal_option_number, temporal_option_string,
temporal_option_value, temporal_overflow, valid_iso_date, valid_plain_date_time_range, valid_plain_time,
zoned_date_time_plain_parts, DateTimeParts, FractionalSecondDigitsInput, IsoDateTime,
parse_temporal_string_impl, plain_date_time_object_parts, plain_time_components, plain_time_like_ns,
receiver_obj, reject_partial_object_with_calendar_or_time_zone, resolve_date_time_parts, round_instant_ns,
temporal_calendar_id, temporal_calendar_id_strict, temporal_number_component, temporal_option_number,
temporal_option_string, temporal_option_value, temporal_overflow, valid_iso_date, valid_plain_date_time_range,
valid_plain_time, zoned_date_time_plain_parts, DateTimeParts, FractionalSecondDigitsInput, IsoDateTime,
};
// ───────────────────── PlainDateTime 基础方法 ─────────────────────
@@ -530,6 +530,61 @@ pub fn plain_date_time_to_plain_time<H: VmHost>(vm: &mut H, args: &[u8]) -> Nati
make_plain_time(vm, total_ns)
}
/// `Temporal.PlainDateTime.prototype.withCalendar(calendar)`:只换日历槽,日期时间分量不变。
///
/// # 步骤
/// 1. receiver 品牌校验;缺参 / undefined 抛 TypeError。
/// 2. 参数经 temporal_calendar_id(宽松版):字符串走白名单或 ISO 日期(-时间)串,
/// 六类 Temporal 对象直读日历槽(不触发 getter),其他对象 / 原始值抛 TypeError。
/// 3. 读 receiver 槽 0-3 重建,日历槽替换。
///
/// # 边界与前提
/// - 非法日历串 → RangeError;日历 ID 大小写折叠。
pub fn plain_date_time_with_calendar<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_date_time(vm, obj));
let calendar_value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
if calendar_value.is_undefined() {
return NativeResult::Err(crate::error::create_type_error(vm, "calendar is required"));
}
let calendar = native_try!(temporal_calendar_id(vm, calendar_value)).unwrap_or_else(|| "iso8601".to_string());
make_plain_date_time(
vm,
get_double_prop(obj, 0) as i32,
get_double_prop(obj, 1) as u32,
get_double_prop(obj, 2) as u32,
get_double_prop(obj, 3),
&calendar,
)
}
/// `Temporal.PlainDateTime.prototype.withPlainTime(plainTimeLike)`:只换时间槽,日期分量不变。
///
/// # 步骤
/// 1. receiver 品牌校验;读槽 0-2 与日历槽 4 入本地(plain_time_like_ns 的 bag
/// 路径有用户调用窗口,receiver 槽须先于窗口读入)。
/// 2. plain_time_like_ns 取当日纳秒(undefined → 午夜)。
/// 3. valid_plain_date_time_range 严格不等式越界抛 RangeError;make_plain_date_time 重建。
///
/// # 边界与前提
/// - 组合越 PlainDateTime 表示范围 → RangeError;日历槽继承 receiver。
pub fn plain_date_time_with_plain_time<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_date_time(vm, obj));
let year = get_double_prop(obj, 0) as i32;
let month = get_double_prop(obj, 1) as u32;
let day = get_double_prop(obj, 2) as u32;
let calendar = get_calendar_id(obj, 4);
let time_like = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let time_ns = native_try!(plain_time_like_ns(vm, time_like));
if !valid_plain_date_time_range(year, month, day, time_ns) {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time"));
}
make_plain_date_time(vm, year, month, day, time_ns, &calendar)
}
/// Temporal 对象禁止隐式转换为原始值。
pub fn plain_date_time_value_of<H: VmHost>(vm: &mut H, _args: &[u8]) -> NativeResult {
NativeResult::Err(crate::error::create_type_error(vm, "Temporal.PlainDateTime has no valueOf"))
@@ -568,6 +568,26 @@ pub fn bind_temporal(env: BindEnv<'_>, global: &mut JsObject) {
("until", oxide_builtins::temporal::plain_date_until::<crate::vm::Vm> as *const (), 1),
("since", oxide_builtins::temporal::plain_date_since::<crate::vm::Vm> as *const (), 1),
("with", oxide_builtins::temporal::plain_date_with::<crate::vm::Vm> as *const (), 1),
(
"withCalendar",
oxide_builtins::temporal::plain_date_with_calendar::<crate::vm::Vm> as *const (),
1,
),
(
"toPlainDateTime",
oxide_builtins::temporal::plain_date_to_plain_date_time::<crate::vm::Vm> as *const (),
0,
),
(
"toPlainMonthDay",
oxide_builtins::temporal::plain_date_to_plain_month_day::<crate::vm::Vm> as *const (),
0,
),
(
"toPlainYearMonth",
oxide_builtins::temporal::plain_date_to_plain_year_month::<crate::vm::Vm> as *const (),
0,
),
],
);
@@ -814,6 +834,16 @@ pub fn bind_temporal(env: BindEnv<'_>, global: &mut JsObject) {
1,
),
("with", oxide_builtins::temporal::plain_date_time_with::<crate::vm::Vm> as *const (), 1),
(
"withCalendar",
oxide_builtins::temporal::plain_date_time_with_calendar::<crate::vm::Vm> as *const (),
1,
),
(
"withPlainTime",
oxide_builtins::temporal::plain_date_time_with_plain_time::<crate::vm::Vm> as *const (),
0,
),
],
);
bind_well_known_data_property(
@@ -4179,3 +4179,158 @@ fn relativeto_bag_zoned_payload() {
.unwrap();
assert_eq!(str_val(&vm, r), "24|24|24|24|24|23|-1|24|25|25|RangeError|RangeError");
}
// -- PlainDate/PlainDateTime withCalendar / withPlainTime / toPlain* 族 --
#[test]
fn plain_date_with_calendar_inherits_slot_and_forms() {
let mut vm = Vm::new();
// 换日历槽:日期分量不变、返回新对象;大小写不敏感;ISO 日期串与纯时间串接受;
// Temporal 对象直读日历槽(不触发 getter)。
let r = eval(
&mut vm,
"const d = new Temporal.PlainDate(1976, 11, 18, 'hebrew');
const w = d.withCalendar('iso8601');
(w !== d) + '|' + w.calendarId + '|' + w.year + '|' + w.month + '|' + w.day + '|' +
d.withCalendar('iSo8601').calendarId + '|' +
d.withCalendar('2020-01-01').calendarId + '|' +
d.withCalendar('15:23').calendarId + '|' +
d.withCalendar(new Temporal.PlainMonthDay(3, 1, 'japanese')).calendarId + '|' +
d.withCalendar(new Temporal.PlainYearMonth(2020, 5, 'buddhist')).calendarId",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true|iso8601|1976|11|18|iso8601|iso8601|iso8601|japanese|buddhist");
}
#[test]
fn plain_date_with_calendar_errors() {
let mut vm = Vm::new();
// 缺参/undefined/非字符串非对象 → TypeError;非法串 → RangeError。
let r = eval(
&mut vm,
"const d = new Temporal.PlainDate(1976, 11, 18);
(() => { try { d.withCalendar(); return 'no'; }
catch (e) { return e instanceof TypeError; } })() + '|' +
(() => { try { d.withCalendar(undefined); return 'no'; }
catch (e) { return e instanceof TypeError; } })() + '|' +
(() => { try { d.withCalendar(42); return 'no'; }
catch (e) { return e instanceof TypeError; } })() + '|' +
(() => { try { d.withCalendar('notacal'); return 'no'; }
catch (e) { return e instanceof RangeError; } })()",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true|true|true|true");
}
#[test]
fn plain_date_time_with_calendar_inherits_slot_and_forms() {
let mut vm = Vm::new();
// 换日历槽:日期时间分量不变、返回新对象;日历继承 receiver 槽(hebrew)。
let r = eval(
&mut vm,
"const p = new Temporal.PlainDateTime(1976, 11, 18, 15, 23, 30, 123, 456, 789, 'hebrew');
const w = p.withCalendar('iso8601');
(w !== p) + '|' + w.calendarId + '|' + w.year + '|' + w.month + '|' + w.day + '|' +
w.hour + '|' + w.minute + '|' + w.second + '|' + w.nanosecond + '|' +
p.withCalendar('iSo8601').calendarId + '|' +
p.withCalendar(new Temporal.PlainDate(2020, 1, 1, 'islamic')).calendarId",
)
.unwrap();
assert_eq!(
str_val(&vm, r),
"true|iso8601|1976|11|18|15|23|30|789|iso8601|islamic"
);
}
#[test]
fn plain_date_time_with_calendar_errors() {
let mut vm = Vm::new();
// 缺参/undefined/非字符串非对象 → TypeError;非法串 → RangeError。
let r = eval(
&mut vm,
"const p = new Temporal.PlainDateTime(1976, 11, 18);
(() => { try { p.withCalendar(); return 'no'; }
catch (e) { return e instanceof TypeError; } })() + '|' +
(() => { try { p.withCalendar(undefined); return 'no'; }
catch (e) { return e instanceof TypeError; } })() + '|' +
(() => { try { p.withCalendar(42); return 'no'; }
catch (e) { return e instanceof TypeError; } })() + '|' +
(() => { try { p.withCalendar('notacal'); return 'no'; }
catch (e) { return e instanceof RangeError; } })()",
)
.unwrap();
assert_eq!(str_val(&vm, r), "true|true|true|true");
}
#[test]
fn plain_date_time_with_plain_time_forms_and_range() {
let mut vm = Vm::new();
// 缺参/undefined → 午夜;bag 缺省字段 0、second=60 钳制 59;字符串 / PlainTime /
// ZDT(自身时区本地时间)形态;空袋 TypeError;组合越范围 RangeError;
// 日历继承 receiver 槽。
let r = eval(
&mut vm,
"const p = new Temporal.PlainDateTime(1976, 11, 18, 15, 23, 30, 123, 456, 789, 'hebrew');
p.withPlainTime().toString() + '|' +
p.withPlainTime(undefined).toString() + '|' +
p.withPlainTime({ minute: 30 }).toString() + '|' +
p.withPlainTime({ hour: 23, minute: 59, second: 60 }).toString() + '|' +
p.withPlainTime('12:34').toString() + '|' +
p.withPlainTime(new Temporal.PlainTime(11, 22)).hour + '|' +
p.withPlainTime(new Temporal.ZonedDateTime(0n, 'UTC')).toString() + '|' +
p.withPlainTime({ minute: 5 }).calendarId + '|' +
(() => { try { p.withPlainTime({}); return 'no'; }
catch (e) { return e instanceof TypeError; } })() + '|' +
(() => { try { new Temporal.PlainDateTime(-271821, 4, 19, 0, 0, 0, 0, 0, 1).withPlainTime(); return 'no'; }
catch (e) { return e instanceof RangeError; } })()",
)
.unwrap();
assert_eq!(
str_val(&vm, r),
"1976-11-18T00:00:00|1976-11-18T00:00:00|1976-11-18T00:30:00|1976-11-18T23:59:59|1976-11-18T12:34:00|11|1976-11-18T00:00:00|hebrew|true|true"
);
}
#[test]
fn plain_date_to_plain_date_time_limits_and_calendar() {
let mut vm = Vm::new();
// 严格不等式边界:min 午夜抛、min 加 1 纳秒过、max 全满过;日历继承 receiver 槽。
let r = eval(
&mut vm,
"(() => {
const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } };
const min = new Temporal.PlainDate(-271821, 4, 19, 'hebrew');
const max = new Temporal.PlainDate(275760, 9, 13);
return [
kind(() => min.toPlainDateTime()),
min.toPlainDateTime({ nanosecond: 1 }).toString(),
min.toPlainDateTime('23:59:59.999999999').toString(),
min.toPlainDateTime({ nanosecond: 1 }).calendarId,
max.toPlainDateTime('23:59:59.999999999').toString(),
max.toPlainDateTime().toString(),
].join('|');
})()",
)
.unwrap();
assert_eq!(
str_val(&vm, r),
"RangeError|-271821-04-19T00:00:00.000000001|-271821-04-19T23:59:59.999999999|hebrew|+275760-09-13T23:59:59.999999999|+275760-09-13T00:00:00"
);
}
#[test]
fn plain_date_to_plain_month_day_and_year_month() {
let mut vm = Vm::new();
// 纯拷贝:月/日与年/月分量不变;参考年 1972、参考日 1(非 iso8601 日历
// toString 补参考值与日历注解);日历继承 receiver 槽。
let r = eval(
&mut vm,
"const d = new Temporal.PlainDate(1976, 11, 18, 'hebrew');
const md = d.toPlainMonthDay();
const ym = d.toPlainYearMonth();
md.monthCode + '|' + md.day + '|' + md.calendarId + '|' + md.toString() + '|' +
ym.year + '|' + ym.month + '|' + ym.calendarId + '|' + ym.toString()",
)
.unwrap();
assert_eq!(str_val(&vm, r), "M11|18|hebrew|1972-11-18[u-ca=hebrew]|1976|11|hebrew|1976-11-01[u-ca=hebrew]");
}