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