feat: Temporal PlainDate 字符串走 ParseTemporalDateString(注解/UTC/小数位/分数分钟校验),property bag 支持 monthCode 与日历标识符解析
This commit is contained in:
@@ -536,13 +536,6 @@ pub fn instant_compare<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
|||||||
NativeResult::Ok(JsValue::int(result))
|
NativeResult::Ok(JsValue::int(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 读对象的数值字段(普通对象访问器 getter 路径)。
|
|
||||||
fn read_prop_number<H: VmHost>(vm: &mut H, obj: &JsObject, receiver: JsValue, name: &str) -> f64 {
|
|
||||||
let key_val = vm.new_string(name);
|
|
||||||
let si = vm.property_key_si(key_val);
|
|
||||||
vm.ordinary_get(obj, si, receiver).map(to_number).unwrap_or(f64::NAN)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `Temporal.Instant.prototype.epochSeconds` getter。
|
/// `Temporal.Instant.prototype.epochSeconds` getter。
|
||||||
pub fn instant_epoch_seconds<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
pub fn instant_epoch_seconds<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||||
let ptr = native_try!(receiver_obj(vm, args));
|
let ptr = native_try!(receiver_obj(vm, args));
|
||||||
@@ -2070,34 +2063,17 @@ pub fn plain_date_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResul
|
|||||||
pub fn plain_date_from<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
pub fn plain_date_from<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||||
let val = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) };
|
let val = if args.len() < 2 { JsValue::undefined() } else { vm.reg(args[1]) };
|
||||||
let (year, month, day) = if val.is_string() {
|
let (year, month, day) = if val.is_string() {
|
||||||
let s = to_string(val);
|
match parse_plain_date_string(&to_string(val)) {
|
||||||
match parse_iso_date(&s) {
|
|
||||||
Ok(ymd) => ymd,
|
Ok(ymd) => ymd,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO 8601 date"));
|
return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO 8601 date"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if val.is_object() && {
|
|
||||||
let ptr = val.as_js_object_ptr();
|
|
||||||
!ptr.is_null() && unsafe { &*ptr }.is_plain_date_obj()
|
|
||||||
} {
|
|
||||||
let obj = unsafe { &*val.as_js_object_ptr() };
|
|
||||||
(
|
|
||||||
get_double_prop(obj, 0) as i32,
|
|
||||||
get_double_prop(obj, 1) as u32,
|
|
||||||
get_double_prop(obj, 2) as u32,
|
|
||||||
)
|
|
||||||
} else if val.is_object() {
|
|
||||||
let obj = unsafe { &*val.as_js_object_ptr() };
|
|
||||||
let y = read_prop_number(vm, obj, val, "year");
|
|
||||||
let m = read_prop_number(vm, obj, val, "month");
|
|
||||||
let d = read_prop_number(vm, obj, val, "day");
|
|
||||||
if y.is_nan() || m.is_nan() || d.is_nan() {
|
|
||||||
return NativeResult::Err(crate::error::create_type_error(vm, "cannot convert object to PlainDate"));
|
|
||||||
}
|
|
||||||
(y.trunc() as i32, m.trunc() as u32, d.trunc() as u32)
|
|
||||||
} else {
|
} else {
|
||||||
return NativeResult::Err(crate::error::create_type_error(vm, "cannot convert value to PlainDate"));
|
match object_date_ymd(vm, val) {
|
||||||
|
Ok(ymd) => ymd,
|
||||||
|
Err(error) => return NativeResult::Err(error),
|
||||||
|
}
|
||||||
};
|
};
|
||||||
if !valid_iso_date(year, month, day) {
|
if !valid_iso_date(year, month, day) {
|
||||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO date"));
|
return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO date"));
|
||||||
@@ -2474,6 +2450,18 @@ fn strip_iso_offset(input: &str) -> Result<&str, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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();
|
let trimmed = input.trim();
|
||||||
if trimmed.contains('\u{2212}') {
|
if trimmed.contains('\u{2212}') {
|
||||||
return Err("variant minus sign is not valid for PlainDateTime".into());
|
return Err("variant minus sign is not valid for PlainDateTime".into());
|
||||||
@@ -2492,7 +2480,9 @@ fn parse_plain_date_time_string(input: &str) -> Result<(i32, u32, u32, f64), Str
|
|||||||
};
|
};
|
||||||
let (year, month, day) = parse_iso_date(date_part)?;
|
let (year, month, day) = parse_iso_date(date_part)?;
|
||||||
let Some(separator) = separator else {
|
let Some(separator) = separator else {
|
||||||
if !valid_iso_date(year, month, day) || !valid_plain_date_time_range(year, month, day, 0.0) {
|
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 Err("invalid ISO date".into());
|
||||||
}
|
}
|
||||||
return Ok((year, month, day, 0.0));
|
return Ok((year, month, day, 0.0));
|
||||||
@@ -2564,19 +2554,129 @@ fn parse_plain_date_time_string(input: &str) -> Result<(i32, u32, u32, f64), Str
|
|||||||
+ 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) {
|
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());
|
return Err("invalid ISO date".into());
|
||||||
}
|
}
|
||||||
Ok((year, month, day, total_ns))
|
Ok((year, month, day, total_ns))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ParseTemporalCalendarString:日历标识符 = "iso8601"(ASCII 大小写不敏感)或合法 ISO
|
||||||
|
/// 日期(-时间)字符串(含部分日期 YYYY-MM / MM-DD,可选时间、偏移、注解)。
|
||||||
|
fn parse_temporal_calendar_string(input: &str) -> Result<(), String> {
|
||||||
|
let trimmed = input.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return Err("invalid calendar".into());
|
||||||
|
}
|
||||||
|
if trimmed.eq_ignore_ascii_case("iso8601") {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
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(());
|
||||||
|
}
|
||||||
|
// 部分日期: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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 部分 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 校验:非 string 抛 TypeError;字符串必须通过 ParseTemporalCalendarString。
|
||||||
|
fn temporal_calendar_check<H: VmHost>(vm: &mut H, value: JsValue) -> Result<(), JsValue> {
|
||||||
|
if value.is_undefined() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if !value.is_string() {
|
||||||
|
return Err(crate::error::create_type_error(vm, "invalid calendar"));
|
||||||
|
}
|
||||||
|
if parse_temporal_calendar_string(&to_string(value)).is_err() {
|
||||||
|
return Err(crate::error::create_range_error(vm, "invalid calendar"));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn plain_date_time_object_parts<H: VmHost>(
|
fn plain_date_time_object_parts<H: VmHost>(
|
||||||
vm: &mut H, value: JsValue, obj: &JsObject, constrain: bool,
|
vm: &mut H, value: JsValue, obj: &JsObject, constrain: bool,
|
||||||
) -> Result<(i32, u32, u32, f64), JsValue> {
|
) -> Result<(i32, u32, u32, f64), JsValue> {
|
||||||
let calendar = temporal_option_value(vm, obj, value, "calendar")?;
|
let calendar = temporal_option_value(vm, obj, value, "calendar")?;
|
||||||
if !calendar.is_undefined() && !temporal_option_string(vm, calendar)?.eq_ignore_ascii_case("iso8601") {
|
temporal_calendar_check(vm, calendar)?;
|
||||||
return Err(crate::error::create_range_error(vm, "unsupported calendar"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 先按规范顺序读取全部原始字段,暂不转换类型。
|
// 先按规范顺序读取全部原始字段,暂不转换类型。
|
||||||
let (
|
let (
|
||||||
@@ -3941,36 +4041,32 @@ fn plain_date_ymd_checked<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<(i32, u3
|
|||||||
|
|
||||||
/// 从对象式日期字段(`{year, month, day}`)读三字段;PlainDate 对象直接读内部槽;
|
/// 从对象式日期字段(`{year, month, day}`)读三字段;PlainDate 对象直接读内部槽;
|
||||||
/// 缺字段返回 None。
|
/// 缺字段返回 None。
|
||||||
fn object_ymd<H: VmHost>(vm: &mut H, val: JsValue) -> Option<(i32, u32, u32)> {
|
fn object_date_ymd<H: VmHost>(vm: &mut H, val: JsValue) -> Result<(i32, u32, u32), JsValue> {
|
||||||
if !val.is_object() {
|
if !val.is_object() {
|
||||||
return None;
|
return Err(crate::error::create_type_error(vm, "cannot convert to PlainDate"));
|
||||||
}
|
}
|
||||||
let ptr = val.as_js_object_ptr();
|
let ptr = val.as_js_object_ptr();
|
||||||
if ptr.is_null() {
|
if ptr.is_null() {
|
||||||
return None;
|
return Err(crate::error::create_type_error(vm, "cannot convert to PlainDate"));
|
||||||
}
|
}
|
||||||
let obj = unsafe { &*ptr };
|
let obj = unsafe { &*ptr };
|
||||||
if obj.is_plain_date_obj() {
|
if obj.is_plain_date_obj() {
|
||||||
return Some((
|
return Ok((
|
||||||
get_double_prop(obj, 0) as i32,
|
get_double_prop(obj, 0) as i32,
|
||||||
get_double_prop(obj, 1) as u32,
|
get_double_prop(obj, 1) as u32,
|
||||||
get_double_prop(obj, 2) as u32,
|
get_double_prop(obj, 2) as u32,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let y = read_prop_number(vm, obj, val, "year");
|
let (year, month, day, _) = plain_date_time_object_parts(vm, val, obj, true)?;
|
||||||
let m = read_prop_number(vm, obj, val, "month");
|
Ok((year, month, day))
|
||||||
let d = read_prop_number(vm, obj, val, "day");
|
|
||||||
if y.is_nan() || m.is_nan() || d.is_nan() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some((y.trunc() as i32, m.trunc() as u32, d.trunc() as u32))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn date_like_ymd<H: VmHost>(vm: &mut H, val: JsValue) -> Result<(i32, u32, u32), JsValue> {
|
fn date_like_ymd<H: VmHost>(vm: &mut H, val: JsValue) -> Result<(i32, u32, u32), JsValue> {
|
||||||
let ymd = if val.is_string() {
|
let ymd = if val.is_string() {
|
||||||
parse_iso_date(&to_string(val)).map_err(|_| crate::error::create_range_error(vm, "invalid ISO 8601 date"))?
|
parse_plain_date_string(&to_string(val))
|
||||||
|
.map_err(|_| crate::error::create_range_error(vm, "invalid ISO 8601 date"))?
|
||||||
} else {
|
} else {
|
||||||
object_ymd(vm, val).ok_or_else(|| crate::error::create_type_error(vm, "cannot convert to PlainDate"))?
|
object_date_ymd(vm, val)?
|
||||||
};
|
};
|
||||||
if !valid_iso_date(ymd.0, ymd.1, ymd.2) {
|
if !valid_iso_date(ymd.0, ymd.1, ymd.2) {
|
||||||
return Err(crate::error::create_range_error(vm, "invalid ISO date"));
|
return Err(crate::error::create_range_error(vm, "invalid ISO date"));
|
||||||
|
|||||||
Reference in New Issue
Block a user