1 use std::str::FromStr;
2 // we still support rust that doesn't have the inherent methods
3 #[allow(deprecated, unused_imports)]
4 use std::ascii::AsciiExt;
5 
6 use regex::{Captures, Regex};
7 
8 pub struct LineParser {
9     rule_line: Regex,
10     day_field: Regex,
11     hm_field: Regex,
12     hms_field: Regex,
13     zone_line: Regex,
14     continuation_line: Regex,
15     link_line: Regex,
16     empty_line: Regex,
17 }
18 
19 #[derive(PartialEq, Debug, Clone)]
20 pub enum Error {
21     FailedYearParse(String),
22     FailedMonthParse(String),
23     FailedWeekdayParse(String),
24     InvalidLineType(String),
25     TypeColumnContainedNonHyphen(String),
26     CouldNotParseSaving(String),
27     InvalidDaySpec(String),
28     InvalidTimeSpecAndType(String),
29     NonWallClockInTimeSpec(String),
30     NotParsedAsRuleLine,
31     NotParsedAsZoneLine,
32     NotParsedAsLinkLine,
33 }
34 
35 impl LineParser {
new() -> Self36     pub fn new() -> Self {
37         LineParser {
38             rule_line: Regex::new(
39                 r##"(?x) ^
40                 Rule \s+
41                 ( ?P<name>    \S+)  \s+
42                 ( ?P<from>    \S+)  \s+
43                 ( ?P<to>      \S+)  \s+
44                 ( ?P<type>    \S+)  \s+
45                 ( ?P<in>      \S+)  \s+
46                 ( ?P<on>      \S+)  \s+
47                 ( ?P<at>      \S+)  \s+
48                 ( ?P<save>    \S+)  \s+
49                 ( ?P<letters> \S+)  \s*
50                 (\#.*)?
51             $ "##,
52             )
53             .unwrap(),
54 
55             day_field: Regex::new(
56                 r##"(?x) ^
57                 ( ?P<weekday> \w+ )
58                 ( ?P<sign>    [<>] = )
59                 ( ?P<day>     \d+ )
60             $ "##,
61             )
62             .unwrap(),
63 
64             hm_field: Regex::new(
65                 r##"(?x) ^
66                 ( ?P<sign> -? )
67                 ( ?P<hour> \d{1,2} ) : ( ?P<minute> \d{2} )
68                 ( ?P<flag> [wsugz] )?
69             $ "##,
70             )
71             .unwrap(),
72 
73             hms_field: Regex::new(
74                 r##"(?x) ^
75                 ( ?P<sign> -? )
76                 ( ?P<hour> \d{1,2} ) : ( ?P<minute> \d{2} ) : ( ?P<second> \d{2} )
77                 ( ?P<flag> [wsugz] )?
78             $ "##,
79             )
80             .unwrap(),
81 
82             zone_line: Regex::new(
83                 r##"(?x) ^
84                 Zone \s+
85                 ( ?P<name> [ A-Z a-z 0-9 / _ + - ]+ )  \s+
86                 ( ?P<gmtoff>     \S+ )  \s+
87                 ( ?P<rulessave>  \S+ )  \s+
88                 ( ?P<format>     \S+ )  \s*
89                 ( ?P<year>       \S+ )? \s*
90                 ( ?P<month>      \S+ )? \s*
91                 ( ?P<day>        \S+ )? \s*
92                 ( ?P<time>       \S+ )? \s*
93                 (\#.*)?
94             $ "##,
95             )
96             .unwrap(),
97 
98             continuation_line: Regex::new(
99                 r##"(?x) ^
100                 \s+
101                 ( ?P<gmtoff>     \S+ )  \s+
102                 ( ?P<rulessave>  \S+ )  \s+
103                 ( ?P<format>     \S+ )  \s*
104                 ( ?P<year>       \S+ )? \s*
105                 ( ?P<month>      \S+ )? \s*
106                 ( ?P<day>        \S+ )? \s*
107                 ( ?P<time>       \S+ )? \s*
108                 (\#.*)?
109             $ "##,
110             )
111             .unwrap(),
112 
113             link_line: Regex::new(
114                 r##"(?x) ^
115                 Link  \s+
116                 ( ?P<target>  \S+ )  \s+
117                 ( ?P<name>    \S+ )  \s*
118                 (\#.*)?
119             $ "##,
120             )
121             .unwrap(),
122 
123             empty_line: Regex::new(
124                 r##"(?x) ^
125                 \s*
126                 (\#.*)?
127             $"##,
128             )
129             .unwrap(),
130         }
131     }
132 }
133 
134 #[derive(PartialEq, Debug, Copy, Clone)]
135 pub enum Year {
136     Minimum,
137     Maximum,
138     Number(i64),
139 }
140 
141 impl FromStr for Year {
142     type Err = Error;
143 
from_str(input: &str) -> Result<Year, Self::Err>144     fn from_str(input: &str) -> Result<Year, Self::Err> {
145         Ok(match &*input.to_ascii_lowercase() {
146             "min" | "minimum" => Year::Minimum,
147             "max" | "maximum" => Year::Maximum,
148             year => match year.parse() {
149                 Ok(year) => Year::Number(year),
150                 Err(_) => return Err(Error::FailedYearParse(input.to_string())),
151             },
152         })
153     }
154 }
155 
156 #[derive(PartialEq, Debug, Copy, Clone)]
157 pub enum Month {
158     January = 1,
159     February = 2,
160     March = 3,
161     April = 4,
162     May = 5,
163     June = 6,
164     July = 7,
165     August = 8,
166     September = 9,
167     October = 10,
168     November = 11,
169     December = 12,
170 }
171 
172 impl Month {
length(self, is_leap: bool) -> i8173     fn length(self, is_leap: bool) -> i8 {
174         match self {
175             Month::January => 31,
176             Month::February if is_leap => 29,
177             Month::February => 28,
178             Month::March => 31,
179             Month::April => 30,
180             Month::May => 31,
181             Month::June => 30,
182             Month::July => 31,
183             Month::August => 31,
184             Month::September => 30,
185             Month::October => 31,
186             Month::November => 30,
187             Month::December => 31,
188         }
189     }
190 
191     /// Get the next calendar month, with an error going from Dec->Jan
next_in_year(self) -> Result<Month, &'static str>192     fn next_in_year(self) -> Result<Month, &'static str> {
193         Ok(match self {
194             Month::January => Month::February,
195             Month::February => Month::March,
196             Month::March => Month::April,
197             Month::April => Month::May,
198             Month::May => Month::June,
199             Month::June => Month::July,
200             Month::July => Month::August,
201             Month::August => Month::September,
202             Month::September => Month::October,
203             Month::October => Month::November,
204             Month::November => Month::December,
205             Month::December => Err("Cannot wrap year from dec->jan")?,
206         })
207     }
208 
209     /// Get the previous calendar month, with an error going from Jan->Dec
prev_in_year(self) -> Result<Month, &'static str>210     fn prev_in_year(self) -> Result<Month, &'static str> {
211         Ok(match self {
212             Month::January => Err("Cannot wrap years from jan->dec")?,
213             Month::February => Month::January,
214             Month::March => Month::February,
215             Month::April => Month::March,
216             Month::May => Month::April,
217             Month::June => Month::May,
218             Month::July => Month::June,
219             Month::August => Month::July,
220             Month::September => Month::August,
221             Month::October => Month::September,
222             Month::November => Month::October,
223             Month::December => Month::November,
224         })
225     }
226 }
227 
228 impl FromStr for Month {
229     type Err = Error;
230 
from_str(input: &str) -> Result<Month, Self::Err>231     fn from_str(input: &str) -> Result<Month, Self::Err> {
232         Ok(match &*input.to_ascii_lowercase() {
233             "jan" | "january" => Month::January,
234             "feb" | "february" => Month::February,
235             "mar" | "march" => Month::March,
236             "apr" | "april" => Month::April,
237             "may" => Month::May,
238             "jun" | "june" => Month::June,
239             "jul" | "july" => Month::July,
240             "aug" | "august" => Month::August,
241             "sep" | "september" => Month::September,
242             "oct" | "october" => Month::October,
243             "nov" | "november" => Month::November,
244             "dec" | "december" => Month::December,
245             other => return Err(Error::FailedMonthParse(other.to_string())),
246         })
247     }
248 }
249 
250 #[derive(PartialEq, Debug, Copy, Clone)]
251 pub enum Weekday {
252     Sunday,
253     Monday,
254     Tuesday,
255     Wednesday,
256     Thursday,
257     Friday,
258     Saturday,
259 }
260 
261 impl FromStr for Weekday {
262     type Err = Error;
263 
from_str(input: &str) -> Result<Weekday, Self::Err>264     fn from_str(input: &str) -> Result<Weekday, Self::Err> {
265         Ok(match &*input.to_ascii_lowercase() {
266             "mon" | "monday" => Weekday::Monday,
267             "tue" | "tuesday" => Weekday::Tuesday,
268             "wed" | "wednesday" => Weekday::Wednesday,
269             "thu" | "thursday" => Weekday::Thursday,
270             "fri" | "friday" => Weekday::Friday,
271             "sat" | "saturday" => Weekday::Saturday,
272             "sun" | "sunday" => Weekday::Sunday,
273             other => return Err(Error::FailedWeekdayParse(other.to_string())),
274         })
275     }
276 }
277 
278 #[derive(PartialEq, Debug, Copy, Clone)]
279 pub enum DaySpec {
280     Ordinal(i8),
281     Last(Weekday),
282     LastOnOrBefore(Weekday, i8),
283     FirstOnOrAfter(Weekday, i8),
284 }
285 
286 impl Weekday {
calculate(year: i64, month: Month, day: i8) -> Weekday287     fn calculate(year: i64, month: Month, day: i8) -> Weekday {
288         let m = month as i64;
289         let y = if m < 3 { year - 1 } else { year };
290         let d = day as i64;
291         const T: [i64; 12] = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
292         match (y + y / 4 - y / 100 + y / 400 + T[m as usize - 1] + d) % 7 {
293             0 => Weekday::Sunday,
294             1 => Weekday::Monday,
295             2 => Weekday::Tuesday,
296             3 => Weekday::Wednesday,
297             4 => Weekday::Thursday,
298             5 => Weekday::Friday,
299             6 => Weekday::Saturday,
300             _ => panic!("why is negative modulus designed so?"),
301         }
302     }
303 }
304 
305 #[cfg(test)]
306 #[test]
weekdays()307 fn weekdays() {
308     assert_eq!(
309         Weekday::calculate(1970, Month::January, 1),
310         Weekday::Thursday
311     );
312     assert_eq!(
313         Weekday::calculate(2017, Month::February, 11),
314         Weekday::Saturday
315     );
316     assert_eq!(Weekday::calculate(1890, Month::March, 2), Weekday::Sunday);
317     assert_eq!(Weekday::calculate(2100, Month::April, 20), Weekday::Tuesday);
318     assert_eq!(Weekday::calculate(2009, Month::May, 31), Weekday::Sunday);
319     assert_eq!(Weekday::calculate(2001, Month::June, 9), Weekday::Saturday);
320     assert_eq!(Weekday::calculate(1995, Month::July, 21), Weekday::Friday);
321     assert_eq!(Weekday::calculate(1982, Month::August, 8), Weekday::Sunday);
322     assert_eq!(
323         Weekday::calculate(1962, Month::September, 6),
324         Weekday::Thursday
325     );
326     assert_eq!(
327         Weekday::calculate(1899, Month::October, 14),
328         Weekday::Saturday
329     );
330     assert_eq!(
331         Weekday::calculate(2016, Month::November, 18),
332         Weekday::Friday
333     );
334     assert_eq!(
335         Weekday::calculate(2010, Month::December, 19),
336         Weekday::Sunday
337     );
338     assert_eq!(
339         Weekday::calculate(2016, Month::February, 29),
340         Weekday::Monday
341     );
342 }
343 
is_leap(year: i64) -> bool344 fn is_leap(year: i64) -> bool {
345     // Leap year rules: years which are factors of 4, except those divisible
346     // by 100, unless they are divisible by 400.
347     //
348     // We test most common cases first: 4th year, 100th year, then 400th year.
349     //
350     // We factor out 4 from 100 since it was already tested, leaving us checking
351     // if it's divisible by 25. Afterwards, we do the same, factoring 25 from
352     // 400, leaving us with 16.
353     //
354     // Factors of 4 and 16 can quickly be found with bitwise AND.
355     year & 3 == 0 && (year % 25 != 0 || year & 15 == 0)
356 }
357 
358 #[cfg(test)]
359 #[test]
leap_years()360 fn leap_years() {
361     assert!(!is_leap(1900));
362     assert!(is_leap(1904));
363     assert!(is_leap(1964));
364     assert!(is_leap(1996));
365     assert!(!is_leap(1997));
366     assert!(!is_leap(1997));
367     assert!(!is_leap(1999));
368     assert!(is_leap(2000));
369     assert!(is_leap(2016));
370     assert!(!is_leap(2100));
371 }
372 
373 impl DaySpec {
to_concrete_day(&self, year: i64, month: Month) -> (Month, i8)374     pub fn to_concrete_day(&self, year: i64, month: Month) -> (Month, i8) {
375         let leap = is_leap(year);
376         let length = month.length(leap);
377         // we will never hit the 0 because we unwrap prev_in_year below
378         let prev_length = month.prev_in_year().map(|m| m.length(leap)).unwrap_or(0);
379 
380         match *self {
381             DaySpec::Ordinal(day) => (month, day),
382             DaySpec::Last(weekday) => (
383                 month,
384                 (1..length + 1)
385                     .rev()
386                     .find(|&day| Weekday::calculate(year, month, day) == weekday)
387                     .unwrap(),
388             ),
389             DaySpec::LastOnOrBefore(weekday, day) => (-7..day + 1)
390                 .rev()
391                 .flat_map(|inner_day| {
392                     if inner_day >= 1 && Weekday::calculate(year, month, inner_day) == weekday {
393                         Some((month, inner_day))
394                     } else if inner_day < 1
395                         && Weekday::calculate(
396                             year,
397                             month.prev_in_year().unwrap(),
398                             prev_length + inner_day,
399                         ) == weekday
400                     {
401                         // inner_day is negative, so this is subtraction
402                         Some((month.prev_in_year().unwrap(), prev_length + inner_day))
403                     } else {
404                         None
405                     }
406                 })
407                 .next()
408                 .unwrap(),
409             DaySpec::FirstOnOrAfter(weekday, day) => (day..day + 8)
410                 .flat_map(|inner_day| {
411                     if inner_day <= length && Weekday::calculate(year, month, inner_day) == weekday
412                     {
413                         Some((month, inner_day))
414                     } else if inner_day > length
415                         && Weekday::calculate(
416                             year,
417                             month.next_in_year().unwrap(),
418                             inner_day - length,
419                         ) == weekday
420                     {
421                         Some((month.next_in_year().unwrap(), inner_day - length))
422                     } else {
423                         None
424                     }
425                 })
426                 .next()
427                 .unwrap(),
428         }
429     }
430 }
431 
432 #[cfg(test)]
433 #[test]
last_monday()434 fn last_monday() {
435     let dayspec = DaySpec::Last(Weekday::Monday);
436     assert_eq!(
437         dayspec.to_concrete_day(2016, Month::January),
438         (Month::January, 25)
439     );
440     assert_eq!(
441         dayspec.to_concrete_day(2016, Month::February),
442         (Month::February, 29)
443     );
444     assert_eq!(
445         dayspec.to_concrete_day(2016, Month::March),
446         (Month::March, 28)
447     );
448     assert_eq!(
449         dayspec.to_concrete_day(2016, Month::April),
450         (Month::April, 25)
451     );
452     assert_eq!(dayspec.to_concrete_day(2016, Month::May), (Month::May, 30));
453     assert_eq!(
454         dayspec.to_concrete_day(2016, Month::June),
455         (Month::June, 27)
456     );
457     assert_eq!(
458         dayspec.to_concrete_day(2016, Month::July),
459         (Month::July, 25)
460     );
461     assert_eq!(
462         dayspec.to_concrete_day(2016, Month::August),
463         (Month::August, 29)
464     );
465     assert_eq!(
466         dayspec.to_concrete_day(2016, Month::September),
467         (Month::September, 26)
468     );
469     assert_eq!(
470         dayspec.to_concrete_day(2016, Month::October),
471         (Month::October, 31)
472     );
473     assert_eq!(
474         dayspec.to_concrete_day(2016, Month::November),
475         (Month::November, 28)
476     );
477     assert_eq!(
478         dayspec.to_concrete_day(2016, Month::December),
479         (Month::December, 26)
480     );
481 }
482 
483 #[cfg(test)]
484 #[test]
first_monday_on_or_after()485 fn first_monday_on_or_after() {
486     let dayspec = DaySpec::FirstOnOrAfter(Weekday::Monday, 20);
487     assert_eq!(
488         dayspec.to_concrete_day(2016, Month::January),
489         (Month::January, 25)
490     );
491     assert_eq!(
492         dayspec.to_concrete_day(2016, Month::February),
493         (Month::February, 22)
494     );
495     assert_eq!(
496         dayspec.to_concrete_day(2016, Month::March),
497         (Month::March, 21)
498     );
499     assert_eq!(
500         dayspec.to_concrete_day(2016, Month::April),
501         (Month::April, 25)
502     );
503     assert_eq!(dayspec.to_concrete_day(2016, Month::May), (Month::May, 23));
504     assert_eq!(
505         dayspec.to_concrete_day(2016, Month::June),
506         (Month::June, 20)
507     );
508     assert_eq!(
509         dayspec.to_concrete_day(2016, Month::July),
510         (Month::July, 25)
511     );
512     assert_eq!(
513         dayspec.to_concrete_day(2016, Month::August),
514         (Month::August, 22)
515     );
516     assert_eq!(
517         dayspec.to_concrete_day(2016, Month::September),
518         (Month::September, 26)
519     );
520     assert_eq!(
521         dayspec.to_concrete_day(2016, Month::October),
522         (Month::October, 24)
523     );
524     assert_eq!(
525         dayspec.to_concrete_day(2016, Month::November),
526         (Month::November, 21)
527     );
528     assert_eq!(
529         dayspec.to_concrete_day(2016, Month::December),
530         (Month::December, 26)
531     );
532 }
533 
534 // A couple of specific timezone transitions that we care about
535 #[cfg(test)]
536 #[test]
first_sunday_in_toronto()537 fn first_sunday_in_toronto() {
538     let dayspec = DaySpec::FirstOnOrAfter(Weekday::Sunday, 25);
539     assert_eq!(dayspec.to_concrete_day(1932, Month::April), (Month::May, 1));
540     // asia/zion
541     let dayspec = DaySpec::LastOnOrBefore(Weekday::Friday, 1);
542     assert_eq!(
543         dayspec.to_concrete_day(2012, Month::April),
544         (Month::March, 30)
545     );
546 }
547 
548 #[derive(PartialEq, Debug, Copy, Clone)]
549 pub enum TimeSpec {
550     Hours(i8),
551     HoursMinutes(i8, i8),
552     HoursMinutesSeconds(i8, i8, i8),
553     Zero,
554 }
555 
556 impl TimeSpec {
as_seconds(self) -> i64557     pub fn as_seconds(self) -> i64 {
558         match self {
559             TimeSpec::Hours(h) => h as i64 * 60 * 60,
560             TimeSpec::HoursMinutes(h, m) => h as i64 * 60 * 60 + m as i64 * 60,
561             TimeSpec::HoursMinutesSeconds(h, m, s) => h as i64 * 60 * 60 + m as i64 * 60 + s as i64,
562             TimeSpec::Zero => 0,
563         }
564     }
565 }
566 
567 #[derive(PartialEq, Debug, Copy, Clone)]
568 pub enum TimeType {
569     Wall,
570     Standard,
571     UTC,
572 }
573 
574 #[derive(PartialEq, Debug, Copy, Clone)]
575 pub struct TimeSpecAndType(pub TimeSpec, pub TimeType);
576 
577 impl TimeSpec {
with_type(self, timetype: TimeType) -> TimeSpecAndType578     pub fn with_type(self, timetype: TimeType) -> TimeSpecAndType {
579         TimeSpecAndType(self, timetype)
580     }
581 }
582 
583 #[derive(PartialEq, Debug, Copy, Clone)]
584 pub enum ChangeTime {
585     UntilYear(Year),
586     UntilMonth(Year, Month),
587     UntilDay(Year, Month, DaySpec),
588     UntilTime(Year, Month, DaySpec, TimeSpecAndType),
589 }
590 
591 impl ChangeTime {
to_timestamp(&self) -> i64592     pub fn to_timestamp(&self) -> i64 {
593         fn seconds_in_year(year: i64) -> i64 {
594             if is_leap(year) {
595                 366 * 24 * 60 * 60
596             } else {
597                 365 * 24 * 60 * 60
598             }
599         }
600 
601         fn seconds_until_start_of_year(year: i64) -> i64 {
602             if year >= 1970 {
603                 (1970..year).map(seconds_in_year).sum()
604             } else {
605                 -(year..1970).map(seconds_in_year).sum::<i64>()
606             }
607         }
608 
609         fn time_to_timestamp(
610             year: i64,
611             month: i8,
612             day: i8,
613             hour: i8,
614             minute: i8,
615             second: i8,
616         ) -> i64 {
617             const MONTHS_NON_LEAP: [i64; 12] = [
618                 0,
619                 31,
620                 31 + 28,
621                 31 + 28 + 31,
622                 31 + 28 + 31 + 30,
623                 31 + 28 + 31 + 30 + 31,
624                 31 + 28 + 31 + 30 + 31 + 30,
625                 31 + 28 + 31 + 30 + 31 + 30 + 31,
626                 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31,
627                 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30,
628                 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31,
629                 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30,
630             ];
631             const MONTHS_LEAP: [i64; 12] = [
632                 0,
633                 31,
634                 31 + 29,
635                 31 + 29 + 31,
636                 31 + 29 + 31 + 30,
637                 31 + 29 + 31 + 30 + 31,
638                 31 + 29 + 31 + 30 + 31 + 30,
639                 31 + 29 + 31 + 30 + 31 + 30 + 31,
640                 31 + 29 + 31 + 30 + 31 + 30 + 31 + 31,
641                 31 + 29 + 31 + 30 + 31 + 30 + 31 + 31 + 30,
642                 31 + 29 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31,
643                 31 + 29 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30,
644             ];
645             seconds_until_start_of_year(year)
646                 + 60 * 60
647                     * 24
648                     * if is_leap(year) {
649                         MONTHS_LEAP[month as usize - 1]
650                     } else {
651                         MONTHS_NON_LEAP[month as usize - 1]
652                     }
653                 + 60 * 60 * 24 * (day as i64 - 1)
654                 + 60 * 60 * hour as i64
655                 + 60 * minute as i64
656                 + second as i64
657         }
658 
659         match *self {
660             ChangeTime::UntilYear(Year::Number(y)) => time_to_timestamp(y, 1, 1, 0, 0, 0),
661             ChangeTime::UntilMonth(Year::Number(y), m) => time_to_timestamp(y, m as i8, 1, 0, 0, 0),
662             ChangeTime::UntilDay(Year::Number(y), m, d) => {
663                 let (m, wd) = d.to_concrete_day(y, m);
664                 time_to_timestamp(y, m as i8, wd, 0, 0, 0)
665             }
666             ChangeTime::UntilTime(Year::Number(y), m, d, time) => match time.0 {
667                 TimeSpec::Zero => {
668                     let (m, wd) = d.to_concrete_day(y, m);
669                     time_to_timestamp(y, m as i8, wd, 0, 0, 0)
670                 }
671                 TimeSpec::Hours(h) => {
672                     let (m, wd) = d.to_concrete_day(y, m);
673                     time_to_timestamp(y, m as i8, wd, h, 0, 0)
674                 }
675                 TimeSpec::HoursMinutes(h, min) => {
676                     let (m, wd) = d.to_concrete_day(y, m);
677                     time_to_timestamp(y, m as i8, wd, h, min, 0)
678                 }
679                 TimeSpec::HoursMinutesSeconds(h, min, s) => {
680                     let (m, wd) = d.to_concrete_day(y, m);
681                     time_to_timestamp(y, m as i8, wd, h, min, s)
682                 }
683             },
684             _ => unreachable!(),
685         }
686     }
687 
year(&self) -> i64688     pub fn year(&self) -> i64 {
689         match *self {
690             ChangeTime::UntilYear(Year::Number(y)) => y,
691             ChangeTime::UntilMonth(Year::Number(y), ..) => y,
692             ChangeTime::UntilDay(Year::Number(y), ..) => y,
693             ChangeTime::UntilTime(Year::Number(y), ..) => y,
694             _ => unreachable!(),
695         }
696     }
697 }
698 
699 #[cfg(test)]
700 #[test]
to_timestamp()701 fn to_timestamp() {
702     let time = ChangeTime::UntilYear(Year::Number(1970));
703     assert_eq!(time.to_timestamp(), 0);
704     let time = ChangeTime::UntilYear(Year::Number(2016));
705     assert_eq!(time.to_timestamp(), 1451606400);
706     let time = ChangeTime::UntilYear(Year::Number(1900));
707     assert_eq!(time.to_timestamp(), -2208988800);
708     let time = ChangeTime::UntilTime(
709         Year::Number(2000),
710         Month::February,
711         DaySpec::Last(Weekday::Sunday),
712         TimeSpecAndType(TimeSpec::Hours(9), TimeType::Wall),
713     );
714     assert_eq!(time.to_timestamp(), 951642000);
715 }
716 
717 #[derive(PartialEq, Debug, Copy, Clone)]
718 pub struct ZoneInfo<'a> {
719     pub utc_offset: TimeSpec,
720     pub saving: Saving<'a>,
721     pub format: &'a str,
722     pub time: Option<ChangeTime>,
723 }
724 
725 #[derive(PartialEq, Debug, Copy, Clone)]
726 pub enum Saving<'a> {
727     NoSaving,
728     OneOff(TimeSpec),
729     Multiple(&'a str),
730 }
731 
732 #[derive(PartialEq, Debug, Copy, Clone)]
733 pub struct Rule<'a> {
734     pub name: &'a str,
735     pub from_year: Year,
736     pub to_year: Option<Year>,
737     pub month: Month,
738     pub day: DaySpec,
739     pub time: TimeSpecAndType,
740     pub time_to_add: TimeSpec,
741     pub letters: Option<&'a str>,
742 }
743 
744 #[derive(PartialEq, Debug, Copy, Clone)]
745 pub struct Zone<'a> {
746     pub name: &'a str,
747     pub info: ZoneInfo<'a>,
748 }
749 
750 #[derive(PartialEq, Debug, Copy, Clone)]
751 pub struct Link<'a> {
752     pub existing: &'a str,
753     pub new: &'a str,
754 }
755 
756 #[derive(PartialEq, Debug, Copy, Clone)]
757 pub enum Line<'a> {
758     Space,
759     Zone(Zone<'a>),
760     Continuation(ZoneInfo<'a>),
761     Rule(Rule<'a>),
762     Link(Link<'a>),
763 }
764 
parse_time_type(c: &str) -> Option<TimeType>765 fn parse_time_type(c: &str) -> Option<TimeType> {
766     Some(match c {
767         "w" => TimeType::Wall,
768         "s" => TimeType::Standard,
769         "u" | "g" | "z" => TimeType::UTC,
770         _ => return None,
771     })
772 }
773 
774 impl LineParser {
parse_timespec_and_type(&self, input: &str) -> Result<TimeSpecAndType, Error>775     fn parse_timespec_and_type(&self, input: &str) -> Result<TimeSpecAndType, Error> {
776         if input == "-" {
777             Ok(TimeSpecAndType(TimeSpec::Zero, TimeType::Wall))
778         } else if input.chars().all(|c| c == '-' || c.is_digit(10)) {
779             Ok(TimeSpecAndType(
780                 TimeSpec::Hours(input.parse().unwrap()),
781                 TimeType::Wall,
782             ))
783         } else if let Some(caps) = self.hm_field.captures(input) {
784             let sign: i8 = if caps.name("sign").unwrap().as_str() == "-" {
785                 -1
786             } else {
787                 1
788             };
789             let hour: i8 = caps.name("hour").unwrap().as_str().parse().unwrap();
790             let minute: i8 = caps.name("minute").unwrap().as_str().parse().unwrap();
791             let flag = caps
792                 .name("flag")
793                 .and_then(|c| parse_time_type(&c.as_str()[0..1]))
794                 .unwrap_or(TimeType::Wall);
795 
796             Ok(TimeSpecAndType(
797                 TimeSpec::HoursMinutes(hour * sign, minute * sign),
798                 flag,
799             ))
800         } else if let Some(caps) = self.hms_field.captures(input) {
801             let sign: i8 = if caps.name("sign").unwrap().as_str() == "-" {
802                 -1
803             } else {
804                 1
805             };
806             let hour: i8 = caps.name("hour").unwrap().as_str().parse().unwrap();
807             let minute: i8 = caps.name("minute").unwrap().as_str().parse().unwrap();
808             let second: i8 = caps.name("second").unwrap().as_str().parse().unwrap();
809             let flag = caps
810                 .name("flag")
811                 .and_then(|c| parse_time_type(&c.as_str()[0..1]))
812                 .unwrap_or(TimeType::Wall);
813 
814             Ok(TimeSpecAndType(
815                 TimeSpec::HoursMinutesSeconds(hour * sign, minute * sign, second * sign),
816                 flag,
817             ))
818         } else {
819             Err(Error::InvalidTimeSpecAndType(input.to_string()))
820         }
821     }
822 
parse_timespec(&self, input: &str) -> Result<TimeSpec, Error>823     fn parse_timespec(&self, input: &str) -> Result<TimeSpec, Error> {
824         match self.parse_timespec_and_type(input) {
825             Ok(TimeSpecAndType(spec, TimeType::Wall)) => Ok(spec),
826             Ok(TimeSpecAndType(_, _)) => Err(Error::NonWallClockInTimeSpec(input.to_string())),
827             Err(e) => Err(e),
828         }
829     }
830 
parse_dayspec(&self, input: &str) -> Result<DaySpec, Error>831     fn parse_dayspec(&self, input: &str) -> Result<DaySpec, Error> {
832         if input.chars().all(|c| c.is_digit(10)) {
833             Ok(DaySpec::Ordinal(input.parse().unwrap()))
834         } else if input.starts_with("last") {
835             let weekday = input[4..].parse()?;
836             Ok(DaySpec::Last(weekday))
837         } else if let Some(caps) = self.day_field.captures(input) {
838             let weekday = caps.name("weekday").unwrap().as_str().parse().unwrap();
839             let day = caps.name("day").unwrap().as_str().parse().unwrap();
840 
841             match caps.name("sign").unwrap().as_str() {
842                 "<=" => Ok(DaySpec::LastOnOrBefore(weekday, day)),
843                 ">=" => Ok(DaySpec::FirstOnOrAfter(weekday, day)),
844                 _ => unreachable!("The regex only matches one of those two!"),
845             }
846         } else {
847             Err(Error::InvalidDaySpec(input.to_string()))
848         }
849     }
850 
parse_rule<'a>(&self, input: &'a str) -> Result<Rule<'a>, Error>851     fn parse_rule<'a>(&self, input: &'a str) -> Result<Rule<'a>, Error> {
852         if let Some(caps) = self.rule_line.captures(input) {
853             let name = caps.name("name").unwrap().as_str();
854             let from_year = caps.name("from").unwrap().as_str().parse()?;
855 
856             // The end year can be ‘only’ to indicate that this rule only
857             // takes place on that year.
858             let to_year = match caps.name("to").unwrap().as_str() {
859                 "only" => None,
860                 to => Some(to.parse()?),
861             };
862 
863             // According to the spec, the only value inside the ‘type’ column
864             // should be “-”, so throw an error if it isn’t. (It only exists
865             // for compatibility with old versions that used to contain year
866             // types.) Sometimes “‐”, a Unicode hyphen, is used as well.
867             let t = caps.name("type").unwrap().as_str();
868             if t != "-" && t != "\u{2010}" {
869                 return Err(Error::TypeColumnContainedNonHyphen(t.to_string()));
870             }
871 
872             let month = caps.name("in").unwrap().as_str().parse()?;
873             let day = self.parse_dayspec(caps.name("on").unwrap().as_str())?;
874             let time = self.parse_timespec_and_type(caps.name("at").unwrap().as_str())?;
875             let time_to_add = self.parse_timespec(caps.name("save").unwrap().as_str())?;
876             let letters = match caps.name("letters").unwrap().as_str() {
877                 "-" => None,
878                 l => Some(l),
879             };
880 
881             Ok(Rule {
882                 name: name,
883                 from_year: from_year,
884                 to_year: to_year,
885                 month: month,
886                 day: day,
887                 time: time,
888                 time_to_add: time_to_add,
889                 letters: letters,
890             })
891         } else {
892             Err(Error::NotParsedAsRuleLine)
893         }
894     }
895 
saving_from_str<'a>(&self, input: &'a str) -> Result<Saving<'a>, Error>896     fn saving_from_str<'a>(&self, input: &'a str) -> Result<Saving<'a>, Error> {
897         if input == "-" {
898             Ok(Saving::NoSaving)
899         } else if input
900             .chars()
901             .all(|c| c == '-' || c == '_' || c.is_alphabetic())
902         {
903             Ok(Saving::Multiple(input))
904         } else if self.hm_field.is_match(input) {
905             let time = self.parse_timespec(input)?;
906             Ok(Saving::OneOff(time))
907         } else {
908             Err(Error::CouldNotParseSaving(input.to_string()))
909         }
910     }
911 
zoneinfo_from_captures<'a>(&self, caps: Captures<'a>) -> Result<ZoneInfo<'a>, Error>912     fn zoneinfo_from_captures<'a>(&self, caps: Captures<'a>) -> Result<ZoneInfo<'a>, Error> {
913         let utc_offset = self.parse_timespec(caps.name("gmtoff").unwrap().as_str())?;
914         let saving = self.saving_from_str(caps.name("rulessave").unwrap().as_str())?;
915         let format = caps.name("format").unwrap().as_str();
916 
917         let time = match (
918             caps.name("year"),
919             caps.name("month"),
920             caps.name("day"),
921             caps.name("time"),
922         ) {
923             (Some(y), Some(m), Some(d), Some(t)) => Some(ChangeTime::UntilTime(
924                 y.as_str().parse()?,
925                 m.as_str().parse()?,
926                 self.parse_dayspec(d.as_str())?,
927                 self.parse_timespec_and_type(t.as_str())?,
928             )),
929             (Some(y), Some(m), Some(d), _) => Some(ChangeTime::UntilDay(
930                 y.as_str().parse()?,
931                 m.as_str().parse()?,
932                 self.parse_dayspec(d.as_str())?,
933             )),
934             (Some(y), Some(m), _, _) => Some(ChangeTime::UntilMonth(
935                 y.as_str().parse()?,
936                 m.as_str().parse()?,
937             )),
938             (Some(y), _, _, _) => Some(ChangeTime::UntilYear(y.as_str().parse()?)),
939             (None, None, None, None) => None,
940             _ => unreachable!("Out-of-order capturing groups!"),
941         };
942 
943         Ok(ZoneInfo {
944             utc_offset: utc_offset,
945             saving: saving,
946             format: format,
947             time: time,
948         })
949     }
950 
parse_zone<'a>(&self, input: &'a str) -> Result<Zone<'a>, Error>951     fn parse_zone<'a>(&self, input: &'a str) -> Result<Zone<'a>, Error> {
952         if let Some(caps) = self.zone_line.captures(input) {
953             let name = caps.name("name").unwrap().as_str();
954             let info = self.zoneinfo_from_captures(caps)?;
955             Ok(Zone {
956                 name: name,
957                 info: info,
958             })
959         } else {
960             Err(Error::NotParsedAsZoneLine)
961         }
962     }
963 
parse_link<'a>(&self, input: &'a str) -> Result<Link<'a>, Error>964     fn parse_link<'a>(&self, input: &'a str) -> Result<Link<'a>, Error> {
965         if let Some(caps) = self.link_line.captures(input) {
966             let target = caps.name("target").unwrap().as_str();
967             let name = caps.name("name").unwrap().as_str();
968             Ok(Link {
969                 existing: target,
970                 new: name,
971             })
972         } else {
973             Err(Error::NotParsedAsLinkLine)
974         }
975     }
976 
parse_str<'a>(&self, input: &'a str) -> Result<Line<'a>, Error>977     pub fn parse_str<'a>(&self, input: &'a str) -> Result<Line<'a>, Error> {
978         if self.empty_line.is_match(input) {
979             return Ok(Line::Space);
980         }
981 
982         match self.parse_zone(input) {
983             Err(Error::NotParsedAsZoneLine) => {}
984             result => return result.map(Line::Zone),
985         }
986 
987         match self.continuation_line.captures(input) {
988             None => {}
989             Some(caps) => return self.zoneinfo_from_captures(caps).map(Line::Continuation),
990         }
991 
992         match self.parse_rule(input) {
993             Err(Error::NotParsedAsRuleLine) => {}
994             result => return result.map(Line::Rule),
995         }
996 
997         match self.parse_link(input) {
998             Err(Error::NotParsedAsLinkLine) => {}
999             result => return result.map(Line::Link),
1000         }
1001 
1002         Err(Error::InvalidLineType(input.to_string()))
1003     }
1004 }
1005 
1006 #[cfg(test)]
1007 mod tests {
1008     use super::*;
1009 
1010     macro_rules! test {
1011         ($name:ident: $input:expr => $result:expr) => {
1012             #[test]
1013             fn $name() {
1014                 let parser = LineParser::new();
1015                 assert_eq!(parser.parse_str($input), $result);
1016             }
1017         };
1018     }
1019 
1020     test!(empty:    ""          => Ok(Line::Space));
1021     test!(spaces:   "        "  => Ok(Line::Space));
1022 
1023     test!(rule_1: "Rule  US    1967  1973  ‐     Apr  lastSun  2:00  1:00  D" => Ok(Line::Rule(Rule {
1024         name:         "US",
1025         from_year:    Year::Number(1967),
1026         to_year:      Some(Year::Number(1973)),
1027         month:        Month::April,
1028         day:          DaySpec::Last(Weekday::Sunday),
1029         time:         TimeSpec::HoursMinutes(2, 0).with_type(TimeType::Wall),
1030         time_to_add:  TimeSpec::HoursMinutes(1, 0),
1031         letters:      Some("D"),
1032     })));
1033 
1034     test!(rule_2: "Rule	Greece	1976	only	-	Oct	10	2:00s	0	-" => Ok(Line::Rule(Rule {
1035         name:         "Greece",
1036         from_year:    Year::Number(1976),
1037         to_year:      None,
1038         month:        Month::October,
1039         day:          DaySpec::Ordinal(10),
1040         time:         TimeSpec::HoursMinutes(2, 0).with_type(TimeType::Standard),
1041         time_to_add:  TimeSpec::Hours(0),
1042         letters:      None,
1043     })));
1044 
1045     test!(rule_3: "Rule	EU	1977	1980	-	Apr	Sun>=1	 1:00u	1:00	S" => Ok(Line::Rule(Rule {
1046         name:         "EU",
1047         from_year:    Year::Number(1977),
1048         to_year:      Some(Year::Number(1980)),
1049         month:        Month::April,
1050         day:          DaySpec::FirstOnOrAfter(Weekday::Sunday, 1),
1051         time:         TimeSpec::HoursMinutes(1, 0).with_type(TimeType::UTC),
1052         time_to_add:  TimeSpec::HoursMinutes(1, 0),
1053         letters:      Some("S"),
1054     })));
1055 
1056     test!(no_hyphen: "Rule	EU	1977	1980	HEY	Apr	Sun>=1	 1:00u	1:00	S"         => Err(Error::TypeColumnContainedNonHyphen("HEY".to_string())));
1057     test!(bad_month: "Rule	EU	1977	1980	-	Febtober	Sun>=1	 1:00u	1:00	S" => Err(Error::FailedMonthParse("febtober".to_string())));
1058 
1059     test!(zone: "Zone  Australia/Adelaide  9:30    Aus         AC%sT   1971 Oct 31  2:00:00" => Ok(Line::Zone(Zone {
1060         name: "Australia/Adelaide",
1061         info: ZoneInfo {
1062             utc_offset:  TimeSpec::HoursMinutes(9, 30),
1063             saving:      Saving::Multiple("Aus"),
1064             format:      "AC%sT",
1065             time:        Some(ChangeTime::UntilTime(Year::Number(1971), Month::October, DaySpec::Ordinal(31), TimeSpec::HoursMinutesSeconds(2, 0, 0).with_type(TimeType::Wall))),
1066         },
1067     })));
1068 
1069     test!(continuation_1: "                          9:30    Aus         AC%sT   1971 Oct 31  2:00:00" => Ok(Line::Continuation(ZoneInfo {
1070         utc_offset:  TimeSpec::HoursMinutes(9, 30),
1071         saving:      Saving::Multiple("Aus"),
1072         format:      "AC%sT",
1073         time:        Some(ChangeTime::UntilTime(Year::Number(1971), Month::October, DaySpec::Ordinal(31), TimeSpec::HoursMinutesSeconds(2, 0, 0).with_type(TimeType::Wall))),
1074     })));
1075 
1076     test!(continuation_2: "			1:00	C-Eur	CE%sT	1943 Oct 25" => Ok(Line::Continuation(ZoneInfo {
1077         utc_offset:  TimeSpec::HoursMinutes(1, 00),
1078         saving:      Saving::Multiple("C-Eur"),
1079         format:      "CE%sT",
1080         time:        Some(ChangeTime::UntilDay(Year::Number(1943), Month::October, DaySpec::Ordinal(25))),
1081     })));
1082 
1083     test!(zone_hyphen: "Zone Asia/Ust-Nera\t 9:32:54 -\tLMT\t1919" => Ok(Line::Zone(Zone {
1084         name: "Asia/Ust-Nera",
1085         info: ZoneInfo {
1086             utc_offset:  TimeSpec::HoursMinutesSeconds(9, 32, 54),
1087             saving:      Saving::NoSaving,
1088             format:      "LMT",
1089             time:        Some(ChangeTime::UntilYear(Year::Number(1919))),
1090         },
1091     })));
1092 
1093     #[test]
negative_offsets()1094     fn negative_offsets() {
1095         static LINE: &'static str = "Zone    Europe/London   -0:01:15 -  LMT 1847 Dec  1  0:00s";
1096         let parser = LineParser::new();
1097         let zone = parser.parse_zone(LINE).unwrap();
1098         assert_eq!(
1099             zone.info.utc_offset,
1100             TimeSpec::HoursMinutesSeconds(0, -1, -15)
1101         );
1102     }
1103 
1104     #[test]
negative_offsets_2()1105     fn negative_offsets_2() {
1106         static LINE: &'static str =
1107             "Zone        Europe/Madrid   -0:14:44 -      LMT     1901 Jan  1  0:00s";
1108         let parser = LineParser::new();
1109         let zone = parser.parse_zone(LINE).unwrap();
1110         assert_eq!(
1111             zone.info.utc_offset,
1112             TimeSpec::HoursMinutesSeconds(0, -14, -44)
1113         );
1114     }
1115 
1116     #[test]
negative_offsets_3()1117     fn negative_offsets_3() {
1118         static LINE: &'static str = "Zone America/Danmarkshavn -1:14:40 -    LMT 1916 Jul 28";
1119         let parser = LineParser::new();
1120         let zone = parser.parse_zone(LINE).unwrap();
1121         assert_eq!(
1122             zone.info.utc_offset,
1123             TimeSpec::HoursMinutesSeconds(-1, -14, -40)
1124         );
1125     }
1126 
1127     test!(link: "Link  Europe/Istanbul  Asia/Istanbul" => Ok(Line::Link(Link {
1128         existing:  "Europe/Istanbul",
1129         new:       "Asia/Istanbul",
1130     })));
1131 
1132     #[test]
month()1133     fn month() {
1134         assert_eq!(Month::from_str("Aug"), Ok(Month::August));
1135         assert_eq!(Month::from_str("December"), Ok(Month::December));
1136     }
1137 
1138     test!(golb: "GOLB" => Err(Error::InvalidLineType("GOLB".to_string())));
1139 
1140     test!(comment: "# this is a comment" => Ok(Line::Space));
1141     test!(another_comment: "     # so is this" => Ok(Line::Space));
1142     test!(multiple_hash: "     # so is this ## " => Ok(Line::Space));
1143     test!(non_comment: " this is not a # comment" => Err(Error::InvalidTimeSpecAndType("this".to_string())));
1144 
1145     test!(comment_after: "Link  Europe/Istanbul  Asia/Istanbul #with a comment after" => Ok(Line::Link(Link {
1146         existing:  "Europe/Istanbul",
1147         new:       "Asia/Istanbul",
1148     })));
1149 
1150     test!(two_comments_after: "Link  Europe/Istanbul  Asia/Istanbul   # comment ## comment" => Ok(Line::Link(Link {
1151         existing:  "Europe/Istanbul",
1152         new:       "Asia/Istanbul",
1153     })));
1154 }
1155