libtzfile/
lib.rs

1//! This library reads and parses the system timezone information files (TZ Files) provided by IANA.
2//!
3//! The default feature is ```std```. With ```default-features = false```, the crate is ```no_std``` and uses ```alloc::vec```. In both cases the ```new()``` method returns a Tz struct containing the TZfile
4//! fields as described in the man page (<http://man7.org/linux/man-pages/man5/tzfile.5.html>).
5//!
6//! - with ```no_std``` the function signature is ```new(buf: Vec<u8>)``` where ```buf``` is the TZ File data
7//!
8//!```text
9//! // no_std
10//! [dependencies]
11//! libtzfile = { version = "3.1.0", default-features = false }
12//! ```
13//! ```text
14//! let tzfile = include_bytes!("/usr/share/zoneinfo/America/Phoenix").to_vec();
15//! let tz = Tz::new(tzfile).unwrap();
16//! ```
17//!
18//! - with ```std``` which is the default feature the function signature is ```new(tz: &str)``` where ```tz``` is the TZ File name
19//!
20//!```text
21//! // std is the default
22//! [dependencies]
23//! libtzfile = "3.1.0"
24//! ```
25//!
26//!```text
27//! use libtzfile::Tz;
28//! let tzfile: &str = "/usr/share/zoneinfo/America/Phoenix";
29//! println!("{:?}", Tz::new(tzfile).unwrap());
30//!```
31//!
32//!```text
33//! Tz { tzh_timecnt_data: [-2717643600, -1633273200, -1615132800, -1601823600, -1583683200, -880210800, -820519140, -812653140, -796845540, -84380400, -68659200], tzh_timecnt_indices: [2, 1, 2, 1, 2, 3, 2, 3, 2, 1, 2], tzh_typecnt: [Ttinfo { tt_utoff: -26898, tt_isdst: 0, tt_abbrind: 0 }, Ttinfo { tt_utoff: -21600, tt_isdst: 1, tt_abbrind: 1 }, Ttinfo { tt_utoff: -25200, tt_isdst: 0, tt_abbrind: 2 }, Ttinfo { tt_utoff: -21600, tt_isdst: 1, tt_abbrind: 3 }], tz_abbr: ["LMT", "MDT", "MST", "MWT"] }
34//! ```
35//!
36//! For higher level parsing, you can enable the **parse** or **json** features.
37//! For instance, to display 2020 DST transitions in France, you can use the transition_times method:
38//!
39//! ```text
40//! use libtzfile::Tz;
41//! let tzfile: &str = "/usr/share/zoneinfo/Europe/Paris";
42//! println!("{:?}", Tz::new(tzfile).unwrap().transition_times(Some(2020)).unwrap());
43//! ```
44//!
45//! ```text
46//! [TransitionTime { time: 2020-03-29T01:00:00Z, utc_offset: 7200, isdst: true, abbreviation: "CEST" }, TransitionTime { time: 2020-10-25T01:00:00Z, utc_offset: 3600, isdst: false, abbreviation: "CET" }]
47//! ```
48//!
49//! If you want more complete information about the timezone, you can use the zoneinfo method, which returns a more complete structure:
50//!
51//! ```text
52//! use libtzfile::Tz;
53//! let tzfile: &str = "/usr/share/zoneinfo/Europe/Paris";
54//! println!("{:?}", Tz::new(tzfile).unwrap().zoneinfo().unwrap());
55//!```
56//!
57//! ```text
58//! Tzinfo { timezone: "Europe/Paris", utc_datetime: 2020-09-05T16:41:44.279502100Z, datetime: 2020-09-05T18:41:44.279502100+02:00, dst_from: Some(2020-03-29T01:00:00Z), dst_until: Some(2020-10-25T01:00:00Z), dst_period: true, raw_offset: 3600, dst_offset: 7200, utc_offset: +02:00, abbreviation: "CEST", week_number: 36 }
59//! ```
60//!
61//! This more complete structure implements the Serialize trait and can be transformed to a json string via a method of the json feature (which includes methods from the parse feature):
62//!```text
63//! use libtzfile::{Tz, TzError};
64//! let tzfile: &str = "/usr/share/zoneinfo/Europe/Paris";
65//! let tz = Tz::new(tzfile)?
66//!     .zoneinfo()?
67//!     .to_json()?;
68//! println!("{}", tz);
69//!```
70//!
71//!```text
72//! {"timezone":"Europe/Paris","utc_datetime":"2020-09-05T18:04:50.546668500Z","datetime":"2020-09-05T20:04:50.546668500+02:00","dst_from":"2020-03-29T01:00:00Z","dst_until":"2020-10-25T01:00:00Z","dst_period":true,"raw_offset":3600,"dst_offset":7200,"utc_offset":"+02:00","abbreviation":"CEST","week_number":36}
73//!```
74//!
75//! This feature is used in my [world time API](https://crates.io/crates/world-time-api).
76//!
77//! The tests (`cargo test`, ```cargo test --no-default-features``` or ```cargo test --features parse|json```) are working with the [2024b timezone database](https://data.iana.org/time-zones/tz-link.html).
78
79// Support using libtzfile without the standard library
80#![cfg_attr(not(any(feature = "std", feature = "parse", feature = "json")), no_std)]
81
82#[cfg(any(feature = "std", feature = "parse", feature = "json"))]
83#[cfg(test)]
84mod tests;
85#[cfg(any(feature = "std", feature = "parse", feature = "json"))]
86extern crate std;
87#[cfg(any(feature = "std", feature = "parse", feature = "json"))]
88use std::{
89    error, fmt, fs::File, io::Read, str::from_utf8, string::String, string::ToString, vec::Vec,
90};
91
92#[cfg(not(any(feature = "std", feature = "parse", feature = "json")))]
93#[cfg(test)]
94mod tests_nostd;
95#[cfg(not(any(feature = "std", feature = "parse", feature = "json")))]
96extern crate alloc;
97#[cfg(not(any(feature = "std", feature = "parse", feature = "json")))]
98use alloc::{str::from_utf8, string::String, string::ToString, vec::Vec};
99
100#[cfg(any(feature = "parse", feature = "json"))]
101use chrono::{DateTime, FixedOffset, TimeZone, Utc};
102#[cfg(feature = "json")]
103use serde::Serialize;
104
105#[cfg(feature = "json")]
106mod offset_serializer {
107    use serde::Serialize;
108    use std::{format, string::String};
109    fn offset_to_json(t: chrono::FixedOffset) -> String {
110        format!("{:?}", t)
111    }
112
113    pub fn serialize<S: serde::Serializer>(
114        time: &chrono::FixedOffset,
115        serializer: S,
116    ) -> Result<S::Ok, S::Error> {
117        offset_to_json(time.clone()).serialize(serializer)
118    }
119}
120
121use byteorder::{ByteOrder, BE};
122
123// TZif magic four bytes
124const MAGIC: u32 = 0x545A6966;
125// Header length
126const HEADER_LEN: usize = 0x2C;
127
128#[derive(Debug, PartialEq, Eq, Clone)]
129pub enum TzError {
130    // Invalid timezone
131    InvalidTimezone,
132    // Invalid file format.
133    InvalidMagic,
134    // Bad utf8 string
135    BadUtf8String,
136    // Only V2 format is supported
137    UnsupportedFormat,
138    // No data matched the request
139    NoData,
140    // Parsing Error
141    ParseError,
142    // Empty String
143    EmptyString,
144    // Json conversion error
145    JsonError,
146}
147
148#[cfg(any(feature = "std", feature = "parse", feature = "json"))]
149impl fmt::Display for TzError {
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        f.write_str("TZfile error : ")?;
152        f.write_str(match self {
153            TzError::InvalidTimezone => "Invalid timezone",
154            TzError::InvalidMagic => "Invalid TZfile",
155            TzError::BadUtf8String => "Bad utf8 string",
156            TzError::UnsupportedFormat => "Only V2 format is supported",
157            TzError::NoData => "No data matched the request",
158            TzError::ParseError => "Parsing error",
159            TzError::EmptyString => "Empty string",
160            TzError::JsonError => "Could not convert to json",
161        })
162    }
163}
164
165#[cfg(any(feature = "std", feature = "parse", feature = "json"))]
166impl From<std::io::Error> for TzError {
167    fn from(_e: std::io::Error) -> TzError {
168        TzError::InvalidTimezone
169    }
170}
171
172#[cfg(any(feature = "std", feature = "parse", feature = "json"))]
173impl From<std::num::ParseIntError> for TzError {
174    fn from(_e: std::num::ParseIntError) -> TzError {
175        TzError::ParseError
176    }
177}
178
179#[cfg(any(feature = "std", feature = "parse", feature = "json"))]
180impl From<std::str::Utf8Error> for TzError {
181    fn from(_e: std::str::Utf8Error) -> TzError {
182        TzError::BadUtf8String
183    }
184}
185
186#[cfg(any(feature = "std", feature = "parse", feature = "json"))]
187impl From<TzError> for std::io::Error {
188    fn from(e: TzError) -> std::io::Error {
189        std::io::Error::new(std::io::ErrorKind::Other, e)
190    }
191}
192
193#[cfg(feature = "json")]
194impl From<serde_json::error::Error> for TzError {
195    fn from(_e: serde_json::error::Error) -> TzError {
196        TzError::JsonError
197    }
198}
199
200#[cfg(any(feature = "std", feature = "parse", feature = "json"))]
201impl error::Error for TzError {}
202
203/// This is the crate's primary structure, which contains the TZfile fields.
204#[derive(Debug)]
205pub struct Tz {
206    /// transition times timestamps table
207    pub tzh_timecnt_data: Vec<i64>,
208    /// indices for the next field
209    pub tzh_timecnt_indices: Vec<u8>,
210    /// a struct containing UTC offset, daylight saving time, abbreviation index
211    pub tzh_typecnt: Vec<Ttinfo>,
212    /// abbreviations table
213    pub tz_abbr: Vec<String>,
214    #[cfg(any(feature = "parse", feature = "json"))]
215    name: String,
216}
217
218/// This sub-structure of the Tz struct is part of the TZfile format specifications, and contains UTC offset, daylight saving time, abbreviation index.
219#[derive(Debug)]
220pub struct Ttinfo {
221    pub tt_utoff: isize,
222    pub tt_isdst: u8,
223    pub tt_abbrind: u8,
224}
225
226#[derive(Debug, PartialEq)]
227struct Header {
228    tzh_ttisutcnt: usize,
229    tzh_ttisstdcnt: usize,
230    tzh_leapcnt: usize,
231    tzh_timecnt: usize,
232    tzh_typecnt: usize,
233    tzh_charcnt: usize,
234    v2_header_start: usize,
235}
236
237#[cfg(any(feature = "parse", feature = "json"))]
238/// The TransitionTime struct (available with the parse or json features) contains one transition time.
239#[derive(Debug, PartialEq)]
240pub struct TransitionTime {
241    /// The UTC time and date of the transition time, BEFORE new parameters apply
242    pub time: DateTime<Utc>,
243    /// The UPCOMING offset to UTC
244    pub utc_offset: isize,
245    /// Is upcoming change dst ?
246    pub isdst: bool,
247    /// TZ abbreviation of upcoming change
248    pub abbreviation: String,
249}
250
251/// Convenient and human-readable informations about a timezone (available with the parse or json features).
252/// With the json feature enabled, the Tzinfo struct implements the Serialize trait.
253///
254/// Some explanations about the offset fields:
255/// - raw_offset : the "normal" offset to utc, in seconds
256/// - dst_offset : the offset to utc during daylight saving time, in seconds
257/// - utc_offset : the current offset to utc, taking into account daylight saving time or not (according to dst_from and dst_until), in +/- HH:MM
258#[cfg(feature = "json")]
259#[derive(Debug, Serialize)]
260pub struct Tzinfo {
261    /// Timezone name
262    pub timezone: String,
263    /// UTC time
264    pub utc_datetime: DateTime<Utc>,
265    /// Local time
266    pub datetime: DateTime<FixedOffset>,
267    /// Start of DST period
268    pub dst_from: Option<DateTime<Utc>>,
269    /// End of DST period
270    pub dst_until: Option<DateTime<Utc>>,
271    /// Are we in DST period ?
272    pub dst_period: bool,
273    /// Normal offset to UTC, in seconds
274    pub raw_offset: isize,
275    /// DST offset to UTC, in seconds
276    pub dst_offset: isize,
277    /// current offset to UTC, in +/-HH:MM
278    #[serde(with = "offset_serializer")]
279    pub utc_offset: FixedOffset,
280    /// Timezone abbreviation
281    pub abbreviation: String,
282    /// Week number
283    pub week_number: i32,
284}
285
286#[cfg(feature = "parse")]
287#[derive(Debug)]
288pub struct Tzinfo {
289    /// Timezone name
290    pub timezone: String,
291    /// UTC time
292    pub utc_datetime: DateTime<Utc>,
293    /// Local time
294    pub datetime: DateTime<FixedOffset>,
295    /// Start of DST period
296    pub dst_from: Option<DateTime<Utc>>,
297    /// End of DST period
298    pub dst_until: Option<DateTime<Utc>>,
299    /// Are we in DST period ?
300    pub dst_period: bool,
301    /// Normal offset to UTC, in seconds
302    pub raw_offset: isize,
303    /// DST offset to UTC, in seconds
304    pub dst_offset: isize,
305    /// current offset to UTC, in +/-HH:MM
306    pub utc_offset: FixedOffset,
307    /// Timezone abbreviation
308    pub abbreviation: String,
309    /// Week number
310    pub week_number: i32,
311}
312
313#[cfg(feature = "json")]
314impl Tzinfo {
315    /// Transforms the Tzinfo struct to a JSON string
316    ///
317    ///```rust
318    /// # let tzfile = if cfg!(windows) { "c:\\Users\\nbauw\\Dev\\zoneinfo\\Europe\\Paris" } else { "/usr/share/zoneinfo/Europe/Paris" };
319    /// use libtzfile::{Tz, TzError};
320    /// let tz = Tz::new(tzfile)?
321    ///     .zoneinfo()?
322    ///     .to_json()?;
323    /// println!("{}", tz);
324    /// # Ok::<(), TzError>(())
325    ///```
326    ///
327    ///```text
328    /// {"timezone":"Europe/Paris","utc_datetime":"2020-09-05T18:04:50.546668500Z","datetime":"2020-09-05T20:04:50.546668500+02:00","dst_from":"2020-03-29T01:00:00Z","dst_until":"2020-10-25T01:00:00Z","dst_period":true,"raw_offset":3600,"dst_offset":7200,"utc_offset":"+02:00","abbreviation":"CEST","week_number":36}
329    ///```
330    pub fn to_json(&self) -> Result<String, serde_json::error::Error> {
331        serde_json::to_string(self)
332    }
333}
334
335impl Tz {
336    #[cfg(not(any(feature = "std", feature = "parse", feature = "json")))]
337    pub fn new(buf: Vec<u8>) -> Result<Tz, TzError> {
338        // Parses TZfile header
339        let header = Tz::parse_header(&buf)?;
340        // Parses data
341        Tz::parse_data(&buf, header)
342    }
343
344    #[cfg(any(feature = "std", feature = "parse", feature = "json"))]
345    /// Creates a Tz struct from a TZ system file
346    ///
347    ///```rust
348    /// use libtzfile::Tz;
349    /// let tzfile: &str = "/usr/share/zoneinfo/America/Phoenix";
350    /// println!("{:?}", Tz::new(tzfile).unwrap());
351    ///```
352    ///```text
353    /// Tz { tzh_timecnt_data: [-2717643600, -1633273200, -1615132800, -1601823600, -1583683200, -880210800, -820519140, -812653140, -796845540, -84380400, -68659200], tzh_timecnt_indices: [2, 1, 2, 1, 2, 3, 2, 3, 2, 1, 2], tzh_typecnt: [Ttinfo { tt_utoff: -26898, tt_isdst: 0, tt_abbrind: 0 }, Ttinfo { tt_utoff: -21600, tt_isdst: 1, tt_abbrind: 1 }, Ttinfo { tt_utoff: -25200, tt_isdst: 0, tt_abbrind: 2 }, Ttinfo { tt_utoff: -21600, tt_isdst: 1, tt_abbrind: 3 }], tz_abbr: ["LMT", "MDT", "MST", "MWT"] }
354    ///```
355    pub fn new(tz: &str) -> Result<Tz, TzError> {
356        // Reads TZfile
357        let buf = Tz::read(tz)?;
358        // Parses TZfile header
359        let header = Tz::parse_header(&buf)?;
360        // Parses data
361        Tz::parse_data(&buf, header, tz)
362    }
363
364    fn parse_header(buffer: &[u8]) -> Result<Header, TzError> {
365        let magic = BE::read_u32(&buffer[0x00..=0x03]);
366        if magic != MAGIC {
367            return Err(TzError::InvalidMagic);
368        }
369        if buffer[4] != 50 {
370            return Err(TzError::UnsupportedFormat);
371        }
372        let tzh_ttisutcnt = BE::read_i32(&buffer[0x14..=0x17]) as usize;
373        let tzh_ttisstdcnt = BE::read_i32(&buffer[0x18..=0x1B]) as usize;
374        let tzh_leapcnt = BE::read_i32(&buffer[0x1C..=0x1F]) as usize;
375        let tzh_timecnt = BE::read_i32(&buffer[0x20..=0x23]) as usize;
376        let tzh_typecnt = BE::read_i32(&buffer[0x24..=0x27]) as usize;
377        let tzh_charcnt = BE::read_i32(&buffer[0x28..=0x2b]) as usize;
378        // V2 format data start
379        let s: usize = tzh_timecnt * 5
380            + tzh_typecnt * 6
381            + tzh_leapcnt * 8
382            + tzh_charcnt
383            + tzh_ttisstdcnt
384            + tzh_ttisutcnt
385            + 44;
386        Ok(Header {
387            tzh_ttisutcnt: BE::read_i32(&buffer[s + 0x14..=s + 0x17]) as usize,
388            tzh_ttisstdcnt: BE::read_i32(&buffer[s + 0x18..=s + 0x1B]) as usize,
389            tzh_leapcnt: BE::read_i32(&buffer[s + 0x1C..=s + 0x1F]) as usize,
390            tzh_timecnt: BE::read_i32(&buffer[s + 0x20..=s + 0x23]) as usize,
391            tzh_typecnt: BE::read_i32(&buffer[s + 0x24..=s + 0x27]) as usize,
392            tzh_charcnt: BE::read_i32(&buffer[s + 0x28..=s + 0x2b]) as usize,
393            v2_header_start: s,
394        })
395    }
396
397    #[cfg(not(any(feature = "std", feature = "parse", feature = "json")))]
398    fn parse_data(buffer: &Vec<u8>, header: Header) -> Result<Tz, TzError> {
399        // Calculates fields lengths and indexes (Version 2 format)
400        let tzh_timecnt_len: usize = header.tzh_timecnt * 9;
401        let tzh_typecnt_len: usize = header.tzh_typecnt * 6;
402        let tzh_leapcnt_len: usize = header.tzh_leapcnt * 12;
403        let tzh_charcnt_len: usize = header.tzh_charcnt;
404        let tzh_timecnt_end: usize = HEADER_LEN + header.v2_header_start + tzh_timecnt_len;
405        let tzh_typecnt_end: usize = tzh_timecnt_end + tzh_typecnt_len;
406        let tzh_leapcnt_end: usize = tzh_typecnt_end + tzh_leapcnt_len;
407        let tzh_charcnt_end: usize = tzh_leapcnt_end + tzh_charcnt_len;
408
409        // Extracting data fields
410        let tzh_timecnt_data: Vec<i64> = buffer[HEADER_LEN + header.v2_header_start
411            ..HEADER_LEN + header.v2_header_start + header.tzh_timecnt * 8]
412            .chunks_exact(8)
413            .map(|tt| BE::read_i64(tt))
414            .collect();
415
416        let tzh_timecnt_indices: &[u8] =
417            &buffer[HEADER_LEN + header.v2_header_start + header.tzh_timecnt * 8..tzh_timecnt_end];
418
419        let abbrs = from_utf8(&buffer[tzh_leapcnt_end..tzh_charcnt_end]).unwrap();
420
421        let tzh_typecnt: Vec<Ttinfo> = buffer[tzh_timecnt_end..tzh_typecnt_end]
422            .chunks_exact(6)
423            .map(|tti| {
424                let offset = tti[5];
425                let index = abbrs
426                    .chars()
427                    .take(offset as usize)
428                    .filter(|x| *x == '\0')
429                    .count();
430                Ttinfo {
431                    tt_utoff: BE::read_i32(&tti[0..4]) as isize,
432                    tt_isdst: tti[4],
433                    tt_abbrind: index as u8,
434                }
435            })
436            .collect();
437
438        let mut tz_abbr: Vec<String> = abbrs.split("\u{0}").map(|st| st.to_string()).collect();
439        // Removes last empty char
440        if tz_abbr.pop().is_none() {
441            return Err(TzError::EmptyString);
442        };
443
444        Ok(Tz {
445            tzh_timecnt_data,
446            tzh_timecnt_indices: tzh_timecnt_indices.to_vec(),
447            tzh_typecnt,
448            tz_abbr,
449        })
450    }
451
452    #[cfg(feature = "std")]
453    fn parse_data(buffer: &[u8], header: Header, filename: &str) -> Result<Tz, TzError> {
454        // Calculates fields lengths and indexes (Version 2 format)
455        let tzh_timecnt_len: usize = header.tzh_timecnt * 9;
456        let tzh_typecnt_len: usize = header.tzh_typecnt * 6;
457        let tzh_leapcnt_len: usize = header.tzh_leapcnt * 12;
458        let tzh_charcnt_len: usize = header.tzh_charcnt;
459        let tzh_timecnt_end: usize = HEADER_LEN + header.v2_header_start + tzh_timecnt_len;
460        let tzh_typecnt_end: usize = tzh_timecnt_end + tzh_typecnt_len;
461        let tzh_leapcnt_end: usize = tzh_typecnt_end + tzh_leapcnt_len;
462        let tzh_charcnt_end: usize = tzh_leapcnt_end + tzh_charcnt_len;
463
464        // Extracting data fields
465        let tzh_timecnt_data: Vec<i64> = buffer[HEADER_LEN + header.v2_header_start
466            ..HEADER_LEN + header.v2_header_start + header.tzh_timecnt * 8]
467            .chunks_exact(8)
468            .map(BE::read_i64)
469            .collect();
470
471        let tzh_timecnt_indices: &[u8] =
472            &buffer[HEADER_LEN + header.v2_header_start + header.tzh_timecnt * 8..tzh_timecnt_end];
473
474        let abbrs = from_utf8(&buffer[tzh_leapcnt_end..tzh_charcnt_end])?;
475
476        let tzh_typecnt: Vec<Ttinfo> = buffer[tzh_timecnt_end..tzh_typecnt_end]
477            .chunks_exact(6)
478            .map(|tti| {
479                let offset = tti[5];
480                let index = abbrs
481                    .chars()
482                    .take(offset as usize)
483                    .filter(|x| *x == '\0')
484                    .count();
485                Ttinfo {
486                    tt_utoff: BE::read_i32(&tti[0..4]) as isize,
487                    tt_isdst: tti[4],
488                    tt_abbrind: index as u8,
489                }
490            })
491            .collect();
492
493        let mut tz_abbr: Vec<String> = abbrs.split('\u{0}').map(|st| st.to_string()).collect();
494        // Removes last empty char
495        if tz_abbr.pop().is_none() {
496            return Err(TzError::EmptyString);
497        };
498
499        // Generating zone name (ie. Europe/Paris) from requested file name
500        let mut timezone = String::new();
501        #[cfg(not(windows))]
502        let mut tz: Vec<&str> = filename.split('/').collect();
503        #[cfg(windows)]
504        let mut tz: Vec<&str> = filename.split("\\").collect();
505        // To prevent crash (case of requested directory separator unmatching OS separator)
506        if tz.len() < 3 {
507            return Err(TzError::InvalidTimezone);
508        }
509        for _ in 0..(tz.len()) - 2 {
510            tz.remove(0);
511        }
512        if tz[0] != "zoneinfo" {
513            timezone.push_str(tz[0]);
514            timezone.push('/');
515        }
516        timezone.push_str(tz[1]);
517
518        #[cfg(any(feature = "parse", feature = "json"))]
519        {
520            return Ok(Tz {
521                tzh_timecnt_data,
522                tzh_timecnt_indices: tzh_timecnt_indices.to_vec(),
523                tzh_typecnt,
524                tz_abbr,
525                name: timezone,
526            });
527        }
528
529        #[cfg(not(any(feature = "parse", feature = "json")))]
530        Ok(Tz {
531            tzh_timecnt_data,
532            tzh_timecnt_indices: tzh_timecnt_indices.to_vec(),
533            tzh_typecnt,
534            tz_abbr,
535        })
536    }
537
538    #[cfg(any(feature = "std", feature = "parse", feature = "json"))]
539    fn read(tz: &str) -> Result<Vec<u8>, std::io::Error> {
540        let mut f = File::open(tz)?;
541        let mut buffer = Vec::new();
542        f.read_to_end(&mut buffer)?;
543        Ok(buffer)
544    }
545
546    #[cfg(any(feature = "parse", feature = "json"))]
547    /// Returns year's transition times for a timezone.
548    /// If year is Some(0), returns current year's transition times.
549    /// If there's no transition time for selected year, returns the last occured transition time (zone's current parameters).
550    /// If no year (None) is specified, returns all transition times recorded in the TZfile .
551    ///
552    /// ```rust
553    /// # let tzfile = if cfg!(windows) { "c:\\Users\\nbauw\\Dev\\zoneinfo\\Europe\\Paris" } else { "/usr/share/zoneinfo/Europe/Paris" };
554    /// use libtzfile::Tz;
555    /// println!("{:?}", Tz::new(tzfile).unwrap().transition_times(Some(2020)).unwrap());
556    /// ```
557    ///
558    /// ```text
559    /// [TransitionTime { time: 2020-03-29T01:00:00Z, utc_offset: 7200, isdst: true, abbreviation: "CEST" }, TransitionTime { time: 2020-10-25T01:00:00Z, utc_offset: 3600, isdst: false, abbreviation: "CET" }]
560    /// ```
561    pub fn transition_times(&self, y: Option<i32>) -> Result<Vec<TransitionTime>, TzError> {
562        let timezone = self;
563
564        // Fix for issue #3 "Calling zoneinfo on a file without transition times panics"
565        // We return a NoData error if no transition times are recorded in the TZFile.
566        if timezone.tzh_timecnt_data.len() == 0 {
567            return Err(TzError::NoData);
568        }
569
570        // used to store transition time indices
571        let mut timechanges = Vec::new();
572        let mut nearest_timechange: usize = 0;
573
574        // Used to store parsed transition times
575        let mut parsedtimechanges = Vec::new();
576
577        // Get and store the transition time indices for requested
578        if y.is_some() {
579            let d = Utc::now();
580            let y = y.unwrap();
581            // year = 0 ? current year is requested
582            let y = if y == 0 {
583                d.format("%Y").to_string().parse()?
584            } else {
585                y
586            };
587            // for year comparison
588            // We can use unwrap safely with Utc:
589            // (from Chrono doc) unwrap() is best combined with time zone types where the mapping can never fail like Utc and FixedOffset.
590            let yearbeg = Utc.with_ymd_and_hms(y, 1, 1, 0, 0, 0).unwrap().timestamp();
591            let yearend = Utc
592                .with_ymd_and_hms(y, 12, 31, 0, 0, 0)
593                .unwrap()
594                .timestamp();
595            for t in 0..timezone.tzh_timecnt_data.len() {
596                if timezone.tzh_timecnt_data[t] > yearbeg && timezone.tzh_timecnt_data[t] < yearend
597                {
598                    timechanges.push(t);
599                }
600                if timezone.tzh_timecnt_data[t] < yearbeg {
601                    nearest_timechange = t;
602                };
603            }
604        } else {
605            // No year requested ? stores all transition times
606            for t in 0..timezone.tzh_timecnt_data.len() {
607                /* patch : chrono panics on an overflowing timestamp, and a 0xF800000000000000 timestamp is present in some Debian 10 TZfiles.*/
608                if timezone.tzh_timecnt_data[t] != -576460752303423488 {
609                    timechanges.push(t)
610                };
611            }
612        }
613
614        // Populating returned Vec<Tt>
615        if timechanges.len() != 0 {
616            for t in 0..timechanges.len() {
617                let tc = TransitionTime {
618                    time: Utc
619                        .timestamp_opt(timezone.tzh_timecnt_data[timechanges[t]], 0)
620                        .unwrap(),
621                    utc_offset: timezone.tzh_typecnt
622                        [timezone.tzh_timecnt_indices[timechanges[t]] as usize]
623                        .tt_utoff,
624                    isdst: timezone.tzh_typecnt
625                        [timezone.tzh_timecnt_indices[timechanges[t]] as usize]
626                        .tt_isdst
627                        == 1,
628                    abbreviation: timezone.tz_abbr[timezone.tzh_typecnt
629                        [timezone.tzh_timecnt_indices[timechanges[t]] as usize]
630                        .tt_abbrind as usize]
631                        .to_string(),
632                };
633                parsedtimechanges.push(tc);
634            }
635        } else {
636            let tc = TransitionTime {
637                time: Utc
638                    .timestamp_opt(timezone.tzh_timecnt_data[nearest_timechange], 0)
639                    .unwrap(),
640                utc_offset: timezone.tzh_typecnt
641                    [timezone.tzh_timecnt_indices[nearest_timechange] as usize]
642                    .tt_utoff,
643                isdst: timezone.tzh_typecnt
644                    [timezone.tzh_timecnt_indices[nearest_timechange] as usize]
645                    .tt_isdst
646                    == 1,
647                abbreviation: timezone.tz_abbr[timezone.tzh_typecnt
648                    [timezone.tzh_timecnt_indices[nearest_timechange] as usize]
649                    .tt_abbrind as usize]
650                    .to_string(),
651            };
652            parsedtimechanges.push(tc);
653        }
654        Ok(parsedtimechanges)
655    }
656
657    #[cfg(any(feature = "parse", feature = "json"))]
658    /// Returns convenient data about a timezone for current date and time.
659    /// ```rust
660    /// # let tzfile = if cfg!(windows) { "c:\\Users\\nbauw\\Dev\\zoneinfo\\Europe\\Paris" } else { "/usr/share/zoneinfo/Europe/Paris" };
661    /// use libtzfile::Tz;
662    /// println!("{:?}", Tz::new(tzfile).unwrap().zoneinfo().unwrap());
663    /// ```
664    ///
665    /// ```text
666    /// Tzinfo { timezone: "Europe/Paris", utc_datetime: 2020-09-05T16:41:44.279502100Z, datetime: 2020-09-05T18:41:44.279502100+02:00, dst_from: Some(2020-03-29T01:00:00Z), dst_until: Some(2020-10-25T01:00:00Z), dst_period: true, raw_offset: 3600, dst_offset: 7200, utc_offset: +02:00, abbreviation: "CEST", week_number: 36 }
667    /// ```
668    pub fn zoneinfo(&self) -> Result<Tzinfo, TzError> {
669        let parsedtimechanges = match self.transition_times(Some(0)) {
670            Ok(p) => p,
671            Err(TzError::NoData) => Vec::new(),
672            Err(e) => return Err(e),
673        };
674        let d = Utc::now();
675        if parsedtimechanges.len() == 2 {
676            // 2 times changes the same year ? DST observed
677            // Are we in a dst period ? true / false
678            let dst = d > parsedtimechanges[0].time && d < parsedtimechanges[1].time;
679            let utc_offset = if dst == true {
680                FixedOffset::east_opt(parsedtimechanges[0].utc_offset as i32).unwrap()
681            } else {
682                FixedOffset::east_opt(parsedtimechanges[1].utc_offset as i32).unwrap()
683            };
684            Ok(Tzinfo {
685                timezone: (self.name).clone(),
686                week_number: d
687                    .with_timezone(&utc_offset)
688                    .format("%V")
689                    .to_string()
690                    .parse()?,
691                utc_datetime: d,
692                datetime: d.with_timezone(&utc_offset),
693                dst_from: Some(parsedtimechanges[0].time),
694                dst_until: Some(parsedtimechanges[1].time),
695                dst_period: dst,
696                raw_offset: parsedtimechanges[1].utc_offset,
697                dst_offset: parsedtimechanges[0].utc_offset,
698                utc_offset: utc_offset,
699                abbreviation: if dst == true {
700                    parsedtimechanges[0].abbreviation.clone()
701                } else {
702                    parsedtimechanges[1].abbreviation.clone()
703                },
704            })
705        } else if parsedtimechanges.len() == 1 {
706            let utc_offset = FixedOffset::east_opt(parsedtimechanges[0].utc_offset as i32).unwrap();
707            Ok(Tzinfo {
708                timezone: (self.name).clone(),
709                week_number: d
710                    .with_timezone(&utc_offset)
711                    .format("%V")
712                    .to_string()
713                    .parse()?,
714                utc_datetime: d,
715                datetime: d.with_timezone(&utc_offset),
716                dst_from: None,
717                dst_until: None,
718                dst_period: false,
719                raw_offset: parsedtimechanges[0].utc_offset,
720                dst_offset: 0,
721                utc_offset: utc_offset,
722                abbreviation: parsedtimechanges[0].abbreviation.clone(),
723            })
724        } else if parsedtimechanges.len() == 0 {
725            // Addition for TZFiles that does NOT contain any transition time
726            let utc_offset = FixedOffset::east_opt(self.tzh_typecnt[0].tt_utoff as i32).unwrap();
727            Ok(Tzinfo {
728                timezone: (self.name).clone(),
729                week_number: d
730                    .with_timezone(&utc_offset)
731                    .format("%V")
732                    .to_string()
733                    .parse()?,
734                utc_datetime: d,
735                datetime: d.with_timezone(&utc_offset),
736                dst_from: None,
737                dst_until: None,
738                dst_period: false,
739                raw_offset: self.tzh_typecnt[0].tt_utoff,
740                dst_offset: 0,
741                utc_offset: utc_offset,
742                abbreviation: (self.name).clone(),
743            })
744        } else {
745            Err(TzError::NoData)
746        }
747    }
748}