feat: Temporal.PlainMonthDay.from——ISO 串解析/字段 bag/实例复制与 overflow 语义

This commit is contained in:
2026-09-12 04:23:44 +00:00
parent e4e278dd19
commit 2fcae4fbca
3 changed files with 638 additions and 5 deletions
+483 -5
View File
@@ -7219,10 +7219,24 @@ pub fn plain_date_day_of_year<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResul
/// 计算给定年月的天数(含闰年)。
fn days_in_month_iso(year: i32, month: u32) -> u32 {
NaiveDate::from_ymd_opt(year, month + 1, 1)
.and_then(|next| next.checked_sub_days(Days::new(1)))
.map(|d| d.day())
.unwrap_or(31)
if let Some(next) = NaiveDate::from_ymd_opt(year, month + 1, 1) {
if let Some(prev) = next.checked_sub_days(Days::new(1)) {
return prev.day();
}
}
// 年越 chrono 表示范围(PMD from 的 bag year 可任意 i32):直接按 ISO 规则。
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
_ => {
// 闰年判定须用数学取模(负年),与 chrono 域内结果一致。
if year.rem_euclid(4) == 0 && (year.rem_euclid(100) != 0 || year.rem_euclid(400) == 0) {
29
} else {
28
}
}
}
}
/// `Temporal.PlainDate.prototype.daysInMonth` getter。
@@ -7518,7 +7532,6 @@ fn iso_year_month_within_limits(year: i32, month: u32) -> bool {
/// 构造 PlainMonthDay 实例对象(槽 0-3 = 月/日/参考年/日历 ID)。
/// from/toPlainDate 等返回新对象的成员使用;构造器走 receiver 初始化路径。
#[expect(dead_code)]
fn make_plain_month_day<H: VmHost>(vm: &mut H, month: u32, day: u32, ref_year: i32, calendar: &str) -> NativeResult {
let proto = JsValue::from_js_object(vm.session().builtin_world().plain_month_day_proto.as_ptr() as *mut JsObject);
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto);
@@ -7922,6 +7935,471 @@ pub fn plain_year_month_value_of<H: VmHost>(vm: &mut H, _args: &[u8]) -> NativeR
NativeResult::Err(crate::error::create_type_error(vm, "Temporal.PlainYearMonth has no valueOf"))
}
// ==================== PlainMonthDay.from ====================
/// PlainMonthDay 串注解校验:与 validate_temporal_annotation_suffix 同构,
/// 但 u-ca 值只接受 iso8601(大小写不敏感),其余内置日历 ID 拒绝。
fn validate_month_day_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 !value.eq_ignore_ascii_case("iso8601") {
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());
} else {
time_zone_seen = true;
}
suffix = &suffix[end + 1..];
}
Ok(())
}
/// PlainMonthDay 串的时间部分校验(值丢弃):偏移剥离须恰好耗尽剩余文本;
/// 接受 T/t/空格分隔、小时可缺分钟秒、秒可 60(leap second)、小数秒 ≤9 位;
/// 禁止小数时/分、UTC 设计符在调用侧先行拒绝。
fn validate_month_day_time_suffix(input: &str) -> Result<(), String> {
let time_end = input
.char_indices()
.find_map(|(index, ch)| matches!(ch, '+' | '-').then_some(index))
.unwrap_or(input.len());
if time_end < input.len() && !strip_iso_offset(&input[time_end..])?.is_empty() {
return Err("invalid ISO offset".into());
}
let time = &input[..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,
};
if !valid_plain_time(hour, minute, second, subsecond / 1_000_000, subsecond / 1_000 % 1_000, subsecond % 1_000) {
return Err("invalid ISO time".into());
}
Ok(())
}
/// "MM-DD" 或 "MMDD" 形态的月日解析。
fn parse_month_day_md(input: &str) -> Result<(u32, u32), String> {
let bytes = input.as_bytes();
let (m_s, d_s) = if bytes.len() == 4 && bytes.iter().all(|b| b.is_ascii_digit()) {
(&input[..2], &input[2..])
} else if bytes.len() == 5
&& bytes[2] == b'-'
&& bytes[..2].iter().all(|b| b.is_ascii_digit())
&& bytes[3..].iter().all(|b| b.is_ascii_digit())
{
(&input[..2], &input[3..])
} else {
return Err("invalid ISO month-day".into());
};
let month: u32 = m_s.parse().map_err(|_| "invalid ISO month".to_string())?;
let day: u32 = d_s.parse().map_err(|_| "invalid ISO day".to_string())?;
Ok((month, day))
}
/// PlainMonthDay 串的日期部分:MM-DD / MMDD / --MM-DD / --MMDD / 完整日期(扩展或紧凑)。
/// 年份仅用于负零年拒绝(-000000 → 错),无范围检查;返回 (month, day)。
fn parse_month_day_date_part(input: &str) -> Result<(u32, u32), String> {
if input.is_empty() {
return Err("invalid ISO date".into());
}
if let Some(rest) = input.strip_prefix("--") {
let (month, day) = parse_month_day_md(rest)?;
return finish_month_day_range(month, day);
}
let bytes = input.as_bytes();
let signed = matches!(bytes[0], b'-' | b'+');
let negative = bytes[0] == b'-';
let mut i = usize::from(signed);
let y_start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
let digits = &input[y_start..i];
if i < bytes.len() && bytes[i] == b'-' {
// 扩展形:年份 + -MM-DD;无符号两位引导是 MM-DD 形态。
if digits.len() == 2 && !signed {
let (month, day) = parse_month_day_md(input)?;
return finish_month_day_range(month, day);
}
let expected = usize::from(signed) * 6 + 4 * (1 - usize::from(signed));
if digits.len() != expected {
return Err("invalid ISO year".into());
}
i += 1;
let month = read_iso2(input, &mut i, bytes)?;
if i >= bytes.len() || bytes[i] != b'-' {
return Err("invalid ISO date".into());
}
i += 1;
let day = read_iso2(input, &mut i, bytes)?;
if i != bytes.len() {
return Err("invalid trailing content".into());
}
if negative && digits == "000000" {
return Err("invalid ISO negative zero year".into());
}
return finish_month_day_range(month, day);
}
if i != bytes.len() {
return Err("invalid trailing content".into());
}
// 紧凑纯数字:4 位 = MMDD(无符号);否则 (4-6 位年) + 4 位月日。
let len = digits.len();
if !(len == 4 && !signed || len == 10 && signed || (!signed && (8..=10).contains(&len))) {
return Err("invalid ISO date".into());
}
let tail = &input[y_start..];
let y_len = tail.len() - 4;
let (month, day) = parse_month_day_md(&tail[y_len..])?;
if y_len == 6 && negative && &tail[..6] == "000000" {
return Err("invalid ISO negative zero year".into());
}
finish_month_day_range(month, day)
}
/// 月日取值边界:月 1..=12、日 1..=31(串路径无参考年,不做月长检查)。
fn finish_month_day_range(month: u32, day: u32) -> Result<(u32, u32), String> {
if month == 0 || month > 12 || day == 0 || day > 31 {
return Err("invalid ISO date".into());
}
Ok((month, day))
}
/// 解析 PlainMonthDay ISO 串("MM-DD"/"MMDD"/"--MM-DD"/完整日期/完整 datetime)。
/// 注解与时间部分按规则校验;参考年恒 1972(串中年份丢弃)。
fn parse_month_day_string(input: &str) -> Result<(u32, u32), String> {
let trimmed = input.trim();
if trimmed.contains('\u{2212}') {
return Err("variant minus sign is not valid for PlainMonthDay".into());
}
let text = trimmed.to_owned();
let annotation_start = text.find('[').unwrap_or(text.len());
validate_month_day_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 PlainMonthDay".into());
}
let separator = text.find(['T', 't', ' ']);
let (date_part, time_part) = match separator {
Some(index) => (&text[..index], &text[index + 1..]),
None => (text, ""),
};
if !time_part.is_empty() {
validate_month_day_time_suffix(time_part)?;
}
parse_month_day_date_part(date_part)
}
/// bag 日历字符串是否合法:接受任何 PlainMonthDay 串形态,外加 "YYYY-MM" 年月形。
fn is_valid_month_day_calendar_string(input: &str) -> bool {
let trimmed = input.trim();
if trimmed.contains('\u{2212}') {
return false;
}
let text = trimmed.to_owned();
let annotation_start = text.find('[').unwrap_or(text.len());
if validate_month_day_annotation_suffix(&text[annotation_start..]).is_err() {
return false;
}
let text = &text[..annotation_start];
if text.contains('Z') || text.contains('z') {
return false;
}
let separator = text.find(['T', 't', ' ']);
let (date_part, time_part) = match separator {
Some(index) => (&text[..index], &text[index + 1..]),
None => (text, ""),
};
if !time_part.is_empty() && validate_month_day_time_suffix(time_part).is_err() {
return false;
}
if parse_month_day_date_part(date_part).is_ok() {
return true;
}
// "YYYY-MM" 年月形(bag 日历独有宽形态)。
let bytes = date_part.as_bytes();
let (start, negative) = match bytes.first() {
Some(b'-') => (1usize, true),
Some(b'+') => (1usize, false),
_ => (0usize, false),
};
let mut i = start;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
let digits = &date_part[start..i];
let expected = if start > 0 { 6 } else { 4 };
if digits.len() != expected || i >= bytes.len() || bytes[i] != b'-' || date_part.len() != i + 3 {
return false;
}
let m_s = &date_part[i + 1..];
if !m_s.bytes().all(|b| b.is_ascii_digit()) {
return false;
}
let month: u32 = m_s.parse().unwrap_or(0);
if month == 0 || month > 12 {
return false;
}
!(negative && digits == "000000")
}
/// bag 日历解析:undefined → iso8601;字符串(iso8601 大小写不敏感 / 合法 ISO 串原样
/// 保留为 ID);PD/PDT/ZDT/PMD/PYM 实例 → 直读日历槽;其余对象与原始值 → TypeError。
fn month_day_bag_calendar_id<H: VmHost>(vm: &mut H, value: JsValue) -> Result<String, JsValue> {
if value.is_undefined() {
return Ok("iso8601".to_string());
}
if value.is_string() {
let text = to_string(value);
if text.to_lowercase() == "iso8601" {
return Ok("iso8601".to_string());
}
if is_valid_month_day_calendar_string(&text) {
return Ok(text);
}
return 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 };
let slot = if obj.is_plain_date_obj() {
Some(3)
} else if obj.is_plain_date_time_obj() {
Some(4)
} else if obj.is_zoned_date_time_obj() {
Some(2)
} else if obj.is_plain_month_day_obj() || obj.is_plain_year_month_obj() {
Some(3)
} else {
None
};
if let Some(slot) = slot {
return Ok(get_calendar_id(obj, slot));
}
}
return Err(crate::error::create_type_error(vm, "invalid calendar"));
}
Err(crate::error::create_type_error(vm, "invalid calendar"))
}
/// `Temporal.PlainMonthDay.from(item[, options])`:ISO 串 / PMD 实例 / PD 实例 / 字段对象。
/// 参考年恒 1972(串中年份与 bag year 均不入结果,year 仅供 overflow 月长判断);
/// options.overflow 在全部字段 Get 之后读取。
pub fn plain_month_day_from<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
if value.is_string() {
// 规范顺序:先解析(错误即抛,不触 options),再读 overflow(结果不受其影响)。
let (month, day) = native_try!(parse_month_day_string(&to_string(value))
.map_err(|_| crate::error::create_range_error(vm, "invalid ISO 8601 month-day string")));
native_try!(temporal_overflow(vm, args));
return make_plain_month_day(vm, month, day, 1972, "iso8601");
}
if !value.is_object() {
return NativeResult::Err(crate::error::create_type_error(vm, "from() argument must be a string or an object"));
}
let ptr = value.as_js_object_ptr();
if ptr.is_null() {
return NativeResult::Err(crate::error::create_type_error(vm, "from() argument must be a string or an object"));
}
let obj = unsafe { &*ptr };
// 实例快路径:槽直读,不触发属性。
if obj.is_plain_month_day_obj() {
// 实例复制:槽直读(refYear/日历保留),overflow 读取但结果不受其影响。
native_try!(temporal_overflow(vm, args));
let calendar = get_calendar_id(obj, 3);
return make_plain_month_day(
vm,
get_double_prop(obj, 0) as u32,
get_double_prop(obj, 1) as u32,
get_double_prop(obj, 2) as i32,
&calendar,
);
}
if obj.is_plain_date_obj() {
// PlainDate 实例:年月日槽直读,overflow 按其年应用,参考年 1972。
let constrain = native_try!(temporal_overflow(vm, args));
let year = get_double_prop(obj, 0) as i32;
let month = get_double_prop(obj, 1) as u32;
let mut day = get_double_prop(obj, 2) as u32;
let calendar = get_calendar_id(obj, 3);
if constrain {
day = day.min(days_in_month_iso(year, month));
}
return make_plain_month_day(vm, month, day, 1972, &calendar);
}
// 字段对象:Get 序 calendar → day → month → monthCode → year → era → eraYear,
// 各分量在 Get 处即转换(观测序钉死)。
let calendar_raw = native_try!(temporal_option_value(vm, obj, value, "calendar"));
let calendar = native_try!(month_day_bag_calendar_id(vm, calendar_raw));
let day_raw = native_try!(temporal_option_value(vm, obj, value, "day"));
let day_f = match day_raw.is_undefined() {
true => None,
false => Some(native_try!(temporal_number_component(vm, day_raw))),
};
let month_raw = native_try!(temporal_option_value(vm, obj, value, "month"));
let month_f = match month_raw.is_undefined() {
true => None,
false => Some(native_try!(temporal_number_component(vm, month_raw))),
};
let month_code_raw = native_try!(temporal_option_value(vm, obj, value, "monthCode"));
let month_code = match month_code_raw.is_undefined() {
true => None,
false => Some(native_try!(temporal_option_string(vm, month_code_raw))),
};
let year_raw = native_try!(temporal_option_value(vm, obj, value, "year"));
let year = match year_raw.is_undefined() {
true => None,
false => Some(native_try!(temporal_number_component(vm, year_raw)) as i32),
};
let era_raw = native_try!(temporal_option_value(vm, obj, value, "era"));
let era_year_raw = native_try!(temporal_option_value(vm, obj, value, "eraYear"));
let constrain = native_try!(temporal_overflow(vm, args));
// era 族:双现 → RangeError;恰一个 → 忽略(ISO 无纪元体系)。
if !era_raw.is_undefined() && !era_year_raw.is_undefined() {
return NativeResult::Err(crate::error::create_range_error(vm, "era and eraYear cannot both be present"));
}
// 月来源:双给 → RangeError;双缺 → TypeError(先于日存在性)。
if month_f.is_some() && month_code.is_some() {
return NativeResult::Err(crate::error::create_range_error(vm, "month and monthCode cannot both be present"));
}
if month_f.is_none() && month_code.is_none() {
return NativeResult::Err(crate::error::create_type_error(vm, "month or monthCode is required"));
}
if day_f.is_none() {
return NativeResult::Err(crate::error::create_type_error(vm, "day is required"));
}
// monthCode 两段校验:第一段语法("M"+两位)先于第二段 ISO 适配。
let month_code_num = match &month_code {
Some(text) => {
let b = text.as_bytes();
if b.len() != 3 || b[0] != b'M' || !b[1].is_ascii_digit() || !b[2].is_ascii_digit() {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid monthCode"));
}
Some(u32::from(b[1] - b'0') * 10 + u32::from(b[2] - b'0'))
}
None => None,
};
// 月定值:负值恒 RangeError;>12 时 constrain 钳 12、reject 抛错。
let month = match (month_f, month_code_num) {
(Some(f), Some(code)) => {
if f as u32 != code {
return NativeResult::Err(crate::error::create_range_error(vm, "month and monthCode conflict"));
}
code
}
(Some(f), None) => {
if f < 1.0 {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid month"));
}
if f > 12.0 {
if constrain {
12
} else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid month"));
}
} else {
f as u32
}
}
(None, Some(code)) => {
if !(1..=12).contains(&code) {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid monthCode"));
}
code
}
_ => unreachable!("month or monthCode is required checked above"),
};
let day_f = day_f.unwrap();
if day_f < 1.0 {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid day"));
}
// overflow 应用:月长按 year(bag 缺省 1972)判断;constrain 钳制,reject 抛错。
let overflow_year = year.unwrap_or(1972);
let days = days_in_month_iso(overflow_year, month) as i32;
let day_i = day_f as i32;
let day = if day_i > days {
if constrain {
days
} else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid day"));
}
} else {
day_i
};
make_plain_month_day(vm, month, day as u32, 1972, &calendar)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -974,6 +974,12 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
.map_or(0, |props| props.len() as u32)
.saturating_sub(1);
plain_month_day_ctor.set_data_meta(length_pos, PropAttributes::new(false, false, true));
apply_binding_table(
world,
plain_month_day_ctor,
core,
&[("from", oxide_builtins::temporal::plain_month_day_from::<crate::vm::Vm> as *const (), 1)],
);
let plain_month_day_proto_ptr = world.plain_month_day_proto.as_ptr() as *mut JsObject;
let plain_month_day_proto = unsafe { &mut *plain_month_day_proto_ptr };
@@ -2826,3 +2826,152 @@ fn plain_month_day_and_year_month_to_locale_string_value_of_to_string_tag() {
"05-02|2021-02|TypeError|TypeError|TypeError|[object Temporal.PlainMonthDay]|[object Temporal.PlainYearMonth]"
);
}
#[test]
fn plain_month_day_from_string_matrix() {
let mut vm = Vm::new();
// 串形态矩阵:MM-DD / MMDD / --前缀 / 完整日期(年份丢弃,参考年恒 1972)/
// 完整 datetime(leap second 接受、小数时分拒、小数秒 ≤9 位)/ 注解规则。
let r = eval(
&mut vm,
"(() => {
const run = (s) => { try { const m = Temporal.PlainMonthDay.from(s); return m.monthCode + '-' + m.day; } catch (e) { return e.constructor.name; } };
return [
run('10-01'), run('1001'), run('--10-01'), run('--1001'),
run('1965-10-01'), run('+001976-10-01'), run('-999999-10-01'),
run('1976-10-01T15:23:30.1+00:00'), run('19761001T15:23:30.1+0000'),
run('2000-05-02T00+00:00'), run('1976-05-02 15:23'),
run('2016-12-31T23:59:60'),
run('1976-05-02T15:23[u-ca=iso8601]'),
run('11-18[u-ca=ISO8601][u-ca=discord]'),
Temporal.PlainMonthDay.from('2019-10-31').toString({calendarName: 'always'}),
run('11-18junk'), run('11-18[u-ca=gregory]'), run('11-18[U-CA=iso8601]'),
run('11-18[FOO=bar]'), run('11-18[!foo=bar]'),
run('-000000-08-24'), run('-000000-08-24T15:43:27'),
run('2019-10-01T09:00:00Z'), run('11-18Z'),
run('13-34'), run('2025-04-03T12.5'),
run('1970-01-01T00:00:00.1234567891'), run('2022-09-15+00:00'),
].join('|');
})()",
)
.unwrap();
assert_eq!(
str_val(&vm, r),
"M10-1|M10-1|M10-1|M10-1|M10-1|M10-1|M10-1|M10-1|M10-1|M05-2|M05-2|M12-31|M05-2|M11-18|1972-10-31[u-ca=iso8601]|RangeError|RangeError|RangeError|RangeError|RangeError|RangeError|RangeError|RangeError|RangeError|RangeError|RangeError|RangeError|RangeError"
);
}
#[test]
fn plain_month_day_from_bag_matrix() {
let mut vm = Vm::new();
// bag 矩阵:字段序/必填/冲突、monthCode 两段校验、era 双现、year 仅 overflow、
// constrain/reject、自定义日历 ID(ISO 串原样保留)、错误类型。
let r = eval(
&mut vm,
"(() => {
const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } };
const run = (f, o) => { const m = Temporal.PlainMonthDay.from(f, o); return m.monthCode + '-' + m.day; };
const always = (f, o) => Temporal.PlainMonthDay.from(f, o).toString({calendarName: 'always'});
return [
run({month: 10, day: 1}), run({monthCode: 'M12', day: 31}),
always({year: 2021, month: 7, day: 3}),
run({month: 10, day: 1, days: 31}),
kind(() => Temporal.PlainMonthDay.from({})),
kind(() => Temporal.PlainMonthDay.from({day: 15})),
kind(() => Temporal.PlainMonthDay.from({month: 12})),
kind(() => Temporal.PlainMonthDay.from({monthCode: 'M12'})),
kind(() => Temporal.PlainMonthDay.from({monthCode: undefined, day: 15})),
kind(() => Temporal.PlainMonthDay.from({months: 12, day: 31})),
kind(() => Temporal.PlainMonthDay.from({month: 12, monthCode: 'M11', day: 17})),
kind(() => Temporal.PlainMonthDay.from({monthCode: 'M99L'})),
kind(() => Temporal.PlainMonthDay.from({day: 32})),
kind(() => Temporal.PlainMonthDay.from({monthCode: 'm01', day: 17})),
kind(() => Temporal.PlainMonthDay.from({monthCode: 'M00', day: 17})),
kind(() => Temporal.PlainMonthDay.from({monthCode: 'M13', day: 17})),
kind(() => Temporal.PlainMonthDay.from({monthCode: 'M05L', day: 17})),
kind(() => Temporal.PlainMonthDay.from({monthCode: 'M99L', day: 1})),
run({year: 2000, month: 5, day: 2, era: 'ce'}),
run({year: 2000, month: 5, day: 2, eraYear: 1}),
kind(() => Temporal.PlainMonthDay.from({year: 2000, month: 5, day: 2, era: 'ce', eraYear: 1})),
run({year: -999999, month: 1, day: 1}),
run({year: -999999, monthCode: 'M02', day: 29}),
run({year: -1000000, monthCode: 'M02', day: 29}),
kind(() => Temporal.PlainMonthDay.from({year: -999999, monthCode: 'M02', day: 29}, {overflow: 'reject'})),
run({monthCode: 'M02', day: 30}, {overflow: 'constrain'}),
run({month: 2, day: 31}),
run({year: 2000, month: 13, day: 34}, {overflow: 'constrain'}),
kind(() => Temporal.PlainMonthDay.from({year: 2000, month: 13, day: 34}, {overflow: 'reject'})),
kind(() => Temporal.PlainMonthDay.from({day: 1, month: -1})),
kind(() => Temporal.PlainMonthDay.from({month: 1, day: -1})),
kind(() => Temporal.PlainMonthDay.from({year: Infinity, month: 5, day: 2})),
kind(() => Temporal.PlainMonthDay.from({year: 2000, month: 5, day: 1n})),
kind(() => Temporal.PlainMonthDay.from(1)),
kind(() => Temporal.PlainMonthDay.from(1n)),
kind(() => Temporal.PlainMonthDay.from()),
Temporal.PlainMonthDay.from({monthCode: 'M11', day: 18, calendar: 'iso8601'}).calendarId,
Temporal.PlainMonthDay.from({monthCode: 'M11', day: 18, calendar: 'IsO8601'}).calendarId,
Temporal.PlainMonthDay.from({monthCode: 'M11', day: 18, calendar: '2020-01-01'}).calendarId,
Temporal.PlainMonthDay.from({monthCode: 'M11', day: 18, calendar: '2020-01'}).monthCode,
kind(() => Temporal.PlainMonthDay.from({monthCode: 'M11', day: 18, calendar: 'notacal'})),
kind(() => Temporal.PlainMonthDay.from({monthCode: 'M11', day: 18, calendar: ''})),
kind(() => Temporal.PlainMonthDay.from({monthCode: 'M11', day: 18, calendar: {}})),
kind(() => Temporal.PlainMonthDay.from({monthCode: 'M11', day: 18, calendar: new Temporal.Duration()})),
].join('|');
})()",
)
.unwrap();
assert_eq!(
str_val(&vm, r),
"M10-1|M12-31|1972-07-03[u-ca=iso8601]|M10-1|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|RangeError|TypeError|TypeError|RangeError|RangeError|RangeError|RangeError|RangeError|M05-2|M05-2|RangeError|M01-1|M02-28|M02-29|RangeError|M02-29|M02-29|M12-31|RangeError|RangeError|RangeError|RangeError|TypeError|TypeError|TypeError|TypeError|iso8601|iso8601|2020-01-01|M11|RangeError|RangeError|TypeError|TypeError"
);
}
#[test]
fn plain_month_day_from_instances_and_options() {
let mut vm = Vm::new();
// 实例复制(refYear/日历保留、非同一对象)/ PD 实例 / options 形态与读取序
// (options.overflow 在所有字段 Get 之后;number 参数与坏串在读 options 前抛)。
let r = eval(
&mut vm,
"(() => {
const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } };
const orig = new Temporal.PlainMonthDay(5, 2, undefined, 2000);
const copy = Temporal.PlainMonthDay.from(orig);
let pdSeen = [];
let pd = Temporal.PlainDate.from('2019-10-01');
let pdCopy = Temporal.PlainMonthDay.from(pd);
let optsSeen = [];
const options = { get overflow() { optsSeen.push(1); return 'constrain'; } };
const a = Temporal.PlainMonthDay.from({month: 2, day: 3}, options);
let numSeen = [];
const numOptions = { get overflow() { numSeen.push(1); return 'constrain'; } };
kind(() => Temporal.PlainMonthDay.from(7, numOptions));
let strSeen = [];
const strOptions = { get overflow() { strSeen.push(1); return 'constrain'; } };
kind(() => Temporal.PlainMonthDay.from('13-34', strOptions));
return [
copy.monthCode + '-' + copy.day,
copy.toString({calendarName: 'always'}),
copy.calendarId === orig.calendarId,
copy !== orig,
Temporal.PlainMonthDay.from(orig, {overflow: 'reject'}).day,
pdCopy.monthCode + '-' + pdCopy.day + ':' + pdCopy.toString({calendarName: 'always'}),
a.monthCode + '-' + a.day + ':' + optsSeen.length,
numSeen.length + ':' + strSeen.length,
kind(() => Temporal.PlainMonthDay.from({month: 2, day: 3}, null)),
kind(() => Temporal.PlainMonthDay.from({month: 2, day: 3}, true)),
kind(() => Temporal.PlainMonthDay.from({month: 2, day: 3}, 1n)),
kind(() => Temporal.PlainMonthDay.from({month: 2, day: 3}, Symbol())),
kind(() => Temporal.PlainMonthDay.from({month: 2, day: 3}, {overflow: 'CONSTRAIN'})),
kind(() => Temporal.PlainMonthDay.from({month: 2, day: 3}, {overflow: 'reject\\0'})),
Temporal.PlainMonthDay.from({month: 2, day: 3}, () => {}).monthCode + '-' +
Temporal.PlainMonthDay.from('05-02', {overflow: 'reject'}).day,
].join('|');
})()",
)
.unwrap();
assert_eq!(
str_val(&vm, r),
"M05-2|2000-05-02[u-ca=iso8601]|true|true|2|M10-1:1972-10-01[u-ca=iso8601]|M02-3:1|0:0|TypeError|TypeError|TypeError|TypeError|RangeError|RangeError|M02-2"
);
}