mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
refactor: temporal 年月与月日族拆出双模块
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,730 @@
|
||||
//! Temporal.PlainMonthDay:ISO 月日解析、构造、getter 与 with/toPlainDate。
|
||||
|
||||
use oxide_runtime_api::{to_string, NativeResult, VmHost};
|
||||
use oxide_types::object::JsObject;
|
||||
use oxide_types::value::JsValue;
|
||||
|
||||
use super::common::native_try;
|
||||
use super::{
|
||||
calendar_annotation, days_from_civil, days_in_month_iso, ensure_plain_month_day, format_iso_year, get_calendar_id,
|
||||
get_double_prop, initialize_temporal_receiver, is_ctor_call, make_plain_date, make_plain_month_day,
|
||||
month_day_bag_calendar_id, read_iso2, receiver_obj, reject_partial_object_with_calendar_or_time_zone,
|
||||
strip_iso_offset, temporal_calendar_id_strict, temporal_number_component, temporal_option_string,
|
||||
temporal_option_value, temporal_overflow, temporal_to_show_calendar, valid_iso_date, valid_plain_time,
|
||||
ShowCalendar,
|
||||
};
|
||||
|
||||
/// 读 PlainMonthDay 的 月/日/参考年 三元组(槽 0-2),含 receiver 校验。
|
||||
fn plain_month_day_mdy<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<(f64, f64, f64), JsValue> {
|
||||
let ptr = receiver_obj(vm, args)?;
|
||||
let obj = unsafe { &*ptr };
|
||||
ensure_plain_month_day(vm, obj)?;
|
||||
Ok((get_double_prop(obj, 0), get_double_prop(obj, 1), get_double_prop(obj, 2)))
|
||||
}
|
||||
|
||||
/// 构造器日历参数解析(ToTemporalCalendarSlotValue,缺省 "iso8601"):
|
||||
/// undefined → 缺省;字符串 → 严格白名单(非法 RangeError);Temporal 日期实例 →
|
||||
/// 日历槽直读(不触发属性 getter);函数对象 → 缺省(无日历行为);
|
||||
/// 其他对象与原始值 → TypeError。
|
||||
pub(crate) fn temporal_constructor_calendar_id<H: VmHost>(vm: &mut H, value: JsValue) -> Result<String, JsValue> {
|
||||
if value.is_undefined() {
|
||||
return Ok("iso8601".to_string());
|
||||
}
|
||||
if value.is_string() {
|
||||
return temporal_calendar_id_strict(vm, value)
|
||||
.map(|calendar| calendar.unwrap_or_else(|| "iso8601".to_string()));
|
||||
}
|
||||
if value.is_object() {
|
||||
let ptr = value.as_js_object_ptr();
|
||||
if !ptr.is_null() {
|
||||
let obj = unsafe { &*ptr };
|
||||
if obj.is_function() {
|
||||
return Ok("iso8601".to_string());
|
||||
}
|
||||
let slot = if obj.is_plain_date_obj() {
|
||||
3
|
||||
} else if obj.is_plain_date_time_obj() {
|
||||
4
|
||||
} else if obj.is_zoned_date_time_obj() {
|
||||
2
|
||||
} else if obj.is_plain_month_day_obj() || obj.is_plain_year_month_obj() {
|
||||
3
|
||||
} else {
|
||||
return Err(crate::error::create_type_error(vm, "invalid calendar"));
|
||||
};
|
||||
return Ok(get_calendar_id(obj, slot));
|
||||
}
|
||||
}
|
||||
Err(crate::error::create_type_error(vm, "invalid calendar"))
|
||||
}
|
||||
|
||||
/// `Temporal.PlainMonthDay` 构造器:`new PlainMonthDay(month, day[, calendar[, referenceISOYear]])`。
|
||||
pub fn plain_month_day_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let ctor_proto = vm.session().builtin_world().plain_month_day_proto.as_ptr();
|
||||
if !is_ctor_call(vm, args, ctor_proto) {
|
||||
return NativeResult::Err(crate::error::create_type_error(
|
||||
vm,
|
||||
"Class constructor Temporal.PlainMonthDay cannot be invoked without 'new'",
|
||||
));
|
||||
}
|
||||
// 转换序:month → day → calendar → referenceISOYear(后两者有缺省值)。
|
||||
let month = native_try!(temporal_number_component(
|
||||
vm,
|
||||
if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() },
|
||||
));
|
||||
let day = native_try!(temporal_number_component(
|
||||
vm,
|
||||
if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() },
|
||||
));
|
||||
let calendar_raw = if args.len() > 3 { vm.reg(args[3]) } else { JsValue::undefined() };
|
||||
let calendar = native_try!(temporal_constructor_calendar_id(vm, calendar_raw));
|
||||
let ref_year = if args.len() > 4 && !vm.reg(args[4]).is_undefined() {
|
||||
native_try!(temporal_number_component(vm, vm.reg(args[4]))) as i32
|
||||
} else {
|
||||
1972
|
||||
};
|
||||
if !valid_iso_date(ref_year, month as u32, day as u32) {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO date"));
|
||||
}
|
||||
// 表示范围(ISODateWithinLimits):-271821-04-19 … +275760-09-13。
|
||||
let day_count = days_from_civil(i128::from(ref_year), i128::from(month as i32), i128::from(day as i32));
|
||||
if !(-100_000_001..=100_000_000).contains(&day_count) {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "ISO date is out of range"));
|
||||
}
|
||||
let calendar_value = vm.new_string(&calendar);
|
||||
initialize_temporal_receiver(
|
||||
vm,
|
||||
args,
|
||||
JsObject::OBJ_TYPE_PLAIN_MONTH_DAY,
|
||||
[
|
||||
JsValue::float(month),
|
||||
JsValue::float(day),
|
||||
JsValue::float(ref_year as f64),
|
||||
calendar_value,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/// `Temporal.PlainMonthDay.prototype.day` getter(槽 1)。
|
||||
pub fn plain_month_day_day<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let (_, day, _) = native_try!(plain_month_day_mdy(vm, args));
|
||||
NativeResult::Ok(JsValue::float(day))
|
||||
}
|
||||
|
||||
/// `Temporal.PlainMonthDay.prototype.monthCode` getter:`M01`..`M12`。
|
||||
pub fn plain_month_day_month_code<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let (month, _, _) = native_try!(plain_month_day_mdy(vm, args));
|
||||
NativeResult::Ok(vm.new_string(&format!("M{:02}", month as u32)))
|
||||
}
|
||||
|
||||
/// `Temporal.PlainMonthDay.prototype.calendarId` getter:读日历槽(槽 3)。
|
||||
pub fn plain_month_day_calendar_id<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let ptr = native_try!(receiver_obj(vm, args));
|
||||
let obj = unsafe { &*ptr };
|
||||
native_try!(ensure_plain_month_day(vm, obj));
|
||||
NativeResult::Ok(vm.new_string(&get_calendar_id(obj, 3)))
|
||||
}
|
||||
|
||||
/// PlainMonthDay ISO 串:always/critical 或非 iso8601 日历补参考年与日历注解;
|
||||
/// iso8601 日历的 auto/never 形保持裸 `MM-DD`。
|
||||
fn plain_month_day_iso_string(obj: &JsObject, show: ShowCalendar) -> String {
|
||||
let month = get_double_prop(obj, 0) as i32;
|
||||
let day = get_double_prop(obj, 1) as i32;
|
||||
let calendar = get_calendar_id(obj, 3);
|
||||
let with_year = matches!(show, ShowCalendar::Always | ShowCalendar::Critical) || calendar != "iso8601";
|
||||
if !with_year {
|
||||
return format!("{month:02}-{day:02}");
|
||||
}
|
||||
let ref_year = get_double_prop(obj, 2) as i32;
|
||||
let annotation = calendar_annotation(&calendar, show);
|
||||
format!("{}-{month:02}-{day:02}{annotation}", format_iso_year(ref_year as i128))
|
||||
}
|
||||
|
||||
/// PlainMonthDay 默认形串(auto 形),含 receiver 校验,不读 options。
|
||||
fn plain_month_day_default_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let ptr = native_try!(receiver_obj(vm, args));
|
||||
let obj = unsafe { &*ptr };
|
||||
native_try!(ensure_plain_month_day(vm, obj));
|
||||
NativeResult::Ok(vm.new_string_owned(plain_month_day_iso_string(obj, ShowCalendar::Auto)))
|
||||
}
|
||||
|
||||
/// `Temporal.PlainMonthDay.prototype.toString([options])`:
|
||||
/// 默认 `MM-DD`;非 iso8601 日历补参考年与 `[u-ca=…]` 注解,
|
||||
/// always/critical 另按注解表补 `[u-ca=…]`/`[!u-ca=…]`。
|
||||
pub fn plain_month_day_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let ptr = native_try!(receiver_obj(vm, args));
|
||||
let obj = unsafe { &*ptr };
|
||||
native_try!(ensure_plain_month_day(vm, obj));
|
||||
let show = native_try!(temporal_to_show_calendar(vm, args));
|
||||
NativeResult::Ok(vm.new_string_owned(plain_month_day_iso_string(obj, show)))
|
||||
}
|
||||
|
||||
/// `Temporal.PlainMonthDay.prototype.toJSON()`:默认形串,忽略参数。
|
||||
pub fn plain_month_day_to_json<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
plain_month_day_default_string(vm, args)
|
||||
}
|
||||
|
||||
/// `Temporal.PlainMonthDay.prototype.toLocaleString()`:locale/options 无引擎行为,
|
||||
/// 恒输出默认形串。
|
||||
pub fn plain_month_day_to_locale_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
plain_month_day_default_string(vm, args)
|
||||
}
|
||||
|
||||
/// `Temporal.PlainMonthDay.prototype.valueOf()`:Temporal 对象无值表示,恒 TypeError。
|
||||
pub fn plain_month_day_value_of<H: VmHost>(vm: &mut H, _args: &[u8]) -> NativeResult {
|
||||
NativeResult::Err(crate::error::create_type_error(vm, "Temporal.PlainMonthDay has no valueOf"))
|
||||
}
|
||||
|
||||
// ==================== PlainMonthDay.from ====================
|
||||
|
||||
/// PlainMonthDay 串注解校验:与 validate_temporal_annotation_suffix 同构,
|
||||
/// 但 u-ca 值只接受 iso8601(大小写不敏感),其余内置日历 ID 拒绝。
|
||||
pub(crate) fn validate_month_day_annotation_suffix(mut suffix: &str) -> Result<(), String> {
|
||||
let mut calendar_seen = false;
|
||||
let mut saw_critical_calendar = false;
|
||||
let mut time_zone_seen = false;
|
||||
while !suffix.is_empty() {
|
||||
if !suffix.starts_with('[') {
|
||||
return Err("invalid annotation suffix".into());
|
||||
}
|
||||
let Some(end) = suffix.find(']') else {
|
||||
return Err("unterminated annotation".into());
|
||||
};
|
||||
if end == 1 {
|
||||
return Err("empty annotation".into());
|
||||
}
|
||||
let annotation = &suffix[1..end];
|
||||
let (critical, body) = annotation.strip_prefix('!').map_or((false, annotation), |body| (true, body));
|
||||
if let Some((key, value)) = body.split_once('=') {
|
||||
if key.bytes().any(|byte| byte.is_ascii_uppercase()) {
|
||||
return Err("annotation keys must be lowercase".into());
|
||||
}
|
||||
if key == "u-ca" {
|
||||
if calendar_seen {
|
||||
if critical || saw_critical_calendar {
|
||||
return Err("invalid calendar annotation".into());
|
||||
}
|
||||
} else if !value.eq_ignore_ascii_case("iso8601") {
|
||||
return Err("invalid calendar annotation".into());
|
||||
} else {
|
||||
calendar_seen = true;
|
||||
saw_critical_calendar = critical;
|
||||
}
|
||||
} else if critical {
|
||||
return Err("unknown critical annotation".into());
|
||||
}
|
||||
} else if time_zone_seen {
|
||||
return Err("invalid time-zone annotation".into());
|
||||
} else {
|
||||
time_zone_seen = true;
|
||||
}
|
||||
suffix = &suffix[end + 1..];
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// PlainMonthDay 串的时间部分校验(值丢弃):偏移剥离须恰好耗尽剩余文本;
|
||||
/// 接受 T/t/空格分隔、小时可缺分钟秒、秒可 60(leap second)、小数秒 ≤9 位;
|
||||
/// 禁止小数时/分、UTC 设计符在调用侧先行拒绝。
|
||||
pub(crate) fn validate_month_day_time_suffix(input: &str) -> Result<(), String> {
|
||||
let time_end = input
|
||||
.char_indices()
|
||||
.find_map(|(index, ch)| matches!(ch, '+' | '-').then_some(index))
|
||||
.unwrap_or(input.len());
|
||||
if time_end < input.len() && !strip_iso_offset(&input[time_end..])?.is_empty() {
|
||||
return Err("invalid ISO offset".into());
|
||||
}
|
||||
let time = &input[..time_end];
|
||||
if time.is_empty() {
|
||||
return Err("missing ISO time".into());
|
||||
}
|
||||
let (clock, fraction) = match time.find(['.', ',']) {
|
||||
Some(index) => (&time[..index], Some(&time[index + 1..])),
|
||||
None => (time, None),
|
||||
};
|
||||
let fields = if clock.contains(':') {
|
||||
clock.split(':').collect::<Vec<_>>()
|
||||
} else {
|
||||
if clock.len() % 2 != 0 || clock.len() > 6 {
|
||||
return Err("invalid ISO time".into());
|
||||
}
|
||||
clock
|
||||
.as_bytes()
|
||||
.chunks(2)
|
||||
.map(std::str::from_utf8)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| "invalid ISO time".to_string())?
|
||||
};
|
||||
if fields.is_empty() || fields.len() > 3 || fields.iter().any(|field| field.len() != 2) {
|
||||
return Err("invalid ISO time".into());
|
||||
}
|
||||
if fraction.is_some() && fields.len() < 3 {
|
||||
return Err("fractional hours and minutes are not valid".into());
|
||||
}
|
||||
let parse_field = |index: usize| -> Result<u32, String> {
|
||||
fields
|
||||
.get(index)
|
||||
.map_or(Ok(0), |field| field.parse().map_err(|_| "invalid ISO time".to_string()))
|
||||
};
|
||||
let hour = parse_field(0)?;
|
||||
let minute = parse_field(1)?;
|
||||
let mut second = parse_field(2)?;
|
||||
if second == 60 {
|
||||
second = 59;
|
||||
}
|
||||
let subsecond = match fraction {
|
||||
Some(digits) if !digits.is_empty() && digits.len() <= 9 && digits.bytes().all(|byte| byte.is_ascii_digit()) => {
|
||||
let mut padded = digits.to_owned();
|
||||
padded.extend(std::iter::repeat('0').take(9 - digits.len()));
|
||||
padded.parse::<u32>().map_err(|_| "invalid ISO fraction".to_string())?
|
||||
}
|
||||
Some(_) => return Err("invalid ISO fraction".into()),
|
||||
None => 0,
|
||||
};
|
||||
if !valid_plain_time(hour, minute, second, subsecond / 1_000_000, subsecond / 1_000 % 1_000, subsecond % 1_000) {
|
||||
return Err("invalid ISO time".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// "MM-DD" 或 "MMDD" 形态的月日解析。
|
||||
fn parse_month_day_md(input: &str) -> Result<(u32, u32), String> {
|
||||
let bytes = input.as_bytes();
|
||||
let (m_s, d_s) = if bytes.len() == 4 && bytes.iter().all(|b| b.is_ascii_digit()) {
|
||||
(&input[..2], &input[2..])
|
||||
} else if bytes.len() == 5
|
||||
&& bytes[2] == b'-'
|
||||
&& bytes[..2].iter().all(|b| b.is_ascii_digit())
|
||||
&& bytes[3..].iter().all(|b| b.is_ascii_digit())
|
||||
{
|
||||
(&input[..2], &input[3..])
|
||||
} else {
|
||||
return Err("invalid ISO month-day".into());
|
||||
};
|
||||
let month: u32 = m_s.parse().map_err(|_| "invalid ISO month".to_string())?;
|
||||
let day: u32 = d_s.parse().map_err(|_| "invalid ISO day".to_string())?;
|
||||
Ok((month, day))
|
||||
}
|
||||
|
||||
/// PlainMonthDay 串的日期部分:MM-DD / MMDD / --MM-DD / --MMDD / 完整日期(扩展或紧凑)。
|
||||
/// 年份仅用于负零年拒绝(-000000 → 错),无范围检查;返回 (month, day)。
|
||||
fn parse_month_day_date_part(input: &str) -> Result<(u32, u32), String> {
|
||||
if input.is_empty() {
|
||||
return Err("invalid ISO date".into());
|
||||
}
|
||||
if let Some(rest) = input.strip_prefix("--") {
|
||||
let (month, day) = parse_month_day_md(rest)?;
|
||||
return finish_month_day_range(month, day);
|
||||
}
|
||||
let bytes = input.as_bytes();
|
||||
let signed = matches!(bytes[0], b'-' | b'+');
|
||||
let negative = bytes[0] == b'-';
|
||||
let mut i = usize::from(signed);
|
||||
let y_start = i;
|
||||
while i < bytes.len() && bytes[i].is_ascii_digit() {
|
||||
i += 1;
|
||||
}
|
||||
let digits = &input[y_start..i];
|
||||
if i < bytes.len() && bytes[i] == b'-' {
|
||||
// 扩展形:年份 + -MM-DD;无符号两位引导是 MM-DD 形态。
|
||||
if digits.len() == 2 && !signed {
|
||||
let (month, day) = parse_month_day_md(input)?;
|
||||
return finish_month_day_range(month, day);
|
||||
}
|
||||
let expected = usize::from(signed) * 6 + 4 * (1 - usize::from(signed));
|
||||
if digits.len() != expected {
|
||||
return Err("invalid ISO year".into());
|
||||
}
|
||||
i += 1;
|
||||
let month = read_iso2(input, &mut i, bytes)?;
|
||||
if i >= bytes.len() || bytes[i] != b'-' {
|
||||
return Err("invalid ISO date".into());
|
||||
}
|
||||
i += 1;
|
||||
let day = read_iso2(input, &mut i, bytes)?;
|
||||
if i != bytes.len() {
|
||||
return Err("invalid trailing content".into());
|
||||
}
|
||||
if negative && digits == "000000" {
|
||||
return Err("invalid ISO negative zero year".into());
|
||||
}
|
||||
return finish_month_day_range(month, day);
|
||||
}
|
||||
if i != bytes.len() {
|
||||
return Err("invalid trailing content".into());
|
||||
}
|
||||
// 紧凑纯数字:4 位 = MMDD(无符号);否则 (4-6 位年) + 4 位月日。
|
||||
let len = digits.len();
|
||||
if !(len == 4 && !signed || len == 10 && signed || (!signed && (8..=10).contains(&len))) {
|
||||
return Err("invalid ISO date".into());
|
||||
}
|
||||
let tail = &input[y_start..];
|
||||
let y_len = tail.len() - 4;
|
||||
let (month, day) = parse_month_day_md(&tail[y_len..])?;
|
||||
if y_len == 6 && negative && &tail[..6] == "000000" {
|
||||
return Err("invalid ISO negative zero year".into());
|
||||
}
|
||||
finish_month_day_range(month, day)
|
||||
}
|
||||
|
||||
/// 月日取值边界:月 1..=12、日 1..=31(串路径无参考年,不做月长检查)。
|
||||
fn finish_month_day_range(month: u32, day: u32) -> Result<(u32, u32), String> {
|
||||
if month == 0 || month > 12 || day == 0 || day > 31 {
|
||||
return Err("invalid ISO date".into());
|
||||
}
|
||||
Ok((month, day))
|
||||
}
|
||||
|
||||
/// 解析 PlainMonthDay ISO 串("MM-DD"/"MMDD"/"--MM-DD"/完整日期/完整 datetime)。
|
||||
/// 注解与时间部分按规则校验;参考年恒 1972(串中年份丢弃)。
|
||||
fn parse_month_day_string(input: &str) -> Result<(u32, u32), String> {
|
||||
let trimmed = input.trim();
|
||||
if trimmed.contains('\u{2212}') {
|
||||
return Err("variant minus sign is not valid for PlainMonthDay".into());
|
||||
}
|
||||
let text = trimmed.to_owned();
|
||||
let annotation_start = text.find('[').unwrap_or(text.len());
|
||||
validate_month_day_annotation_suffix(&text[annotation_start..])?;
|
||||
let text = &text[..annotation_start];
|
||||
if text.contains('Z') || text.contains('z') {
|
||||
return Err("UTC designator is not valid for PlainMonthDay".into());
|
||||
}
|
||||
let separator = text.find(['T', 't', ' ']);
|
||||
let (date_part, time_part) = match separator {
|
||||
Some(index) => (&text[..index], &text[index + 1..]),
|
||||
None => (text, ""),
|
||||
};
|
||||
if !time_part.is_empty() {
|
||||
validate_month_day_time_suffix(time_part)?;
|
||||
}
|
||||
parse_month_day_date_part(date_part)
|
||||
}
|
||||
|
||||
/// `Temporal.PlainMonthDay.from(item[, options])`:ISO 串 / PMD 实例 / PD 实例 / 字段对象。
|
||||
/// 参考年恒 1972(串中年份与 bag year 均不入结果,year 仅供 overflow 月长判断);
|
||||
/// options.overflow 在全部字段 Get 之后读取。
|
||||
pub fn plain_month_day_from<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
|
||||
if value.is_string() {
|
||||
// 规范顺序:先解析(错误即抛,不触 options),再读 overflow(结果不受其影响)。
|
||||
let (month, day) = native_try!(parse_month_day_string(&to_string(value))
|
||||
.map_err(|_| crate::error::create_range_error(vm, "invalid ISO 8601 month-day string")));
|
||||
native_try!(temporal_overflow(vm, args));
|
||||
return make_plain_month_day(vm, month, day, 1972, "iso8601");
|
||||
}
|
||||
if !value.is_object() {
|
||||
return NativeResult::Err(crate::error::create_type_error(vm, "from() argument must be a string or an object"));
|
||||
}
|
||||
let ptr = value.as_js_object_ptr();
|
||||
if ptr.is_null() {
|
||||
return NativeResult::Err(crate::error::create_type_error(vm, "from() argument must be a string or an object"));
|
||||
}
|
||||
let obj = unsafe { &*ptr };
|
||||
// 实例快路径:槽直读,不触发属性。
|
||||
if obj.is_plain_month_day_obj() {
|
||||
// 实例复制:槽直读(refYear/日历保留),overflow 读取但结果不受其影响。
|
||||
native_try!(temporal_overflow(vm, args));
|
||||
let calendar = get_calendar_id(obj, 3);
|
||||
return make_plain_month_day(
|
||||
vm,
|
||||
get_double_prop(obj, 0) as u32,
|
||||
get_double_prop(obj, 1) as u32,
|
||||
get_double_prop(obj, 2) as i32,
|
||||
&calendar,
|
||||
);
|
||||
}
|
||||
if obj.is_plain_date_obj() {
|
||||
// PlainDate 实例:年月日槽直读,overflow 按其年应用,参考年 1972。
|
||||
let constrain = native_try!(temporal_overflow(vm, args));
|
||||
let year = get_double_prop(obj, 0) as i32;
|
||||
let month = get_double_prop(obj, 1) as u32;
|
||||
let mut day = get_double_prop(obj, 2) as u32;
|
||||
let calendar = get_calendar_id(obj, 3);
|
||||
if constrain {
|
||||
day = day.min(days_in_month_iso(year, month));
|
||||
}
|
||||
return make_plain_month_day(vm, month, day, 1972, &calendar);
|
||||
}
|
||||
// 字段对象:Get 序 calendar → day → month → monthCode → year → era → eraYear,
|
||||
// 全部读原始值(不转换);overflow 在全部 Get 之后读取。
|
||||
let calendar_raw = native_try!(temporal_option_value(vm, obj, value, "calendar"));
|
||||
let calendar = native_try!(month_day_bag_calendar_id(vm, calendar_raw));
|
||||
let day_raw = native_try!(temporal_option_value(vm, obj, value, "day"));
|
||||
let month_raw = native_try!(temporal_option_value(vm, obj, value, "month"));
|
||||
let month_code_raw = native_try!(temporal_option_value(vm, obj, value, "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, obj, value, "year"));
|
||||
let era_raw = native_try!(temporal_option_value(vm, obj, value, "era"));
|
||||
let era_year_raw = native_try!(temporal_option_value(vm, obj, value, "eraYear"));
|
||||
let constrain = native_try!(temporal_overflow(vm, args));
|
||||
// era 族:双现 → RangeError;恰一个 → 忽略(ISO 无纪元体系)。
|
||||
if !era_raw.is_undefined() && !era_year_raw.is_undefined() {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "era and eraYear cannot both be present"));
|
||||
}
|
||||
// 月来源:双给 → RangeError;双缺 → TypeError(先于日存在性)。
|
||||
if !month_raw.is_undefined() && month_code.is_some() {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "month and monthCode cannot both be present"));
|
||||
}
|
||||
if month_raw.is_undefined() && month_code.is_none() {
|
||||
return NativeResult::Err(crate::error::create_type_error(vm, "month or monthCode is required"));
|
||||
}
|
||||
if day_raw.is_undefined() {
|
||||
return NativeResult::Err(crate::error::create_type_error(vm, "day is required"));
|
||||
}
|
||||
// monthCode 两段校验:第一段语法("M"+两位数字,可选再 +"L")先于一切数值转换;
|
||||
// 第二段适配(闰月后缀、1..12 范围)在 year 转换之后。
|
||||
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),
|
||||
};
|
||||
// year 转换(TypeError/RangeError)在 monthCode 语法之后。
|
||||
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))),
|
||||
};
|
||||
// monthCode 第二段适配:闰月后缀或月值越界 → RangeError。
|
||||
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 负值恒 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 NativeResult::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 NativeResult::Err(crate::error::create_range_error(vm, "invalid month"));
|
||||
}
|
||||
if f > 12.0 {
|
||||
if constrain {
|
||||
12
|
||||
} else {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid month"));
|
||||
}
|
||||
} else {
|
||||
f as u32
|
||||
}
|
||||
};
|
||||
let day_f = native_try!(temporal_number_component(vm, day_raw));
|
||||
if day_f < 1.0 {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid day"));
|
||||
}
|
||||
// overflow 应用:月长按 year(bag 缺省 1972)判断;constrain 钳制,reject 抛错。
|
||||
let overflow_year = year.unwrap_or(1972);
|
||||
let days = days_in_month_iso(overflow_year, month) as i32;
|
||||
let day_i = day_f as i32;
|
||||
let day = if day_i > days {
|
||||
if constrain {
|
||||
days
|
||||
} else {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid day"));
|
||||
}
|
||||
} else {
|
||||
day_i
|
||||
};
|
||||
make_plain_month_day(vm, month, day as u32, 1972, &calendar)
|
||||
}
|
||||
|
||||
/// `Temporal.PlainMonthDay.prototype.equals(other)`:比较 (月, 日, 参考年) 三元与日历标识;
|
||||
/// other 经 ToTemporalMonthDay(串 / 字段对象 / 实例)转换。
|
||||
pub fn plain_month_day_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_month_day(vm, obj));
|
||||
let other = match plain_month_day_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.with(monthDayLike[, options])`:覆盖 day/month/monthCode;
|
||||
/// year 只参与 overflow 月长判断;undefined 不覆盖;calendar/timeZone 键拒绝;
|
||||
/// 无识别字段 TypeError;overflow 读自 options(缺省 constrain)。
|
||||
/// 结果保留 receiver 日历,参考年恒 1972。
|
||||
pub fn plain_month_day_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_month_day(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 序(字典序):day → month → monthCode → year;无识别字段 → TypeError。
|
||||
let day_raw = native_try!(temporal_option_value(vm, like_obj, like, "day"));
|
||||
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 [day_raw, 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")先于 year 转换;
|
||||
// 适配(闰月后缀、1..12 范围)与冲突检查在 options 读取之后。
|
||||
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))),
|
||||
};
|
||||
let day_f = match day_raw.is_undefined() {
|
||||
true => None,
|
||||
false => Some(native_try!(temporal_number_component(vm, day_raw))),
|
||||
};
|
||||
if let Some(f) = month_f {
|
||||
if f < 1.0 {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid month"));
|
||||
}
|
||||
}
|
||||
if let Some(f) = day_f {
|
||||
if f < 1.0 {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid day"));
|
||||
}
|
||||
}
|
||||
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"));
|
||||
}
|
||||
}
|
||||
// 月定值:receiver 月(槽 0)为缺省;monthCode 与数值 month 并存须一致;
|
||||
// 数值 month 负值恒 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 NativeResult::Err(crate::error::create_range_error(vm, "month and monthCode conflict"));
|
||||
}
|
||||
}
|
||||
code
|
||||
} else {
|
||||
match month_f {
|
||||
Some(f) => {
|
||||
if f < 1.0 {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid month"));
|
||||
}
|
||||
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, 0) as u32,
|
||||
}
|
||||
};
|
||||
// 日:receiver 日(槽 1)为缺省(partial 日已先行转换并过 <1 检查)。
|
||||
let day_f = day_f.unwrap_or_else(|| get_double_prop(obj, 1));
|
||||
// overflow 应用:月长按 bag year(缺省 1972)判断;constrain 钳制,reject 抛错。
|
||||
let overflow_year = year.unwrap_or(1972);
|
||||
let days = days_in_month_iso(overflow_year, month) as i32;
|
||||
let day_i = day_f as i32;
|
||||
let day = if day_i > days {
|
||||
if constrain {
|
||||
days
|
||||
} else {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid day"));
|
||||
}
|
||||
} else {
|
||||
day_i
|
||||
};
|
||||
let calendar = get_calendar_id(obj, 3);
|
||||
make_plain_month_day(vm, month, day as u32, 1972, &calendar)
|
||||
}
|
||||
|
||||
/// `Temporal.PlainMonthDay.prototype.toPlainDate(yearLike)`:yearLike 为 number 或 {year};
|
||||
/// 只 Get item 的 year;overflow 恒 constrain 且不读 options;
|
||||
/// constrain 钳 day、day 级范围检查后产出 PlainDate(receiver 日历)。
|
||||
pub fn plain_month_day_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_month_day(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 year_raw = native_try!(temporal_option_value(vm, item_obj, item, "year"));
|
||||
if year_raw.is_undefined() {
|
||||
return NativeResult::Err(crate::error::create_type_error(vm, "year is required"));
|
||||
}
|
||||
let year = native_try!(temporal_number_component(vm, year_raw)) as i32;
|
||||
let month = get_double_prop(obj, 0) as u32;
|
||||
// constrain:day 钳到 (year, month) 月长。
|
||||
let day = (get_double_prop(obj, 1) as u32).min(days_in_month_iso(year, month));
|
||||
// day 级表示范围:-100_000_001..=100_000_000。
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,875 @@
|
||||
//! Temporal.PlainYearMonth:ISO 年月解析、构造、getter、日历属性与 with/加减差值。
|
||||
|
||||
use oxide_runtime_api::{to_string, NativeResult, VmHost};
|
||||
use oxide_types::object::JsObject;
|
||||
use oxide_types::value::JsValue;
|
||||
|
||||
use super::common::native_try;
|
||||
use super::{
|
||||
calendar_annotation, compare_iso_date, days_from_civil, days_in_month_iso, difference_core, duration_like_values,
|
||||
ensure_plain_year_month, format_iso_year, get_calendar_id, get_double_prop, initialize_temporal_receiver,
|
||||
is_ctor_call, is_leap_year_iso, iso_year_month_within_limits, make_duration, make_plain_date,
|
||||
make_plain_year_month, month_day_bag_calendar_id, native_engine_error, parse_year_month_difference_settings,
|
||||
read_iso2, receiver_obj, reject_partial_object_with_calendar_or_time_zone, temporal_constructor_calendar_id,
|
||||
temporal_number_component, temporal_option_string, temporal_option_value, temporal_overflow,
|
||||
temporal_to_show_calendar, validate_month_day_annotation_suffix, validate_month_day_time_suffix, ShowCalendar,
|
||||
MAX_ISO_DAY,
|
||||
};
|
||||
|
||||
/// 读 PlainYearMonth 的 年/月/参考日 三元组(槽 0-2),含 receiver 校验。
|
||||
fn plain_year_month_ymd<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<(f64, f64, f64), JsValue> {
|
||||
let ptr = receiver_obj(vm, args)?;
|
||||
let obj = unsafe { &*ptr };
|
||||
ensure_plain_year_month(vm, obj)?;
|
||||
Ok((get_double_prop(obj, 0), get_double_prop(obj, 1), get_double_prop(obj, 2)))
|
||||
}
|
||||
|
||||
/// `Temporal.PlainYearMonth` 构造器:`new PlainYearMonth(year, month[, calendar[, referenceISODay]])`。
|
||||
pub fn plain_year_month_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let ctor_proto = vm.session().builtin_world().plain_year_month_proto.as_ptr();
|
||||
if !is_ctor_call(vm, args, ctor_proto) {
|
||||
return NativeResult::Err(crate::error::create_type_error(
|
||||
vm,
|
||||
"Class constructor Temporal.PlainYearMonth cannot be invoked without 'new'",
|
||||
));
|
||||
}
|
||||
// 转换序:year → month → calendar → referenceISODay(后两者有缺省值)。
|
||||
let year = native_try!(temporal_number_component(
|
||||
vm,
|
||||
if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() },
|
||||
)) as i32;
|
||||
let month = native_try!(temporal_number_component(
|
||||
vm,
|
||||
if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() },
|
||||
));
|
||||
let calendar_raw = if args.len() > 3 { vm.reg(args[3]) } else { JsValue::undefined() };
|
||||
let calendar = native_try!(temporal_constructor_calendar_id(vm, calendar_raw));
|
||||
let ref_day = if args.len() > 4 && !vm.reg(args[4]).is_undefined() {
|
||||
native_try!(temporal_number_component(vm, vm.reg(args[4])))
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let month_i = month as i32;
|
||||
if !(1..=12).contains(&month_i) {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO date"));
|
||||
}
|
||||
let ref_day_i = ref_day as i32;
|
||||
if ref_day_i < 1 || ref_day_i > days_in_month_iso(year, month_i as u32) as i32 {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid ISO date"));
|
||||
}
|
||||
if !iso_year_month_within_limits(year, month_i as u32) {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "ISO date is out of range"));
|
||||
}
|
||||
let calendar_value = vm.new_string(&calendar);
|
||||
initialize_temporal_receiver(
|
||||
vm,
|
||||
args,
|
||||
JsObject::OBJ_TYPE_PLAIN_YEAR_MONTH,
|
||||
[
|
||||
JsValue::float(year as f64),
|
||||
JsValue::float(month),
|
||||
JsValue::float(ref_day),
|
||||
calendar_value,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/// `Temporal.PlainYearMonth.prototype.year` getter(槽 0)。
|
||||
pub fn plain_year_month_year<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let (year, _, _) = native_try!(plain_year_month_ymd(vm, args));
|
||||
NativeResult::Ok(JsValue::float(year))
|
||||
}
|
||||
|
||||
/// `Temporal.PlainYearMonth.prototype.month` getter(槽 1)。
|
||||
pub fn plain_year_month_month<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let (_, month, _) = native_try!(plain_year_month_ymd(vm, args));
|
||||
NativeResult::Ok(JsValue::float(month))
|
||||
}
|
||||
|
||||
/// `Temporal.PlainYearMonth.prototype.monthCode` getter:`M01`..`M12`。
|
||||
pub fn plain_year_month_month_code<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let (_, month, _) = native_try!(plain_year_month_ymd(vm, args));
|
||||
NativeResult::Ok(vm.new_string(&format!("M{:02}", month as u32)))
|
||||
}
|
||||
|
||||
/// `Temporal.PlainYearMonth.prototype.calendarId` getter:读日历槽(槽 3)。
|
||||
pub fn plain_year_month_calendar_id<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));
|
||||
NativeResult::Ok(vm.new_string(&get_calendar_id(obj, 3)))
|
||||
}
|
||||
|
||||
/// `Temporal.PlainYearMonth.prototype.daysInMonth` getter:按年月的 ISO 月长。
|
||||
pub fn plain_year_month_days_in_month<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let (year, month, _) = native_try!(plain_year_month_ymd(vm, args));
|
||||
NativeResult::Ok(JsValue::float(days_in_month_iso(year as i32, month as u32) as f64))
|
||||
}
|
||||
|
||||
/// `Temporal.PlainYearMonth.prototype.daysInYear` getter:366(闰年)或 365。
|
||||
pub fn plain_year_month_days_in_year<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let (year, _, _) = native_try!(plain_year_month_ymd(vm, args));
|
||||
NativeResult::Ok(JsValue::float(if is_leap_year_iso(year as i32) { 366.0 } else { 365.0 }))
|
||||
}
|
||||
|
||||
/// `Temporal.PlainYearMonth.prototype.monthsInYear` getter:恒 12(ISO 日历)。
|
||||
pub fn plain_year_month_months_in_year<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let _ = native_try!(plain_year_month_ymd(vm, args));
|
||||
NativeResult::Ok(JsValue::float(12.0))
|
||||
}
|
||||
|
||||
/// `Temporal.PlainYearMonth.prototype.inLeapYear` getter。
|
||||
pub fn plain_year_month_in_leap_year<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let (year, _, _) = native_try!(plain_year_month_ymd(vm, args));
|
||||
NativeResult::Ok(JsValue::bool(is_leap_year_iso(year as i32)))
|
||||
}
|
||||
|
||||
/// `Temporal.PlainYearMonth.prototype.era` getter:ISO 日历无纪元,恒 undefined。
|
||||
pub fn plain_year_month_era<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let _ = native_try!(plain_year_month_ymd(vm, args));
|
||||
NativeResult::Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
/// `Temporal.PlainYearMonth.prototype.eraYear` getter:ISO 日历无纪元,恒 undefined。
|
||||
pub fn plain_year_month_era_year<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let _ = native_try!(plain_year_month_ymd(vm, args));
|
||||
NativeResult::Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
/// PlainYearMonth ISO 串:always/critical 或非 iso8601 日历补参考日与日历注解;
|
||||
/// iso8601 日历的 auto/never 形保持裸 `±YYYY-MM`。
|
||||
fn plain_year_month_iso_string(obj: &JsObject, show: ShowCalendar) -> String {
|
||||
let year = get_double_prop(obj, 0) as i32;
|
||||
let month = get_double_prop(obj, 1) as i32;
|
||||
let calendar = get_calendar_id(obj, 3);
|
||||
let with_day = matches!(show, ShowCalendar::Always | ShowCalendar::Critical) || calendar != "iso8601";
|
||||
if !with_day {
|
||||
return format!("{}-{month:02}", format_iso_year(year as i128));
|
||||
}
|
||||
let ref_day = get_double_prop(obj, 2) as i32;
|
||||
let annotation = calendar_annotation(&calendar, show);
|
||||
format!("{}-{month:02}-{ref_day:02}{annotation}", format_iso_year(year as i128))
|
||||
}
|
||||
|
||||
/// PlainYearMonth 默认形串(auto 形),含 receiver 校验,不读 options。
|
||||
fn plain_year_month_default_string<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));
|
||||
NativeResult::Ok(vm.new_string_owned(plain_year_month_iso_string(obj, ShowCalendar::Auto)))
|
||||
}
|
||||
|
||||
/// `Temporal.PlainYearMonth.prototype.toString([options])`:
|
||||
/// 默认 `±YYYY-MM`;非 iso8601 日历补参考日与 `[u-ca=…]` 注解,
|
||||
/// always/critical 另按注解表补 `[u-ca=…]`/`[!u-ca=…]`。
|
||||
pub fn plain_year_month_to_string<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 show = native_try!(temporal_to_show_calendar(vm, args));
|
||||
NativeResult::Ok(vm.new_string_owned(plain_year_month_iso_string(obj, show)))
|
||||
}
|
||||
|
||||
/// `Temporal.PlainYearMonth.prototype.toJSON()`:默认形串,忽略参数。
|
||||
pub fn plain_year_month_to_json<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
plain_year_month_default_string(vm, args)
|
||||
}
|
||||
|
||||
/// `Temporal.PlainYearMonth.prototype.toLocaleString()`:locale/options 无引擎行为,
|
||||
/// 恒输出默认形串。
|
||||
pub fn plain_year_month_to_locale_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
plain_year_month_default_string(vm, args)
|
||||
}
|
||||
|
||||
/// `Temporal.PlainYearMonth.prototype.valueOf()`:Temporal 对象无值表示,恒 TypeError。
|
||||
pub fn plain_year_month_value_of<H: VmHost>(vm: &mut H, _args: &[u8]) -> NativeResult {
|
||||
NativeResult::Err(crate::error::create_type_error(vm, "Temporal.PlainYearMonth has no valueOf"))
|
||||
}
|
||||
|
||||
/// 年月串边界:月 1..=12;日(若有)1..=31 且过语法级月日静态检查
|
||||
/// (2 月 30/31、30 天月份 31 拒;无参考年,2 月 29 不拒)。
|
||||
fn finish_year_month_range(year: i32, month: u32, day: Option<u32>) -> Result<(i32, u32, Option<u32>), String> {
|
||||
if !(1..=12).contains(&month) {
|
||||
return Err("invalid ISO month".into());
|
||||
}
|
||||
if let Some(day) = day {
|
||||
if !(1..=31).contains(&day) || (month == 2 && day > 29) || (matches!(month, 4 | 6 | 9 | 11) && day == 31) {
|
||||
return Err("invalid ISO date".into());
|
||||
}
|
||||
}
|
||||
Ok((year, month, day))
|
||||
}
|
||||
|
||||
/// PlainYearMonth 串的日期部分:扩展形(无符号 4 位年或带符号 6 位年)+ -MM[-DD],
|
||||
/// 紧凑形(无符号 6/8 位、带符号 8/10 位纯数字);日部分可缺(年-月形);
|
||||
/// 负零年(-000000)拒;返回 (year, month, day)。
|
||||
fn parse_year_month_date_part(input: &str) -> Result<(i32, u32, Option<u32>), String> {
|
||||
if input.is_empty() {
|
||||
return Err("invalid ISO date".into());
|
||||
}
|
||||
let bytes = input.as_bytes();
|
||||
let signed = matches!(bytes[0], b'-' | b'+');
|
||||
let negative = bytes[0] == b'-';
|
||||
let mut i = usize::from(signed);
|
||||
let y_start = i;
|
||||
while i < bytes.len() && bytes[i].is_ascii_digit() {
|
||||
i += 1;
|
||||
}
|
||||
let digits = &input[y_start..i];
|
||||
if negative && digits == "000000" {
|
||||
return Err("invalid ISO negative zero year".into());
|
||||
}
|
||||
if i < bytes.len() && bytes[i] == b'-' {
|
||||
// 扩展形:无符号 4 位年或带符号 6 位年,随后 -MM[-DD]。
|
||||
let expected = usize::from(signed) * 6 + 4 * (1 - usize::from(signed));
|
||||
if digits.len() != expected {
|
||||
return Err("invalid ISO year".into());
|
||||
}
|
||||
let year = digits.parse::<i32>().map_err(|_| "invalid ISO year".to_string())?;
|
||||
let year = if negative { -year } else { year };
|
||||
i += 1;
|
||||
let month = read_iso2(input, &mut i, bytes)?;
|
||||
if i >= bytes.len() {
|
||||
return finish_year_month_range(year, month, None);
|
||||
}
|
||||
if bytes[i] != b'-' {
|
||||
return Err("invalid trailing content".into());
|
||||
}
|
||||
i += 1;
|
||||
let day = read_iso2(input, &mut i, bytes)?;
|
||||
if i != bytes.len() {
|
||||
return Err("invalid trailing content".into());
|
||||
}
|
||||
return finish_year_month_range(year, month, Some(day));
|
||||
}
|
||||
if i != bytes.len() {
|
||||
return Err("invalid trailing content".into());
|
||||
}
|
||||
// 紧凑纯数字:无符号 6(年+月)/ 8(年+月+日);带符号 8 / 10(6 位年起步)。
|
||||
let len = digits.len();
|
||||
if !(matches!(len, 6 | 8) && !signed || matches!(len, 8 | 10) && signed) {
|
||||
return Err("invalid ISO date".into());
|
||||
}
|
||||
let month_start = if signed { 6 } else { 4 };
|
||||
let year = digits[..month_start]
|
||||
.parse::<i32>()
|
||||
.map_err(|_| "invalid ISO year".to_string())?;
|
||||
let year = if negative { -year } else { year };
|
||||
let month = digits[month_start..month_start + 2]
|
||||
.parse::<u32>()
|
||||
.map_err(|_| "invalid ISO month".to_string())?;
|
||||
let day = if digits.len() > month_start + 2 {
|
||||
Some(
|
||||
digits[month_start + 2..]
|
||||
.parse::<u32>()
|
||||
.map_err(|_| "invalid ISO day".to_string())?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
finish_year_month_range(year, month, day)
|
||||
}
|
||||
|
||||
/// 解析 PlainYearMonth ISO 串(年-月 / 年-月-日 / 完整 datetime 形态)。
|
||||
/// 返回 (year, month):日部分仅参与语法级校验,参考日恒 1(不入结果)。
|
||||
fn parse_year_month_string(input: &str) -> Result<(i32, u32), String> {
|
||||
let trimmed = input.trim();
|
||||
if trimmed.contains('\u{2212}') {
|
||||
return Err("variant minus sign is not valid for PlainYearMonth".into());
|
||||
}
|
||||
let text = trimmed.to_owned();
|
||||
let annotation_start = text.find('[').unwrap_or(text.len());
|
||||
validate_month_day_annotation_suffix(&text[annotation_start..])?;
|
||||
let text = &text[..annotation_start];
|
||||
if text.contains('Z') || text.contains('z') {
|
||||
return Err("UTC designator is not valid for PlainYearMonth".into());
|
||||
}
|
||||
let separator = text.find(['T', 't', ' ']);
|
||||
let (date_part, time_part) = match separator {
|
||||
Some(index) => (&text[..index], &text[index + 1..]),
|
||||
None => (text, ""),
|
||||
};
|
||||
if !time_part.is_empty() {
|
||||
validate_month_day_time_suffix(time_part)?;
|
||||
}
|
||||
let (year, month, _) = parse_year_month_date_part(date_part)?;
|
||||
Ok((year, month))
|
||||
}
|
||||
|
||||
/// `Temporal.PlainYearMonth.from(item[, options])`:ISO 串 / PYM 实例 / PD 实例 / 字段对象。
|
||||
/// 串路径按 (年, 月) 查表示范围(日不参与);实例分支槽直读(PYM 复制保留参考日,
|
||||
/// PD 参考日恒 1);bag 路径 year 必填、day 从不读取(参考日恒 1)、
|
||||
/// monthCode 先 ToPrimitive 再两段校验(语法先于 year 转换、适配在 year 转换后)。
|
||||
pub fn plain_year_month_from<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
|
||||
if value.is_string() {
|
||||
// 规范顺序:先解析(坏串 RangeError 不触 options),再读 overflow,再查 (年, 月) 范围。
|
||||
let (year, month) = native_try!(parse_year_month_string(&to_string(value))
|
||||
.map_err(|_| crate::error::create_range_error(vm, "invalid ISO 8601 year-month string")));
|
||||
native_try!(temporal_overflow(vm, args));
|
||||
if !iso_year_month_within_limits(year, month) {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "ISO year-month is out of range"));
|
||||
}
|
||||
return make_plain_year_month(vm, year, month, 1, "iso8601");
|
||||
}
|
||||
if !value.is_object() {
|
||||
return NativeResult::Err(crate::error::create_type_error(vm, "from() argument must be a string or an object"));
|
||||
}
|
||||
let ptr = value.as_js_object_ptr();
|
||||
if ptr.is_null() {
|
||||
return NativeResult::Err(crate::error::create_type_error(vm, "from() argument must be a string or an object"));
|
||||
}
|
||||
let obj = unsafe { &*ptr };
|
||||
// 实例快路径:槽直读,不触发属性。
|
||||
if obj.is_plain_year_month_obj() {
|
||||
// 实例复制:overflow 先读(坏 options 先抛)后复制槽(参考日/日历保留)。
|
||||
native_try!(temporal_overflow(vm, args));
|
||||
let calendar = get_calendar_id(obj, 3);
|
||||
return make_plain_year_month(
|
||||
vm,
|
||||
get_double_prop(obj, 0) as i32,
|
||||
get_double_prop(obj, 1) as u32,
|
||||
get_double_prop(obj, 2) as u32,
|
||||
&calendar,
|
||||
);
|
||||
}
|
||||
if obj.is_plain_date_obj() {
|
||||
// PlainDate 实例:年/月槽直读,参考日恒 1(日不读),日历槽直读。
|
||||
native_try!(temporal_overflow(vm, args));
|
||||
let year = get_double_prop(obj, 0) as i32;
|
||||
let month = get_double_prop(obj, 1) as u32;
|
||||
let calendar = get_calendar_id(obj, 3);
|
||||
return make_plain_year_month(vm, year, month, 1, &calendar);
|
||||
}
|
||||
// 字段对象:Get 序 calendar → year → month → monthCode → era → eraYear(day 不读);
|
||||
// overflow 在全部 Get 之后读取。
|
||||
let calendar_raw = native_try!(temporal_option_value(vm, obj, value, "calendar"));
|
||||
let calendar = native_try!(month_day_bag_calendar_id(vm, calendar_raw));
|
||||
let year_raw = native_try!(temporal_option_value(vm, obj, value, "year"));
|
||||
let month_raw = native_try!(temporal_option_value(vm, obj, value, "month"));
|
||||
let month_code_raw = native_try!(temporal_option_value(vm, obj, value, "monthCode"));
|
||||
let era_raw = native_try!(temporal_option_value(vm, obj, value, "era"));
|
||||
let era_year_raw = native_try!(temporal_option_value(vm, obj, value, "eraYear"));
|
||||
let constrain = native_try!(temporal_overflow(vm, args));
|
||||
// era 族:双现 → RangeError;恰一个 → 忽略(ISO 无纪元体系)。
|
||||
if !era_raw.is_undefined() && !era_year_raw.is_undefined() {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "era and eraYear cannot both be present"));
|
||||
}
|
||||
// 存在性:year 必填(先于 monthCode 语法);month|monthCode 至少其一。
|
||||
if year_raw.is_undefined() {
|
||||
return NativeResult::Err(crate::error::create_type_error(vm, "year is required"));
|
||||
}
|
||||
if month_raw.is_undefined() && month_code_raw.is_undefined() {
|
||||
return NativeResult::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 NativeResult::Err(native_engine_error(vm, &error)),
|
||||
};
|
||||
if !primitive.is_string() {
|
||||
return NativeResult::Err(crate::error::create_type_error(vm, "invalid monthCode"));
|
||||
}
|
||||
Some(to_string(primitive))
|
||||
} else {
|
||||
return NativeResult::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 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),
|
||||
};
|
||||
// year 转换(TypeError/RangeError)在 monthCode 语法之后。
|
||||
let year = 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))),
|
||||
};
|
||||
// monthCode 第二段适配:闰月后缀或月值越界 → RangeError。
|
||||
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 <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 NativeResult::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 NativeResult::Err(crate::error::create_range_error(vm, "invalid month"));
|
||||
}
|
||||
if f > 12.0 {
|
||||
if constrain {
|
||||
12
|
||||
} else {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "invalid month"));
|
||||
}
|
||||
} else {
|
||||
f as u32
|
||||
}
|
||||
};
|
||||
// (年, 月) 表示范围。
|
||||
if !iso_year_month_within_limits(year, month) {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "ISO year-month is out of range"));
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
/// add/subtract 的 (年, 月) 可表示性检查,按日 = 1 计算(ISODateWithinLimits)。
|
||||
/// 与 ISOYearMonthWithinLimits 的差异仅在 -271821 年:该年 4 月 1 日早于最早
|
||||
/// 可表示的日期时间,故 4 月及以前越界(5 月起在界内);+275760 年须 9 月及以前。
|
||||
fn iso_year_month_day1_within_limits(year: i64, month: i64) -> bool {
|
||||
if !(-271_821_i64..=275_760_i64).contains(&year) {
|
||||
return false;
|
||||
}
|
||||
!((year == -271_821 && month < 5) || (year == 275_760 && month > 9))
|
||||
}
|
||||
|
||||
/// `Temporal.PlainYearMonth.prototype.add/subtract(durationLike [, options])` 核心。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. receiver branding(TypeError)。
|
||||
/// 2. ToTemporalDuration(串 / bag / 实例;TypeError、RangeError 各自语义)。
|
||||
/// 3. 读 overflow(规范顺序先于后续算法校验;ISO 日历下取值不可观测,仅校验)。
|
||||
/// 4. weeks / days / 时间分量任一非零 → RangeError。
|
||||
/// 5. receiver (年, 月) 按日 1 检查可表示性 → RangeError。
|
||||
/// 6. 年、月分量折算绝对月数后相加再平衡(div/rem_euclid)。
|
||||
/// 7. 结果 (年, 月) 按日 1 检查可表示性 → RangeError。
|
||||
///
|
||||
/// # 边界与副作用
|
||||
/// - 结果参考日恒 1,日历标识取自 receiver 槽 3。
|
||||
/// - subtract 与 add 共用本函数,sign 参数取反时长分量。
|
||||
fn plain_year_month_apply_duration<H: VmHost>(vm: &mut H, args: &[u8], sign: i64) -> NativeResult {
|
||||
let ptr = native_try!(receiver_obj(vm, args));
|
||||
let obj = unsafe { &*ptr };
|
||||
native_try!(ensure_plain_year_month(vm, obj));
|
||||
let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
|
||||
let values = match duration_like_values(vm, val) {
|
||||
Ok(values) => values,
|
||||
Err(error) => return NativeResult::Err(error),
|
||||
};
|
||||
let _overflow = native_try!(temporal_overflow(vm, args));
|
||||
if values[2..].iter().any(|value| *value != 0.0) {
|
||||
return NativeResult::Err(crate::error::create_range_error(
|
||||
vm,
|
||||
"year-month cannot add weeks, days, or time units",
|
||||
));
|
||||
}
|
||||
let year = get_double_prop(obj, 0) as i64;
|
||||
let month = get_double_prop(obj, 1) as i64;
|
||||
if !iso_year_month_day1_within_limits(year, month) {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "year-month is out of range"));
|
||||
}
|
||||
let months = (values[0] as i64) * 12 + values[1] as i64;
|
||||
let absolute = year * 12 + month - 1 + sign * months;
|
||||
let result_year = absolute.div_euclid(12);
|
||||
let result_month = absolute.rem_euclid(12) + 1;
|
||||
if !iso_year_month_day1_within_limits(result_year, result_month) {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "year-month is out of range"));
|
||||
}
|
||||
make_plain_year_month(vm, result_year as i32, result_month as u32, 1, &get_calendar_id(obj, 3))
|
||||
}
|
||||
|
||||
/// `Temporal.PlainYearMonth.prototype.add(durationLike)`。
|
||||
pub fn plain_year_month_add<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
plain_year_month_apply_duration(vm, args, 1)
|
||||
}
|
||||
|
||||
/// `Temporal.PlainYearMonth.prototype.subtract(durationLike)`。
|
||||
pub fn plain_year_month_subtract<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
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.PlainYearMonth.prototype.until(other[, options])`。
|
||||
pub fn plain_year_month_until<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
plain_year_month_difference(vm, args, false)
|
||||
}
|
||||
|
||||
/// `Temporal.PlainYearMonth.prototype.since(other[, options])`。
|
||||
pub fn plain_year_month_since<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
plain_year_month_difference(vm, args, true)
|
||||
}
|
||||
|
||||
/// until/since 差值核心(DifferenceTemporalPlainYearMonth)。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. receiver branding(TypeError)。
|
||||
/// 2. other 经 ToTemporalYearMonth(串 / bag / PD / PYM 实例;day 不读)。
|
||||
/// 3. 日历相等(不等 RangeError;在 options 读取之前)。
|
||||
/// 4. options 对象 + 差值设置(单位表仅 year/month,smallestUnit 缺省 month,
|
||||
/// largestUnit 缺省/auto 为 year)。
|
||||
/// 5. (年, 月, 参考日) 三元全等 → 空 Duration(先于可表示性检查)。
|
||||
/// 6. 两端按日 1 查可表示性(ISODateWithinLimits,day 级)→ RangeError。
|
||||
/// 7. 既有差值机件(nudge 窗口端点同样带 day 级范围检查);since 整体取反。
|
||||
///
|
||||
/// # 边界与副作用
|
||||
/// - 参考日只参与第 5 步的全等判断;差值本身恒按日 1 计算。
|
||||
/// - 第 2 步的字符串/袋路径为年月级范围检查(自 ToTemporalYearMonth),
|
||||
/// day 级补查在第 6 步,两步错误类相同(RangeError),仅抛错时点不同。
|
||||
fn plain_year_month_difference<H: VmHost>(vm: &mut H, args: &[u8], since: bool) -> NativeResult {
|
||||
let ptr = native_try!(receiver_obj(vm, args));
|
||||
let obj = unsafe { &*ptr };
|
||||
native_try!(ensure_plain_year_month(vm, obj));
|
||||
let other_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
|
||||
let (oy, om, od, ocal) = native_try!(year_month_like_parts(vm, other_val, true));
|
||||
// 日历相等(ToTemporalYearMonth 之后、options 读取之前)。
|
||||
if get_calendar_id(obj, 3) != ocal {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "calendars must be equal"));
|
||||
}
|
||||
let options_value = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() };
|
||||
let settings = match parse_year_month_difference_settings(vm, options_value) {
|
||||
Ok(settings) => settings,
|
||||
Err(error) => return NativeResult::Err(error),
|
||||
};
|
||||
// (年, 月, 参考日) 三元全等 → 空 Duration(先于可表示性检查)。
|
||||
let ry = get_double_prop(obj, 0) as i128;
|
||||
let rm = get_double_prop(obj, 1) as i128;
|
||||
let rd = get_double_prop(obj, 2) as i128;
|
||||
if compare_iso_date((ry, rm, rd), (i128::from(oy), i128::from(om), i128::from(od))) == 0 {
|
||||
return make_duration(vm, [0.0; 10]);
|
||||
}
|
||||
// 两端按日 1 的可表示性检查(ISODateWithinLimits,day 级)。
|
||||
if days_from_civil(ry, rm, 1).abs() > MAX_ISO_DAY
|
||||
|| days_from_civil(i128::from(oy), i128::from(om), 1).abs() > MAX_ISO_DAY
|
||||
{
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "date is out of range"));
|
||||
}
|
||||
difference_core(vm, (ry, rm, 1), 0, (i128::from(oy), i128::from(om), 1), 0, settings, since, 1)
|
||||
}
|
||||
Reference in New Issue
Block a user