fix: Date 构造器缺省字段与溢出滚动语义符合规范

多参缺省字段固定为日=1、时分秒毫秒=0(不再取当前时刻);
月/日/时等分量越界按 MakeDay/MakeTime 滚动进位,负月同样归一;
单参字符串与 Date.parse 支持时区偏移(Z/+hh:mm/+hhmm)折算 UTC,
修复此前 NaiveDateTime 解析静默丢弃偏移的问题。
This commit is contained in:
2026-08-16 03:43:42 +08:00
parent 4029259255
commit 2fc5a28876
2 changed files with 165 additions and 75 deletions
+72 -71
View File
@@ -84,6 +84,62 @@ fn naive_from_ms(ms: f64) -> Option<NaiveDateTime> {
dt_from_ms(ms).map(|dt| dt.naive_utc())
}
/// MakeDay + MakeTime 语义组合本地时间戳:月/日/时/分/秒/毫秒分量越界时滚动进位,
/// 再按本地时区映射到真实 UTC 时刻。
///
/// # 边界与前提
/// - 调用方保证各分量已 ToNumber 且非 NaN(任一 NaN 由调用方短路)。
/// - 分量按 ToIntegerOrInfinity 截断;组合时刻超出 chrono 可表示范围返回 NaN。
fn make_local_timestamp(y: f64, m: f64, d: f64, h: f64, min: f64, sec: f64, ms: f64) -> f64 {
// 月溢出归一到 [0, 12),商进位到年份(负月同样成立)。
let m_norm = m.trunc().rem_euclid(12.0);
let y_carry = (m.trunc() / 12.0).floor();
// 基准取当月 1 号零点,日偏移与全日毫秒统一折算(日可为负或越界)。
let base = match NaiveDate::from_ymd_opt((y.trunc() + y_carry) as i32, m_norm as u32 + 1, 1)
.and_then(|nd| nd.and_hms_opt(0, 0, 0))
{
Some(ndt) => ndt,
None => return f64::NAN,
};
let day_ms = base.and_utc().timestamp_millis() as f64;
let time_ms = h.trunc() * 3_600_000.0 + min.trunc() * 60_000.0 + sec.trunc() * 1_000.0 + ms.trunc();
let naive_ms = day_ms + (d.trunc() - 1.0) * 86_400_000.0 + time_ms;
// naive 时刻按本地时区解释(DST 歧义取最早),得到真实 UTC 时间戳。
match DateTime::from_timestamp_millis(naive_ms as i64) {
Some(dt) => dt
.naive_utc()
.and_local_timezone(Local)
.earliest()
.map(|dt| dt.timestamp_millis() as f64)
.unwrap_or(f64::NAN),
None => f64::NAN,
}
}
/// 解析 ISO 8601 日期时间字符串(含时区偏移),返回 UTC 毫秒时间戳;无法解析返回 NaN。
///
/// # 边界与前提
/// - 偏移形态 `Z` / `+hh:mm` / `+hhmm` 均折算为 UTC;无偏移完整时间按 UTC;纯日期按 UTC 零点。
fn parse_iso_timestamp(s: &str) -> f64 {
// 带偏移(含 Z):DateTime 保留偏移直接折算 UTC。
if let Ok(dt) = chrono::DateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f%#z") {
return dt.timestamp_millis() as f64;
}
// 无偏移完整时间按 UTC。
if let Ok(ndt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f") {
return ndt.and_utc().timestamp_millis() as f64;
}
// 纯日期按 UTC 零点。
if let Ok(nd) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
if let Some(ndt) = nd.and_hms_opt(0, 0, 0) {
return ndt.and_utc().timestamp_millis() as f64;
}
}
f64::NAN
}
/// JS `Date()` 构造逻辑:无参取当前时间;单参支持时间戳/字符串/Date 对象;
/// 多参按本地时间字段(年/月/日/时/分/秒/毫秒)组合。非构造调用返回日期字符串。
pub fn date_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
@@ -111,68 +167,31 @@ pub fn date_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let timestamp = if args.len() < 2 {
Utc::now().timestamp_millis() as f64
} else if args.len() > 2 {
let now = chrono::Local::now();
let y_val = oxide_runtime_api::to_number(vm.reg(args[1]));
let m_val = oxide_runtime_api::to_number(vm.reg(args[2]));
if y_val.is_nan() || m_val.is_nan() {
let d_val = if args.len() > 3 { oxide_runtime_api::to_number(vm.reg(args[3])) } else { 1.0 };
let h_val = if args.len() > 4 { oxide_runtime_api::to_number(vm.reg(args[4])) } else { 0.0 };
let min_val = if args.len() > 5 { oxide_runtime_api::to_number(vm.reg(args[5])) } else { 0.0 };
let sec_val = if args.len() > 6 { oxide_runtime_api::to_number(vm.reg(args[6])) } else { 0.0 };
let ms_val = if args.len() > 7 { oxide_runtime_api::to_number(vm.reg(args[7])) } else { 0.0 };
if y_val.is_nan()
|| m_val.is_nan()
|| d_val.is_nan()
|| h_val.is_nan()
|| min_val.is_nan()
|| sec_val.is_nan()
|| ms_val.is_nan()
{
f64::NAN
} else {
let y = y_val.trunc() as i32;
let m = m_val.trunc() as u32;
let d = if args.len() > 3 {
oxide_runtime_api::to_number(vm.reg(args[3])).trunc() as u32
} else {
now.day()
};
let h = if args.len() > 4 {
oxide_runtime_api::to_number(vm.reg(args[4])).trunc() as u32
} else {
now.hour()
};
let min = if args.len() > 5 {
oxide_runtime_api::to_number(vm.reg(args[5])).trunc() as u32
} else {
now.minute()
};
let sec = if args.len() > 6 {
oxide_runtime_api::to_number(vm.reg(args[6])).trunc() as u32
} else {
now.second()
};
let ms = if args.len() > 7 {
oxide_runtime_api::to_number(vm.reg(args[7])).trunc() as u32
} else {
now.timestamp_subsec_millis()
};
NaiveDate::from_ymd_opt(y, m + 1, d)
.and_then(|nd| {
nd.and_hms_milli_opt(h, min, sec, ms)
.and_then(|ndt| ndt.and_local_timezone(Local).earliest())
})
.map(|dt| dt.timestamp_millis() as f64)
.unwrap_or(f64::NAN)
make_local_timestamp(y_val, m_val, d_val, h_val, min_val, sec_val, ms_val)
}
} else {
let val = vm.reg(args[1]);
if val.is_string() {
// SAFETY: val 已确认是字符串值。
let s = unsafe { (*val.as_string_ptr()).to_owned_string() };
let formats = ["%Y-%m-%dT%H:%M:%S%.fZ", "%Y-%m-%dT%H:%M:%S%.f"];
let mut ts = f64::NAN;
for fmt in &formats {
if let Ok(ndt) = NaiveDateTime::parse_from_str(&s, fmt) {
ts = ndt.and_utc().timestamp_millis() as f64;
break;
}
}
if ts.is_nan() {
if let Ok(nd) = NaiveDate::parse_from_str(&s, "%Y-%m-%d") {
if let Some(ndt) = nd.and_hms_opt(0, 0, 0).and_then(|n| n.and_local_timezone(Utc).earliest()) {
ts = ndt.timestamp_millis() as f64;
}
}
}
ts
parse_iso_timestamp(&s)
} else if val.is_int() || val.is_double() {
if val.is_int() {
val.as_int() as f64
@@ -247,25 +266,7 @@ pub fn date_parse<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
ts = dt.timestamp_millis() as f64;
}
if ts.is_nan() {
let formats = [
"%Y-%m-%dT%H:%M:%S%.fZ",
"%Y-%m-%dT%H:%M:%S%.f",
"%Y-%m-%dT%H:%M:%S%.f%:z",
"%Y-%m-%dT%H:%M:%S%.f%#z",
];
for fmt in &formats {
if let Ok(ndt) = NaiveDateTime::parse_from_str(&s, fmt) {
ts = ndt.and_utc().timestamp_millis() as f64;
break;
}
}
}
if ts.is_nan() {
if let Ok(nd) = NaiveDate::parse_from_str(&s, "%Y-%m-%d") {
if let Some(ndt) = nd.and_hms_opt(0, 0, 0).and_then(|n| n.and_local_timezone(Utc).earliest()) {
ts = ndt.timestamp_millis() as f64;
}
}
ts = parse_iso_timestamp(&s);
}
if ts.is_nan() {
if let Ok(nd) = NaiveDate::parse_from_str(&s, "%Y/%m/%d") {
+93 -4
View File
@@ -35,6 +35,20 @@ fn date_parse_invalid() {
assert!(r.as_double().is_nan());
}
#[test]
fn date_parse_iso_with_offset() {
let mut vm = Vm::new();
// 带偏移按偏移折算 UTC:+08:00 → 2019-12-31T16:00:00Z。
let r = eval(&mut vm, "Date.parse('2020-01-01T00:00:00+08:00')").unwrap();
assert_eq!(r.as_double(), 1577808000000.0);
// 负偏移同样折算:-05:00 → 2020-01-01T05:00:00Z。
let r = eval(&mut vm, "Date.parse('2020-01-01T00:00:00-05:00')").unwrap();
assert_eq!(r.as_double(), 1577854800000.0);
// Z 后缀保持 UTC。
let r = eval(&mut vm, "Date.parse('2020-01-01T00:00:00Z')").unwrap();
assert_eq!(r.as_double(), 1577836800000.0);
}
// ── 经 JS 调用 new Date() 构造器 ──
#[test]
@@ -106,6 +120,19 @@ fn date_to_iso_string() {
assert!(s.starts_with("2020-01-01"), "got: {s}");
}
#[test]
fn date_constructor_string_with_offset() {
let mut vm = Vm::new();
// 单参字符串带时区偏移:+08:00 的午夜等于前一日 16:00 UTC。
let r = eval(&mut vm, "new Date('2020-01-01T00:00:00+08:00').getTime()").unwrap();
assert_eq!(r.as_double(), 1577808000000.0);
let r = eval(&mut vm, "new Date('2020-01-01T00:00:00-05:00').getTime()").unwrap();
assert_eq!(r.as_double(), 1577854800000.0);
// 无偏移完整时间按 UTC。
let r = eval(&mut vm, "new Date('2020-01-01T00:00:00').getTime()").unwrap();
assert_eq!(r.as_double(), 1577836800000.0);
}
#[test]
fn date_to_json() {
let mut vm = Vm::new();
@@ -165,13 +192,75 @@ fn date_constructor_uses_local_timezone() {
}
#[test]
fn date_constructor_defaults_from_current_time() {
fn date_constructor_missing_fields_default_fixed() {
let mut vm = Vm::new();
// 缺省参数回退到当前时间分量。
// 缺省日/时分秒毫秒取固定值(日=1、时分秒毫秒=0),不随当前时刻漂移。
let r = eval(&mut vm, "var d = new Date(2020, 5); d.getMonth()").unwrap();
assert_eq!(r.as_double(), 5.0);
let r = eval(&mut vm, "new Date(2020, 5).getDate()").unwrap();
assert!(r.as_double() >= 1.0 && r.as_double() <= 31.0);
let r = eval(&mut vm, "var d = new Date(2020, 5); d.getDate()").unwrap();
assert_eq!(r.as_double(), 1.0);
let r = eval(&mut vm, "var d = new Date(2020, 5); d.getHours()").unwrap();
assert_eq!(r.as_double(), 0.0);
let r = eval(&mut vm, "var d = new Date(2020, 5); d.getMinutes()").unwrap();
assert_eq!(r.as_double(), 0.0);
let r = eval(&mut vm, "var d = new Date(2020, 5); d.getSeconds()").unwrap();
assert_eq!(r.as_double(), 0.0);
let r = eval(&mut vm, "var d = new Date(2020, 5); d.getMilliseconds()").unwrap();
assert_eq!(r.as_double(), 0.0);
}
#[test]
fn date_constructor_month_overflow_rolls_year() {
let mut vm = Vm::new();
// 月分量越界滚动到下一年(MakeDay 语义)。
let r = eval(
&mut vm,
"var d = new Date(2020, 12, 1); [d.getFullYear(), d.getMonth(), d.getDate()].join(',')",
)
.unwrap();
assert_eq!(str_val(&vm, r), "2021,0,1");
// 多月越界同样进位。
let r = eval(
&mut vm,
"var d = new Date(2020, 24, 1); [d.getFullYear(), d.getMonth(), d.getDate()].join(',')",
)
.unwrap();
assert_eq!(str_val(&vm, r), "2022,0,1");
}
#[test]
fn date_constructor_zero_date_rolls_back() {
let mut vm = Vm::new();
// 日为 0 回退到上月末。
let r = eval(
&mut vm,
"var d = new Date(2020, 0, 0); [d.getFullYear(), d.getMonth(), d.getDate()].join(',')",
)
.unwrap();
assert_eq!(str_val(&vm, r), "2019,11,31");
}
#[test]
fn date_constructor_negative_month_rolls_back() {
let mut vm = Vm::new();
// 负月回退到上一年。
let r = eval(
&mut vm,
"var d = new Date(2020, -1, 1); [d.getFullYear(), d.getMonth(), d.getDate()].join(',')",
)
.unwrap();
assert_eq!(str_val(&vm, r), "2019,11,1");
}
#[test]
fn date_constructor_time_overflow_rolls_day() {
let mut vm = Vm::new();
// 小时越界滚动到下一天。
let r = eval(&mut vm, "var d = new Date(2020, 0, 1, 24); d.getDate()").unwrap();
assert_eq!(r.as_double(), 2.0);
// 秒越界同样滚动。
let r = eval(&mut vm, "var d = new Date(2020, 0, 1, 0, 0, 60); d.getMinutes()").unwrap();
assert_eq!(r.as_double(), 1.0);
}
#[test]