use std::str::FromStr;
use std::ascii::AsciiExt;
use regex::{Regex, Captures};
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 LineParser {
pub fn new() -> 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-Z a-z 0-9 / _ + - ]+ ) \s+
( ?P<gmtoff> \S+ ) \s+
( ?P<rulessave> \S+ ) \s+
( ?P<format> \S+ ) \s*
( ?P<year> \S+ )? \s*
( ?P<month> \S+ )? \s*
( ?P<day> \S+ )? \s*
( ?P<time> \S+ )? \s*
(\#.*)?
$ "##).unwrap(),
continuation_line: Regex::new(r##"(?x) ^
\s+
( ?P<gmtoff> \S+ ) \s+
( ?P<rulessave> \S+ ) \s+
( ?P<format> \S+ ) \s*
( ?P<year> \S+ )? \s*
( ?P<month> \S+ )? \s*
( ?P<day> \S+ )? \s*
( ?P<time> \S+ )? \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(),
}
}
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum Year {
Minimum,
Maximum,
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())),
}
})
}
}
#[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,
}
}
}
impl FromStr for Month {
type Err = Error;
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())),
})
}
}
#[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())),
})
}
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum DaySpec {
Ordinal(i8),
Last(Weekday),
LastOnOrBefore(Weekday, i8),
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 {
year % 4 == 0 && year % 100 != 0 || year % 400 == 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 {
pub fn to_concrete_day(&self, year: i64, month: Month) -> i8 {
let length = month.length(is_leap(year));
match *self {
DaySpec::Ordinal(day) => day,
DaySpec::Last(weekday) => (1..length+1).rev()
.find(|&day| Weekday::calculate(year, month, day) == weekday).unwrap(),
DaySpec::LastOnOrBefore(weekday, day) => (1..day+1).rev()
.find(|&day| Weekday::calculate(year, month, day) == weekday).unwrap(),
DaySpec::FirstOnOrAfter(weekday, day) => (day..length+1)
.find(|&day| Weekday::calculate(year, month, day) == weekday).unwrap(),
}
}
}
#[cfg(test)]
#[test]
fn last_monday() {
let dayspec = DaySpec::Last(Weekday::Monday);
assert_eq!(dayspec.to_concrete_day(2016, Month::January), 25);
assert_eq!(dayspec.to_concrete_day(2016, Month::February), 29);
assert_eq!(dayspec.to_concrete_day(2016, Month::March), 28);
assert_eq!(dayspec.to_concrete_day(2016, Month::April), 25);
assert_eq!(dayspec.to_concrete_day(2016, Month::May), 30);
assert_eq!(dayspec.to_concrete_day(2016, Month::June), 27);
assert_eq!(dayspec.to_concrete_day(2016, Month::July), 25);
assert_eq!(dayspec.to_concrete_day(2016, Month::August), 29);
assert_eq!(dayspec.to_concrete_day(2016, Month::September), 26);
assert_eq!(dayspec.to_concrete_day(2016, Month::October), 31);
assert_eq!(dayspec.to_concrete_day(2016, Month::November), 28);
assert_eq!(dayspec.to_concrete_day(2016, Month::December), 26);
}
#[cfg(test)]
#[test]
fn first_monday_on_or_after() {
let dayspec = DaySpec::FirstOnOrAfter(Weekday::Monday, 20);
assert_eq!(dayspec.to_concrete_day(2016, Month::January), 25);
assert_eq!(dayspec.to_concrete_day(2016, Month::February), 22);
assert_eq!(dayspec.to_concrete_day(2016, Month::March), 21);
assert_eq!(dayspec.to_concrete_day(2016, Month::April), 25);
assert_eq!(dayspec.to_concrete_day(2016, Month::May), 23);
assert_eq!(dayspec.to_concrete_day(2016, Month::June), 20);
assert_eq!(dayspec.to_concrete_day(2016, Month::July), 25);
assert_eq!(dayspec.to_concrete_day(2016, Month::August), 22);
assert_eq!(dayspec.to_concrete_day(2016, Month::September), 26);
assert_eq!(dayspec.to_concrete_day(2016, Month::October), 24);
assert_eq!(dayspec.to_concrete_day(2016, Month::November), 21);
assert_eq!(dayspec.to_concrete_day(2016, Month::December), 26);
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum TimeSpec {
Hours(i8),
HoursMinutes(i8, i8),
HoursMinutesSeconds(i8, i8, i8),
Zero,
}
impl TimeSpec {
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)
}
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum ChangeTime {
UntilYear(Year),
UntilMonth(Year, Month),
UntilDay(Year, Month, DaySpec),
UntilTime(Year, Month, DaySpec, TimeSpecAndType),
}
impl ChangeTime {
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) => time_to_timestamp(y, m as i8, d.to_concrete_day(y, m), 0, 0, 0),
ChangeTime::UntilTime(Year::Number(y), m, d, time) => match time.0 {
TimeSpec::Zero => time_to_timestamp(y, m as i8, d.to_concrete_day(y, m), 0, 0, 0),
TimeSpec::Hours(h) => time_to_timestamp(y, m as i8, d.to_concrete_day(y, m), h, 0, 0),
TimeSpec::HoursMinutes(h, min) => time_to_timestamp(y, m as i8, d.to_concrete_day(y, m), h, min, 0),
TimeSpec::HoursMinutesSeconds(h, min, s) => time_to_timestamp(y, m as i8, d.to_concrete_day(y, m), 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!()
}
}
}
#[cfg(test)]
#[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);
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct ZoneInfo<'a> {
pub utc_offset: TimeSpec,
pub saving: Saving<'a>,
pub format: &'a str,
pub time: Option<ChangeTime>,
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum Saving<'a> {
NoSaving,
OneOff(TimeSpec),
Multiple(&'a str),
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct Rule<'a> {
pub name: &'a str,
pub from_year: Year,
pub to_year: Option<Year>,
pub month: Month,
pub day: DaySpec,
pub time: TimeSpecAndType,
pub time_to_add: TimeSpec,
pub letters: Option<&'a str>,
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct Zone<'a> {
pub name: &'a str,
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> {
Space,
Zone(Zone<'a>),
Continuation(ZoneInfo<'a>),
Rule(Rule<'a>),
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 {
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_digit(10)) {
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() == "-" { -1 } else { 1 };
let hour : i8 = caps.name("hour").unwrap().parse().unwrap();
let minute : i8 = caps.name("minute").unwrap().parse().unwrap();
let flag = caps.name("flag").and_then(|c| parse_time_type(&c[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() == "-" { -1 } else { 1 };
let hour : i8 = caps.name("hour").unwrap().parse().unwrap();
let minute : i8 = caps.name("minute").unwrap().parse().unwrap();
let second : i8 = caps.name("second").unwrap().parse().unwrap();
let flag = caps.name("flag").and_then(|c| parse_time_type(&c[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> {
if input.chars().all(|c| c.is_digit(10)) {
Ok(DaySpec::Ordinal(input.parse().unwrap()))
} else if input.starts_with("last") {
let weekday = input[4..].parse()?;
Ok(DaySpec::Last(weekday))
} else if let Some(caps) = self.day_field.captures(input) {
let weekday = caps.name("weekday").unwrap().parse().unwrap();
let day = caps.name("day").unwrap().parse().unwrap();
match caps.name("sign").unwrap() {
"<=" => Ok(DaySpec::LastOnOrBefore(weekday, day)),
">=" => Ok(DaySpec::FirstOnOrAfter(weekday, day)),
_ => unreachable!("The regex only matches one of those two!"),
}
} 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();
let from_year = caps.name("from").unwrap().parse()?;
let to_year = match caps.name("to").unwrap() {
"only" => None,
to => Some(to.parse()?),
};
let t = caps.name("type").unwrap();
if t != "-" && t != "\u{2010}" {
return Err(Error::TypeColumnContainedNonHyphen(t.to_string()));
}
let month = caps.name("in").unwrap().parse()?;
let day = self.parse_dayspec(caps.name("on").unwrap())?;
let time = self.parse_timespec_and_type(caps.name("at").unwrap())?;
let time_to_add = self.parse_timespec(caps.name("save").unwrap())?;
let letters = match caps.name("letters").unwrap() {
"-" => None,
l => Some(l),
};
Ok(Rule {
name: name,
from_year: from_year,
to_year: to_year,
month: month,
day: day,
time: time,
time_to_add: time_to_add,
letters: 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())?;
let saving = self.saving_from_str(caps.name("rulessave").unwrap())?;
let format = caps.name("format").unwrap();
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.parse()?, m.parse()?, self.parse_dayspec(d)?, self.parse_timespec_and_type(t)?)),
(Some(y), Some(m), Some(d), _ ) => Some(ChangeTime::UntilDay (y.parse()?, m.parse()?, self.parse_dayspec(d)?)),
(Some(y), Some(m), _ , _ ) => Some(ChangeTime::UntilMonth (y.parse()?, m.parse()?)),
(Some(y), _ , _ , _ ) => Some(ChangeTime::UntilYear (y.parse()?)),
(None , None , None , None ) => None,
_ => unreachable!("Out-of-order capturing groups!"),
};
Ok(ZoneInfo {
utc_offset: utc_offset,
saving: saving,
format: format,
time: 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();
let info = self.zoneinfo_from_captures(caps)?;
Ok(Zone {
name: name,
info: 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();
let name = caps.name("name").unwrap();
Ok(Link { existing: target, new: name })
}
else {
Err(Error::NotParsedAsLinkLine)
}
}
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::*;
macro_rules! test {
($name:ident: $input:expr => $result:expr) => {
#[test]
fn $name() {
let parser = LineParser::new();
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: &'static str = "Zone Europe/London -0:01:15 - LMT 1847 Dec 1 0:00s";
let parser = LineParser::new();
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: &'static str = "Zone Europe/Madrid -0:14:44 - LMT 1901 Jan 1 0:00s";
let parser = LineParser::new();
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: &'static str = "Zone America/Danmarkshavn -1:14:40 - LMT 1916 Jul 28";
let parser = LineParser::new();
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",
})));
}