mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
feat: Temporal.PlainYearMonth compare/equals——(年,月,参考日) 三元比较与 ToTemporalYearMonth 共享件
This commit is contained in:
@@ -8602,6 +8602,176 @@ pub fn plain_year_month_from<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
|
|||||||
make_plain_year_month(vm, year, month, 1, &calendar)
|
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<H: VmHost>(
|
||||||
|
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<H: VmHost>(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<H: VmHost>(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)`:比较 (月, 日, 参考年) 三元与日历标识;
|
/// `Temporal.PlainMonthDay.prototype.equals(other)`:比较 (月, 日, 参考年) 三元与日历标识;
|
||||||
/// other 经 ToTemporalMonthDay(串 / 字段对象 / 实例)转换。
|
/// other 经 ToTemporalMonthDay(串 / 字段对象 / 实例)转换。
|
||||||
pub fn plain_month_day_equals<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
pub fn plain_month_day_equals<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||||
|
|||||||
@@ -1066,7 +1066,14 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
|
|||||||
world,
|
world,
|
||||||
plain_year_month_ctor,
|
plain_year_month_ctor,
|
||||||
core,
|
core,
|
||||||
&[("from", oxide_builtins::temporal::plain_year_month_from::<crate::vm::Vm> as *const (), 1)],
|
&[
|
||||||
|
("from", oxide_builtins::temporal::plain_year_month_from::<crate::vm::Vm> as *const (), 1),
|
||||||
|
(
|
||||||
|
"compare",
|
||||||
|
oxide_builtins::temporal::plain_year_month_compare::<crate::vm::Vm> as *const (),
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
let plain_year_month_proto_ptr = world.plain_year_month_proto.as_ptr() as *mut JsObject;
|
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<KernelCore>, session: &KernelSession, global: &m
|
|||||||
oxide_builtins::temporal::plain_year_month_value_of::<crate::vm::Vm> as *const (),
|
oxide_builtins::temporal::plain_year_month_value_of::<crate::vm::Vm> as *const (),
|
||||||
0,
|
0,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"equals",
|
||||||
|
oxide_builtins::temporal::plain_year_month_equals::<crate::vm::Vm> as *const (),
|
||||||
|
1,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
bind_well_known_data_property(
|
bind_well_known_data_property(
|
||||||
|
|||||||
@@ -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"
|
"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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user