mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
feat: zdt 创建解析 zone-aware 加 offset 与区校验
This commit is contained in:
@@ -8,8 +8,8 @@ use oxide_types::value::JsValue;
|
||||
use super::common::native_try;
|
||||
use super::common::{
|
||||
canonical_time_zone, civil_from_days, days_from_civil, ensure_duration, get_double_prop, get_instant_epoch_ns,
|
||||
initialize_temporal_receiver, is_ctor_call, native_engine_error, plain_date_time_object_parts, receiver_obj,
|
||||
temporal_option_number, temporal_option_string, temporal_option_value, zoned_date_time_plain_parts,
|
||||
initialize_temporal_receiver, is_ctor_call, local_to_epoch_ns, native_engine_error, plain_date_time_object_parts,
|
||||
receiver_obj, temporal_option_number, temporal_option_string, temporal_option_value, zoned_date_time_plain_parts,
|
||||
};
|
||||
use super::difference::{
|
||||
add_date_duration, add_days_iso, compare_iso_date, date_duration_sign, date_until_iso, nudge_iso_difference,
|
||||
@@ -17,7 +17,9 @@ use super::difference::{
|
||||
};
|
||||
use super::instant::{instant_rounding_mode, InstantRoundingMode};
|
||||
use super::time_zone::zone_offset_seconds;
|
||||
use super::zoned_date_time::{parse_any_offset_seconds, valid_offset_fraction, zoned_date_time_string_parts};
|
||||
use super::zoned_date_time::{
|
||||
extract_time_zone_annotation, parse_any_offset_seconds, valid_offset_fraction, zoned_date_time_string_parts,
|
||||
};
|
||||
use super::{make_duration, parse_plain_date_time_string};
|
||||
|
||||
fn parse_duration_string(input: &str) -> Option<[f64; 10]> {
|
||||
@@ -285,16 +287,18 @@ fn duration_values(obj: &JsObject) -> [f64; 10] {
|
||||
/// 归一 options.relativeTo 为相对日期 `(y, m, d)`。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. string → parse_plain_date_time_string,失败回退 parse_plain_date_string(PDT 优先、PD 回退);失败 RangeError。
|
||||
/// 1. string 先检测 `[tz]` 注解:含注解走 zoned 解析(offset-vs-zone 校验,reject 语义);
|
||||
/// 无注解走 plain 解析(纯日期、日期时间、带偏移的 plain 串)。
|
||||
/// 2. PlainDateTime / PlainDate 对象 → 直读 prop 0/1/2。
|
||||
/// 3. ZonedDateTime → zoned_date_time_plain_parts 取本地 (y,m,d)。
|
||||
/// 4. Instant → epoch 纳秒按 UTC 分解(div_euclid(DAY_NS) → civil_from_days)。
|
||||
/// 5. 其他对象按 property bag 解析(ToTemporalDateTime,constrain)。
|
||||
/// 5. 其他对象按 property bag 解析(ToRelativeTemporalObject,constrain)。
|
||||
/// 6. undefined → None;其余原始值 → TypeError。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 调用方须已把 raw 从 options 取出(读序由调用方保证)。
|
||||
/// - 本函数只取日期分量;时间分量由调用方的 duration 分量另行加。
|
||||
/// - bag 支 offset 语法校验先于数值字段转换;offset-vs-zone 匹配校验在字段转换之后。
|
||||
fn duration_relative_to_date<H: VmHost>(
|
||||
vm: &mut H, relative_raw: JsValue,
|
||||
) -> Result<Option<(i128, i128, i128)>, JsValue> {
|
||||
@@ -303,12 +307,9 @@ fn duration_relative_to_date<H: VmHost>(
|
||||
}
|
||||
if relative_raw.is_string() {
|
||||
let text = to_string(relative_raw);
|
||||
// 先按 PlainDateTime/PlainDate 解析(纯日期、日期时间、带 offset/注解的 plain 串)。
|
||||
if let Ok((year, month, day, _time_ns)) = parse_plain_date_time_string(&text) {
|
||||
return Ok(Some((i128::from(year), i128::from(month), i128::from(day))));
|
||||
}
|
||||
// 回退支:plain 首支解析失败的串(含 Z 的串、IANA 区注解串)。
|
||||
// 含 [tz] 注解走 zoned 路径(offset-vs-zone 校验,reject 语义):
|
||||
// 按 instant 解析 + 注解时区偏移(epoch 依赖查找)反推墙历日期。
|
||||
if extract_time_zone_annotation(&text).is_some() {
|
||||
let (epoch_ns, time_zone_id, _calendar) = zoned_date_time_string_parts(vm, &text, "reject")?;
|
||||
let offset_seconds = zone_offset_seconds(&time_zone_id, epoch_ns.div_euclid(1_000_000_000) as i64)
|
||||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))?;
|
||||
@@ -316,6 +317,12 @@ fn duration_relative_to_date<H: VmHost>(
|
||||
let (year, month, day) = civil_from_days(wall_ns.div_euclid(DAY_NS));
|
||||
return Ok(Some((year, month, day)));
|
||||
}
|
||||
// 无注解走 plain 路径:纯日期、日期时间、带偏移的 plain 串(偏移不作时区标识符)。
|
||||
if let Ok((year, month, day, _time_ns)) = parse_plain_date_time_string(&text) {
|
||||
return Ok(Some((i128::from(year), i128::from(month), i128::from(day))));
|
||||
}
|
||||
return Err(crate::error::create_range_error(vm, "invalid relativeTo"));
|
||||
}
|
||||
if !relative_raw.is_object() {
|
||||
return Err(crate::error::create_type_error(vm, "invalid relativeTo"));
|
||||
}
|
||||
@@ -348,27 +355,42 @@ fn duration_relative_to_date<H: VmHost>(
|
||||
return Err(crate::error::create_type_error(vm, "invalid relativeTo"));
|
||||
}
|
||||
let time_zone_raw = temporal_option_value(vm, obj, relative_raw, "timeZone")?;
|
||||
if !time_zone_raw.is_undefined() {
|
||||
let time_zone_id = if time_zone_raw.is_undefined() {
|
||||
None
|
||||
} else {
|
||||
// 非字符串(对象/符号/null/数字)→ TypeError;字符串非合法时区 → RangeError。
|
||||
if !time_zone_raw.is_string() {
|
||||
return Err(crate::error::create_type_error(vm, "invalid time zone"));
|
||||
}
|
||||
if canonical_time_zone(&to_string(time_zone_raw)).is_none() {
|
||||
return Err(crate::error::create_range_error(vm, "invalid time zone"));
|
||||
}
|
||||
}
|
||||
let tz_input = to_string(time_zone_raw);
|
||||
Some(canonical_time_zone(&tz_input).ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))?)
|
||||
};
|
||||
let offset_raw = temporal_option_value(vm, obj, relative_raw, "offset")?;
|
||||
if !offset_raw.is_undefined() {
|
||||
// 非字符串 → TypeError;字符串格式非法(含亚秒偏移)→ RangeError。
|
||||
let bag_offset_seconds = if offset_raw.is_undefined() {
|
||||
None
|
||||
} else {
|
||||
// 非字符串 → TypeError;字符串格式非法(含亚秒偏移位数超限)→ RangeError。
|
||||
if !offset_raw.is_string() {
|
||||
return Err(crate::error::create_type_error(vm, "invalid offset"));
|
||||
}
|
||||
let offset_input = to_string(offset_raw);
|
||||
if parse_any_offset_seconds(&offset_input).is_none() || !valid_offset_fraction(&offset_input) {
|
||||
return Err(crate::error::create_range_error(vm, "invalid offset"));
|
||||
let (seconds, _has_sub_minute) = parse_any_offset_seconds(&offset_input)
|
||||
.filter(|_| valid_offset_fraction(&offset_input))
|
||||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid offset"))?;
|
||||
Some(seconds)
|
||||
};
|
||||
let (year, month, day, time_ns, _calendar) = plain_date_time_object_parts(vm, relative_raw, obj, true, false)?;
|
||||
// offset-vs-zone 校验(对象支精确一致、reject 语义):offset 与 timeZone 同时在场时,
|
||||
// 候选 epoch 点 zone 偏移须与输入偏移秒级相等。
|
||||
if let (Some(offset), Some(tz_id)) = (bag_offset_seconds, time_zone_id.as_deref()) {
|
||||
let candidate_ns = local_to_epoch_ns(year, month, day, time_ns, 0)
|
||||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid date-time"))?;
|
||||
let zone_offset = zone_offset_seconds(tz_id, candidate_ns.div_euclid(1_000_000_000) as i64)
|
||||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))?;
|
||||
if zone_offset != offset {
|
||||
return Err(crate::error::create_range_error(vm, "offset and time zone disagree"));
|
||||
}
|
||||
}
|
||||
let (year, month, day, _time_ns, _calendar) = plain_date_time_object_parts(vm, relative_raw, obj, true, false)?;
|
||||
Ok(Some((i128::from(year), i128::from(month), i128::from(day))))
|
||||
}
|
||||
|
||||
|
||||
@@ -176,11 +176,14 @@ fn canonical_time_zone_4_digit_offset() {
|
||||
|
||||
#[test]
|
||||
fn offset_seconds_representation_sub_minute() {
|
||||
// i64 秒表示保留亚分钟偏移(±HH:MM:SS 秒位),旧分钟表示丢弃秒位。
|
||||
assert_eq!(parse_any_offset_seconds("+00:44:30"), Some(2670));
|
||||
assert_eq!(parse_any_offset_seconds("-00:44:30"), Some(-2670));
|
||||
assert_eq!(parse_any_offset_seconds("+01:00:00"), Some(3600));
|
||||
assert_eq!(parse_any_offset_seconds("-23:59:59"), Some(-(23 * 3600 + 59 * 60 + 59)));
|
||||
// i64 秒表示保留亚分钟偏移(±HH:MM:SS 秒位),旧分钟表示丢弃秒位;
|
||||
// 第二分量标记秒分量在场(亚分钟精度,比较口径切精确一致)。
|
||||
assert_eq!(parse_any_offset_seconds("+00:44:30"), Some((2670, true)));
|
||||
assert_eq!(parse_any_offset_seconds("-00:44:30"), Some((-2670, true)));
|
||||
assert_eq!(parse_any_offset_seconds("+01:00:00"), Some((3600, true)));
|
||||
assert_eq!(parse_any_offset_seconds("-23:59:59"), Some((-(23 * 3600 + 59 * 60 + 59), true)));
|
||||
assert_eq!(parse_any_offset_seconds("+00:45"), Some((2700, false)));
|
||||
assert_eq!(parse_any_offset_seconds("-00:45:00"), Some((-2700, true)));
|
||||
// 固定偏移秒值与分钟值 ×60 一致(±HH:MM 六字符形)。
|
||||
assert_eq!(canonical_time_zone("+01:00"), Some("+01:00".to_string()));
|
||||
assert_eq!(canonical_time_zone("-05:30"), Some("-05:30".to_string()));
|
||||
@@ -433,3 +436,68 @@ fn sub_minute_offset_getter_branch() {
|
||||
Some("1969-12-31T23:15:30-00:44:30".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_offset_to_minutes_half_expand() {
|
||||
// 半值远离零:正负两侧对称,整分钟偏移不变;半分钟边界(±30 秒)向上舍入。
|
||||
assert_eq!(round_offset_to_minutes(2700), 2700);
|
||||
assert_eq!(round_offset_to_minutes(-2700), -2700);
|
||||
assert_eq!(round_offset_to_minutes(2670), 2700); // +00:44:30 → +00:45
|
||||
assert_eq!(round_offset_to_minutes(-2670), -2700); // -00:44:30 → -00:45
|
||||
assert_eq!(round_offset_to_minutes(2640), 2640);
|
||||
assert_eq!(round_offset_to_minutes(2669), 2640); // 差 1 秒不到半分钟,仍舍入到 +00:44
|
||||
assert_eq!(round_offset_to_minutes(-2669), -2640);
|
||||
assert_eq!(round_offset_to_minutes(0), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_matches_exact_and_minutes() {
|
||||
// 精确口径:秒级相等才成立。
|
||||
assert!(offset_matches(2700, 2700, true));
|
||||
assert!(!offset_matches(2670, 2700, true));
|
||||
assert!(!offset_matches(2670, 2669, true));
|
||||
// 分钟口径:zone 偏移舍入到分钟后与输入相等;精确相等是其子集。
|
||||
assert!(offset_matches(2700, 2700, false));
|
||||
assert!(offset_matches(2670, 2700, false)); // zone 亚分钟 -00:44:30 舍入后接受分钟输入
|
||||
assert!(offset_matches(-2670, -2700, false));
|
||||
assert!(!offset_matches(2670, 2640, false)); // 舍入到 2700,与 2640 不符
|
||||
assert!(!offset_matches(0, 19800, false)); // UTC 区对 +05:30 输入
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zone_aware_string_offset_vs_zone() {
|
||||
// zone-aware 创建解析(纯函数层):注解经 canonical_time_zone 得规范名,
|
||||
// 候选 epoch 点 zone 偏移经 zone_offset_seconds 读取,offset-vs-zone 判定走 offset_matches。
|
||||
// 固定偏移区退化情形:zone 偏移恒为常量,分钟输入精确一致。
|
||||
let tz = canonical_time_zone("+05:30").unwrap();
|
||||
let zone = zone_offset_seconds(&tz, 0).unwrap();
|
||||
let (offset, has_sub) = parse_any_offset_seconds("+05:30").unwrap();
|
||||
assert_eq!(offset, 19800);
|
||||
assert!(!has_sub);
|
||||
assert!(offset_matches(zone, offset, has_sub));
|
||||
// 亚分钟输入走精确口径:zone 偏移 -00:44:30 与输入 -00:44:30 秒级相等。
|
||||
let (offset, has_sub) = parse_any_offset_seconds("-00:44:30").unwrap();
|
||||
assert!(has_sub);
|
||||
assert_eq!(offset, -2670);
|
||||
assert!(offset_matches(-2670, offset, has_sub));
|
||||
// IANA 区亚分钟历史偏移:Monrovia 1970 年 zone 偏移 -00:44:30,
|
||||
// 分钟输入 -00:45 经舍入口径接受,亚分钟输入 -00:44:30 精确一致、-00:44:31 精确口径拒绝。
|
||||
let zone = zone_offset_seconds("Africa/Monrovia", 0).unwrap();
|
||||
assert_eq!(zone, -2670);
|
||||
let (minute_input, has_sub) = parse_any_offset_seconds("-00:45").unwrap();
|
||||
assert!(!has_sub);
|
||||
assert!(offset_matches(zone, minute_input, has_sub));
|
||||
let (sub_input, has_sub) = parse_any_offset_seconds("-00:44:30").unwrap();
|
||||
assert!(has_sub);
|
||||
assert!(offset_matches(zone, sub_input, has_sub));
|
||||
let (sub_input, has_sub) = parse_any_offset_seconds("-00:44:31").unwrap();
|
||||
assert!(has_sub);
|
||||
assert!(!offset_matches(zone, sub_input, has_sub));
|
||||
// 注解提取:IANA 区注解命中,key 注解(u-ca)跳过,亚分钟偏移注解拒作时区标识符。
|
||||
assert_eq!(
|
||||
extract_time_zone_annotation("2000-01-01T00:00[Africa/Monrovia]"),
|
||||
Some(("Africa/Monrovia".to_string(), false))
|
||||
);
|
||||
assert_eq!(extract_time_zone_annotation("2000-01-01T00:00[u-ca=iso8601]"), None);
|
||||
assert_eq!(extract_time_zone_annotation("2000-01-01T00:00[-00:44:30]"), None);
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@ fn zoned_date_time_options<H: VmHost>(
|
||||
|
||||
/// 从剥注解后的 Instant 主体提取字符串内数值偏移秒数(±HH / ±HHMM / ±HH:MM / 亚秒形式)。
|
||||
/// Z/z 结尾视为 0 秒;无偏移或偏移不可提取返回 None。
|
||||
fn extract_string_offset_seconds(body: &str) -> Option<i64> {
|
||||
fn extract_string_offset_seconds(body: &str) -> Option<(i64, bool)> {
|
||||
let time_start = body.find(['T', 't', ' '])?;
|
||||
let time = &body[time_start + 1..];
|
||||
let offset_start = time
|
||||
@@ -221,8 +221,9 @@ fn extract_string_offset_seconds(body: &str) -> Option<i64> {
|
||||
parse_any_offset_seconds(&time[offset_start..])
|
||||
}
|
||||
|
||||
/// 解析数值偏移串(±HH / ±HHMM / ±HH:MM / ±HHMMSS / ±HH:MM:SS,可带小数秒)为秒数。
|
||||
pub(crate) fn parse_any_offset_seconds(value: &str) -> Option<i64> {
|
||||
/// 解析数值偏移串(±HH / ±HHMM / ±HH:MM / ±HHMMSS / ±HH:MM:SS,可带小数秒)。
|
||||
/// 返回 (秒数, 是否含秒分量):秒分量在场即亚分钟精度,比较口径为精确一致。
|
||||
pub(crate) fn parse_any_offset_seconds(value: &str) -> Option<(i64, bool)> {
|
||||
let bytes = value.as_bytes();
|
||||
let sign = match bytes.first() {
|
||||
Some(b'+') => 1_i64,
|
||||
@@ -240,13 +241,13 @@ pub(crate) fn parse_any_offset_seconds(value: &str) -> Option<i64> {
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let second = if colon_format && bytes.get(cursor) == Some(&b':') {
|
||||
let (second, has_seconds) = if colon_format && bytes.get(cursor) == Some(&b':') {
|
||||
cursor += 1;
|
||||
parse_digits(bytes, &mut cursor, 2)?
|
||||
(parse_digits(bytes, &mut cursor, 2)?, true)
|
||||
} else if !colon_format && matches!(bytes.get(cursor), Some(byte) if byte.is_ascii_digit()) {
|
||||
parse_digits(bytes, &mut cursor, 2)?
|
||||
(parse_digits(bytes, &mut cursor, 2)?, true)
|
||||
} else {
|
||||
0
|
||||
(0, false)
|
||||
};
|
||||
if matches!(bytes.get(cursor), Some(b'.' | b',')) {
|
||||
cursor += 1;
|
||||
@@ -257,7 +258,28 @@ pub(crate) fn parse_any_offset_seconds(value: &str) -> Option<i64> {
|
||||
if cursor != bytes.len() || hour > 23 || minute > 59 || second > 59 {
|
||||
return None;
|
||||
}
|
||||
Some((hour * 3600 + minute * 60 + second) as i64 * sign)
|
||||
Some(((hour * 3600 + minute * 60 + second) as i64 * sign, has_seconds))
|
||||
}
|
||||
|
||||
/// 偏移舍入到分钟(half-expand:半值远离零),用于分钟精度比较口径。
|
||||
/// 偏移量域 ±24h,unsigned_abs 无溢出面。
|
||||
pub(crate) fn round_offset_to_minutes(seconds: i64) -> i64 {
|
||||
let sign = seconds.signum();
|
||||
let magnitude = seconds.unsigned_abs() + 30;
|
||||
(magnitude / 60 * 60) as i64 * sign
|
||||
}
|
||||
|
||||
/// 输入偏移与 zone 偏移的一致性判定。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. 精确口径(输入含秒分量 / 对象支):秒级相等。
|
||||
/// 2. 分钟口径(字符串支默认):zone 偏移舍入到分钟后与输入相等。
|
||||
pub(crate) fn offset_matches(zone_offset_seconds: i64, input_offset_seconds: i64, match_exactly: bool) -> bool {
|
||||
if match_exactly {
|
||||
zone_offset_seconds == input_offset_seconds
|
||||
} else {
|
||||
round_offset_to_minutes(zone_offset_seconds) == input_offset_seconds
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 ZDT 对象 / ISO 字符串 / property bag 解析 (epoch_ns, time_zone_id, calendar_id) 三元组。
|
||||
@@ -300,7 +322,8 @@ fn zoned_date_time_like_epoch_ns<H: VmHost>(
|
||||
/// 2. 主体剥注解后按 Z / 数值偏移 / 无偏移分路:Z 定 exact time,有偏移经 parse_instant_string
|
||||
/// 反推墙钟,无偏移直接解析墙钟。
|
||||
/// 3. 时区偏移按候选 epoch(墙钟按 UTC 解释)单次查找。
|
||||
/// 4. 按 offset 选项 use/ignore/prefer/reject 决策 epoch,并做 Instant 范围校验。
|
||||
/// 4. 按 offset 选项 use/ignore/prefer/reject 决策 epoch:偏移串含秒分量走精确一致,
|
||||
/// 分钟精度走 zone 偏移舍入到分钟后一致;并做 Instant 范围校验。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 字符串缺时区注解或注解无法规范化抛 RangeError;主体非法抛 RangeError。
|
||||
@@ -317,15 +340,18 @@ pub(crate) fn zoned_date_time_string_parts<H: VmHost>(
|
||||
return Err(crate::error::create_range_error(vm, "invalid ISO 8601 date-time"));
|
||||
};
|
||||
let has_utc_designator = body.ends_with(['Z', 'z']);
|
||||
let string_offset_seconds = if has_utc_designator { Some(0) } else { extract_string_offset_seconds(body) };
|
||||
let string_offset_seconds = if has_utc_designator {
|
||||
Some((0, false))
|
||||
} else {
|
||||
extract_string_offset_seconds(body)
|
||||
};
|
||||
|
||||
// 有偏移/Z 时经 parse_instant_string 得 epoch 并反推墙钟;否则直接解析墙钟。
|
||||
const DAY_NS: i128 = 86_400_000_000_000;
|
||||
let (epoch_from_string, wall_parts) = if string_offset_seconds.is_some() {
|
||||
let (epoch_from_string, wall_parts) = if let Some((offset, _has_sub_minute)) = string_offset_seconds {
|
||||
let epoch_ns = parse_instant_string(input)
|
||||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid ISO 8601 date-time"))?;
|
||||
let offset_seconds = string_offset_seconds.unwrap_or(0);
|
||||
let wall_ns = epoch_ns + i128::from(offset_seconds) * 1_000_000_000;
|
||||
let wall_ns = epoch_ns + i128::from(offset) * 1_000_000_000;
|
||||
let days = wall_ns.div_euclid(DAY_NS);
|
||||
let (year, month, day) = civil_from_days(days);
|
||||
(Some(epoch_ns), (year as i32, month as u32, day as u32, wall_ns.rem_euclid(DAY_NS) as f64))
|
||||
@@ -349,24 +375,25 @@ pub(crate) fn zoned_date_time_string_parts<H: VmHost>(
|
||||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone"))?;
|
||||
|
||||
// 按 offsetBehaviour 决策:Z → exact(墙钟 epoch);无偏移 → wall(墙钟 + 时区偏移);
|
||||
// 有偏移 → 按 offset 选项在字符串偏移与时区偏移之间选择。
|
||||
// 有偏移 → 按 offset 选项在字符串偏移与时区偏移之间选择(偏移串含秒分量走精确口径,
|
||||
// 分钟精度走舍入到分钟口径)。
|
||||
let epoch_ns = if has_utc_designator {
|
||||
epoch_from_string
|
||||
} else {
|
||||
match string_offset_seconds {
|
||||
None => wall_epoch(time_zone_offset),
|
||||
Some(offset) => match offset_mode {
|
||||
Some((offset, has_sub_minute)) => match offset_mode {
|
||||
"use" => epoch_from_string,
|
||||
"ignore" => wall_epoch(time_zone_offset),
|
||||
"prefer" => {
|
||||
if offset == time_zone_offset {
|
||||
if offset_matches(time_zone_offset, offset, has_sub_minute) {
|
||||
epoch_from_string
|
||||
} else {
|
||||
wall_epoch(time_zone_offset)
|
||||
}
|
||||
}
|
||||
"reject" => {
|
||||
if offset == time_zone_offset {
|
||||
if offset_matches(time_zone_offset, offset, has_sub_minute) {
|
||||
epoch_from_string
|
||||
} else {
|
||||
return Err(crate::error::create_range_error(vm, "offset and time zone disagree"));
|
||||
@@ -900,7 +927,7 @@ fn zoned_date_time_with_fields<H: VmHost>(
|
||||
None
|
||||
} else {
|
||||
let offset_input = temporal_string_strict(vm, offset_raw)?;
|
||||
let seconds = parse_any_offset_seconds(&offset_input)
|
||||
let (seconds, _has_sub_minute) = parse_any_offset_seconds(&offset_input)
|
||||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid offset"))?;
|
||||
if !valid_offset_fraction(&offset_input) {
|
||||
return Err(crate::error::create_range_error(vm, "invalid offset"));
|
||||
@@ -933,7 +960,8 @@ fn zoned_date_time_with_fields<H: VmHost>(
|
||||
/// 2. receiver 本地分量与偏移;zoned_date_time_with_fields 读字段合并。
|
||||
/// 3. options:disambiguation → offset(默认 prefer)→ overflow(逐项 Get/校验)。
|
||||
/// 4. monthCode 闰月/超界/冲突校验 + constrain/reject 钳制 + PlainDateTime 范围校验。
|
||||
/// 5. offset 选项决策 → local_to_epoch_ns → Instant 范围校验 → make_zoned_date_time。
|
||||
/// 5. 候选 epoch 点 zone 偏移与合并偏移按 offset 选项决策 → local_to_epoch_ns
|
||||
/// → Instant 范围校验 → make_zoned_date_time。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 字段读取先于 options 解析:即使 options 本身类型不合法,也先读完 partial 字段,
|
||||
@@ -1045,21 +1073,29 @@ pub fn zoned_date_time_with<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "date-time out of range"));
|
||||
}
|
||||
|
||||
// InterpretISODateTimeOffset(固定偏移简化):按 offset 选项在 bag 偏移与时区偏移间选择。
|
||||
let epoch_ns = match (merged.bag_offset, offset_mode.as_str()) {
|
||||
(None, _) => local_to_epoch_ns(year, month, day, total_ns, offset_seconds),
|
||||
(Some(offset), "use") => local_to_epoch_ns(year, month, day, total_ns, offset),
|
||||
(Some(_), "ignore") => local_to_epoch_ns(year, month, day, total_ns, offset_seconds),
|
||||
(Some(offset), "prefer") => {
|
||||
if offset == offset_seconds {
|
||||
local_to_epoch_ns(year, month, day, total_ns, offset)
|
||||
// InterpretISODateTimeOffset:时区偏移是 epoch 的函数,候选 epoch 取墙钟按 UTC 解释的
|
||||
// 单次查找近似,zone 偏移作 offset 选项判据与墙钟回退。
|
||||
let candidate_ns = native_try!(local_to_epoch_ns(year, month, day, total_ns, 0)
|
||||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid date-time")));
|
||||
let zone_offset_at_candidate =
|
||||
native_try!(zone_offset_seconds(&time_zone_id, candidate_ns.div_euclid(1_000_000_000) as i64)
|
||||
.ok_or_else(|| crate::error::create_range_error(vm, "invalid time zone")));
|
||||
// 输入偏移为合并偏移(partial 的 offset 优先,缺省取 receiver 偏移);
|
||||
// offset 选项在输入偏移与候选 epoch 点 zone 偏移之间决策。
|
||||
let input_offset = merged.bag_offset.unwrap_or(offset_seconds);
|
||||
let epoch_ns = match offset_mode.as_str() {
|
||||
"use" => local_to_epoch_ns(year, month, day, total_ns, input_offset),
|
||||
"ignore" => local_to_epoch_ns(year, month, day, total_ns, zone_offset_at_candidate),
|
||||
"prefer" => {
|
||||
if input_offset == zone_offset_at_candidate {
|
||||
local_to_epoch_ns(year, month, day, total_ns, input_offset)
|
||||
} else {
|
||||
local_to_epoch_ns(year, month, day, total_ns, offset_seconds)
|
||||
local_to_epoch_ns(year, month, day, total_ns, zone_offset_at_candidate)
|
||||
}
|
||||
}
|
||||
(Some(offset), "reject") => {
|
||||
if offset == offset_seconds {
|
||||
local_to_epoch_ns(year, month, day, total_ns, offset)
|
||||
"reject" => {
|
||||
if input_offset == zone_offset_at_candidate {
|
||||
local_to_epoch_ns(year, month, day, total_ns, input_offset)
|
||||
} else {
|
||||
return NativeResult::Err(crate::error::create_range_error(vm, "offset and time zone disagree"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user