Files
OxideJS-New/crates/oxide_builtins/src/temporal/plain_time.rs
T

847 lines
37 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Temporal.PlainTime:ISO 时间解析、构造、getter、with/until/since 与加减舍入。
use oxide_runtime_api::{to_number, to_string, NativeResult, ProtoKind, 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_seconds, parse_difference_settings, parse_fractional_second_digits,
parse_iso_date, parse_round_options, 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, IsoDateTime,
};
/// 时间串的日期歧义判定:形如 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<usize>| {
bytes.get(range.clone()).is_some_and(|part| part.iter().all(u8::is_ascii_digit))
};
let two_digits = |at: usize| -> Option<i32> {
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) => match (two_digits(0), two_digits(2)) {
(Some(month), Some(day)) => day_in_month(month, day),
_ => false,
},
6 if all_digits(0..6) => match two_digits(4) {
Some(m) => (1..=12).contains(&m),
None => false,
},
5 if bytes[2] == b'-' && all_digits(0..2) && all_digits(3..5) => match (two_digits(0), two_digits(3)) {
(Some(month), Some(day)) => day_in_month(month, day),
_ => false,
},
7 if bytes[4] == b'-' && all_digits(0..4) && all_digits(5..7) => match two_digits(5) {
Some(m) => (1..=12).contains(&m),
None => false,
},
_ => false,
}
}
/// 解析时间主体(时[:分[:秒[.小数]]] 或 HHMM / HHMMSS),闰秒按前一秒。
fn parse_plain_clock(clock: &str) -> Option<f64> {
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::<u32>().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::<u32>().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<f64> {
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<f64> {
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_seconds(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<H: VmHost>(vm: &mut H, value: JsValue) -> Result<f64, JsValue> {
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<H: VmHost>(vm: &mut H, value: JsValue, obj: &JsObject) -> Result<f64, JsValue> {
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<f64, JsValue> {
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<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ctor_proto = vm.builtin_proto(ProtoKind::PlainTimeProto);
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<f64, JsValue> {
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"));
}
// f64 截断后可能超出 u32 范围,先做有界转换。
let mut to_u32 = |v: f64| -> Result<u32, JsValue> {
if v >= 0.0 && v <= u32::MAX as f64 {
Ok(v as u32)
} else {
Err(crate::error::create_range_error(vm, "invalid time component"))
}
};
let hour = native_try!(to_u32(hour_value));
let minute = native_try!(to_u32(minute_value));
let second = native_try!(to_u32(second_value));
let ms = native_try!(to_u32(ms_value));
let us = native_try!(to_u32(us_value));
let ns = native_try!(to_u32(ns_value));
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<H: VmHost>(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<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_time_get(vm, args, |h, _, _, _, _, _| h)
}
/// `Temporal.PlainTime.prototype.minute` getter。
pub fn plain_time_minute<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_time_get(vm, args, |_, m, _, _, _, _| m)
}
/// `Temporal.PlainTime.prototype.second` getter。
pub fn plain_time_second<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_time_get(vm, args, |_, _, s, _, _, _| s)
}
/// `Temporal.PlainTime.prototype.millisecond` getter。
pub fn plain_time_millisecond<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_time_get(vm, args, |_, _, _, ms, _, _| ms)
}
/// `Temporal.PlainTime.prototype.microsecond` getter。
pub fn plain_time_microsecond<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_time_get(vm, args, |_, _, _, _, us, _| us)
}
/// `Temporal.PlainTime.prototype.nanosecond` getter。
pub fn plain_time_nanosecond<H: VmHost>(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。
///
/// # 边界与前提
/// - 字段读取先于选项解析:即使选项本身类型不合法,也先读完 partial 字段并让字段的
/// 类型错误先抛出,与 ZonedDateTime 的 with 一致。
/// - receiver 分量取自午夜后纳秒槽;partial 中 undefined 的分量保留 receiver 值。
pub fn plain_time_with<H: VmHost>(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<Option<f64>, 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<usize>) -> 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<H: VmHost>(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<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_time_default_string(vm, args)
}
/// `Temporal.PlainTime.prototype.toLocaleString()`:当前使用稳定的默认 ISO 时间表示。
pub fn plain_time_to_locale_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_time_default_string(vm, args)
}
/// 输出无 options 的默认 PlainTime 串(全秒 + 非零亚秒)。
fn plain_time_default_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let ptr = native_try!(receiver_obj(vm, args));
let obj = unsafe { &*ptr };
native_try!(ensure_plain_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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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,
IsoDateTime {
date: (0, 0, 0),
time_ns: receiver_ns as i128,
},
IsoDateTime {
date: (0, 0, 0),
time_ns: other_ns as i128,
},
settings,
since,
9,
)
}
/// `Temporal.PlainTime.prototype.until(other, options)`。
pub fn plain_time_until<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_time_difference(vm, args, false)
}
/// `Temporal.PlainTime.prototype.since(other, options)`。
pub fn plain_time_since<H: VmHost>(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<H: VmHost>(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<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
plain_time_apply_duration(vm, args, 1)
}
/// `Temporal.PlainTime.prototype.subtract(durationLike, options)`。
pub fn plain_time_subtract<H: VmHost>(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: 串};对象 → 按
/// roundingIncrement、roundingMode、smallestUnit 的固定读取顺序依次取值。
/// 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<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let round_to = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let (increment, mode, unit_value) = native_try!(parse_round_options(vm, round_to));
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(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"));
};
// 真因子校验:增量须严格小于最大增量且能整除(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)
}