fix: Date 构造器缺省字段与溢出滚动语义符合规范
多参缺省字段固定为日=1、时分秒毫秒=0(不再取当前时刻); 月/日/时等分量越界按 MakeDay/MakeTime 滚动进位,负月同样归一; 单参字符串与 Date.parse 支持时区偏移(Z/+hh:mm/+hhmm)折算 UTC, 修复此前 NaiveDateTime 解析静默丢弃偏移的问题。
This commit is contained in:
@@ -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") {
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user