use chrono::prelude::*;
pub use libtzfile::TzError;
#[cfg(feature = "json")]
use serde::Serialize;
#[cfg(feature = "json")]
mod offset_serializer {
use serde::Serialize;
fn offset_to_json(t: chrono::FixedOffset) -> String {
format!("{:?}", t)
}
pub fn serialize<S: serde::Serializer>(
time: &chrono::FixedOffset,
serializer: S,
) -> Result<S::Ok, S::Error> {
offset_to_json(time.clone()).serialize(serializer)
}
}
#[cfg(feature = "json")]
#[derive(Debug, Serialize)]
pub struct Tzinfo {
pub timezone: String,
pub utc_datetime: DateTime<Utc>,
pub datetime: DateTime<FixedOffset>,
pub dst_from: Option<DateTime<Utc>>,
pub dst_until: Option<DateTime<Utc>>,
pub dst_period: bool,
pub raw_offset: isize,
pub dst_offset: isize,
#[serde(with = "offset_serializer")]
pub utc_offset: FixedOffset,
pub abbreviation: String,
pub week_number: i32,
}
#[cfg(not(feature = "json"))]
#[derive(Debug)]
pub struct Tzinfo {
pub timezone: String,
pub utc_datetime: DateTime<Utc>,
pub datetime: DateTime<FixedOffset>,
pub dst_from: Option<DateTime<Utc>>,
pub dst_until: Option<DateTime<Utc>>,
pub dst_period: bool,
pub raw_offset: isize,
pub dst_offset: isize,
pub utc_offset: FixedOffset,
pub abbreviation: String,
pub week_number: i32,
}
#[derive(Debug, PartialEq)]
pub struct Timechange {
pub time: DateTime<Utc>,
pub gmtoff: isize,
pub isdst: bool,
pub abbreviation: String,
}
#[cfg(feature = "json")]
impl Tzinfo {
pub fn to_json(&self) -> Result<String, serde_json::error::Error> {
serde_json::to_string(self)
}
}
pub fn get_timechanges(
requested_timezone: &str,
y: Option<i32>,
) -> Result<Vec<Timechange>, TzError> {
let timezone = libtzfile::parse(requested_timezone)?;
let mut timechanges = Vec::new();
let mut nearest_timechange: usize = 0;
let mut parsedtimechanges = Vec::new();
if y.is_some() {
let d = Utc::now();
let y = y.unwrap();
let y = if y == 0 {
d.format("%Y").to_string().parse()?
} else {
y
};
let yearbeg = Utc.ymd(y, 1, 1).and_hms(0, 0, 0).timestamp();
let yearend = Utc.ymd(y, 12, 31).and_hms(0, 0, 0).timestamp();
for t in 0..timezone.tzh_timecnt_data.len() {
if timezone.tzh_timecnt_data[t] > yearbeg && timezone.tzh_timecnt_data[t] < yearend {
timechanges.push(t);
}
if timezone.tzh_timecnt_data[t] < yearbeg {
nearest_timechange = t;
};
}
} else {
for t in 0..timezone.tzh_timecnt_data.len() {
if timezone.tzh_timecnt_data[t] != -576460752303423488 { timechanges.push(t) };
}
}
if timechanges.len() != 0 {
for t in 0..timechanges.len() {
let tc = Timechange {
time: Utc.timestamp(timezone.tzh_timecnt_data[timechanges[t]], 0),
gmtoff: timezone.tzh_typecnt[timezone.tzh_timecnt_indices[timechanges[t]] as usize]
.tt_gmtoff,
isdst: timezone.tzh_typecnt[timezone.tzh_timecnt_indices[timechanges[t]] as usize]
.tt_isdst
== 1,
abbreviation: timezone.tz_abbr[timezone.tzh_typecnt
[timezone.tzh_timecnt_indices[timechanges[t]] as usize]
.tt_abbrind as usize]
.to_string(),
};
parsedtimechanges.push(tc);
}
} else {
let tc = Timechange {
time: Utc.timestamp(timezone.tzh_timecnt_data[nearest_timechange], 0),
gmtoff: timezone.tzh_typecnt[timezone.tzh_timecnt_indices[nearest_timechange] as usize]
.tt_gmtoff,
isdst: timezone.tzh_typecnt[timezone.tzh_timecnt_indices[nearest_timechange] as usize]
.tt_isdst
== 1,
abbreviation: timezone.tz_abbr[timezone.tzh_typecnt
[timezone.tzh_timecnt_indices[nearest_timechange] as usize]
.tt_abbrind as usize]
.to_string(),
};
parsedtimechanges.push(tc);
}
Ok(parsedtimechanges)
}
pub fn get_zoneinfo(requested_timezone: &str) -> Result<Tzinfo, TzError> {
let mut timezone = String::new();
#[cfg(not(windows))]
let mut tz: Vec<&str> = requested_timezone.split("/").collect();
#[cfg(windows)]
let mut tz: Vec<&str> = requested_timezone.split("\\").collect();
if tz.len() < 3 { return Err(TzError::InvalidTimezone)}
for _ in 0..(tz.len()) - 2 {
tz.remove(0);
}
if tz[0] != "zoneinfo" {
timezone.push_str(tz[0]);
timezone.push_str("/");
}
timezone.push_str(tz[1]);
let parsedtimechanges = get_timechanges(requested_timezone, Some(0))?;
let d = Utc::now();
if parsedtimechanges.len() == 2 {
let dst = d > parsedtimechanges[0].time && d < parsedtimechanges[1].time;
let utc_offset = if dst == true {
FixedOffset::east(parsedtimechanges[0].gmtoff as i32)
} else {
FixedOffset::east(parsedtimechanges[1].gmtoff as i32)
};
Ok(Tzinfo {
timezone: timezone,
week_number: d
.with_timezone(&utc_offset)
.format("%V")
.to_string()
.parse()?,
utc_datetime: d,
datetime: d.with_timezone(&utc_offset),
dst_from: Some(parsedtimechanges[0].time),
dst_until: Some(parsedtimechanges[1].time),
dst_period: dst,
raw_offset: parsedtimechanges[1].gmtoff,
dst_offset: parsedtimechanges[0].gmtoff,
utc_offset: utc_offset,
abbreviation: if dst == true {
parsedtimechanges[0].abbreviation.clone()
} else {
parsedtimechanges[1].abbreviation.clone()
},
})
} else if parsedtimechanges.len() == 1 {
let utc_offset = FixedOffset::east(parsedtimechanges[0].gmtoff as i32);
Ok(Tzinfo {
timezone: timezone,
week_number: d
.with_timezone(&utc_offset)
.format("%V")
.to_string()
.parse()?,
utc_datetime: d,
datetime: d.with_timezone(&utc_offset),
dst_from: None,
dst_until: None,
dst_period: false,
raw_offset: parsedtimechanges[0].gmtoff,
dst_offset: 0,
utc_offset: utc_offset,
abbreviation: parsedtimechanges[0].abbreviation.clone(),
})
} else {
Err(TzError::NoData)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn partial_timechanges() {
let tz = vec![
Timechange {
time: Utc.ymd(2019, 3, 31).and_hms(1, 0, 0),
gmtoff: 7200,
isdst: true,
abbreviation: String::from("CEST"),
},
Timechange {
time: Utc.ymd(2019, 10, 27).and_hms(1, 0, 0),
gmtoff: 3600,
isdst: false,
abbreviation: String::from("CET"),
},
];
#[cfg(not(windows))]
assert_eq!(
get_timechanges("/usr/share/zoneinfo/Europe/Paris", Some(2019)).unwrap(),
tz
);
#[cfg(windows)]
assert_eq!(
get_timechanges("c:\\Users\\nbauw\\Dev\\zoneinfo\\Europe\\Paris", Some(2019)).unwrap(),
tz
);
}
#[test]
fn total_timechanges() {
let tz = vec![
Timechange { time: Utc.ymd(1883, 11, 18).and_hms(19, 0, 0), gmtoff: -25200, isdst: false, abbreviation: String::from("MST") },
Timechange { time: Utc.ymd(1918, 03, 31).and_hms(9, 0, 0), gmtoff: -21600, isdst: true, abbreviation: String::from("MDT") },
Timechange { time: Utc.ymd(1918, 10, 27).and_hms(8, 0, 0), gmtoff: -25200, isdst: false, abbreviation: String::from("MST") },
Timechange { time: Utc.ymd(1919, 03, 30).and_hms(9, 0, 0), gmtoff: -21600, isdst: true, abbreviation: String::from("MDT") },
Timechange { time: Utc.ymd(1919, 10, 26).and_hms(8, 0, 0), gmtoff: -25200, isdst: false, abbreviation: String::from("MST") },
Timechange { time: Utc.ymd(1942, 02, 09).and_hms(9, 0, 0), gmtoff: -21600, isdst: true, abbreviation: String::from("MWT") },
Timechange { time: Utc.ymd(1944, 01, 01).and_hms(6, 1, 0), gmtoff: -25200, isdst: false, abbreviation: String::from("MST") },
Timechange { time: Utc.ymd(1944, 04, 01).and_hms(7, 1, 0), gmtoff: -21600, isdst: true, abbreviation: String::from("MWT") },
Timechange { time: Utc.ymd(1944, 10, 01).and_hms(6, 1, 0), gmtoff: -25200, isdst: false, abbreviation: String::from("MST") },
Timechange { time: Utc.ymd(1967, 04, 30).and_hms(9, 0, 0), gmtoff: -21600, isdst: true, abbreviation: String::from("MDT") },
Timechange { time: Utc.ymd(1967, 10, 29).and_hms(8, 0, 0), gmtoff: -25200, isdst: false, abbreviation: String::from("MST") }
];
#[cfg(not(windows))]
assert_eq!(
get_timechanges("/usr/share/zoneinfo/America/Phoenix", None).unwrap(),
tz
);
#[cfg(windows)]
assert_eq!(
get_timechanges("c:\\Users\\nbauw\\Dev\\zoneinfo\\America\\Phoenix", None).unwrap(),
tz
);
}
#[test]
fn zoneinfo() {
#[cfg(not(windows))]
let tztest = get_zoneinfo("/usr/share/zoneinfo/Europe/Paris").unwrap();
#[cfg(windows)]
let tztest = get_zoneinfo("c:\\Users\\nbauw\\Dev\\zoneinfo\\Europe\\Paris").unwrap();
assert_eq!(tztest.timezone, String::from("Europe/Paris"));
assert_eq!(tztest.raw_offset, 3600);
assert_eq!(tztest.dst_offset, 7200);
}
}