use time::format_description::well_known::Rfc3339; use time::{OffsetDateTime, UtcOffset}; /// Current UTC time as an RFC3339 string with second precision, e.g. /// `2026-04-15T10:32:00Z`. Subsecond fractions are deliberately stripped /// so every caller produces the same filename shape — the server's /// filename parser expects exactly `YYYY-MM-DDTHH-MM-SSZ` after the /// `:` → `-` substitution, and dictation events don't need finer /// resolution than one second. pub fn now_rfc3339() -> String { OffsetDateTime::now_utc() .replace_nanosecond(0) .expect("0 is always a valid nanosecond value") .format(&Rfc3339) .expect("RFC3339 formatting of OffsetDateTime is infallible") } /// Map an RFC3339 timestamp to a filesystem-safe stem by replacing every /// `:` with `-`. Intentionally naive — RFC3339 has no colons outside the /// time/offset portion, so the result is bijective against /// `filename_stem_to_recorded_at`. /// /// Example: `"2026-04-15T10:32:00Z"` → `"2026-04-15T10-32-00Z"`. pub fn recorded_at_to_filename_stem(recorded_at: &str) -> String { recorded_at.replace(':', "-") } /// Reverse of `recorded_at_to_filename_stem`. Only undoes the mapping in /// the time portion (after `T`) so date-part hyphens stay intact. /// /// Example: `"2026-04-15T10-32-00Z"` → `"2026-04-15T10:32:00Z"`. pub fn filename_stem_to_recorded_at(stem: &str) -> String { match stem.split_once('T') { Some((date, time)) => format!("{}T{}", date, time.replace('-', ":")), None => stem.to_string(), } } /// Parse an RFC3339 timestamp. Returns `None` on any error so callers /// can fall back to a lenient display path without having to classify /// failures. pub fn parse_rfc3339(s: &str) -> Option { OffsetDateTime::parse(s, &Rfc3339).ok() } /// Extract `HH:MM` in the local timezone from an RFC3339 UTC timestamp. /// On parse failure, returns the raw string truncated to its first 16 /// characters — the user still sees something readable even if the /// value is shaped unexpectedly. pub fn extract_hhmm(ts: &str) -> String { match parse_rfc3339(ts) { Some(t) => { let local = t.to_offset(UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC)); format!("{:02}:{:02}", local.hour(), local.minute()) } None => ts.chars().take(16).collect(), } } #[cfg(test)] mod tests { use super::*; #[test] fn filename_stem_roundtrip() { assert_eq!( filename_stem_to_recorded_at("2026-04-15T10-32-00Z"), "2026-04-15T10:32:00Z" ); assert_eq!( filename_stem_to_recorded_at("2026-04-15T10-32-00.123Z"), "2026-04-15T10:32:00.123Z" ); } #[test] fn recorded_at_stem_roundtrip_utc() { let original = "2026-04-15T10:32:00Z"; let stem = recorded_at_to_filename_stem(original); assert_eq!(stem, "2026-04-15T10-32-00Z"); assert_eq!(filename_stem_to_recorded_at(&stem), original); } #[test] fn recorded_at_stem_roundtrip_with_milliseconds() { let original = "2026-04-15T10:32:00.123Z"; let stem = recorded_at_to_filename_stem(original); assert_eq!(filename_stem_to_recorded_at(&stem), original); } #[test] fn now_rfc3339_has_expected_shape() { let now = now_rfc3339(); assert!(now.starts_with("20"), "unexpected year prefix: {now}"); assert!(now.contains('T'), "missing T separator: {now}"); } /// Regression guard: subsecond precision must be stripped. A single /// nanosecond leak would feed the server filename parser /// `YYYY-MM-DDTHH-MM-SS.nnnnnnnnnZ.m4a`, which it treats as /// malformed — the resulting `