feat: Temporal.PlainYearMonth with/toPlainDate——字段 bag 覆盖(month→monthCode→year)、monthCode 两段校验与 day 级日期合成

This commit is contained in:
2026-09-12 07:24:28 +00:00
parent db0521f950
commit 6c9dbdaa06
3 changed files with 231 additions and 0 deletions
+142
View File
@@ -8837,6 +8837,148 @@ pub fn plain_year_month_subtract<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeRe
plain_year_month_apply_duration(vm, args, -1)
}
/// `Temporal.PlainYearMonth.prototype.with(yearMonthLike[, options])`:覆盖 year/month/monthCode;
/// undefined 不覆盖;calendar/timeZone 键拒绝;无识别字段 → TypeError。
///
/// # 步骤
/// 1. receiver branding(TypeError)。
/// 2. reject calendar/timeZone 键与 Temporal 实例(TypeError)。
/// 3. Get 序 month → monthCode → year(字典序);全 undefined → TypeError。
/// 4. monthCode 第一段语法校验与字段数值转换先于 options 读取。
/// 5. 读 overflow(缺省 constrain);monthCode 第二段适配(闰月后缀/1..12)与
/// month/monthCode 冲突、>12 钳制在此。
/// 6. 缺省分量取 receiver 槽(年 0 / 月 1);(年, 月) 过 ISOYearMonthWithinLimits。
///
/// # 边界与副作用
/// - 结果参考日恒 1,日历标识取自 receiver 槽 3。
/// - 年月级范围检查(非按日 1),-271821-04 可经 with 产出。
pub fn plain_year_month_with<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 like = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
native_try!(reject_partial_object_with_calendar_or_time_zone(vm, like));
let lptr = like.as_js_object_ptr();
if lptr.is_null() {
return NativeResult::Err(crate::error::create_type_error(vm, "invalid argument"));
}
let like_obj = unsafe { &*lptr };
// Get 序(字典序):month → monthCode → year;无识别字段 → TypeError。
let month_raw = native_try!(temporal_option_value(vm, like_obj, like, "month"));
let month_code_raw = native_try!(temporal_option_value(vm, like_obj, like, "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, like_obj, like, "year"));
if [month_raw, month_code_raw, year_raw].iter().all(|raw| raw.is_undefined()) {
return NativeResult::Err(crate::error::create_type_error(vm, "no properties present"));
}
// 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 NativeResult::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),
};
// 部分字段先于 options 完整转换(RangeError/TypeError 先于 options 形态错误)。
let year = match year_raw.is_undefined() {
true => None,
false => Some(native_try!(temporal_number_component(vm, year_raw)) as i32),
};
let month_f = match month_raw.is_undefined() {
true => None,
false => Some(native_try!(temporal_number_component(vm, month_raw))),
};
if let Some(f) = month_f {
if f < 1.0 {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid month"));
}
}
let constrain = native_try!(temporal_overflow(vm, args));
if let Some(code) = month_code_num {
if leap_month || !(1..=12).contains(&code) {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid monthCode"));
}
}
// 月定值:monthCode 与数值 month 并存须一致(冲突 → RangeError);
// 数值 month >12 时 constrain 钳 12、reject 抛错;缺省取 receiver 月(槽 1)。
let month = if let Some(code) = month_code_num {
if let Some(f) = month_f {
if f as u32 != code {
return NativeResult::Err(crate::error::create_range_error(vm, "month and monthCode conflict"));
}
}
code
} else {
match month_f {
Some(f) => {
if f > 12.0 {
if constrain {
12
} else {
return NativeResult::Err(crate::error::create_range_error(vm, "invalid month"));
}
} else {
f as u32
}
}
None => get_double_prop(obj, 1) as u32,
}
};
let year = year.unwrap_or_else(|| get_double_prop(obj, 0) as i32);
// (年, 月) 可表示范围(年月级,非按日 1)。
if !iso_year_month_within_limits(year, month) {
return NativeResult::Err(crate::error::create_range_error(vm, "ISO year-month is out of range"));
}
let calendar = get_calendar_id(obj, 3);
make_plain_year_month(vm, year, month, 1, &calendar)
}
/// `Temporal.PlainYearMonth.prototype.toPlainDate(dayLike)`:dayLike 须为对象,
/// 只 Get `day`(year/month/monthCode/calendar 从不读取);options.overflow 绝不读取;
/// 合并 (年, 月, 日) 后 constrain 钳 day 至 [1, 月长],day 级可表示范围检查后
/// 产出 PlainDate(receiver 日历)。
pub fn plain_year_month_to_plain_date<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 item = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
if !item.is_object() {
return NativeResult::Err(crate::error::create_type_error(vm, "invalid argument"));
}
let iptr = item.as_js_object_ptr();
if iptr.is_null() {
return NativeResult::Err(crate::error::create_type_error(vm, "invalid argument"));
}
let item_obj = unsafe { &*iptr };
let day_raw = native_try!(temporal_option_value(vm, item_obj, item, "day"));
if day_raw.is_undefined() {
return NativeResult::Err(crate::error::create_type_error(vm, "day is required"));
}
let day = native_try!(temporal_number_component(vm, day_raw));
let year = get_double_prop(obj, 0) as i32;
let month = get_double_prop(obj, 1) as u32;
// constrain:day 钳到 [1, 月长]。
let day = (day as i32).clamp(1, days_in_month_iso(year, month) as i32) as u32;
// day 级表示范围:-100_000_001..=100_000_000(年月级合法而 day 级越界在此抛)。
let day_count = days_from_civil(i128::from(year), i128::from(month), i128::from(day));
if !(-100_000_001..=100_000_000).contains(&day_count) {
return NativeResult::Err(crate::error::create_range_error(vm, "date is out of range"));
}
let calendar = get_calendar_id(obj, 3);
make_plain_date(vm, year, month, day, &calendar)
}
/// `Temporal.PlainMonthDay.prototype.equals(other)`:比较 (月, 日, 参考年) 三元与日历标识;
/// other 经 ToTemporalMonthDay(串 / 字段对象 / 实例)转换。
pub fn plain_month_day_equals<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
@@ -1149,6 +1149,12 @@ pub fn bind_temporal(core: &Arc<KernelCore>, session: &KernelSession, global: &m
oxide_builtins::temporal::plain_year_month_subtract::<crate::vm::Vm> as *const (),
1,
),
("with", oxide_builtins::temporal::plain_year_month_with::<crate::vm::Vm> as *const (), 1),
(
"toPlainDate",
oxide_builtins::temporal::plain_year_month_to_plain_date::<crate::vm::Vm> as *const (),
1,
),
],
);
bind_well_known_data_property(
@@ -3406,3 +3406,86 @@ fn plain_year_month_subtract_matrix() {
"1994-11|2000-02|2018-09|+275760-08|+275759-09|-271821-05|RangeError|RangeError|RangeError|TypeError|RangeError|RangeError|TypeError|RangeError"
);
}
#[test]
fn plain_year_month_with_matrix() {
let mut vm = Vm::new();
// with:calendar/timeZone 键与实例拒绝;Get 序 month→monthCode→year;
// 字段转换先于 options;monthCode 两段校验;constrain 钳 month>12;
// 结果参考日恒 1;年月级范围检查。
let r = eval(
&mut vm,
"(() => {
const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } };
const ym = (s) => Temporal.PlainYearMonth.from(s);
return [
ym('2019-10').with({ year: 2020 }).toString(),
ym('2019-10').with({ month: 9 }).toString(),
ym('2019-10').with({ monthCode: 'M09' }).toString(),
kind(() => ym('2019-10').with({ month: 9, monthCode: 'M10' })),
ym('2019-10').with({ month: 1, years: 2020 }).toString(),
ym('2019-10').with({ month: 11, year: undefined }).toString(),
ym('2019-10').with({ month: 1.7 }).toString(),
ym('2000-05').with({ month: 13 }).month,
ym('2000-05').with({ month: 15 }, { overflow: undefined }).month,
kind(() => ym('2000-05').with({ month: 15 }, { overflow: 'reject' })),
kind(() => ym('2000-05').with({ month: 8 }, { overflow: 'CONSTRAIN' })),
kind(() => ym('2000-05').with({ month: 8 }, { overflow: 'balance' })),
kind(() => ym('2019-10').with({ month: -1 }, null)),
kind(() => ym('2019-10').with({ year: 2020 }, null)),
kind(() => ym('2019-10').with({})),
kind(() => ym('2019-10').with({ months: 12 })),
kind(() => ym('2019-10').with({ year: 2021, calendar: 'iso8601' })),
kind(() => ym('2019-10').with({ year: 2021, timeZone: 'UTC' })),
kind(() => ym('2019-10').with(42)),
kind(() => ym('2019-10').with(new Temporal.PlainDate(2019, 5, 17))),
kind(() => ym('2019-10').with({ month: Infinity })),
kind(() => ym('2019-10').with({ monthCode: 'M08L' })),
kind(() => ym('2019-10').with({ monthCode: 'M13' })),
new Temporal.PlainYearMonth(2019, 10, 'iso8601', 5).with({ month: 3 }).toString({ calendarName: 'always' }),
ym('2019-10').with({ year: 2020 }, () => {}).toString(),
ym('-271821-05').with({ month: 4 }).toString(),
].join('|');
})()",
)
.unwrap();
assert_eq!(
str_val(&vm, r),
"2020-10|2019-09|2019-09|RangeError|2019-01|2019-11|2019-01|12|12|RangeError|RangeError|RangeError|RangeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|RangeError|RangeError|RangeError|2019-03-01[u-ca=iso8601]|2020-10|-271821-04"
);
}
#[test]
fn plain_year_month_to_plain_date_matrix() {
let mut vm = Vm::new();
// toPlainDate:item 须对象且只读 day;options.overflow 绝不读取;
// constrain 钳 day 至 [1, 月长];day 级范围检查(年月级合法仍可抛)。
let r = eval(
&mut vm,
"(() => {
const kind = (fn) => { try { fn(); return 'no'; } catch (e) { return e.constructor.name; } };
const ym = (s) => Temporal.PlainYearMonth.from(s);
return [
ym('2002-01').toPlainDate({ day: 22 }).toString(),
kind(() => ym('2002-01').toPlainDate({ something: 'nothing' })),
kind(() => ym('2002-01').toPlainDate(7)),
kind(() => ym('2002-01').toPlainDate(null)),
kind(() => ym('2002-01').toPlainDate({})),
ym('2023-02').toPlainDate({ day: 29 }).toString(),
ym('1998-06').toPlainDate({ day: 31 }).toString(),
ym('2000-05').toPlainDate({ day: 1.9 }).toString(),
ym('2000-05').toPlainDate({ day: 0 }).toString(),
ym('-271821-04').toPlainDate({ day: 19 }).toString(),
kind(() => ym('-271821-04').toPlainDate({ day: 18 })),
ym('+275760-09').toPlainDate({ day: 13 }).toString(),
kind(() => ym('+275760-09').toPlainDate({ day: 14 })),
kind(() => ym('2000-05').toPlainDate({ day: Infinity })),
].join('|');
})()",
)
.unwrap();
assert_eq!(
str_val(&vm, r),
"2002-01-22|TypeError|TypeError|TypeError|TypeError|2023-02-28|1998-06-30|2000-05-01|2000-05-01|-271821-04-19|RangeError|275760-09-13|RangeError|RangeError"
);
}