mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-22 19:08:51 +00:00
Use chrono for UTC/calendar conversion instead of hand-rolled copies
Replaces the two hand-rolled Howard Hinnant days-from-civil copies with chrono as a workspace dependency: - crates/collab/src/message.rs: utc_date (YYYY-MM-DD HH:MM UTC) - crates/ui-egui/src/home_recents.rs: date (Mon D, YYYY, UTC; keeps the translated month names and trf formatting) Output is unchanged. Neither function had a test, so each gains one pinning the exact strings the old code produced (leap days included). chrono uses only std, so wasm stays safe. AGENTS.md/CLAUDE.md now say time goes through chrono. Follow-up to storytold/cadcraft#1785
This commit is contained in:
1 parent
5fa586306a
commit
0e3876e6d6
8 files changed
+53
-23
No files matched your search
@@ -81,6 +81,7 @@ code.
|
||||
L6 depends on egui/eframe/rfd/winit. **The UI crate is swappable.**
|
||||
- **The UI is thin**: panels read engine state and act through `app.run(id, params)` /
|
||||
`app.set_props(ids, props)`. Colours come from `theme::Tokens`.
|
||||
- **Time goes through `chrono`** (workspace dependency). Never hand-roll UTC/calendar conversion (days-to-date algorithms, leap-year math); a crate owns only its output format string.
|
||||
- **Rust only** (no handwritten JS/TS). **Never break wasm** (`cargo xtask wasm`).
|
||||
|
||||
### Quality gates (before every commit)
|
||||
|
||||
@@ -81,6 +81,7 @@ code.
|
||||
L6 depends on egui/eframe/rfd/winit. **The UI crate is swappable.**
|
||||
- **The UI is thin**: panels read engine state and act through `app.run(id, params)` /
|
||||
`app.set_props(ids, props)`. Colours come from `theme::Tokens`.
|
||||
- **Time goes through `chrono`** (workspace dependency). Never hand-roll UTC/calendar conversion (days-to-date algorithms, leap-year math); a crate owns only its output format string.
|
||||
- **Rust only** (no handwritten JS/TS). **Never break wasm** (`cargo xtask wasm`).
|
||||
|
||||
### Quality gates (before every commit)
|
||||
|
||||
Generated
+11
@@ -742,6 +742,15 @@ dependencies = [
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
version = "5.4.1"
|
||||
@@ -1499,6 +1508,7 @@ name = "figcraft-collab"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"figcraft-color",
|
||||
"figcraft-doc",
|
||||
"figcraft-geom",
|
||||
@@ -1744,6 +1754,7 @@ name = "figcraft-ui-egui"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"egui",
|
||||
"egui_extras",
|
||||
"figcraft-color",
|
||||
|
||||
@@ -45,6 +45,8 @@ serde = { version = "1", features = ["derive", "rc"] }
|
||||
serde_json = { version = "1", features = ["float_roundtrip"] }
|
||||
regex = "1.13"
|
||||
log = "0.4"
|
||||
# All date/time conversion goes through chrono (no hand-rolled calendar code; AGENTS.md).
|
||||
chrono = { version = "0.4.45", default-features = false, features = ["std"] }
|
||||
proptest = "1"
|
||||
imbl = { version = "7", features = ["serde"] }
|
||||
postcard = { version = "1", default-features = false, features = ["alloc"] }
|
||||
|
||||
@@ -13,6 +13,7 @@ figcraft-doc = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
imbl = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
postcard = { workspace = true }
|
||||
|
||||
@@ -699,19 +699,8 @@ pub enum ServerMsg {
|
||||
/// `YYYY-MM-DD HH:MM UTC` for seconds since 1970 (how servers and apps show the times in these
|
||||
/// messages).
|
||||
pub fn utc_date(secs: u64) -> String {
|
||||
let days = i64::try_from(secs / 86_400).unwrap_or(0);
|
||||
let (h, m) = ((secs % 86_400) / 3600, (secs % 3600) / 60);
|
||||
// Days to a civil date (proleptic Gregorian).
|
||||
let z = days + 719_468;
|
||||
let era = z.div_euclid(146_097);
|
||||
let doe = z.rem_euclid(146_097);
|
||||
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
|
||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
let mp = (5 * doy + 2) / 153;
|
||||
let d = doy - (153 * mp + 2) / 5 + 1;
|
||||
let mo = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||
let y = yoe + era * 400 + i64::from(mo <= 2);
|
||||
format!("{y:04}-{mo:02}-{d:02} {h:02}:{m:02} UTC")
|
||||
let t = i64::try_from(secs).ok().and_then(|s| chrono::DateTime::from_timestamp(s, 0)).unwrap_or_default();
|
||||
t.format("%Y-%m-%d %H:%M UTC").to_string()
|
||||
}
|
||||
|
||||
/// The binary form of `msg`: [`WIRE_VERSION`], then postcard.
|
||||
@@ -752,3 +741,19 @@ pub fn decode_json<T: DeserializeOwned>(text: &str, max_bytes: usize) -> Result<
|
||||
}
|
||||
serde_json::from_str(text).map_err(|e| Error::Decode(e.to_string()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// `utc_date` keeps the exact `YYYY-MM-DD HH:MM UTC` output the hand-rolled civil-from-days
|
||||
/// code produced (leap days and all).
|
||||
#[test]
|
||||
fn utc_date_formats_seconds_since_1970() {
|
||||
assert_eq!(utc_date(0), "1970-01-01 00:00 UTC");
|
||||
assert_eq!(utc_date(1_000_000), "1970-01-12 13:46 UTC");
|
||||
assert_eq!(utc_date(951_782_400), "2000-02-29 00:00 UTC");
|
||||
assert_eq!(utc_date(1_709_164_800), "2024-02-29 00:00 UTC");
|
||||
assert_eq!(utc_date(1_791_208_980), "2026-10-05 14:03 UTC");
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ egui = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
log = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
image = { workspace = true }
|
||||
egui_extras = { workspace = true }
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use chrono::Datelike;
|
||||
use egui::{Align2, Color32, CornerRadius, Id, Key, Rect, Response, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
|
||||
use figcraft_engine::Prefs;
|
||||
use figcraft_format::thumbnail::{FileKind, Preview};
|
||||
@@ -202,16 +203,8 @@ const MONTHS: [&str; 12] = [
|
||||
|
||||
/// `secs` since 1970 as a date in UTC: "Mar 4, 2026".
|
||||
fn date(secs: u64) -> String {
|
||||
// Days to a civil date (Howard Hinnant's days_from_civil, inverted).
|
||||
let z = i64::try_from(secs / 86_400).unwrap_or(0) + 719_468;
|
||||
let era = z.div_euclid(146_097);
|
||||
let doe = z.rem_euclid(146_097);
|
||||
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
|
||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
let mp = (5 * doy + 2) / 153;
|
||||
let day = doy - (153 * mp + 2) / 5 + 1;
|
||||
let month = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||
let year = yoe + era * 400 + i64::from(month <= 2);
|
||||
let dt = i64::try_from(secs).ok().and_then(|s| chrono::DateTime::from_timestamp(s, 0)).unwrap_or_default().naive_utc();
|
||||
let (year, month, day) = (i64::from(dt.year()), i64::from(dt.month()), i64::from(dt.day()));
|
||||
let name = usize::try_from(month - 1).ok().and_then(|m| MONTHS.get(m)).map_or("", |m| tr(m));
|
||||
trf("{month} {day}, {year}", &[("month", &name), ("day", &day), ("year", &year)])
|
||||
}
|
||||
@@ -965,3 +958,18 @@ fn list(ui: &mut Ui, t: &Tokens, recents: &mut Recents, files: &[Shown], now: u6
|
||||
arrows(ui, recents, files, 0, 1);
|
||||
action
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// `date` keeps the exact "Mon D, YYYY" (UTC) output the hand-rolled days-from-civil code
|
||||
/// produced. With no catalog loaded the month and format strings pass through in English.
|
||||
#[test]
|
||||
fn date_formats_seconds_since_1970_in_utc() {
|
||||
assert_eq!(date(0), "Jan 1, 1970");
|
||||
assert_eq!(date(951_782_400), "Feb 29, 2000");
|
||||
assert_eq!(date(1_709_164_800), "Feb 29, 2024");
|
||||
assert_eq!(date(1_741_046_400), "Mar 4, 2025");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user