parse_zoneinfo/
line.rs

1//! Parsing zoneinfo data files, line-by-line.
2//!
3//! This module provides functions that take a line of input from a zoneinfo
4//! data file and attempts to parse it, returning the details of the line if
5//! it gets parsed successfully. It classifies them as `Rule`, `Link`,
6//! `Zone`, or `Continuation` lines.
7//!
8//! `Line` is the type that parses and holds zoneinfo line data. To try to
9//! parse a string, use the `Line::from_str` constructor. (This isn’t the
10//! `FromStr` trait, so you can’t use `parse` on a string. Sorry!)
11//!
12//! ## Examples
13//!
14//! Parsing a `Rule` line:
15//!
16//! ```
17//! use parse_zoneinfo::line::*;
18//!
19//! let parser = LineParser::default();
20//! let line = parser.parse_str("Rule  EU  1977    1980    -   Apr Sun>=1   1:00u  1:00    S");
21//!
22//! assert_eq!(line, Ok(Line::Rule(Rule {
23//!     name:         "EU",
24//!     from_year:    Year::Number(1977),
25//!     to_year:      Some(Year::Number(1980)),
26//!     month:        Month::April,
27//!     day:          DaySpec::FirstOnOrAfter(Weekday::Sunday, 1),
28//!     time:         TimeSpec::HoursMinutes(1, 0).with_type(TimeType::UTC),
29//!     time_to_add:  TimeSpec::HoursMinutes(1, 0),
30//!     letters:      Some("S"),
31//! })));
32//! ```
33//!
34//! Parsing a `Zone` line:
35//!
36//! ```
37//! use parse_zoneinfo::line::*;
38//!
39//! let parser = LineParser::default();
40//! let line = parser.parse_str("Zone  Australia/Adelaide  9:30  Aus  AC%sT  1971 Oct 31  2:00:00");
41//!
42//! assert_eq!(line, Ok(Line::Zone(Zone {
43//!     name: "Australia/Adelaide",
44//!     info: ZoneInfo {
45//!         utc_offset:  TimeSpec::HoursMinutes(9, 30),
46//!         saving:      Saving::Multiple("Aus"),
47//!         format:      "AC%sT",
48//!         time:        Some(ChangeTime::UntilTime(
49//!                         Year::Number(1971),
50//!                         Month::October,
51//!                         DaySpec::Ordinal(31),
52//!                         TimeSpec::HoursMinutesSeconds(2, 0, 0).with_type(TimeType::Wall))
53//!                      ),
54//!     },
55//! })));
56//! ```
57//!
58//! Parsing a `Link` line:
59//!
60//! ```
61//! use parse_zoneinfo::line::*;
62//!
63//! let parser = LineParser::default();
64//! let line = parser.parse_str("Link  Europe/Istanbul  Asia/Istanbul");
65//! assert_eq!(line, Ok(Line::Link(Link {
66//!     existing:  "Europe/Istanbul",
67//!     new:       "Asia/Istanbul",
68//! })));
69//! ```
70
71use std::fmt;
72use std::str::FromStr;
73
74use regex::{Captures, Regex};
75
76pub struct LineParser {
77    rule_line: Regex,
78    day_field: Regex,
79    hm_field: Regex,
80    hms_field: Regex,
81    zone_line: Regex,
82    continuation_line: Regex,
83    link_line: Regex,
84    empty_line: Regex,
85}
86
87#[derive(PartialEq, Debug, Clone)]
88pub enum Error {
89    FailedYearParse(String),
90    FailedMonthParse(String),
91    FailedWeekdayParse(String),
92    InvalidLineType(String),
93    TypeColumnContainedNonHyphen(String),
94    CouldNotParseSaving(String),
95    InvalidDaySpec(String),
96    InvalidTimeSpecAndType(String),
97    NonWallClockInTimeSpec(String),
98    NotParsedAsRuleLine,
99    NotParsedAsZoneLine,
100    NotParsedAsLinkLine,
101}
102
103impl fmt::Display for Error {
104    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
105        match self {
106            Error::FailedYearParse(s) => write!(f, "failed to parse as a year value: \"{}\"", s),
107            Error::FailedMonthParse(s) => write!(f, "failed to parse as a month value: \"{}\"", s),
108            Error::FailedWeekdayParse(s) => {
109                write!(f, "failed to parse as a weekday value: \"{}\"", s)
110            }
111            Error::InvalidLineType(s) => write!(f, "line with invalid format: \"{}\"", s),
112            Error::TypeColumnContainedNonHyphen(s) => {
113                write!(
114                    f,
115                    "'type' column is not a hyphen but has the value: \"{}\"",
116                    s
117                )
118            }
119            Error::CouldNotParseSaving(s) => write!(f, "failed to parse RULES column: \"{}\"", s),
120            Error::InvalidDaySpec(s) => write!(f, "invalid day specification ('ON'): \"{}\"", s),
121            Error::InvalidTimeSpecAndType(s) => write!(f, "invalid time: \"{}\"", s),
122            Error::NonWallClockInTimeSpec(s) => {
123                write!(f, "time value not given as wall time: \"{}\"", s)
124            }
125            Error::NotParsedAsRuleLine => write!(f, "failed to parse line as a rule"),
126            Error::NotParsedAsZoneLine => write!(f, "failed to parse line as a zone"),
127            Error::NotParsedAsLinkLine => write!(f, "failed to parse line as a link"),
128        }
129    }
130}
131
132impl std::error::Error for Error {}
133
134impl Default for LineParser {
135    fn default() -> Self {
136        LineParser {
137            rule_line: Regex::new(
138                r##"(?x) ^
139                Rule \s+
140                ( ?P<name>    \S+)  \s+
141                ( ?P<from>    \S+)  \s+
142                ( ?P<to>      \S+)  \s+
143                ( ?P<type>    \S+)  \s+
144                ( ?P<in>      \S+)  \s+
145                ( ?P<on>      \S+)  \s+
146                ( ?P<at>      \S+)  \s+
147                ( ?P<save>    \S+)  \s+
148                ( ?P<letters> \S+)  \s*
149                (\#.*)?
150            $ "##,
151            )
152            .unwrap(),
153
154            day_field: Regex::new(
155                r##"(?x) ^
156                ( ?P<weekday> \w+ )
157                ( ?P<sign>    [<>] = )
158                ( ?P<day>     \d+ )
159            $ "##,
160            )
161            .unwrap(),
162
163            hm_field: Regex::new(
164                r##"(?x) ^
165                ( ?P<sign> -? )
166                ( ?P<hour> \d{1,2} ) : ( ?P<minute> \d{2} )
167                ( ?P<flag> [wsugz] )?
168            $ "##,
169            )
170            .unwrap(),
171
172            hms_field: Regex::new(
173                r##"(?x) ^
174                ( ?P<sign> -? )
175                ( ?P<hour> \d{1,2} ) : ( ?P<minute> \d{2} ) : ( ?P<second> \d{2} )
176                ( ?P<flag> [wsugz] )?
177            $ "##,
178            )
179            .unwrap(),
180
181            zone_line: Regex::new(
182                r##"(?x) ^
183                Zone \s+
184                ( ?P<name> [A-Za-z0-9/_+-]+ )  \s+
185                ( ?P<gmtoff>     \S+ )  \s+
186                ( ?P<rulessave>  \S+ )  \s+
187                ( ?P<format>     \S+ )  \s*
188                ( ?P<year>       [0-9]+)? \s*
189                ( ?P<month>      [A-Za-z]+)? \s*
190                ( ?P<day>        [A-Za-z0-9><=]+ )? \s*
191                ( ?P<time>       [0-9:]+[suwz]? )? \s*
192                (\#.*)?
193            $ "##,
194            )
195            .unwrap(),
196
197            continuation_line: Regex::new(
198                r##"(?x) ^
199                \s+
200                ( ?P<gmtoff>     \S+ )  \s+
201                ( ?P<rulessave>  \S+ )  \s+
202                ( ?P<format>     \S+ )  \s*
203                ( ?P<year>       [0-9]+)? \s*
204                ( ?P<month>      [A-Za-z]+)? \s*
205                ( ?P<day>        [A-Za-z0-9><=]+ )? \s*
206                ( ?P<time>       [0-9:]+[suwz]? )? \s*
207                (\#.*)?
208            $ "##,
209            )
210            .unwrap(),
211
212            link_line: Regex::new(
213                r##"(?x) ^
214                Link  \s+
215                ( ?P<target>  \S+ )  \s+
216                ( ?P<name>    \S+ )  \s*
217                (\#.*)?
218            $ "##,
219            )
220            .unwrap(),
221
222            empty_line: Regex::new(
223                r##"(?x) ^
224                \s*
225                (\#.*)?
226            $"##,
227            )
228            .unwrap(),
229        }
230    }
231}
232
233/// A **year** definition field.
234///
235/// A year has one of the following representations in a file:
236///
237/// - `min` or `minimum`, the minimum year possible, for when a rule needs to
238///   apply up until the first rule with a specific year;
239/// - `max` or `maximum`, the maximum year possible, for when a rule needs to
240///   apply after the last rule with a specific year;
241/// - a year number, referring to a specific year.
242#[derive(PartialEq, Debug, Copy, Clone)]
243pub enum Year {
244    /// The minimum year possible: `min` or `minimum`.
245    Minimum,
246    /// The maximum year possible: `max` or `maximum`.
247    Maximum,
248    /// A specific year number.
249    Number(i64),
250}
251
252impl FromStr for Year {
253    type Err = Error;
254
255    fn from_str(input: &str) -> Result<Year, Self::Err> {
256        Ok(match &*input.to_ascii_lowercase() {
257            "min" | "minimum" => Year::Minimum,
258            "max" | "maximum" => Year::Maximum,
259            year => match year.parse() {
260                Ok(year) => Year::Number(year),
261                Err(_) => return Err(Error::FailedYearParse(input.to_string())),
262            },
263        })
264    }
265}
266
267/// A **month** field, which is actually just a wrapper around
268/// `datetime::Month`.
269#[derive(PartialEq, Debug, Copy, Clone)]
270pub enum Month {
271    January = 1,
272    February = 2,
273    March = 3,
274    April = 4,
275    May = 5,
276    June = 6,
277    July = 7,
278    August = 8,
279    September = 9,
280    October = 10,
281    November = 11,
282    December = 12,
283}
284
285impl Month {
286    fn length(self, is_leap: bool) -> i8 {
287        match self {
288            Month::January => 31,
289            Month::February if is_leap => 29,
290            Month::February => 28,
291            Month::March => 31,
292            Month::April => 30,
293            Month::May => 31,
294            Month::June => 30,
295            Month::July => 31,
296            Month::August => 31,
297            Month::September => 30,
298            Month::October => 31,
299            Month::November => 30,
300            Month::December => 31,
301        }
302    }
303
304    /// Get the next calendar month, with an error going from Dec->Jan
305    fn next_in_year(self) -> Result<Month, &'static str> {
306        Ok(match self {
307            Month::January => Month::February,
308            Month::February => Month::March,
309            Month::March => Month::April,
310            Month::April => Month::May,
311            Month::May => Month::June,
312            Month::June => Month::July,
313            Month::July => Month::August,
314            Month::August => Month::September,
315            Month::September => Month::October,
316            Month::October => Month::November,
317            Month::November => Month::December,
318            Month::December => Err("Cannot wrap year from dec->jan")?,
319        })
320    }
321
322    /// Get the previous calendar month, with an error going from Jan->Dec
323    fn prev_in_year(self) -> Result<Month, &'static str> {
324        Ok(match self {
325            Month::January => Err("Cannot wrap years from jan->dec")?,
326            Month::February => Month::January,
327            Month::March => Month::February,
328            Month::April => Month::March,
329            Month::May => Month::April,
330            Month::June => Month::May,
331            Month::July => Month::June,
332            Month::August => Month::July,
333            Month::September => Month::August,
334            Month::October => Month::September,
335            Month::November => Month::October,
336            Month::December => Month::November,
337        })
338    }
339}
340
341impl FromStr for Month {
342    type Err = Error;
343
344    /// Attempts to parse the given string into a value of this type.
345    fn from_str(input: &str) -> Result<Month, Self::Err> {
346        Ok(match &*input.to_ascii_lowercase() {
347            "jan" | "january" => Month::January,
348            "feb" | "february" => Month::February,
349            "mar" | "march" => Month::March,
350            "apr" | "april" => Month::April,
351            "may" => Month::May,
352            "jun" | "june" => Month::June,
353            "jul" | "july" => Month::July,
354            "aug" | "august" => Month::August,
355            "sep" | "september" => Month::September,
356            "oct" | "october" => Month::October,
357            "nov" | "november" => Month::November,
358            "dec" | "december" => Month::December,
359            other => return Err(Error::FailedMonthParse(other.to_string())),
360        })
361    }
362}
363
364/// A **weekday** field, which is actually just a wrapper around
365/// `datetime::Weekday`.
366#[derive(PartialEq, Debug, Copy, Clone)]
367pub enum Weekday {
368    Sunday,
369    Monday,
370    Tuesday,
371    Wednesday,
372    Thursday,
373    Friday,
374    Saturday,
375}
376
377impl FromStr for Weekday {
378    type Err = Error;
379
380    fn from_str(input: &str) -> Result<Weekday, Self::Err> {
381        Ok(match &*input.to_ascii_lowercase() {
382            "mon" | "monday" => Weekday::Monday,
383            "tue" | "tuesday" => Weekday::Tuesday,
384            "wed" | "wednesday" => Weekday::Wednesday,
385            "thu" | "thursday" => Weekday::Thursday,
386            "fri" | "friday" => Weekday::Friday,
387            "sat" | "saturday" => Weekday::Saturday,
388            "sun" | "sunday" => Weekday::Sunday,
389            other => return Err(Error::FailedWeekdayParse(other.to_string())),
390        })
391    }
392}
393
394/// A **day** definition field.
395///
396/// This can be given in either absolute terms (such as “the fifth day of the
397/// month”), or relative terms (such as “the last Sunday of the month”, or
398/// “the last Friday before or including the 13th”).
399///
400/// Note that in the last example, it’s allowed for that particular Friday to
401/// *be* the 13th in question.
402#[derive(PartialEq, Debug, Copy, Clone)]
403pub enum DaySpec {
404    /// A specific day of the month, given by its number.
405    Ordinal(i8),
406    /// The last day of the month with a specific weekday.
407    Last(Weekday),
408    /// The **last** day with the given weekday **before** (or including) a
409    /// day with a specific number.
410    LastOnOrBefore(Weekday, i8),
411    /// The **first** day with the given weekday **after** (or including) a
412    /// day with a specific number.
413    FirstOnOrAfter(Weekday, i8),
414}
415
416impl Weekday {
417    fn calculate(year: i64, month: Month, day: i8) -> Weekday {
418        let m = month as i64;
419        let y = if m < 3 { year - 1 } else { year };
420        let d = day as i64;
421        const T: [i64; 12] = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
422        match (y + y / 4 - y / 100 + y / 400 + T[m as usize - 1] + d) % 7 {
423            0 => Weekday::Sunday,
424            1 => Weekday::Monday,
425            2 => Weekday::Tuesday,
426            3 => Weekday::Wednesday,
427            4 => Weekday::Thursday,
428            5 => Weekday::Friday,
429            6 => Weekday::Saturday,
430            _ => panic!("why is negative modulus designed so?"),
431        }
432    }
433}
434
435#[cfg(test)]
436#[test]
437fn weekdays() {
438    assert_eq!(
439        Weekday::calculate(1970, Month::January, 1),
440        Weekday::Thursday
441    );
442    assert_eq!(
443        Weekday::calculate(2017, Month::February, 11),
444        Weekday::Saturday
445    );
446    assert_eq!(Weekday::calculate(1890, Month::March, 2), Weekday::Sunday);
447    assert_eq!(Weekday::calculate(2100, Month::April, 20), Weekday::Tuesday);
448    assert_eq!(Weekday::calculate(2009, Month::May, 31), Weekday::Sunday);
449    assert_eq!(Weekday::calculate(2001, Month::June, 9), Weekday::Saturday);
450    assert_eq!(Weekday::calculate(1995, Month::July, 21), Weekday::Friday);
451    assert_eq!(Weekday::calculate(1982, Month::August, 8), Weekday::Sunday);
452    assert_eq!(
453        Weekday::calculate(1962, Month::September, 6),
454        Weekday::Thursday
455    );
456    assert_eq!(
457        Weekday::calculate(1899, Month::October, 14),
458        Weekday::Saturday
459    );
460    assert_eq!(
461        Weekday::calculate(2016, Month::November, 18),
462        Weekday::Friday
463    );
464    assert_eq!(
465        Weekday::calculate(2010, Month::December, 19),
466        Weekday::Sunday
467    );
468    assert_eq!(
469        Weekday::calculate(2016, Month::February, 29),
470        Weekday::Monday
471    );
472}
473
474fn is_leap(year: i64) -> bool {
475    // Leap year rules: years which are factors of 4, except those divisible
476    // by 100, unless they are divisible by 400.
477    //
478    // We test most common cases first: 4th year, 100th year, then 400th year.
479    //
480    // We factor out 4 from 100 since it was already tested, leaving us checking
481    // if it's divisible by 25. Afterwards, we do the same, factoring 25 from
482    // 400, leaving us with 16.
483    //
484    // Factors of 4 and 16 can quickly be found with bitwise AND.
485    year & 3 == 0 && (year % 25 != 0 || year & 15 == 0)
486}
487
488#[cfg(test)]
489#[test]
490fn leap_years() {
491    assert!(!is_leap(1900));
492    assert!(is_leap(1904));
493    assert!(is_leap(1964));
494    assert!(is_leap(1996));
495    assert!(!is_leap(1997));
496    assert!(!is_leap(1997));
497    assert!(!is_leap(1999));
498    assert!(is_leap(2000));
499    assert!(is_leap(2016));
500    assert!(!is_leap(2100));
501}
502
503impl DaySpec {
504    /// Converts this day specification to a concrete date, given the year and
505    /// month it should occur in.
506    pub fn to_concrete_day(&self, year: i64, month: Month) -> (Month, i8) {
507        let leap = is_leap(year);
508        let length = month.length(leap);
509        // we will never hit the 0 because we unwrap prev_in_year below
510        let prev_length = month.prev_in_year().map(|m| m.length(leap)).unwrap_or(0);
511
512        match *self {
513            DaySpec::Ordinal(day) => (month, day),
514            DaySpec::Last(weekday) => (
515                month,
516                (1..length + 1)
517                    .rev()
518                    .find(|&day| Weekday::calculate(year, month, day) == weekday)
519                    .unwrap(),
520            ),
521            DaySpec::LastOnOrBefore(weekday, day) => (-7..day + 1)
522                .rev()
523                .flat_map(|inner_day| {
524                    if inner_day >= 1 && Weekday::calculate(year, month, inner_day) == weekday {
525                        Some((month, inner_day))
526                    } else if inner_day < 1
527                        && Weekday::calculate(
528                            year,
529                            month.prev_in_year().unwrap(),
530                            prev_length + inner_day,
531                        ) == weekday
532                    {
533                        // inner_day is negative, so this is subtraction
534                        Some((month.prev_in_year().unwrap(), prev_length + inner_day))
535                    } else {
536                        None
537                    }
538                })
539                .next()
540                .unwrap(),
541            DaySpec::FirstOnOrAfter(weekday, day) => (day..day + 8)
542                .flat_map(|inner_day| {
543                    if inner_day <= length && Weekday::calculate(year, month, inner_day) == weekday
544                    {
545                        Some((month, inner_day))
546                    } else if inner_day > length
547                        && Weekday::calculate(
548                            year,
549                            month.next_in_year().unwrap(),
550                            inner_day - length,
551                        ) == weekday
552                    {
553                        Some((month.next_in_year().unwrap(), inner_day - length))
554                    } else {
555                        None
556                    }
557                })
558                .next()
559                .unwrap(),
560        }
561    }
562}
563
564/// A **time** definition field.
565///
566/// A time must have an hours component, with optional minutes and seconds
567/// components. It can also be negative with a starting ‘-’.
568///
569/// Hour 0 is midnight at the start of the day, and Hour 24 is midnight at the
570/// end of the day.
571#[derive(PartialEq, Debug, Copy, Clone)]
572pub enum TimeSpec {
573    /// A number of hours.
574    Hours(i8),
575    /// A number of hours and minutes.
576    HoursMinutes(i8, i8),
577    /// A number of hours, minutes, and seconds.
578    HoursMinutesSeconds(i8, i8, i8),
579    /// Zero, or midnight at the start of the day.
580    Zero,
581}
582
583impl TimeSpec {
584    /// Returns the number of seconds past midnight that this time spec
585    /// represents.
586    pub fn as_seconds(self) -> i64 {
587        match self {
588            TimeSpec::Hours(h) => h as i64 * 60 * 60,
589            TimeSpec::HoursMinutes(h, m) => h as i64 * 60 * 60 + m as i64 * 60,
590            TimeSpec::HoursMinutesSeconds(h, m, s) => h as i64 * 60 * 60 + m as i64 * 60 + s as i64,
591            TimeSpec::Zero => 0,
592        }
593    }
594}
595
596#[derive(PartialEq, Debug, Copy, Clone)]
597pub enum TimeType {
598    Wall,
599    Standard,
600    UTC,
601}
602
603#[derive(PartialEq, Debug, Copy, Clone)]
604pub struct TimeSpecAndType(pub TimeSpec, pub TimeType);
605
606impl TimeSpec {
607    pub fn with_type(self, timetype: TimeType) -> TimeSpecAndType {
608        TimeSpecAndType(self, timetype)
609    }
610}
611
612/// The time at which the rules change for a location.
613///
614/// This is described with as few units as possible: a change that occurs at
615/// the beginning of the year lists only the year, a change that occurs on a
616/// particular day has to list the year, month, and day, and one that occurs
617/// at a particular second has to list everything.
618#[derive(PartialEq, Debug, Copy, Clone)]
619pub enum ChangeTime {
620    /// The earliest point in a particular **year**.
621    UntilYear(Year),
622    /// The earliest point in a particular **month**.
623    UntilMonth(Year, Month),
624    /// The earliest point in a particular **day**.
625    UntilDay(Year, Month, DaySpec),
626    /// The earliest point in a particular **hour, minute, or second**.
627    UntilTime(Year, Month, DaySpec, TimeSpecAndType),
628}
629
630impl ChangeTime {
631    /// Convert this change time to an absolute timestamp, as the number of
632    /// seconds since the Unix epoch that the change occurs at.
633    pub fn to_timestamp(&self) -> i64 {
634        fn seconds_in_year(year: i64) -> i64 {
635            if is_leap(year) {
636                366 * 24 * 60 * 60
637            } else {
638                365 * 24 * 60 * 60
639            }
640        }
641
642        fn seconds_until_start_of_year(year: i64) -> i64 {
643            if year >= 1970 {
644                (1970..year).map(seconds_in_year).sum()
645            } else {
646                -(year..1970).map(seconds_in_year).sum::<i64>()
647            }
648        }
649
650        fn time_to_timestamp(
651            year: i64,
652            month: i8,
653            day: i8,
654            hour: i8,
655            minute: i8,
656            second: i8,
657        ) -> i64 {
658            const MONTHS_NON_LEAP: [i64; 12] = [
659                0,
660                31,
661                31 + 28,
662                31 + 28 + 31,
663                31 + 28 + 31 + 30,
664                31 + 28 + 31 + 30 + 31,
665                31 + 28 + 31 + 30 + 31 + 30,
666                31 + 28 + 31 + 30 + 31 + 30 + 31,
667                31 + 28 + 31 + 30 + 31 + 30 + 31 + 31,
668                31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30,
669                31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31,
670                31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30,
671            ];
672            const MONTHS_LEAP: [i64; 12] = [
673                0,
674                31,
675                31 + 29,
676                31 + 29 + 31,
677                31 + 29 + 31 + 30,
678                31 + 29 + 31 + 30 + 31,
679                31 + 29 + 31 + 30 + 31 + 30,
680                31 + 29 + 31 + 30 + 31 + 30 + 31,
681                31 + 29 + 31 + 30 + 31 + 30 + 31 + 31,
682                31 + 29 + 31 + 30 + 31 + 30 + 31 + 31 + 30,
683                31 + 29 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31,
684                31 + 29 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30,
685            ];
686            seconds_until_start_of_year(year)
687                + 60 * 60
688                    * 24
689                    * if is_leap(year) {
690                        MONTHS_LEAP[month as usize - 1]
691                    } else {
692                        MONTHS_NON_LEAP[month as usize - 1]
693                    }
694                + 60 * 60 * 24 * (day as i64 - 1)
695                + 60 * 60 * hour as i64
696                + 60 * minute as i64
697                + second as i64
698        }
699
700        match *self {
701            ChangeTime::UntilYear(Year::Number(y)) => time_to_timestamp(y, 1, 1, 0, 0, 0),
702            ChangeTime::UntilMonth(Year::Number(y), m) => time_to_timestamp(y, m as i8, 1, 0, 0, 0),
703            ChangeTime::UntilDay(Year::Number(y), m, d) => {
704                let (m, wd) = d.to_concrete_day(y, m);
705                time_to_timestamp(y, m as i8, wd, 0, 0, 0)
706            }
707            ChangeTime::UntilTime(Year::Number(y), m, d, time) => match time.0 {
708                TimeSpec::Zero => {
709                    let (m, wd) = d.to_concrete_day(y, m);
710                    time_to_timestamp(y, m as i8, wd, 0, 0, 0)
711                }
712                TimeSpec::Hours(h) => {
713                    let (m, wd) = d.to_concrete_day(y, m);
714                    time_to_timestamp(y, m as i8, wd, h, 0, 0)
715                }
716                TimeSpec::HoursMinutes(h, min) => {
717                    let (m, wd) = d.to_concrete_day(y, m);
718                    time_to_timestamp(y, m as i8, wd, h, min, 0)
719                }
720                TimeSpec::HoursMinutesSeconds(h, min, s) => {
721                    let (m, wd) = d.to_concrete_day(y, m);
722                    time_to_timestamp(y, m as i8, wd, h, min, s)
723                }
724            },
725            _ => unreachable!(),
726        }
727    }
728
729    pub fn year(&self) -> i64 {
730        match *self {
731            ChangeTime::UntilYear(Year::Number(y)) => y,
732            ChangeTime::UntilMonth(Year::Number(y), ..) => y,
733            ChangeTime::UntilDay(Year::Number(y), ..) => y,
734            ChangeTime::UntilTime(Year::Number(y), ..) => y,
735            _ => unreachable!(),
736        }
737    }
738}
739
740/// The information contained in both zone lines *and* zone continuation lines.
741#[derive(PartialEq, Debug, Copy, Clone)]
742pub struct ZoneInfo<'a> {
743    /// The amount of time that needs to be added to UTC to get the standard
744    /// time in this zone.
745    pub utc_offset: TimeSpec,
746    /// The name of all the rules that should apply in the time zone, or the
747    /// amount of time to add.
748    pub saving: Saving<'a>,
749    /// The format for time zone abbreviations, with `%s` as the string marker.
750    pub format: &'a str,
751    /// The time at which the rules change for this location, or `None` if
752    /// these rules are in effect until the end of time (!).
753    pub time: Option<ChangeTime>,
754}
755
756/// The amount of daylight saving time (DST) to apply to this timespan. This
757/// is a special type for a certain field in a zone line, which can hold
758/// different types of value.
759#[derive(PartialEq, Debug, Copy, Clone)]
760pub enum Saving<'a> {
761    /// Just stick to the base offset.
762    NoSaving,
763    /// This amount of time should be saved while this timespan is in effect.
764    /// (This is the equivalent to there being a single one-off rule with the
765    /// given amount of time to save).
766    OneOff(TimeSpec),
767    /// All rules with the given name should apply while this timespan is in
768    /// effect.
769    Multiple(&'a str),
770}
771
772/// A **rule** definition line.
773///
774/// According to the `zic(8)` man page, a rule line has this form, along with
775/// an example:
776///
777/// ```text
778///     Rule  NAME  FROM  TO    TYPE  IN   ON       AT    SAVE  LETTER/S
779///     Rule  US    1967  1973  ‐     Apr  lastSun  2:00  1:00  D
780/// ```
781///
782/// Apart from the opening `Rule` to specify which kind of line this is, and
783/// the `type` column, every column in the line has a field in this struct.
784#[derive(PartialEq, Debug, Copy, Clone)]
785pub struct Rule<'a> {
786    /// The name of the set of rules that this rule is part of.
787    pub name: &'a str,
788    /// The first year in which the rule applies.
789    pub from_year: Year,
790    /// The final year, or `None` if’s ‘only’.
791    pub to_year: Option<Year>,
792    /// The month in which the rule takes effect.
793    pub month: Month,
794    /// The day on which the rule takes effect.
795    pub day: DaySpec,
796    /// The time of day at which the rule takes effect.
797    pub time: TimeSpecAndType,
798    /// The amount of time to be added when the rule is in effect.
799    pub time_to_add: TimeSpec,
800    /// The variable part of time zone abbreviations to be used when this rule
801    /// is in effect, if any.
802    pub letters: Option<&'a str>,
803}
804
805/// A **zone** definition line.
806///
807/// According to the `zic(8)` man page, a zone line has this form, along with
808/// an example:
809///
810/// ```text
811///     Zone  NAME                GMTOFF  RULES/SAVE  FORMAT  [UNTILYEAR [MONTH [DAY [TIME]]]]
812///     Zone  Australia/Adelaide  9:30    Aus         AC%sT   1971       Oct    31   2:00
813/// ```
814///
815/// The opening `Zone` identifier is ignored, and the last four columns are
816/// all optional, with their variants consolidated into a `ChangeTime`.
817///
818/// The `Rules/Save` column, if it contains a value, *either* contains the
819/// name of the rules to use for this zone, *or* contains a one-off period of
820/// time to save.
821///
822/// A continuation rule line contains all the same fields apart from the
823/// `Name` column and the opening `Zone` identifier.
824#[derive(PartialEq, Debug, Copy, Clone)]
825pub struct Zone<'a> {
826    /// The name of the time zone.
827    pub name: &'a str,
828    /// All the other fields of info.
829    pub info: ZoneInfo<'a>,
830}
831
832#[derive(PartialEq, Debug, Copy, Clone)]
833pub struct Link<'a> {
834    pub existing: &'a str,
835    pub new: &'a str,
836}
837
838#[derive(PartialEq, Debug, Copy, Clone)]
839pub enum Line<'a> {
840    /// This line is empty.
841    Space,
842    /// This line contains a **zone** definition.
843    Zone(Zone<'a>),
844    /// This line contains a **continuation** of a zone definition.
845    Continuation(ZoneInfo<'a>),
846    /// This line contains a **rule** definition.
847    Rule(Rule<'a>),
848    /// This line contains a **link** definition.
849    Link(Link<'a>),
850}
851
852fn parse_time_type(c: &str) -> Option<TimeType> {
853    Some(match c {
854        "w" => TimeType::Wall,
855        "s" => TimeType::Standard,
856        "u" | "g" | "z" => TimeType::UTC,
857        _ => return None,
858    })
859}
860
861impl LineParser {
862    #[deprecated]
863    pub fn new() -> Self {
864        Self::default()
865    }
866
867    fn parse_timespec_and_type(&self, input: &str) -> Result<TimeSpecAndType, Error> {
868        if input == "-" {
869            Ok(TimeSpecAndType(TimeSpec::Zero, TimeType::Wall))
870        } else if input.chars().all(|c| c == '-' || c.is_ascii_digit()) {
871            Ok(TimeSpecAndType(
872                TimeSpec::Hours(input.parse().unwrap()),
873                TimeType::Wall,
874            ))
875        } else if let Some(caps) = self.hm_field.captures(input) {
876            let sign: i8 = if caps.name("sign").unwrap().as_str() == "-" {
877                -1
878            } else {
879                1
880            };
881            let hour: i8 = caps.name("hour").unwrap().as_str().parse().unwrap();
882            let minute: i8 = caps.name("minute").unwrap().as_str().parse().unwrap();
883            let flag = caps
884                .name("flag")
885                .and_then(|c| parse_time_type(&c.as_str()[0..1]))
886                .unwrap_or(TimeType::Wall);
887
888            Ok(TimeSpecAndType(
889                TimeSpec::HoursMinutes(hour * sign, minute * sign),
890                flag,
891            ))
892        } else if let Some(caps) = self.hms_field.captures(input) {
893            let sign: i8 = if caps.name("sign").unwrap().as_str() == "-" {
894                -1
895            } else {
896                1
897            };
898            let hour: i8 = caps.name("hour").unwrap().as_str().parse().unwrap();
899            let minute: i8 = caps.name("minute").unwrap().as_str().parse().unwrap();
900            let second: i8 = caps.name("second").unwrap().as_str().parse().unwrap();
901            let flag = caps
902                .name("flag")
903                .and_then(|c| parse_time_type(&c.as_str()[0..1]))
904                .unwrap_or(TimeType::Wall);
905
906            Ok(TimeSpecAndType(
907                TimeSpec::HoursMinutesSeconds(hour * sign, minute * sign, second * sign),
908                flag,
909            ))
910        } else {
911            Err(Error::InvalidTimeSpecAndType(input.to_string()))
912        }
913    }
914
915    fn parse_timespec(&self, input: &str) -> Result<TimeSpec, Error> {
916        match self.parse_timespec_and_type(input) {
917            Ok(TimeSpecAndType(spec, TimeType::Wall)) => Ok(spec),
918            Ok(TimeSpecAndType(_, _)) => Err(Error::NonWallClockInTimeSpec(input.to_string())),
919            Err(e) => Err(e),
920        }
921    }
922
923    fn parse_dayspec(&self, input: &str) -> Result<DaySpec, Error> {
924        // Parse the field as a number if it vaguely resembles one.
925        if input.chars().all(|c| c.is_ascii_digit()) {
926            Ok(DaySpec::Ordinal(input.parse().unwrap()))
927        }
928        // Check if it stars with ‘last’, and trim off the first four bytes if
929        // it does. (Luckily, the file is ASCII, so ‘last’ is four bytes)
930        else if let Some(remainder) = input.strip_prefix("last") {
931            let weekday = remainder.parse()?;
932            Ok(DaySpec::Last(weekday))
933        }
934        // Check if it’s a relative expression with the regex.
935        else if let Some(caps) = self.day_field.captures(input) {
936            let weekday = caps.name("weekday").unwrap().as_str().parse().unwrap();
937            let day = caps.name("day").unwrap().as_str().parse().unwrap();
938
939            match caps.name("sign").unwrap().as_str() {
940                "<=" => Ok(DaySpec::LastOnOrBefore(weekday, day)),
941                ">=" => Ok(DaySpec::FirstOnOrAfter(weekday, day)),
942                _ => unreachable!("The regex only matches one of those two!"),
943            }
944        }
945        // Otherwise, give up.
946        else {
947            Err(Error::InvalidDaySpec(input.to_string()))
948        }
949    }
950
951    fn parse_rule<'a>(&self, input: &'a str) -> Result<Rule<'a>, Error> {
952        if let Some(caps) = self.rule_line.captures(input) {
953            let name = caps.name("name").unwrap().as_str();
954
955            let from_year = caps.name("from").unwrap().as_str().parse()?;
956
957            // The end year can be ‘only’ to indicate that this rule only
958            // takes place on that year.
959            let to_year = match caps.name("to").unwrap().as_str() {
960                "only" => None,
961                to => Some(to.parse()?),
962            };
963
964            // According to the spec, the only value inside the ‘type’ column
965            // should be “-”, so throw an error if it isn’t. (It only exists
966            // for compatibility with old versions that used to contain year
967            // types.) Sometimes “‐”, a Unicode hyphen, is used as well.
968            let t = caps.name("type").unwrap().as_str();
969            if t != "-" && t != "\u{2010}" {
970                return Err(Error::TypeColumnContainedNonHyphen(t.to_string()));
971            }
972
973            let month = caps.name("in").unwrap().as_str().parse()?;
974            let day = self.parse_dayspec(caps.name("on").unwrap().as_str())?;
975            let time = self.parse_timespec_and_type(caps.name("at").unwrap().as_str())?;
976            let time_to_add = self.parse_timespec(caps.name("save").unwrap().as_str())?;
977            let letters = match caps.name("letters").unwrap().as_str() {
978                "-" => None,
979                l => Some(l),
980            };
981
982            Ok(Rule {
983                name,
984                from_year,
985                to_year,
986                month,
987                day,
988                time,
989                time_to_add,
990                letters,
991            })
992        } else {
993            Err(Error::NotParsedAsRuleLine)
994        }
995    }
996
997    fn saving_from_str<'a>(&self, input: &'a str) -> Result<Saving<'a>, Error> {
998        if input == "-" {
999            Ok(Saving::NoSaving)
1000        } else if input
1001            .chars()
1002            .all(|c| c == '-' || c == '_' || c.is_alphabetic())
1003        {
1004            Ok(Saving::Multiple(input))
1005        } else if self.hm_field.is_match(input) {
1006            let time = self.parse_timespec(input)?;
1007            Ok(Saving::OneOff(time))
1008        } else {
1009            Err(Error::CouldNotParseSaving(input.to_string()))
1010        }
1011    }
1012
1013    fn zoneinfo_from_captures<'a>(&self, caps: Captures<'a>) -> Result<ZoneInfo<'a>, Error> {
1014        let utc_offset = self.parse_timespec(caps.name("gmtoff").unwrap().as_str())?;
1015        let saving = self.saving_from_str(caps.name("rulessave").unwrap().as_str())?;
1016        let format = caps.name("format").unwrap().as_str();
1017
1018        // The year, month, day, and time fields are all optional, meaning
1019        // that it should be impossible to, say, have a defined month but not
1020        // a defined year.
1021        let time = match (
1022            caps.name("year"),
1023            caps.name("month"),
1024            caps.name("day"),
1025            caps.name("time"),
1026        ) {
1027            (Some(y), Some(m), Some(d), Some(t)) => Some(ChangeTime::UntilTime(
1028                y.as_str().parse()?,
1029                m.as_str().parse()?,
1030                self.parse_dayspec(d.as_str())?,
1031                self.parse_timespec_and_type(t.as_str())?,
1032            )),
1033            (Some(y), Some(m), Some(d), _) => Some(ChangeTime::UntilDay(
1034                y.as_str().parse()?,
1035                m.as_str().parse()?,
1036                self.parse_dayspec(d.as_str())?,
1037            )),
1038            (Some(y), Some(m), _, _) => Some(ChangeTime::UntilMonth(
1039                y.as_str().parse()?,
1040                m.as_str().parse()?,
1041            )),
1042            (Some(y), _, _, _) => Some(ChangeTime::UntilYear(y.as_str().parse()?)),
1043            (None, None, None, None) => None,
1044            _ => unreachable!("Out-of-order capturing groups!"),
1045        };
1046
1047        Ok(ZoneInfo {
1048            utc_offset,
1049            saving,
1050            format,
1051            time,
1052        })
1053    }
1054
1055    fn parse_zone<'a>(&self, input: &'a str) -> Result<Zone<'a>, Error> {
1056        if let Some(caps) = self.zone_line.captures(input) {
1057            let name = caps.name("name").unwrap().as_str();
1058            let info = self.zoneinfo_from_captures(caps)?;
1059            Ok(Zone { name, info })
1060        } else {
1061            Err(Error::NotParsedAsZoneLine)
1062        }
1063    }
1064
1065    fn parse_link<'a>(&self, input: &'a str) -> Result<Link<'a>, Error> {
1066        if let Some(caps) = self.link_line.captures(input) {
1067            let target = caps.name("target").unwrap().as_str();
1068            let name = caps.name("name").unwrap().as_str();
1069            Ok(Link {
1070                existing: target,
1071                new: name,
1072            })
1073        } else {
1074            Err(Error::NotParsedAsLinkLine)
1075        }
1076    }
1077
1078    /// Attempt to parse this line, returning a `Line` depending on what
1079    /// type of line it was, or an `Error` if it couldn't be parsed.
1080    pub fn parse_str<'a>(&self, input: &'a str) -> Result<Line<'a>, Error> {
1081        if self.empty_line.is_match(input) {
1082            return Ok(Line::Space);
1083        }
1084
1085        match self.parse_zone(input) {
1086            Err(Error::NotParsedAsZoneLine) => {}
1087            result => return result.map(Line::Zone),
1088        }
1089
1090        match self.continuation_line.captures(input) {
1091            None => {}
1092            Some(caps) => return self.zoneinfo_from_captures(caps).map(Line::Continuation),
1093        }
1094
1095        match self.parse_rule(input) {
1096            Err(Error::NotParsedAsRuleLine) => {}
1097            result => return result.map(Line::Rule),
1098        }
1099
1100        match self.parse_link(input) {
1101            Err(Error::NotParsedAsLinkLine) => {}
1102            result => return result.map(Line::Link),
1103        }
1104
1105        Err(Error::InvalidLineType(input.to_string()))
1106    }
1107}
1108
1109#[cfg(test)]
1110mod tests {
1111    use super::*;
1112
1113    #[test]
1114    fn last_monday() {
1115        let dayspec = DaySpec::Last(Weekday::Monday);
1116        assert_eq!(
1117            dayspec.to_concrete_day(2016, Month::January),
1118            (Month::January, 25)
1119        );
1120        assert_eq!(
1121            dayspec.to_concrete_day(2016, Month::February),
1122            (Month::February, 29)
1123        );
1124        assert_eq!(
1125            dayspec.to_concrete_day(2016, Month::March),
1126            (Month::March, 28)
1127        );
1128        assert_eq!(
1129            dayspec.to_concrete_day(2016, Month::April),
1130            (Month::April, 25)
1131        );
1132        assert_eq!(dayspec.to_concrete_day(2016, Month::May), (Month::May, 30));
1133        assert_eq!(
1134            dayspec.to_concrete_day(2016, Month::June),
1135            (Month::June, 27)
1136        );
1137        assert_eq!(
1138            dayspec.to_concrete_day(2016, Month::July),
1139            (Month::July, 25)
1140        );
1141        assert_eq!(
1142            dayspec.to_concrete_day(2016, Month::August),
1143            (Month::August, 29)
1144        );
1145        assert_eq!(
1146            dayspec.to_concrete_day(2016, Month::September),
1147            (Month::September, 26)
1148        );
1149        assert_eq!(
1150            dayspec.to_concrete_day(2016, Month::October),
1151            (Month::October, 31)
1152        );
1153        assert_eq!(
1154            dayspec.to_concrete_day(2016, Month::November),
1155            (Month::November, 28)
1156        );
1157        assert_eq!(
1158            dayspec.to_concrete_day(2016, Month::December),
1159            (Month::December, 26)
1160        );
1161    }
1162
1163    #[test]
1164    fn first_monday_on_or_after() {
1165        let dayspec = DaySpec::FirstOnOrAfter(Weekday::Monday, 20);
1166        assert_eq!(
1167            dayspec.to_concrete_day(2016, Month::January),
1168            (Month::January, 25)
1169        );
1170        assert_eq!(
1171            dayspec.to_concrete_day(2016, Month::February),
1172            (Month::February, 22)
1173        );
1174        assert_eq!(
1175            dayspec.to_concrete_day(2016, Month::March),
1176            (Month::March, 21)
1177        );
1178        assert_eq!(
1179            dayspec.to_concrete_day(2016, Month::April),
1180            (Month::April, 25)
1181        );
1182        assert_eq!(dayspec.to_concrete_day(2016, Month::May), (Month::May, 23));
1183        assert_eq!(
1184            dayspec.to_concrete_day(2016, Month::June),
1185            (Month::June, 20)
1186        );
1187        assert_eq!(
1188            dayspec.to_concrete_day(2016, Month::July),
1189            (Month::July, 25)
1190        );
1191        assert_eq!(
1192            dayspec.to_concrete_day(2016, Month::August),
1193            (Month::August, 22)
1194        );
1195        assert_eq!(
1196            dayspec.to_concrete_day(2016, Month::September),
1197            (Month::September, 26)
1198        );
1199        assert_eq!(
1200            dayspec.to_concrete_day(2016, Month::October),
1201            (Month::October, 24)
1202        );
1203        assert_eq!(
1204            dayspec.to_concrete_day(2016, Month::November),
1205            (Month::November, 21)
1206        );
1207        assert_eq!(
1208            dayspec.to_concrete_day(2016, Month::December),
1209            (Month::December, 26)
1210        );
1211    }
1212
1213    // A couple of specific timezone transitions that we care about
1214    #[test]
1215    fn first_sunday_in_toronto() {
1216        let dayspec = DaySpec::FirstOnOrAfter(Weekday::Sunday, 25);
1217        assert_eq!(dayspec.to_concrete_day(1932, Month::April), (Month::May, 1));
1218        // asia/zion
1219        let dayspec = DaySpec::LastOnOrBefore(Weekday::Friday, 1);
1220        assert_eq!(
1221            dayspec.to_concrete_day(2012, Month::April),
1222            (Month::March, 30)
1223        );
1224    }
1225
1226    #[test]
1227    fn to_timestamp() {
1228        let time = ChangeTime::UntilYear(Year::Number(1970));
1229        assert_eq!(time.to_timestamp(), 0);
1230        let time = ChangeTime::UntilYear(Year::Number(2016));
1231        assert_eq!(time.to_timestamp(), 1451606400);
1232        let time = ChangeTime::UntilYear(Year::Number(1900));
1233        assert_eq!(time.to_timestamp(), -2208988800);
1234        let time = ChangeTime::UntilTime(
1235            Year::Number(2000),
1236            Month::February,
1237            DaySpec::Last(Weekday::Sunday),
1238            TimeSpecAndType(TimeSpec::Hours(9), TimeType::Wall),
1239        );
1240        assert_eq!(time.to_timestamp(), 951642000);
1241    }
1242
1243    macro_rules! test {
1244        ($name:ident: $input:expr => $result:expr) => {
1245            #[test]
1246            fn $name() {
1247                let parser = LineParser::default();
1248                assert_eq!(parser.parse_str($input), $result);
1249            }
1250        };
1251    }
1252
1253    test!(empty:    ""          => Ok(Line::Space));
1254    test!(spaces:   "        "  => Ok(Line::Space));
1255
1256    test!(rule_1: "Rule  US    1967  1973  ‐     Apr  lastSun  2:00  1:00  D" => Ok(Line::Rule(Rule {
1257        name:         "US",
1258        from_year:    Year::Number(1967),
1259        to_year:      Some(Year::Number(1973)),
1260        month:        Month::April,
1261        day:          DaySpec::Last(Weekday::Sunday),
1262        time:         TimeSpec::HoursMinutes(2, 0).with_type(TimeType::Wall),
1263        time_to_add:  TimeSpec::HoursMinutes(1, 0),
1264        letters:      Some("D"),
1265    })));
1266
1267    test!(rule_2: "Rule	Greece	1976	only	-	Oct	10	2:00s	0	-" => Ok(Line::Rule(Rule {
1268        name:         "Greece",
1269        from_year:    Year::Number(1976),
1270        to_year:      None,
1271        month:        Month::October,
1272        day:          DaySpec::Ordinal(10),
1273        time:         TimeSpec::HoursMinutes(2, 0).with_type(TimeType::Standard),
1274        time_to_add:  TimeSpec::Hours(0),
1275        letters:      None,
1276    })));
1277
1278    test!(rule_3: "Rule	EU	1977	1980	-	Apr	Sun>=1	 1:00u	1:00	S" => Ok(Line::Rule(Rule {
1279        name:         "EU",
1280        from_year:    Year::Number(1977),
1281        to_year:      Some(Year::Number(1980)),
1282        month:        Month::April,
1283        day:          DaySpec::FirstOnOrAfter(Weekday::Sunday, 1),
1284        time:         TimeSpec::HoursMinutes(1, 0).with_type(TimeType::UTC),
1285        time_to_add:  TimeSpec::HoursMinutes(1, 0),
1286        letters:      Some("S"),
1287    })));
1288
1289    test!(no_hyphen: "Rule	EU	1977	1980	HEY	Apr	Sun>=1	 1:00u	1:00	S"         => Err(Error::TypeColumnContainedNonHyphen("HEY".to_string())));
1290    test!(bad_month: "Rule	EU	1977	1980	-	Febtober	Sun>=1	 1:00u	1:00	S" => Err(Error::FailedMonthParse("febtober".to_string())));
1291
1292    test!(zone: "Zone  Australia/Adelaide  9:30    Aus         AC%sT   1971 Oct 31  2:00:00" => Ok(Line::Zone(Zone {
1293        name: "Australia/Adelaide",
1294        info: ZoneInfo {
1295            utc_offset:  TimeSpec::HoursMinutes(9, 30),
1296            saving:      Saving::Multiple("Aus"),
1297            format:      "AC%sT",
1298            time:        Some(ChangeTime::UntilTime(Year::Number(1971), Month::October, DaySpec::Ordinal(31), TimeSpec::HoursMinutesSeconds(2, 0, 0).with_type(TimeType::Wall))),
1299        },
1300    })));
1301
1302    test!(continuation_1: "                          9:30    Aus         AC%sT   1971 Oct 31  2:00:00" => Ok(Line::Continuation(ZoneInfo {
1303        utc_offset:  TimeSpec::HoursMinutes(9, 30),
1304        saving:      Saving::Multiple("Aus"),
1305        format:      "AC%sT",
1306        time:        Some(ChangeTime::UntilTime(Year::Number(1971), Month::October, DaySpec::Ordinal(31), TimeSpec::HoursMinutesSeconds(2, 0, 0).with_type(TimeType::Wall))),
1307    })));
1308
1309    test!(continuation_2: "			1:00	C-Eur	CE%sT	1943 Oct 25" => Ok(Line::Continuation(ZoneInfo {
1310        utc_offset:  TimeSpec::HoursMinutes(1, 00),
1311        saving:      Saving::Multiple("C-Eur"),
1312        format:      "CE%sT",
1313        time:        Some(ChangeTime::UntilDay(Year::Number(1943), Month::October, DaySpec::Ordinal(25))),
1314    })));
1315
1316    test!(zone_hyphen: "Zone Asia/Ust-Nera\t 9:32:54 -\tLMT\t1919" => Ok(Line::Zone(Zone {
1317        name: "Asia/Ust-Nera",
1318        info: ZoneInfo {
1319            utc_offset:  TimeSpec::HoursMinutesSeconds(9, 32, 54),
1320            saving:      Saving::NoSaving,
1321            format:      "LMT",
1322            time:        Some(ChangeTime::UntilYear(Year::Number(1919))),
1323        },
1324    })));
1325
1326    #[test]
1327    fn negative_offsets() {
1328        static LINE: &str = "Zone    Europe/London   -0:01:15 -  LMT 1847 Dec  1  0:00s";
1329        let parser = LineParser::default();
1330        let zone = parser.parse_zone(LINE).unwrap();
1331        assert_eq!(
1332            zone.info.utc_offset,
1333            TimeSpec::HoursMinutesSeconds(0, -1, -15)
1334        );
1335    }
1336
1337    #[test]
1338    fn negative_offsets_2() {
1339        static LINE: &str =
1340            "Zone        Europe/Madrid   -0:14:44 -      LMT     1901 Jan  1  0:00s";
1341        let parser = LineParser::default();
1342        let zone = parser.parse_zone(LINE).unwrap();
1343        assert_eq!(
1344            zone.info.utc_offset,
1345            TimeSpec::HoursMinutesSeconds(0, -14, -44)
1346        );
1347    }
1348
1349    #[test]
1350    fn negative_offsets_3() {
1351        static LINE: &str = "Zone America/Danmarkshavn -1:14:40 -    LMT 1916 Jul 28";
1352        let parser = LineParser::default();
1353        let zone = parser.parse_zone(LINE).unwrap();
1354        assert_eq!(
1355            zone.info.utc_offset,
1356            TimeSpec::HoursMinutesSeconds(-1, -14, -40)
1357        );
1358    }
1359
1360    test!(link: "Link  Europe/Istanbul  Asia/Istanbul" => Ok(Line::Link(Link {
1361        existing:  "Europe/Istanbul",
1362        new:       "Asia/Istanbul",
1363    })));
1364
1365    #[test]
1366    fn month() {
1367        assert_eq!(Month::from_str("Aug"), Ok(Month::August));
1368        assert_eq!(Month::from_str("December"), Ok(Month::December));
1369    }
1370
1371    test!(golb: "GOLB" => Err(Error::InvalidLineType("GOLB".to_string())));
1372
1373    test!(comment: "# this is a comment" => Ok(Line::Space));
1374    test!(another_comment: "     # so is this" => Ok(Line::Space));
1375    test!(multiple_hash: "     # so is this ## " => Ok(Line::Space));
1376    test!(non_comment: " this is not a # comment" => Err(Error::InvalidTimeSpecAndType("this".to_string())));
1377
1378    test!(comment_after: "Link  Europe/Istanbul  Asia/Istanbul #with a comment after" => Ok(Line::Link(Link {
1379        existing:  "Europe/Istanbul",
1380        new:       "Asia/Istanbul",
1381    })));
1382
1383    test!(two_comments_after: "Link  Europe/Istanbul  Asia/Istanbul   # comment ## comment" => Ok(Line::Link(Link {
1384        existing:  "Europe/Istanbul",
1385        new:       "Asia/Istanbul",
1386    })));
1387}