blob: 19bc5a19a9c280de8bcbe3ada0d770d5dde1db7d [file] [log] [blame] [edit]
//! Parsing zoneinfo data files, line-by-line.
//!
//! This module provides functions that take a line of input from a zoneinfo
//! data file and attempts to parse it, returning the details of the line if
//! it gets parsed successfully. It classifies them as `Rule`, `Link`,
//! `Zone`, or `Continuation` lines.
//!
//! `Line` is the type that parses and holds zoneinfo line data. To try to
//! parse a string, use the `Line::from_str` constructor. (This isn’t the
//! `FromStr` trait, so you can’t use `parse` on a string. Sorry!)
//!
//! ## Examples
//!
//! Parsing a `Rule` line:
//!
//! ```
//! use parse_zoneinfo::line::*;
//!
//! let parser = LineParser::default();
//! let line = parser.parse_str("Rule EU 1977 1980 - Apr Sun>=1 1:00u 1:00 S");
//!
//! assert_eq!(line, Ok(Line::Rule(Rule {
//! name: "EU",
//! from_year: Year::Number(1977),
//! to_year: Some(Year::Number(1980)),
//! month: Month::April,
//! day: DaySpec::FirstOnOrAfter(Weekday::Sunday, 1),
//! time: TimeSpec::HoursMinutes(1, 0).with_type(TimeType::UTC),
//! time_to_add: TimeSpec::HoursMinutes(1, 0),
//! letters: Some("S"),
//! })));
//! ```
//!
//! Parsing a `Zone` line:
//!
//! ```
//! use parse_zoneinfo::line::*;
//!
//! let parser = LineParser::default();
//! let line = parser.parse_str("Zone Australia/Adelaide 9:30 Aus AC%sT 1971 Oct 31 2:00:00");
//!
//! assert_eq!(line, Ok(Line::Zone(Zone {
//! name: "Australia/Adelaide",
//! info: ZoneInfo {
//! utc_offset: TimeSpec::HoursMinutes(9, 30),
//! saving: Saving::Multiple("Aus"),
//! format: "AC%sT",
//! time: Some(ChangeTime::UntilTime(
//! Year::Number(1971),
//! Month::October,
//! DaySpec::Ordinal(31),
//! TimeSpec::HoursMinutesSeconds(2, 0, 0).with_type(TimeType::Wall))
//! ),
//! },
//! })));
//! ```
//!
//! Parsing a `Link` line:
//!
//! ```
//! use parse_zoneinfo::line::*;
//!
//! let parser = LineParser::default();
//! let line = parser.parse_str("Link Europe/Istanbul Asia/Istanbul");
//! assert_eq!(line, Ok(Line::Link(Link {
//! existing: "Europe/Istanbul",
//! new: "Asia/Istanbul",
//! })));
//! ```
use std::fmt;
use std::str::FromStr;
use regex::{Captures, Regex};
pub struct LineParser {
rule_line: Regex,
day_field: Regex,
hm_field: Regex,
hms_field: Regex,
zone_line: Regex,
continuation_line: Regex,
link_line: Regex,
empty_line: Regex,
}
#[derive(PartialEq, Debug, Clone)]
pub enum Error {
FailedYearParse(String),
FailedMonthParse(String),
FailedWeekdayParse(String),
InvalidLineType(String),
TypeColumnContainedNonHyphen(String),
CouldNotParseSaving(String),
InvalidDaySpec(String),
InvalidTimeSpecAndType(String),
NonWallClockInTimeSpec(String),
NotParsedAsRuleLine,
NotParsedAsZoneLine,
NotParsedAsLinkLine,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::FailedYearParse(s) => write!(f, "failed to parse as a year value: \"{}\"", s),
Error::FailedMonthParse(s) => write!(f, "failed to parse as a month value: \"{}\"", s),
Error::FailedWeekdayParse(s) => {
write!(f, "failed to parse as a weekday value: \"{}\"", s)
}
Error::InvalidLineType(s) => write!(f, "line with invalid format: \"{}\"", s),
Error::TypeColumnContainedNonHyphen(s) => {
write!(
f,
"'type' column is not a hyphen but has the value: \"{}\"",
s
)
}
Error::CouldNotParseSaving(s) => write!(f, "failed to parse RULES column: \"{}\"", s),
Error::InvalidDaySpec(s) => write!(f, "invalid day specification ('ON'): \"{}\"", s),
Error::InvalidTimeSpecAndType(s) => write!(f, "invalid time: \"{}\"", s),
Error::NonWallClockInTimeSpec(s) => {
write!(f, "time value not given as wall time: \"{}\"", s)
}
Error::NotParsedAsRuleLine => write!(f, "failed to parse line as a rule"),
Error::NotParsedAsZoneLine => write!(f, "failed to parse line as a zone"),
Error::NotParsedAsLinkLine => write!(f, "failed to parse line as a link"),
}
}
}
impl std::error::Error for Error {}
impl Default for LineParser {
fn default() -> Self {
LineParser {
rule_line: Regex::new(
r##"(?x) ^
Rule \s+
( ?P<name> \S+) \s+
( ?P<from> \S+) \s+
( ?P<to> \S+) \s+
( ?P<type> \S+) \s+
( ?P<in> \S+) \s+
( ?P<on> \S+) \s+
( ?P<at> \S+) \s+
( ?P<save> \S+) \s+
( ?P<letters> \S+) \s*
(\#.*)?
$ "##,
)
.unwrap(),
day_field: Regex::new(
r##"(?x) ^
( ?P<weekday> \w+ )
( ?P<sign> [<>] = )
( ?P<day> \d+ )
$ "##,
)
.unwrap(),
hm_field: Regex::new(
r##"(?x) ^
( ?P<sign> -? )
( ?P<hour> \d{1,2} ) : ( ?P<minute> \d{2} )
( ?P<flag> [wsugz] )?
$ "##,
)
.unwrap(),
hms_field: Regex::new(
r##"(?x) ^
( ?P<sign> -? )
( ?P<hour> \d{1,2} ) : ( ?P<minute> \d{2} ) : ( ?P<second> \d{2} )
( ?P<flag> [wsugz] )?
$ "##,
)
.unwrap(),
zone_line: Regex::new(
r##"(?x) ^
Zone \s+
( ?P<name> [A-Za-z0-9/_+-]+ ) \s+
( ?P<gmtoff> \S+ ) \s+
( ?P<rulessave> \S+ ) \s+
( ?P<format> \S+ ) \s*
( ?P<year> [0-9]+)? \s*
( ?P<month> [A-Za-z]+)? \s*
( ?P<day> [A-Za-z0-9><=]+ )? \s*
( ?P<time> [0-9:]+[suwz]? )? \s*
(\#.*)?
$ "##,
)
.unwrap(),
continuation_line: Regex::new(
r##"(?x) ^
\s+
( ?P<gmtoff> \S+ ) \s+
( ?P<rulessave> \S+ ) \s+
( ?P<format> \S+ ) \s*
( ?P<year> [0-9]+)? \s*
( ?P<month> [A-Za-z]+)? \s*
( ?P<day> [A-Za-z0-9><=]+ )? \s*
( ?P<time> [0-9:]+[suwz]? )? \s*
(\#.*)?
$ "##,
)
.unwrap(),
link_line: Regex::new(
r##"(?x) ^
Link \s+
( ?P<target> \S+ ) \s+
( ?P<name> \S+ ) \s*
(\#.*)?
$ "##,
)
.unwrap(),
empty_line: Regex::new(
r##"(?x) ^
\s*
(\#.*)?
$"##,
)
.unwrap(),
}
}
}
/// A **year** definition field.
///
/// A year has one of the following representations in a file:
///
/// - `min` or `minimum`, the minimum year possible, for when a rule needs to
/// apply up until the first rule with a specific year;
/// - `max` or `maximum`, the maximum year possible, for when a rule needs to
/// apply after the last rule with a specific year;
/// - a year number, referring to a specific year.
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum Year {
/// The minimum year possible: `min` or `minimum`.
Minimum,
/// The maximum year possible: `max` or `maximum`.
Maximum,
/// A specific year number.
Number(i64),
}
impl FromStr for Year {
type Err = Error;
fn from_str(input: &str) -> Result<Year, Self::Err> {
Ok(match &*input.to_ascii_lowercase() {
"min" | "minimum" => Year::Minimum,
"max" | "maximum" => Year::Maximum,
year => match year.parse() {
Ok(year) => Year::Number(year),
Err(_) => return Err(Error::FailedYearParse(input.to_string())),
},
})
}
}
/// A **month** field, which is actually just a wrapper around
/// `datetime::Month`.
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum Month {
January = 1,
February = 2,
March = 3,
April = 4,
May = 5,
June = 6,
July = 7,
August = 8,
September = 9,
October = 10,
November = 11,
December = 12,
}
impl Month {
fn length(self, is_leap: bool) -> i8 {
match self {
Month::January => 31,
Month::February if is_leap => 29,
Month::February => 28,
Month::March => 31,
Month::April => 30,
Month::May => 31,
Month::June => 30,
Month::July => 31,
Month::August => 31,
Month::September => 30,
Month::October => 31,
Month::November => 30,
Month::December => 31,
}
}
/// Get the next calendar month, with an error going from Dec->Jan
fn next_in_year(self) -> Result<Month, &'static str> {
Ok(match self {
Month::January => Month::February,
Month::February => Month::March,
Month::March => Month::April,
Month::April => Month::May,
Month::May => Month::June,
Month::June => Month::July,
Month::July => Month::August,
Month::August => Month::September,
Month::September => Month::October,
Month::October => Month::November,
Month::November => Month::December,
Month::December => Err("Cannot wrap year from dec->jan")?,
})
}
/// Get the previous calendar month, with an error going from Jan->Dec
fn prev_in_year(self) -> Result<Month, &'static str> {
Ok(match self {
Month::January => Err("Cannot wrap years from jan->dec")?,
Month::February => Month::January,
Month::March => Month::February,
Month::April => Month::March,
Month::May => Month::April,
Month::June => Month::May,
Month::July => Month::June,
Month::August => Month::July,
Month::September => Month::August,
Month::October => Month::September,
Month::November => Month::October,
Month::December => Month::November,
})
}
}
impl FromStr for Month {
type Err = Error;
/// Attempts to parse the given string into a value of this type.
fn from_str(input: &str) -> Result<Month, Self::Err> {
Ok(match &*input.to_ascii_lowercase() {
"jan" | "january" => Month::January,
"feb" | "february" => Month::February,
"mar" | "march" => Month::March,
"apr" | "april" => Month::April,
"may" => Month::May,
"jun" | "june" => Month::June,
"jul" | "july" => Month::July,
"aug" | "august" => Month::August,
"sep" | "september" => Month::September,
"oct" | "october" => Month::October,
"nov" | "november" => Month::November,
"dec" | "december" => Month::December,
other => return Err(Error::FailedMonthParse(other.to_string())),
})
}
}
/// A **weekday** field, which is actually just a wrapper around
/// `datetime::Weekday`.
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum Weekday {
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
}
impl FromStr for Weekday {
type Err = Error;
fn from_str(input: &str) -> Result<Weekday, Self::Err> {
Ok(match &*input.to_ascii_lowercase() {
"mon" | "monday" => Weekday::Monday,
"tue" | "tuesday" => Weekday::Tuesday,
"wed" | "wednesday" => Weekday::Wednesday,
"thu" | "thursday" => Weekday::Thursday,
"fri" | "friday" => Weekday::Friday,
"sat" | "saturday" => Weekday::Saturday,
"sun" | "sunday" => Weekday::Sunday,
other => return Err(Error::FailedWeekdayParse(other.to_string())),
})
}
}
/// A **day** definition field.
///
/// This can be given in either absolute terms (such as “the fifth day of the
/// month”), or relative terms (such as “the last Sunday of the month”, or
/// “the last Friday before or including the 13th”).
///
/// Note that in the last example, it’s allowed for that particular Friday to
/// *be* the 13th in question.
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum DaySpec {
/// A specific day of the month, given by its number.
Ordinal(i8),
/// The last day of the month with a specific weekday.
Last(Weekday),
/// The **last** day with the given weekday **before** (or including) a
/// day with a specific number.
LastOnOrBefore(Weekday, i8),
/// The **first** day with the given weekday **after** (or including) a
/// day with a specific number.
FirstOnOrAfter(Weekday, i8),
}
impl Weekday {
fn calculate(year: i64, month: Month, day: i8) -> Weekday {
let m = month as i64;
let y = if m < 3 { year - 1 } else { year };
let d = day as i64;
const T: [i64; 12] = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
match (y + y / 4 - y / 100 + y / 400 + T[m as usize - 1] + d) % 7 {
0 => Weekday::Sunday,
1 => Weekday::Monday,
2 => Weekday::Tuesday,
3 => Weekday::Wednesday,
4 => Weekday::Thursday,
5 => Weekday::Friday,
6 => Weekday::Saturday,
_ => panic!("why is negative modulus designed so?"),
}
}
}
#[cfg(test)]
#[test]
fn weekdays() {
assert_eq!(
Weekday::calculate(1970, Month::January, 1),
Weekday::Thursday
);
assert_eq!(
Weekday::calculate(2017, Month::February, 11),
Weekday::Saturday
);
assert_eq!(Weekday::calculate(1890, Month::March, 2), Weekday::Sunday);
assert_eq!(Weekday::calculate(2100, Month::April, 20), Weekday::Tuesday);
assert_eq!(Weekday::calculate(2009, Month::May, 31), Weekday::Sunday);
assert_eq!(Weekday::calculate(2001, Month::June, 9), Weekday::Saturday);
assert_eq!(Weekday::calculate(1995, Month::July, 21), Weekday::Friday);
assert_eq!(Weekday::calculate(1982, Month::August, 8), Weekday::Sunday);
assert_eq!(
Weekday::calculate(1962, Month::September, 6),
Weekday::Thursday
);
assert_eq!(
Weekday::calculate(1899, Month::October, 14),
Weekday::Saturday
);
assert_eq!(
Weekday::calculate(2016, Month::November, 18),
Weekday::Friday
);
assert_eq!(
Weekday::calculate(2010, Month::December, 19),
Weekday::Sunday
);
assert_eq!(
Weekday::calculate(2016, Month::February, 29),
Weekday::Monday
);
}
fn is_leap(year: i64) -> bool {
// Leap year rules: years which are factors of 4, except those divisible
// by 100, unless they are divisible by 400.
//
// We test most common cases first: 4th year, 100th year, then 400th year.
//
// We factor out 4 from 100 since it was already tested, leaving us checking
// if it's divisible by 25. Afterwards, we do the same, factoring 25 from
// 400, leaving us with 16.
//
// Factors of 4 and 16 can quickly be found with bitwise AND.
year & 3 == 0 && (year % 25 != 0 || year & 15 == 0)
}
#[cfg(test)]
#[test]
fn leap_years() {
assert!(!is_leap(1900));
assert!(is_leap(1904));
assert!(is_leap(1964));
assert!(is_leap(1996));
assert!(!is_leap(1997));
assert!(!is_leap(1997));
assert!(!is_leap(1999));
assert!(is_leap(2000));
assert!(is_leap(2016));
assert!(!is_leap(2100));
}
impl DaySpec {
/// Converts this day specification to a concrete date, given the year and
/// month it should occur in.
pub fn to_concrete_day(&self, year: i64, month: Month) -> (Month, i8) {
let leap = is_leap(year);
let length = month.length(leap);
// we will never hit the 0 because we unwrap prev_in_year below
let prev_length = month.prev_in_year().map(|m| m.length(leap)).unwrap_or(0);
match *self {
DaySpec::Ordinal(day) => (month, day),
DaySpec::Last(weekday) => (
month,
(1..length + 1)
.rev()
.find(|&day| Weekday::calculate(year, month, day) == weekday)
.unwrap(),
),
DaySpec::LastOnOrBefore(weekday, day) => (-7..day + 1)
.rev()
.flat_map(|inner_day| {
if inner_day >= 1 && Weekday::calculate(year, month, inner_day) == weekday {
Some((month, inner_day))
} else if inner_day < 1
&& Weekday::calculate(
year,
month.prev_in_year().unwrap(),
prev_length + inner_day,
) == weekday
{
// inner_day is negative, so this is subtraction
Some((month.prev_in_year().unwrap(), prev_length + inner_day))
} else {
None
}
})
.next()
.unwrap(),
DaySpec::FirstOnOrAfter(weekday, day) => (day..day + 8)
.flat_map(|inner_day| {
if inner_day <= length && Weekday::calculate(year, month, inner_day) == weekday
{
Some((month, inner_day))
} else if inner_day > length
&& Weekday::calculate(
year,
month.next_in_year().unwrap(),
inner_day - length,
) == weekday
{
Some((month.next_in_year().unwrap(), inner_day - length))
} else {
None
}
})
.next()
.unwrap(),
}
}
}
/// A **time** definition field.
///
/// A time must have an hours component, with optional minutes and seconds
/// components. It can also be negative with a starting ‘-’.
///
/// Hour 0 is midnight at the start of the day, and Hour 24 is midnight at the
/// end of the day.
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum TimeSpec {
/// A number of hours.
Hours(i8),
/// A number of hours and minutes.
HoursMinutes(i8, i8),
/// A number of hours, minutes, and seconds.
HoursMinutesSeconds(i8, i8, i8),
/// Zero, or midnight at the start of the day.
Zero,
}
impl TimeSpec {
/// Returns the number of seconds past midnight that this time spec
/// represents.
pub fn as_seconds(self) -> i64 {
match self {
TimeSpec::Hours(h) => h as i64 * 60 * 60,
TimeSpec::HoursMinutes(h, m) => h as i64 * 60 * 60 + m as i64 * 60,
TimeSpec::HoursMinutesSeconds(h, m, s) => h as i64 * 60 * 60 + m as i64 * 60 + s as i64,
TimeSpec::Zero => 0,
}
}
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum TimeType {
Wall,
Standard,
UTC,
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct TimeSpecAndType(pub TimeSpec, pub TimeType);
impl TimeSpec {
pub fn with_type(self, timetype: TimeType) -> TimeSpecAndType {
TimeSpecAndType(self, timetype)
}
}
/// The time at which the rules change for a location.
///
/// This is described with as few units as possible: a change that occurs at
/// the beginning of the year lists only the year, a change that occurs on a
/// particular day has to list the year, month, and day, and one that occurs
/// at a particular second has to list everything.
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum ChangeTime {
/// The earliest point in a particular **year**.
UntilYear(Year),
/// The earliest point in a particular **month**.
UntilMonth(Year, Month),
/// The earliest point in a particular **day**.
UntilDay(Year, Month, DaySpec),
/// The earliest point in a particular **hour, minute, or second**.
UntilTime(Year, Month, DaySpec, TimeSpecAndType),
}
impl ChangeTime {
/// Convert this change time to an absolute timestamp, as the number of
/// seconds since the Unix epoch that the change occurs at.
pub fn to_timestamp(&self) -> i64 {
fn seconds_in_year(year: i64) -> i64 {
if is_leap(year) {
366 * 24 * 60 * 60
} else {
365 * 24 * 60 * 60
}
}
fn seconds_until_start_of_year(year: i64) -> i64 {
if year >= 1970 {
(1970..year).map(seconds_in_year).sum()
} else {
-(year..1970).map(seconds_in_year).sum::<i64>()
}
}
fn time_to_timestamp(
year: i64,
month: i8,
day: i8,
hour: i8,
minute: i8,
second: i8,
) -> i64 {
const MONTHS_NON_LEAP: [i64; 12] = [
0,
31,
31 + 28,
31 + 28 + 31,
31 + 28 + 31 + 30,
31 + 28 + 31 + 30 + 31,
31 + 28 + 31 + 30 + 31 + 30,
31 + 28 + 31 + 30 + 31 + 30 + 31,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30,
];
const MONTHS_LEAP: [i64; 12] = [
0,
31,
31 + 29,
31 + 29 + 31,
31 + 29 + 31 + 30,
31 + 29 + 31 + 30 + 31,
31 + 29 + 31 + 30 + 31 + 30,
31 + 29 + 31 + 30 + 31 + 30 + 31,
31 + 29 + 31 + 30 + 31 + 30 + 31 + 31,
31 + 29 + 31 + 30 + 31 + 30 + 31 + 31 + 30,
31 + 29 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31,
31 + 29 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30,
];
seconds_until_start_of_year(year)
+ 60 * 60
* 24
* if is_leap(year) {
MONTHS_LEAP[month as usize - 1]
} else {
MONTHS_NON_LEAP[month as usize - 1]
}
+ 60 * 60 * 24 * (day as i64 - 1)
+ 60 * 60 * hour as i64
+ 60 * minute as i64
+ second as i64
}
match *self {
ChangeTime::UntilYear(Year::Number(y)) => time_to_timestamp(y, 1, 1, 0, 0, 0),
ChangeTime::UntilMonth(Year::Number(y), m) => time_to_timestamp(y, m as i8, 1, 0, 0, 0),
ChangeTime::UntilDay(Year::Number(y), m, d) => {
let (m, wd) = d.to_concrete_day(y, m);
time_to_timestamp(y, m as i8, wd, 0, 0, 0)
}
ChangeTime::UntilTime(Year::Number(y), m, d, time) => match time.0 {
TimeSpec::Zero => {
let (m, wd) = d.to_concrete_day(y, m);
time_to_timestamp(y, m as i8, wd, 0, 0, 0)
}
TimeSpec::Hours(h) => {
let (m, wd) = d.to_concrete_day(y, m);
time_to_timestamp(y, m as i8, wd, h, 0, 0)
}
TimeSpec::HoursMinutes(h, min) => {
let (m, wd) = d.to_concrete_day(y, m);
time_to_timestamp(y, m as i8, wd, h, min, 0)
}
TimeSpec::HoursMinutesSeconds(h, min, s) => {
let (m, wd) = d.to_concrete_day(y, m);
time_to_timestamp(y, m as i8, wd, h, min, s)
}
},
_ => unreachable!(),
}
}
pub fn year(&self) -> i64 {
match *self {
ChangeTime::UntilYear(Year::Number(y)) => y,
ChangeTime::UntilMonth(Year::Number(y), ..) => y,
ChangeTime::UntilDay(Year::Number(y), ..) => y,
ChangeTime::UntilTime(Year::Number(y), ..) => y,
_ => unreachable!(),
}
}
}
/// The information contained in both zone lines *and* zone continuation lines.
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct ZoneInfo<'a> {
/// The amount of time that needs to be added to UTC to get the standard
/// time in this zone.
pub utc_offset: TimeSpec,
/// The name of all the rules that should apply in the time zone, or the
/// amount of time to add.
pub saving: Saving<'a>,
/// The format for time zone abbreviations, with `%s` as the string marker.
pub format: &'a str,
/// The time at which the rules change for this location, or `None` if
/// these rules are in effect until the end of time (!).
pub time: Option<ChangeTime>,
}
/// The amount of daylight saving time (DST) to apply to this timespan. This
/// is a special type for a certain field in a zone line, which can hold
/// different types of value.
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum Saving<'a> {
/// Just stick to the base offset.
NoSaving,
/// This amount of time should be saved while this timespan is in effect.
/// (This is the equivalent to there being a single one-off rule with the
/// given amount of time to save).
OneOff(TimeSpec),
/// All rules with the given name should apply while this timespan is in
/// effect.
Multiple(&'a str),
}
/// A **rule** definition line.
///
/// According to the `zic(8)` man page, a rule line has this form, along with
/// an example:
///
/// ```text
/// Rule NAME FROM TO TYPE IN ON AT SAVE LETTER/S
/// Rule US 1967 1973 ‐ Apr lastSun 2:00 1:00 D
/// ```
///
/// Apart from the opening `Rule` to specify which kind of line this is, and
/// the `type` column, every column in the line has a field in this struct.
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct Rule<'a> {
/// The name of the set of rules that this rule is part of.
pub name: &'a str,
/// The first year in which the rule applies.
pub from_year: Year,
/// The final year, or `None` if’s ‘only’.
pub to_year: Option<Year>,
/// The month in which the rule takes effect.
pub month: Month,
/// The day on which the rule takes effect.
pub day: DaySpec,
/// The time of day at which the rule takes effect.
pub time: TimeSpecAndType,
/// The amount of time to be added when the rule is in effect.
pub time_to_add: TimeSpec,
/// The variable part of time zone abbreviations to be used when this rule
/// is in effect, if any.
pub letters: Option<&'a str>,
}
/// A **zone** definition line.
///
/// According to the `zic(8)` man page, a zone line has this form, along with
/// an example:
///
/// ```text
/// Zone NAME GMTOFF RULES/SAVE FORMAT [UNTILYEAR [MONTH [DAY [TIME]]]]
/// Zone Australia/Adelaide 9:30 Aus AC%sT 1971 Oct 31 2:00
/// ```
///
/// The opening `Zone` identifier is ignored, and the last four columns are
/// all optional, with their variants consolidated into a `ChangeTime`.
///
/// The `Rules/Save` column, if it contains a value, *either* contains the
/// name of the rules to use for this zone, *or* contains a one-off period of
/// time to save.
///
/// A continuation rule line contains all the same fields apart from the
/// `Name` column and the opening `Zone` identifier.
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct Zone<'a> {
/// The name of the time zone.
pub name: &'a str,
/// All the other fields of info.
pub info: ZoneInfo<'a>,
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct Link<'a> {
pub existing: &'a str,
pub new: &'a str,
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum Line<'a> {
/// This line is empty.
Space,
/// This line contains a **zone** definition.
Zone(Zone<'a>),
/// This line contains a **continuation** of a zone definition.
Continuation(ZoneInfo<'a>),
/// This line contains a **rule** definition.
Rule(Rule<'a>),
/// This line contains a **link** definition.
Link(Link<'a>),
}
fn parse_time_type(c: &str) -> Option<TimeType> {
Some(match c {
"w" => TimeType::Wall,
"s" => TimeType::Standard,
"u" | "g" | "z" => TimeType::UTC,
_ => return None,
})
}
impl LineParser {
#[deprecated]
pub fn new() -> Self {
Self::default()
}
fn parse_timespec_and_type(&self, input: &str) -> Result<TimeSpecAndType, Error> {
if input == "-" {
Ok(TimeSpecAndType(TimeSpec::Zero, TimeType::Wall))
} else if input.chars().all(|c| c == '-' || c.is_ascii_digit()) {
Ok(TimeSpecAndType(
TimeSpec::Hours(input.parse().unwrap()),
TimeType::Wall,
))
} else if let Some(caps) = self.hm_field.captures(input) {
let sign: i8 = if caps.name("sign").unwrap().as_str() == "-" {
-1
} else {
1
};
let hour: i8 = caps.name("hour").unwrap().as_str().parse().unwrap();
let minute: i8 = caps.name("minute").unwrap().as_str().parse().unwrap();
let flag = caps
.name("flag")
.and_then(|c| parse_time_type(&c.as_str()[0..1]))
.unwrap_or(TimeType::Wall);
Ok(TimeSpecAndType(
TimeSpec::HoursMinutes(hour * sign, minute * sign),
flag,
))
} else if let Some(caps) = self.hms_field.captures(input) {
let sign: i8 = if caps.name("sign").unwrap().as_str() == "-" {
-1
} else {
1
};
let hour: i8 = caps.name("hour").unwrap().as_str().parse().unwrap();
let minute: i8 = caps.name("minute").unwrap().as_str().parse().unwrap();
let second: i8 = caps.name("second").unwrap().as_str().parse().unwrap();
let flag = caps
.name("flag")
.and_then(|c| parse_time_type(&c.as_str()[0..1]))
.unwrap_or(TimeType::Wall);
Ok(TimeSpecAndType(
TimeSpec::HoursMinutesSeconds(hour * sign, minute * sign, second * sign),
flag,
))
} else {
Err(Error::InvalidTimeSpecAndType(input.to_string()))
}
}
fn parse_timespec(&self, input: &str) -> Result<TimeSpec, Error> {
match self.parse_timespec_and_type(input) {
Ok(TimeSpecAndType(spec, TimeType::Wall)) => Ok(spec),
Ok(TimeSpecAndType(_, _)) => Err(Error::NonWallClockInTimeSpec(input.to_string())),
Err(e) => Err(e),
}
}
fn parse_dayspec(&self, input: &str) -> Result<DaySpec, Error> {
// Parse the field as a number if it vaguely resembles one.
if input.chars().all(|c| c.is_ascii_digit()) {
Ok(DaySpec::Ordinal(input.parse().unwrap()))
}
// Check if it stars with ‘last’, and trim off the first four bytes if
// it does. (Luckily, the file is ASCII, so ‘last’ is four bytes)
else if let Some(remainder) = input.strip_prefix("last") {
let weekday = remainder.parse()?;
Ok(DaySpec::Last(weekday))
}
// Check if it’s a relative expression with the regex.
else if let Some(caps) = self.day_field.captures(input) {
let weekday = caps.name("weekday").unwrap().as_str().parse().unwrap();
let day = caps.name("day").unwrap().as_str().parse().unwrap();
match caps.name("sign").unwrap().as_str() {
"<=" => Ok(DaySpec::LastOnOrBefore(weekday, day)),
">=" => Ok(DaySpec::FirstOnOrAfter(weekday, day)),
_ => unreachable!("The regex only matches one of those two!"),
}
}
// Otherwise, give up.
else {
Err(Error::InvalidDaySpec(input.to_string()))
}
}
fn parse_rule<'a>(&self, input: &'a str) -> Result<Rule<'a>, Error> {
if let Some(caps) = self.rule_line.captures(input) {
let name = caps.name("name").unwrap().as_str();
let from_year = caps.name("from").unwrap().as_str().parse()?;
// The end year can be ‘only’ to indicate that this rule only
// takes place on that year.
let to_year = match caps.name("to").unwrap().as_str() {
"only" => None,
to => Some(to.parse()?),
};
// According to the spec, the only value inside the ‘type’ column
// should be “-”, so throw an error if it isn’t. (It only exists
// for compatibility with old versions that used to contain year
// types.) Sometimes “‐”, a Unicode hyphen, is used as well.
let t = caps.name("type").unwrap().as_str();
if t != "-" && t != "\u{2010}" {
return Err(Error::TypeColumnContainedNonHyphen(t.to_string()));
}
let month = caps.name("in").unwrap().as_str().parse()?;
let day = self.parse_dayspec(caps.name("on").unwrap().as_str())?;
let time = self.parse_timespec_and_type(caps.name("at").unwrap().as_str())?;
let time_to_add = self.parse_timespec(caps.name("save").unwrap().as_str())?;
let letters = match caps.name("letters").unwrap().as_str() {
"-" => None,
l => Some(l),
};
Ok(Rule {
name,
from_year,
to_year,
month,
day,
time,
time_to_add,
letters,
})
} else {
Err(Error::NotParsedAsRuleLine)
}
}
fn saving_from_str<'a>(&self, input: &'a str) -> Result<Saving<'a>, Error> {
if input == "-" {
Ok(Saving::NoSaving)
} else if input
.chars()
.all(|c| c == '-' || c == '_' || c.is_alphabetic())
{
Ok(Saving::Multiple(input))
} else if self.hm_field.is_match(input) {
let time = self.parse_timespec(input)?;
Ok(Saving::OneOff(time))
} else {
Err(Error::CouldNotParseSaving(input.to_string()))
}
}
fn zoneinfo_from_captures<'a>(&self, caps: Captures<'a>) -> Result<ZoneInfo<'a>, Error> {
let utc_offset = self.parse_timespec(caps.name("gmtoff").unwrap().as_str())?;
let saving = self.saving_from_str(caps.name("rulessave").unwrap().as_str())?;
let format = caps.name("format").unwrap().as_str();
// The year, month, day, and time fields are all optional, meaning
// that it should be impossible to, say, have a defined month but not
// a defined year.
let time = match (
caps.name("year"),
caps.name("month"),
caps.name("day"),
caps.name("time"),
) {
(Some(y), Some(m), Some(d), Some(t)) => Some(ChangeTime::UntilTime(
y.as_str().parse()?,
m.as_str().parse()?,
self.parse_dayspec(d.as_str())?,
self.parse_timespec_and_type(t.as_str())?,
)),
(Some(y), Some(m), Some(d), _) => Some(ChangeTime::UntilDay(
y.as_str().parse()?,
m.as_str().parse()?,
self.parse_dayspec(d.as_str())?,
)),
(Some(y), Some(m), _, _) => Some(ChangeTime::UntilMonth(
y.as_str().parse()?,
m.as_str().parse()?,
)),
(Some(y), _, _, _) => Some(ChangeTime::UntilYear(y.as_str().parse()?)),
(None, None, None, None) => None,
_ => unreachable!("Out-of-order capturing groups!"),
};
Ok(ZoneInfo {
utc_offset,
saving,
format,
time,
})
}
fn parse_zone<'a>(&self, input: &'a str) -> Result<Zone<'a>, Error> {
if let Some(caps) = self.zone_line.captures(input) {
let name = caps.name("name").unwrap().as_str();
let info = self.zoneinfo_from_captures(caps)?;
Ok(Zone { name, info })
} else {
Err(Error::NotParsedAsZoneLine)
}
}
fn parse_link<'a>(&self, input: &'a str) -> Result<Link<'a>, Error> {
if let Some(caps) = self.link_line.captures(input) {
let target = caps.name("target").unwrap().as_str();
let name = caps.name("name").unwrap().as_str();
Ok(Link {
existing: target,
new: name,
})
} else {
Err(Error::NotParsedAsLinkLine)
}
}
/// Attempt to parse this line, returning a `Line` depending on what
/// type of line it was, or an `Error` if it couldn't be parsed.
pub fn parse_str<'a>(&self, input: &'a str) -> Result<Line<'a>, Error> {
if self.empty_line.is_match(input) {
return Ok(Line::Space);
}
match self.parse_zone(input) {
Err(Error::NotParsedAsZoneLine) => {}
result => return result.map(Line::Zone),
}
match self.continuation_line.captures(input) {
None => {}
Some(caps) => return self.zoneinfo_from_captures(caps).map(Line::Continuation),
}
match self.parse_rule(input) {
Err(Error::NotParsedAsRuleLine) => {}
result => return result.map(Line::Rule),
}
match self.parse_link(input) {
Err(Error::NotParsedAsLinkLine) => {}
result => return result.map(Line::Link),
}
Err(Error::InvalidLineType(input.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn last_monday() {
let dayspec = DaySpec::Last(Weekday::Monday);
assert_eq!(
dayspec.to_concrete_day(2016, Month::January),
(Month::January, 25)
);
assert_eq!(
dayspec.to_concrete_day(2016, Month::February),
(Month::February, 29)
);
assert_eq!(
dayspec.to_concrete_day(2016, Month::March),
(Month::March, 28)
);
assert_eq!(
dayspec.to_concrete_day(2016, Month::April),
(Month::April, 25)
);
assert_eq!(dayspec.to_concrete_day(2016, Month::May), (Month::May, 30));
assert_eq!(
dayspec.to_concrete_day(2016, Month::June),
(Month::June, 27)
);
assert_eq!(
dayspec.to_concrete_day(2016, Month::July),
(Month::July, 25)
);
assert_eq!(
dayspec.to_concrete_day(2016, Month::August),
(Month::August, 29)
);
assert_eq!(
dayspec.to_concrete_day(2016, Month::September),
(Month::September, 26)
);
assert_eq!(
dayspec.to_concrete_day(2016, Month::October),
(Month::October, 31)
);
assert_eq!(
dayspec.to_concrete_day(2016, Month::November),
(Month::November, 28)
);
assert_eq!(
dayspec.to_concrete_day(2016, Month::December),
(Month::December, 26)
);
}
#[test]
fn first_monday_on_or_after() {
let dayspec = DaySpec::FirstOnOrAfter(Weekday::Monday, 20);
assert_eq!(
dayspec.to_concrete_day(2016, Month::January),
(Month::January, 25)
);
assert_eq!(
dayspec.to_concrete_day(2016, Month::February),
(Month::February, 22)
);
assert_eq!(
dayspec.to_concrete_day(2016, Month::March),
(Month::March, 21)
);
assert_eq!(
dayspec.to_concrete_day(2016, Month::April),
(Month::April, 25)
);
assert_eq!(dayspec.to_concrete_day(2016, Month::May), (Month::May, 23));
assert_eq!(
dayspec.to_concrete_day(2016, Month::June),
(Month::June, 20)
);
assert_eq!(
dayspec.to_concrete_day(2016, Month::July),
(Month::July, 25)
);
assert_eq!(
dayspec.to_concrete_day(2016, Month::August),
(Month::August, 22)
);
assert_eq!(
dayspec.to_concrete_day(2016, Month::September),
(Month::September, 26)
);
assert_eq!(
dayspec.to_concrete_day(2016, Month::October),
(Month::October, 24)
);
assert_eq!(
dayspec.to_concrete_day(2016, Month::November),
(Month::November, 21)
);
assert_eq!(
dayspec.to_concrete_day(2016, Month::December),
(Month::December, 26)
);
}
// A couple of specific timezone transitions that we care about
#[test]
fn first_sunday_in_toronto() {
let dayspec = DaySpec::FirstOnOrAfter(Weekday::Sunday, 25);
assert_eq!(dayspec.to_concrete_day(1932, Month::April), (Month::May, 1));
// asia/zion
let dayspec = DaySpec::LastOnOrBefore(Weekday::Friday, 1);
assert_eq!(
dayspec.to_concrete_day(2012, Month::April),
(Month::March, 30)
);
}
#[test]
fn to_timestamp() {
let time = ChangeTime::UntilYear(Year::Number(1970));
assert_eq!(time.to_timestamp(), 0);
let time = ChangeTime::UntilYear(Year::Number(2016));
assert_eq!(time.to_timestamp(), 1451606400);
let time = ChangeTime::UntilYear(Year::Number(1900));
assert_eq!(time.to_timestamp(), -2208988800);
let time = ChangeTime::UntilTime(
Year::Number(2000),
Month::February,
DaySpec::Last(Weekday::Sunday),
TimeSpecAndType(TimeSpec::Hours(9), TimeType::Wall),
);
assert_eq!(time.to_timestamp(), 951642000);
}
macro_rules! test {
($name:ident: $input:expr => $result:expr) => {
#[test]
fn $name() {
let parser = LineParser::default();
assert_eq!(parser.parse_str($input), $result);
}
};
}
test!(empty: "" => Ok(Line::Space));
test!(spaces: " " => Ok(Line::Space));
test!(rule_1: "Rule US 1967 1973 ‐ Apr lastSun 2:00 1:00 D" => Ok(Line::Rule(Rule {
name: "US",
from_year: Year::Number(1967),
to_year: Some(Year::Number(1973)),
month: Month::April,
day: DaySpec::Last(Weekday::Sunday),
time: TimeSpec::HoursMinutes(2, 0).with_type(TimeType::Wall),
time_to_add: TimeSpec::HoursMinutes(1, 0),
letters: Some("D"),
})));
test!(rule_2: "Rule Greece 1976 only - Oct 10 2:00s 0 -" => Ok(Line::Rule(Rule {
name: "Greece",
from_year: Year::Number(1976),
to_year: None,
month: Month::October,
day: DaySpec::Ordinal(10),
time: TimeSpec::HoursMinutes(2, 0).with_type(TimeType::Standard),
time_to_add: TimeSpec::Hours(0),
letters: None,
})));
test!(rule_3: "Rule EU 1977 1980 - Apr Sun>=1 1:00u 1:00 S" => Ok(Line::Rule(Rule {
name: "EU",
from_year: Year::Number(1977),
to_year: Some(Year::Number(1980)),
month: Month::April,
day: DaySpec::FirstOnOrAfter(Weekday::Sunday, 1),
time: TimeSpec::HoursMinutes(1, 0).with_type(TimeType::UTC),
time_to_add: TimeSpec::HoursMinutes(1, 0),
letters: Some("S"),
})));
test!(no_hyphen: "Rule EU 1977 1980 HEY Apr Sun>=1 1:00u 1:00 S" => Err(Error::TypeColumnContainedNonHyphen("HEY".to_string())));
test!(bad_month: "Rule EU 1977 1980 - Febtober Sun>=1 1:00u 1:00 S" => Err(Error::FailedMonthParse("febtober".to_string())));
test!(zone: "Zone Australia/Adelaide 9:30 Aus AC%sT 1971 Oct 31 2:00:00" => Ok(Line::Zone(Zone {
name: "Australia/Adelaide",
info: ZoneInfo {
utc_offset: TimeSpec::HoursMinutes(9, 30),
saving: Saving::Multiple("Aus"),
format: "AC%sT",
time: Some(ChangeTime::UntilTime(Year::Number(1971), Month::October, DaySpec::Ordinal(31), TimeSpec::HoursMinutesSeconds(2, 0, 0).with_type(TimeType::Wall))),
},
})));
test!(continuation_1: " 9:30 Aus AC%sT 1971 Oct 31 2:00:00" => Ok(Line::Continuation(ZoneInfo {
utc_offset: TimeSpec::HoursMinutes(9, 30),
saving: Saving::Multiple("Aus"),
format: "AC%sT",
time: Some(ChangeTime::UntilTime(Year::Number(1971), Month::October, DaySpec::Ordinal(31), TimeSpec::HoursMinutesSeconds(2, 0, 0).with_type(TimeType::Wall))),
})));
test!(continuation_2: " 1:00 C-Eur CE%sT 1943 Oct 25" => Ok(Line::Continuation(ZoneInfo {
utc_offset: TimeSpec::HoursMinutes(1, 00),
saving: Saving::Multiple("C-Eur"),
format: "CE%sT",
time: Some(ChangeTime::UntilDay(Year::Number(1943), Month::October, DaySpec::Ordinal(25))),
})));
test!(zone_hyphen: "Zone Asia/Ust-Nera\t 9:32:54 -\tLMT\t1919" => Ok(Line::Zone(Zone {
name: "Asia/Ust-Nera",
info: ZoneInfo {
utc_offset: TimeSpec::HoursMinutesSeconds(9, 32, 54),
saving: Saving::NoSaving,
format: "LMT",
time: Some(ChangeTime::UntilYear(Year::Number(1919))),
},
})));
#[test]
fn negative_offsets() {
static LINE: &str = "Zone Europe/London -0:01:15 - LMT 1847 Dec 1 0:00s";
let parser = LineParser::default();
let zone = parser.parse_zone(LINE).unwrap();
assert_eq!(
zone.info.utc_offset,
TimeSpec::HoursMinutesSeconds(0, -1, -15)
);
}
#[test]
fn negative_offsets_2() {
static LINE: &str =
"Zone Europe/Madrid -0:14:44 - LMT 1901 Jan 1 0:00s";
let parser = LineParser::default();
let zone = parser.parse_zone(LINE).unwrap();
assert_eq!(
zone.info.utc_offset,
TimeSpec::HoursMinutesSeconds(0, -14, -44)
);
}
#[test]
fn negative_offsets_3() {
static LINE: &str = "Zone America/Danmarkshavn -1:14:40 - LMT 1916 Jul 28";
let parser = LineParser::default();
let zone = parser.parse_zone(LINE).unwrap();
assert_eq!(
zone.info.utc_offset,
TimeSpec::HoursMinutesSeconds(-1, -14, -40)
);
}
test!(link: "Link Europe/Istanbul Asia/Istanbul" => Ok(Line::Link(Link {
existing: "Europe/Istanbul",
new: "Asia/Istanbul",
})));
#[test]
fn month() {
assert_eq!(Month::from_str("Aug"), Ok(Month::August));
assert_eq!(Month::from_str("December"), Ok(Month::December));
}
test!(golb: "GOLB" => Err(Error::InvalidLineType("GOLB".to_string())));
test!(comment: "# this is a comment" => Ok(Line::Space));
test!(another_comment: " # so is this" => Ok(Line::Space));
test!(multiple_hash: " # so is this ## " => Ok(Line::Space));
test!(non_comment: " this is not a # comment" => Err(Error::InvalidTimeSpecAndType("this".to_string())));
test!(comment_after: "Link Europe/Istanbul Asia/Istanbul #with a comment after" => Ok(Line::Link(Link {
existing: "Europe/Istanbul",
new: "Asia/Istanbul",
})));
test!(two_comments_after: "Link Europe/Istanbul Asia/Istanbul # comment ## comment" => Ok(Line::Link(Link {
existing: "Europe/Istanbul",
new: "Asia/Istanbul",
})));
}