From 6bc85aa9b14165a84dab433ef146838ffdb34e81 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Thu, 15 Oct 2026 17:08:08 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20PlainDate=20toString=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=97=A5=E5=8E=86=E9=80=89=E9=A1=B9=E4=B8=8E=E5=85=AD?= =?UTF-8?q?=E4=BD=8D=E5=B9=B4=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 42 ++++++------- Cargo.toml | 2 +- crates/oxide_builtins/src/temporal/common.rs | 20 +++++- .../oxide_builtins/src/temporal/plain_date.rs | 39 +++++++----- .../oxide_vm/tests/builtins_temporal_tests.rs | 62 ++++++++++++++++++- 5 files changed, 123 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17df1a8d..3e743adb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 7b7e45e4..2e9cf753 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ members = [ ] [workspace.package] -version = "0.19.20" +version = "0.19.21" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_builtins/src/temporal/common.rs b/crates/oxide_builtins/src/temporal/common.rs index 2da6aad1..a43cacd1 100644 --- a/crates/oxide_builtins/src/temporal/common.rs +++ b/crates/oxide_builtins/src/temporal/common.rs @@ -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 { let trimmed = input.trim(); diff --git a/crates/oxide_builtins/src/temporal/plain_date.rs b/crates/oxide_builtins/src/temporal/plain_date.rs index 647223e4..e53ca4b6 100644 --- a/crates/oxide_builtins/src/temporal/plain_date.rs +++ b/crates/oxide_builtins/src/temporal/plain_date.rs @@ -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(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(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(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(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(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 大单位运算,月份不足日时取月末)。 diff --git a/crates/oxide_vm/tests/builtins_temporal_tests.rs b/crates/oxide_vm/tests/builtins_temporal_tests.rs index 8861144d..f57f4525 100644 --- a/crates/oxide_vm/tests/builtins_temporal_tests.rs +++ b/crates/oxide_vm/tests/builtins_temporal_tests.rs @@ -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" ); }