mirror of
https://github.com/storytold/craft-launcher.git
synced 2026-11-03 17:20:49 +00:00
Use jiff for UTC/calendar conversion instead of hand-rolled copies
This repo already uses jiff (ui-egui, for the system time zone), so the hand-rolled Howard Hinnant civil/days-from-civil copies move to jiff rather than pulling in a second time crate. jiff is lifted to a workspace dependency: - crates/core/src/time.rs: parse_rfc3339's days_from_civil and civil_from_unix - xtask/src/local.rs: rfc3339 (feed timestamps) ui-egui now takes jiff from the workspace. core keeps only civil-date math (no time zone), so the wasm build is unaffected. Output and parsing semantics are unchanged and pinned by the existing tests (RFC 3339 parsing, leap days, day rollover, the feed's YYYY-MM-DDTHH:MM:SSZ). AGENTS.md now says time goes through jiff. Follow-up to storytold/cadcraft#1785
This commit is contained in:
8 files changed
+35
-35
No files matched your search
@@ -113,6 +113,8 @@ only; English where there is none): never bundle a CJK font, they are 10 MB and
|
||||
GitHub release JSON fixtures (small, committed under `crates/core/tests/fixtures/`).
|
||||
5. When fetching anything, use a generic `ArtCraft-Launcher/<version>` User-Agent and never put
|
||||
a person's name, email or other personal details in requests.
|
||||
6. **Time goes through `jiff`** (workspace dependency). Never hand-roll UTC/calendar conversion
|
||||
(days-to-date algorithms, leap-year math); a module owns only its output format string.
|
||||
|
||||
## 3. Before you finish a task
|
||||
|
||||
|
||||
Generated
+4
@@ -303,6 +303,7 @@ dependencies = [
|
||||
name = "artcraft-launcher-core"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"jiff",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -5875,6 +5876,9 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"jiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
|
||||
@@ -28,6 +28,8 @@ serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
semver = { version = "1", features = ["serde"] }
|
||||
log = "0.4"
|
||||
# All date/time conversion goes through jiff (no hand-rolled calendar code; AGENTS.md).
|
||||
jiff = "0.2"
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 2
|
||||
|
||||
@@ -14,3 +14,5 @@ workspace = true
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
# Calendar conversion (jiff); see AGENTS.md. Only civil-date math is used, no time zone.
|
||||
jiff = { workspace = true }
|
||||
+16
-22
@@ -1,4 +1,5 @@
|
||||
//! Just enough calendar to read GitHub's timestamps and show them, without a date crate.
|
||||
//! Just enough calendar to read GitHub's timestamps and show them (the conversion goes through
|
||||
//! jiff; this module owns only the parsing and the output format strings).
|
||||
//!
|
||||
//! # Internationalisation
|
||||
//! [`format_ago`] and [`format_date`] are English-only fallbacks. Translatable UI must use
|
||||
@@ -64,32 +65,25 @@ pub fn parse_rfc3339(text: &str) -> Option<i64> {
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
Some(days_from_civil(y, mo, d) * 86_400 + h * 3600 + mi * 60 + s - offset)
|
||||
Some(days_from_civil(y, mo, d)? * 86_400 + h * 3600 + mi * 60 + s - offset)
|
||||
}
|
||||
|
||||
/// Days since 1970-01-01 (Howard Hinnant's algorithm).
|
||||
fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
|
||||
let y = if m <= 2 { y - 1 } else { y };
|
||||
let era = y.div_euclid(400);
|
||||
let yoe = y - era * 400;
|
||||
let mp = (m + 9) % 12;
|
||||
let doy = (153 * mp + 2) / 5 + d - 1;
|
||||
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
||||
era * 146_097 + doe - 719_468
|
||||
/// Days since 1970-01-01 (through jiff's civil calendar). A day past the month's length rolls over
|
||||
/// the way the old Hinnant arithmetic did (e.g. `2026-02-30` → `2026-03-02`). `None` for a year
|
||||
/// outside jiff's range.
|
||||
fn days_from_civil(y: i64, m: i64, d: i64) -> Option<i64> {
|
||||
let first = jiff::civil::Date::new(i16::try_from(y).ok()?, i8::try_from(m).ok()?, 1).ok()?;
|
||||
let date = first.checked_add(jiff::SignedDuration::from_hours(d.checked_sub(1)?.checked_mul(24)?)).ok()?;
|
||||
Some(date.duration_since(jiff::civil::Date::constant(1970, 1, 1)).as_secs() / 86_400)
|
||||
}
|
||||
|
||||
/// (year, month, day) for a Unix time.
|
||||
/// (year, month, day) for a Unix time (UTC). A time outside jiff's calendar range reads as the
|
||||
/// epoch rather than panicking.
|
||||
pub fn civil_from_unix(secs: i64) -> (i64, u32, u32) {
|
||||
let z = secs.div_euclid(86_400) + 719_468;
|
||||
let era = z.div_euclid(146_097);
|
||||
let doe = z - era * 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 m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||
let y = yoe + era * 400 + i64::from(m <= 2);
|
||||
(y, u32::try_from(m).unwrap_or(1), u32::try_from(d).unwrap_or(1))
|
||||
let epoch = jiff::civil::Date::constant(1970, 1, 1);
|
||||
let whole_days = jiff::SignedDuration::from_secs(secs.div_euclid(86_400).saturating_mul(86_400));
|
||||
let date = epoch.checked_add(whole_days).unwrap_or(epoch);
|
||||
(i64::from(date.year()), u32::try_from(date.month()).unwrap_or(1), u32::try_from(date.day()).unwrap_or(1))
|
||||
}
|
||||
|
||||
const MONTHS: [&str; 12] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||
|
||||
@@ -24,7 +24,7 @@ sys-locale = "0.3"
|
||||
# Finding the Simplified Chinese face in a system font collection; egui already uses it.
|
||||
skrifa = { version = "0.44", default-features = false, features = ["std"] }
|
||||
# The system time zone, so dates read as this computer's calendar; already in the desktop build.
|
||||
jiff = "0.2"
|
||||
jiff = { workspace = true }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
# The browser's time zone, for the same dates.
|
||||
|
||||
@@ -7,3 +7,6 @@ publish = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
jiff = { workspace = true }
|
||||
+5
-12
@@ -493,20 +493,13 @@ fn unix_now() -> i64 {
|
||||
SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| i64::try_from(d.as_secs()).unwrap_or(0))
|
||||
}
|
||||
|
||||
/// `2026-10-10T12:00:00Z` (civil-from-days, Howard Hinnant's algorithm).
|
||||
/// `2026-10-10T12:00:00Z` (through jiff's civil calendar).
|
||||
fn rfc3339(secs: i64) -> String {
|
||||
let days = secs.div_euclid(86_400);
|
||||
let rem = secs.rem_euclid(86_400);
|
||||
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 m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||
let y = yoe + era * 400 + i64::from(m <= 2);
|
||||
format!("{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}Z", rem / 3600, rem % 3600 / 60, rem % 60)
|
||||
let epoch = jiff::civil::Date::constant(1970, 1, 1);
|
||||
let whole_days = jiff::SignedDuration::from_secs(secs.div_euclid(86_400).saturating_mul(86_400));
|
||||
let date = epoch.checked_add(whole_days).unwrap_or(epoch);
|
||||
format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", date.year(), date.month(), date.day(), rem / 3600, rem % 3600 / 60, rem % 60)
|
||||
}
|
||||
|
||||
// ---- versions -----------------------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user