feat: implement Temporal PlainDateTime conversion and serialization
This commit is contained in:
@@ -1343,6 +1343,17 @@ fn make_plain_time<H: VmHost>(vm: &mut H, total_ns: f64) -> NativeResult {
|
|||||||
NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj)))
|
NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn make_plain_date_time<H: VmHost>(vm: &mut H, year: i32, month: u32, day: u32, total_ns: f64) -> NativeResult {
|
||||||
|
let proto = JsValue::from_js_object(vm.session().builtin_world().plain_date_time_proto.as_ptr() as *mut JsObject);
|
||||||
|
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto);
|
||||||
|
obj.type_tag = JsObject::OBJ_TYPE_PLAIN_DATE_TIME;
|
||||||
|
obj.set_prop_at(0, JsValue::float(year as f64));
|
||||||
|
obj.set_prop_at(1, JsValue::float(month as f64));
|
||||||
|
obj.set_prop_at(2, JsValue::float(day as f64));
|
||||||
|
obj.set_prop_at(3, JsValue::float(total_ns));
|
||||||
|
NativeResult::Ok(JsValue::from_js_object(vm.alloc_object(obj)))
|
||||||
|
}
|
||||||
|
|
||||||
fn make_duration<H: VmHost>(vm: &mut H, values: [f64; 10]) -> NativeResult {
|
fn make_duration<H: VmHost>(vm: &mut H, values: [f64; 10]) -> NativeResult {
|
||||||
let proto = JsValue::from_js_object(vm.session().builtin_world().duration_proto.as_ptr() as *mut JsObject);
|
let proto = JsValue::from_js_object(vm.session().builtin_world().duration_proto.as_ptr() as *mut JsObject);
|
||||||
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto);
|
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto);
|
||||||
@@ -1812,7 +1823,8 @@ fn parse_iso_date(s: &str) -> Result<(i32, u32, u32), String> {
|
|||||||
}
|
}
|
||||||
let bytes = t.as_bytes();
|
let bytes = t.as_bytes();
|
||||||
let mut i = 0usize;
|
let mut i = 0usize;
|
||||||
let negative = if i < bytes.len() && (bytes[i] == b'-' || bytes[i] == b'+') {
|
let signed_year = i < bytes.len() && (bytes[i] == b'-' || bytes[i] == b'+');
|
||||||
|
let negative = if signed_year {
|
||||||
let neg = bytes[i] == b'-';
|
let neg = bytes[i] == b'-';
|
||||||
i += 1;
|
i += 1;
|
||||||
neg
|
neg
|
||||||
@@ -1838,7 +1850,8 @@ fn parse_iso_date(s: &str) -> Result<(i32, u32, u32), String> {
|
|||||||
} else {
|
} else {
|
||||||
(digits, None)
|
(digits, None)
|
||||||
};
|
};
|
||||||
if year_digits.len() < 4 || year_digits.len() > 6 {
|
let expected_year_digits = if signed_year { 6 } else { 4 };
|
||||||
|
if year_digits.len() != expected_year_digits {
|
||||||
return Err("invalid ISO year".into());
|
return Err("invalid ISO year".into());
|
||||||
}
|
}
|
||||||
let mut year: i64 = year_digits.parse().map_err(|_| "invalid ISO year".to_string())?;
|
let mut year: i64 = year_digits.parse().map_err(|_| "invalid ISO year".to_string())?;
|
||||||
@@ -1874,8 +1887,8 @@ fn parse_iso_date(s: &str) -> Result<(i32, u32, u32), String> {
|
|||||||
if month == 0 || month > 12 || day == 0 {
|
if month == 0 || month > 12 || day == 0 {
|
||||||
return Err("invalid ISO date".into());
|
return Err("invalid ISO date".into());
|
||||||
}
|
}
|
||||||
if year == 0 {
|
if negative && year == 0 {
|
||||||
return Err("invalid ISO year zero".into());
|
return Err("invalid ISO negative zero year".into());
|
||||||
}
|
}
|
||||||
Ok((year as i32, month, day))
|
Ok((year as i32, month, day))
|
||||||
}
|
}
|
||||||
@@ -2177,6 +2190,333 @@ 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 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 || !value.eq_ignore_ascii_case("iso8601") {
|
||||||
|
return Err("invalid calendar annotation".into());
|
||||||
|
}
|
||||||
|
calendar_seen = true;
|
||||||
|
} else if critical {
|
||||||
|
return Err("unknown critical annotation".into());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if critical || time_zone_seen {
|
||||||
|
return Err("invalid time-zone annotation".into());
|
||||||
|
}
|
||||||
|
time_zone_seen = true;
|
||||||
|
}
|
||||||
|
suffix = &suffix[end + 1..];
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_plain_date_time_string(input: &str) -> 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 (year, month, day) = parse_iso_date(text)?;
|
||||||
|
let separator = text.find(['T', 't', ' ']);
|
||||||
|
let Some(separator) = separator else {
|
||||||
|
if text.contains('+') {
|
||||||
|
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 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() {
|
||||||
|
let suffix = &time_and_suffix[time_end..];
|
||||||
|
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];
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
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
|
||||||
|
+ minute as f64 * 60_000_000_000.0
|
||||||
|
+ second as f64 * 1_000_000_000.0
|
||||||
|
+ subsecond as f64;
|
||||||
|
Ok((year, month, day, total_ns))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plain_date_time_object_parts<H: VmHost>(
|
||||||
|
vm: &mut H, value: JsValue, obj: &JsObject, constrain: bool,
|
||||||
|
) -> Result<(i32, u32, u32, f64), JsValue> {
|
||||||
|
let calendar = temporal_option_value(vm, obj, value, "calendar")?;
|
||||||
|
if !calendar.is_undefined() && !temporal_option_string(vm, calendar)?.eq_ignore_ascii_case("iso8601") {
|
||||||
|
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 raw = temporal_option_value(vm, obj, value, name)?;
|
||||||
|
if raw.is_undefined() {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
temporal_option_number(vm, raw).map(|number| Some(number.trunc()))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(
|
||||||
|
number_field("day")?,
|
||||||
|
number_field("hour")?.unwrap_or(0.0),
|
||||||
|
number_field("microsecond")?.unwrap_or(0.0),
|
||||||
|
number_field("millisecond")?.unwrap_or(0.0),
|
||||||
|
number_field("minute")?.unwrap_or(0.0),
|
||||||
|
number_field("month")?,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let month_code_raw = temporal_option_value(vm, obj, value, "monthCode")?;
|
||||||
|
let month_code = if month_code_raw.is_undefined() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let code = temporal_option_string(vm, month_code_raw)?;
|
||||||
|
let parsed = code
|
||||||
|
.strip_prefix('M')
|
||||||
|
.filter(|digits| digits.len() == 2)
|
||||||
|
.and_then(|digits| digits.parse::<u32>().ok())
|
||||||
|
.filter(|number| (1..=12).contains(number))
|
||||||
|
.ok_or_else(|| crate::error::create_range_error(vm, "invalid monthCode"))?;
|
||||||
|
Some(parsed as f64)
|
||||||
|
};
|
||||||
|
let mut number_field = |name: &str| -> Result<Option<f64>, JsValue> {
|
||||||
|
let raw = temporal_option_value(vm, obj, value, name)?;
|
||||||
|
if raw.is_undefined() {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
temporal_option_number(vm, raw).map(|number| Some(number.trunc()))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let nanosecond = number_field("nanosecond")?.unwrap_or(0.0);
|
||||||
|
let second = number_field("second")?.unwrap_or(0.0);
|
||||||
|
let year = number_field("year")?;
|
||||||
|
|
||||||
|
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 month = match (month, month_code) {
|
||||||
|
(Some(month), Some(code)) if month != code => {
|
||||||
|
return Err(crate::error::create_range_error(vm, "month and monthCode disagree"));
|
||||||
|
}
|
||||||
|
(Some(month), _) => month,
|
||||||
|
(None, Some(code)) => code,
|
||||||
|
(None, None) => return Err(crate::error::create_type_error(vm, "month is required")),
|
||||||
|
};
|
||||||
|
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) {
|
||||||
|
return Err(crate::error::create_range_error(vm, "invalid date-time component"));
|
||||||
|
}
|
||||||
|
if constrain {
|
||||||
|
values[1] = values[1].clamp(1.0, 12.0);
|
||||||
|
values[2] = values[2].clamp(1.0, days_in_month(values[0] as i128, values[1] as i128).unwrap_or(31) as f64);
|
||||||
|
values[3] = values[3].clamp(0.0, 23.0);
|
||||||
|
values[4] = values[4].clamp(0.0, 59.0);
|
||||||
|
values[5] = values[5].clamp(0.0, 59.0);
|
||||||
|
values[6] = values[6].clamp(0.0, 999.0);
|
||||||
|
values[7] = values[7].clamp(0.0, 999.0);
|
||||||
|
values[8] = values[8].clamp(0.0, 999.0);
|
||||||
|
}
|
||||||
|
let [year, month, day, hour, minute, second, millisecond, microsecond, nanosecond] = values;
|
||||||
|
let (year, month, day) = (year as i32, month as u32, day as u32);
|
||||||
|
let (hour, minute, second) = (hour as u32, minute as u32, second as u32);
|
||||||
|
let (millisecond, microsecond, nanosecond) = (millisecond as u32, microsecond as u32, nanosecond as u32);
|
||||||
|
if !valid_iso_date(year, month, day)
|
||||||
|
|| !valid_plain_time(hour, minute, second, millisecond, microsecond, nanosecond)
|
||||||
|
{
|
||||||
|
return Err(crate::error::create_range_error(vm, "invalid date-time component"));
|
||||||
|
}
|
||||||
|
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
|
||||||
|
+ millisecond as f64 * 1_000_000.0
|
||||||
|
+ microsecond as f64 * 1_000.0
|
||||||
|
+ nanosecond as f64;
|
||||||
|
Ok((year, month, day, total_ns))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plain_date_time_like_parts<H: VmHost>(
|
||||||
|
vm: &mut H, value: JsValue, constrain: bool,
|
||||||
|
) -> Result<(i32, u32, u32, f64), JsValue> {
|
||||||
|
if value.is_string() {
|
||||||
|
return parse_plain_date_time_string(&to_string(value))
|
||||||
|
.map_err(|_| crate::error::create_range_error(vm, "invalid ISO 8601 date-time"));
|
||||||
|
}
|
||||||
|
if !value.is_object() {
|
||||||
|
return Err(crate::error::create_type_error(vm, "cannot convert to PlainDateTime"));
|
||||||
|
}
|
||||||
|
let ptr = value.as_js_object_ptr();
|
||||||
|
if ptr.is_null() {
|
||||||
|
return Err(crate::error::create_type_error(vm, "cannot convert to PlainDateTime"));
|
||||||
|
}
|
||||||
|
let obj = unsafe { &*ptr };
|
||||||
|
if obj.is_plain_date_time_obj() {
|
||||||
|
return Ok((
|
||||||
|
get_double_prop(obj, 0) as i32,
|
||||||
|
get_double_prop(obj, 1) as u32,
|
||||||
|
get_double_prop(obj, 2) as u32,
|
||||||
|
get_double_prop(obj, 3),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if obj.is_plain_date_obj() {
|
||||||
|
return Ok((
|
||||||
|
get_double_prop(obj, 0) as i32,
|
||||||
|
get_double_prop(obj, 1) as u32,
|
||||||
|
get_double_prop(obj, 2) as u32,
|
||||||
|
0.0,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
plain_date_time_object_parts(vm, value, obj, constrain)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plain_date_time_overflow<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<bool, JsValue> {
|
||||||
|
let options = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() };
|
||||||
|
if options.is_undefined() {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
if !options.is_object() {
|
||||||
|
return Err(crate::error::create_type_error(vm, "options must be an object"));
|
||||||
|
}
|
||||||
|
let ptr = options.as_js_object_ptr();
|
||||||
|
if ptr.is_null() {
|
||||||
|
return Err(crate::error::create_type_error(vm, "options must be an object"));
|
||||||
|
}
|
||||||
|
let raw = temporal_option_value(vm, unsafe { &*ptr }, options, "overflow")?;
|
||||||
|
if raw.is_undefined() {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
let value = temporal_option_string(vm, raw)?;
|
||||||
|
match value.as_str() {
|
||||||
|
"constrain" => Ok(true),
|
||||||
|
"reject" => Ok(false),
|
||||||
|
_ => Err(crate::error::create_range_error(vm, "invalid overflow")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Temporal.PlainDateTime.from(item)`:从实例、ISO 字符串或字段对象创建副本。
|
||||||
|
pub fn plain_date_time_from<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||||
|
let value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
|
||||||
|
let constrain = native_try!(plain_date_time_overflow(vm, args));
|
||||||
|
let (year, month, day, total_ns) = native_try!(plain_date_time_like_parts(vm, value, constrain));
|
||||||
|
make_plain_date_time(vm, year, month, day, total_ns)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Temporal.PlainDateTime.compare(one, two)`:按 ISO 日期时间字段做字典序比较。
|
||||||
|
pub fn plain_date_time_compare<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||||
|
let one = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
|
||||||
|
let two = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() };
|
||||||
|
let one = native_try!(plain_date_time_like_parts(vm, one, true));
|
||||||
|
let two = native_try!(plain_date_time_like_parts(vm, two, true));
|
||||||
|
let ordering = (one.0, one.1, one.2, one.3 as u64).cmp(&(two.0, two.1, two.2, two.3 as u64));
|
||||||
|
NativeResult::Ok(JsValue::int(match ordering {
|
||||||
|
std::cmp::Ordering::Less => -1,
|
||||||
|
std::cmp::Ordering::Equal => 0,
|
||||||
|
std::cmp::Ordering::Greater => 1,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
fn plain_date_time_parts<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<(i32, u32, u32, f64), JsValue> {
|
fn plain_date_time_parts<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<(i32, u32, u32, f64), JsValue> {
|
||||||
let ptr = receiver_obj(vm, args)?;
|
let ptr = receiver_obj(vm, args)?;
|
||||||
let obj = unsafe { &*ptr };
|
let obj = unsafe { &*ptr };
|
||||||
@@ -2340,6 +2680,35 @@ 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"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (hour, minute, second, millisecond, microsecond, nanosecond) = plain_time_components(total_ns);
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Temporal.PlainDateTime.prototype.toString()`:输出本地 ISO 日期时间。
|
||||||
|
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() {
|
||||||
|
return NativeResult::Err(crate::error::create_type_error(vm, "options must be an object"));
|
||||||
|
}
|
||||||
|
plain_date_time_iso_string(vm, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Temporal.PlainDateTime.prototype.toJSON()`:输出默认 ISO 日期时间。
|
||||||
|
pub fn plain_date_time_to_json<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||||
|
plain_date_time_iso_string(vm, args)
|
||||||
|
}
|
||||||
|
|
||||||
// ───────────────────── PlainDate 扩展方法 ─────────────────────
|
// ───────────────────── PlainDate 扩展方法 ─────────────────────
|
||||||
|
|
||||||
/// 从 PlainDate receiver 读 year/month/day 并构造 chrono NaiveDate(供日期计算)。
|
/// 从 PlainDate receiver 读 year/month/day 并构造 chrono NaiveDate(供日期计算)。
|
||||||
|
|||||||
@@ -394,6 +394,19 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
|
|||||||
.map_or(0, |props| props.len() as u32)
|
.map_or(0, |props| props.len() as u32)
|
||||||
.saturating_sub(1);
|
.saturating_sub(1);
|
||||||
plain_date_time_ctor.set_data_meta(length_pos, PropAttributes::new(false, false, true));
|
plain_date_time_ctor.set_data_meta(length_pos, PropAttributes::new(false, false, true));
|
||||||
|
apply_binding_table(
|
||||||
|
world,
|
||||||
|
plain_date_time_ctor,
|
||||||
|
core,
|
||||||
|
&[
|
||||||
|
("from", oxide_builtins::temporal::plain_date_time_from::<crate::vm::Vm> as *const (), 1),
|
||||||
|
(
|
||||||
|
"compare",
|
||||||
|
oxide_builtins::temporal::plain_date_time_compare::<crate::vm::Vm> as *const (),
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
let plain_date_time_proto_ptr = world.plain_date_time_proto.as_ptr() as *mut JsObject;
|
let plain_date_time_proto_ptr = world.plain_date_time_proto.as_ptr() as *mut JsObject;
|
||||||
let plain_date_time_proto = unsafe { &mut *plain_date_time_proto_ptr };
|
let plain_date_time_proto = unsafe { &mut *plain_date_time_proto_ptr };
|
||||||
for (name, getter) in [
|
for (name, getter) in [
|
||||||
@@ -487,6 +500,16 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
|
|||||||
oxide_builtins::temporal::plain_date_time_value_of::<crate::vm::Vm> as *const (),
|
oxide_builtins::temporal::plain_date_time_value_of::<crate::vm::Vm> as *const (),
|
||||||
0,
|
0,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"toString",
|
||||||
|
oxide_builtins::temporal::plain_date_time_to_string::<crate::vm::Vm> as *const (),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"toJSON",
|
||||||
|
oxide_builtins::temporal::plain_date_time_to_json::<crate::vm::Vm> as *const (),
|
||||||
|
0,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
bind_well_known_data_property(
|
bind_well_known_data_property(
|
||||||
|
|||||||
@@ -686,3 +686,47 @@ fn plain_date_time_metadata_and_value_of() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(str_val(&vm, r), "3,true,[object Temporal.PlainDateTime],TypeError");
|
assert_eq!(str_val(&vm, r), "3,true,[object Temporal.PlainDateTime],TypeError");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plain_date_time_from_instances_strings_and_objects() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let r = eval(
|
||||||
|
&mut vm,
|
||||||
|
"let original = new Temporal.PlainDateTime(1976, 11, 18, 15, 23, 30, 1, 123, 456);
|
||||||
|
let copy = Temporal.PlainDateTime.from(original);
|
||||||
|
let parsed = Temporal.PlainDateTime.from('1976-11-18T15:23:30.001123456');
|
||||||
|
let bag = Temporal.PlainDateTime.from({year: 1976, monthCode: 'M11', day: 18, hour: 15});
|
||||||
|
(copy !== original) + ',' + parsed.toString() + ',' + bag.toString()",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(str_val(&vm, r), "true,1976-11-18T15:23:30.001123456,1976-11-18T15:00:00");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plain_date_time_compare_uses_internal_slots() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let r = eval(
|
||||||
|
&mut vm,
|
||||||
|
"let earlier = new Temporal.PlainDateTime(2000, 5, 2, 12, 34, 56);
|
||||||
|
let later = new Temporal.PlainDateTime(2000, 5, 2, 12, 34, 57);
|
||||||
|
Object.defineProperty(earlier, 'year', {get() { throw new Error('getter') }});
|
||||||
|
Object.defineProperty(later, 'year', {get() { throw new Error('getter') }});
|
||||||
|
Temporal.PlainDateTime.compare(earlier, later) + ',' +
|
||||||
|
Temporal.PlainDateTime.compare(later, earlier) + ',' +
|
||||||
|
Temporal.PlainDateTime.compare(earlier, earlier)",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(str_val(&vm, r), "-1,1,0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plain_date_time_serializes_extended_years() {
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let r = eval(
|
||||||
|
&mut vm,
|
||||||
|
"new Temporal.PlainDateTime(-1, 8, 7, 6, 54, 32, 100).toString() + ',' +
|
||||||
|
new Temporal.PlainDateTime(10000, 6, 7, 8, 9, 10, 987).toJSON()",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(str_val(&vm, r), "-000001-08-07T06:54:32.1,+010000-06-07T08:09:10.987");
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user