mirror of
https://github.com/storytold/craft-launcher.git
synced 2026-11-02 18:07:04 +00:00
Merge pull request #726 from dylanmtaylor/time/chrono-1785
Use jiff instead of hand-rolled UTC conversion
This commit is contained in:
@@ -113,6 +113,12 @@ 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. The one
|
||||
exception is range extension: past the years jiff can represent, shift the date by whole 400-year
|
||||
Gregorian cycles (146,097 days, after which the calendar repeats), let jiff compute the month and
|
||||
day, and add the cycles back to the year (splitting seconds into hours, minutes and seconds is
|
||||
plain arithmetic).
|
||||
|
||||
## 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",
|
||||
@@ -5882,6 +5883,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 beyond the 400-year range extension; 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 }
|
||||
|
||||
+34
-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,35 @@ 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). jiff's civil calendar only spans years -9999..=9999,
|
||||
/// far narrower than the whole `i64` the old hand-rolled Hinnant code converted, so the instant is
|
||||
/// first moved into range by whole 400-year Gregorian cycles (the calendar repeats every 400 years
|
||||
/// = 146_097 days) and the real year restored afterwards. jiff still does the month/day; the cycle
|
||||
/// count only shifts the year, keeping every `i64` exact (e.g. `i64::MAX` → year 292277026596,
|
||||
/// `i64::MIN` → year -292277022657), with no epoch fallback for a value the old code converted.
|
||||
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 days = secs.div_euclid(86_400);
|
||||
let (cycles, rest) = (days.div_euclid(146_097), days.rem_euclid(146_097));
|
||||
// `rest` is in `0..146_097` (under one 400-year cycle), so `epoch + rest days` always lands in
|
||||
// 1970..=2369 — comfortably inside jiff's range — and `rest * 86_400` never overflows.
|
||||
let date = epoch.checked_add(jiff::SignedDuration::from_secs(rest * 86_400)).unwrap_or(epoch);
|
||||
// Restore the real year through `i128` so `cycles * 400` can't overflow at the `i64` extremes.
|
||||
let y = i128::from(date.year()) + i128::from(cycles) * 400;
|
||||
let year = i64::try_from(y).unwrap_or(if y < 0 { i64::MIN } else { i64::MAX });
|
||||
(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"];
|
||||
@@ -161,6 +165,14 @@ mod tests {
|
||||
fn shows_dates_people_read() {
|
||||
assert_eq!(format_date(1_791_471_170), "Oct 8, 2026");
|
||||
assert_eq!(civil_from_unix(951_782_400), (2000, 2, 29));
|
||||
// Year 10000 is past jiff's own calendar range, but the 400-year cycle reduction keeps the
|
||||
// old conversion (jiff spans only -9999..=9999).
|
||||
assert_eq!(civil_from_unix(253_402_300_800), (10000, 1, 1));
|
||||
// The `i64` extremes stay the dates the old Hinnant code produced, not the epoch.
|
||||
assert_eq!(civil_from_unix(i64::MAX), (292_277_026_596, 12, 4));
|
||||
assert_eq!(civil_from_unix(i64::MIN), (-292_277_022_657, 1, 27));
|
||||
assert_eq!(format_date(i64::MAX), "Dec 4, 292277026596");
|
||||
assert_eq!(format_date(i64::MIN), "Jan 27, -292277022657");
|
||||
let now = 1_791_471_170;
|
||||
assert_eq!(format_ago(now - 5, now), "just now");
|
||||
assert_eq!(format_ago(now - 600, now), "10 min ago");
|
||||
|
||||
@@ -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 }
|
||||
|
||||
+20
-12
@@ -493,20 +493,23 @@ 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). jiff spans only years -9999..=9999, far
|
||||
/// narrower than the whole `i64` the old hand-rolled Hinnant code converted, so the instant is
|
||||
/// moved into range by whole 400-year Gregorian cycles (146_097 days) and the real year restored
|
||||
/// afterwards. jiff still does the month/day; the cycle count only shifts the year, keeping every
|
||||
/// `i64` exact (e.g. `i64::MAX` → year 292277026596) with no epoch fallback.
|
||||
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 days = secs.div_euclid(86_400);
|
||||
let (cycles, rest) = (days.div_euclid(146_097), days.rem_euclid(146_097));
|
||||
// `rest` is in `0..146_097` (under one 400-year cycle), so `epoch + rest days` lands in
|
||||
// 1970..=2369 (inside jiff's range) and `rest * 86_400` never overflows.
|
||||
let date = epoch.checked_add(jiff::SignedDuration::from_secs(rest * 86_400)).unwrap_or(epoch);
|
||||
// Restore the real year through `i128` so `cycles * 400` can't overflow at the `i64` extremes.
|
||||
let y = i128::from(date.year()) + i128::from(cycles) * 400;
|
||||
let y = i64::try_from(y).unwrap_or(if y < 0 { i64::MIN } else { i64::MAX });
|
||||
format!("{y:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", date.month(), date.day(), rem / 3600, rem % 3600 / 60, rem % 60)
|
||||
}
|
||||
|
||||
// ---- versions -----------------------------------------------------------------------------------
|
||||
@@ -692,6 +695,11 @@ mod tests {
|
||||
fn dates_and_versions() {
|
||||
assert_eq!(rfc3339(0), "1970-01-01T00:00:00Z");
|
||||
assert_eq!(rfc3339(1_791_500_000), "2026-10-08T22:53:20Z");
|
||||
// Year 10000 and the `i64` extremes are past jiff's own calendar range (-9999..=9999), but
|
||||
// the 400-year cycle reduction keeps the dates the old Hinnant code produced, not the epoch.
|
||||
assert_eq!(rfc3339(253_402_300_800), "10000-01-01T00:00:00Z");
|
||||
assert_eq!(rfc3339(i64::MAX), "292277026596-12-04T15:30:07Z");
|
||||
assert_eq!(rfc3339(i64::MIN), "-292277022657-01-27T08:29:52Z");
|
||||
assert_eq!(compare_versions("0.10.0", "0.9.0"), std::cmp::Ordering::Greater);
|
||||
assert_eq!(compare_versions("1.0.0", "1.0.0-rc.1"), std::cmp::Ordering::Greater);
|
||||
assert!(parse_version("1.2").is_none() && parse_version(".building-1.2.3").is_none());
|
||||
|
||||
Reference in New Issue
Block a user