feat: implement PlainDateTime toString options and ISO range parsing
This commit is contained in:
@@ -1791,9 +1791,28 @@ duration_getter!(duration_milliseconds, 7);
|
|||||||
duration_getter!(duration_microseconds, 8);
|
duration_getter!(duration_microseconds, 8);
|
||||||
duration_getter!(duration_nanoseconds, 9);
|
duration_getter!(duration_nanoseconds, 9);
|
||||||
|
|
||||||
/// 校验 ISO 日期分量(month 1-12、day 按月份与闰年)。
|
/// 校验 ISO 日期分量(month 1-12、day 按月份与闰年,不依赖 chrono 年份范围)。
|
||||||
fn valid_iso_date(year: i32, month: u32, day: u32) -> bool {
|
fn valid_iso_date(year: i32, month: u32, day: u32) -> bool {
|
||||||
NaiveDate::from_ymd_opt(year, month, day).is_some()
|
match days_in_month(i128::from(year), i128::from(month)) {
|
||||||
|
Some(days) => day != 0 && day as i128 <= days,
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 计算 ISO 日期时间的纪元纳秒;分量须已通过 `valid_iso_date`。
|
||||||
|
fn iso_date_time_epoch_ns(year: i32, month: u32, day: u32, time_ns: f64) -> Option<i128> {
|
||||||
|
let days = days_from_civil(i128::from(year), i128::from(month), i128::from(day));
|
||||||
|
days.checked_mul(86_400_000_000_000)?.checked_add(time_ns as i128)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 校验日期时间落在 PlainDateTime 表示范围(约 ±(10^8 + 1) 天,边界互斥)。
|
||||||
|
fn valid_plain_date_time_range(year: i32, month: u32, day: u32, time_ns: f64) -> bool {
|
||||||
|
const MAX_ISO_DAY: i128 = 100_000_000;
|
||||||
|
let limit = (MAX_ISO_DAY + 1) * 86_400_000_000_000;
|
||||||
|
match iso_date_time_epoch_ns(year, month, day, time_ns) {
|
||||||
|
Some(epoch_ns) => epoch_ns > -limit && epoch_ns < limit,
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 读两位数字(basic 格式的月/日等紧凑分量)。
|
/// 读两位数字(basic 格式的月/日等紧凑分量)。
|
||||||
@@ -1879,7 +1898,7 @@ fn parse_iso_date(s: &str) -> Result<(i32, u32, u32), String> {
|
|||||||
};
|
};
|
||||||
let rest = &t[i..];
|
let rest = &t[i..];
|
||||||
if !rest.is_empty() {
|
if !rest.is_empty() {
|
||||||
let ok = rest.starts_with(['T', 't', '[', '+', '-', 'Z', 'z']);
|
let ok = rest.starts_with(['T', 't', '[']);
|
||||||
if !ok {
|
if !ok {
|
||||||
return Err("invalid trailing content".into());
|
return Err("invalid trailing content".into());
|
||||||
}
|
}
|
||||||
@@ -2177,6 +2196,9 @@ pub fn plain_date_time_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> Native
|
|||||||
+ millisecond as f64 * 1_000_000.0
|
+ millisecond as f64 * 1_000_000.0
|
||||||
+ microsecond as f64 * 1_000.0
|
+ microsecond as f64 * 1_000.0
|
||||||
+ nanosecond as f64;
|
+ nanosecond as f64;
|
||||||
|
if !valid_plain_date_time_range(year, month, day, total_ns) {
|
||||||
|
return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time component"));
|
||||||
|
}
|
||||||
initialize_temporal_receiver(
|
initialize_temporal_receiver(
|
||||||
vm,
|
vm,
|
||||||
args,
|
args,
|
||||||
@@ -2192,6 +2214,7 @@ pub fn plain_date_time_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> Native
|
|||||||
|
|
||||||
fn validate_temporal_annotation_suffix(mut suffix: &str) -> Result<(), String> {
|
fn validate_temporal_annotation_suffix(mut suffix: &str) -> Result<(), String> {
|
||||||
let mut calendar_seen = false;
|
let mut calendar_seen = false;
|
||||||
|
let mut saw_critical_calendar = false;
|
||||||
let mut time_zone_seen = false;
|
let mut time_zone_seen = false;
|
||||||
while !suffix.is_empty() {
|
while !suffix.is_empty() {
|
||||||
if !suffix.starts_with('[') {
|
if !suffix.starts_with('[') {
|
||||||
@@ -2210,15 +2233,21 @@ fn validate_temporal_annotation_suffix(mut suffix: &str) -> Result<(), String> {
|
|||||||
return Err("annotation keys must be lowercase".into());
|
return Err("annotation keys must be lowercase".into());
|
||||||
}
|
}
|
||||||
if key == "u-ca" {
|
if key == "u-ca" {
|
||||||
if calendar_seen || !value.eq_ignore_ascii_case("iso8601") {
|
if calendar_seen {
|
||||||
|
if critical || saw_critical_calendar {
|
||||||
|
return Err("invalid calendar annotation".into());
|
||||||
|
}
|
||||||
|
} else if !value.eq_ignore_ascii_case("iso8601") {
|
||||||
return Err("invalid calendar annotation".into());
|
return Err("invalid calendar annotation".into());
|
||||||
|
} else {
|
||||||
|
calendar_seen = true;
|
||||||
|
saw_critical_calendar = critical;
|
||||||
}
|
}
|
||||||
calendar_seen = true;
|
|
||||||
} else if critical {
|
} else if critical {
|
||||||
return Err("unknown critical annotation".into());
|
return Err("unknown critical annotation".into());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if critical || time_zone_seen {
|
if time_zone_seen {
|
||||||
return Err("invalid time-zone annotation".into());
|
return Err("invalid time-zone annotation".into());
|
||||||
}
|
}
|
||||||
time_zone_seen = true;
|
time_zone_seen = true;
|
||||||
@@ -2228,6 +2257,78 @@ fn validate_temporal_annotation_suffix(mut suffix: &str) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 剥离 ISO 偏移(±HH、±HHMM、±HH:MM、±HHMMSS、±HH:MM:SS,秒可带小数),返回剩余部分。
|
||||||
|
fn strip_iso_offset(input: &str) -> Result<&str, String> {
|
||||||
|
let bytes = input.as_bytes();
|
||||||
|
if bytes.len() < 3 || !matches!(bytes[0], b'+' | b'-') {
|
||||||
|
return Err("invalid ISO offset".into());
|
||||||
|
}
|
||||||
|
let colon = bytes.len() > 3 && bytes[3] == b':';
|
||||||
|
let mut i = 1usize;
|
||||||
|
let take_digits = |count: usize, cursor: &mut usize| -> Result<(), String> {
|
||||||
|
if *cursor + count > bytes.len() || !bytes[*cursor..*cursor + count].iter().all(u8::is_ascii_digit) {
|
||||||
|
return Err("invalid ISO offset".into());
|
||||||
|
}
|
||||||
|
*cursor += count;
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
take_digits(2, &mut i)?;
|
||||||
|
if i < bytes.len() {
|
||||||
|
if colon {
|
||||||
|
if bytes[i] != b':' {
|
||||||
|
return Err("invalid ISO offset".into());
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
if i + 2 <= bytes.len() && bytes[i..i + 2].iter().all(u8::is_ascii_digit) {
|
||||||
|
i += 2;
|
||||||
|
if i < bytes.len() {
|
||||||
|
if colon {
|
||||||
|
if bytes[i] != b':' {
|
||||||
|
return Err("invalid ISO offset".into());
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
if i + 2 <= bytes.len() && bytes[i..i + 2].iter().all(u8::is_ascii_digit) {
|
||||||
|
i += 2;
|
||||||
|
if i < bytes.len() && matches!(bytes[i], b'.' | b',') {
|
||||||
|
i += 1;
|
||||||
|
let start = i;
|
||||||
|
while i < bytes.len() && bytes[i].is_ascii_digit() {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
if i == start {
|
||||||
|
return Err("invalid ISO offset fraction".into());
|
||||||
|
}
|
||||||
|
if i - start > 9 {
|
||||||
|
return Err("invalid ISO offset fraction".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let digits: String = input[1..i].chars().filter(|ch| ch.is_ascii_digit()).take(6).collect();
|
||||||
|
if digits.len() < 2 {
|
||||||
|
return Err("invalid ISO offset".into());
|
||||||
|
}
|
||||||
|
let hour: u32 = digits[..2].parse().map_err(|_| "invalid ISO offset".to_string())?;
|
||||||
|
let minute: u32 = if digits.len() >= 4 {
|
||||||
|
digits[2..4].parse().map_err(|_| "invalid ISO offset".to_string())?
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let second: u32 = if digits.len() >= 6 {
|
||||||
|
digits[4..6].parse().map_err(|_| "invalid ISO offset".to_string())?
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
if hour > 23 || minute > 59 || second > 59 {
|
||||||
|
return Err("invalid ISO offset".into());
|
||||||
|
}
|
||||||
|
Ok(&input[i..])
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_plain_date_time_string(input: &str) -> Result<(i32, u32, u32, f64), String> {
|
fn parse_plain_date_time_string(input: &str) -> Result<(i32, u32, u32, f64), String> {
|
||||||
let trimmed = input.trim();
|
let trimmed = input.trim();
|
||||||
if trimmed.contains('\u{2212}') {
|
if trimmed.contains('\u{2212}') {
|
||||||
@@ -2240,13 +2341,14 @@ fn parse_plain_date_time_string(input: &str) -> Result<(i32, u32, u32, f64), Str
|
|||||||
if text.contains('Z') || text.contains('z') {
|
if text.contains('Z') || text.contains('z') {
|
||||||
return Err("UTC designator is not valid for PlainDateTime".into());
|
return Err("UTC designator is not valid for PlainDateTime".into());
|
||||||
}
|
}
|
||||||
let (year, month, day) = parse_iso_date(text)?;
|
|
||||||
let separator = text.find(['T', 't', ' ']);
|
let separator = text.find(['T', 't', ' ']);
|
||||||
|
let date_part = match separator {
|
||||||
|
Some(index) => &text[..index],
|
||||||
|
None => text,
|
||||||
|
};
|
||||||
|
let (year, month, day) = parse_iso_date(date_part)?;
|
||||||
let Some(separator) = separator else {
|
let Some(separator) = separator else {
|
||||||
if text.contains('+') {
|
if !valid_iso_date(year, month, day) || !valid_plain_date_time_range(year, month, day, 0.0) {
|
||||||
return Err("offset without time is not valid for PlainDateTime".into());
|
|
||||||
}
|
|
||||||
if !valid_iso_date(year, month, day) {
|
|
||||||
return Err("invalid ISO date".into());
|
return Err("invalid ISO date".into());
|
||||||
}
|
}
|
||||||
return Ok((year, month, day, 0.0));
|
return Ok((year, month, day, 0.0));
|
||||||
@@ -2255,30 +2357,10 @@ fn parse_plain_date_time_string(input: &str) -> Result<(i32, u32, u32, f64), Str
|
|||||||
let time_and_suffix = &text[separator + 1..];
|
let time_and_suffix = &text[separator + 1..];
|
||||||
let time_end = time_and_suffix
|
let time_end = time_and_suffix
|
||||||
.char_indices()
|
.char_indices()
|
||||||
.find_map(|(index, ch)| matches!(ch, '+' | '-' | '[').then_some(index))
|
.find_map(|(index, ch)| matches!(ch, '+' | '-').then_some(index))
|
||||||
.unwrap_or(time_and_suffix.len());
|
.unwrap_or(time_and_suffix.len());
|
||||||
if time_end < time_and_suffix.len() {
|
if time_end < time_and_suffix.len() && !strip_iso_offset(&time_and_suffix[time_end..])?.is_empty() {
|
||||||
let suffix = &time_and_suffix[time_end..];
|
return Err("invalid ISO offset".into());
|
||||||
let annotations = if suffix.starts_with('[') {
|
|
||||||
suffix
|
|
||||||
} else {
|
|
||||||
let offset_end = if suffix.len() >= 6 && suffix.as_bytes().get(3) == Some(&b':') {
|
|
||||||
6
|
|
||||||
} else if suffix.len() >= 5 {
|
|
||||||
5
|
|
||||||
} else if suffix.len() >= 3 {
|
|
||||||
3
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
if offset_end == 0 {
|
|
||||||
return Err("invalid ISO offset".into());
|
|
||||||
}
|
|
||||||
&suffix[offset_end..]
|
|
||||||
};
|
|
||||||
if annotations.starts_with(':') || validate_temporal_annotation_suffix(annotations).is_err() {
|
|
||||||
return Err("invalid ISO offset".into());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let time = &time_and_suffix[..time_end];
|
let time = &time_and_suffix[..time_end];
|
||||||
if time.is_empty() {
|
if time.is_empty() {
|
||||||
@@ -2334,13 +2416,13 @@ fn parse_plain_date_time_string(input: &str) -> Result<(i32, u32, u32, f64), Str
|
|||||||
if !valid_plain_time(hour, minute, second, millisecond, microsecond, nanosecond) {
|
if !valid_plain_time(hour, minute, second, millisecond, microsecond, nanosecond) {
|
||||||
return Err("invalid ISO time".into());
|
return Err("invalid ISO time".into());
|
||||||
}
|
}
|
||||||
if !valid_iso_date(year, month, day) {
|
|
||||||
return Err("invalid ISO date".into());
|
|
||||||
}
|
|
||||||
let total_ns = hour as f64 * 3_600_000_000_000.0
|
let total_ns = hour as f64 * 3_600_000_000_000.0
|
||||||
+ minute as f64 * 60_000_000_000.0
|
+ minute as f64 * 60_000_000_000.0
|
||||||
+ second as f64 * 1_000_000_000.0
|
+ second as f64 * 1_000_000_000.0
|
||||||
+ subsecond as f64;
|
+ subsecond as f64;
|
||||||
|
if !valid_iso_date(year, month, day) || !valid_plain_date_time_range(year, month, day, total_ns) {
|
||||||
|
return Err("invalid ISO date".into());
|
||||||
|
}
|
||||||
Ok((year, month, day, total_ns))
|
Ok((year, month, day, total_ns))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2352,59 +2434,106 @@ fn plain_date_time_object_parts<H: VmHost>(
|
|||||||
return Err(crate::error::create_range_error(vm, "unsupported calendar"));
|
return Err(crate::error::create_range_error(vm, "unsupported calendar"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let (day, hour, microsecond, millisecond, minute, month) = {
|
// 先按规范顺序读取全部原始字段,暂不转换类型。
|
||||||
let mut number_field = |name: &str| -> Result<Option<f64>, JsValue> {
|
let (
|
||||||
let raw = temporal_option_value(vm, obj, value, name)?;
|
day_raw,
|
||||||
if raw.is_undefined() {
|
hour_raw,
|
||||||
Ok(None)
|
microsecond_raw,
|
||||||
} else {
|
millisecond_raw,
|
||||||
temporal_option_number(vm, raw).map(|number| Some(number.trunc()))
|
minute_raw,
|
||||||
}
|
month_raw,
|
||||||
};
|
month_code_raw,
|
||||||
(
|
nanosecond_raw,
|
||||||
number_field("day")?,
|
second_raw,
|
||||||
number_field("hour")?.unwrap_or(0.0),
|
year_raw,
|
||||||
number_field("microsecond")?.unwrap_or(0.0),
|
) = (
|
||||||
number_field("millisecond")?.unwrap_or(0.0),
|
temporal_option_value(vm, obj, value, "day")?,
|
||||||
number_field("minute")?.unwrap_or(0.0),
|
temporal_option_value(vm, obj, value, "hour")?,
|
||||||
number_field("month")?,
|
temporal_option_value(vm, obj, value, "microsecond")?,
|
||||||
)
|
temporal_option_value(vm, obj, value, "millisecond")?,
|
||||||
};
|
temporal_option_value(vm, obj, value, "minute")?,
|
||||||
let month_code_raw = temporal_option_value(vm, obj, value, "monthCode")?;
|
temporal_option_value(vm, obj, value, "month")?,
|
||||||
|
temporal_option_value(vm, obj, value, "monthCode")?,
|
||||||
|
temporal_option_value(vm, obj, value, "nanosecond")?,
|
||||||
|
temporal_option_value(vm, obj, value, "second")?,
|
||||||
|
temporal_option_value(vm, obj, value, "year")?,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 缺失必填字段先抛 TypeError(先于任何 RangeError 值校验)。
|
||||||
|
if day_raw.is_undefined() {
|
||||||
|
return Err(crate::error::create_type_error(vm, "day is required"));
|
||||||
|
}
|
||||||
|
if year_raw.is_undefined() {
|
||||||
|
return Err(crate::error::create_type_error(vm, "year is required"));
|
||||||
|
}
|
||||||
|
if month_raw.is_undefined() && month_code_raw.is_undefined() {
|
||||||
|
return Err(crate::error::create_type_error(vm, "month is required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// monthCode 必须是 string 类型,否则 TypeError。
|
||||||
let month_code = if month_code_raw.is_undefined() {
|
let month_code = if month_code_raw.is_undefined() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let code = temporal_option_string(vm, month_code_raw)?;
|
if !month_code_raw.is_string() {
|
||||||
let parsed = code
|
return Err(crate::error::create_type_error(vm, "monthCode must be a string"));
|
||||||
.strip_prefix('M')
|
}
|
||||||
.filter(|digits| digits.len() == 2)
|
let code = to_string(month_code_raw);
|
||||||
.and_then(|digits| digits.parse::<u32>().ok())
|
// 语法(well-formed):M 后两位数字,可选 L 后缀。
|
||||||
.filter(|number| (1..=12).contains(number))
|
let digits_ok =
|
||||||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid monthCode"))?;
|
code.len() >= 3 && code.starts_with('M') && code.as_bytes()[1..3].iter().all(u8::is_ascii_digit);
|
||||||
Some(parsed as f64)
|
let well_formed = digits_ok && (code.len() == 3 || (code.len() == 4 && code.ends_with('L')));
|
||||||
|
if !well_formed {
|
||||||
|
return Err(crate::error::create_range_error(vm, "invalid monthCode"));
|
||||||
|
}
|
||||||
|
Some((code.clone(), code.ends_with('L')))
|
||||||
};
|
};
|
||||||
let mut number_field = |name: &str| -> Result<Option<f64>, JsValue> {
|
|
||||||
let raw = temporal_option_value(vm, obj, value, name)?;
|
// 数值字段类型转换:Symbol/BigInt 抛 TypeError。
|
||||||
|
let mut convert_number = |_name: &str, raw: JsValue| -> Result<Option<f64>, JsValue> {
|
||||||
if raw.is_undefined() {
|
if raw.is_undefined() {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
} else {
|
} else {
|
||||||
temporal_option_number(vm, raw).map(|number| Some(number.trunc()))
|
temporal_option_number(vm, raw).map(|number| Some(number.trunc()))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let nanosecond = number_field("nanosecond")?.unwrap_or(0.0);
|
let (day, hour, microsecond, millisecond, minute, month, nanosecond, second, year) = (
|
||||||
let second = number_field("second")?.unwrap_or(0.0);
|
convert_number("day", day_raw)?,
|
||||||
let year = number_field("year")?;
|
convert_number("hour", hour_raw)?.unwrap_or(0.0),
|
||||||
|
convert_number("microsecond", microsecond_raw)?.unwrap_or(0.0),
|
||||||
|
convert_number("millisecond", millisecond_raw)?.unwrap_or(0.0),
|
||||||
|
convert_number("minute", minute_raw)?.unwrap_or(0.0),
|
||||||
|
convert_number("month", month_raw)?,
|
||||||
|
convert_number("nanosecond", nanosecond_raw)?.unwrap_or(0.0),
|
||||||
|
convert_number("second", second_raw)?.unwrap_or(0.0),
|
||||||
|
convert_number("year", year_raw)?,
|
||||||
|
);
|
||||||
|
|
||||||
let day = day.ok_or_else(|| crate::error::create_type_error(vm, "day is required"))?;
|
// monthCode 适配 ISO 日历:仅 M01-M12,L 后缀不支持;无效抛 RangeError。
|
||||||
let year = year.ok_or_else(|| crate::error::create_type_error(vm, "year is required"))?;
|
let month_code = match month_code {
|
||||||
|
Some((code, leap)) => {
|
||||||
|
if leap {
|
||||||
|
return Err(crate::error::create_range_error(vm, "monthCode is not valid for ISO calendar"));
|
||||||
|
}
|
||||||
|
let number = code[1..3]
|
||||||
|
.parse::<f64>()
|
||||||
|
.map_err(|_| crate::error::create_range_error(vm, "invalid monthCode"))?;
|
||||||
|
if !(1.0..=12.0).contains(&number) {
|
||||||
|
return Err(crate::error::create_range_error(vm, "monthCode is not valid for ISO calendar"));
|
||||||
|
}
|
||||||
|
Some(number)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
let month = match (month, month_code) {
|
let month = match (month, month_code) {
|
||||||
(Some(month), Some(code)) if month != code => {
|
(Some(month), Some(code)) if month != code => {
|
||||||
return Err(crate::error::create_range_error(vm, "month and monthCode disagree"));
|
return Err(crate::error::create_range_error(vm, "month and monthCode disagree"));
|
||||||
}
|
}
|
||||||
(Some(month), _) => month,
|
(Some(month), _) => month,
|
||||||
(None, Some(code)) => code,
|
(None, Some(code)) => code,
|
||||||
(None, None) => return Err(crate::error::create_type_error(vm, "month is required")),
|
(None, None) => unreachable!(),
|
||||||
};
|
};
|
||||||
|
let day = day.ok_or_else(|| crate::error::create_type_error(vm, "day is required"))?;
|
||||||
|
let year = year.ok_or_else(|| crate::error::create_type_error(vm, "year is required"))?;
|
||||||
let mut values = [year, month, day, hour, minute, second, millisecond, microsecond, nanosecond];
|
let mut values = [year, month, day, hour, minute, second, millisecond, microsecond, nanosecond];
|
||||||
if values.iter().any(|number| !number.is_finite()) || values[1..].iter().any(|number| *number < 0.0) {
|
if values.iter().any(|number| !number.is_finite()) || values[1..].iter().any(|number| *number < 0.0) {
|
||||||
return Err(crate::error::create_range_error(vm, "invalid date-time component"));
|
return Err(crate::error::create_range_error(vm, "invalid date-time component"));
|
||||||
@@ -2434,9 +2563,32 @@ fn plain_date_time_object_parts<H: VmHost>(
|
|||||||
+ millisecond as f64 * 1_000_000.0
|
+ millisecond as f64 * 1_000_000.0
|
||||||
+ microsecond as f64 * 1_000.0
|
+ microsecond as f64 * 1_000.0
|
||||||
+ nanosecond as f64;
|
+ nanosecond as f64;
|
||||||
|
if !valid_plain_date_time_range(year, month, day, total_ns) {
|
||||||
|
return Err(crate::error::create_range_error(vm, "invalid date-time component"));
|
||||||
|
}
|
||||||
Ok((year, month, day, total_ns))
|
Ok((year, month, day, total_ns))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 将 ZonedDateTime 按时区偏移转换为本地 PlainDateTime 分量。
|
||||||
|
fn zoned_date_time_plain_parts<H: VmHost>(vm: &mut H, obj: &JsObject) -> Result<(i32, u32, u32, f64), JsValue> {
|
||||||
|
let Some(epoch_ns) = get_instant_epoch_ns(obj) else {
|
||||||
|
return Err(crate::error::create_range_error(vm, "invalid ZonedDateTime"));
|
||||||
|
};
|
||||||
|
let time_zone_id = to_string(obj.get_prop_at(1));
|
||||||
|
let Some(offset_minutes) = instant_time_zone_offset(&time_zone_id) else {
|
||||||
|
return Err(crate::error::create_range_error(vm, "invalid time zone"));
|
||||||
|
};
|
||||||
|
const DAY_NS: i128 = 86_400_000_000_000;
|
||||||
|
let local_ns = epoch_ns + i128::from(offset_minutes) * 60_000_000_000;
|
||||||
|
let days = local_ns.div_euclid(DAY_NS);
|
||||||
|
let time_ns = local_ns.rem_euclid(DAY_NS) as f64;
|
||||||
|
let (year, month, day) = civil_from_days(days);
|
||||||
|
if !valid_plain_date_time_range(year as i32, month as u32, day as u32, time_ns) {
|
||||||
|
return Err(crate::error::create_range_error(vm, "invalid date-time"));
|
||||||
|
}
|
||||||
|
Ok((year as i32, month as u32, day as u32, time_ns))
|
||||||
|
}
|
||||||
|
|
||||||
fn plain_date_time_like_parts<H: VmHost>(
|
fn plain_date_time_like_parts<H: VmHost>(
|
||||||
vm: &mut H, value: JsValue, constrain: bool,
|
vm: &mut H, value: JsValue, constrain: bool,
|
||||||
) -> Result<(i32, u32, u32, f64), JsValue> {
|
) -> Result<(i32, u32, u32, f64), JsValue> {
|
||||||
@@ -2468,6 +2620,9 @@ fn plain_date_time_like_parts<H: VmHost>(
|
|||||||
0.0,
|
0.0,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if obj.is_zoned_date_time_obj() {
|
||||||
|
return zoned_date_time_plain_parts(vm, obj);
|
||||||
|
}
|
||||||
plain_date_time_object_parts(vm, value, obj, constrain)
|
plain_date_time_object_parts(vm, value, obj, constrain)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2680,28 +2835,131 @@ pub fn plain_date_time_value_of<H: VmHost>(vm: &mut H, _args: &[u8]) -> NativeRe
|
|||||||
NativeResult::Err(crate::error::create_type_error(vm, "Temporal.PlainDateTime has no valueOf"))
|
NativeResult::Err(crate::error::create_type_error(vm, "Temporal.PlainDateTime has no valueOf"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 格式化 PlainDateTime 本地 ISO 日期时间(无偏移),可裁秒与小数位并追加日历注解。
|
||||||
|
fn format_plain_date_time_iso(
|
||||||
|
year: i32, month: u32, day: u32, time_ns: i128, include_seconds: bool, fractional_digits: Option<usize>,
|
||||||
|
calendar_name: &str,
|
||||||
|
) -> 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!("{}-{month:02}-{day:02}T{hour:02}:{minute:02}", format_iso_year(i128::from(year)));
|
||||||
|
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 => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match calendar_name {
|
||||||
|
"always" => output.push_str("[u-ca=iso8601]"),
|
||||||
|
"critical" => output.push_str("[!u-ca=iso8601]"),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
fn plain_date_time_iso_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
fn plain_date_time_iso_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||||
let (year, month, day, total_ns) = native_try!(plain_date_time_parts(vm, args));
|
let (year, month, day, total_ns) = native_try!(plain_date_time_parts(vm, args));
|
||||||
let (hour, minute, second, millisecond, microsecond, nanosecond) = plain_time_components(total_ns);
|
let output = format_plain_date_time_iso(year, month, day, total_ns as i128, true, None, "never");
|
||||||
let fraction = millisecond as u64 * 1_000_000 + microsecond as u64 * 1_000 + nanosecond as u64;
|
|
||||||
let mut output = format!("{}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}", format_iso_year(year as i128));
|
|
||||||
if fraction != 0 {
|
|
||||||
let mut digits = format!("{fraction:09}");
|
|
||||||
while digits.ends_with('0') {
|
|
||||||
digits.pop();
|
|
||||||
}
|
|
||||||
output.push('.');
|
|
||||||
output.push_str(&digits);
|
|
||||||
}
|
|
||||||
NativeResult::Ok(vm.new_string(&output))
|
NativeResult::Ok(vm.new_string(&output))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `Temporal.PlainDateTime.prototype.toString()`:输出本地 ISO 日期时间。
|
/// `Temporal.PlainDateTime.prototype.toString(options)`:按精度、舍入模式与日历显示输出 ISO 8601。
|
||||||
pub fn plain_date_time_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
pub fn plain_date_time_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||||
if args.len() > 1 && !vm.reg(args[1]).is_undefined() && !vm.reg(args[1]).is_object() {
|
let (year, month, day, total_ns) = native_try!(plain_date_time_parts(vm, args));
|
||||||
return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object"));
|
let options_value = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) };
|
||||||
|
let (calendar_name, fractional_input, mode_value, smallest_value) = if options_value.is_undefined() {
|
||||||
|
("auto".to_string(), 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 calendar_raw = native_try!(temporal_option_value(vm, options, options_value, "calendarName"));
|
||||||
|
let calendar_name = if calendar_raw.is_undefined() {
|
||||||
|
"auto".to_string()
|
||||||
|
} else {
|
||||||
|
native_try!(temporal_option_string(vm, calendar_raw))
|
||||||
|
};
|
||||||
|
let fractional_raw = native_try!(temporal_option_value(vm, options, options_value, "fractionalSecondDigits"));
|
||||||
|
let fractional_input = 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_value = 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_value = if smallest_raw.is_undefined() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(native_try!(temporal_option_string(vm, smallest_raw)))
|
||||||
|
};
|
||||||
|
(calendar_name, fractional_input, mode_value, smallest_value)
|
||||||
|
};
|
||||||
|
|
||||||
|
match calendar_name.as_str() {
|
||||||
|
"auto" | "always" | "never" | "critical" => {}
|
||||||
|
_ => return NativeResult::Err(crate::error::create_range_error(vm, "invalid calendarName")),
|
||||||
}
|
}
|
||||||
plain_date_time_iso_string(vm, args)
|
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),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const DAY_NS: i128 = 86_400_000_000_000;
|
||||||
|
let Some(rounded_ns) = round_instant_ns(total_ns as i128, quantum_ns, mode) else {
|
||||||
|
return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time"));
|
||||||
|
};
|
||||||
|
let extra_days = rounded_ns / DAY_NS;
|
||||||
|
let time_ns = rounded_ns % DAY_NS;
|
||||||
|
let total_days = days_from_civil(i128::from(year), i128::from(month), i128::from(day)) + extra_days;
|
||||||
|
let (year, month, day) = civil_from_days(total_days);
|
||||||
|
if !valid_plain_date_time_range(year as i32, month as u32, day as u32, time_ns as f64) {
|
||||||
|
return NativeResult::Err(crate::error::create_range_error(vm, "invalid date-time"));
|
||||||
|
}
|
||||||
|
let output = format_plain_date_time_iso(
|
||||||
|
year as i32,
|
||||||
|
month as u32,
|
||||||
|
day as u32,
|
||||||
|
time_ns,
|
||||||
|
include_seconds,
|
||||||
|
output_digits,
|
||||||
|
&calendar_name,
|
||||||
|
);
|
||||||
|
NativeResult::Ok(vm.new_string(&output))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `Temporal.PlainDateTime.prototype.toJSON()`:输出默认 ISO 日期时间。
|
/// `Temporal.PlainDateTime.prototype.toJSON()`:输出默认 ISO 日期时间。
|
||||||
|
|||||||
@@ -229,8 +229,11 @@ impl Emitter {
|
|||||||
if is_derived {
|
if is_derived {
|
||||||
module.insts.clear();
|
module.insts.clear();
|
||||||
module.constants.clear();
|
module.constants.clear();
|
||||||
module.n_registers = 1;
|
module.n_registers = 2;
|
||||||
module.insts.push(Inst::super_call(Operand::None, Operand::None, 0));
|
// 隐式派生构造器等价于 `constructor(...args) { super(...args); }`:
|
||||||
|
// 收集全部实参为数组,再展开传给 super。
|
||||||
|
module.insts.push(Inst::create_rest_array(Operand::Reg(1), 0));
|
||||||
|
module.insts.push(Inst::super_call_spread(Operand::Reg(0), &[0x8000_0000 | 1]));
|
||||||
// 字段初始化直接重发:构造器 upvalue/内置槽引用须与首轮编译产物对齐。
|
// 字段初始化直接重发:构造器 upvalue/内置槽引用须与首轮编译产物对齐。
|
||||||
let mut field_ctx = CompileCtx::new();
|
let mut field_ctx = CompileCtx::new();
|
||||||
field_ctx.scopes.private_name_map =
|
field_ctx.scopes.private_name_map =
|
||||||
|
|||||||
@@ -719,6 +719,81 @@ fn plain_date_time_compare_uses_internal_slots() {
|
|||||||
assert_eq!(str_val(&vm, r), "-1,1,0");
|
assert_eq!(str_val(&vm, r), "-1,1,0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plain_date_time_from_accepts_variants_and_full_range() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let r = eval(
|
||||||
|
&mut vm,
|
||||||
|
"const lower = Temporal.PlainDateTime.from('1976-11-18t15:23').toString();
|
||||||
|
const space = Temporal.PlainDateTime.from('1976-11-18 15:23').toString();
|
||||||
|
const offset = Temporal.PlainDateTime.from('1976-11-18T15:23+00:00:00,0[UTC]').toString();
|
||||||
|
const criticalZone = Temporal.PlainDateTime.from('1976-11-18T15:23[!Europe/Vienna]').toString();
|
||||||
|
const secondCalendar = Temporal.PlainDateTime.from('1976-11-18T15:23[u-ca=iso8601][u-ca=discord]').toString();
|
||||||
|
const minOk = Temporal.PlainDateTime.from('-271821-04-19T00:00:00.000000001').toString();
|
||||||
|
const maxOk = Temporal.PlainDateTime.from('+275760-09-13').toString();
|
||||||
|
const minBad = (() => { try { Temporal.PlainDateTime.from('-271821-04-19T00:00'); return false }
|
||||||
|
catch (e) { return e instanceof RangeError } })();
|
||||||
|
const maxBad = (() => { try { Temporal.PlainDateTime.from('+275760-09-14'); return false }
|
||||||
|
catch (e) { return e instanceof RangeError } })();
|
||||||
|
const dateOffsetBad = (() => { try { Temporal.PlainDateTime.from('2022-09-15+00:00'); return false }
|
||||||
|
catch (e) { return e instanceof RangeError } })();
|
||||||
|
const badCalendar = (() => { try { Temporal.PlainDateTime.from('1997-12-04[u-ca=notacal]'); return false }
|
||||||
|
catch (e) { return e instanceof RangeError } })();
|
||||||
|
const duplicateCritical = (() => { try { Temporal.PlainDateTime.from('1970-01-01[u-ca=iso8601][!u-ca=iso8601]'); return false }
|
||||||
|
catch (e) { return e instanceof RangeError } })();
|
||||||
|
[lower, space, offset, criticalZone, secondCalendar, minOk, maxOk,
|
||||||
|
minBad, maxBad, dateOffsetBad, badCalendar, duplicateCritical].join('|')",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
str_val(&vm, r),
|
||||||
|
concat!(
|
||||||
|
"1976-11-18T15:23:00|1976-11-18T15:23:00|1976-11-18T15:23:00|",
|
||||||
|
"1976-11-18T15:23:00|1976-11-18T15:23:00|-271821-04-19T00:00:00.000000001|",
|
||||||
|
"+275760-09-13T00:00:00|true|true|true|true|true"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn plain_date_time_to_string_honors_options() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let r = eval(
|
||||||
|
&mut vm,
|
||||||
|
"(() => {
|
||||||
|
const dt = new Temporal.PlainDateTime(2000, 5, 2, 12, 34, 56, 987, 650, 0);
|
||||||
|
const minute = dt.toString({ smallestUnit: 'minute' });
|
||||||
|
const seconds = dt.toString({ smallestUnit: 'second' });
|
||||||
|
const micros = dt.toString({ smallestUnit: 'microsecond' });
|
||||||
|
const two = dt.toString({ fractionalSecondDigits: 2.5 });
|
||||||
|
const nine = dt.toString({ fractionalSecondDigits: 9.7 });
|
||||||
|
const midnight = new Temporal.PlainDateTime(1999, 12, 31, 23, 59, 59, 999, 999, 999)
|
||||||
|
.toString({ fractionalSecondDigits: 8, roundingMode: 'ceil' });
|
||||||
|
const always = dt.toString({ calendarName: 'always' });
|
||||||
|
const critical = dt.toString({ calendarName: 'critical' });
|
||||||
|
const never = dt.toString({ calendarName: 'never' });
|
||||||
|
const badCalendar = (() => { try { dt.toString({ calendarName: 'ALWAYS' }); return false }
|
||||||
|
catch (e) { return e instanceof RangeError } })();
|
||||||
|
const badUnit = (() => { try { dt.toString({ smallestUnit: 'hour' }); return false }
|
||||||
|
catch (e) { return e instanceof RangeError } })();
|
||||||
|
const badDigits = (() => { try { dt.toString({ fractionalSecondDigits: -0.6 }); return false }
|
||||||
|
catch (e) { return e instanceof RangeError } })();
|
||||||
|
const badOptions = (() => { try { dt.toString(null); return false }
|
||||||
|
catch (e) { return e instanceof TypeError } })();
|
||||||
|
return [minute, seconds, micros, two, nine, midnight, always, critical, never,
|
||||||
|
badCalendar, badUnit, badDigits, badOptions].join('|');
|
||||||
|
})()",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
str_val(&vm, r),
|
||||||
|
concat!(
|
||||||
|
"2000-05-02T12:34|2000-05-02T12:34:56|2000-05-02T12:34:56.987650|",
|
||||||
|
"2000-05-02T12:34:56.98|2000-05-02T12:34:56.987650000|2000-01-01T00:00:00.00000000|",
|
||||||
|
"2000-05-02T12:34:56.98765[u-ca=iso8601]|2000-05-02T12:34:56.98765[!u-ca=iso8601]|",
|
||||||
|
"2000-05-02T12:34:56.98765|true|true|true|true"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn plain_date_time_serializes_extended_years() {
|
fn plain_date_time_serializes_extended_years() {
|
||||||
let mut vm = Vm::new();
|
let mut vm = Vm::new();
|
||||||
@@ -730,3 +805,19 @@ fn plain_date_time_serializes_extended_years() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(str_val(&vm, r), "-000001-08-07T06:54:32.1,+010000-06-07T08:09:10.987");
|
assert_eq!(str_val(&vm, r), "-000001-08-07T06:54:32.1,+010000-06-07T08:09:10.987");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zdt_subclass_compare_debug() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let r = eval(
|
||||||
|
&mut vm,
|
||||||
|
"class AvoidGettersDateTime extends Temporal.PlainDateTime {
|
||||||
|
get year() { throw new Error('getter'); }
|
||||||
|
}
|
||||||
|
const one = new AvoidGettersDateTime(2000, 5, 2, 12, 34, 56, 987, 654, 321);
|
||||||
|
const two = new AvoidGettersDateTime(2006, 3, 25, 6, 54, 32, 123, 456, 789);
|
||||||
|
typeof Temporal.PlainDateTime.compare(one, two) + ':' + Temporal.PlainDateTime.compare(one, two)",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(str_val(&vm, r), "number:-1");
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user