mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
feat: Temporal.PlainYearMonth with/toPlainDate——字段 bag 覆盖(month→monthCode→year)、monthCode 两段校验与 day 级日期合成
This commit is contained in:
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user