From d41b2fe9ff512c0d8f05da1f423af2b2046c2e1d Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Thu, 17 Sep 2026 10:02:43 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20temporal=20PlainTime=20=E6=97=8F?= =?UTF-8?q?=E6=8B=86=E5=87=BA=E7=8B=AC=E7=AB=8B=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/temporal/mod.rs | 845 +---------------- .../oxide_builtins/src/temporal/plain_time.rs | 858 ++++++++++++++++++ 2 files changed, 860 insertions(+), 843 deletions(-) create mode 100644 crates/oxide_builtins/src/temporal/plain_time.rs diff --git a/crates/oxide_builtins/src/temporal/mod.rs b/crates/oxide_builtins/src/temporal/mod.rs index 8891974a..0fbe8a81 100644 --- a/crates/oxide_builtins/src/temporal/mod.rs +++ b/crates/oxide_builtins/src/temporal/mod.rs @@ -10,6 +10,7 @@ mod difference; mod duration; mod instant; mod plain_date; +mod plain_time; mod zoned_date_time; use common::*; @@ -17,254 +18,9 @@ use difference::*; pub use duration::*; pub use instant::*; pub use plain_date::*; +pub use plain_time::*; pub use zoned_date_time::*; -/// 时间串的日期歧义判定:形如 YYYY-MM / MMDD / YYYYMM / MM-DD 且作为日期合法 → 歧义。 -/// -/// # 边界与前提 -/// - 2 月按闰年处理(0229 判歧义、0230 不判)。 -/// - 月/日非法(13、00、2 月 30 日等)不算歧义,按时间解析。 -fn is_ambiguous_date_string(s: &str) -> bool { - let bytes = s.as_bytes(); - let all_digits = |range: std::ops::Range| { - bytes.get(range.clone()).is_some_and(|part| part.iter().all(u8::is_ascii_digit)) - }; - let two_digits = |at: usize| -> Option { - if at + 1 >= bytes.len() { - None - } else { - Some((bytes[at] - b'0') as i32 * 10 + (bytes[at + 1] - b'0') as i32) - } - }; - let day_in_month = |month: i32, day: i32| -> bool { - // 2 月按闰年(29 天)判定歧义。 - let max = match month { - 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, - 4 | 6 | 9 | 11 => 30, - 2 => 29, - _ => return false, - }; - day >= 1 && day <= max - }; - match s.len() { - 4 if all_digits(0..4) => { - let (month, day) = (two_digits(0).unwrap(), two_digits(2).unwrap()); - day_in_month(month, day) - } - 6 if all_digits(0..6) => (1..=12).contains(&two_digits(4).unwrap()), - 5 if bytes[2] == b'-' && all_digits(0..2) && all_digits(3..5) => { - let (month, day) = (two_digits(0).unwrap(), two_digits(3).unwrap()); - day_in_month(month, day) - } - 7 if bytes[4] == b'-' && all_digits(0..4) && all_digits(5..7) => (1..=12).contains(&two_digits(5).unwrap()), - _ => false, - } -} - -/// 解析时间主体(时[:分[:秒[.小数]]] 或 HHMM / HHMMSS),闰秒按前一秒。 -fn parse_plain_clock(clock: &str) -> Option { - let (clock, fraction) = match clock.find(['.', ',']) { - Some(index) => (&clock[..index], Some(&clock[index + 1..])), - None => (clock, None), - }; - let subsecond = match fraction { - Some(digits) => { - if digits.is_empty() || digits.len() > 9 || !digits.bytes().all(|byte| byte.is_ascii_digit()) { - return None; - } - let mut padded = digits.to_string(); - padded.extend(std::iter::repeat('0').take(9 - digits.len())); - padded.parse::().ok()? - } - None => 0, - }; - let (hour, minute, second) = if clock.contains(':') { - let fields: Vec<&str> = clock.split(':').collect(); - if fields.is_empty() - || fields.len() > 3 - || fields - .iter() - .any(|field| field.len() != 2 || !field.bytes().all(|byte| byte.is_ascii_digit())) - { - return None; - } - if fraction.is_some() && fields.len() < 3 { - return None; - } - ( - fields[0].parse::().ok()?, - fields.get(1).map_or(Ok(0), |field| field.parse()).ok()?, - fields.get(2).map_or(Ok(0), |field| field.parse()).ok()?, - ) - } else { - if !clock.bytes().all(|byte| byte.is_ascii_digit()) - || clock.len() % 2 != 0 - || clock.len() > 6 - || clock.is_empty() - { - return None; - } - if fraction.is_some() && clock.len() < 6 { - return None; - } - let read2 = |at: usize| (clock.as_bytes()[at] - b'0') as u32 * 10 + (clock.as_bytes()[at + 1] - b'0') as u32; - ( - read2(0), - if clock.len() >= 4 { read2(2) } else { 0 }, - if clock.len() >= 6 { read2(4) } else { 0 }, - ) - }; - let second = if second == 60 { 59 } else { second }; - if hour > 23 || minute > 59 || second > 59 { - return None; - } - Some( - hour as f64 * 3_600_000_000_000.0 - + minute as f64 * 60_000_000_000.0 - + second as f64 * 1_000_000_000.0 - + subsecond as f64, - ) -} - -/// ParseTemporalTimeString:时间串 → 当日纳秒(offset/注解被忽略、Z 拒绝)。 -/// -/// # 边界与前提 -/// - Z/z designator → None;纯日期串 → None(不隐式午夜)。 -/// - 日期+时间("1976-11-18T12:34...")→ 取 T 后时间部分;空格分隔需带日期部分。 -/// - 无 T 的数字/连字符形式先做日期歧义判定:日期合法 → None(须 T 前缀); -/// 日期非法 → 按时间解析(HHMM-UU 形式,offset 被忽略)。 -/// - 小数秒 ≤9 位;offset 小数秒 ≤9 位;闰秒按前一秒。 -fn parse_plain_time_string(input: &str) -> Option { - let text = input.trim(); - if text.contains('\u{2212}') { - return None; - } - let body = instant_string_without_annotations(text)?; - if body.contains(['Z', 'z']) { - return None; - } - let sep = body.find(['T', 't', ' ']); - let (date_part, time_part, has_t) = match sep { - Some(0) => ("", &body[1..], matches!(body.as_bytes().first(), Some(b'T' | b't'))), - Some(index) => (&body[..index], &body[index + 1..], matches!(body.as_bytes()[index], b'T' | b't')), - None => ("", body, false), - }; - if !has_t && sep == Some(0) { - // 前导空格不能替代 T 前缀(无日期部分的时间串)。 - return None; - } - if !date_part.is_empty() { - // 完整日期部分必须可解析(负零年等由 parse_iso_date 拒绝)。 - if parse_iso_date(date_part).is_err() { - return None; - } - return parse_plain_time_spec(time_part); - } - if !has_t && !time_part.contains(':') && is_ambiguous_date_string(time_part) { - return None; - } - parse_plain_time_spec(time_part) -} - -/// 时间主体 + 尾部 offset(剥离并忽略):offset 语法非法返回 None。 -fn parse_plain_time_spec(s: &str) -> Option { - let offset_start = s - .char_indices() - .rev() - .find_map(|(index, ch)| matches!(ch, '+' | '-').then_some(index)); - let (clock, offset) = match offset_start { - Some(index) => (&s[..index], &s[index..]), - None => (s, ""), - }; - if !offset.is_empty() && (parse_any_offset_minutes(offset).is_none() || !valid_offset_fraction(offset)) { - return None; - } - parse_plain_clock(clock) -} - -/// ToTemporalTime:PlainTime 对象 / ZDT / PlainDateTime / 字符串 / property bag → 当日纳秒。 -/// -/// # 步骤 -/// 1. undefined → 0(午夜);字符串走 parse_plain_time_string(失败 RangeError)。 -/// 2. PlainTime 读槽 0;ZDT 用其自身时区取本地时间;PlainDateTime 读时间槽。 -/// 3. 其余对象按 ToTemporalTimeRecord 读时间字段(无字段 TypeError)。 -/// -/// # 边界与前提 -/// - 非字符串原始值(number/bigint/null/boolean)→ TypeError;Symbol → TypeError。 -/// - bag 字段缺省 0(完整模式),越界值按 constrain 钳制(second=60 → 59)。 -fn plain_time_like_ns(vm: &mut H, value: JsValue) -> Result { - if value.is_undefined() { - return Ok(0.0); - } - if value.is_string() { - return parse_plain_time_string(&to_string(value)) - .ok_or_else(|| crate::error::create_range_error(vm, "invalid ISO 8601 time")); - } - if value.is_object() { - let ptr = value.as_js_object_ptr(); - if !ptr.is_null() { - let obj = unsafe { &*ptr }; - if obj.is_plain_time_obj() { - return Ok(get_double_prop(obj, 0)); - } - if obj.is_zoned_date_time_obj() { - return zoned_date_time_plain_parts(vm, obj).map(|(_, _, _, time_ns)| time_ns); - } - if obj.is_plain_date_time_obj() { - return Ok(get_double_prop(obj, 3)); - } - return plain_time_bag_ns(vm, value, obj); - } - } - Err(crate::error::create_type_error(vm, "cannot convert to PlainTime")) -} - -/// ToTemporalTimeRecord(完整模式):按字母序读时间字段,缺省 0,无字段 TypeError。 -fn plain_time_bag_ns(vm: &mut H, value: JsValue, obj: &JsObject) -> Result { - let hour_raw = temporal_option_value(vm, obj, value, "hour")?; - let microsecond_raw = temporal_option_value(vm, obj, value, "microsecond")?; - let millisecond_raw = temporal_option_value(vm, obj, value, "millisecond")?; - let minute_raw = temporal_option_value(vm, obj, value, "minute")?; - let nanosecond_raw = temporal_option_value(vm, obj, value, "nanosecond")?; - let second_raw = temporal_option_value(vm, obj, value, "second")?; - if [hour_raw, microsecond_raw, millisecond_raw, minute_raw, nanosecond_raw, second_raw] - .iter() - .all(|raw| raw.is_undefined()) - { - return Err(crate::error::create_type_error(vm, "no time units present")); - } - let mut convert = |raw: JsValue| -> Result { - if raw.is_undefined() { - Ok(0.0) - } else { - let number = temporal_option_number(vm, raw)?; - if !number.is_finite() { - return Err(crate::error::create_range_error(vm, "invalid time component")); - } - Ok(number.trunc()) - } - }; - let hour = convert(hour_raw)?; - let microsecond = convert(microsecond_raw)?; - let millisecond = convert(millisecond_raw)?; - let minute = convert(minute_raw)?; - let nanosecond = convert(nanosecond_raw)?; - let second = convert(second_raw)?; - // RegulateTime(constrain):越界钳制(second=60 → 59)。 - let hour = hour.clamp(0.0, 23.0); - let minute = minute.clamp(0.0, 59.0); - let second = second.clamp(0.0, 59.0); - let millisecond = millisecond.clamp(0.0, 999.0); - let microsecond = microsecond.clamp(0.0, 999.0); - let nanosecond = nanosecond.clamp(0.0, 999.0); - Ok(hour * 3_600_000_000_000.0 - + minute * 60_000_000_000.0 - + second * 1_000_000_000.0 - + millisecond * 1_000_000.0 - + microsecond * 1_000.0 - + nanosecond) -} - 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); @@ -308,603 +64,6 @@ fn make_duration(vm: &mut H, values: [f64; 10]) -> NativeResult { NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj))) } -fn valid_plain_time(hour: u32, minute: u32, second: u32, ms: u32, us: u32, ns: u32) -> bool { - hour <= 23 && minute <= 59 && second <= 59 && ms <= 999 && us <= 999 && ns <= 999 -} - -/// `Temporal.PlainTime` 构造器:`new PlainTime(h, m, s, ms, us, ns)`,缺省为 0。 -pub fn plain_time_constructor(vm: &mut H, args: &[u8]) -> NativeResult { - let ctor_proto = vm.session().builtin_world().plain_time_proto.as_ptr(); - if !is_ctor_call(vm, args, ctor_proto) { - return NativeResult::Err(crate::error::create_type_error( - vm, - "Class constructor Temporal.PlainTime cannot be invoked without 'new'", - )); - } - // 时间分量:缺参或显式 undefined 取默认 0(undefined 是合法分量,与 PD - // 缺参抛错相反);其余经 number-only 路径(Symbol/BigInt → TypeError, - // NaN/±Inf/不可解析串 → RangeError)。 - let mut get = |i: usize| -> Result { - let raw = if args.len() > i { vm.reg(args[i]) } else { JsValue::undefined() }; - if raw.is_undefined() { - return Ok(0.0); - } - temporal_number_component(vm, raw) - }; - // 负值在 u32 转换前检查:截断后为负的整分量(含负分数整数化为负)须先抛错, - // 不能让 f64→u32 饱和把其静默归 0 而逃过范围检查。 - let hour_value = native_try!(get(1)); - let minute_value = native_try!(get(2)); - let second_value = native_try!(get(3)); - let ms_value = native_try!(get(4)); - let us_value = native_try!(get(5)); - let ns_value = native_try!(get(6)); - if [hour_value, minute_value, second_value, ms_value, us_value, ns_value] - .iter() - .any(|value| *value < 0.0) - { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid time component")); - } - let hour = hour_value as u32; - let minute = minute_value as u32; - let second = second_value as u32; - let ms = ms_value as u32; - let us = us_value as u32; - let ns = ns_value as u32; - if !valid_plain_time(hour, minute, second, ms, us, ns) { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid time component")); - } - let total_ns = hour as f64 * 3.6e12 - + minute as f64 * 6e10 - + second as f64 * 1e9 - + ms as f64 * 1e6 - + us as f64 * 1e3 - + ns as f64; - initialize_temporal_receiver(vm, args, JsObject::OBJ_TYPE_PLAIN_TIME, [JsValue::float(total_ns)]) -} - -/// 拆解午夜后纳秒为各分量。 -/// 带符号分解午夜后纳秒为时/分/秒/毫秒/微秒/纳秒(各分量向零截断,保持同号)。 -fn plain_time_components_signed(total_ns: i128) -> [i128; 6] { - const SCALES: [i128; 5] = [3_600_000_000_000, 60_000_000_000, 1_000_000_000, 1_000_000, 1_000]; - let mut values = [0_i128; 6]; - let mut remainder = total_ns; - for (index, scale) in SCALES.iter().enumerate() { - values[index] = remainder / scale; - remainder %= scale; - } - values[5] = remainder; - values -} - -fn plain_time_components(total_ns: f64) -> (u32, u32, u32, u32, u32, u32) { - let total = total_ns as u64; - let ns = total % 1_000; - let us = total / 1_000 % 1_000; - let ms = total / 1_000_000 % 1_000; - let second = total / 1_000_000_000 % 60; - let minute = total / 60_000_000_000 % 60; - let hour = total / 3_600_000_000_000 % 24; - (hour as u32, minute as u32, second as u32, ms as u32, us as u32, ns as u32) -} - -fn plain_time_get(vm: &mut H, args: &[u8], select: fn(u32, u32, u32, u32, u32, u32) -> u32) -> NativeResult { - let ptr = match receiver_obj(vm, args) { - Ok(p) => p, - Err(e) => return NativeResult::Err(e), - }; - let obj = unsafe { &*ptr }; - if let Err(e) = ensure_plain_time(vm, obj) { - return NativeResult::Err(e); - } - let (h, m, s, ms, us, ns) = plain_time_components(get_double_prop(obj, 0)); - NativeResult::Ok(JsValue::float(select(h, m, s, ms, us, ns) as f64)) -} - -/// `Temporal.PlainTime.prototype.hour` getter。 -pub fn plain_time_hour(vm: &mut H, args: &[u8]) -> NativeResult { - plain_time_get(vm, args, |h, _, _, _, _, _| h) -} - -/// `Temporal.PlainTime.prototype.minute` getter。 -pub fn plain_time_minute(vm: &mut H, args: &[u8]) -> NativeResult { - plain_time_get(vm, args, |_, m, _, _, _, _| m) -} - -/// `Temporal.PlainTime.prototype.second` getter。 -pub fn plain_time_second(vm: &mut H, args: &[u8]) -> NativeResult { - plain_time_get(vm, args, |_, _, s, _, _, _| s) -} - -/// `Temporal.PlainTime.prototype.millisecond` getter。 -pub fn plain_time_millisecond(vm: &mut H, args: &[u8]) -> NativeResult { - plain_time_get(vm, args, |_, _, _, ms, _, _| ms) -} - -/// `Temporal.PlainTime.prototype.microsecond` getter。 -pub fn plain_time_microsecond(vm: &mut H, args: &[u8]) -> NativeResult { - plain_time_get(vm, args, |_, _, _, _, us, _| us) -} - -/// `Temporal.PlainTime.prototype.nanosecond` getter。 -pub fn plain_time_nanosecond(vm: &mut H, args: &[u8]) -> NativeResult { - plain_time_get(vm, args, |_, _, _, _, _, ns| ns) -} - -/// `Temporal.PlainTime.prototype.with(temporalTimeLike [, options])`:替换 -/// receiver 的指定时间分量,返回新 PlainTime。 -/// -/// # 步骤 -/// 1. branding + ToTemporalTimeLike(非对象 / Temporal 实例 / calendar、 -/// timeZone 有定义 → TypeError;字段按字典序读 hour → microsecond → -/// millisecond → minute → nanosecond → second,非数字 → TypeError, -/// NaN/±Inf → RangeError)。 -/// 2. 分量全 undefined → TypeError。 -/// 3. options → overflow(缺省 constrain)。 -/// 4. RegulateTime:constrain 钳制;reject 先校验范围再截断(负 / 超界分量 -/// 立即抛 RangeError)。 -/// 5. 合并分量算午夜后纳秒,构造新 PlainTime。 -/// -/// # 边界与前提 -/// - 字段读取先于选项解析(options-wrong-type 先报字段错误,与 ZDT with 同口径)。 -/// - receiver 分量取自午夜后纳秒槽;partial 中 undefined 的分量保留 receiver 值。 -pub fn plain_time_with(vm: &mut H, args: &[u8]) -> NativeResult { - let ptr = native_try!(receiver_obj(vm, args)); - let obj = unsafe { &*ptr }; - native_try!(ensure_plain_time(vm, obj)); - let value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - native_try!(reject_partial_object_with_calendar_or_time_zone(vm, value)); - - // 字段读取按字典序,undefined 不覆盖;number-only 路径(Symbol/BigInt → - // TypeError,NaN/±Inf → RangeError)。 - let bag = unsafe { &*value.as_js_object_ptr() }; - let read = |vm: &mut H, name: &str| -> Result, JsValue> { - let raw = temporal_option_value(vm, bag, value, name)?; - if raw.is_undefined() { - Ok(None) - } else { - Ok(Some(temporal_number_component(vm, raw)?)) - } - }; - let hour = native_try!(read(vm, "hour")); - let microsecond = native_try!(read(vm, "microsecond")); - let millisecond = native_try!(read(vm, "millisecond")); - let minute = native_try!(read(vm, "minute")); - let nanosecond = native_try!(read(vm, "nanosecond")); - let second = native_try!(read(vm, "second")); - if [hour, microsecond, millisecond, minute, nanosecond, second] - .iter() - .all(|c| c.is_none()) - { - return NativeResult::Err(crate::error::create_type_error(vm, "no properties present")); - } - - // options:字段读取完成后解析 overflow。 - let constrain = native_try!(temporal_overflow(vm, args)); - - // RegulateTime:partial 有定义的分量覆盖 receiver 分量。 - let (recv_hour, recv_minute, recv_second, recv_ms, recv_us, recv_ns) = - plain_time_components(get_double_prop(obj, 0)); - let merged = [ - hour.unwrap_or(recv_hour as f64), - minute.unwrap_or(recv_minute as f64), - second.unwrap_or(recv_second as f64), - millisecond.unwrap_or(recv_ms as f64), - microsecond.unwrap_or(recv_us as f64), - nanosecond.unwrap_or(recv_ns as f64), - ]; - let values = if constrain { - [ - merged[0].clamp(0.0, 23.0), - merged[1].clamp(0.0, 59.0), - merged[2].clamp(0.0, 59.0), - merged[3].clamp(0.0, 999.0), - merged[4].clamp(0.0, 999.0), - merged[5].clamp(0.0, 999.0), - ] - } else { - // reject 语义须先做范围校验再截断:负分量或超上限分量立即抛错, - // 不允许 f64→u32 饱和把负值静默归 0 而逃过范围检查。 - let limits = [23.0, 59.0, 59.0, 999.0, 999.0, 999.0]; - if merged.iter().zip(limits.iter()).any(|(v, limit)| *v < 0.0 || *v > *limit) { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid time component")); - } - merged - }; - let total_ns = values[0] * 3_600_000_000_000.0 - + values[1] * 60_000_000_000.0 - + values[2] * 1_000_000_000.0 - + values[3] * 1_000_000.0 - + values[4] * 1_000.0 - + values[5]; - make_plain_time(vm, total_ns) -} - -/// 按是否含秒与小数位格式化当日纳秒为 `HH:MM:SS[.frac]`。 -/// -/// # 边界与前提 -/// - `include_seconds=false` 时忽略小数位,仅输出 `HH:MM`。 -/// - `Some(0)` 不输出小数;`Some(digits)` 固定补足位数;`None` 按实际非零亚秒去尾零。 -fn format_plain_time_iso(time_ns: i128, include_seconds: bool, fractional_digits: Option) -> String { - let hour = time_ns / 3_600_000_000_000; - let minute = time_ns / 60_000_000_000 % 60; - let second = time_ns / 1_000_000_000 % 60; - let subsecond = time_ns % 1_000_000_000; - let mut output = format!("{hour:02}:{minute:02}"); - if include_seconds { - output.push_str(&format!(":{second:02}")); - match fractional_digits { - Some(0) => {} - Some(digits) => { - let fraction = format!("{subsecond:09}"); - output.push('.'); - output.push_str(&fraction[..digits]); - } - None if subsecond != 0 => { - output.push('.'); - output.push_str(format!("{subsecond:09}").trim_end_matches('0')); - } - None => {} - } - } - output -} - -/// `Temporal.PlainTime.prototype.toString(options)`:按精度、舍入模式与最小单位输出 ISO 8601 时间。 -pub fn plain_time_to_string(vm: &mut H, args: &[u8]) -> NativeResult { - let ptr = native_try!(receiver_obj(vm, args)); - let obj = unsafe { &*ptr }; - native_try!(ensure_plain_time(vm, obj)); - let total_ns = get_double_prop(obj, 0) as i128; - let options_value = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; - let (fractional_input, mode_value, smallest_value) = if options_value.is_undefined() { - (FractionalSecondDigitsInput::Auto, "trunc".to_string(), None) - } else { - if !options_value.is_object() { - return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object")); - } - let options_ptr = options_value.as_js_object_ptr(); - if options_ptr.is_null() { - return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object")); - } - let options = unsafe { &*options_ptr }; - let fractional_raw = native_try!(temporal_option_value(vm, options, options_value, "fractionalSecondDigits")); - let fractional = if fractional_raw.is_undefined() { - FractionalSecondDigitsInput::Auto - } else if fractional_raw.is_int() || fractional_raw.is_double() { - FractionalSecondDigitsInput::Number(to_number(fractional_raw)) - } else { - FractionalSecondDigitsInput::String(native_try!(temporal_option_string(vm, fractional_raw))) - }; - let mode_raw = native_try!(temporal_option_value(vm, options, options_value, "roundingMode")); - let mode = if mode_raw.is_undefined() { - "trunc".to_string() - } else { - native_try!(temporal_option_string(vm, mode_raw)) - }; - let smallest_raw = native_try!(temporal_option_value(vm, options, options_value, "smallestUnit")); - let smallest = if smallest_raw.is_undefined() { - None - } else { - Some(native_try!(temporal_option_string(vm, smallest_raw))) - }; - (fractional, mode, smallest) - }; - let fractional_digits = native_try!(parse_fractional_second_digits(vm, fractional_input)); - let Some(mode) = instant_rounding_mode(&mode_value) else { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding mode")); - }; - let (quantum_ns, include_seconds, output_digits) = match smallest_value.as_deref() { - Some("minute" | "minutes") => (60_000_000_000, false, Some(0)), - Some("second" | "seconds") => (1_000_000_000, true, Some(0)), - Some("millisecond" | "milliseconds") => (1_000_000, true, Some(3)), - Some("microsecond" | "microseconds") => (1_000, true, Some(6)), - Some("nanosecond" | "nanoseconds") => (1, true, Some(9)), - Some(_) => return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallest unit")), - None => match fractional_digits { - Some(digits) => (10_i128.pow((9 - digits) as u32), true, Some(digits)), - None => (1, true, None), - }, - }; - // 舍入后可能跨到次日(rounding-cross-midnight),取模保持 0-24 域。 - const DAY_NS: i128 = 86_400_000_000_000; - let Some(rounded_ns) = round_instant_ns(total_ns, quantum_ns, mode) else { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid time")); - }; - let time_ns = rounded_ns.rem_euclid(DAY_NS); - let output = format_plain_time_iso(time_ns, include_seconds, output_digits); - NativeResult::Ok(vm.new_string_owned(output)) -} - -/// `Temporal.PlainTime.prototype.toJSON()`:输出默认 ISO 时间字符串,忽略参数。 -pub fn plain_time_to_json(vm: &mut H, args: &[u8]) -> NativeResult { - plain_time_default_string(vm, args) -} - -/// `Temporal.PlainTime.prototype.toLocaleString()`:当前使用稳定的默认 ISO 时间表示。 -pub fn plain_time_to_locale_string(vm: &mut H, args: &[u8]) -> NativeResult { - plain_time_default_string(vm, args) -} - -/// 输出无 options 的默认 PlainTime 串(全秒 + 非零亚秒)。 -fn plain_time_default_string(vm: &mut H, args: &[u8]) -> NativeResult { - let ptr = native_try!(receiver_obj(vm, args)); - let obj = unsafe { &*ptr }; - native_try!(ensure_plain_time(vm, obj)); - let total_ns = get_double_prop(obj, 0) as i128; - NativeResult::Ok(vm.new_string_owned(format_plain_time_iso(total_ns, true, None))) -} - -/// `Temporal.PlainTime.prototype.valueOf()`:Temporal 对象禁止转原始值。 -pub fn plain_time_value_of(vm: &mut H, _args: &[u8]) -> NativeResult { - NativeResult::Err(crate::error::create_type_error(vm, "Temporal.PlainTime has no valueOf")) -} - -/// `Temporal.PlainTime.from(item, options)`:从字符串、PlainTime/ZDT/PlainDateTime 实例或 property bag 构造。 -pub fn plain_time_from(vm: &mut H, args: &[u8]) -> NativeResult { - let value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - if value.is_string() { - // 规范顺序:先 ParseTemporalTimeString,再 ToTemporalOverflow(options)。 - let total_ns = match parse_plain_time_string(&to_string(value)) { - Some(ns) => ns, - None => { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO 8601 time")); - } - }; - native_try!(temporal_overflow(vm, args)); - make_plain_time(vm, total_ns) - } else { - // 对象分支:实例复制 / property bag 统一走 plain_time_like_ns(含缺省 0 与约束钳制)。 - native_try!(temporal_overflow(vm, args)); - let total_ns = native_try!(plain_time_like_ns(vm, value)); - make_plain_time(vm, total_ns) - } -} - -/// `Temporal.PlainTime.prototype.equals(other)`:各分量全等返回 true,否则 false。 -/// -/// # 边界与前提 -/// - other 经 `plain_time_like_ns` 归一(实例/字符串/property bag),无法转换抛 TypeError/RangeError。 -pub fn plain_time_equals(vm: &mut H, args: &[u8]) -> NativeResult { - let ptr = match receiver_obj(vm, args) { - Ok(p) => p, - Err(error) => return NativeResult::Err(error), - }; - let obj = unsafe { &*ptr }; - if let Err(error) = ensure_plain_time(vm, obj) { - return NativeResult::Err(error); - } - let receiver_ns = get_double_prop(obj, 0); - let other_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let other_ns = match plain_time_like_ns(vm, other_val) { - Ok(ns) => ns, - Err(error) => return NativeResult::Err(error), - }; - NativeResult::Ok(JsValue::bool(receiver_ns as i128 == other_ns as i128)) -} - -/// `Temporal.PlainTime.compare(one, two)`:静态比较,按午夜后纳秒返回 -1、0 或 1。 -pub fn plain_time_compare(vm: &mut H, args: &[u8]) -> NativeResult { - let one = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let two = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; - let one_ns = match plain_time_like_ns(vm, one) { - Ok(ns) => ns, - Err(error) => return NativeResult::Err(error), - }; - let two_ns = match plain_time_like_ns(vm, two) { - Ok(ns) => ns, - Err(error) => return NativeResult::Err(error), - }; - let result = match (one_ns as i128).cmp(&(two_ns as i128)) { - std::cmp::Ordering::Less => -1, - std::cmp::Ordering::Equal => 0, - std::cmp::Ordering::Greater => 1, - }; - NativeResult::Ok(JsValue::int(result)) -} - -// ───────────────────── PlainTime until / since ───────────────────── - -/// until/since 核心:receiver 与 other 归一为午夜后纳秒,经 `difference_core` 计算差值。 -/// -/// # 步骤 -/// 1. branding receiver 取槽 0 ns,other 经 `plain_time_like_ns` 归一。 -/// 2. `parse_difference_settings` 解析 options(default_largest=4=hour)。 -/// 3. 日期固定为 epoch(0,0,0),调 `difference_core` 输出 Duration。 -/// -/// # 边界与前提 -/// - since 复用 same 语义并整体取反(difference_core 内部处理),不调换两端。 -fn plain_time_difference(vm: &mut H, args: &[u8], since: bool) -> NativeResult { - let ptr = match receiver_obj(vm, args) { - Ok(p) => p, - Err(error) => return NativeResult::Err(error), - }; - let obj = unsafe { &*ptr }; - if let Err(error) = ensure_plain_time(vm, obj) { - return NativeResult::Err(error); - } - let receiver_ns = get_double_prop(obj, 0); - let other_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let other_ns = match plain_time_like_ns(vm, other_val) { - Ok(ns) => ns, - Err(error) => return NativeResult::Err(error), - }; - let options_value = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; - let settings = match parse_difference_settings(vm, options_value, false, 4) { - Ok(settings) => settings, - Err(error) => return NativeResult::Err(error), - }; - difference_core(vm, (0, 0, 0), receiver_ns as i128, (0, 0, 0), other_ns as i128, settings, since, 9) -} - -/// `Temporal.PlainTime.prototype.until(other, options)`。 -pub fn plain_time_until(vm: &mut H, args: &[u8]) -> NativeResult { - plain_time_difference(vm, args, false) -} - -/// `Temporal.PlainTime.prototype.since(other, options)`。 -pub fn plain_time_since(vm: &mut H, args: &[u8]) -> NativeResult { - plain_time_difference(vm, args, true) -} - -// ───────────────────── PlainTime add / subtract ───────────────────── - -/// add/subtract 核心:receiver 午夜后纳秒叠加时长的"时间域"增量。 -/// -/// # 步骤 -/// 1. branding receiver 取槽 0 ns。 -/// 2. `duration_like_values` 归一 duration,`temporal_overflow` 解析 options(读序:duration → options)。 -/// 3. 仅取 hours 起的时间字段换算纳秒增量(日期字段 days 及以上规范忽略)。 -/// 4. 按 sign 叠加后 `rem_euclid(DAY_NS)` 保持 0-24 域 → `make_plain_time`。 -/// -/// # 边界与前提 -/// - duration 全 0 → 值不变的新对象(blank-duration 语义)。 -/// - duration 含日期字段(y/m/w/d)不报错,规范对 PlainTime 直接忽略。 -fn plain_time_apply_duration(vm: &mut H, args: &[u8], sign: i64) -> NativeResult { - let ptr = match receiver_obj(vm, args) { - Ok(p) => p, - Err(error) => return NativeResult::Err(error), - }; - let obj = unsafe { &*ptr }; - if let Err(error) = ensure_plain_time(vm, obj) { - return NativeResult::Err(error); - } - let time_ns = get_double_prop(obj, 0) as i128; - let duration_like = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let values = match duration_like_values(vm, duration_like) { - Ok(values) => values, - Err(error) => return NativeResult::Err(error), - }; - if let Err(error) = temporal_overflow(vm, args) { - return NativeResult::Err(error); - } - const DAY_NS: i128 = 86_400_000_000_000; - // 时间字段(hours 起)单独换算纳秒;日期字段 days 及以上对 PlainTime 无意义直接忽略。 - let [_, _, _, _, h, min, s, ms, us, ns] = values; - let time_delta = duration_component_integer(h).unwrap_or(0) * 3_600_000_000_000 - + duration_component_integer(min).unwrap_or(0) * 60_000_000_000 - + duration_component_integer(s).unwrap_or(0) * 1_000_000_000 - + duration_component_integer(ms).unwrap_or(0) * 1_000_000 - + duration_component_integer(us).unwrap_or(0) * 1_000 - + duration_component_integer(ns).unwrap_or(0); - // 时间溢出跨午夜:rem_euclid 保持 0-24 域。 - let new_time_ns = (time_ns + time_delta * sign as i128).rem_euclid(DAY_NS); - make_plain_time(vm, new_time_ns as f64) -} - -/// `Temporal.PlainTime.prototype.add(durationLike, options)`。 -pub fn plain_time_add(vm: &mut H, args: &[u8]) -> NativeResult { - plain_time_apply_duration(vm, args, 1) -} - -/// `Temporal.PlainTime.prototype.subtract(durationLike, options)`。 -pub fn plain_time_subtract(vm: &mut H, args: &[u8]) -> NativeResult { - plain_time_apply_duration(vm, args, -1) -} - -// ───────────────────── PlainTime round ───────────────────── - -/// PlainTime round 的最小单位表:小时..纳秒(不含 day)。 -/// 返回 (单位纳秒, 最大增量)。最大增量 = MaximumTemporalDurationRoundingIncrement: -/// hour→24、minute→60、second→60、millisecond→1000、microsecond→1000、nanosecond→1000。 -fn plain_time_round_unit(value: &str) -> Option<(i128, i128)> { - match value { - "hour" | "hours" => Some((3_600_000_000_000, 24)), - "minute" | "minutes" => Some((60_000_000_000, 60)), - "second" | "seconds" => Some((1_000_000_000, 60)), - "millisecond" | "milliseconds" => Some((1_000_000, 1_000)), - "microsecond" | "microseconds" => Some((1_000, 1_000)), - "nanosecond" | "nanoseconds" => Some((1, 1_000)), - _ => None, - } -} - -/// `Temporal.PlainTime.prototype.round(roundTo)`:按最小单位、增量和模式在"当日"域内舍入。 -/// -/// # 步骤 -/// 1. roundTo 解析:undefined → TypeError;字符串 → {smallestUnit: 串};对象 → 依次 Get -/// roundingIncrement → roundingMode → smallestUnit(读序对齐 order-of-operations)。 -/// 2. branding receiver 取槽 0 ns。 -/// 3. 单位表查 smallestUnit(hour..nanosecond;None → RangeError "invalid smallest unit")。 -/// 4. roundingIncrement 校验:1..=1e9 且 **真因子**(increment < max_increment 且 -/// max_increment % increment == 0,否则 RangeError)。 -/// 5. mode = instant_rounding_mode(None → RangeError);缺省 "halfExpand"。 -/// 6. round_instant_ns(time_ns, unit_ns*increment, mode),结果 rem_euclid(DAY_NS) 保持 0-24 域。 -/// -/// # 边界与前提 -/// - 舍入跨午夜(如 23:59:59.9 舍入到秒)→ 取模回 00:00,符合 rounding-cross-midnight 语义。 -pub fn plain_time_round(vm: &mut H, args: &[u8]) -> NativeResult { - let round_to = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let (increment_value, mode_value, unit_value) = if round_to.is_string() { - (1.0, "halfExpand".to_string(), Some(to_string(round_to))) - } else { - if !round_to.is_object() { - return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be a string or object")); - } - let options_ptr = round_to.as_js_object_ptr(); - if options_ptr.is_null() { - return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be a string or object")); - } - let options = unsafe { &*options_ptr }; - let increment_raw = native_try!(temporal_option_value(vm, options, round_to, "roundingIncrement")); - let increment = if increment_raw.is_undefined() { - 1.0 - } else { - native_try!(temporal_option_number(vm, increment_raw)) - }; - let mode_raw = native_try!(temporal_option_value(vm, options, round_to, "roundingMode")); - let mode = if mode_raw.is_undefined() { - "halfExpand".to_string() - } else { - native_try!(temporal_option_string(vm, mode_raw)) - }; - let unit_raw = native_try!(temporal_option_value(vm, options, round_to, "smallestUnit")); - let unit = if unit_raw.is_undefined() { - None - } else { - Some(native_try!(temporal_option_string(vm, unit_raw))) - }; - (increment, mode, unit) - }; - - let ptr = native_try!(receiver_obj(vm, args)); - let obj = unsafe { &*ptr }; - native_try!(ensure_plain_time(vm, obj)); - let time_ns = get_double_prop(obj, 0) as i128; - - let Some(mode) = instant_rounding_mode(&mode_value) else { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding mode")); - }; - let Some(unit_value) = unit_value else { - return NativeResult::Err(crate::error::create_range_error(vm, "smallestUnit is required")); - }; - let Some((unit_ns, max_increment)) = plain_time_round_unit(&unit_value) else { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallest unit")); - }; - if !increment_value.is_finite() { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); - } - let increment = increment_value.trunc(); - if !(1.0..=1_000_000_000.0).contains(&increment) { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); - } - let increment = increment as i128; - // 真因子校验:增量须严格小于最大增量且能整除(MaximumTemporalDurationRoundingIncrement - // 语义;instant_round 只查 % 的缺陷不在此沿用)。 - if increment >= max_increment || max_increment % increment != 0 { - return NativeResult::Err(crate::error::create_range_error(vm, "rounding increment must divide a day")); - } - let Some(quantum_ns) = unit_ns.checked_mul(increment) else { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); - }; - let Some(rounded_ns) = round_instant_ns(time_ns, quantum_ns, mode) else { - return NativeResult::Err(crate::error::create_range_error(vm, "invalid time")); - }; - const DAY_NS: i128 = 86_400_000_000_000; - let time_ns = rounded_ns.rem_euclid(DAY_NS); - make_plain_time(vm, time_ns as f64) -} - // ───────────────────── PlainDateTime 基础方法 ───────────────────── /// `Temporal.PlainDateTime` 构造器:保存 ISO 日期与午夜后纳秒。 diff --git a/crates/oxide_builtins/src/temporal/plain_time.rs b/crates/oxide_builtins/src/temporal/plain_time.rs new file mode 100644 index 00000000..c6c66193 --- /dev/null +++ b/crates/oxide_builtins/src/temporal/plain_time.rs @@ -0,0 +1,858 @@ +//! Temporal.PlainTime:ISO 时间解析、构造、getter、with/until/since 与加减舍入。 + +use oxide_runtime_api::{to_number, to_string, NativeResult, VmHost}; +use oxide_types::object::JsObject; +use oxide_types::value::JsValue; + +use super::common::native_try; +use super::{ + difference_core, duration_component_integer, duration_like_values, ensure_plain_time, get_double_prop, + initialize_temporal_receiver, instant_rounding_mode, instant_string_without_annotations, is_ctor_call, + make_plain_time, parse_any_offset_minutes, parse_difference_settings, parse_fractional_second_digits, + parse_iso_date, receiver_obj, reject_partial_object_with_calendar_or_time_zone, round_instant_ns, + temporal_number_component, temporal_option_number, temporal_option_string, temporal_option_value, + temporal_overflow, valid_offset_fraction, zoned_date_time_plain_parts, FractionalSecondDigitsInput, +}; + +/// 时间串的日期歧义判定:形如 YYYY-MM / MMDD / YYYYMM / MM-DD 且作为日期合法 → 歧义。 +/// +/// # 边界与前提 +/// - 2 月按闰年处理(0229 判歧义、0230 不判)。 +/// - 月/日非法(13、00、2 月 30 日等)不算歧义,按时间解析。 +fn is_ambiguous_date_string(s: &str) -> bool { + let bytes = s.as_bytes(); + let all_digits = |range: std::ops::Range| { + bytes.get(range.clone()).is_some_and(|part| part.iter().all(u8::is_ascii_digit)) + }; + let two_digits = |at: usize| -> Option { + if at + 1 >= bytes.len() { + None + } else { + Some((bytes[at] - b'0') as i32 * 10 + (bytes[at + 1] - b'0') as i32) + } + }; + let day_in_month = |month: i32, day: i32| -> bool { + // 2 月按闰年(29 天)判定歧义。 + let max = match month { + 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, + 4 | 6 | 9 | 11 => 30, + 2 => 29, + _ => return false, + }; + day >= 1 && day <= max + }; + match s.len() { + 4 if all_digits(0..4) => { + let (month, day) = (two_digits(0).unwrap(), two_digits(2).unwrap()); + day_in_month(month, day) + } + 6 if all_digits(0..6) => (1..=12).contains(&two_digits(4).unwrap()), + 5 if bytes[2] == b'-' && all_digits(0..2) && all_digits(3..5) => { + let (month, day) = (two_digits(0).unwrap(), two_digits(3).unwrap()); + day_in_month(month, day) + } + 7 if bytes[4] == b'-' && all_digits(0..4) && all_digits(5..7) => (1..=12).contains(&two_digits(5).unwrap()), + _ => false, + } +} + +/// 解析时间主体(时[:分[:秒[.小数]]] 或 HHMM / HHMMSS),闰秒按前一秒。 +fn parse_plain_clock(clock: &str) -> Option { + let (clock, fraction) = match clock.find(['.', ',']) { + Some(index) => (&clock[..index], Some(&clock[index + 1..])), + None => (clock, None), + }; + let subsecond = match fraction { + Some(digits) => { + if digits.is_empty() || digits.len() > 9 || !digits.bytes().all(|byte| byte.is_ascii_digit()) { + return None; + } + let mut padded = digits.to_string(); + padded.extend(std::iter::repeat('0').take(9 - digits.len())); + padded.parse::().ok()? + } + None => 0, + }; + let (hour, minute, second) = if clock.contains(':') { + let fields: Vec<&str> = clock.split(':').collect(); + if fields.is_empty() + || fields.len() > 3 + || fields + .iter() + .any(|field| field.len() != 2 || !field.bytes().all(|byte| byte.is_ascii_digit())) + { + return None; + } + if fraction.is_some() && fields.len() < 3 { + return None; + } + ( + fields[0].parse::().ok()?, + fields.get(1).map_or(Ok(0), |field| field.parse()).ok()?, + fields.get(2).map_or(Ok(0), |field| field.parse()).ok()?, + ) + } else { + if !clock.bytes().all(|byte| byte.is_ascii_digit()) + || clock.len() % 2 != 0 + || clock.len() > 6 + || clock.is_empty() + { + return None; + } + if fraction.is_some() && clock.len() < 6 { + return None; + } + let read2 = |at: usize| (clock.as_bytes()[at] - b'0') as u32 * 10 + (clock.as_bytes()[at + 1] - b'0') as u32; + ( + read2(0), + if clock.len() >= 4 { read2(2) } else { 0 }, + if clock.len() >= 6 { read2(4) } else { 0 }, + ) + }; + let second = if second == 60 { 59 } else { second }; + if hour > 23 || minute > 59 || second > 59 { + return None; + } + Some( + hour as f64 * 3_600_000_000_000.0 + + minute as f64 * 60_000_000_000.0 + + second as f64 * 1_000_000_000.0 + + subsecond as f64, + ) +} + +/// ParseTemporalTimeString:时间串 → 当日纳秒(offset/注解被忽略、Z 拒绝)。 +/// +/// # 边界与前提 +/// - Z/z designator → None;纯日期串 → None(不隐式午夜)。 +/// - 日期+时间("1976-11-18T12:34...")→ 取 T 后时间部分;空格分隔需带日期部分。 +/// - 无 T 的数字/连字符形式先做日期歧义判定:日期合法 → None(须 T 前缀); +/// 日期非法 → 按时间解析(HHMM-UU 形式,offset 被忽略)。 +/// - 小数秒 ≤9 位;offset 小数秒 ≤9 位;闰秒按前一秒。 +pub(crate) fn parse_plain_time_string(input: &str) -> Option { + let text = input.trim(); + if text.contains('\u{2212}') { + return None; + } + let body = instant_string_without_annotations(text)?; + if body.contains(['Z', 'z']) { + return None; + } + let sep = body.find(['T', 't', ' ']); + let (date_part, time_part, has_t) = match sep { + Some(0) => ("", &body[1..], matches!(body.as_bytes().first(), Some(b'T' | b't'))), + Some(index) => (&body[..index], &body[index + 1..], matches!(body.as_bytes()[index], b'T' | b't')), + None => ("", body, false), + }; + if !has_t && sep == Some(0) { + // 前导空格不能替代 T 前缀(无日期部分的时间串)。 + return None; + } + if !date_part.is_empty() { + // 完整日期部分必须可解析(负零年等由 parse_iso_date 拒绝)。 + if parse_iso_date(date_part).is_err() { + return None; + } + return parse_plain_time_spec(time_part); + } + if !has_t && !time_part.contains(':') && is_ambiguous_date_string(time_part) { + return None; + } + parse_plain_time_spec(time_part) +} + +/// 时间主体 + 尾部 offset(剥离并忽略):offset 语法非法返回 None。 +fn parse_plain_time_spec(s: &str) -> Option { + let offset_start = s + .char_indices() + .rev() + .find_map(|(index, ch)| matches!(ch, '+' | '-').then_some(index)); + let (clock, offset) = match offset_start { + Some(index) => (&s[..index], &s[index..]), + None => (s, ""), + }; + if !offset.is_empty() && (parse_any_offset_minutes(offset).is_none() || !valid_offset_fraction(offset)) { + return None; + } + parse_plain_clock(clock) +} + +/// ToTemporalTime:PlainTime 对象 / ZDT / PlainDateTime / 字符串 / property bag → 当日纳秒。 +/// +/// # 步骤 +/// 1. undefined → 0(午夜);字符串走 parse_plain_time_string(失败 RangeError)。 +/// 2. PlainTime 读槽 0;ZDT 用其自身时区取本地时间;PlainDateTime 读时间槽。 +/// 3. 其余对象按 ToTemporalTimeRecord 读时间字段(无字段 TypeError)。 +/// +/// # 边界与前提 +/// - 非字符串原始值(number/bigint/null/boolean)→ TypeError;Symbol → TypeError。 +/// - bag 字段缺省 0(完整模式),越界值按 constrain 钳制(second=60 → 59)。 +pub(crate) fn plain_time_like_ns(vm: &mut H, value: JsValue) -> Result { + if value.is_undefined() { + return Ok(0.0); + } + if value.is_string() { + return parse_plain_time_string(&to_string(value)) + .ok_or_else(|| crate::error::create_range_error(vm, "invalid ISO 8601 time")); + } + if value.is_object() { + let ptr = value.as_js_object_ptr(); + if !ptr.is_null() { + let obj = unsafe { &*ptr }; + if obj.is_plain_time_obj() { + return Ok(get_double_prop(obj, 0)); + } + if obj.is_zoned_date_time_obj() { + return zoned_date_time_plain_parts(vm, obj).map(|(_, _, _, time_ns)| time_ns); + } + if obj.is_plain_date_time_obj() { + return Ok(get_double_prop(obj, 3)); + } + return plain_time_bag_ns(vm, value, obj); + } + } + Err(crate::error::create_type_error(vm, "cannot convert to PlainTime")) +} + +/// ToTemporalTimeRecord(完整模式):按字母序读时间字段,缺省 0,无字段 TypeError。 +fn plain_time_bag_ns(vm: &mut H, value: JsValue, obj: &JsObject) -> Result { + let hour_raw = temporal_option_value(vm, obj, value, "hour")?; + let microsecond_raw = temporal_option_value(vm, obj, value, "microsecond")?; + let millisecond_raw = temporal_option_value(vm, obj, value, "millisecond")?; + let minute_raw = temporal_option_value(vm, obj, value, "minute")?; + let nanosecond_raw = temporal_option_value(vm, obj, value, "nanosecond")?; + let second_raw = temporal_option_value(vm, obj, value, "second")?; + if [hour_raw, microsecond_raw, millisecond_raw, minute_raw, nanosecond_raw, second_raw] + .iter() + .all(|raw| raw.is_undefined()) + { + return Err(crate::error::create_type_error(vm, "no time units present")); + } + let mut convert = |raw: JsValue| -> Result { + if raw.is_undefined() { + Ok(0.0) + } else { + let number = temporal_option_number(vm, raw)?; + if !number.is_finite() { + return Err(crate::error::create_range_error(vm, "invalid time component")); + } + Ok(number.trunc()) + } + }; + let hour = convert(hour_raw)?; + let microsecond = convert(microsecond_raw)?; + let millisecond = convert(millisecond_raw)?; + let minute = convert(minute_raw)?; + let nanosecond = convert(nanosecond_raw)?; + let second = convert(second_raw)?; + // RegulateTime(constrain):越界钳制(second=60 → 59)。 + let hour = hour.clamp(0.0, 23.0); + let minute = minute.clamp(0.0, 59.0); + let second = second.clamp(0.0, 59.0); + let millisecond = millisecond.clamp(0.0, 999.0); + let microsecond = microsecond.clamp(0.0, 999.0); + let nanosecond = nanosecond.clamp(0.0, 999.0); + Ok(hour * 3_600_000_000_000.0 + + minute * 60_000_000_000.0 + + second * 1_000_000_000.0 + + millisecond * 1_000_000.0 + + microsecond * 1_000.0 + + nanosecond) +} + +pub(crate) fn valid_plain_time(hour: u32, minute: u32, second: u32, ms: u32, us: u32, ns: u32) -> bool { + hour <= 23 && minute <= 59 && second <= 59 && ms <= 999 && us <= 999 && ns <= 999 +} + +/// `Temporal.PlainTime` 构造器:`new PlainTime(h, m, s, ms, us, ns)`,缺省为 0。 +pub fn plain_time_constructor(vm: &mut H, args: &[u8]) -> NativeResult { + let ctor_proto = vm.session().builtin_world().plain_time_proto.as_ptr(); + if !is_ctor_call(vm, args, ctor_proto) { + return NativeResult::Err(crate::error::create_type_error( + vm, + "Class constructor Temporal.PlainTime cannot be invoked without 'new'", + )); + } + // 时间分量:缺参或显式 undefined 取默认 0(undefined 是合法分量,与 PD + // 缺参抛错相反);其余经 number-only 路径(Symbol/BigInt → TypeError, + // NaN/±Inf/不可解析串 → RangeError)。 + let mut get = |i: usize| -> Result { + let raw = if args.len() > i { vm.reg(args[i]) } else { JsValue::undefined() }; + if raw.is_undefined() { + return Ok(0.0); + } + temporal_number_component(vm, raw) + }; + // 负值在 u32 转换前检查:截断后为负的整分量(含负分数整数化为负)须先抛错, + // 不能让 f64→u32 饱和把其静默归 0 而逃过范围检查。 + let hour_value = native_try!(get(1)); + let minute_value = native_try!(get(2)); + let second_value = native_try!(get(3)); + let ms_value = native_try!(get(4)); + let us_value = native_try!(get(5)); + let ns_value = native_try!(get(6)); + if [hour_value, minute_value, second_value, ms_value, us_value, ns_value] + .iter() + .any(|value| *value < 0.0) + { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid time component")); + } + let hour = hour_value as u32; + let minute = minute_value as u32; + let second = second_value as u32; + let ms = ms_value as u32; + let us = us_value as u32; + let ns = ns_value as u32; + if !valid_plain_time(hour, minute, second, ms, us, ns) { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid time component")); + } + let total_ns = hour as f64 * 3.6e12 + + minute as f64 * 6e10 + + second as f64 * 1e9 + + ms as f64 * 1e6 + + us as f64 * 1e3 + + ns as f64; + initialize_temporal_receiver(vm, args, JsObject::OBJ_TYPE_PLAIN_TIME, [JsValue::float(total_ns)]) +} + +/// 拆解午夜后纳秒为各分量。 +/// 带符号分解午夜后纳秒为时/分/秒/毫秒/微秒/纳秒(各分量向零截断,保持同号)。 +pub(crate) fn plain_time_components_signed(total_ns: i128) -> [i128; 6] { + const SCALES: [i128; 5] = [3_600_000_000_000, 60_000_000_000, 1_000_000_000, 1_000_000, 1_000]; + let mut values = [0_i128; 6]; + let mut remainder = total_ns; + for (index, scale) in SCALES.iter().enumerate() { + values[index] = remainder / scale; + remainder %= scale; + } + values[5] = remainder; + values +} + +pub(crate) fn plain_time_components(total_ns: f64) -> (u32, u32, u32, u32, u32, u32) { + let total = total_ns as u64; + let ns = total % 1_000; + let us = total / 1_000 % 1_000; + let ms = total / 1_000_000 % 1_000; + let second = total / 1_000_000_000 % 60; + let minute = total / 60_000_000_000 % 60; + let hour = total / 3_600_000_000_000 % 24; + (hour as u32, minute as u32, second as u32, ms as u32, us as u32, ns as u32) +} + +fn plain_time_get(vm: &mut H, args: &[u8], select: fn(u32, u32, u32, u32, u32, u32) -> u32) -> NativeResult { + let ptr = match receiver_obj(vm, args) { + Ok(p) => p, + Err(e) => return NativeResult::Err(e), + }; + let obj = unsafe { &*ptr }; + if let Err(e) = ensure_plain_time(vm, obj) { + return NativeResult::Err(e); + } + let (h, m, s, ms, us, ns) = plain_time_components(get_double_prop(obj, 0)); + NativeResult::Ok(JsValue::float(select(h, m, s, ms, us, ns) as f64)) +} + +/// `Temporal.PlainTime.prototype.hour` getter。 +pub fn plain_time_hour(vm: &mut H, args: &[u8]) -> NativeResult { + plain_time_get(vm, args, |h, _, _, _, _, _| h) +} + +/// `Temporal.PlainTime.prototype.minute` getter。 +pub fn plain_time_minute(vm: &mut H, args: &[u8]) -> NativeResult { + plain_time_get(vm, args, |_, m, _, _, _, _| m) +} + +/// `Temporal.PlainTime.prototype.second` getter。 +pub fn plain_time_second(vm: &mut H, args: &[u8]) -> NativeResult { + plain_time_get(vm, args, |_, _, s, _, _, _| s) +} + +/// `Temporal.PlainTime.prototype.millisecond` getter。 +pub fn plain_time_millisecond(vm: &mut H, args: &[u8]) -> NativeResult { + plain_time_get(vm, args, |_, _, _, ms, _, _| ms) +} + +/// `Temporal.PlainTime.prototype.microsecond` getter。 +pub fn plain_time_microsecond(vm: &mut H, args: &[u8]) -> NativeResult { + plain_time_get(vm, args, |_, _, _, _, us, _| us) +} + +/// `Temporal.PlainTime.prototype.nanosecond` getter。 +pub fn plain_time_nanosecond(vm: &mut H, args: &[u8]) -> NativeResult { + plain_time_get(vm, args, |_, _, _, _, _, ns| ns) +} + +/// `Temporal.PlainTime.prototype.with(temporalTimeLike [, options])`:替换 +/// receiver 的指定时间分量,返回新 PlainTime。 +/// +/// # 步骤 +/// 1. branding + ToTemporalTimeLike(非对象 / Temporal 实例 / calendar、 +/// timeZone 有定义 → TypeError;字段按字典序读 hour → microsecond → +/// millisecond → minute → nanosecond → second,非数字 → TypeError, +/// NaN/±Inf → RangeError)。 +/// 2. 分量全 undefined → TypeError。 +/// 3. options → overflow(缺省 constrain)。 +/// 4. RegulateTime:constrain 钳制;reject 先校验范围再截断(负 / 超界分量 +/// 立即抛 RangeError)。 +/// 5. 合并分量算午夜后纳秒,构造新 PlainTime。 +/// +/// # 边界与前提 +/// - 字段读取先于选项解析(options-wrong-type 先报字段错误,与 ZDT with 同口径)。 +/// - receiver 分量取自午夜后纳秒槽;partial 中 undefined 的分量保留 receiver 值。 +pub fn plain_time_with(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_plain_time(vm, obj)); + let value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + native_try!(reject_partial_object_with_calendar_or_time_zone(vm, value)); + + // 字段读取按字典序,undefined 不覆盖;number-only 路径(Symbol/BigInt → + // TypeError,NaN/±Inf → RangeError)。 + let bag = unsafe { &*value.as_js_object_ptr() }; + let read = |vm: &mut H, name: &str| -> Result, JsValue> { + let raw = temporal_option_value(vm, bag, value, name)?; + if raw.is_undefined() { + Ok(None) + } else { + Ok(Some(temporal_number_component(vm, raw)?)) + } + }; + let hour = native_try!(read(vm, "hour")); + let microsecond = native_try!(read(vm, "microsecond")); + let millisecond = native_try!(read(vm, "millisecond")); + let minute = native_try!(read(vm, "minute")); + let nanosecond = native_try!(read(vm, "nanosecond")); + let second = native_try!(read(vm, "second")); + if [hour, microsecond, millisecond, minute, nanosecond, second] + .iter() + .all(|c| c.is_none()) + { + return NativeResult::Err(crate::error::create_type_error(vm, "no properties present")); + } + + // options:字段读取完成后解析 overflow。 + let constrain = native_try!(temporal_overflow(vm, args)); + + // RegulateTime:partial 有定义的分量覆盖 receiver 分量。 + let (recv_hour, recv_minute, recv_second, recv_ms, recv_us, recv_ns) = + plain_time_components(get_double_prop(obj, 0)); + let merged = [ + hour.unwrap_or(recv_hour as f64), + minute.unwrap_or(recv_minute as f64), + second.unwrap_or(recv_second as f64), + millisecond.unwrap_or(recv_ms as f64), + microsecond.unwrap_or(recv_us as f64), + nanosecond.unwrap_or(recv_ns as f64), + ]; + let values = if constrain { + [ + merged[0].clamp(0.0, 23.0), + merged[1].clamp(0.0, 59.0), + merged[2].clamp(0.0, 59.0), + merged[3].clamp(0.0, 999.0), + merged[4].clamp(0.0, 999.0), + merged[5].clamp(0.0, 999.0), + ] + } else { + // reject 语义须先做范围校验再截断:负分量或超上限分量立即抛错, + // 不允许 f64→u32 饱和把负值静默归 0 而逃过范围检查。 + let limits = [23.0, 59.0, 59.0, 999.0, 999.0, 999.0]; + if merged.iter().zip(limits.iter()).any(|(v, limit)| *v < 0.0 || *v > *limit) { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid time component")); + } + merged + }; + let total_ns = values[0] * 3_600_000_000_000.0 + + values[1] * 60_000_000_000.0 + + values[2] * 1_000_000_000.0 + + values[3] * 1_000_000.0 + + values[4] * 1_000.0 + + values[5]; + make_plain_time(vm, total_ns) +} + +/// 按是否含秒与小数位格式化当日纳秒为 `HH:MM:SS[.frac]`。 +/// +/// # 边界与前提 +/// - `include_seconds=false` 时忽略小数位,仅输出 `HH:MM`。 +/// - `Some(0)` 不输出小数;`Some(digits)` 固定补足位数;`None` 按实际非零亚秒去尾零。 +pub(crate) fn format_plain_time_iso(time_ns: i128, include_seconds: bool, fractional_digits: Option) -> String { + let hour = time_ns / 3_600_000_000_000; + let minute = time_ns / 60_000_000_000 % 60; + let second = time_ns / 1_000_000_000 % 60; + let subsecond = time_ns % 1_000_000_000; + let mut output = format!("{hour:02}:{minute:02}"); + if include_seconds { + output.push_str(&format!(":{second:02}")); + match fractional_digits { + Some(0) => {} + Some(digits) => { + let fraction = format!("{subsecond:09}"); + output.push('.'); + output.push_str(&fraction[..digits]); + } + None if subsecond != 0 => { + output.push('.'); + output.push_str(format!("{subsecond:09}").trim_end_matches('0')); + } + None => {} + } + } + output +} + +/// `Temporal.PlainTime.prototype.toString(options)`:按精度、舍入模式与最小单位输出 ISO 8601 时间。 +pub fn plain_time_to_string(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_plain_time(vm, obj)); + let total_ns = get_double_prop(obj, 0) as i128; + let options_value = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) }; + let (fractional_input, mode_value, smallest_value) = if options_value.is_undefined() { + (FractionalSecondDigitsInput::Auto, "trunc".to_string(), None) + } else { + if !options_value.is_object() { + return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object")); + } + let options_ptr = options_value.as_js_object_ptr(); + if options_ptr.is_null() { + return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object")); + } + let options = unsafe { &*options_ptr }; + let fractional_raw = native_try!(temporal_option_value(vm, options, options_value, "fractionalSecondDigits")); + let fractional = if fractional_raw.is_undefined() { + FractionalSecondDigitsInput::Auto + } else if fractional_raw.is_int() || fractional_raw.is_double() { + FractionalSecondDigitsInput::Number(to_number(fractional_raw)) + } else { + FractionalSecondDigitsInput::String(native_try!(temporal_option_string(vm, fractional_raw))) + }; + let mode_raw = native_try!(temporal_option_value(vm, options, options_value, "roundingMode")); + let mode = if mode_raw.is_undefined() { + "trunc".to_string() + } else { + native_try!(temporal_option_string(vm, mode_raw)) + }; + let smallest_raw = native_try!(temporal_option_value(vm, options, options_value, "smallestUnit")); + let smallest = if smallest_raw.is_undefined() { + None + } else { + Some(native_try!(temporal_option_string(vm, smallest_raw))) + }; + (fractional, mode, smallest) + }; + let fractional_digits = native_try!(parse_fractional_second_digits(vm, fractional_input)); + let Some(mode) = instant_rounding_mode(&mode_value) else { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding mode")); + }; + let (quantum_ns, include_seconds, output_digits) = match smallest_value.as_deref() { + Some("minute" | "minutes") => (60_000_000_000, false, Some(0)), + Some("second" | "seconds") => (1_000_000_000, true, Some(0)), + Some("millisecond" | "milliseconds") => (1_000_000, true, Some(3)), + Some("microsecond" | "microseconds") => (1_000, true, Some(6)), + Some("nanosecond" | "nanoseconds") => (1, true, Some(9)), + Some(_) => return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallest unit")), + None => match fractional_digits { + Some(digits) => (10_i128.pow((9 - digits) as u32), true, Some(digits)), + None => (1, true, None), + }, + }; + // 舍入后可能跨到次日(rounding-cross-midnight),取模保持 0-24 域。 + const DAY_NS: i128 = 86_400_000_000_000; + let Some(rounded_ns) = round_instant_ns(total_ns, quantum_ns, mode) else { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid time")); + }; + let time_ns = rounded_ns.rem_euclid(DAY_NS); + let output = format_plain_time_iso(time_ns, include_seconds, output_digits); + NativeResult::Ok(vm.new_string_owned(output)) +} + +/// `Temporal.PlainTime.prototype.toJSON()`:输出默认 ISO 时间字符串,忽略参数。 +pub fn plain_time_to_json(vm: &mut H, args: &[u8]) -> NativeResult { + plain_time_default_string(vm, args) +} + +/// `Temporal.PlainTime.prototype.toLocaleString()`:当前使用稳定的默认 ISO 时间表示。 +pub fn plain_time_to_locale_string(vm: &mut H, args: &[u8]) -> NativeResult { + plain_time_default_string(vm, args) +} + +/// 输出无 options 的默认 PlainTime 串(全秒 + 非零亚秒)。 +fn plain_time_default_string(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_plain_time(vm, obj)); + let total_ns = get_double_prop(obj, 0) as i128; + NativeResult::Ok(vm.new_string_owned(format_plain_time_iso(total_ns, true, None))) +} + +/// `Temporal.PlainTime.prototype.valueOf()`:Temporal 对象禁止转原始值。 +pub fn plain_time_value_of(vm: &mut H, _args: &[u8]) -> NativeResult { + NativeResult::Err(crate::error::create_type_error(vm, "Temporal.PlainTime has no valueOf")) +} + +/// `Temporal.PlainTime.from(item, options)`:从字符串、PlainTime/ZDT/PlainDateTime 实例或 property bag 构造。 +pub fn plain_time_from(vm: &mut H, args: &[u8]) -> NativeResult { + let value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + if value.is_string() { + // 规范顺序:先 ParseTemporalTimeString,再 ToTemporalOverflow(options)。 + let total_ns = match parse_plain_time_string(&to_string(value)) { + Some(ns) => ns, + None => { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO 8601 time")); + } + }; + native_try!(temporal_overflow(vm, args)); + make_plain_time(vm, total_ns) + } else { + // 对象分支:实例复制 / property bag 统一走 plain_time_like_ns(含缺省 0 与约束钳制)。 + native_try!(temporal_overflow(vm, args)); + let total_ns = native_try!(plain_time_like_ns(vm, value)); + make_plain_time(vm, total_ns) + } +} + +/// `Temporal.PlainTime.prototype.equals(other)`:各分量全等返回 true,否则 false。 +/// +/// # 边界与前提 +/// - other 经 `plain_time_like_ns` 归一(实例/字符串/property bag),无法转换抛 TypeError/RangeError。 +pub fn plain_time_equals(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = match receiver_obj(vm, args) { + Ok(p) => p, + Err(error) => return NativeResult::Err(error), + }; + let obj = unsafe { &*ptr }; + if let Err(error) = ensure_plain_time(vm, obj) { + return NativeResult::Err(error); + } + let receiver_ns = get_double_prop(obj, 0); + let other_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let other_ns = match plain_time_like_ns(vm, other_val) { + Ok(ns) => ns, + Err(error) => return NativeResult::Err(error), + }; + NativeResult::Ok(JsValue::bool(receiver_ns as i128 == other_ns as i128)) +} + +/// `Temporal.PlainTime.compare(one, two)`:静态比较,按午夜后纳秒返回 -1、0 或 1。 +pub fn plain_time_compare(vm: &mut H, args: &[u8]) -> NativeResult { + let one = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let two = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; + let one_ns = match plain_time_like_ns(vm, one) { + Ok(ns) => ns, + Err(error) => return NativeResult::Err(error), + }; + let two_ns = match plain_time_like_ns(vm, two) { + Ok(ns) => ns, + Err(error) => return NativeResult::Err(error), + }; + let result = match (one_ns as i128).cmp(&(two_ns as i128)) { + std::cmp::Ordering::Less => -1, + std::cmp::Ordering::Equal => 0, + std::cmp::Ordering::Greater => 1, + }; + NativeResult::Ok(JsValue::int(result)) +} + +// ───────────────────── PlainTime until / since ───────────────────── + +/// until/since 核心:receiver 与 other 归一为午夜后纳秒,经 `difference_core` 计算差值。 +/// +/// # 步骤 +/// 1. branding receiver 取槽 0 ns,other 经 `plain_time_like_ns` 归一。 +/// 2. `parse_difference_settings` 解析 options(default_largest=4=hour)。 +/// 3. 日期固定为 epoch(0,0,0),调 `difference_core` 输出 Duration。 +/// +/// # 边界与前提 +/// - since 复用 same 语义并整体取反(difference_core 内部处理),不调换两端。 +fn plain_time_difference(vm: &mut H, args: &[u8], since: bool) -> NativeResult { + let ptr = match receiver_obj(vm, args) { + Ok(p) => p, + Err(error) => return NativeResult::Err(error), + }; + let obj = unsafe { &*ptr }; + if let Err(error) = ensure_plain_time(vm, obj) { + return NativeResult::Err(error); + } + let receiver_ns = get_double_prop(obj, 0); + let other_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let other_ns = match plain_time_like_ns(vm, other_val) { + Ok(ns) => ns, + Err(error) => return NativeResult::Err(error), + }; + let options_value = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; + let settings = match parse_difference_settings(vm, options_value, false, 4) { + Ok(settings) => settings, + Err(error) => return NativeResult::Err(error), + }; + difference_core(vm, (0, 0, 0), receiver_ns as i128, (0, 0, 0), other_ns as i128, settings, since, 9) +} + +/// `Temporal.PlainTime.prototype.until(other, options)`。 +pub fn plain_time_until(vm: &mut H, args: &[u8]) -> NativeResult { + plain_time_difference(vm, args, false) +} + +/// `Temporal.PlainTime.prototype.since(other, options)`。 +pub fn plain_time_since(vm: &mut H, args: &[u8]) -> NativeResult { + plain_time_difference(vm, args, true) +} + +// ───────────────────── PlainTime add / subtract ───────────────────── + +/// add/subtract 核心:receiver 午夜后纳秒叠加时长的"时间域"增量。 +/// +/// # 步骤 +/// 1. branding receiver 取槽 0 ns。 +/// 2. `duration_like_values` 归一 duration,`temporal_overflow` 解析 options(读序:duration → options)。 +/// 3. 仅取 hours 起的时间字段换算纳秒增量(日期字段 days 及以上规范忽略)。 +/// 4. 按 sign 叠加后 `rem_euclid(DAY_NS)` 保持 0-24 域 → `make_plain_time`。 +/// +/// # 边界与前提 +/// - duration 全 0 → 值不变的新对象(blank-duration 语义)。 +/// - duration 含日期字段(y/m/w/d)不报错,规范对 PlainTime 直接忽略。 +fn plain_time_apply_duration(vm: &mut H, args: &[u8], sign: i64) -> NativeResult { + let ptr = match receiver_obj(vm, args) { + Ok(p) => p, + Err(error) => return NativeResult::Err(error), + }; + let obj = unsafe { &*ptr }; + if let Err(error) = ensure_plain_time(vm, obj) { + return NativeResult::Err(error); + } + let time_ns = get_double_prop(obj, 0) as i128; + let duration_like = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let values = match duration_like_values(vm, duration_like) { + Ok(values) => values, + Err(error) => return NativeResult::Err(error), + }; + if let Err(error) = temporal_overflow(vm, args) { + return NativeResult::Err(error); + } + const DAY_NS: i128 = 86_400_000_000_000; + // 时间字段(hours 起)单独换算纳秒;日期字段 days 及以上对 PlainTime 无意义直接忽略。 + let [_, _, _, _, h, min, s, ms, us, ns] = values; + let time_delta = duration_component_integer(h).unwrap_or(0) * 3_600_000_000_000 + + duration_component_integer(min).unwrap_or(0) * 60_000_000_000 + + duration_component_integer(s).unwrap_or(0) * 1_000_000_000 + + duration_component_integer(ms).unwrap_or(0) * 1_000_000 + + duration_component_integer(us).unwrap_or(0) * 1_000 + + duration_component_integer(ns).unwrap_or(0); + // 时间溢出跨午夜:rem_euclid 保持 0-24 域。 + let new_time_ns = (time_ns + time_delta * sign as i128).rem_euclid(DAY_NS); + make_plain_time(vm, new_time_ns as f64) +} + +/// `Temporal.PlainTime.prototype.add(durationLike, options)`。 +pub fn plain_time_add(vm: &mut H, args: &[u8]) -> NativeResult { + plain_time_apply_duration(vm, args, 1) +} + +/// `Temporal.PlainTime.prototype.subtract(durationLike, options)`。 +pub fn plain_time_subtract(vm: &mut H, args: &[u8]) -> NativeResult { + plain_time_apply_duration(vm, args, -1) +} + +// ───────────────────── PlainTime round ───────────────────── + +/// PlainTime round 的最小单位表:小时..纳秒(不含 day)。 +/// 返回 (单位纳秒, 最大增量)。最大增量 = MaximumTemporalDurationRoundingIncrement: +/// hour→24、minute→60、second→60、millisecond→1000、microsecond→1000、nanosecond→1000。 +pub(crate) fn plain_time_round_unit(value: &str) -> Option<(i128, i128)> { + match value { + "hour" | "hours" => Some((3_600_000_000_000, 24)), + "minute" | "minutes" => Some((60_000_000_000, 60)), + "second" | "seconds" => Some((1_000_000_000, 60)), + "millisecond" | "milliseconds" => Some((1_000_000, 1_000)), + "microsecond" | "microseconds" => Some((1_000, 1_000)), + "nanosecond" | "nanoseconds" => Some((1, 1_000)), + _ => None, + } +} + +/// `Temporal.PlainTime.prototype.round(roundTo)`:按最小单位、增量和模式在"当日"域内舍入。 +/// +/// # 步骤 +/// 1. roundTo 解析:undefined → TypeError;字符串 → {smallestUnit: 串};对象 → 依次 Get +/// roundingIncrement → roundingMode → smallestUnit(读序对齐 order-of-operations)。 +/// 2. branding receiver 取槽 0 ns。 +/// 3. 单位表查 smallestUnit(hour..nanosecond;None → RangeError "invalid smallest unit")。 +/// 4. roundingIncrement 校验:1..=1e9 且 **真因子**(increment < max_increment 且 +/// max_increment % increment == 0,否则 RangeError)。 +/// 5. mode = instant_rounding_mode(None → RangeError);缺省 "halfExpand"。 +/// 6. round_instant_ns(time_ns, unit_ns*increment, mode),结果 rem_euclid(DAY_NS) 保持 0-24 域。 +/// +/// # 边界与前提 +/// - 舍入跨午夜(如 23:59:59.9 舍入到秒)→ 取模回 00:00,符合 rounding-cross-midnight 语义。 +pub fn plain_time_round(vm: &mut H, args: &[u8]) -> NativeResult { + let round_to = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let (increment_value, mode_value, unit_value) = if round_to.is_string() { + (1.0, "halfExpand".to_string(), Some(to_string(round_to))) + } else { + if !round_to.is_object() { + return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be a string or object")); + } + let options_ptr = round_to.as_js_object_ptr(); + if options_ptr.is_null() { + return NativeResult::Err(crate::error::create_type_error(vm, "roundTo must be a string or object")); + } + let options = unsafe { &*options_ptr }; + let increment_raw = native_try!(temporal_option_value(vm, options, round_to, "roundingIncrement")); + let increment = if increment_raw.is_undefined() { + 1.0 + } else { + native_try!(temporal_option_number(vm, increment_raw)) + }; + let mode_raw = native_try!(temporal_option_value(vm, options, round_to, "roundingMode")); + let mode = if mode_raw.is_undefined() { + "halfExpand".to_string() + } else { + native_try!(temporal_option_string(vm, mode_raw)) + }; + let unit_raw = native_try!(temporal_option_value(vm, options, round_to, "smallestUnit")); + let unit = if unit_raw.is_undefined() { + None + } else { + Some(native_try!(temporal_option_string(vm, unit_raw))) + }; + (increment, mode, unit) + }; + + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_plain_time(vm, obj)); + let time_ns = get_double_prop(obj, 0) as i128; + + let Some(mode) = instant_rounding_mode(&mode_value) else { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding mode")); + }; + let Some(unit_value) = unit_value else { + return NativeResult::Err(crate::error::create_range_error(vm, "smallestUnit is required")); + }; + let Some((unit_ns, max_increment)) = plain_time_round_unit(&unit_value) else { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid smallest unit")); + }; + if !increment_value.is_finite() { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); + } + let increment = increment_value.trunc(); + if !(1.0..=1_000_000_000.0).contains(&increment) { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); + } + let increment = increment as i128; + // 真因子校验:增量须严格小于最大增量且能整除(MaximumTemporalDurationRoundingIncrement + // 语义;instant_round 只查 % 的缺陷不在此沿用)。 + if increment >= max_increment || max_increment % increment != 0 { + return NativeResult::Err(crate::error::create_range_error(vm, "rounding increment must divide a day")); + } + let Some(quantum_ns) = unit_ns.checked_mul(increment) else { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid rounding increment")); + }; + let Some(rounded_ns) = round_instant_ns(time_ns, quantum_ns, mode) else { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid time")); + }; + const DAY_NS: i128 = 86_400_000_000_000; + let time_ns = rounded_ns.rem_euclid(DAY_NS); + make_plain_time(vm, time_ns as f64) +}