1use crate::errors::AppError;
13use crate::i18n::validation;
14use chrono::{DateTime, TimeZone, Utc};
15use chrono_tz::Tz;
16use std::sync::OnceLock;
17
18static GLOBAL_TZ: OnceLock<Tz> = OnceLock::new();
19
20fn resolve_tz_from_xdg() -> Result<Tz, AppError> {
25 match crate::config::get_setting("display.tz") {
26 Ok(Some(v)) if !v.trim().is_empty() => v
27 .trim()
28 .parse::<Tz>()
29 .map_err(|_| AppError::Validation(validation::invalid_tz(v.trim()))),
30 _ => Ok(Tz::UTC),
31 }
32}
33
34pub fn init(explicit: Option<Tz>) -> Result<(), AppError> {
42 let fuso = match explicit {
43 Some(tz) => tz,
44 None => resolve_tz_from_xdg()?,
45 };
46 let _ = GLOBAL_TZ.set(fuso);
47 Ok(())
48}
49
50pub fn current_tz() -> Tz {
54 *GLOBAL_TZ.get_or_init(|| resolve_tz_from_xdg().unwrap_or(Tz::UTC))
55}
56
57pub fn format_iso(ts: DateTime<Utc>) -> String {
62 let fuso = current_tz();
63 ts.with_timezone(&fuso)
64 .format("%Y-%m-%dT%H:%M:%S%:z")
65 .to_string()
66}
67
68pub fn epoch_to_iso(epoch: i64) -> String {
73 Utc.timestamp_opt(epoch, 0)
74 .single()
75 .map(format_iso)
76 .unwrap_or_else(|| "1970-01-01T00:00:00+00:00".to_string())
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn utc_default_when_xdg_unset() {
85 let result = resolve_tz_from_xdg();
88 assert!(result.is_ok(), "xdg tz resolve must not error when unset/valid");
89 }
90
91 #[test]
92 fn epoch_zero_yields_utc_iso() {
93 let result = {
94 let tz = Tz::UTC;
95 Utc.timestamp_opt(0, 0)
96 .single()
97 .map(|dt| {
98 dt.with_timezone(&tz)
99 .format("%Y-%m-%dT%H:%M:%S%:z")
100 .to_string()
101 })
102 .unwrap_or_else(|| "1970-01-01T00:00:00+00:00".to_string())
103 };
104 assert_eq!(result, "1970-01-01T00:00:00+00:00");
105 }
106
107 #[test]
108 fn format_iso_utc_preserves_zero_offset() {
109 let ts = Utc.timestamp_opt(1_705_320_000, 0).single().unwrap();
110 let result = ts
111 .with_timezone(&Tz::UTC)
112 .format("%Y-%m-%dT%H:%M:%S%:z")
113 .to_string();
114 assert_eq!(result, "2024-01-15T12:00:00+00:00");
115 }
116
117 #[test]
118 fn format_iso_sao_paulo_applies_offset() {
119 let ts = Utc.timestamp_opt(1_705_320_000, 0).single().unwrap();
120 let sao_paulo: Tz = "America/Sao_Paulo".parse().unwrap();
121 let result = ts
122 .with_timezone(&sao_paulo)
123 .format("%Y-%m-%dT%H:%M:%S%:z")
124 .to_string();
125 assert!(
126 result.contains("-03:00"),
127 "expected offset -03:00, got: {result}"
128 );
129 }
130
131 #[test]
132 fn invalid_iana_parse_is_validation() {
133 let bad = "Invalid/Nonexistent";
134 let parsed = bad.parse::<Tz>();
135 assert!(parsed.is_err());
136 }
137}