diff --git a/crates/oxide_builtins/src/temporal.rs b/crates/oxide_builtins/src/temporal.rs index 5152bd74..71d1d261 100644 --- a/crates/oxide_builtins/src/temporal.rs +++ b/crates/oxide_builtins/src/temporal.rs @@ -7219,10 +7219,24 @@ pub fn plain_date_day_of_year(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(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(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::>() + } 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::, _>>() + .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 { + 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::().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(vm: &mut H, value: JsValue) -> Result { + 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(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::*; diff --git a/crates/oxide_vm/src/bindings/bind_temporal.rs b/crates/oxide_vm/src/bindings/bind_temporal.rs index 4eecd1b3..bb9c9f55 100644 --- a/crates/oxide_vm/src/bindings/bind_temporal.rs +++ b/crates/oxide_vm/src/bindings/bind_temporal.rs @@ -974,6 +974,12 @@ pub fn bind_temporal(core: &Arc, 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:: 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 }; diff --git a/crates/oxide_vm/tests/builtins_temporal_tests.rs b/crates/oxide_vm/tests/builtins_temporal_tests.rs index 450346fc..3964e443 100644 --- a/crates/oxide_vm/tests/builtins_temporal_tests.rs +++ b/crates/oxide_vm/tests/builtins_temporal_tests.rs @@ -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" + ); +}