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!(
89 result.is_ok(),
90 "xdg tz resolve must not error when unset/valid"
91 );
92 }
93
94 #[test]
95 fn epoch_zero_yields_utc_iso() {
96 let result = {
97 let tz = Tz::UTC;
98 Utc.timestamp_opt(0, 0)
99 .single()
100 .map(|dt| {
101 dt.with_timezone(&tz)
102 .format("%Y-%m-%dT%H:%M:%S%:z")
103 .to_string()
104 })
105 .unwrap_or_else(|| "1970-01-01T00:00:00+00:00".to_string())
106 };
107 assert_eq!(result, "1970-01-01T00:00:00+00:00");
108 }
109
110 #[test]
111 fn format_iso_utc_preserves_zero_offset() {
112 let ts = Utc.timestamp_opt(1_705_320_000, 0).single().unwrap();
113 let result = ts
114 .with_timezone(&Tz::UTC)
115 .format("%Y-%m-%dT%H:%M:%S%:z")
116 .to_string();
117 assert_eq!(result, "2024-01-15T12:00:00+00:00");
118 }
119
120 #[test]
121 fn format_iso_sao_paulo_applies_offset() {
122 let ts = Utc.timestamp_opt(1_705_320_000, 0).single().unwrap();
123 let sao_paulo: Tz = "America/Sao_Paulo".parse().unwrap();
124 let result = ts
125 .with_timezone(&sao_paulo)
126 .format("%Y-%m-%dT%H:%M:%S%:z")
127 .to_string();
128 assert!(
129 result.contains("-03:00"),
130 "expected offset -03:00, got: {result}"
131 );
132 }
133
134 #[test]
135 fn invalid_iana_parse_is_validation() {
136 let bad = "Invalid/Nonexistent";
137 let parsed = bad.parse::<Tz>();
138 assert!(parsed.is_err());
139 }
140}