mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-25 19:21:11 +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)
|
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)`:比较 (月, 日, 参考年) 三元与日历标识;
|
/// `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 {
|
||||||
|
|||||||
@@ -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 (),
|
oxide_builtins::temporal::plain_year_month_subtract::<crate::vm::Vm> as *const (),
|
||||||
1,
|
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(
|
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"
|
"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