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