use std::sync::RwLock;
use chrono::{Local, NaiveDateTime, TimeZone as ChronoTz};
use super::file::TzFile;
static CACHED_LOCAL: RwLock<Option<TzLocal>> = RwLock::new(None);
#[derive(Debug, Clone)]
pub struct TzLocal {
inner: Option<TzFile>,
name: Box<str>,
}
impl Default for TzLocal {
fn default() -> Self {
Self::new()
}
}
impl TzLocal {
pub fn new() -> Self {
{
let guard = CACHED_LOCAL.read().unwrap();
if let Some(cached) = guard.as_ref() {
return cached.clone();
}
}
let mut guard = CACHED_LOCAL.write().unwrap();
if let Some(cached) = guard.as_ref() {
return cached.clone();
}
let name = iana_time_zone::get_timezone().unwrap_or_else(|_| "UTC".to_string());
let inner = Self::resolve_tzfile(&name);
let tz = Self {
inner,
name: name.into(),
};
*guard = Some(tz.clone());
tz
}
pub fn invalidate_cache() {
let mut guard = CACHED_LOCAL.write().unwrap();
*guard = None;
}
fn resolve_tzfile(name: &str) -> Option<TzFile> {
for base in super::TZPATHS {
let path = format!("{}/{}", base, name);
if let Ok(tz) = TzFile::from_path(&path) {
return Some(tz);
}
}
None
}
pub fn utcoffset(&self, dt: NaiveDateTime, fold: bool) -> i32 {
if let Some(ref tz) = self.inner {
return tz.utcoffset(dt, fold);
}
chrono_local_offset(dt)
}
pub fn dst(&self, dt: NaiveDateTime, fold: bool) -> i32 {
if let Some(ref tz) = self.inner {
return tz.dst(dt, fold);
}
0 }
pub fn tzname(&self, dt: NaiveDateTime, fold: bool) -> &str {
if let Some(ref tz) = self.inner {
return tz.tzname(dt, fold);
}
&self.name
}
pub fn is_ambiguous(&self, dt: NaiveDateTime) -> bool {
if let Some(ref tz) = self.inner {
return tz.is_ambiguous(dt);
}
false
}
pub fn fromutc(&self, dt: NaiveDateTime) -> NaiveDateTime {
if let Some(ref tz) = self.inner {
return tz.fromutc(dt);
}
dt + chrono::TimeDelta::seconds(chrono_local_offset(dt) as i64)
}
pub fn iana_name(&self) -> &str {
&self.name
}
}
fn chrono_local_offset(dt: NaiveDateTime) -> i32 {
match Local.from_local_datetime(&dt) {
chrono::LocalResult::Single(aware) => aware.offset().local_minus_utc(),
chrono::LocalResult::Ambiguous(a, _) => a.offset().local_minus_utc(),
chrono::LocalResult::None => 0,
}
}
#[cfg(test)]
mod tests {
use chrono::NaiveDate;
use super::*;
fn dt(y: i32, m: u32, d: u32, h: u32, mi: u32, s: u32) -> NaiveDateTime {
NaiveDate::from_ymd_opt(y, m, d).unwrap().and_hms_opt(h, mi, s).unwrap()
}
#[test]
fn test_tzlocal_creates() {
let tz = TzLocal::new();
assert!(!tz.iana_name().is_empty());
}
#[test]
fn test_tzlocal_offset_nonzero_or_utc() {
let tz = TzLocal::new();
let d = dt(2024, 6, 15, 12, 0, 0);
let off = tz.utcoffset(d, false);
assert!(off.abs() <= 14 * 3600);
}
#[test]
fn test_tzlocal_has_tzfile() {
let tz = TzLocal::new();
assert!(tz.inner.is_some(), "TzLocal should resolve to a TzFile on this system");
}
#[test]
fn test_tzlocal_fromutc_roundtrip() {
let tz = TzLocal::new();
let utc = dt(2024, 6, 15, 0, 0, 0);
let wall = tz.fromutc(utc);
let off = tz.utcoffset(wall, false);
let back = wall - chrono::TimeDelta::seconds(off as i64);
assert_eq!(back, utc);
}
#[test]
fn test_tzlocal_dst() {
let tz = TzLocal::new();
let d = dt(2024, 6, 15, 12, 0, 0);
let dst = tz.dst(d, false);
assert!(dst >= 0 && dst <= 7200);
}
#[test]
fn test_tzlocal_tzname() {
let tz = TzLocal::new();
let d = dt(2024, 6, 15, 12, 0, 0);
let name = tz.tzname(d, false);
assert!(!name.is_empty());
}
#[test]
fn test_tzlocal_is_ambiguous() {
let tz = TzLocal::new();
let d = dt(2024, 6, 15, 12, 0, 0);
assert!(!tz.is_ambiguous(d));
}
#[test]
fn test_tzlocal_fold_parameter() {
let tz = TzLocal::new();
let d = dt(2024, 6, 15, 12, 0, 0);
assert_eq!(tz.utcoffset(d, false), tz.utcoffset(d, true));
}
#[test]
fn test_tzlocal_winter_vs_summer() {
let tz = TzLocal::new();
let summer = dt(2024, 7, 15, 12, 0, 0);
let winter = dt(2024, 1, 15, 12, 0, 0);
let off_summer = tz.utcoffset(summer, false);
let off_winter = tz.utcoffset(winter, false);
assert!(off_summer.abs() <= 14 * 3600);
assert!(off_winter.abs() <= 14 * 3600);
}
#[test]
fn test_tzlocal_default() {
let tz = TzLocal::default();
assert!(!tz.iana_name().is_empty());
}
}