Read the browser clock for NOW(), TODAY() and year-less dates on wasm (#202)

This commit is contained in:
MarekSpodBiedry
2026-10-10 23:43:32 +02:00
parent 90b3c41eec
commit 27be1753c2
5 changed files with 41 additions and 19 deletions
Generated
+1
View File
@@ -1469,6 +1469,7 @@ dependencies = [
name = "gridcraft-core"
version = "0.4.0"
dependencies = [
"js-sys",
"serde",
"serde_json",
"thiserror 2.0.21",
+4 -10
View File
@@ -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
+3
View File
@@ -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 }
+28
View File
@@ -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<f64> {
#[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;
+5 -9
View File
@@ -273,16 +273,12 @@ fn valid_ymd(sys: DateSystem, y: i64, m: i64, d: i64) -> Option<f64> {
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
}