From 27be1753c2d8c1284498a46b4547ccc4d7d7a83c Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:43:32 +0200 Subject: [PATCH] Read the browser clock for NOW(), TODAY() and year-less dates on wasm (#202) --- Cargo.lock | 1 + crates/calc/src/recalc.rs | 14 ++++---------- crates/core/Cargo.toml | 3 +++ crates/core/src/date.rs | 28 ++++++++++++++++++++++++++++ crates/core/src/parse.rs | 14 +++++--------- 5 files changed, 41 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5dce47768..e1d8d3e5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1469,6 +1469,7 @@ dependencies = [ name = "gridcraft-core" version = "0.4.0" dependencies = [ + "js-sys", "serde", "serde_json", "thiserror 2.0.21", diff --git a/crates/calc/src/recalc.rs b/crates/calc/src/recalc.rs index 4cf0c6a21..9df724573 100644 --- a/crates/calc/src/recalc.rs +++ b/crates/calc/src/recalc.rs @@ -919,17 +919,11 @@ impl Calc { } } -/// Local date-time as a serial (1900 system). Wasm without a clock returns a fixed date. -#[allow(clippy::disallowed_methods)] // the clock is read only off wasm +/// Current date-time as a serial (1900 system), from the system clock (browser clock on wasm). +/// UTC; the local offset isn't available without platform calls. Falls back to a fixed date only +/// when no clock is available. pub fn now_serial() -> f64 { - #[cfg(not(target_arch = "wasm32"))] - { - if let Ok(d) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) { - // UTC; local offset isn't available without platform calls. - return 25569.0 + d.as_secs_f64() / 86400.0; - } - } - 46302.5 + gridcraft_core::date::unix_now_ms().map_or(46302.5, gridcraft_core::date::serial_from_unix_ms) } /// Evaluates a formula in the context of a cell without storing it (conditional formats, data diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 26cd19f08..1decf6257 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -12,5 +12,8 @@ workspace = true serde = { workspace = true } thiserror = { workspace = true } +[target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys = "0.3" + [dev-dependencies] serde_json = { workspace = true } diff --git a/crates/core/src/date.rs b/crates/core/src/date.rs index 81fa6c55c..3b126b7fd 100644 --- a/crates/core/src/date.rs +++ b/crates/core/src/date.rs @@ -39,6 +39,26 @@ pub fn days_from_civil(y: i32, m: u32, d: u32) -> i64 { era * 146097 + doe - 719468 } +/// Milliseconds since the Unix epoch (UTC) from the system clock; `None` when the clock is unavailable. +/// On wasm32 `std::time::SystemTime::now` traps, so the browser clock is read through `Date.now()`. +#[allow(clippy::disallowed_methods)] // the std clock is read only off wasm +pub fn unix_now_ms() -> Option { + #[cfg(not(target_arch = "wasm32"))] + { + let d = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).ok()?; + Some(d.as_secs_f64() * 1000.0) + } + #[cfg(target_arch = "wasm32")] + { + Some(js_sys::Date::now()) + } +} + +/// Date serial (1900 system, with fraction) for a Unix time in milliseconds. +pub fn serial_from_unix_ms(ms: f64) -> f64 { + 25569.0 + ms / 86_400_000.0 +} + pub fn civil_from_days(z: i64) -> (i32, u32, u32) { let z = z + 719468; let era = if z >= 0 { z } else { z - 146096 } / 146097; @@ -160,6 +180,14 @@ pub const WEEKDAYS: [&str; 7] = ["Sunday", "Monday", "Tuesday", "Wednesday", "Th mod tests { use super::*; + #[test] + fn serial_from_unix_time() { + assert_eq!(serial_from_unix_ms(0.0), 25569.0); + // 2026-10-10T12:00:00Z + assert_eq!(serial_from_unix_ms(1_791_633_600_000.0), 46305.5); + assert!(unix_now_ms().is_some_and(|ms| serial_from_unix_ms(ms) > 46305.0)); + } + #[test] fn known_serials() { let s = DateSystem::D1900; diff --git a/crates/core/src/parse.rs b/crates/core/src/parse.rs index 7169ae545..b0ed80e07 100644 --- a/crates/core/src/parse.rs +++ b/crates/core/src/parse.rs @@ -273,16 +273,12 @@ fn valid_ymd(sys: DateSystem, y: i64, m: i64, d: i64) -> Option { serial_from_ymd(sys, y, m, d) } -/// The year used for dates typed without one. Fixed per process from the system clock when -/// available (wasm without clock falls back to 2026). -#[allow(clippy::disallowed_methods)] // the clock is read only off wasm +/// The year used for dates typed without one, from the system clock (browser clock on wasm). +/// Falls back to 2026 only when no clock is available. pub fn current_year() -> i64 { - #[cfg(not(target_arch = "wasm32"))] - { - if let Ok(d) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) { - let days = (d.as_secs() / 86400) as i64; - return crate::date::civil_from_days(days).0 as i64; - } + if let Some(ms) = crate::date::unix_now_ms() { + let days = (ms / 86_400_000.0).floor() as i64; + return crate::date::civil_from_days(days).0 as i64; } 2026 }