mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-24 18:51:13 +08:00
refactor: temporal 字符串解析与算历层并入 common
This commit is contained in:
@@ -16,8 +16,8 @@ use oxide_types::object::JsObject;
|
|||||||
use oxide_types::value::JsValue;
|
use oxide_types::value::JsValue;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
days_in_month, parse_temporal_calendar_string, temporal_calendar_id, valid_iso_date, valid_plain_date_time_range,
|
instant_time_zone_offset, parse_iso_date, parse_plain_date_time_string, parse_plain_time_string, valid_iso_date,
|
||||||
valid_plain_time,
|
valid_plain_date_time_range, valid_plain_time,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) const MAX_INSTANT_NS: i128 = 8_640_000_000_000_000_000_000;
|
pub(crate) const MAX_INSTANT_NS: i128 = 8_640_000_000_000_000_000_000;
|
||||||
@@ -549,3 +549,610 @@ pub(crate) fn month_day_bag_calendar_id<H: VmHost>(vm: &mut H, value: JsValue) -
|
|||||||
}
|
}
|
||||||
Err(crate::error::create_type_error(vm, "invalid calendar"))
|
Err(crate::error::create_type_error(vm, "invalid calendar"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_digits(bytes: &[u8], cursor: &mut usize, count: usize) -> Option<i128> {
|
||||||
|
let end = cursor.checked_add(count)?;
|
||||||
|
let digits = bytes.get(*cursor..end)?;
|
||||||
|
if !digits.iter().all(u8::is_ascii_digit) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
*cursor = end;
|
||||||
|
digits
|
||||||
|
.iter()
|
||||||
|
.try_fold(0_i128, |value, digit| value.checked_mul(10)?.checked_add((digit - b'0') as i128))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_leap_year(year: i128) -> bool {
|
||||||
|
year.rem_euclid(4) == 0 && (year.rem_euclid(100) != 0 || year.rem_euclid(400) == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn days_in_month(year: i128, month: i128) -> Option<i128> {
|
||||||
|
match month {
|
||||||
|
1 | 3 | 5 | 7 | 8 | 10 | 12 => Some(31),
|
||||||
|
4 | 6 | 9 | 11 => Some(30),
|
||||||
|
2 => Some(if is_leap_year(year) { 29 } else { 28 }),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn days_from_civil(mut year: i128, month: i128, day: i128) -> i128 {
|
||||||
|
year -= i128::from(month <= 2);
|
||||||
|
let era = year.div_euclid(400);
|
||||||
|
let year_of_era = year - era * 400;
|
||||||
|
let shifted_month = month + if month > 2 { -3 } else { 9 };
|
||||||
|
let day_of_year = (153 * shifted_month + 2) / 5 + day - 1;
|
||||||
|
let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
|
||||||
|
era * 146_097 + day_of_era - 719_468
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) enum FractionalSecondDigitsInput {
|
||||||
|
Auto,
|
||||||
|
Number(f64),
|
||||||
|
String(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_fractional_second_digits<H: VmHost>(
|
||||||
|
vm: &mut H, input: FractionalSecondDigitsInput,
|
||||||
|
) -> Result<Option<usize>, JsValue> {
|
||||||
|
match input {
|
||||||
|
FractionalSecondDigitsInput::Auto => Ok(None),
|
||||||
|
FractionalSecondDigitsInput::String(value) if value == "auto" => Ok(None),
|
||||||
|
FractionalSecondDigitsInput::String(_) => {
|
||||||
|
Err(crate::error::create_range_error(vm, "invalid fractionalSecondDigits"))
|
||||||
|
}
|
||||||
|
FractionalSecondDigitsInput::Number(value) => {
|
||||||
|
let digits = value.floor();
|
||||||
|
if !digits.is_finite() || !(0.0..=9.0).contains(&digits) {
|
||||||
|
return Err(crate::error::create_range_error(vm, "invalid fractionalSecondDigits"));
|
||||||
|
}
|
||||||
|
Ok(Some(digits as usize))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_offset_minutes(value: &str) -> Option<i32> {
|
||||||
|
let bytes = value.as_bytes();
|
||||||
|
if bytes.len() != 6 || !matches!(bytes[0], b'+' | b'-') || bytes[3] != b':' {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !bytes[1..3].iter().all(u8::is_ascii_digit) || !bytes[4..6].iter().all(u8::is_ascii_digit) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let hours = i32::from(bytes[1] - b'0') * 10 + i32::from(bytes[2] - b'0');
|
||||||
|
let minutes = i32::from(bytes[4] - b'0') * 10 + i32::from(bytes[5] - b'0');
|
||||||
|
if hours > 23 || minutes > 59 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let magnitude = hours * 60 + minutes;
|
||||||
|
Some(if bytes[0] == b'-' { -magnitude } else { magnitude })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn canonical_time_zone(value: &str) -> Option<(String, i32)> {
|
||||||
|
let input = value.trim();
|
||||||
|
if input.eq_ignore_ascii_case("UTC") || input.eq_ignore_ascii_case("Z") {
|
||||||
|
return Some(("UTC".to_string(), 0));
|
||||||
|
}
|
||||||
|
if let Some(offset) = parse_offset_minutes(input) {
|
||||||
|
return Some((input.to_string(), offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ±HH 基本偏移("+01" / "-05"):3 字符,hour ≤ 23,分钟恒 0。仅放宽构造器时区接受面。
|
||||||
|
if input.len() == 3
|
||||||
|
&& matches!(input.as_bytes()[0], b'+' | b'-')
|
||||||
|
&& input.as_bytes()[1..3].iter().all(u8::is_ascii_digit)
|
||||||
|
{
|
||||||
|
let hour = (input.as_bytes()[1] - b'0') as i32 * 10 + (input.as_bytes()[2] - b'0') as i32;
|
||||||
|
if hour <= 23 {
|
||||||
|
let sign = if input.as_bytes()[0] == b'-' { -1 } else { 1 };
|
||||||
|
return Some((input.to_string(), sign * hour * 60));
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ±HHMM 基本偏移("+0000" / "-0530"):5 字符,无冒号,分钟位可选非零。4 位无冒号归一形式。
|
||||||
|
if input.len() == 5
|
||||||
|
&& matches!(input.as_bytes()[0], b'+' | b'-')
|
||||||
|
&& input.as_bytes()[1..5].iter().all(u8::is_ascii_digit)
|
||||||
|
{
|
||||||
|
let hour = (input.as_bytes()[1] - b'0') as i32 * 10 + (input.as_bytes()[2] - b'0') as i32;
|
||||||
|
let minute = (input.as_bytes()[3] - b'0') as i32 * 10 + (input.as_bytes()[4] - b'0') as i32;
|
||||||
|
if hour <= 23 && minute <= 59 {
|
||||||
|
let sign = if input.as_bytes()[0] == b'-' { -1 } else { 1 };
|
||||||
|
return Some((input.to_string(), sign * (hour * 60 + minute)));
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if input.starts_with("-000000") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 带 annotation 的日期时间以最后一个方括号内标识符为准。
|
||||||
|
if let Some(open) = input.rfind('[') {
|
||||||
|
if !input.ends_with(']') || open + 2 > input.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let annotation = &input[open + 1..input.len() - 1];
|
||||||
|
if annotation.eq_ignore_ascii_case("UTC") {
|
||||||
|
return Some(("UTC".to_string(), 0));
|
||||||
|
}
|
||||||
|
return parse_offset_minutes(annotation).map(|offset| (annotation.to_string(), offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
let time_start = input.find(['T', 't', ' '])?;
|
||||||
|
let time = &input[time_start + 1..];
|
||||||
|
if time.ends_with(['Z', 'z']) {
|
||||||
|
return Some(("UTC".to_string(), 0));
|
||||||
|
}
|
||||||
|
let offset_start = time
|
||||||
|
.char_indices()
|
||||||
|
.rev()
|
||||||
|
.find_map(|(index, ch)| matches!(ch, '+' | '-').then_some(index))?;
|
||||||
|
let offset_id = &time[offset_start..];
|
||||||
|
parse_offset_minutes(offset_id).map(|offset| (offset_id.to_string(), offset))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn civil_from_days(days: i128) -> (i128, i128, i128) {
|
||||||
|
let shifted = days + 719_468;
|
||||||
|
let era = shifted.div_euclid(146_097);
|
||||||
|
let day_of_era = shifted - era * 146_097;
|
||||||
|
let year_of_era = (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
|
||||||
|
let mut year = year_of_era + era * 400;
|
||||||
|
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
|
||||||
|
let month_prime = (5 * day_of_year + 2) / 153;
|
||||||
|
let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
|
||||||
|
let month = month_prime + if month_prime < 10 { 3 } else { -9 };
|
||||||
|
year += i128::from(month <= 2);
|
||||||
|
(year, month, day)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn format_iso_year(year: i128) -> String {
|
||||||
|
if (0..=9_999).contains(&year) {
|
||||||
|
format!("{year:04}")
|
||||||
|
} else if year >= 0 {
|
||||||
|
format!("+{year:06}")
|
||||||
|
} else {
|
||||||
|
format!("-{:06}", -year)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn validate_temporal_annotation_suffix(mut suffix: &str) -> Result<(), String> {
|
||||||
|
let mut calendar_seen = false;
|
||||||
|
let mut saw_critical_calendar = false;
|
||||||
|
let mut time_zone_seen = false;
|
||||||
|
while !suffix.is_empty() {
|
||||||
|
if !suffix.starts_with('[') {
|
||||||
|
return Err("invalid annotation suffix".into());
|
||||||
|
}
|
||||||
|
let Some(end) = suffix.find(']') else {
|
||||||
|
return Err("unterminated annotation".into());
|
||||||
|
};
|
||||||
|
if end == 1 {
|
||||||
|
return Err("empty annotation".into());
|
||||||
|
}
|
||||||
|
let annotation = &suffix[1..end];
|
||||||
|
let (critical, body) = annotation.strip_prefix('!').map_or((false, annotation), |body| (true, body));
|
||||||
|
if let Some((key, value)) = body.split_once('=') {
|
||||||
|
if key.bytes().any(|byte| byte.is_ascii_uppercase()) {
|
||||||
|
return Err("annotation keys must be lowercase".into());
|
||||||
|
}
|
||||||
|
if key == "u-ca" {
|
||||||
|
if calendar_seen {
|
||||||
|
if critical || saw_critical_calendar {
|
||||||
|
return Err("invalid calendar annotation".into());
|
||||||
|
}
|
||||||
|
} else if is_builtin_calendar_id(value).is_none() {
|
||||||
|
return Err("invalid calendar annotation".into());
|
||||||
|
} else {
|
||||||
|
calendar_seen = true;
|
||||||
|
saw_critical_calendar = critical;
|
||||||
|
}
|
||||||
|
} else if critical {
|
||||||
|
return Err("unknown critical annotation".into());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if time_zone_seen {
|
||||||
|
return Err("invalid time-zone annotation".into());
|
||||||
|
}
|
||||||
|
time_zone_seen = true;
|
||||||
|
}
|
||||||
|
suffix = &suffix[end + 1..];
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 剥离 ISO 偏移(±HH、±HHMM、±HH:MM、±HHMMSS、±HH:MM:SS,秒可带小数),返回剩余部分。
|
||||||
|
pub(crate) 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..])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ParseTemporalDateString:PlainDate 字符串(时间部分可选且被忽略,仅按日期做范围校验)。
|
||||||
|
pub(crate) fn parse_plain_date_string(input: &str) -> Result<(i32, u32, u32), String> {
|
||||||
|
parse_temporal_string_impl(input, false).map(|(year, month, day, _)| (year, month, day))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_temporal_string_impl(
|
||||||
|
input: &str, enforce_date_time_range: bool,
|
||||||
|
) -> Result<(i32, u32, u32, f64), String> {
|
||||||
|
let trimmed = input.trim();
|
||||||
|
if trimmed.contains('\u{2212}') {
|
||||||
|
return Err("variant minus sign is not valid for PlainDateTime".into());
|
||||||
|
}
|
||||||
|
let text = trimmed.to_owned();
|
||||||
|
let annotation_start = text.find('[').unwrap_or(text.len());
|
||||||
|
validate_temporal_annotation_suffix(&text[annotation_start..])?;
|
||||||
|
let text = &text[..annotation_start];
|
||||||
|
if text.contains('Z') || text.contains('z') {
|
||||||
|
return Err("UTC designator is not valid for PlainDateTime".into());
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
if !valid_iso_date(year, month, day)
|
||||||
|
|| (enforce_date_time_range && !valid_plain_date_time_range(year, month, day, 0.0))
|
||||||
|
{
|
||||||
|
return Err("invalid ISO date".into());
|
||||||
|
}
|
||||||
|
return Ok((year, month, day, 0.0));
|
||||||
|
};
|
||||||
|
|
||||||
|
let time_and_suffix = &text[separator + 1..];
|
||||||
|
let time_end = time_and_suffix
|
||||||
|
.char_indices()
|
||||||
|
.find_map(|(index, ch)| matches!(ch, '+' | '-').then_some(index))
|
||||||
|
.unwrap_or(time_and_suffix.len());
|
||||||
|
if time_end < time_and_suffix.len() && !strip_iso_offset(&time_and_suffix[time_end..])?.is_empty() {
|
||||||
|
return Err("invalid ISO offset".into());
|
||||||
|
}
|
||||||
|
let time = &time_and_suffix[..time_end];
|
||||||
|
if time.is_empty() {
|
||||||
|
return Err("missing ISO time".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (clock, fraction) = match time.find(['.', ',']) {
|
||||||
|
Some(index) => (&time[..index], Some(&time[index + 1..])),
|
||||||
|
None => (time, None),
|
||||||
|
};
|
||||||
|
let fields = if clock.contains(':') {
|
||||||
|
clock.split(':').collect::<Vec<_>>()
|
||||||
|
} else {
|
||||||
|
if clock.len() % 2 != 0 || clock.len() > 6 {
|
||||||
|
return Err("invalid ISO time".into());
|
||||||
|
}
|
||||||
|
clock
|
||||||
|
.as_bytes()
|
||||||
|
.chunks(2)
|
||||||
|
.map(std::str::from_utf8)
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|_| "invalid ISO time".to_string())?
|
||||||
|
};
|
||||||
|
if fields.is_empty() || fields.len() > 3 || fields.iter().any(|field| field.len() != 2) {
|
||||||
|
return Err("invalid ISO time".into());
|
||||||
|
}
|
||||||
|
if fraction.is_some() && fields.len() < 3 {
|
||||||
|
return Err("fractional hours and minutes are not valid".into());
|
||||||
|
}
|
||||||
|
let parse_field = |index: usize| -> Result<u32, String> {
|
||||||
|
fields
|
||||||
|
.get(index)
|
||||||
|
.map_or(Ok(0), |field| field.parse().map_err(|_| "invalid ISO time".to_string()))
|
||||||
|
};
|
||||||
|
let hour = parse_field(0)?;
|
||||||
|
let minute = parse_field(1)?;
|
||||||
|
let mut second = parse_field(2)?;
|
||||||
|
if second == 60 {
|
||||||
|
second = 59;
|
||||||
|
}
|
||||||
|
let subsecond = match fraction {
|
||||||
|
Some(digits) if !digits.is_empty() && digits.len() <= 9 && digits.bytes().all(|byte| byte.is_ascii_digit()) => {
|
||||||
|
let mut padded = digits.to_owned();
|
||||||
|
padded.extend(std::iter::repeat('0').take(9 - digits.len()));
|
||||||
|
padded.parse::<u32>().map_err(|_| "invalid ISO fraction".to_string())?
|
||||||
|
}
|
||||||
|
Some(_) => return Err("invalid ISO fraction".into()),
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
let millisecond = subsecond / 1_000_000;
|
||||||
|
let microsecond = subsecond / 1_000 % 1_000;
|
||||||
|
let nanosecond = subsecond % 1_000;
|
||||||
|
if !valid_plain_time(hour, minute, second, millisecond, microsecond, nanosecond) {
|
||||||
|
return Err("invalid ISO time".into());
|
||||||
|
}
|
||||||
|
let total_ns = 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;
|
||||||
|
if !valid_iso_date(year, month, day)
|
||||||
|
|| (enforce_date_time_range && !valid_plain_date_time_range(year, month, day, total_ns))
|
||||||
|
{
|
||||||
|
return Err("invalid ISO date".into());
|
||||||
|
}
|
||||||
|
Ok((year, month, day, total_ns))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 规范 BuiltinCalendarID 全集(18 项)。匹配用 eq_ignore_ascii_case(ASCII 折叠,
|
||||||
|
/// 禁 to_lowercase:U+0130 点 I 会被 Unicode 折叠成 i+组合符,误判为 iso8601)。
|
||||||
|
pub(crate) const CALENDAR_ID_WHITELIST: [&str; 18] = [
|
||||||
|
"buddhist",
|
||||||
|
"chinese",
|
||||||
|
"coptic",
|
||||||
|
"dangi",
|
||||||
|
"ethioaa",
|
||||||
|
"ethiopic",
|
||||||
|
"gregory",
|
||||||
|
"hebrew",
|
||||||
|
"indian",
|
||||||
|
"islamic",
|
||||||
|
"islamic-civil",
|
||||||
|
"islamic-rcy",
|
||||||
|
"islamic-tbla",
|
||||||
|
"islamic-umalqura",
|
||||||
|
"iso8601",
|
||||||
|
"japanese",
|
||||||
|
"persian",
|
||||||
|
"roc",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// 严格白名单匹配:命中返回规范小写形式,未命中返回 None。
|
||||||
|
pub(crate) fn is_builtin_calendar_id(value: &str) -> Option<&'static str> {
|
||||||
|
CALENDAR_ID_WHITELIST.iter().find(|id| value.eq_ignore_ascii_case(id)).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 严格日历 ID 解析:只走 18 项白名单,拒绝 ISO 串(含注解/compact/extended/空串)。
|
||||||
|
/// 供构造器日历参数与注解值判定使用。
|
||||||
|
pub(crate) fn parse_temporal_calendar_id_strict(input: &str) -> Result<String, String> {
|
||||||
|
let trimmed = input.trim();
|
||||||
|
is_builtin_calendar_id(trimmed)
|
||||||
|
.map(str::to_string)
|
||||||
|
.ok_or_else(|| "invalid calendar".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ParseTemporalCalendarString:日历标识符 = 18 项内置日历 ID(ASCII 大小写不敏感)
|
||||||
|
/// 或合法 ISO 日期(-时间)字符串(含部分日期 YYYY-MM / MM-DD,可选时间、偏移、注解)。
|
||||||
|
/// 返回规范化日历 ID:白名单命中返回规范小写,ISO 串路径恒为 "iso8601"。
|
||||||
|
pub(crate) fn parse_temporal_calendar_string(input: &str) -> Result<String, String> {
|
||||||
|
let trimmed = input.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return Err("invalid calendar".into());
|
||||||
|
}
|
||||||
|
if let Some(id) = is_builtin_calendar_id(trimmed) {
|
||||||
|
return Ok(id.to_string());
|
||||||
|
}
|
||||||
|
if trimmed.contains('\u{2212}') {
|
||||||
|
return Err("variant minus sign is not valid for calendar".into());
|
||||||
|
}
|
||||||
|
// 完整日期时间字符串(含注解校验),例如 2020-01-01T00:00:00.000000000[u-ca=iso8601]
|
||||||
|
if parse_plain_date_time_string(trimmed).is_ok() {
|
||||||
|
return Ok("iso8601".to_string());
|
||||||
|
}
|
||||||
|
// 纯时间字符串(TemporalTimeString),例如 "15:23" / "T0030" / "152330.1-08"。
|
||||||
|
if parse_plain_time_string(trimmed).is_some() {
|
||||||
|
return Ok("iso8601".to_string());
|
||||||
|
}
|
||||||
|
// 部分日期:YYYY-MM 或 MM-DD(可带注解)
|
||||||
|
let text = trimmed.to_owned();
|
||||||
|
let annotation_start = text.find('[').unwrap_or(text.len());
|
||||||
|
validate_temporal_annotation_suffix(&text[annotation_start..])?;
|
||||||
|
let text = &text[..annotation_start];
|
||||||
|
if text.contains(['Z', 'z']) {
|
||||||
|
return Err("UTC designator is not valid for calendar".into());
|
||||||
|
}
|
||||||
|
parse_partial_calendar_date(text)?;
|
||||||
|
Ok("iso8601".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 部分 ISO 日期(无时间):YYYY[-MM[-DD]] 或 MM-DD;校验月份/日期基本范围并拒绝负零年。
|
||||||
|
pub(crate) fn parse_partial_calendar_date(input: &str) -> Result<(), String> {
|
||||||
|
let bytes = input.as_bytes();
|
||||||
|
let mut i = 0usize;
|
||||||
|
let signed = i < bytes.len() && matches!(bytes[i], b'+' | b'-');
|
||||||
|
let negative = signed && bytes[i] == b'-';
|
||||||
|
if signed {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
let start = i;
|
||||||
|
while i < bytes.len() && bytes[i].is_ascii_digit() {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
let digits = &input[start..i];
|
||||||
|
let rest = &input[i..];
|
||||||
|
if digits.is_empty() {
|
||||||
|
return Err("invalid calendar date".into());
|
||||||
|
}
|
||||||
|
// MM-DD 形式(无年份)
|
||||||
|
if digits.len() == 2 {
|
||||||
|
if !rest.starts_with('-') {
|
||||||
|
return Err("invalid calendar date".into());
|
||||||
|
}
|
||||||
|
let day_part = &rest[1..];
|
||||||
|
if day_part.len() != 2 || !day_part.bytes().all(|byte| byte.is_ascii_digit()) {
|
||||||
|
return Err("invalid calendar day".into());
|
||||||
|
}
|
||||||
|
let month: u32 = digits.parse().map_err(|_| "invalid calendar month".to_string())?;
|
||||||
|
let day: u32 = day_part.parse().map_err(|_| "invalid calendar day".to_string())?;
|
||||||
|
if month == 0 || month > 12 || day == 0 || day > 31 {
|
||||||
|
return Err("invalid calendar date".into());
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if !(4..=6).contains(&digits.len()) {
|
||||||
|
return Err("invalid calendar year".into());
|
||||||
|
}
|
||||||
|
if negative && digits.bytes().all(|byte| byte == b'0') {
|
||||||
|
return Err("invalid calendar negative zero year".into());
|
||||||
|
}
|
||||||
|
let mut rest2 = rest;
|
||||||
|
let mut month: Option<u32> = None;
|
||||||
|
let mut day: Option<u32> = None;
|
||||||
|
if rest2.starts_with('-') {
|
||||||
|
rest2 = &rest2[1..];
|
||||||
|
if rest2.len() < 2 || !rest2.as_bytes()[..2].iter().all(u8::is_ascii_digit) {
|
||||||
|
return Err("invalid calendar month".into());
|
||||||
|
}
|
||||||
|
month = Some(rest2[..2].parse().map_err(|_| "invalid calendar month".to_string())?);
|
||||||
|
rest2 = &rest2[2..];
|
||||||
|
if rest2.starts_with('-') {
|
||||||
|
rest2 = &rest2[1..];
|
||||||
|
if rest2.len() != 2 || !rest2.bytes().all(|byte| byte.is_ascii_digit()) {
|
||||||
|
return Err("invalid calendar day".into());
|
||||||
|
}
|
||||||
|
day = Some(rest2.parse().map_err(|_| "invalid calendar day".to_string())?);
|
||||||
|
rest2 = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !rest2.is_empty() {
|
||||||
|
return Err("invalid trailing calendar content".into());
|
||||||
|
}
|
||||||
|
if month.is_some_and(|m| m == 0 || m > 12) || day.is_some_and(|d| d == 0 || d > 31) {
|
||||||
|
return Err("invalid calendar date".into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ToTemporalCalendar(宽松版,property bag 路径用):undefined → None;
|
||||||
|
/// string → 宽松解析(白名单 ID 或 ISO 串);Temporal 实例 → 读日历槽(不触发属性 getter);
|
||||||
|
/// 其他类型 → TypeError。
|
||||||
|
pub(crate) fn temporal_calendar_id<H: VmHost>(vm: &mut H, value: JsValue) -> Result<Option<String>, JsValue> {
|
||||||
|
if value.is_undefined() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
if value.is_string() {
|
||||||
|
return parse_temporal_calendar_string(&to_string(value))
|
||||||
|
.map(Some)
|
||||||
|
.map_err(|_| crate::error::create_range_error(vm, "invalid calendar"));
|
||||||
|
}
|
||||||
|
if value.is_object() {
|
||||||
|
let ptr = value.as_js_object_ptr();
|
||||||
|
if !ptr.is_null() {
|
||||||
|
let obj = unsafe { &*ptr };
|
||||||
|
if obj.is_plain_date_obj() {
|
||||||
|
return Ok(Some(get_calendar_id(obj, 3)));
|
||||||
|
}
|
||||||
|
if obj.is_plain_date_time_obj() {
|
||||||
|
return Ok(Some(get_calendar_id(obj, 4)));
|
||||||
|
}
|
||||||
|
if obj.is_zoned_date_time_obj() {
|
||||||
|
return Ok(Some(get_calendar_id(obj, 2)));
|
||||||
|
}
|
||||||
|
if obj.is_plain_month_day_obj() || obj.is_plain_year_month_obj() {
|
||||||
|
return Ok(Some(get_calendar_id(obj, 3)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(crate::error::create_type_error(vm, "invalid calendar"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ToTemporalCalendar 严格版(构造器日历参数用):字符串只走 18 项白名单,拒绝 ISO 串;
|
||||||
|
/// undefined / 其他分支与宽松版一致。
|
||||||
|
pub(crate) fn temporal_calendar_id_strict<H: VmHost>(vm: &mut H, value: JsValue) -> Result<Option<String>, JsValue> {
|
||||||
|
if value.is_string() {
|
||||||
|
return parse_temporal_calendar_id_strict(&to_string(value))
|
||||||
|
.map(Some)
|
||||||
|
.map_err(|_| crate::error::create_range_error(vm, "invalid calendar"));
|
||||||
|
}
|
||||||
|
temporal_calendar_id(vm, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将 ZonedDateTime 按时区偏移转换为本地 PlainDateTime 分量。
|
||||||
|
pub(crate) 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 本地墙钟分量 + 时区偏移(分钟)→ 纪元纳秒(zoned_date_time_plain_parts 的逆)。
|
||||||
|
///
|
||||||
|
/// # 边界与前提
|
||||||
|
/// - (year, month, day, time_ns) 须已通过 valid_iso_date / valid_plain_time 校验(调用方保证)。
|
||||||
|
/// - 仅做 checked 溢出防护,Instant 范围校验由调用方按需执行。
|
||||||
|
pub(crate) fn local_to_epoch_ns(year: i32, month: u32, day: u32, time_ns: f64, offset_minutes: i32) -> 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)?
|
||||||
|
.checked_sub(i128::from(offset_minutes) * 60_000_000_000)
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,41 +10,6 @@ mod common;
|
|||||||
|
|
||||||
use common::*;
|
use common::*;
|
||||||
|
|
||||||
fn parse_digits(bytes: &[u8], cursor: &mut usize, count: usize) -> Option<i128> {
|
|
||||||
let end = cursor.checked_add(count)?;
|
|
||||||
let digits = bytes.get(*cursor..end)?;
|
|
||||||
if !digits.iter().all(u8::is_ascii_digit) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
*cursor = end;
|
|
||||||
digits
|
|
||||||
.iter()
|
|
||||||
.try_fold(0_i128, |value, digit| value.checked_mul(10)?.checked_add((digit - b'0') as i128))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_leap_year(year: i128) -> bool {
|
|
||||||
year.rem_euclid(4) == 0 && (year.rem_euclid(100) != 0 || year.rem_euclid(400) == 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn days_in_month(year: i128, month: i128) -> Option<i128> {
|
|
||||||
match month {
|
|
||||||
1 | 3 | 5 | 7 | 8 | 10 | 12 => Some(31),
|
|
||||||
4 | 6 | 9 | 11 => Some(30),
|
|
||||||
2 => Some(if is_leap_year(year) { 29 } else { 28 }),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn days_from_civil(mut year: i128, month: i128, day: i128) -> i128 {
|
|
||||||
year -= i128::from(month <= 2);
|
|
||||||
let era = year.div_euclid(400);
|
|
||||||
let year_of_era = year - era * 400;
|
|
||||||
let shifted_month = month + if month > 2 { -3 } else { 9 };
|
|
||||||
let day_of_year = (153 * shifted_month + 2) / 5 + day - 1;
|
|
||||||
let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
|
|
||||||
era * 146_097 + day_of_era - 719_468
|
|
||||||
}
|
|
||||||
|
|
||||||
fn instant_string_without_annotations(input: &str) -> Option<&str> {
|
fn instant_string_without_annotations(input: &str) -> Option<&str> {
|
||||||
let Some(first_annotation) = input.find('[') else {
|
let Some(first_annotation) = input.find('[') else {
|
||||||
return Some(input);
|
return Some(input);
|
||||||
@@ -796,140 +761,10 @@ pub fn instant_since<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
|||||||
instant_difference(vm, args, true)
|
instant_difference(vm, args, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FractionalSecondDigitsInput {
|
|
||||||
Auto,
|
|
||||||
Number(f64),
|
|
||||||
String(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_fractional_second_digits<H: VmHost>(
|
|
||||||
vm: &mut H, input: FractionalSecondDigitsInput,
|
|
||||||
) -> Result<Option<usize>, JsValue> {
|
|
||||||
match input {
|
|
||||||
FractionalSecondDigitsInput::Auto => Ok(None),
|
|
||||||
FractionalSecondDigitsInput::String(value) if value == "auto" => Ok(None),
|
|
||||||
FractionalSecondDigitsInput::String(_) => {
|
|
||||||
Err(crate::error::create_range_error(vm, "invalid fractionalSecondDigits"))
|
|
||||||
}
|
|
||||||
FractionalSecondDigitsInput::Number(value) => {
|
|
||||||
let digits = value.floor();
|
|
||||||
if !digits.is_finite() || !(0.0..=9.0).contains(&digits) {
|
|
||||||
return Err(crate::error::create_range_error(vm, "invalid fractionalSecondDigits"));
|
|
||||||
}
|
|
||||||
Ok(Some(digits as usize))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_offset_minutes(value: &str) -> Option<i32> {
|
|
||||||
let bytes = value.as_bytes();
|
|
||||||
if bytes.len() != 6 || !matches!(bytes[0], b'+' | b'-') || bytes[3] != b':' {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if !bytes[1..3].iter().all(u8::is_ascii_digit) || !bytes[4..6].iter().all(u8::is_ascii_digit) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let hours = i32::from(bytes[1] - b'0') * 10 + i32::from(bytes[2] - b'0');
|
|
||||||
let minutes = i32::from(bytes[4] - b'0') * 10 + i32::from(bytes[5] - b'0');
|
|
||||||
if hours > 23 || minutes > 59 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let magnitude = hours * 60 + minutes;
|
|
||||||
Some(if bytes[0] == b'-' { -magnitude } else { magnitude })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn canonical_time_zone(value: &str) -> Option<(String, i32)> {
|
|
||||||
let input = value.trim();
|
|
||||||
if input.eq_ignore_ascii_case("UTC") || input.eq_ignore_ascii_case("Z") {
|
|
||||||
return Some(("UTC".to_string(), 0));
|
|
||||||
}
|
|
||||||
if let Some(offset) = parse_offset_minutes(input) {
|
|
||||||
return Some((input.to_string(), offset));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ±HH 基本偏移("+01" / "-05"):3 字符,hour ≤ 23,分钟恒 0。仅放宽构造器时区接受面。
|
|
||||||
if input.len() == 3
|
|
||||||
&& matches!(input.as_bytes()[0], b'+' | b'-')
|
|
||||||
&& input.as_bytes()[1..3].iter().all(u8::is_ascii_digit)
|
|
||||||
{
|
|
||||||
let hour = (input.as_bytes()[1] - b'0') as i32 * 10 + (input.as_bytes()[2] - b'0') as i32;
|
|
||||||
if hour <= 23 {
|
|
||||||
let sign = if input.as_bytes()[0] == b'-' { -1 } else { 1 };
|
|
||||||
return Some((input.to_string(), sign * hour * 60));
|
|
||||||
}
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ±HHMM 基本偏移("+0000" / "-0530"):5 字符,无冒号,分钟位可选非零。4 位无冒号归一形式。
|
|
||||||
if input.len() == 5
|
|
||||||
&& matches!(input.as_bytes()[0], b'+' | b'-')
|
|
||||||
&& input.as_bytes()[1..5].iter().all(u8::is_ascii_digit)
|
|
||||||
{
|
|
||||||
let hour = (input.as_bytes()[1] - b'0') as i32 * 10 + (input.as_bytes()[2] - b'0') as i32;
|
|
||||||
let minute = (input.as_bytes()[3] - b'0') as i32 * 10 + (input.as_bytes()[4] - b'0') as i32;
|
|
||||||
if hour <= 23 && minute <= 59 {
|
|
||||||
let sign = if input.as_bytes()[0] == b'-' { -1 } else { 1 };
|
|
||||||
return Some((input.to_string(), sign * (hour * 60 + minute)));
|
|
||||||
}
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if input.starts_with("-000000") {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 带 annotation 的日期时间以最后一个方括号内标识符为准。
|
|
||||||
if let Some(open) = input.rfind('[') {
|
|
||||||
if !input.ends_with(']') || open + 2 > input.len() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let annotation = &input[open + 1..input.len() - 1];
|
|
||||||
if annotation.eq_ignore_ascii_case("UTC") {
|
|
||||||
return Some(("UTC".to_string(), 0));
|
|
||||||
}
|
|
||||||
return parse_offset_minutes(annotation).map(|offset| (annotation.to_string(), offset));
|
|
||||||
}
|
|
||||||
|
|
||||||
let time_start = input.find(['T', 't', ' '])?;
|
|
||||||
let time = &input[time_start + 1..];
|
|
||||||
if time.ends_with(['Z', 'z']) {
|
|
||||||
return Some(("UTC".to_string(), 0));
|
|
||||||
}
|
|
||||||
let offset_start = time
|
|
||||||
.char_indices()
|
|
||||||
.rev()
|
|
||||||
.find_map(|(index, ch)| matches!(ch, '+' | '-').then_some(index))?;
|
|
||||||
let offset_id = &time[offset_start..];
|
|
||||||
parse_offset_minutes(offset_id).map(|offset| (offset_id.to_string(), offset))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn instant_time_zone_offset(value: &str) -> Option<i32> {
|
fn instant_time_zone_offset(value: &str) -> Option<i32> {
|
||||||
canonical_time_zone(value).map(|(_, offset)| offset)
|
canonical_time_zone(value).map(|(_, offset)| offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn civil_from_days(days: i128) -> (i128, i128, i128) {
|
|
||||||
let shifted = days + 719_468;
|
|
||||||
let era = shifted.div_euclid(146_097);
|
|
||||||
let day_of_era = shifted - era * 146_097;
|
|
||||||
let year_of_era = (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
|
|
||||||
let mut year = year_of_era + era * 400;
|
|
||||||
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
|
|
||||||
let month_prime = (5 * day_of_year + 2) / 153;
|
|
||||||
let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
|
|
||||||
let month = month_prime + if month_prime < 10 { 3 } else { -9 };
|
|
||||||
year += i128::from(month <= 2);
|
|
||||||
(year, month, day)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_iso_year(year: i128) -> String {
|
|
||||||
if (0..=9_999).contains(&year) {
|
|
||||||
format!("{year:04}")
|
|
||||||
} else if year >= 0 {
|
|
||||||
format!("+{year:06}")
|
|
||||||
} else {
|
|
||||||
format!("-{:06}", -year)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_instant_iso(
|
fn format_instant_iso(
|
||||||
epoch_ns: i128, offset_minutes: Option<i32>, include_seconds: bool, fractional_digits: Option<usize>,
|
epoch_ns: i128, offset_minutes: Option<i32>, include_seconds: bool, fractional_digits: Option<usize>,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
@@ -5057,436 +4892,10 @@ pub fn plain_date_time_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> Native
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_temporal_annotation_suffix(mut suffix: &str) -> Result<(), String> {
|
|
||||||
let mut calendar_seen = false;
|
|
||||||
let mut saw_critical_calendar = false;
|
|
||||||
let mut time_zone_seen = false;
|
|
||||||
while !suffix.is_empty() {
|
|
||||||
if !suffix.starts_with('[') {
|
|
||||||
return Err("invalid annotation suffix".into());
|
|
||||||
}
|
|
||||||
let Some(end) = suffix.find(']') else {
|
|
||||||
return Err("unterminated annotation".into());
|
|
||||||
};
|
|
||||||
if end == 1 {
|
|
||||||
return Err("empty annotation".into());
|
|
||||||
}
|
|
||||||
let annotation = &suffix[1..end];
|
|
||||||
let (critical, body) = annotation.strip_prefix('!').map_or((false, annotation), |body| (true, body));
|
|
||||||
if let Some((key, value)) = body.split_once('=') {
|
|
||||||
if key.bytes().any(|byte| byte.is_ascii_uppercase()) {
|
|
||||||
return Err("annotation keys must be lowercase".into());
|
|
||||||
}
|
|
||||||
if key == "u-ca" {
|
|
||||||
if calendar_seen {
|
|
||||||
if critical || saw_critical_calendar {
|
|
||||||
return Err("invalid calendar annotation".into());
|
|
||||||
}
|
|
||||||
} else if is_builtin_calendar_id(value).is_none() {
|
|
||||||
return Err("invalid calendar annotation".into());
|
|
||||||
} else {
|
|
||||||
calendar_seen = true;
|
|
||||||
saw_critical_calendar = critical;
|
|
||||||
}
|
|
||||||
} else if critical {
|
|
||||||
return Err("unknown critical annotation".into());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if time_zone_seen {
|
|
||||||
return Err("invalid time-zone annotation".into());
|
|
||||||
}
|
|
||||||
time_zone_seen = true;
|
|
||||||
}
|
|
||||||
suffix = &suffix[end + 1..];
|
|
||||||
}
|
|
||||||
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> {
|
||||||
parse_temporal_string_impl(input, true)
|
parse_temporal_string_impl(input, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ParseTemporalDateString:PlainDate 字符串(时间部分可选且被忽略,仅按日期做范围校验)。
|
|
||||||
fn parse_plain_date_string(input: &str) -> Result<(i32, u32, u32), String> {
|
|
||||||
parse_temporal_string_impl(input, false).map(|(year, month, day, _)| (year, month, day))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_temporal_string_impl(input: &str, enforce_date_time_range: bool) -> Result<(i32, u32, u32, f64), String> {
|
|
||||||
let trimmed = input.trim();
|
|
||||||
if trimmed.contains('\u{2212}') {
|
|
||||||
return Err("variant minus sign is not valid for PlainDateTime".into());
|
|
||||||
}
|
|
||||||
let text = trimmed.to_owned();
|
|
||||||
let annotation_start = text.find('[').unwrap_or(text.len());
|
|
||||||
validate_temporal_annotation_suffix(&text[annotation_start..])?;
|
|
||||||
let text = &text[..annotation_start];
|
|
||||||
if text.contains('Z') || text.contains('z') {
|
|
||||||
return Err("UTC designator is not valid for PlainDateTime".into());
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
if !valid_iso_date(year, month, day)
|
|
||||||
|| (enforce_date_time_range && !valid_plain_date_time_range(year, month, day, 0.0))
|
|
||||||
{
|
|
||||||
return Err("invalid ISO date".into());
|
|
||||||
}
|
|
||||||
return Ok((year, month, day, 0.0));
|
|
||||||
};
|
|
||||||
|
|
||||||
let time_and_suffix = &text[separator + 1..];
|
|
||||||
let time_end = time_and_suffix
|
|
||||||
.char_indices()
|
|
||||||
.find_map(|(index, ch)| matches!(ch, '+' | '-').then_some(index))
|
|
||||||
.unwrap_or(time_and_suffix.len());
|
|
||||||
if time_end < time_and_suffix.len() && !strip_iso_offset(&time_and_suffix[time_end..])?.is_empty() {
|
|
||||||
return Err("invalid ISO offset".into());
|
|
||||||
}
|
|
||||||
let time = &time_and_suffix[..time_end];
|
|
||||||
if time.is_empty() {
|
|
||||||
return Err("missing ISO time".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let (clock, fraction) = match time.find(['.', ',']) {
|
|
||||||
Some(index) => (&time[..index], Some(&time[index + 1..])),
|
|
||||||
None => (time, None),
|
|
||||||
};
|
|
||||||
let fields = if clock.contains(':') {
|
|
||||||
clock.split(':').collect::<Vec<_>>()
|
|
||||||
} else {
|
|
||||||
if clock.len() % 2 != 0 || clock.len() > 6 {
|
|
||||||
return Err("invalid ISO time".into());
|
|
||||||
}
|
|
||||||
clock
|
|
||||||
.as_bytes()
|
|
||||||
.chunks(2)
|
|
||||||
.map(std::str::from_utf8)
|
|
||||||
.collect::<Result<Vec<_>, _>>()
|
|
||||||
.map_err(|_| "invalid ISO time".to_string())?
|
|
||||||
};
|
|
||||||
if fields.is_empty() || fields.len() > 3 || fields.iter().any(|field| field.len() != 2) {
|
|
||||||
return Err("invalid ISO time".into());
|
|
||||||
}
|
|
||||||
if fraction.is_some() && fields.len() < 3 {
|
|
||||||
return Err("fractional hours and minutes are not valid".into());
|
|
||||||
}
|
|
||||||
let parse_field = |index: usize| -> Result<u32, String> {
|
|
||||||
fields
|
|
||||||
.get(index)
|
|
||||||
.map_or(Ok(0), |field| field.parse().map_err(|_| "invalid ISO time".to_string()))
|
|
||||||
};
|
|
||||||
let hour = parse_field(0)?;
|
|
||||||
let minute = parse_field(1)?;
|
|
||||||
let mut second = parse_field(2)?;
|
|
||||||
if second == 60 {
|
|
||||||
second = 59;
|
|
||||||
}
|
|
||||||
let subsecond = match fraction {
|
|
||||||
Some(digits) if !digits.is_empty() && digits.len() <= 9 && digits.bytes().all(|byte| byte.is_ascii_digit()) => {
|
|
||||||
let mut padded = digits.to_owned();
|
|
||||||
padded.extend(std::iter::repeat('0').take(9 - digits.len()));
|
|
||||||
padded.parse::<u32>().map_err(|_| "invalid ISO fraction".to_string())?
|
|
||||||
}
|
|
||||||
Some(_) => return Err("invalid ISO fraction".into()),
|
|
||||||
None => 0,
|
|
||||||
};
|
|
||||||
let millisecond = subsecond / 1_000_000;
|
|
||||||
let microsecond = subsecond / 1_000 % 1_000;
|
|
||||||
let nanosecond = subsecond % 1_000;
|
|
||||||
if !valid_plain_time(hour, minute, second, millisecond, microsecond, nanosecond) {
|
|
||||||
return Err("invalid ISO time".into());
|
|
||||||
}
|
|
||||||
let total_ns = 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;
|
|
||||||
if !valid_iso_date(year, month, day)
|
|
||||||
|| (enforce_date_time_range && !valid_plain_date_time_range(year, month, day, total_ns))
|
|
||||||
{
|
|
||||||
return Err("invalid ISO date".into());
|
|
||||||
}
|
|
||||||
Ok((year, month, day, total_ns))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 规范 BuiltinCalendarID 全集(18 项)。匹配用 eq_ignore_ascii_case(ASCII 折叠,
|
|
||||||
/// 禁 to_lowercase:U+0130 点 I 会被 Unicode 折叠成 i+组合符,误判为 iso8601)。
|
|
||||||
const CALENDAR_ID_WHITELIST: [&str; 18] = [
|
|
||||||
"buddhist",
|
|
||||||
"chinese",
|
|
||||||
"coptic",
|
|
||||||
"dangi",
|
|
||||||
"ethioaa",
|
|
||||||
"ethiopic",
|
|
||||||
"gregory",
|
|
||||||
"hebrew",
|
|
||||||
"indian",
|
|
||||||
"islamic",
|
|
||||||
"islamic-civil",
|
|
||||||
"islamic-rcy",
|
|
||||||
"islamic-tbla",
|
|
||||||
"islamic-umalqura",
|
|
||||||
"iso8601",
|
|
||||||
"japanese",
|
|
||||||
"persian",
|
|
||||||
"roc",
|
|
||||||
];
|
|
||||||
|
|
||||||
/// 严格白名单匹配:命中返回规范小写形式,未命中返回 None。
|
|
||||||
fn is_builtin_calendar_id(value: &str) -> Option<&'static str> {
|
|
||||||
CALENDAR_ID_WHITELIST.iter().find(|id| value.eq_ignore_ascii_case(id)).copied()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 严格日历 ID 解析:只走 18 项白名单,拒绝 ISO 串(含注解/compact/extended/空串)。
|
|
||||||
/// 供构造器日历参数与注解值判定使用。
|
|
||||||
fn parse_temporal_calendar_id_strict(input: &str) -> Result<String, String> {
|
|
||||||
let trimmed = input.trim();
|
|
||||||
is_builtin_calendar_id(trimmed)
|
|
||||||
.map(str::to_string)
|
|
||||||
.ok_or_else(|| "invalid calendar".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ParseTemporalCalendarString:日历标识符 = 18 项内置日历 ID(ASCII 大小写不敏感)
|
|
||||||
/// 或合法 ISO 日期(-时间)字符串(含部分日期 YYYY-MM / MM-DD,可选时间、偏移、注解)。
|
|
||||||
/// 返回规范化日历 ID:白名单命中返回规范小写,ISO 串路径恒为 "iso8601"。
|
|
||||||
fn parse_temporal_calendar_string(input: &str) -> Result<String, String> {
|
|
||||||
let trimmed = input.trim();
|
|
||||||
if trimmed.is_empty() {
|
|
||||||
return Err("invalid calendar".into());
|
|
||||||
}
|
|
||||||
if let Some(id) = is_builtin_calendar_id(trimmed) {
|
|
||||||
return Ok(id.to_string());
|
|
||||||
}
|
|
||||||
if trimmed.contains('\u{2212}') {
|
|
||||||
return Err("variant minus sign is not valid for calendar".into());
|
|
||||||
}
|
|
||||||
// 完整日期时间字符串(含注解校验),例如 2020-01-01T00:00:00.000000000[u-ca=iso8601]
|
|
||||||
if parse_plain_date_time_string(trimmed).is_ok() {
|
|
||||||
return Ok("iso8601".to_string());
|
|
||||||
}
|
|
||||||
// 纯时间字符串(TemporalTimeString),例如 "15:23" / "T0030" / "152330.1-08"。
|
|
||||||
if parse_plain_time_string(trimmed).is_some() {
|
|
||||||
return Ok("iso8601".to_string());
|
|
||||||
}
|
|
||||||
// 部分日期:YYYY-MM 或 MM-DD(可带注解)
|
|
||||||
let text = trimmed.to_owned();
|
|
||||||
let annotation_start = text.find('[').unwrap_or(text.len());
|
|
||||||
validate_temporal_annotation_suffix(&text[annotation_start..])?;
|
|
||||||
let text = &text[..annotation_start];
|
|
||||||
if text.contains(['Z', 'z']) {
|
|
||||||
return Err("UTC designator is not valid for calendar".into());
|
|
||||||
}
|
|
||||||
parse_partial_calendar_date(text)?;
|
|
||||||
Ok("iso8601".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 部分 ISO 日期(无时间):YYYY[-MM[-DD]] 或 MM-DD;校验月份/日期基本范围并拒绝负零年。
|
|
||||||
fn parse_partial_calendar_date(input: &str) -> Result<(), String> {
|
|
||||||
let bytes = input.as_bytes();
|
|
||||||
let mut i = 0usize;
|
|
||||||
let signed = i < bytes.len() && matches!(bytes[i], b'+' | b'-');
|
|
||||||
let negative = signed && bytes[i] == b'-';
|
|
||||||
if signed {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
let start = i;
|
|
||||||
while i < bytes.len() && bytes[i].is_ascii_digit() {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
let digits = &input[start..i];
|
|
||||||
let rest = &input[i..];
|
|
||||||
if digits.is_empty() {
|
|
||||||
return Err("invalid calendar date".into());
|
|
||||||
}
|
|
||||||
// MM-DD 形式(无年份)
|
|
||||||
if digits.len() == 2 {
|
|
||||||
if !rest.starts_with('-') {
|
|
||||||
return Err("invalid calendar date".into());
|
|
||||||
}
|
|
||||||
let day_part = &rest[1..];
|
|
||||||
if day_part.len() != 2 || !day_part.bytes().all(|byte| byte.is_ascii_digit()) {
|
|
||||||
return Err("invalid calendar day".into());
|
|
||||||
}
|
|
||||||
let month: u32 = digits.parse().map_err(|_| "invalid calendar month".to_string())?;
|
|
||||||
let day: u32 = day_part.parse().map_err(|_| "invalid calendar day".to_string())?;
|
|
||||||
if month == 0 || month > 12 || day == 0 || day > 31 {
|
|
||||||
return Err("invalid calendar date".into());
|
|
||||||
}
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if !(4..=6).contains(&digits.len()) {
|
|
||||||
return Err("invalid calendar year".into());
|
|
||||||
}
|
|
||||||
if negative && digits.bytes().all(|byte| byte == b'0') {
|
|
||||||
return Err("invalid calendar negative zero year".into());
|
|
||||||
}
|
|
||||||
let mut rest2 = rest;
|
|
||||||
let mut month: Option<u32> = None;
|
|
||||||
let mut day: Option<u32> = None;
|
|
||||||
if rest2.starts_with('-') {
|
|
||||||
rest2 = &rest2[1..];
|
|
||||||
if rest2.len() < 2 || !rest2.as_bytes()[..2].iter().all(u8::is_ascii_digit) {
|
|
||||||
return Err("invalid calendar month".into());
|
|
||||||
}
|
|
||||||
month = Some(rest2[..2].parse().map_err(|_| "invalid calendar month".to_string())?);
|
|
||||||
rest2 = &rest2[2..];
|
|
||||||
if rest2.starts_with('-') {
|
|
||||||
rest2 = &rest2[1..];
|
|
||||||
if rest2.len() != 2 || !rest2.bytes().all(|byte| byte.is_ascii_digit()) {
|
|
||||||
return Err("invalid calendar day".into());
|
|
||||||
}
|
|
||||||
day = Some(rest2.parse().map_err(|_| "invalid calendar day".to_string())?);
|
|
||||||
rest2 = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !rest2.is_empty() {
|
|
||||||
return Err("invalid trailing calendar content".into());
|
|
||||||
}
|
|
||||||
if month.is_some_and(|m| m == 0 || m > 12) || day.is_some_and(|d| d == 0 || d > 31) {
|
|
||||||
return Err("invalid calendar date".into());
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ToTemporalCalendar(宽松版,property bag 路径用):undefined → None;
|
|
||||||
/// string → 宽松解析(白名单 ID 或 ISO 串);Temporal 实例 → 读日历槽(不触发属性 getter);
|
|
||||||
/// 其他类型 → TypeError。
|
|
||||||
fn temporal_calendar_id<H: VmHost>(vm: &mut H, value: JsValue) -> Result<Option<String>, JsValue> {
|
|
||||||
if value.is_undefined() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
if value.is_string() {
|
|
||||||
return parse_temporal_calendar_string(&to_string(value))
|
|
||||||
.map(Some)
|
|
||||||
.map_err(|_| crate::error::create_range_error(vm, "invalid calendar"));
|
|
||||||
}
|
|
||||||
if value.is_object() {
|
|
||||||
let ptr = value.as_js_object_ptr();
|
|
||||||
if !ptr.is_null() {
|
|
||||||
let obj = unsafe { &*ptr };
|
|
||||||
if obj.is_plain_date_obj() {
|
|
||||||
return Ok(Some(get_calendar_id(obj, 3)));
|
|
||||||
}
|
|
||||||
if obj.is_plain_date_time_obj() {
|
|
||||||
return Ok(Some(get_calendar_id(obj, 4)));
|
|
||||||
}
|
|
||||||
if obj.is_zoned_date_time_obj() {
|
|
||||||
return Ok(Some(get_calendar_id(obj, 2)));
|
|
||||||
}
|
|
||||||
if obj.is_plain_month_day_obj() || obj.is_plain_year_month_obj() {
|
|
||||||
return Ok(Some(get_calendar_id(obj, 3)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(crate::error::create_type_error(vm, "invalid calendar"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ToTemporalCalendar 严格版(构造器日历参数用):字符串只走 18 项白名单,拒绝 ISO 串;
|
|
||||||
/// undefined / 其他分支与宽松版一致。
|
|
||||||
fn temporal_calendar_id_strict<H: VmHost>(vm: &mut H, value: JsValue) -> Result<Option<String>, JsValue> {
|
|
||||||
if value.is_string() {
|
|
||||||
return parse_temporal_calendar_id_strict(&to_string(value))
|
|
||||||
.map(Some)
|
|
||||||
.map_err(|_| crate::error::create_range_error(vm, "invalid calendar"));
|
|
||||||
}
|
|
||||||
temporal_calendar_id(vm, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 将 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))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 当地午夜纪元纳秒(含 Instant 范围校验,spec GetStartOfDay 语义);越界返回 None。
|
/// 当地午夜纪元纳秒(含 Instant 范围校验,spec GetStartOfDay 语义);越界返回 None。
|
||||||
fn start_of_day_epoch_ns(year: i32, month: u32, day: u32, offset_minutes: i32) -> Option<i128> {
|
fn start_of_day_epoch_ns(year: i32, month: u32, day: u32, offset_minutes: i32) -> Option<i128> {
|
||||||
start_of_day_epoch_ns_by_days(days_from_civil(i128::from(year), i128::from(month), i128::from(day)), offset_minutes)
|
start_of_day_epoch_ns_by_days(days_from_civil(i128::from(year), i128::from(month), i128::from(day)), offset_minutes)
|
||||||
@@ -5500,18 +4909,6 @@ fn start_of_day_epoch_ns_by_days(days: i128, offset_minutes: i32) -> Option<i128
|
|||||||
(start.unsigned_abs() <= MAX_INSTANT_NS as u128).then_some(start)
|
(start.unsigned_abs() <= MAX_INSTANT_NS as u128).then_some(start)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 本地墙钟分量 + 时区偏移(分钟)→ 纪元纳秒(zoned_date_time_plain_parts 的逆)。
|
|
||||||
///
|
|
||||||
/// # 边界与前提
|
|
||||||
/// - (year, month, day, time_ns) 须已通过 valid_iso_date / valid_plain_time 校验(调用方保证)。
|
|
||||||
/// - 仅做 checked 溢出防护,Instant 范围校验由调用方按需执行。
|
|
||||||
fn local_to_epoch_ns(year: i32, month: u32, day: u32, time_ns: f64, offset_minutes: i32) -> 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)?
|
|
||||||
.checked_sub(i128::from(offset_minutes) * 60_000_000_000)
|
|
||||||
}
|
|
||||||
|
|
||||||
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, Option<String>), JsValue> {
|
) -> Result<(i32, u32, u32, f64, Option<String>), JsValue> {
|
||||||
|
|||||||
Reference in New Issue
Block a user