use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
use chrono_tz::Tz;
pub fn active_tz() -> Tz {
let Some(settings) = crate::settings::get_opt() else {
return Tz::UTC;
};
let Some(name) = settings.time_zone.as_deref() else {
return Tz::UTC;
};
tz_or_utc(name)
}
pub fn tz_or_utc(name: &str) -> Tz {
match name.parse::<Tz>() {
Ok(tz) => tz,
Err(_) => {
tracing::warn!(
tz = name,
"umbral::timezone: unknown IANA tz `{name}` — falling back to UTC"
);
Tz::UTC
}
}
}
pub fn naive_local_to_utc(naive: NaiveDateTime) -> Option<DateTime<Utc>> {
let tz = active_tz();
if tz == Tz::UTC {
return Some(naive.and_utc());
}
match tz.from_local_datetime(&naive) {
chrono::LocalResult::Single(dt) => Some(dt.with_timezone(&Utc)),
chrono::LocalResult::Ambiguous(_, _) => None,
chrono::LocalResult::None => None,
}
}
pub fn utc_to_naive_local(utc: DateTime<Utc>) -> NaiveDateTime {
let tz = active_tz();
if tz == Tz::UTC {
return utc.naive_utc();
}
utc.with_timezone(&tz).naive_local()
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::NaiveDate;
#[test]
fn unknown_tz_falls_back_to_utc() {
let tz = tz_or_utc("Not/A/Real/Zone");
assert_eq!(tz, Tz::UTC);
}
#[test]
fn naive_round_trip_through_utc_is_identity() {
let naive = NaiveDate::from_ymd_opt(2026, 6, 7)
.unwrap()
.and_hms_opt(13, 30, 0)
.unwrap();
let utc = naive_local_to_utc(naive).expect("Tz::UTC is unambiguous");
let back = utc_to_naive_local(utc);
assert_eq!(naive, back);
}
}