1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
//! This library's functions are used to retrieve time changes and date/time characteristics for a given TZ.
//! Based on data provided by system timezone files and [low-level parsing library](https://crates.io/crates/libtzfile).
//! System TZfiles default location can be overriden with the TZFILES_DIR environment variable.
//!
//! There are two functions, one using the other's result:
//! 
//! `get_timechanges` obtains time changes for specified year.
//!
//! `get_zoneinfo` further parses the data to provide useful and human-readable output.
//! 
//! Example with get_zoneinfo:
//! ```
//! extern crate tzparse;
//! 
//! fn main() {
//!     match tzparse::get_timechanges("Europe/Paris", Some(2019)) {
//!         Some(tz) => println!("{:?}", tzparse::get_zoneinfo(&tz).unwrap()),
//!         None => println!("Timezone not found")
//!     };
//! }
//! ```
//!
//! Outputs:
//! ```text
//! { utc_datetime: 2019-09-27T07:04:09.366157Z, datetime: 2019-09-27T09:04:09.366157+02:00,
//! dst_from: Some(2019-03-31T01:00:00Z), dst_until: Some(2019-10-27T01:00:00Z),
//! raw_offset: 3600, dst_offset: 7200, utc_offset: +02:00, abbreviation: "CEST" }
//! ```
//! The get_timechanges used alone ouputs:
//! ```text
//! [Timechange { time: 2019-03-31T01:00:00Z, gmtoff: 7200, isdst: true, abbreviation: "CEST" },
//! Timechange { time: 2019-10-27T01:00:00Z, gmtoff: 3600, isdst: false, abbreviation: "CET" }]
//! ```

extern crate libtzfile;
use chrono::prelude::*;
use std::convert::TryInto;

/// Convenient and human-readable informations about a timezone.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Tzinfo {
    /// UTC time
    pub utc_datetime: DateTime<Utc>,
    /// Local time
    pub datetime: DateTime<FixedOffset>,
    /// Start of DST period
    pub dst_from: Option<DateTime<Utc>>,
    /// End of DST period
    pub dst_until: Option<DateTime<Utc>>,
    /// Are we in DST period ?
    pub dst_period: bool,
    /// Normal offset to GMT, in seconds
    pub raw_offset: isize,
    /// DST offset to GMT, in seconds
    pub dst_offset: isize,
    /// current offset to GMT, in +/-HH:MM
    pub utc_offset: FixedOffset,
    /// Timezone abbreviation
    pub abbreviation: String,
}

/// The Timechange struct contains one timechange from the parsed TZfile.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Timechange {
    /// The UTC time and date of the timechange, BEFORE new parameters apply
    pub time: DateTime<Utc>,
    /// The UPCOMING offset to GMT
    pub gmtoff: isize,
    /// Is upcoming change dst ?
    pub isdst: bool,
    /// TZ abbreviation of upcoming change
    pub abbreviation: String
}

/// Returns year's (current year is default) timechanges for a timezone.
/// If there's no timechange for selected year, returns the last occured timechange.
pub fn get_timechanges(requested_timezone: &str, y: Option<i32>) -> Option<Vec<Timechange>> {
    // low-level parse of tzfile
    let timezone = match libtzfile::parse(requested_timezone) {
        Ok(tz) => tz,
        Err(_) => return None
    };

    // used to store timechange indices
    let mut timechanges = Vec::new();
    let mut nearest_timechange: usize = 0;

    // Used to store parsed timechanges
    let mut parsedtimechanges = Vec::new();

    // Provided year (or current year by default)
    let year: i32 = match y {
        Some(y) => y,
        None => {
            let d = Utc::now();
            d.format("%Y").to_string().parse().unwrap()
                }
    };
    
    // for year comparison
    let currentyearbeg = Utc.ymd(year, 1, 1).and_hms(0, 0, 0);
    let currentyearend = Utc.ymd(year, 12, 31).and_hms(0, 0, 0);

    // Get and store the timechange indices
    for t in 0..timezone.tzh_timecnt_data.len() {
        if timezone.tzh_timecnt_data[t] > currentyearbeg
            && timezone.tzh_timecnt_data[t] < currentyearend
        {
            timechanges.push(t);
        }
        if timezone.tzh_timecnt_data[t] < currentyearbeg {
            nearest_timechange = t.try_into().unwrap();
        };
    }

    if timechanges.len() != 0 {
        //println!("Time changes for specified year at index : {:?}", timechanges);
        for t in 0..timechanges.len() {
            let tc = Timechange {
                time: timezone.tzh_timecnt_data[timechanges[t]],
                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: timezone.tzh_timecnt_data[nearest_timechange],
                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);
    }
    Some(parsedtimechanges)
}

/// Returns convenient data about a timezone. Used for example in my [world time API](https://github.com/nicolasbauw/world-time-api).
pub fn get_zoneinfo(parsedtimechanges: &Vec<Timechange>) -> Option<Tzinfo> {
    let d = Utc::now();
    if parsedtimechanges.len() == 2 {
        // 2 times changes the same year ? DST observed
        // Are we in a dst period ? true / false
        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) };
        //println!("{}", dst);
        Some(Tzinfo{
            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);
        Some(Tzinfo {
            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 { None }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn zoneinfo() {
        let tz = vec!(Timechange{time: Utc.ymd(2019, 3, 31).and_hms(1, 0, 0), gmtoff: 7200, isdst: true, abbreviation: "CEST".to_string()},
            Timechange{time: Utc.ymd(2019, 10, 27).and_hms(1, 0, 0), gmtoff: 3600, isdst: false, abbreviation: "CET".to_string()});
        assert_eq!(get_timechanges("Europe/Paris", Some(2019)).unwrap(), tz);
    }
}