diff --git a/crates/oxide_builtins/src/temporal.rs b/crates/oxide_builtins/src/temporal.rs index f3ef36b9..30d46b3d 100644 --- a/crates/oxide_builtins/src/temporal.rs +++ b/crates/oxide_builtins/src/temporal.rs @@ -8602,6 +8602,176 @@ pub fn plain_year_month_from(vm: &mut H, args: &[u8]) -> NativeResult make_plain_year_month(vm, year, month, 1, &calendar) } +/// ToTemporalYearMonth 四分支共享件(compare 等无 options 参数的成员复用): +/// 串(解析 + (年, 月) 范围,参考日恒 1)/ PYM 实例(槽直读,参考日保留)/ +/// PD 实例(年月槽直读,参考日恒 1)/ 字段 bag(year 必填、day 从不读取、参考日恒 1、 +/// monthCode 两段校验同 from);constrain 决定 month >12 钳制或抛错。 +fn year_month_like_parts( + vm: &mut H, value: JsValue, constrain: bool, +) -> Result<(i32, u32, u32, String), JsValue> { + if value.is_string() { + let (year, month) = parse_year_month_string(&to_string(value)) + .map_err(|_| crate::error::create_range_error(vm, "invalid ISO 8601 year-month string"))?; + if !iso_year_month_within_limits(year, month) { + return Err(crate::error::create_range_error(vm, "ISO year-month is out of range")); + } + return Ok((year, month, 1, "iso8601".to_string())); + } + if !value.is_object() { + return Err(crate::error::create_type_error(vm, "argument must be a string or an object")); + } + let ptr = value.as_js_object_ptr(); + if ptr.is_null() { + return Err(crate::error::create_type_error(vm, "argument must be a string or an object")); + } + let obj = unsafe { &*ptr }; + // 实例快路径:槽直读,不触发属性。 + if obj.is_plain_year_month_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_calendar_id(obj, 3), + )); + } + if obj.is_plain_date_obj() { + return Ok((get_double_prop(obj, 0) as i32, get_double_prop(obj, 1) as u32, 1, get_calendar_id(obj, 3))); + } + // 字段对象:Get 序 calendar → year → month → monthCode → era → eraYear(day 不读)。 + let calendar_raw = temporal_option_value(vm, obj, value, "calendar")?; + let calendar = month_day_bag_calendar_id(vm, calendar_raw)?; + let year_raw = temporal_option_value(vm, obj, value, "year")?; + let month_raw = temporal_option_value(vm, obj, value, "month")?; + let month_code_raw = temporal_option_value(vm, obj, value, "monthCode")?; + let era_raw = temporal_option_value(vm, obj, value, "era")?; + let era_year_raw = temporal_option_value(vm, obj, value, "eraYear")?; + // era 族:双现 → RangeError;恰一个 → 忽略(ISO 无纪元体系)。 + if !era_raw.is_undefined() && !era_year_raw.is_undefined() { + return Err(crate::error::create_range_error(vm, "era and eraYear cannot both be present")); + } + // 存在性:year 必填(先于 monthCode 语法);month|monthCode 至少其一。 + if year_raw.is_undefined() { + return Err(crate::error::create_type_error(vm, "year is required")); + } + if month_raw.is_undefined() && month_code_raw.is_undefined() { + return Err(crate::error::create_type_error(vm, "month or monthCode is required")); + } + // monthCode ToPrimitive 语义:字符串直用;对象(含函数)经 valueOf/toString; + // 转换结果非字符串(number/bigint/boolean/null/Symbol 等)→ TypeError。 + let month_code = if month_code_raw.is_undefined() { + None + } else if month_code_raw.is_string() { + Some(to_string(month_code_raw)) + } else if month_code_raw.is_object() { + let primitive = + match oxide_runtime_api::to_primitive(month_code_raw, oxide_runtime_api::ToPrimitiveHint::String, vm) { + Ok(primitive) => primitive, + Err(error) => return Err(native_engine_error(vm, &error)), + }; + if !primitive.is_string() { + return Err(crate::error::create_type_error(vm, "invalid monthCode")); + } + Some(to_string(primitive)) + } else { + return Err(crate::error::create_type_error(vm, "invalid monthCode")); + }; + // monthCode 第一段语法("M"+两位数字,可选再 +"L")先于一切数值转换。 + let (month_code_num, leap_month) = match &month_code { + Some(text) => { + let b = text.as_bytes(); + let well_formed = b[0] == b'M' + && b.len() >= 3 + && b[1].is_ascii_digit() + && b[2].is_ascii_digit() + && (b.len() == 3 || (b.len() == 4 && b[3] == b'L')); + if !well_formed { + return Err(crate::error::create_range_error(vm, "invalid monthCode")); + } + (Some(u32::from(b[1] - b'0') * 10 + u32::from(b[2] - b'0')), b.len() == 4) + } + None => (None, false), + }; + // year 转换(TypeError/RangeError)在 monthCode 语法之后。 + let year = temporal_number_component(vm, year_raw)? as i32; + let month_f = match month_raw.is_undefined() { + true => None, + false => Some(temporal_number_component(vm, month_raw)?), + }; + // monthCode 第二段适配:闰月后缀或月值越界 → RangeError。 + if let Some(code) = month_code_num { + if leap_month || !(1..=12).contains(&code) { + return Err(crate::error::create_range_error(vm, "invalid monthCode")); + } + } + // 月定值:monthCode 与数值 month 并存须一致(冲突 → RangeError); + // 数值 month <1 恒 RangeError,>12 时 constrain 钳 12、reject 抛错。 + let month = if let Some(code) = month_code_num { + if let Some(f) = month_f { + if f as u32 != code { + return Err(crate::error::create_range_error(vm, "month and monthCode conflict")); + } + } + code + } else { + let f = month_f.expect("month or monthCode is required checked above"); + if f < 1.0 { + return Err(crate::error::create_range_error(vm, "invalid month")); + } + if f > 12.0 { + if constrain { + 12 + } else { + return Err(crate::error::create_range_error(vm, "invalid month")); + } + } else { + f as u32 + } + }; + // (年, 月) 表示范围。 + if !iso_year_month_within_limits(year, month) { + return Err(crate::error::create_range_error(vm, "ISO year-month is out of range")); + } + Ok((year, month, 1, calendar)) +} + +/// `Temporal.PlainYearMonth.compare(one, two)`:静态比较,返回 -1/0/1。 +/// 比较 (年, 月, 参考日) 三元字典序;两参均经 ToTemporalYearMonth。 +pub fn plain_year_month_compare(vm: &mut H, args: &[u8]) -> NativeResult { + let a_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let b_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; + let a = native_try!(year_month_like_parts(vm, a_val, true)); + let b = native_try!(year_month_like_parts(vm, b_val, true)); + let cmp = (a.0, a.1, a.2).cmp(&(b.0, b.1, b.2)); + NativeResult::Ok(JsValue::float(match cmp { + std::cmp::Ordering::Less => -1.0, + std::cmp::Ordering::Equal => 0.0, + std::cmp::Ordering::Greater => 1.0, + })) +} + +/// `Temporal.PlainYearMonth.prototype.equals(other)`:比较 (年, 月, 参考日) 三元与日历标识; +/// other 经 ToTemporalYearMonth(串 / 字段对象 / PD 实例 / PYM 实例)转换。 +pub fn plain_year_month_equals(vm: &mut H, args: &[u8]) -> NativeResult { + let ptr = native_try!(receiver_obj(vm, args)); + let obj = unsafe { &*ptr }; + native_try!(ensure_plain_year_month(vm, obj)); + let other = match plain_year_month_from(vm, args) { + NativeResult::Ok(val) => val, + NativeResult::Err(err) => return NativeResult::Err(err), + NativeResult::TailCall { .. } => unreachable!("工厂路径不产生尾调用"), + }; + let other_ptr = other.as_js_object_ptr(); + if other_ptr.is_null() { + return NativeResult::Err(crate::error::create_type_error(vm, "incompatible receiver")); + } + let o = unsafe { &*other_ptr }; + let equal = get_double_prop(obj, 0) == get_double_prop(o, 0) + && get_double_prop(obj, 1) == get_double_prop(o, 1) + && get_double_prop(obj, 2) == get_double_prop(o, 2) + && get_calendar_id(obj, 3) == get_calendar_id(o, 3); + NativeResult::Ok(JsValue::bool(equal)) +} + /// `Temporal.PlainMonthDay.prototype.equals(other)`:比较 (月, 日, 参考年) 三元与日历标识; /// other 经 ToTemporalMonthDay(串 / 字段对象 / 实例)转换。 pub fn plain_month_day_equals(vm: &mut H, args: &[u8]) -> NativeResult { diff --git a/crates/oxide_vm/src/bindings/bind_temporal.rs b/crates/oxide_vm/src/bindings/bind_temporal.rs index a4344e4e..a3d60e25 100644 --- a/crates/oxide_vm/src/bindings/bind_temporal.rs +++ b/crates/oxide_vm/src/bindings/bind_temporal.rs @@ -1066,7 +1066,14 @@ pub fn bind_temporal(core: &Arc, session: &KernelSession, global: &m world, plain_year_month_ctor, core, - &[("from", oxide_builtins::temporal::plain_year_month_from:: as *const (), 1)], + &[ + ("from", oxide_builtins::temporal::plain_year_month_from:: as *const (), 1), + ( + "compare", + oxide_builtins::temporal::plain_year_month_compare:: as *const (), + 2, + ), + ], ); let plain_year_month_proto_ptr = world.plain_year_month_proto.as_ptr() as *mut JsObject; @@ -1131,6 +1138,11 @@ pub fn bind_temporal(core: &Arc, session: &KernelSession, global: &m oxide_builtins::temporal::plain_year_month_value_of:: as *const (), 0, ), + ( + "equals", + oxide_builtins::temporal::plain_year_month_equals:: as *const (), + 1, + ), ], ); bind_well_known_data_property( diff --git a/crates/oxide_vm/tests/builtins_temporal_tests.rs b/crates/oxide_vm/tests/builtins_temporal_tests.rs index 83d6fc5d..7e04464c 100644 --- a/crates/oxide_vm/tests/builtins_temporal_tests.rs +++ b/crates/oxide_vm/tests/builtins_temporal_tests.rs @@ -3247,3 +3247,80 @@ fn plain_year_month_from_instances_and_options() { "2000-5:2000-05-07[u-ca=iso8601]|true|2000|1976-11:1976-11-01[u-ca=iso8601]|2021-1:1|0:0:1|TypeError|TypeError|TypeError|TypeError|RangeError|RangeError|12|12|12" ); } + +#[test] +fn plain_year_month_compare_matrix() { + let mut vm = Vm::new(); + // compare 比较 (年, 月, 参考日) 三元字典序(参考日参与);两参均经 + // ToTemporalYearMonth(串/bag/PD 实例/PYM 实例);缺字段 TypeError; + // 子类实例走槽直读不触发 getter。 + let r = eval( + &mut vm, + "(() => { + const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } }; + const c = Temporal.PlainYearMonth.compare; + const ym = (y, m, rd) => new Temporal.PlainYearMonth(y, m, 'iso8601', rd); + class AvoidGetters extends Temporal.PlainYearMonth { + get year() { throw new Error('no'); } + get month() { throw new Error('no'); } + } + return [ + c(ym(2000, 5, 1), ym(2000, 5, 1)), + c(ym(2000, 5, 1), ym(2000, 5, 2)), + c(ym(2000, 5, 2), ym(2000, 5, 1)), + c(ym(1999, 12, 1), ym(2000, 1, 1)), + c(ym(2000, 5, 1), ym(2000, 6, 1)), + c('1994-11', ym(1994, 11, 1)), + c({year: 1994, month: 11}, ym(1994, 11, 1)), + c(Temporal.PlainDate.from('1994-11-20'), ym(1994, 11, 1)), + c(ym(2013, 6, 1), {year: 2013, month: 6}), + kind(() => c({year: 1994}, ym(1994, 11, 1))), + kind(() => c(1, ym(1994, 11, 1))), + kind(() => c(ym(1994, 11, 1))), + c(new AvoidGetters(2000, 5), new AvoidGetters(2006, 3)), + ].join('|'); + })()", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "0|-1|1|-1|-1|0|0|0|0|TypeError|TypeError|TypeError|-1"); +} + +#[test] +fn plain_year_month_equals_matrix() { + let mut vm = Vm::new(); + // equals 比较 (年, 月, 参考日) 三元与日历;other 经 ToTemporalYearMonth + // (串/bag/PD/PYM 实例,day 不读);receiver branding TypeError。 + let r = eval( + &mut vm, + "(() => { + const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } }; + const ym = (y, m, rd) => new Temporal.PlainYearMonth(y, m, 'iso8601', rd); + const a = ym(2000, 5, 1); + return [ + a.equals(ym(2000, 5, 1)), + a.equals(ym(2000, 5, 2)), + a.equals(ym(2000, 6, 1)), + a.equals(ym(1999, 5, 1)), + a.equals('2000-05'), + a.equals('2000-05-15'), + a.equals({year: 2000, month: 5}), + a.equals({year: 2000, monthCode: 'M05'}), + a.equals({year: 2000, month: 5, day: 20}), + a.equals(Temporal.PlainDate.from('2000-05-15')), + kind(() => a.equals()), + kind(() => a.equals(1)), + kind(() => a.equals(1n)), + kind(() => a.equals('junk')), + kind(() => a.equals({year: 2000})), + kind(() => a.equals({year: 2000, month: Infinity})), + kind(() => Temporal.PlainYearMonth.prototype.equals.call({}, a)), + a.equals(a), + ].join('|'); + })()", + ) + .unwrap(); + assert_eq!( + str_val(&vm, r), + "true|false|false|false|true|true|true|true|true|true|TypeError|TypeError|TypeError|RangeError|TypeError|RangeError|TypeError|true" + ); +}