//! Time: the fixed RFC 3339 form (SPEC conventions), the `Europe/Stockholm` constant (SPEC §10.4) and an injectable //! clock so tests control "now". use std::sync::{Arc, Mutex}; use chrono::{DateTime, NaiveDate, SecondsFormat, Utc}; use chrono_tz::Tz; /// The household's time zone; a constant in code, not configuration (SPEC §10.4). pub const STOCKHOLM: Tz = chrono_tz::Europe::Stockholm; /// Formats an instant in the fixed form `2026-10-17T06:00:00.000Z` (millisecond precision, literal `Z`). pub fn format_ts(instant: DateTime) -> String { instant.to_rfc3339_opts(SecondsFormat::Millis, true) } /// Parses a timestamp in exactly the fixed form; anything else (other offsets, other precision) is `None`. pub fn parse_ts(text: &str) -> Option> { let instant = DateTime::parse_from_rfc3339(text).ok()?.with_timezone(&Utc); (format_ts(instant) == text).then_some(instant) } /// Parses a calendar date `YYYY-MM-DD` (exactly that form). pub fn parse_date(text: &str) -> Option { let date = NaiveDate::parse_from_str(text, "%Y-%m-%d").ok()?; (date.format("%Y-%m-%d").to_string() == text).then_some(date) } /// Today's calendar date in Europe/Stockholm. pub fn stockholm_today(now: DateTime) -> NaiveDate { now.with_timezone(&STOCKHOLM).date_naive() } /// Source of "now". The server uses [`SystemClock`]; tests use [`FixedClock`]. pub trait Clock: Send + Sync + 'static { fn now(&self) -> DateTime; /// `now` in the fixed RFC 3339 form. fn now_ts(&self) -> String { format_ts(self.now()) } } /// The real clock. #[derive(Debug, Clone, Copy, Default)] pub struct SystemClock; impl Clock for SystemClock { fn now(&self) -> DateTime { Utc::now() } } /// A clock that returns a set instant until changed. #[derive(Debug, Clone)] pub struct FixedClock(Arc>>); impl FixedClock { pub fn new(instant: DateTime) -> Self { Self(Arc::new(Mutex::new(instant))) } /// Sets the instant every clone of this clock returns from now on. pub fn set(&self, instant: DateTime) { *crate::lock(&self.0) = instant; } } impl Clock for FixedClock { fn now(&self) -> DateTime { *crate::lock(&self.0) } } #[cfg(test)] mod tests { use super::*; #[test] fn fixed_form_round_trip() { let t = parse_ts("2026-10-17T06:00:00.000Z").unwrap(); assert_eq!(format_ts(t), "2026-10-17T06:00:00.000Z"); assert!(parse_ts("2026-10-17T06:00:00Z").is_none()); assert!(parse_ts("2026-10-17T08:00:00.000+02:00").is_none()); } #[test] fn dates() { assert!(parse_date("2026-02-29").is_none()); assert!(parse_date("2026-2-01").is_none()); assert_eq!(parse_date("2028-02-29").unwrap().to_string(), "2028-02-29"); } #[test] fn stockholm_day_boundary() { // 23:30 UTC on 17 October is 01:30 on 18 October in Stockholm (CEST, UTC+2). let t = parse_ts("2026-10-17T23:30:00.000Z").unwrap(); assert_eq!(stockholm_today(t).to_string(), "2026-10-18"); } #[test] fn fixed_clock_is_shared_by_clones() { let clock = FixedClock::new(parse_ts("2026-10-17T06:00:00.000Z").unwrap()); let other = clock.clone(); clock.set(parse_ts("2026-10-18T06:00:00.000Z").unwrap()); assert_eq!(other.now_ts(), "2026-10-18T06:00:00.000Z"); } }