refactor: temporal 与 json 长参数收进结构体

This commit is contained in:
2026-10-12 18:21:31 +00:00
parent 92fd288b07
commit 4345d14a0b
13 changed files with 603 additions and 313 deletions
+123 -102
View File
@@ -977,46 +977,79 @@ pub fn json_stringify<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let mut visited = HashSet::new();
let mut output: Vec<u16> = Vec::new();
let indent_level: usize = 0;
match jsvalue_to_json(
vm,
value,
&mut visited,
&mut output,
let ctx = StringifyCtx {
visited: &mut visited,
out: &mut output,
replacer_fn,
replacer_whitelist.as_ref(),
&space,
indent_level,
) {
replacer_whitelist: replacer_whitelist.as_ref(),
space: &space,
indent_level: 0,
};
match jsvalue_to_json(vm, value, ctx) {
Ok(()) => NativeResult::Ok(vm.new_string_units_owned(output)),
Err(exc) => NativeResult::Err(exc),
}
}
/// JSON 序列化上下文:环检表、输出流、replacer 与缩进状态。
/// 递归传递时经 [`StringifyCtx::child`] 派生下一缩进层。
struct StringifyCtx<'data, 'w> {
visited: &'w mut HashSet<*const JsObject>,
out: &'w mut Vec<u16>,
replacer_fn: Option<JsValue>,
replacer_whitelist: Option<&'data Vec<String>>,
space: &'data [u16],
indent_level: usize,
}
impl<'data, 'w> StringifyCtx<'data, 'w> {
/// 派生同缩进层上下文:共享同一环检表与输出流,indent_level 不变
/// (值位序列化入口传给对象/数组展开时用)。
fn same(&mut self) -> StringifyCtx<'data, '_> {
StringifyCtx {
visited: &mut *self.visited,
out: &mut *self.out,
replacer_fn: self.replacer_fn,
replacer_whitelist: self.replacer_whitelist,
space: self.space,
indent_level: self.indent_level,
}
}
/// 派生下一缩进层上下文:共享同一环检表与输出流,indent_level 加一
/// (对象/数组展开内递归回值位序列化时用)。
fn child(&mut self) -> StringifyCtx<'data, '_> {
StringifyCtx {
visited: &mut *self.visited,
out: &mut *self.out,
replacer_fn: self.replacer_fn,
replacer_whitelist: self.replacer_whitelist,
space: self.space,
indent_level: self.indent_level + 1,
}
}
}
/// 值位序列化(SerializeValue 及对象/数组展开)。toJSON 钩子不在本入口应用——
/// 唯一应用点在调用方(顶层 `json_stringify` 与 `stringify_object`/`stringify_array`
/// 的属性循环内,Get 之后、replacer 之前),保证每值位恰一次且与 replacer 顺序
/// 符合规范。`Err` 携带的原始异常值(含环检 TypeError)原样上抛。
#[allow(clippy::too_many_arguments)]
fn jsvalue_to_json<H: VmHost>(
vm: &mut H, val: JsValue, visited: &mut HashSet<*const JsObject>, out: &mut Vec<u16>, replacer_fn: Option<JsValue>,
replacer_whitelist: Option<&Vec<String>>, space: &[u16], indent_level: usize,
) -> Result<(), JsValue> {
fn jsvalue_to_json<H: VmHost>(vm: &mut H, val: JsValue, mut ctx: StringifyCtx<'_, '_>) -> Result<(), JsValue> {
if val.is_null() {
push_ascii(out, "null");
push_ascii(ctx.out, "null");
} else if val.is_undefined() {
} else if val.is_symbol() {
push_ascii(out, "null");
push_ascii(ctx.out, "null");
} else if val.is_bool() {
push_ascii(out, if val.as_bool() { "true" } else { "false" });
push_ascii(ctx.out, if val.as_bool() { "true" } else { "false" });
} else if val.is_int() {
push_ascii(out, &val.as_int().to_string());
push_ascii(ctx.out, &val.as_int().to_string());
} else if val.is_double() {
let n = val.as_double();
if !n.is_finite() {
push_ascii(out, "null");
push_ascii(ctx.out, "null");
} else {
push_number(out, n);
push_number(ctx.out, n);
}
} else if val.is_bigint() {
// SerializeJSONProperty 步 10:BigInt 无 JSON 文本形态,抛 TypeError。
@@ -1024,11 +1057,11 @@ fn jsvalue_to_json<H: VmHost>(
} else if val.is_string() {
// SAFETY: val 已确认是字符串值;单元视图按码单元流序列化(见 stringify_string_units)。
let units = unsafe { (*val.as_string_ptr()).units() };
stringify_string_units(&units, out);
stringify_string_units(&units, ctx.out);
} else if val.is_object() {
let obj_ptr = val.as_js_object_ptr();
if obj_ptr.is_null() {
push_ascii(out, "null");
push_ascii(ctx.out, "null");
return Ok(());
}
@@ -1040,12 +1073,12 @@ fn jsvalue_to_json<H: VmHost>(
let raw_val = obj.get_prop_at(0);
// SAFETY: rawJSON 属性构造期即写入字符串值。
let raw_text = unsafe { (*raw_val.as_string_ptr()).to_owned_string() };
push_ascii(out, &raw_text);
push_ascii(ctx.out, &raw_text);
return Ok(());
}
}
if !visited.insert(obj_ptr as *const JsObject) {
if !ctx.visited.insert(obj_ptr as *const JsObject) {
return Err(crate::error::create_type_error(vm, "Converting circular structure to JSON"));
}
@@ -1058,23 +1091,23 @@ fn jsvalue_to_json<H: VmHost>(
let exc = vm
.take_uncaught_value()
.unwrap_or_else(|| crate::error::create_type_error(vm, &msg));
visited.remove(&(obj_ptr as *const JsObject));
ctx.visited.remove(&(obj_ptr as *const JsObject));
return Err(exc);
}
};
if n.is_finite() {
push_number(out, n);
push_number(ctx.out, n);
} else {
push_ascii(out, "null");
push_ascii(ctx.out, "null");
}
visited.remove(&(obj_ptr as *const JsObject));
ctx.visited.remove(&(obj_ptr as *const JsObject));
return Ok(());
}
// 步 4b:装箱 Boolean → [[BooleanData]]。
if obj.is_boolean_obj() {
let b = obj.boxed_value();
push_ascii(out, if b.is_bool() && b.as_bool() { "true" } else { "false" });
visited.remove(&(obj_ptr as *const JsObject));
push_ascii(ctx.out, if b.is_bool() && b.as_bool() { "true" } else { "false" });
ctx.visited.remove(&(obj_ptr as *const JsObject));
return Ok(());
}
// 步 4d:装箱 BigInt 无条件解包 [[BigIntData]],后续步 10 抛 TypeError。
@@ -1090,21 +1123,21 @@ fn jsvalue_to_json<H: VmHost>(
let exc = vm
.take_uncaught_value()
.unwrap_or_else(|| crate::error::create_type_error(vm, &msg));
visited.remove(&(obj_ptr as *const JsObject));
ctx.visited.remove(&(obj_ptr as *const JsObject));
return Err(exc);
}
};
let units = vm.string_units(s);
stringify_string_units(&units, out);
stringify_string_units(&units, ctx.out);
} else if obj.is_typed_array_obj() {
stringify_typed_array(vm, obj, visited, out, replacer_fn, replacer_whitelist, space, indent_level)?;
stringify_typed_array(vm, obj, ctx.same())?;
} else if obj.is_array() {
stringify_array(vm, obj, visited, out, replacer_fn, replacer_whitelist, space, indent_level)?;
stringify_array(vm, obj, ctx.same())?;
} else {
stringify_object(vm, obj, visited, out, replacer_fn, replacer_whitelist, space, indent_level)?;
stringify_object(vm, obj, ctx.same())?;
}
visited.remove(&(obj_ptr as *const JsObject));
ctx.visited.remove(&(obj_ptr as *const JsObject));
}
Ok(())
}
@@ -1153,17 +1186,13 @@ fn stringify_string_units(units: &[u16], out: &mut Vec<u16>) {
push_ascii(out, "\"");
}
#[allow(clippy::too_many_arguments)]
fn stringify_object<H: VmHost>(
vm: &mut H, obj: &JsObject, visited: &mut HashSet<*const JsObject>, out: &mut Vec<u16>,
replacer_fn: Option<JsValue>, replacer_whitelist: Option<&Vec<String>>, space: &[u16], indent_level: usize,
) -> Result<(), JsValue> {
let has_space = !space.is_empty();
push_ascii(out, "{");
fn stringify_object<H: VmHost>(vm: &mut H, obj: &JsObject, mut ctx: StringifyCtx<'_, '_>) -> Result<(), JsValue> {
let has_space = !ctx.space.is_empty();
push_ascii(ctx.out, "{");
// 键集:白名单给定时 K = P(列表序,不限自身可枚举,值读走 Get);
// 无白名单走自身可枚举序(EnumerableOwnPropertyNames)。
let entries: Vec<(Vec<u16>, u32, Option<u32>)> = if let Some(whitelist) = replacer_whitelist {
let entries: Vec<(Vec<u16>, u32, Option<u32>)> = if let Some(whitelist) = ctx.replacer_whitelist {
whitelist
.iter()
.map(|name| {
@@ -1198,7 +1227,7 @@ fn stringify_object<H: VmHost>(
let val = call_to_json(vm, val, &units)?;
// replacer 函数回调。
let val = if let Some(replacer) = replacer_fn {
let val = if let Some(replacer) = ctx.replacer_fn {
let key_val = vm.new_string_units_owned(units.clone());
match vm.call_function_sync(replacer, obj_val, &[key_val, val]) {
Ok(v) => {
@@ -1227,55 +1256,51 @@ fn stringify_object<H: VmHost>(
}
if !first {
push_ascii(out, ",");
push_ascii(ctx.out, ",");
}
first = false;
if has_space {
push_ascii(out, "\n");
for _ in 0..indent_level + 1 {
out.extend_from_slice(space);
push_ascii(ctx.out, "\n");
for _ in 0..ctx.indent_level + 1 {
ctx.out.extend_from_slice(ctx.space);
}
stringify_string_units(&units, out);
push_ascii(out, ":");
push_ascii(out, " ");
stringify_string_units(&units, ctx.out);
push_ascii(ctx.out, ":");
push_ascii(ctx.out, " ");
} else {
stringify_string_units(&units, out);
push_ascii(out, ":");
stringify_string_units(&units, ctx.out);
push_ascii(ctx.out, ":");
}
jsvalue_to_json(vm, val, visited, out, replacer_fn, replacer_whitelist, space, indent_level + 1)?;
jsvalue_to_json(vm, val, ctx.child())?;
}
if has_space && !first {
push_ascii(out, "\n");
for _ in 0..indent_level {
out.extend_from_slice(space);
push_ascii(ctx.out, "\n");
for _ in 0..ctx.indent_level {
ctx.out.extend_from_slice(ctx.space);
}
}
push_ascii(out, "}");
push_ascii(ctx.out, "}");
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn stringify_array<H: VmHost>(
vm: &mut H, obj: &JsObject, visited: &mut HashSet<*const JsObject>, out: &mut Vec<u16>,
replacer_fn: Option<JsValue>, replacer_whitelist: Option<&Vec<String>>, space: &[u16], indent_level: usize,
) -> Result<(), JsValue> {
let has_space = !space.is_empty();
push_ascii(out, "[");
fn stringify_array<H: VmHost>(vm: &mut H, obj: &JsObject, mut ctx: StringifyCtx<'_, '_>) -> Result<(), JsValue> {
let has_space = !ctx.space.is_empty();
push_ascii(ctx.out, "[");
let obj_val = JsValue::from_js_object(obj as *const JsObject as *mut JsObject);
let len = obj.prop_count() as usize;
for i in 0..len {
if i > 0 {
push_ascii(out, ",");
push_ascii(ctx.out, ",");
}
if has_space {
push_ascii(out, "\n");
for _ in 0..indent_level + 1 {
out.extend_from_slice(space);
push_ascii(ctx.out, "\n");
for _ in 0..ctx.indent_level + 1 {
ctx.out.extend_from_slice(ctx.space);
}
}
@@ -1287,12 +1312,12 @@ fn stringify_array<H: VmHost>(
let val = call_to_json(vm, val, &index_units)?;
// replacer 函数回调。
let val = if let Some(replacer) = replacer_fn {
let val = if let Some(replacer) = ctx.replacer_fn {
let key_val = vm.new_string(&index_str);
match vm.call_function_sync(replacer, obj_val, &[key_val, val]) {
Ok(v) => {
if v.is_undefined() {
push_ascii(out, "null");
push_ascii(ctx.out, "null");
continue;
}
v
@@ -1313,39 +1338,35 @@ fn stringify_array<H: VmHost>(
!ptr.is_null() && unsafe { (*ptr).is_function() }
};
if is_function || val.is_undefined() {
push_ascii(out, "null");
push_ascii(ctx.out, "null");
} else {
jsvalue_to_json(vm, val, visited, out, replacer_fn, replacer_whitelist, space, indent_level + 1)?;
jsvalue_to_json(vm, val, ctx.child())?;
}
}
if has_space && len > 0 {
push_ascii(out, "\n");
for _ in 0..indent_level {
out.extend_from_slice(space);
push_ascii(ctx.out, "\n");
for _ in 0..ctx.indent_level {
ctx.out.extend_from_slice(ctx.space);
}
}
push_ascii(out, "]");
push_ascii(ctx.out, "]");
Ok(())
}
/// TypedArray 序列化:整数索引是可枚举自身属性(元素存于原生载荷,不在形状链),
/// 逐元素按对象臂规范序 Get → toJSON → replacer 处理,输出形态与普通对象一致。
#[allow(clippy::too_many_arguments)]
fn stringify_typed_array<H: VmHost>(
vm: &mut H, obj: &JsObject, visited: &mut HashSet<*const JsObject>, out: &mut Vec<u16>,
replacer_fn: Option<JsValue>, replacer_whitelist: Option<&Vec<String>>, space: &[u16], indent_level: usize,
) -> Result<(), JsValue> {
let has_space = !space.is_empty();
fn stringify_typed_array<H: VmHost>(vm: &mut H, obj: &JsObject, mut ctx: StringifyCtx<'_, '_>) -> Result<(), JsValue> {
let has_space = !ctx.space.is_empty();
let this_val = JsValue::from_js_object(obj as *const JsObject as *mut JsObject);
let view = crate::typed_array::get_typed_array_data(vm, this_val)?;
// live 口径:buffer 收缩/detach 后按当前 live 长度枚举(detached 空对象形态)。
let len = crate::typed_array::ta_live_length(view);
push_ascii(out, "{");
push_ascii(ctx.out, "{");
// 键集:白名单给定时 K = P(列表序,整数索引名读元素、越界名跳过、
// 非索引名经 Get 读命名键如 length);无白名单走升序元素序。
let entries: Vec<(Vec<u16>, JsValue)> = if let Some(whitelist) = replacer_whitelist {
let entries: Vec<(Vec<u16>, JsValue)> = if let Some(whitelist) = ctx.replacer_whitelist {
let mut list: Vec<(Vec<u16>, JsValue)> = Vec::new();
for name in whitelist {
let si = vm.string_key_si(name);
@@ -1377,7 +1398,7 @@ fn stringify_typed_array<H: VmHost>(
let val = call_to_json(vm, val, &index_units)?;
// replacer 函数回调。
let val = if let Some(replacer) = replacer_fn {
let val = if let Some(replacer) = ctx.replacer_fn {
let key_val = vm.new_string_units_owned(index_units.clone());
match vm.call_function_sync(replacer, this_val, &[key_val, val]) {
Ok(v) => {
@@ -1406,32 +1427,32 @@ fn stringify_typed_array<H: VmHost>(
}
if !first {
push_ascii(out, ",");
push_ascii(ctx.out, ",");
}
first = false;
if has_space {
push_ascii(out, "\n");
for _ in 0..indent_level + 1 {
out.extend_from_slice(space);
push_ascii(ctx.out, "\n");
for _ in 0..ctx.indent_level + 1 {
ctx.out.extend_from_slice(ctx.space);
}
stringify_string_units(&index_units, out);
push_ascii(out, ":");
push_ascii(out, " ");
stringify_string_units(&index_units, ctx.out);
push_ascii(ctx.out, ":");
push_ascii(ctx.out, " ");
} else {
stringify_string_units(&index_units, out);
push_ascii(out, ":");
stringify_string_units(&index_units, ctx.out);
push_ascii(ctx.out, ":");
}
jsvalue_to_json(vm, val, visited, out, replacer_fn, replacer_whitelist, space, indent_level + 1)?;
jsvalue_to_json(vm, val, ctx.child())?;
}
if has_space && !first {
push_ascii(out, "\n");
for _ in 0..indent_level {
out.extend_from_slice(space);
push_ascii(ctx.out, "\n");
for _ in 0..ctx.indent_level {
ctx.out.extend_from_slice(ctx.space);
}
}
push_ascii(out, "}");
push_ascii(ctx.out, "}");
Ok(())
}
+40 -18
View File
@@ -26,6 +26,14 @@ pub enum LastChunkHandling {
StopBeforePartial,
}
/// FromBase64 解码策略:字母表、尾块处理与长度上限三项。
#[derive(Debug, Clone, Copy)]
pub struct DecodeConfig {
pub alphabet: Base64Alphabet,
pub handling: LastChunkHandling,
pub max_len: Option<usize>,
}
const B64_ALPHABET_STANDARD: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const B64_ALPHABET_URL: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
@@ -62,9 +70,9 @@ fn base64_value(unit: u16, alphabet: Base64Alphabet) -> Option<u32> {
/// - `chunk` 恰 4 字符;调用方保证只在此处解码满块。
/// - 返回 `(24 位值, 解码字节数 1-3)`:24 位值按 4 槽位左对齐(padding 槽补 0),
/// 字节数 = 3 - padding 数。
fn decode_full_chunk(
chunk: [u16; 4], alphabet: Base64Alphabet, handling: LastChunkHandling,
) -> Result<(u32, usize), DecodeError> {
fn decode_full_chunk(chunk: [u16; 4], config: DecodeConfig) -> Result<(u32, usize), DecodeError> {
let alphabet = config.alphabet;
let handling = config.handling;
let pad = match (chunk[2] == 0x3d, chunk[3] == 0x3d) {
(false, false) => 0,
(false, true) => 1,
@@ -108,11 +116,13 @@ fn write_chunk_bytes(write: &mut dyn FnMut(usize, u8), written: usize, bits: u32
/// `clen - 1` 字节(2 字符 1 字节、3 字符 2 字节)并写出。
/// - 成功 `read`:解出字节的 loose 路径取块后消费位置;各"停在块前"路径
/// (sbp、余量不足)取块前位置 `chunk_start`。
#[allow(clippy::too_many_arguments)]
fn finish_partial(
chunk: [u16; 4], clen: usize, alphabet: Base64Alphabet, handling: LastChunkHandling, max_len: Option<usize>,
write: &mut dyn FnMut(usize, u8), mut written: usize, chunk_start: usize, read: usize,
chunk: [u16; 4], clen: usize, config: DecodeConfig, write: &mut dyn FnMut(usize, u8), mut written: usize,
chunk_start: usize, read: usize,
) -> Result<(usize, usize), DecodeError> {
let alphabet = config.alphabet;
let handling = config.handling;
let max_len = config.max_len;
if (0..clen).any(|i| chunk[i] == 0x3d) {
let trailing = (0..clen).rev().take_while(|&i| chunk[i] == 0x3d).count();
if clen == 3 && trailing == 1 && handling == LastChunkHandling::StopBeforePartial {
@@ -166,9 +176,9 @@ fn finish_partial(
/// - `Ok((read, written))` 中 `read` 为成功语义下的消费位置(含跳过空白,
/// 停止路径为块前位置)。
pub fn decode_base64(
units: &[u16], alphabet: Base64Alphabet, handling: LastChunkHandling, max_len: Option<usize>,
write: &mut dyn FnMut(usize, u8),
units: &[u16], config: DecodeConfig, write: &mut dyn FnMut(usize, u8),
) -> Result<(usize, usize), DecodeError> {
let max_len = config.max_len;
if max_len == Some(0) {
return Ok((0, 0));
}
@@ -197,7 +207,7 @@ pub fn decode_base64(
return Err(DecodeError);
}
}
let (bits, count) = decode_full_chunk(chunk, alphabet, handling)?;
let (bits, count) = decode_full_chunk(chunk, config)?;
if let Some(m) = max_len {
if written + count > m {
return Ok((chunk_start, written));
@@ -211,7 +221,7 @@ pub fn decode_base64(
}
}
0 => return Ok((read, written)),
_ => return finish_partial(chunk, clen, alphabet, handling, max_len, write, written, chunk_start, read),
_ => return finish_partial(chunk, clen, config, write, written, chunk_start, read),
}
}
}
@@ -302,8 +312,16 @@ mod tests {
units: &[u16], alphabet: Base64Alphabet, handling: LastChunkHandling, max_len: Option<usize>,
) -> Result<(usize, Vec<u8>), DecodeError> {
let mut bytes = Vec::new();
decode_base64(units, alphabet, handling, max_len, &mut |_, b| bytes.push(b))
.map(|(r, w)| (r, bytes.into_iter().take(w).collect()))
decode_base64(
units,
DecodeConfig {
alphabet,
handling,
max_len,
},
&mut |_, b| bytes.push(b),
)
.map(|(r, w)| (r, bytes.into_iter().take(w).collect()))
}
fn decode_hex_full(units: &[u16], max_len: Option<usize>) -> Result<(usize, Vec<u8>), DecodeError> {
@@ -475,9 +493,11 @@ mod tests {
let mut out = Vec::new();
let r = decode_base64(
&units("MjYyZm.9v"),
Base64Alphabet::Standard,
LastChunkHandling::Loose,
None,
DecodeConfig {
alphabet: Base64Alphabet::Standard,
handling: LastChunkHandling::Loose,
max_len: None,
},
&mut |i, b| out.insert(i, b),
);
assert!(r.is_err());
@@ -486,9 +506,11 @@ mod tests {
let mut out = Vec::new();
let r = decode_base64(
&units("MjYyZg==="),
Base64Alphabet::Standard,
LastChunkHandling::Loose,
None,
DecodeConfig {
alphabet: Base64Alphabet::Standard,
handling: LastChunkHandling::Loose,
max_len: None,
},
&mut |i, b| out.insert(i, b),
);
assert!(r.is_err());
@@ -86,6 +86,21 @@ pub(crate) struct InternalDuration {
pub(crate) time_ns: i128,
}
/// ISO 日期时间:日期三元(年/月/日)加时间纳秒(不含日分量)。
#[derive(Clone, Copy)]
pub(crate) struct IsoDateTime {
pub(crate) date: (i128, i128, i128),
pub(crate) time_ns: i128,
}
/// 取整规格:增量、单位与舍入模式(吸收取整三参)。
#[derive(Clone, Copy)]
pub(crate) struct RoundingSpec {
pub(crate) increment: i128,
pub(crate) unit: usize,
pub(crate) mode: InstantRoundingMode,
}
impl InternalDuration {
/// 转十分量数组:时间部分按向零截断分解为时/分/秒/毫秒/微秒/纳秒(与时长同号)。
pub(crate) fn to_values(&self) -> [f64; 10] {
@@ -394,11 +409,11 @@ pub(crate) struct NudgeCalendarResult {
///
/// `epoch_of_date` 是窗口端点日期的纪元换算闭包:plain 面传墙钟换算(行为逐字节
/// 不变),zoned 面传 GetEpochNanosecondsFor(compatible) 换算。
#[allow(clippy::too_many_arguments)]
pub(crate) fn nudge_to_calendar_unit(
sign: i128, values: &[f64; 10], origin_epoch: i128, dest_epoch: i128, date1: (i128, i128, i128), increment: i128,
unit: usize, mode: InstantRoundingMode, epoch_of_date: &dyn Fn((i128, i128, i128)) -> Result<i128, ()>,
sign: i128, values: &[f64; 10], origin_epoch: i128, dest_epoch: i128, date1: (i128, i128, i128),
spec: RoundingSpec, epoch_of_date: &dyn Fn((i128, i128, i128)) -> Result<i128, ()>,
) -> Result<NudgeCalendarResult, ()> {
let RoundingSpec { increment, unit, mode } = spec;
let epoch_of = |dur: &[f64; 10]| -> Result<i128, ()> {
if date_duration_sign(dur) == 0 {
return Ok(origin_epoch);
@@ -744,12 +759,10 @@ pub(crate) fn parse_year_month_difference_settings<H: VmHost>(
/// 的舍入模式并在最后整体取反(不调换两端,调换会改变 0.5 边界所在的年长)。
/// no_rounding = 单位索引空间中"最细单位"的索引:该单位且 increment=1 时
/// 舍入为恒等变换,跳过 nudge(边界处避免窗口端点的无效范围抛错)。
#[allow(clippy::too_many_arguments)]
pub(crate) fn difference_core<H: VmHost>(
vm: &mut H, start: (i128, i128, i128), start_time_ns: i128, end: (i128, i128, i128), end_time_ns: i128,
settings: DifferenceSettings, since: bool, no_rounding: usize,
vm: &mut H, start: IsoDateTime, end: IsoDateTime, settings: DifferenceSettings, since: bool, no_rounding: usize,
) -> NativeResult {
let values = match nudge_iso_difference(vm, start, start_time_ns, end, end_time_ns, settings, since, no_rounding) {
let values = match nudge_iso_difference(vm, start, end, settings, since, no_rounding) {
Ok(values) => values,
Err(error) => return NativeResult::Err(error),
};
@@ -769,10 +782,8 @@ pub(crate) fn difference_core<H: VmHost>(
/// - Duration.round 的日历单位路径与本函数共用:round 把 duration 应用 relativeTo 后
/// 以同样的 start/end 差输入本函数,输出即 balance 后的舍入结果。
/// - 时间分量为带符号 i128;start/end 日与时间各自独立,符号由差值推导。
#[allow(clippy::too_many_arguments)]
pub(crate) fn nudge_iso_difference<H: VmHost>(
vm: &mut H, start: (i128, i128, i128), start_time_ns: i128, end: (i128, i128, i128), end_time_ns: i128,
settings: DifferenceSettings, since: bool, no_rounding: usize,
vm: &mut H, start: IsoDateTime, end: IsoDateTime, settings: DifferenceSettings, since: bool, no_rounding: usize,
) -> Result<[f64; 10], JsValue> {
let DifferenceSettings {
largest_index,
@@ -794,17 +805,17 @@ pub(crate) fn nudge_iso_difference<H: VmHost>(
// 规范语义:internal = other - receiver;since 用 NegateRoundingMode 的舍入模式,最后整体取反。
// 不调换两端(调换会改变 0.5 边界所在的年长,破坏对称性)。
let origin_epoch = days_from_civil(start.0, start.1, start.2) * DAY_NS + start_time_ns;
let dest_epoch = days_from_civil(end.0, end.1, end.2) * DAY_NS + end_time_ns;
let origin_epoch = days_from_civil(start.date.0, start.date.1, start.date.2) * DAY_NS + start.time_ns;
let dest_epoch = days_from_civil(end.date.0, end.date.1, end.date.2) * DAY_NS + end.time_ns;
let date1 = start;
let time1_ns = start_time_ns;
let date1 = start.date;
let time1_ns = start.time_ns;
// plain 面纪元换算闭包:墙钟(日 × 24h + 时间),行为与旧内联算法逐字节一致。
let epoch_of_date = |date: (i128, i128, i128)| -> Result<i128, ()> {
Ok(days_from_civil(date.0, date.1, date.2) * DAY_NS + time1_ns)
};
let mut date2 = end;
let mut time_ns = end_time_ns - time1_ns;
let mut date2 = end.date;
let mut time_ns = end.time_ns - time1_ns;
let time_sign = if time_ns > 0 {
1_i128
} else if time_ns < 0 {
@@ -906,9 +917,11 @@ pub(crate) fn nudge_iso_difference<H: VmHost>(
origin_epoch,
dest_epoch,
date1,
increment,
smallest_index,
mode,
RoundingSpec {
increment,
unit: smallest_index,
mode,
},
&epoch_of_date,
) {
Ok(result) => result,
+27 -13
View File
@@ -15,14 +15,14 @@ use super::common::{
use super::difference::{
add_date_duration, add_days_iso, balance_instant_difference, compare_iso_date, date_duration_sign, date_until_iso,
nudge_iso_difference, nudge_window, plain_date_time_unit_index, round_instant_difference, DifferenceSettings,
InternalDuration, DAY_NS, MAX_ISO_DAY,
IsoDateTime, InternalDuration, DAY_NS, MAX_ISO_DAY,
};
use super::instant::{instant_rounding_mode, InstantRoundingMode};
use super::time_zone::zone_offset_seconds;
use super::zoned_date_time::{
add_zoned_datetime, difference_zoned_datetime_with_rounding_vm, difference_zoned_datetime_with_total_vm,
disambiguate_possible_epoch_nanoseconds, extract_time_zone_annotation, get_possible_epoch_nanoseconds,
map_add_zoned_error, parse_any_offset_seconds, valid_offset_fraction, zoned_date_time_string_parts,
map_add_zoned_error, parse_any_offset_seconds, valid_offset_fraction, zoned_date_time_string_parts, ZonedPair,
};
use super::{make_duration, parse_plain_date_time_string};
@@ -708,9 +708,13 @@ pub fn duration_total<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
// Zoned 载荷:AddZonedDateTime → DifferenceZonedDateTimeWithTotal(门与单位无关)。
if let Some(RelativePoint::Zoned { epoch_ns, time_zone, calendar }) = &relative_date {
let target = native_try!(add_zoned_datetime(vm, *epoch_ns, time_zone, calendar, &values, true));
let total = native_try!(difference_zoned_datetime_with_total_vm(
vm, *epoch_ns, target, time_zone, calendar, unit_index
));
let pair = ZonedPair {
ns1: *epoch_ns,
ns2: target,
time_zone,
calendar,
};
let total = native_try!(difference_zoned_datetime_with_total_vm(vm, pair, unit_index));
return NativeResult::Ok(JsValue::float(total));
}
@@ -1369,17 +1373,19 @@ pub fn duration_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
// Zoned 载荷:AddZonedDateTime → DifferenceZonedDateTimeWithRounding(门与单位无关)。
if let Some(RelativePoint::Zoned { epoch_ns, time_zone, calendar }) = &relative_date {
let target = native_try!(add_zoned_datetime(vm, *epoch_ns, time_zone, calendar, &values, true));
let internal = native_try!(difference_zoned_datetime_with_rounding_vm(
vm,
*epoch_ns,
target,
let pair = ZonedPair {
ns1: *epoch_ns,
ns2: target,
time_zone,
calendar,
largest,
increment,
};
let settings = DifferenceSettings {
largest_index: largest,
smallest_index,
increment,
mode,
));
};
let internal = native_try!(difference_zoned_datetime_with_rounding_vm(vm, pair, settings));
// largestUnit 为日期类时降为 hour。
let largest = if largest <= 3 { 4 } else { largest };
let result = internal_duration_to_values(&internal, largest);
@@ -1460,7 +1466,15 @@ pub fn duration_round<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
increment,
mode,
};
let result = match nudge_iso_difference(vm, rel, 0, end, target_time, settings, false, 9) {
let start = IsoDateTime {
date: rel,
time_ns: 0,
};
let end_dt = IsoDateTime {
date: end,
time_ns: target_time,
};
let result = match nudge_iso_difference(vm, start, end_dt, settings, false, 9) {
Ok(values) => values,
Err(error) => return NativeResult::Err(error),
};
@@ -11,7 +11,7 @@ use super::common::{
is_ctor_call, parse_plain_date_string, plain_date_time_object_parts, receiver_obj, temporal_calendar_id_strict,
temporal_number_component, temporal_overflow, zoned_date_time_plain_parts,
};
use super::difference::{difference_core, parse_difference_settings};
use super::difference::{difference_core, parse_difference_settings, IsoDateTime};
use super::duration::duration_like_values;
use super::make_plain_date;
@@ -623,10 +623,14 @@ pub fn plain_date_until<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
};
difference_core(
vm,
(i128::from(start.0), i128::from(start.1), i128::from(start.2)),
0,
(i128::from(other.0), i128::from(other.1), i128::from(other.2)),
0,
IsoDateTime {
date: (i128::from(start.0), i128::from(start.1), i128::from(start.2)),
time_ns: 0,
},
IsoDateTime {
date: (i128::from(other.0), i128::from(other.1), i128::from(other.2)),
time_ns: 0,
},
settings,
false,
9,
@@ -651,10 +655,14 @@ pub fn plain_date_since<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
};
difference_core(
vm,
(i128::from(start.0), i128::from(start.1), i128::from(start.2)),
0,
(i128::from(other.0), i128::from(other.1), i128::from(other.2)),
0,
IsoDateTime {
date: (i128::from(start.0), i128::from(start.1), i128::from(start.2)),
time_ns: 0,
},
IsoDateTime {
date: (i128::from(other.0), i128::from(other.1), i128::from(other.2)),
time_ns: 0,
},
settings,
true,
9,
@@ -14,7 +14,7 @@ use super::{
parse_temporal_string_impl, plain_date_time_object_parts, plain_time_components, receiver_obj, round_instant_ns,
temporal_calendar_id_strict, temporal_option_number, temporal_option_string, temporal_option_value,
temporal_overflow, valid_iso_date, valid_plain_date_time_range, valid_plain_time, zoned_date_time_plain_parts,
FractionalSecondDigitsInput,
FractionalSecondDigitsInput, IsoDateTime,
};
// ───────────────────── PlainDateTime 基础方法 ─────────────────────
@@ -589,10 +589,14 @@ fn plain_date_time_difference<H: VmHost>(vm: &mut H, args: &[u8], since: bool) -
};
difference_core(
vm,
(i128::from(sy), i128::from(sm), i128::from(sd)),
st as i128,
(i128::from(oy), i128::from(om), i128::from(od)),
ot as i128,
IsoDateTime {
date: (i128::from(sy), i128::from(sm), i128::from(sd)),
time_ns: st as i128,
},
IsoDateTime {
date: (i128::from(oy), i128::from(om), i128::from(od)),
time_ns: ot as i128,
},
settings,
since,
9,
@@ -11,7 +11,7 @@ use super::{
make_plain_time, parse_any_offset_seconds, parse_difference_settings, parse_fractional_second_digits,
parse_iso_date, receiver_obj, reject_partial_object_with_calendar_or_time_zone, round_instant_ns,
temporal_number_component, temporal_option_number, temporal_option_string, temporal_option_value,
temporal_overflow, valid_offset_fraction, zoned_date_time_plain_parts, FractionalSecondDigitsInput,
temporal_overflow, valid_offset_fraction, zoned_date_time_plain_parts, FractionalSecondDigitsInput, IsoDateTime,
};
/// 时间串的日期歧义判定:形如 YYYY-MM / MMDD / YYYYMM / MM-DD 且作为日期合法 → 歧义。
@@ -701,7 +701,20 @@ fn plain_time_difference<H: VmHost>(vm: &mut H, args: &[u8], since: bool) -> Nat
Ok(settings) => settings,
Err(error) => return NativeResult::Err(error),
};
difference_core(vm, (0, 0, 0), receiver_ns as i128, (0, 0, 0), other_ns as i128, settings, since, 9)
difference_core(
vm,
IsoDateTime {
date: (0, 0, 0),
time_ns: receiver_ns as i128,
},
IsoDateTime {
date: (0, 0, 0),
time_ns: other_ns as i128,
},
settings,
since,
9,
)
}
/// `Temporal.PlainTime.prototype.until(other, options)`。
@@ -13,7 +13,7 @@ use super::{
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,
IsoDateTime, MAX_ISO_DAY,
};
/// 读 PlainYearMonth 的 年/月/参考日 三元组(槽 0-2),含 receiver 校验。
@@ -884,5 +884,18 @@ fn plain_year_month_difference<H: VmHost>(vm: &mut H, args: &[u8], since: bool)
{
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)
difference_core(
vm,
IsoDateTime {
date: (ry, rm, 1),
time_ns: 0,
},
IsoDateTime {
date: (i128::from(oy), i128::from(om), 1),
time_ns: 0,
},
settings,
since,
1,
)
}
+181 -33
View File
@@ -104,20 +104,79 @@ fn format_time_zone_annotation_normalizes_offset() {
#[test]
fn format_zoned_date_time_iso_annotations() {
// 偏移/时区/日历注解各取值组合,验证段序 `{offset}[{tz}][{ca}]` 与 critical 前缀。
let base = format_zoned_date_time_iso(0, 3600, "+01:00", "iso8601", true, None, "auto", "auto", "auto");
let base = format_zoned_date_time_iso(
0,
3600,
"+01:00",
"iso8601",
true,
None,
FormatNames {
offset_name: "auto",
time_zone_name: "auto",
calendar_name: "auto",
},
);
assert_eq!(base, Some("1970-01-01T01:00:00+01:00[+01:00]".to_string()));
// offset never 省略偏移段,时区注解仍显示。
let no_offset = format_zoned_date_time_iso(0, 3600, "+01:00", "iso8601", true, None, "never", "auto", "auto");
let no_offset = format_zoned_date_time_iso(
0,
3600,
"+01:00",
"iso8601",
true,
None,
FormatNames {
offset_name: "never",
time_zone_name: "auto",
calendar_name: "auto",
},
);
assert_eq!(no_offset, Some("1970-01-01T01:00:00[+01:00]".to_string()));
// calendarName always 追加日历注解。
let ca_always = format_zoned_date_time_iso(0, 3600, "+01:00", "iso8601", true, None, "auto", "auto", "always");
let ca_always = format_zoned_date_time_iso(
0,
3600,
"+01:00",
"iso8601",
true,
None,
FormatNames {
offset_name: "auto",
time_zone_name: "auto",
calendar_name: "always",
},
);
assert_eq!(ca_always, Some("1970-01-01T01:00:00+01:00[+01:00][u-ca=iso8601]".to_string()));
// timeZoneName/calendarName critical 均 `!` 置于括号内。
let critical = format_zoned_date_time_iso(0, 3600, "+01:00", "iso8601", true, None, "auto", "critical", "critical");
let critical = format_zoned_date_time_iso(
0,
3600,
"+01:00",
"iso8601",
true,
None,
FormatNames {
offset_name: "auto",
time_zone_name: "critical",
calendar_name: "critical",
},
);
assert_eq!(critical, Some("1970-01-01T01:00:00+01:00[!+01:00][!u-ca=iso8601]".to_string()));
// offset critical 偏移段前加 !。
let offset_critical =
format_zoned_date_time_iso(0, 3600, "+01:00", "iso8601", true, None, "critical", "auto", "auto");
let offset_critical = format_zoned_date_time_iso(
0,
3600,
"+01:00",
"iso8601",
true,
None,
FormatNames {
offset_name: "critical",
time_zone_name: "auto",
calendar_name: "auto",
},
);
assert_eq!(offset_critical, Some("1970-01-01T01:00:00!+01:00[+01:00]".to_string()));
}
@@ -125,7 +184,19 @@ fn format_zoned_date_time_iso_annotations() {
fn format_zoned_date_time_iso_epoch_rounding_cross_midnight() {
// 2000-01-01 前 1ns 已舍入到 2000-01-01 整点,8 位小数输出。
let rounded = round_instant_ns(946_684_799_999_999_999, 100, InstantRoundingMode::HalfExpand).unwrap();
let output = format_zoned_date_time_iso(rounded, 0, "UTC", "iso8601", true, Some(8), "auto", "auto", "auto");
let output = format_zoned_date_time_iso(
rounded,
0,
"UTC",
"iso8601",
true,
Some(8),
FormatNames {
offset_name: "auto",
time_zone_name: "auto",
calendar_name: "auto",
},
);
assert_eq!(output, Some("2000-01-01T00:00:00.00000000+00:00[UTC]".to_string()));
}
@@ -429,7 +500,19 @@ fn sub_minute_offset_getter_branch() {
assert_eq!(format_offset_seconds_text(2670), "+00:44:30");
assert_eq!(format_offset_seconds_text(3600), "+01:00");
assert_eq!(format_time_zone_annotation("-00:44:30", false), "[-00:44:30]");
let iso = format_zoned_date_time_iso(0, -2670, "-00:44:30", "iso8601", true, None, "auto", "auto", "auto");
let iso = format_zoned_date_time_iso(
0,
-2670,
"-00:44:30",
"iso8601",
true,
None,
FormatNames {
offset_name: "auto",
time_zone_name: "auto",
calendar_name: "auto",
},
);
assert_eq!(iso, Some("1969-12-31T23:15:30-00:44:30[-00:44:30]".to_string()));
assert_eq!(
format_instant_iso(0, Some(-2670), true, None),
@@ -614,10 +697,13 @@ fn calendar_date_add_iso_constrain_semantics() {
use super::zoned_date_time::{calendar_date_add_iso, AddZonedError};
// 月末截断:constrain 钳到目标月末,reject 抛 DayOutOfRange。
assert_eq!(calendar_date_add_iso((2025, 1, 31), 0, 1, 0, 0, true), Ok((2025, 2, 28)));
assert_eq!(calendar_date_add_iso((2025, 1, 31), 0, 1, 0, 0, false), Err(AddZonedError::DayOutOfRange));
assert_eq!(calendar_date_add_iso((2025, 1, 31), [0, 1, 0, 0], true), Ok((2025, 2, 28)));
assert_eq!(
calendar_date_add_iso((2025, 1, 31), [0, 1, 0, 0], false),
Err(AddZonedError::DayOutOfRange)
);
// 闰年二月:constrain 钳到 29。
assert_eq!(calendar_date_add_iso((2024, 1, 31), 0, 1, 0, 0, true), Ok((2024, 2, 29)));
assert_eq!(calendar_date_add_iso((2024, 1, 31), [0, 1, 0, 0], true), Ok((2024, 2, 29)));
}
#[test]
@@ -627,25 +713,61 @@ fn zoned_difference_day_correction() {
// 同日纯时间差:无日期部分,时间部分为纳秒差。
let ns1 = get_epoch_nanos_for("America/New_York", (2025, 6, 15), 43_200_000_000_000).unwrap();
let ns2 = get_epoch_nanos_for("America/New_York", (2025, 6, 15), 66_600_000_000_000).unwrap();
let d = difference_zoned_datetime(ns1, ns2, "America/New_York", "iso8601", 3).unwrap();
let d = difference_zoned_datetime(
ZonedPair {
ns1,
ns2,
time_zone: "America/New_York",
calendar: "iso8601",
},
3,
)
.unwrap();
assert_eq!(d.date, [0.0, 0.0, 0.0, 0.0]);
assert_eq!(d.time_ns, 23_400_000_000_000);
// 跨 DST 间隙日(23 小时):墙历时刻相同,日期部分 1 天、时间部分 0。
let ns1 = get_epoch_nanos_for("America/New_York", (2025, 3, 8), 43_200_000_000_000).unwrap();
let ns2 = get_epoch_nanos_for("America/New_York", (2025, 3, 9), 43_200_000_000_000).unwrap();
let d = difference_zoned_datetime(ns1, ns2, "America/New_York", "iso8601", 3).unwrap();
let d = difference_zoned_datetime(
ZonedPair {
ns1,
ns2,
time_zone: "America/New_York",
calendar: "iso8601",
},
3,
)
.unwrap();
assert_eq!(d.date, [0.0, 0.0, 0.0, 1.0]);
assert_eq!(d.time_ns, 0);
// dayCorrection 循环推进:12 小时跨间隙日,日期部分 0、时间部分 12 小时。
let ns2b = get_epoch_nanos_for("America/New_York", (2025, 3, 9), 0).unwrap();
let d = difference_zoned_datetime(ns1, ns2b, "America/New_York", "iso8601", 3).unwrap();
let d = difference_zoned_datetime(
ZonedPair {
ns1,
ns2: ns2b,
time_zone: "America/New_York",
calendar: "iso8601",
},
3,
)
.unwrap();
assert_eq!(d.date, [0.0, 0.0, 0.0, 0.0]);
assert_eq!(d.time_ns, 43_200_000_000_000);
// 反向(sign 为正):日期部分为负一天、时间部分 0。
let d = difference_zoned_datetime(ns2, ns1, "America/New_York", "iso8601", 3).unwrap();
let d = difference_zoned_datetime(
ZonedPair {
ns1: ns2,
ns2: ns1,
time_zone: "America/New_York",
calendar: "iso8601",
},
3,
)
.unwrap();
assert_eq!(d.date, [0.0, 0.0, 0.0, -1.0]);
assert_eq!(d.time_ns, 0);
}
@@ -657,12 +779,30 @@ fn zoned_total_dst_fraction() {
// 整日差(23 小时间隙日):总量 1。
let ns1 = get_epoch_nanos_for("America/New_York", (2025, 3, 8), 43_200_000_000_000).unwrap();
let ns2 = get_epoch_nanos_for("America/New_York", (2025, 3, 9), 43_200_000_000_000).unwrap();
let total = difference_zoned_datetime_with_total(ns1, ns2, "America/New_York", "iso8601", 3).unwrap();
let total = difference_zoned_datetime_with_total(
ZonedPair {
ns1,
ns2,
time_zone: "America/New_York",
calendar: "iso8601",
},
3,
)
.unwrap();
assert!((total - 1.0).abs() < 1e-12);
// 12 小时跨 23 小时间隙日:总量 12/23(按实际日长折算,非 24 小时)。
let ns2b = get_epoch_nanos_for("America/New_York", (2025, 3, 9), 0).unwrap();
let total = difference_zoned_datetime_with_total(ns1, ns2b, "America/New_York", "iso8601", 3).unwrap();
let total = difference_zoned_datetime_with_total(
ZonedPair {
ns1,
ns2: ns2b,
time_zone: "America/New_York",
calendar: "iso8601",
},
3,
)
.unwrap();
assert!((total - 12.0 / 23.0).abs() < 1e-12);
}
@@ -681,14 +821,18 @@ fn zoned_round_nudge_to_zoned_time() {
let p25h = [0.0_f64, 0.0, 0.0, 0.0, 25.0, 0.0, 0.0, 0.0, 0.0, 0.0];
let target = add_zoned_datetime_pure(origin, "Pacific/Apia", "iso8601", &p25h, true).unwrap();
let internal = difference_zoned_datetime_with_rounding(
origin,
target,
"Pacific/Apia",
"iso8601",
4,
1,
4,
InstantRoundingMode::Trunc,
ZonedPair {
ns1: origin,
ns2: target,
time_zone: "Pacific/Apia",
calendar: "iso8601",
},
DifferenceSettings {
largest_index: 4,
smallest_index: 4,
increment: 1,
mode: InstantRoundingMode::Trunc,
},
)
.unwrap();
let values = internal_duration_to_values(&internal, 4);
@@ -698,14 +842,18 @@ fn zoned_round_nudge_to_zoned_time() {
let p1dt26h = [0.0_f64, 0.0, 0.0, 1.0, 26.0, 0.0, 0.0, 0.0, 0.0, 0.0];
let target = add_zoned_datetime_pure(origin, "Pacific/Apia", "iso8601", &p1dt26h, true).unwrap();
let internal = difference_zoned_datetime_with_rounding(
origin,
target,
"Pacific/Apia",
"iso8601",
3,
1,
4,
InstantRoundingMode::Trunc,
ZonedPair {
ns1: origin,
ns2: target,
time_zone: "Pacific/Apia",
calendar: "iso8601",
},
DifferenceSettings {
largest_index: 3,
smallest_index: 4,
increment: 1,
mode: InstantRoundingMode::Trunc,
},
)
.unwrap();
let values = internal_duration_to_values(&internal, 4);
@@ -19,7 +19,7 @@ use super::common::{
use super::difference::{
add_days_iso, bubble_relative_duration, compare_iso_date, date_duration_sign, date_until_iso, difference_core,
internal_duration_sign, nudge_to_calendar_unit, parse_difference_settings, round_instant_difference,
InternalDuration, DAY_NS, MAX_ISO_DAY, TIME_UNIT_NS,
DifferenceSettings, IsoDateTime, InternalDuration, RoundingSpec, DAY_NS, MAX_ISO_DAY, TIME_UNIT_NS,
};
use super::instant::{
instant_rounding_mode, instant_string_without_annotations, parse_instant_string, primitive_to_bigint,
@@ -533,17 +533,27 @@ pub(crate) fn format_time_zone_annotation(time_zone_id: &str, critical: bool) ->
}
}
/// toString 注解名选项:offset / timeZoneName / calendarName 三取值。
#[derive(Clone, Copy)]
pub(crate) struct FormatNames<'a> {
pub(crate) offset_name: &'a str,
pub(crate) time_zone_name: &'a str,
pub(crate) calendar_name: &'a str,
}
/// ZDT 字符串化核心:epoch 域已舍入后按偏移与注解选项拼 `{date}T{time}.fff{offset}[{tz}][{ca}]`。
///
/// # 边界与前提
/// - epoch_ns 须已按 quantum 舍入(调用方保证),此处仅做本地墙钟分解。
/// - offset_name/time_zone_name 取 auto/never/critical,calendar_name 取 auto/never/always/critical。
/// - 偏移段 `±HH:MM` 带冒号(秒非零时 `±HH:MM:SS`);时区注解命名区原样、偏移区规范化。
#[expect(clippy::too_many_arguments)]
pub(crate) fn format_zoned_date_time_iso(
epoch_ns: i128, offset_seconds: i64, time_zone_id: &str, calendar_id: &str, include_seconds: bool,
output_digits: Option<usize>, offset_name: &str, time_zone_name: &str, calendar_name: &str,
output_digits: Option<usize>, names: FormatNames,
) -> Option<String> {
let offset_name = names.offset_name;
let time_zone_name = names.time_zone_name;
let calendar_name = names.calendar_name;
const DAY_NS: i128 = 86_400_000_000_000;
let offset_ns = i128::from(offset_seconds).checked_mul(1_000_000_000)?;
let local_ns = epoch_ns.checked_add(offset_ns)?;
@@ -716,9 +726,11 @@ pub fn zoned_date_time_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeRe
&calendar_id,
include_seconds,
output_digits,
&offset_name,
&time_zone_name,
&calendar_name,
FormatNames {
offset_name: &offset_name,
time_zone_name: &time_zone_name,
calendar_name: &calendar_name,
},
) {
Some(output) => NativeResult::Ok(vm.new_string_owned(output)),
None => NativeResult::Err(crate::error::create_range_error(vm, "invalid ZonedDateTime")),
@@ -1350,10 +1362,14 @@ fn zoned_date_time_difference<H: VmHost>(vm: &mut H, args: &[u8], since: bool) -
let settings = native_try!(parse_difference_settings(vm, options_value, false, 4));
difference_core(
vm,
(i128::from(y_r), i128::from(m_r), i128::from(d_r)),
time_ns_r as i128,
(y_o, m_o, d_o),
time_ns_o,
IsoDateTime {
date: (i128::from(y_r), i128::from(m_r), i128::from(d_r)),
time_ns: time_ns_r as i128,
},
IsoDateTime {
date: (y_o, m_o, d_o),
time_ns: time_ns_o,
},
settings,
since,
9,
@@ -1710,14 +1726,15 @@ pub(crate) fn add_instant(epoch_ns: i128, time_ns: i128) -> Result<i128, AddZone
Ok(result)
}
/// CalendarDateAdd(ISO 8601 日历):日期叠加年/月/周/日。
/// CalendarDateAdd(ISO 8601 日历):日期叠加年/月/周/日(values 依次年/月/周/日)。
///
/// # 步骤
/// 1. 年/月推进(月末截断:constrain 钳到目标月末,reject 抛 DayOutOfRange)。
/// 2. 周*7 + 日推进。
pub(crate) fn calendar_date_add_iso(
date: (i128, i128, i128), years: i128, months: i128, weeks: i128, days: i128, constrain: bool,
date: (i128, i128, i128), values: [i128; 4], constrain: bool,
) -> Result<(i128, i128, i128), AddZonedError> {
let (years, months, weeks, days) = (values[0], values[1], values[2], values[3]);
let total = date.0 * 12 + (date.1 - 1) + years * 12 + months;
let ny = total.div_euclid(12);
let nm = total.rem_euclid(12) + 1;
@@ -1837,17 +1854,16 @@ pub(crate) fn add_zoned_datetime_pure(
return add_instant(epoch_ns, time_ns);
}
// GetISODateTimeFor:墙历分量。
let ((year, month, day), time_ns) = get_iso_date_time_for(time_zone, epoch_ns)?;
let iso = get_iso_date_time_for(time_zone, epoch_ns)?;
let (year, month, day) = iso.date;
let time_ns = iso.time_ns;
if !valid_plain_date_time_range(year as i32, month as u32, day as u32, time_ns as f64) {
return Err(AddZonedError::OutOfDateTimeRange);
}
// CalendarDateAdd:日期分量(constrain 钳月末,reject 抛错)。
let added = calendar_date_add_iso(
(year, month, day),
values[0] as i128,
values[1] as i128,
values[2] as i128,
values[3] as i128,
[values[0] as i128, values[1] as i128, values[2] as i128, values[3] as i128],
constrain,
)?;
// 中间日期检查(ISODateTimeWithinLimits)。
@@ -1887,20 +1903,18 @@ pub(crate) fn map_add_zoned_error<H: VmHost>(vm: &mut H, error: AddZonedError) -
}
/// DifferenceZonedDateTimeWithRounding 的 VM 包装:错误映射为 RangeError。
#[allow(clippy::too_many_arguments)]
pub(crate) fn difference_zoned_datetime_with_rounding_vm<H: VmHost>(
vm: &mut H, ns1: i128, ns2: i128, time_zone: &str, calendar: &str, largest_unit: usize, increment: i128,
smallest_unit: usize, mode: InstantRoundingMode,
vm: &mut H, pair: ZonedPair, settings: DifferenceSettings,
) -> Result<InternalDuration, JsValue> {
difference_zoned_datetime_with_rounding(ns1, ns2, time_zone, calendar, largest_unit, increment, smallest_unit, mode)
difference_zoned_datetime_with_rounding(pair, settings)
.map_err(|error| map_add_zoned_error(vm, error))
}
/// DifferenceZonedDateTimeWithTotal 的 VM 包装:错误映射为 RangeError。
pub(crate) fn difference_zoned_datetime_with_total_vm<H: VmHost>(
vm: &mut H, ns1: i128, ns2: i128, time_zone: &str, calendar: &str, unit: usize,
vm: &mut H, pair: ZonedPair, unit: usize,
) -> Result<f64, JsValue> {
difference_zoned_datetime_with_total(ns1, ns2, time_zone, calendar, unit)
difference_zoned_datetime_with_total(pair, unit)
.map_err(|error| map_add_zoned_error(vm, error))
}
@@ -1912,16 +1926,17 @@ pub(crate) fn difference_zoned_datetime_with_total_vm<H: VmHost>(
///
/// # 边界与前提
/// - 时区名非法(不在区表且非固定偏移串)返回 InvalidTimeZone。
pub(crate) fn get_iso_date_time_for(
time_zone: &str, epoch_ns: i128,
) -> Result<((i128, i128, i128), i128), AddZonedError> {
pub(crate) fn get_iso_date_time_for(time_zone: &str, epoch_ns: i128) -> Result<IsoDateTime, AddZonedError> {
let offset_seconds = zone_offset_seconds(time_zone, epoch_ns.div_euclid(1_000_000_000) as i64)
.ok_or(AddZonedError::InvalidTimeZone)?;
let local_ns = epoch_ns + i128::from(offset_seconds) * 1_000_000_000;
let days = local_ns.div_euclid(DAY_NS);
let time_ns = local_ns.rem_euclid(DAY_NS);
let (year, month, day) = civil_from_days(days);
Ok(((year, month, day), time_ns))
Ok(IsoDateTime {
date: (year, month, day),
time_ns,
})
}
/// GetEpochNanosecondsFor(规范 11.1.11,compatible 消歧):墙历日期时间的纪元。
@@ -1959,6 +1974,17 @@ fn time_duration_sign(ns: i128) -> i128 {
}
}
/// zoned 差值端点对:两个 epoch 加时区与日历(日历透传,引擎仅 iso8601)。
#[derive(Clone, Copy)]
pub(crate) struct ZonedPair<'a> {
pub(crate) ns1: i128,
pub(crate) ns2: i128,
pub(crate) time_zone: &'a str,
// 透传字段:引擎仅 iso8601 日历,差值计算不消费,保留以对齐规范签名。
#[allow(dead_code)]
pub(crate) calendar: &'a str,
}
/// DifferenceZonedDateTime(规范 6.5.6):两个 epoch 的差,平衡后结果中无大于
/// largestUnit 的非零单位,计入日历 reckoning 与时区偏移变化。
///
@@ -1972,13 +1998,20 @@ fn time_duration_sign(ns: i128) -> i128 {
/// - 全程 i128 纳秒;中间点时间取起点墙历时间(规范 (intermediateDate, startDT.Time))。
/// - calendar 参数透传(引擎仅 iso8601 日历)。
pub(crate) fn difference_zoned_datetime(
ns1: i128, ns2: i128, time_zone: &str, _calendar: &str, largest_unit: usize,
pair: ZonedPair, largest_unit: usize,
) -> Result<InternalDuration, AddZonedError> {
let ns1 = pair.ns1;
let ns2 = pair.ns2;
let time_zone = pair.time_zone;
if ns1 == ns2 {
return Ok(InternalDuration { date: [0.0; 4], time_ns: 0 });
}
let (start_date, start_time) = get_iso_date_time_for(time_zone, ns1)?;
let (end_date, end_time) = get_iso_date_time_for(time_zone, ns2)?;
let start = get_iso_date_time_for(time_zone, ns1)?;
let end = get_iso_date_time_for(time_zone, ns2)?;
let start_date = start.date;
let start_time = start.time_ns;
let end_date = end.date;
let end_time = end.time_ns;
if compare_iso_date(start_date, end_date) == 0 {
return Ok(InternalDuration {
date: [0.0; 4],
@@ -2023,11 +2056,16 @@ pub(crate) fn difference_zoned_datetime(
///
/// # 边界与前提
/// - smallestUnit 不大于 largestUnit(调用方已校验)。
#[allow(clippy::too_many_arguments)]
pub(crate) fn difference_zoned_datetime_with_rounding(
ns1: i128, ns2: i128, time_zone: &str, _calendar: &str, largest_unit: usize, increment: i128, smallest_unit: usize,
mode: InstantRoundingMode,
pair: ZonedPair, settings: DifferenceSettings,
) -> Result<InternalDuration, AddZonedError> {
let ns1 = pair.ns1;
let ns2 = pair.ns2;
let time_zone = pair.time_zone;
let largest_unit = settings.largest_index;
let increment = settings.increment;
let smallest_unit = settings.smallest_index;
let mode = settings.mode;
if largest_unit >= 4 {
let quantum = TIME_UNIT_NS[smallest_unit - 4]
.checked_mul(increment)
@@ -2038,22 +2076,12 @@ pub(crate) fn difference_zoned_datetime_with_rounding(
time_ns: rounded,
});
}
let difference = difference_zoned_datetime(ns1, ns2, time_zone, _calendar, largest_unit)?;
let difference = difference_zoned_datetime(pair, largest_unit)?;
if smallest_unit == 9 && increment == 1 {
return Ok(difference);
}
let (iso_date, iso_time) = get_iso_date_time_for(time_zone, ns1)?;
round_relative_duration_zoned(
difference,
ns1,
ns2,
(iso_date, iso_time),
time_zone,
largest_unit,
increment,
smallest_unit,
mode,
)
let iso = get_iso_date_time_for(time_zone, ns1)?;
round_relative_duration_zoned(difference, ns1, ns2, iso, time_zone, settings)
}
/// RoundRelativeDuration(规范 7.5.38)的 zoned 路由:日历单位(或 day)走日历
@@ -2063,33 +2091,32 @@ pub(crate) fn difference_zoned_datetime_with_rounding(
/// 1. 路由:smallestUnit 为日历单位或 day → NudgeToCalendarUnit(zoned 闭包);
/// 否则 NudgeToZonedTime。
/// 2. 扩窗且 smallestUnit 非 week → BubbleRelativeDuration(startUnit 取 smallestUnit 与 day 的较大者)。
#[allow(clippy::too_many_arguments)]
fn round_relative_duration_zoned(
difference: InternalDuration, origin_ns: i128, dest_ns: i128, iso: ((i128, i128, i128), i128), time_zone: &str,
largest_unit: usize, increment: i128, smallest_unit: usize, mode: InstantRoundingMode,
difference: InternalDuration, origin_ns: i128, dest_ns: i128, iso: IsoDateTime, time_zone: &str,
settings: DifferenceSettings,
) -> Result<InternalDuration, AddZonedError> {
let largest_unit = settings.largest_index;
let increment = settings.increment;
let smallest_unit = settings.smallest_index;
let mode = settings.mode;
let values = difference.to_values();
let sign = if internal_duration_sign(&values) < 0 { -1 } else { 1 };
let (iso_date, iso_time) = iso;
let iso_date = iso.date;
let iso_time = iso.time_ns;
let epoch_of_date = zoned_epoch_of_date(time_zone, iso_time);
// zoned 面时区恒在场:irregular = smallestUnit 为日历单位或 day。
let irregular = smallest_unit <= 3;
let spec = RoundingSpec {
increment,
unit: smallest_unit,
mode,
};
let (mut result, nudged_epoch, did_expand) = if irregular {
let outcome = nudge_to_calendar_unit(
sign,
&values,
origin_ns,
dest_ns,
iso_date,
increment,
smallest_unit,
mode,
&epoch_of_date,
)
.map_err(|_| AddZonedError::OutOfDateTimeRange)?;
let outcome = nudge_to_calendar_unit(sign, &values, origin_ns, dest_ns, iso_date, spec, &epoch_of_date)
.map_err(|_| AddZonedError::OutOfDateTimeRange)?;
(outcome.duration, outcome.nudged_epoch, outcome.did_expand)
} else {
nudge_to_zoned_time(sign, &values, iso_date, iso_time, time_zone, increment, smallest_unit, mode)?
nudge_to_zoned_time(sign, &values, iso, time_zone, spec)?
};
if did_expand && smallest_unit != 2 {
let start_unit = smallest_unit.min(3);
@@ -2113,18 +2140,18 @@ fn round_relative_duration_zoned(
///
/// # 边界与前提
/// - unit 为时间单位(时到纳秒);day 单位走日历窗口路径,不进本函数。
#[allow(clippy::too_many_arguments)]
fn nudge_to_zoned_time(
sign: i128, values: &[f64; 10], iso_date: (i128, i128, i128), iso_time: i128, time_zone: &str, increment: i128,
unit: usize, mode: InstantRoundingMode,
sign: i128, values: &[f64; 10], iso: IsoDateTime, time_zone: &str, spec: RoundingSpec,
) -> Result<([f64; 10], i128, bool), AddZonedError> {
let increment = spec.increment;
let unit = spec.unit;
let mode = spec.mode;
let iso_date = iso.date;
let iso_time = iso.time_ns;
let time_ns = duration_time_only_nanoseconds(values).ok_or(AddZonedError::OutOfTimeRange)?;
let start = calendar_date_add_iso(
iso_date,
values[0] as i128,
values[1] as i128,
values[2] as i128,
values[3] as i128,
[values[0] as i128, values[1] as i128, values[2] as i128, values[3] as i128],
true,
)?;
let start_epoch = get_epoch_nanos_for(time_zone, start, iso_time)?;
@@ -2156,15 +2183,18 @@ fn nudge_to_zoned_time(
/// 1. unit 为时间单位 → TotalTimeDuration 快路径(纳秒差直接除单位长度)。
/// 2. 否则 DifferenceZonedDateTime + TotalRelativeDuration。
pub(crate) fn difference_zoned_datetime_with_total(
ns1: i128, ns2: i128, time_zone: &str, _calendar: &str, unit: usize,
pair: ZonedPair, unit: usize,
) -> Result<f64, AddZonedError> {
let ns1 = pair.ns1;
let ns2 = pair.ns2;
let time_zone = pair.time_zone;
if unit >= 4 {
let difference = ns2 - ns1;
return Ok(difference as f64 / TIME_UNIT_NS[unit - 4] as f64);
}
let difference = difference_zoned_datetime(ns1, ns2, time_zone, _calendar, unit)?;
let (iso_date, iso_time) = get_iso_date_time_for(time_zone, ns1)?;
total_relative_duration(&difference, ns1, ns2, (iso_date, iso_time), time_zone, unit)
let difference = difference_zoned_datetime(pair, unit)?;
let iso = get_iso_date_time_for(time_zone, ns1)?;
total_relative_duration(&difference, ns1, ns2, iso, time_zone, unit)
}
/// TotalRelativeDuration(规范 7.5.39):时长以 unit 计的总量,相对起点折算。
@@ -2174,27 +2204,23 @@ pub(crate) fn difference_zoned_datetime_with_total(
/// (increment = 1、trunc)取 [[Total]]。
/// 2. 否则 TotalTimeDuration(24h 日折算,zoned 面不可达)。
fn total_relative_duration(
difference: &InternalDuration, origin_ns: i128, dest_ns: i128, iso: ((i128, i128, i128), i128), time_zone: &str,
difference: &InternalDuration, origin_ns: i128, dest_ns: i128, iso: IsoDateTime, time_zone: &str,
unit: usize,
) -> Result<f64, AddZonedError> {
let values = difference.to_values();
// zoned 面时区恒在场:unit ≤ day 恒走日历窗口路径。
if unit <= 3 {
let sign = if internal_duration_sign(&values) < 0 { -1 } else { 1 };
let (iso_date, iso_time) = iso;
let iso_date = iso.date;
let iso_time = iso.time_ns;
let epoch_of_date = zoned_epoch_of_date(time_zone, iso_time);
let outcome = nudge_to_calendar_unit(
sign,
&values,
origin_ns,
dest_ns,
iso_date,
1,
let spec = RoundingSpec {
increment: 1,
unit,
InstantRoundingMode::Trunc,
&epoch_of_date,
)
.map_err(|_| AddZonedError::OutOfDateTimeRange)?;
mode: InstantRoundingMode::Trunc,
};
let outcome = nudge_to_calendar_unit(sign, &values, origin_ns, dest_ns, iso_date, spec, &epoch_of_date)
.map_err(|_| AddZonedError::OutOfDateTimeRange)?;
return Ok(outcome.total);
}
let time_ns = duration_time_only_nanoseconds(&values).ok_or(AddZonedError::OutOfTimeRange)?;
+9 -1
View File
@@ -2701,7 +2701,15 @@ fn ta_decode_base64<H: VmHost>(
handling = ta_handling_option(vm, obj, opts_val)?;
}
let units = vm.string_units(string).into_owned();
match crate::ta_codec::decode_base64(&units, alphabet, handling, max_len, write) {
match crate::ta_codec::decode_base64(
&units,
crate::ta_codec::DecodeConfig {
alphabet,
handling,
max_len,
},
write,
) {
Ok(r) => Ok(r),
Err(_) => Err(crate::error::create_syntax_error(vm, "invalid base64 input")),
}