1use 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#[derive(PartialEq, Debug, Copy, Clone)]
128pub enum Year {
129 Minimum,
131 Maximum,
133 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#[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 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 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 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#[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#[derive(PartialEq, Debug, Copy, Clone)]
307pub enum DaySpec {
308 Ordinal(i8),
310 Last(Weekday),
312 LastOnOrBefore(Weekday, i8),
315 FirstOnOrAfter(Weekday, i8),
318}
319
320impl DaySpec {
321 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 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 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 if input.chars().all(|c| c.is_ascii_digit()) {
387 return Ok(DaySpec::Ordinal(input.parse().unwrap()));
388 }
389 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 year & 3 == 0 && (year % 25 != 0 || year & 15 == 0)
430}
431
432#[derive(PartialEq, Debug, Copy, Clone)]
440pub enum TimeSpec {
441 Hours(i8),
443 HoursMinutes(i8, i8),
445 HoursMinutesSeconds(i8, i8, i8),
447 Zero,
449}
450
451impl TimeSpec {
452 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#[derive(PartialEq, Debug, Copy, Clone)]
555pub enum ChangeTime {
556 UntilYear(Year),
558 UntilMonth(Year, Month),
560 UntilDay(Year, Month, DaySpec),
562 UntilTime(Year, Month, DaySpec, TimeSpecAndType),
564}
565
566impl ChangeTime {
567 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#[derive(PartialEq, Debug, Copy, Clone)]
689pub struct ZoneInfo<'a> {
690 pub utc_offset: TimeSpec,
693 pub saving: Saving<'a>,
696 pub format: &'a str,
698 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 (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#[derive(PartialEq, Debug, Copy, Clone)]
892pub enum Saving<'a> {
893 NoSaving,
895 OneOff(TimeSpec),
899 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#[derive(PartialEq, Debug, Copy, Clone)]
934pub struct Rule<'a> {
935 pub name: &'a str,
937 pub from_year: Year,
939 pub to_year: Option<Year>,
941 pub month: Month,
943 pub day: DaySpec,
945 pub time: TimeSpecAndType,
947 pub time_to_add: TimeSpec,
949 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 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 to_year: match year {
977 "only" => None,
978 _ => Some(Year::from_str(year)?),
979 },
980 },
981 (
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#[derive(PartialEq, Debug, Copy, Clone)]
1170pub struct Zone<'a> {
1171 pub name: &'a str,
1173 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 Space,
1220 Zone(Zone<'a>),
1222 Continuation(ZoneInfo<'a>),
1224 Rule(Rule<'a>),
1226 Link(Link<'a>),
1228}
1229
1230impl<'a> Line<'a> {
1231 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 #[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 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}