From 2f7832a12515563f42f7fb530ee8cf0d212a8228 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sun, 16 Aug 2026 21:21:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20ZonedDateTime=2027=20=E4=B8=AA=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=20getter=20=E5=AE=8F=E5=8C=96=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/temporal.rs | 178 +++++++++++++++++ crates/oxide_vm/src/bindings/bind_temporal.rs | 189 ++++++++++++++++++ 2 files changed, 367 insertions(+) diff --git a/crates/oxide_builtins/src/temporal.rs b/crates/oxide_builtins/src/temporal.rs index 1a4f039..0598d8a 100644 --- a/crates/oxide_builtins/src/temporal.rs +++ b/crates/oxide_builtins/src/temporal.rs @@ -1348,6 +1348,155 @@ pub fn zoned_date_time_calendar_id(vm: &mut H, args: &[u8]) -> Native NativeResult::Ok(obj.get_prop_at(2)) } +/// ZDT 字段 getter 宏:branding 后读本地分量,按选择函数取字段。 +macro_rules! zoned_date_time_parts_getter { + ($name:ident, $select:expr) => { + pub fn $name(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_zoned_date_time(vm, obj)); + let (year, month, day, time_ns) = native_try!(zoned_date_time_plain_parts(vm, obj)); + NativeResult::Ok(JsValue::float($select(year, month, day, time_ns) as f64)) + } + }; +} + +/// ZDT epoch 除法 getter 宏:槽 0 按除数向下取整(负值 floor,仿 instant_epoch_*)。 +macro_rules! zoned_date_time_epoch_getter { + ($name:ident, $divisor:expr) => { + pub fn $name(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_zoned_date_time(vm, obj)); + let Some(epoch_ns) = get_instant_epoch_ns(obj) else { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid ZonedDateTime")); + }; + NativeResult::Ok(JsValue::float(epoch_ns.div_euclid($divisor) as f64)) + } + }; +} + +/// ZDT 日期派生 getter 宏:本地日期转 chrono NaiveDate 后按闭包取值。 +macro_rules! zoned_date_time_naive_getter { + ($name:ident, $body:expr) => { + pub fn $name(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_zoned_date_time(vm, obj)); + let (year, month, day, _) = native_try!(zoned_date_time_plain_parts(vm, obj)); + let Some(date) = NaiveDate::from_ymd_opt(year, month, day) else { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid date")); + }; + NativeResult::Ok($body(&date)) + } + }; +} + +/// ZDT 常量 getter 宏:仅做 branding,返回固定值。 +macro_rules! zoned_date_time_brand_only_getter { + ($name:ident, $value:expr) => { + pub fn $name(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_zoned_date_time(vm, obj)); + NativeResult::Ok($value) + } + }; +} + +// 墙钟字段 getter:本地日期分量直读。 +zoned_date_time_parts_getter!(zoned_date_time_year, |y, _, _, _| y as f64); +zoned_date_time_parts_getter!(zoned_date_time_month, |_, m, _, _| m as f64); +zoned_date_time_parts_getter!(zoned_date_time_day, |_, _, d, _| d as f64); +zoned_date_time_parts_getter!(zoned_date_time_hour, |_, _, _, t| plain_time_components(t).0 as f64); +zoned_date_time_parts_getter!(zoned_date_time_minute, |_, _, _, t| plain_time_components(t).1 as f64); +zoned_date_time_parts_getter!(zoned_date_time_second, |_, _, _, t| plain_time_components(t).2 as f64); +zoned_date_time_parts_getter!(zoned_date_time_millisecond, |_, _, _, t| plain_time_components(t).3 as f64); +zoned_date_time_parts_getter!(zoned_date_time_microsecond, |_, _, _, t| plain_time_components(t).4 as f64); +zoned_date_time_parts_getter!(zoned_date_time_nanosecond, |_, _, _, t| plain_time_components(t).5 as f64); + +// epoch 除法 getter:BigInt 除以秒/毫秒/微秒,负值向下取整。 +zoned_date_time_epoch_getter!(zoned_date_time_epoch_seconds, 1_000_000_000); +zoned_date_time_epoch_getter!(zoned_date_time_epoch_milliseconds, 1_000_000); +zoned_date_time_epoch_getter!(zoned_date_time_epoch_microseconds, 1_000); + +// 日期派生 getter:本地日期经 chrono NaiveDate 取周/年/月属性。 +zoned_date_time_naive_getter!(zoned_date_time_day_of_week, |d: &NaiveDate| { + JsValue::float((d.weekday().num_days_from_monday() + 1) as f64) +}); +zoned_date_time_naive_getter!(zoned_date_time_day_of_year, |d: &NaiveDate| { JsValue::float(d.ordinal() as f64) }); +zoned_date_time_naive_getter!(zoned_date_time_week_of_year, |d: &NaiveDate| { + JsValue::float(d.iso_week().week() as f64) +}); +zoned_date_time_naive_getter!(zoned_date_time_year_of_week, |d: &NaiveDate| { + JsValue::float(d.iso_week().year() as f64) +}); +zoned_date_time_naive_getter!(zoned_date_time_days_in_month, |d: &NaiveDate| { + JsValue::float(days_in_month_iso(d.year(), d.month()) as f64) +}); +zoned_date_time_naive_getter!(zoned_date_time_days_in_year, |d: &NaiveDate| { + JsValue::float(if is_leap_year_iso(d.year()) { 366.0 } else { 365.0 }) +}); +zoned_date_time_naive_getter!(zoned_date_time_in_leap_year, |d: &NaiveDate| { + JsValue::bool(is_leap_year_iso(d.year())) +}); + +// 常量 getter:ISO 日历下固定值,仅做 branding。 +zoned_date_time_brand_only_getter!(zoned_date_time_days_in_week, JsValue::float(7.0)); +zoned_date_time_brand_only_getter!(zoned_date_time_months_in_year, JsValue::float(12.0)); +zoned_date_time_brand_only_getter!(zoned_date_time_era, JsValue::undefined()); +zoned_date_time_brand_only_getter!(zoned_date_time_era_year, JsValue::undefined()); + +/// 读 ZDT 时区偏移(分钟);槽 1 解析失败返回 RangeError。调用方须先 ensure_zoned_date_time。 +fn zoned_date_time_offset_minutes(vm: &mut H, obj: &JsObject) -> Result { + let time_zone_id = to_string(obj.get_prop_at(1)); + instant_time_zone_offset(&time_zone_id).ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone")) +} + +/// `Temporal.ZonedDateTime.prototype.offset`:由偏移分钟数规范化为 ±HH:MM 字符串。 +pub fn zoned_date_time_offset(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_zoned_date_time(vm, obj)); + let offset_minutes = native_try!(zoned_date_time_offset_minutes(vm, obj)); + let sign = if offset_minutes < 0 { '-' } else { '+' }; + let magnitude = offset_minutes.abs(); + NativeResult::Ok(vm.new_string(&format!("{sign}{:02}:{:02}", magnitude / 60, magnitude % 60))) +} + +/// `Temporal.ZonedDateTime.prototype.offsetNanoseconds`:偏移分钟数换算纳秒(f64 精确域内)。 +pub fn zoned_date_time_offset_nanoseconds(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_zoned_date_time(vm, obj)); + let offset_minutes = native_try!(zoned_date_time_offset_minutes(vm, obj)); + NativeResult::Ok(JsValue::float(offset_minutes as f64 * 60_000_000_000.0)) +} + +/// `Temporal.ZonedDateTime.prototype.monthCode`:ISO 日历下恒为 `M{month:02}` 补零格式。 +pub fn zoned_date_time_month_code(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_zoned_date_time(vm, obj)); + let (_, month, _, _) = native_try!(zoned_date_time_plain_parts(vm, obj)); + NativeResult::Ok(vm.new_string(&format!("M{month:02}"))) +} + +/// `Temporal.ZonedDateTime.prototype.hoursInDay`:当日与次日当地午夜差 / 小时,含 Instant 范围校验。 +pub fn zoned_date_time_hours_in_day(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_zoned_date_time(vm, obj)); + let (year, month, day, _) = native_try!(zoned_date_time_plain_parts(vm, obj)); + let offset_minutes = native_try!(zoned_date_time_offset_minutes(vm, obj)); + let today = native_try!(start_of_day_epoch_ns(year, month, day, offset_minutes) + .ok_or_else(|| crate::error::create_range_error(vm, "invalid date"))); + let days = days_from_civil(i128::from(year), i128::from(month), i128::from(day)); + let tomorrow = native_try!(start_of_day_epoch_ns_by_days(days + 1, offset_minutes) + .ok_or_else(|| crate::error::create_range_error(vm, "invalid date"))); + NativeResult::Ok(JsValue::float((tomorrow - today) as f64 / 3_600_000_000_000.0)) +} + fn make_plain_date(vm: &mut H, year: i32, month: u32, day: u32, calendar: &str) -> NativeResult { let proto = JsValue::from_js_object(vm.session().builtin_world().plain_date_proto.as_ptr() as *mut JsObject); let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto); @@ -3276,6 +3425,19 @@ fn zoned_date_time_plain_parts(vm: &mut H, obj: &JsObject) -> Result< Ok((year as i32, month as u32, day as u32, time_ns)) } +/// 当地午夜纪元纳秒(含 Instant 范围校验,spec GetStartOfDay 语义);越界返回 None。 +fn start_of_day_epoch_ns(year: i32, month: u32, day: u32, offset_minutes: i32) -> Option { + start_of_day_epoch_ns_by_days(days_from_civil(i128::from(year), i128::from(month), i128::from(day)), offset_minutes) +} + +/// 按日数直接算当地午夜(hoursInDay 的明日边界复用:days+1 不经 civil 回填)。 +fn start_of_day_epoch_ns_by_days(days: i128, offset_minutes: i32) -> Option { + let start = days + .checked_mul(86_400_000_000_000)? + .checked_sub(i128::from(offset_minutes) * 60_000_000_000)?; + (start.unsigned_abs() <= MAX_INSTANT_NS as u128).then_some(start) +} + fn plain_date_time_like_parts( vm: &mut H, value: JsValue, constrain: bool, ) -> Result<(i32, u32, u32, f64, Option), JsValue> { @@ -4932,4 +5094,20 @@ mod tests { obj.set_prop_at(3, JsValue::int(7)); assert_eq!(get_calendar_id(&obj, 3), "iso8601"); } + + #[test] + fn start_of_day_epoch_ns_instant_range() { + // 当地午夜回推 epoch 越 Instant 界(±MAX)返回 None,供 startOfDay/hoursInDay RangeError 依据。 + // -100000001 天 + 1h < -MAX → 越界。 + assert_eq!(start_of_day_epoch_ns(-271_821, 4, 19, -60), None); + // -100000000 天(-271821-04-20)UTC 当地午夜恰为 -MAX,在边界内。 + assert_eq!(start_of_day_epoch_ns(-271_821, 4, 20, 0), Some(-8_640_000_000_000_000_000_000)); + // 同日期 +1h 偏移使当地午夜 -MAX - 1h 越界。 + assert_eq!(start_of_day_epoch_ns(-271_821, 4, 20, 60), None); + // 明日边界越界:+100000001 天(UTC)。 + assert_eq!(start_of_day_epoch_ns_by_days(100_000_001, 0), None); + // 正常日期返回当地午夜。 + assert_eq!(start_of_day_epoch_ns(1970, 1, 1, 0), Some(0)); + assert_eq!(start_of_day_epoch_ns(1970, 1, 1, 60), Some(-3_600_000_000_000)); + } } diff --git a/crates/oxide_vm/src/bindings/bind_temporal.rs b/crates/oxide_vm/src/bindings/bind_temporal.rs index 7102536..554d332 100644 --- a/crates/oxide_vm/src/bindings/bind_temporal.rs +++ b/crates/oxide_vm/src/bindings/bind_temporal.rs @@ -172,6 +172,195 @@ pub fn bind_temporal(core: &Arc, session: &KernelSession, global: &m "calendarId", oxide_builtins::temporal::zoned_date_time_calendar_id:: as *const (), ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "epochSeconds", + oxide_builtins::temporal::zoned_date_time_epoch_seconds:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "epochMilliseconds", + oxide_builtins::temporal::zoned_date_time_epoch_milliseconds:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "epochMicroseconds", + oxide_builtins::temporal::zoned_date_time_epoch_microseconds:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "offset", + oxide_builtins::temporal::zoned_date_time_offset:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "offsetNanoseconds", + oxide_builtins::temporal::zoned_date_time_offset_nanoseconds:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "year", + oxide_builtins::temporal::zoned_date_time_year:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "month", + oxide_builtins::temporal::zoned_date_time_month:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "day", + oxide_builtins::temporal::zoned_date_time_day:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "hour", + oxide_builtins::temporal::zoned_date_time_hour:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "minute", + oxide_builtins::temporal::zoned_date_time_minute:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "second", + oxide_builtins::temporal::zoned_date_time_second:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "millisecond", + oxide_builtins::temporal::zoned_date_time_millisecond:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "microsecond", + oxide_builtins::temporal::zoned_date_time_microsecond:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "nanosecond", + oxide_builtins::temporal::zoned_date_time_nanosecond:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "dayOfWeek", + oxide_builtins::temporal::zoned_date_time_day_of_week:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "dayOfYear", + oxide_builtins::temporal::zoned_date_time_day_of_year:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "weekOfYear", + oxide_builtins::temporal::zoned_date_time_week_of_year:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "yearOfWeek", + oxide_builtins::temporal::zoned_date_time_year_of_week:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "monthCode", + oxide_builtins::temporal::zoned_date_time_month_code:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "daysInMonth", + oxide_builtins::temporal::zoned_date_time_days_in_month:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "daysInWeek", + oxide_builtins::temporal::zoned_date_time_days_in_week:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "daysInYear", + oxide_builtins::temporal::zoned_date_time_days_in_year:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "monthsInYear", + oxide_builtins::temporal::zoned_date_time_months_in_year:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "inLeapYear", + oxide_builtins::temporal::zoned_date_time_in_leap_year:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "era", + oxide_builtins::temporal::zoned_date_time_era:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "eraYear", + oxide_builtins::temporal::zoned_date_time_era_year:: as *const (), + ); + bind_accessor_getter( + core, + session, + zoned_date_time_proto, + "hoursInDay", + oxide_builtins::temporal::zoned_date_time_hours_in_day:: as *const (), + ); bind_well_known_data_property( core, zoned_date_time_proto,