feat: PlainDate toString 支持日历选项与六位年号

This commit is contained in:
2026-10-15 17:08:08 +00:00
parent 5d4ed8908c
commit 6bc85aa9b1
5 changed files with 123 additions and 42 deletions
Generated
+21 -21
View File
@@ -1160,7 +1160,7 @@ dependencies = [
[[package]]
name = "oxide_builtins"
version = "0.19.20"
version = "0.19.21"
dependencies = [
"chrono",
"fast-float",
@@ -1185,7 +1185,7 @@ dependencies = [
[[package]]
name = "oxide_bytecode"
version = "0.19.20"
version = "0.19.21"
dependencies = [
"num-bigint",
"oxide_log",
@@ -1193,7 +1193,7 @@ dependencies = [
[[package]]
name = "oxide_cfg"
version = "0.19.20"
version = "0.19.21"
dependencies = [
"oxide_bytecode",
"oxide_emit",
@@ -1204,7 +1204,7 @@ dependencies = [
[[package]]
name = "oxide_cli"
version = "0.19.20"
version = "0.19.21"
dependencies = [
"ansi_term",
"bumpalo",
@@ -1228,7 +1228,7 @@ dependencies = [
[[package]]
name = "oxide_code_cache"
version = "0.19.20"
version = "0.19.21"
dependencies = [
"lru",
"oxide_bytecode",
@@ -1237,7 +1237,7 @@ dependencies = [
[[package]]
name = "oxide_compiler"
version = "0.19.20"
version = "0.19.21"
dependencies = [
"oxide_bytecode",
"oxide_cfg",
@@ -1258,7 +1258,7 @@ dependencies = [
[[package]]
name = "oxide_dce"
version = "0.19.20"
version = "0.19.21"
dependencies = [
"oxide_bytecode",
"oxide_cfg",
@@ -1272,7 +1272,7 @@ dependencies = [
[[package]]
name = "oxide_emit"
version = "0.19.20"
version = "0.19.21"
dependencies = [
"num-bigint",
"oxc_allocator",
@@ -1288,7 +1288,7 @@ dependencies = [
[[package]]
name = "oxide_engine"
version = "0.19.20"
version = "0.19.21"
dependencies = [
"oxide_bytecode",
"oxide_compiler",
@@ -1300,7 +1300,7 @@ dependencies = [
[[package]]
name = "oxide_fold"
version = "0.19.20"
version = "0.19.21"
dependencies = [
"num-bigint",
"oxide_bytecode",
@@ -1312,7 +1312,7 @@ dependencies = [
[[package]]
name = "oxide_ir"
version = "0.19.20"
version = "0.19.21"
dependencies = [
"oxide_bytecode",
"oxide_log",
@@ -1321,7 +1321,7 @@ dependencies = [
[[package]]
name = "oxide_kernel"
version = "0.19.20"
version = "0.19.21"
dependencies = [
"bumpalo",
"dashmap",
@@ -1335,7 +1335,7 @@ dependencies = [
[[package]]
name = "oxide_liveness"
version = "0.19.20"
version = "0.19.21"
dependencies = [
"oxide_bytecode",
"oxide_cfg",
@@ -1347,7 +1347,7 @@ dependencies = [
[[package]]
name = "oxide_log"
version = "0.19.20"
version = "0.19.21"
dependencies = [
"tracing",
"tracing-appender",
@@ -1356,11 +1356,11 @@ dependencies = [
[[package]]
name = "oxide_net"
version = "0.19.20"
version = "0.19.21"
[[package]]
name = "oxide_parser"
version = "0.19.20"
version = "0.19.21"
dependencies = [
"oxc_allocator",
"oxc_ast",
@@ -1375,7 +1375,7 @@ dependencies = [
[[package]]
name = "oxide_regalloc"
version = "0.19.20"
version = "0.19.21"
dependencies = [
"oxide_bytecode",
"oxide_cfg",
@@ -1391,7 +1391,7 @@ dependencies = [
[[package]]
name = "oxide_runtime_api"
version = "0.19.20"
version = "0.19.21"
dependencies = [
"num-bigint",
"num-traits",
@@ -1404,7 +1404,7 @@ dependencies = [
[[package]]
name = "oxide_test262"
version = "0.19.20"
version = "0.19.21"
dependencies = [
"oxide_bytecode",
"oxide_compiler",
@@ -1424,7 +1424,7 @@ dependencies = [
[[package]]
name = "oxide_types"
version = "0.19.20"
version = "0.19.21"
dependencies = [
"bumpalo",
"dashmap",
@@ -1437,7 +1437,7 @@ dependencies = [
[[package]]
name = "oxide_vm"
version = "0.19.20"
version = "0.19.21"
dependencies = [
"bumpalo",
"chrono",
+1 -1
View File
@@ -25,7 +25,7 @@ members = [
]
[workspace.package]
version = "0.19.20"
version = "0.19.21"
edition = "2021"
license = "MIT"
rust-version = "1.80"
+17 -3
View File
@@ -1247,12 +1247,26 @@ pub(crate) const CALENDAR_ID_WHITELIST: [&str; 18] = [
"roc",
];
/// 严格白名单匹配:命中返回规范小写形式,未命中返回 None。
/// CLDR 日历别名(ParseTemporalCalendarString 的别名表):命中后归一到规范小写 ID。
const CALENDAR_ID_ALIASES: [(&str, &str); 3] = [
("ethiopic-amete-alem", "ethioaa"),
("ethiopic-tana-alem", "ethiopic"),
("islamicc", "islamic-civil"),
];
/// 严格白名单匹配:先查 18 项白名单,未命中再查 CLDR 别名表;
/// 命中返回规范小写形式,未命中返回 None。
pub(crate) fn is_builtin_calendar_id(value: &str) -> Option<&'static str> {
CALENDAR_ID_WHITELIST.iter().find(|id| value.eq_ignore_ascii_case(id)).copied()
if let Some(id) = CALENDAR_ID_WHITELIST.iter().find(|id| value.eq_ignore_ascii_case(id)).copied() {
return Some(id);
}
CALENDAR_ID_ALIASES
.iter()
.find(|(alias, _)| value.eq_ignore_ascii_case(alias))
.map(|(_, id)| *id)
}
/// 严格日历 ID 解析:只走 18 项白名单,拒绝 ISO 串(含注解/compact/extended/空串)。
/// 严格日历 ID 解析:只走 18 项白名单与 CLDR 别名表,拒绝 ISO 串(含注解/compact/extended/空串)。
/// 供构造器日历参数与注解值判定使用。
pub(crate) fn parse_temporal_calendar_id_strict(input: &str) -> Result<String, String> {
let trimmed = input.trim();
@@ -7,11 +7,12 @@ use oxide_types::value::JsValue;
use super::common::native_try;
use super::common::{
civil_from_days, 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, parse_round_options,
plain_date_time_object_parts, receiver_obj, 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,
calendar_annotation, civil_from_days, days_from_civil, days_in_month, ensure_plain_date, format_iso_year,
get_calendar_id, get_double_prop, initialize_temporal_receiver, is_ctor_call, parse_plain_date_string,
parse_round_options, plain_date_time_object_parts, receiver_obj,
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, temporal_to_show_calendar, zoned_date_time_plain_parts, DateTimeParts, ShowCalendar,
};
use super::difference::{difference_core, parse_difference_settings, IsoDateTime};
use super::duration::duration_like_values;
@@ -262,15 +263,26 @@ pub fn plain_date_day<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
NativeResult::Ok(JsValue::float(d))
}
/// `Temporal.PlainDate.prototype.toString()`:输出 `YYYY-MM-DD`。
/// PlainDate ISO 串:auto/never 加 iso8601 日历保持裸 `±YYYY-MM-DD`;
/// 非 iso8601 日历与 always/critical 按注解表补 `[u-ca=…]`/`[!u-ca=…]`。
fn plain_date_iso_string(obj: &JsObject, show: ShowCalendar) -> String {
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 annotation = calendar_annotation(&calendar, show);
format!("{}-{month:02}-{day:02}{annotation}", format_iso_year(year as i128))
}
/// `Temporal.PlainDate.prototype.toString([options])`:默认 `±YYYY-MM-DD`;
/// options 非对象抛 TypeError,calendarName 取值 auto/always/never/critical
/// (其余 RangeError);非 iso8601 日历与 always/critical 补日历注解。
pub fn plain_date_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_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;
NativeResult::Ok(vm.new_string(&format!("{year:04}-{month:02}-{day:02}")))
let show = native_try!(temporal_to_show_calendar(vm, args));
NativeResult::Ok(vm.new_string_owned(plain_date_iso_string(obj, show)))
}
// ───────────────────── PlainDate 扩展方法 ─────────────────────
@@ -710,7 +722,7 @@ pub fn plain_date_value_of<H: VmHost>(vm: &mut H, _args: &[u8]) -> NativeResult
NativeResult::Err(crate::error::create_type_error(vm, "Temporal.PlainDate has no valueOf"))
}
/// `Temporal.PlainDate.prototype.toJSON()`:输出 ISO 日期串(同 toString)。
/// `Temporal.PlainDate.prototype.toJSON()`:输出默认形 ISO 日期串(同 toString 的 auto 形),忽略参数。
pub fn plain_date_to_json<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = match receiver_obj(vm, args) {
Ok(p) => p,
@@ -720,10 +732,7 @@ pub fn plain_date_to_json<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if let Err(e) = ensure_plain_date(vm, obj) {
return NativeResult::Err(e);
}
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;
NativeResult::Ok(vm.new_string(&format!("{year:04}-{month:02}-{day:02}")))
NativeResult::Ok(vm.new_string_owned(plain_date_iso_string(obj, ShowCalendar::Auto)))
}
/// 按 duration-like 日期字段对日期做加减(Temporal 大单位运算,月份不足日时取月末)。
@@ -513,6 +513,64 @@ fn plain_date_to_string() {
assert_eq!(str_val(&vm, r), "2024-01-05");
}
#[test]
fn plain_date_to_string_honors_calendar_name() {
let mut vm = Vm::new();
let r = eval(
&mut vm,
"(() => {
const d = new Temporal.PlainDate(2000, 5, 2);
const auto = d.toString({ calendarName: 'auto' });
const always = d.toString({ calendarName: 'always' });
const critical = d.toString({ calendarName: 'critical' });
const never = d.toString({ calendarName: 'never' });
const badValue = (() => { try { d.toString({ calendarName: 'ALWAYS' }); return false; }
catch (e) { return e instanceof RangeError; } })();
const badOptions = (() => { try { d.toString(null); return false; }
catch (e) { return e instanceof TypeError; } })();
return [auto, always, critical, never, badValue, badOptions].join('|');
})()",
)
.unwrap();
assert_eq!(
str_val(&vm, r),
"2000-05-02|2000-05-02[u-ca=iso8601]|2000-05-02[!u-ca=iso8601]|2000-05-02|true|true"
);
}
#[test]
fn plain_date_serializes_extended_years() {
let mut vm = Vm::new();
let r = eval(
&mut vm,
"[new Temporal.PlainDate(-100000, 12, 3).toString(),
new Temporal.PlainDate(-10000, 4, 5).toString(),
new Temporal.PlainDate(-1, 8, 7).toString(),
new Temporal.PlainDate(0, 6, 5).toString(),
new Temporal.PlainDate(10000, 6, 7).toString(),
new Temporal.PlainDate(100000, 8, 9).toJSON()].join('|')",
)
.unwrap();
assert_eq!(
str_val(&vm, r),
"-100000-12-03|-010000-04-05|-000001-08-07|0000-06-05|+010000-06-07|+100000-08-09"
);
}
#[test]
fn plain_date_constructor_accepts_cldr_calendar_aliases() {
let mut vm = Vm::new();
// CLDR 别名归一到规范小写 ID:ethiopic-amete-alem → ethioaa、islamicc → islamic-civil。
let r = eval(
&mut vm,
"Temporal.PlainDate.from({ year: 2000, month: 5, day: 2, calendar: 'ethiopic-amete-alem' }).calendarId + ',' +
Temporal.PlainDate.from({ year: 2000, month: 5, day: 2, calendar: 'islamicc' }).calendarId + ',' +
Temporal.PlainDate.from({ year: 2000, month: 5, day: 2, calendar: 'ETHIOpic-TANA-alem' }).calendarId",
)
.unwrap();
assert_eq!(str_val(&vm, r), "ethioaa,islamic-civil,ethiopic");
}
#[test]
fn plain_date_requires_new() {
let mut vm = Vm::new();
@@ -3243,7 +3301,7 @@ fn plain_month_day_to_plain_date_matrix() {
.unwrap();
assert_eq!(
str_val(&vm, r),
"2002-01-22|TypeError|TypeError|0012-01-22|RangeError|2020-02-29|2023-02-28|2020-02-29|-271821-04-19|RangeError|275760-09-13|RangeError|iso8601|TypeError|2002-01-22"
"2002-01-22|TypeError|TypeError|0012-01-22|RangeError|2020-02-29|2023-02-28|2020-02-29|-271821-04-19|RangeError|+275760-09-13|RangeError|iso8601|TypeError|2002-01-22"
);
}
@@ -3743,7 +3801,7 @@ fn plain_year_month_to_plain_date_matrix() {
.unwrap();
assert_eq!(
str_val(&vm, r),
"2002-01-22|TypeError|TypeError|TypeError|TypeError|2023-02-28|1998-06-30|2000-05-01|2000-05-01|-271821-04-19|RangeError|275760-09-13|RangeError|RangeError"
"2002-01-22|TypeError|TypeError|TypeError|TypeError|2023-02-28|1998-06-30|2000-05-01|2000-05-01|-271821-04-19|RangeError|+275760-09-13|RangeError|RangeError"
);
}