use std::ascii::AsciiExt;
use std::error::Error as ErrorTrait;
use std::fmt;
use std::str::FromStr;
use datetime::{LocalDate, LocalTime, LocalDateTime, Month, Weekday};
use datetime::zone::TimeType;
use regex::{Regex, Captures};
lazy_static! {
static ref RULE_LINE: Regex = 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+)
"##).unwrap();
static ref DAY_FIELD: Regex = Regex::new(r##"(?x) ^
( ?P<weekday> \w+ )
( ?P<sign> [<>] = )
( ?P<day> \d+ )
$ "##).unwrap();
static ref HM_FIELD: Regex = Regex::new(r##"(?x) ^
( ?P<sign> -? )
( ?P<hour> \d{1,2} ) : ( ?P<minute> \d{2} )
( ?P<flag> [wsugz] )?
$ "##).unwrap();
static ref HMS_FIELD: Regex = Regex::new(r##"(?x) ^
( ?P<sign> -? )
( ?P<hour> \d{1,2} ) : ( ?P<minute> \d{2} ) : ( ?P<second> \d{2} )
( ?P<flag> [wsugz] )?
$ "##).unwrap();
static ref ZONE_LINE: Regex = 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+ )?
"##).unwrap();
static ref CONTINUATION_LINE: Regex = 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+ )?
"##).unwrap();
static ref LINK_LINE: Regex = Regex::new(r##"(?x) ^
Link \s+
( ?P<target> \S+ ) \s+
( ?P<name> \S+ )
"##).unwrap();
static ref EMPTY_LINE: Regex = Regex::new(r##"(?x) ^
\s* (\#.*)?
$"##).unwrap();
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct Rule<'line> {
pub name: &'line str,
pub from_year: YearSpec,
pub to_year: Option<YearSpec>,
pub month: MonthSpec,
pub day: DaySpec,
pub time: TimeSpecAndType,
pub time_to_add: TimeSpec,
pub letters: Option<&'line str>,
}
impl<'line> Rule<'line> {
pub fn from_str(input: &str) -> Result<Rule, Error> {
if let Some(caps) = RULE_LINE.captures(input) {
let name = caps.name("name").unwrap();
let from_year = try!(caps.name("from").unwrap().parse());
let to_year = match caps.name("to").unwrap() {
"only" => None,
to => Some(try!(to.parse())),
};
let t = caps.name("type").unwrap();
if t != "-" && t != "\u{2010}" {
return Err(Error::Fail);
}
let month = try!(caps.name("in").unwrap().parse());
let day = try!(caps.name("on").unwrap().parse());
let time = try!(caps.name("at").unwrap().parse());
let time_to_add = try!(caps.name("save").unwrap().parse());
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::Fail)
}
}
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct Zone<'line> {
pub name: &'line str,
pub info: ZoneInfo<'line>,
}
impl<'line> Zone<'line> {
pub fn from_str(input: &str) -> Result<Zone, Error> {
if let Some(caps) = ZONE_LINE.captures(input) {
let name = caps.name("name").unwrap();
let info = try!(ZoneInfo::from_captures(caps));
Ok(Zone {
name: name,
info: info,
})
}
else {
Err(Error::Fail)
}
}
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct ZoneInfo<'line> {
pub utc_offset: TimeSpec,
pub saving: Saving<'line>,
pub format: &'line str,
pub time: Option<ChangeTime>,
}
impl<'line> ZoneInfo<'line> {
fn from_captures(caps: Captures<'line>) -> Result<ZoneInfo<'line>, Error> {
let utc_offset = try!(caps.name("gmtoff").unwrap().parse());
let saving = try!(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 (try!(y.parse()), try!(m.parse()), try!(d.parse()), try!(t.parse()))),
(Some(y), Some(m), Some(d), _ ) => Some(ChangeTime::UntilDay (try!(y.parse()), try!(m.parse()), try!(d.parse()))),
(Some(y), Some(m), _ , _ ) => Some(ChangeTime::UntilMonth (try!(y.parse()), try!(m.parse()))),
(Some(y), _ , _ , _ ) => Some(ChangeTime::UntilYear (try!(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,
})
}
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum Saving<'line> {
NoSaving,
OneOff(TimeSpec),
Multiple(&'line str),
}
impl<'line> Saving<'line> {
fn from_str(input: &str) -> Result<Saving, Error> {
if input == "-" {
Ok(Saving::NoSaving)
}
else if input.chars().all(|c| c == '-' || c == '_' || c.is_alphabetic()) {
Ok(Saving::Multiple(input))
}
else if HM_FIELD.is_match(input) {
let time = try!(input.parse());
Ok(Saving::OneOff(time))
}
else {
Err(Error::Fail)
}
}
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum ChangeTime {
UntilYear(YearSpec),
UntilMonth(YearSpec, MonthSpec),
UntilDay(YearSpec, MonthSpec, DaySpec),
UntilTime(YearSpec, MonthSpec, DaySpec, TimeSpecAndType),
}
impl ChangeTime {
pub fn to_timestamp(&self) -> i64 {
use self::ChangeTime::*;
use self::YearSpec::Number;
match *self {
UntilYear(Number(y)) => LocalDateTime::new(LocalDate::ymd(y, Month::January, 1).unwrap(), LocalTime::midnight()),
UntilMonth(Number(y), m) => LocalDateTime::new(LocalDate::ymd(y, m.0, 1).unwrap(), LocalTime::midnight()),
UntilDay(Number(y), m, d) => LocalDateTime::new(d.to_concrete_date(y, m.0), LocalTime::midnight()),
UntilTime(Number(y), m, d, time) => {
let local_time = match time.0 {
TimeSpec::Zero => LocalTime::midnight(),
TimeSpec::Hours(h) => LocalTime::hms(h, 0, 0).unwrap(),
TimeSpec::HoursMinutes(h, mm) => LocalTime::hms(h, mm, 0).unwrap(),
TimeSpec::HoursMinutesSeconds(h, mm, s) => LocalTime::hms(h, mm, s).unwrap(),
};
LocalDateTime::new(d.to_concrete_date(y, m.0), local_time)
},
_ => unreachable!("What happened? {:?}", self),
}.to_instant().seconds()
}
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct Link<'line> {
pub existing: &'line str,
pub new: &'line str,
}
impl<'line> Link<'line> {
pub fn from_str(input: &str) -> Result<Link, Error> {
if let Some(caps) = 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::Fail)
}
}
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum YearSpec {
Minimum,
Maximum,
Number(i64),
}
impl FromStr for YearSpec {
type Err = Error;
fn from_str(input: &str) -> Result<YearSpec, Self::Err> {
if input == "min" || input == "minimum" {
Ok(YearSpec::Minimum)
}
else if input == "max" || input == "maximum" {
Ok(YearSpec::Maximum)
}
else if input.chars().all(|c| c.is_digit(10)) {
Ok(YearSpec::Number(input.parse().unwrap()))
}
else {
Err(Error::Fail)
}
}
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct MonthSpec(pub Month);
impl FromStr for MonthSpec {
type Err = Error;
fn from_str(input: &str) -> Result<MonthSpec, Self::Err> {
Ok(match &*input.to_ascii_lowercase() {
"jan" | "january" => MonthSpec(Month::January),
"feb" | "february" => MonthSpec(Month::February),
"mar" | "march" => MonthSpec(Month::March),
"apr" | "april" => MonthSpec(Month::April),
"may" => MonthSpec(Month::May),
"jun" | "june" => MonthSpec(Month::June),
"jul" | "july" => MonthSpec(Month::July),
"aug" | "august" => MonthSpec(Month::August),
"sep" | "september" => MonthSpec(Month::September),
"oct" | "october" => MonthSpec(Month::October),
"nov" | "november" => MonthSpec(Month::November),
"dec" | "december" => MonthSpec(Month::December),
_ => return Err(Error::Fail),
})
}
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct WeekdaySpec(pub Weekday);
impl FromStr for WeekdaySpec {
type Err = Error;
fn from_str(input: &str) -> Result<WeekdaySpec, Self::Err> {
Ok(match &*input.to_ascii_lowercase() {
"mon" | "monday" => WeekdaySpec(Weekday::Monday),
"tue" | "tuesday" => WeekdaySpec(Weekday::Tuesday),
"wed" | "wednesday" => WeekdaySpec(Weekday::Wednesday),
"thu" | "thursday" => WeekdaySpec(Weekday::Thursday),
"fri" | "friday" => WeekdaySpec(Weekday::Friday),
"sat" | "saturday" => WeekdaySpec(Weekday::Saturday),
"sun" | "sunday" => WeekdaySpec(Weekday::Sunday),
_ => return Err(Error::Fail),
})
}
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum DaySpec {
Ordinal(i8),
Last(WeekdaySpec),
LastOnOrBefore(WeekdaySpec, i8),
FirstOnOrAfter(WeekdaySpec, i8)
}
impl DaySpec {
pub fn to_concrete_date(&self, year: i64, month: Month) -> LocalDate {
use datetime::{LocalDate, DatePiece, Year};
match *self {
DaySpec::Ordinal(day) => LocalDate::ymd(year, month, day).unwrap(),
DaySpec::Last(w) => DaySpec::find_weekday(w, Year(year).month(month).days(..).rev()),
DaySpec::LastOnOrBefore(w, day) => DaySpec::find_weekday(w, Year(year).month(month).days(..).rev().filter(|d| d.day() < day)),
DaySpec::FirstOnOrAfter(w, day) => DaySpec::find_weekday(w, Year(year).month(month).days(..).skip(day as usize - 1)),
}
}
fn find_weekday<I>(weekday: WeekdaySpec, mut iterator: I) -> LocalDate
where I: Iterator<Item=LocalDate> {
use datetime::DatePiece;
iterator.find(|date| date.weekday() == weekday.0)
.expect("Failed to find weekday")
}
}
impl FromStr for DaySpec {
type Err = Error;
fn from_str(input: &str) -> Result<DaySpec, Self::Err> {
if input.chars().all(|c| c.is_digit(10)) {
Ok(DaySpec::Ordinal(input.parse().unwrap()))
}
else if input.starts_with("last") {
let weekday = try!(input[4..].parse());
Ok(DaySpec::Last(weekday))
}
else if let Some(caps) = 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::Fail)
}
}
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum TimeSpec {
Hours(i8),
HoursMinutes(i8, i8),
HoursMinutesSeconds(i8, i8, i8),
Zero,
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct TimeSpecAndType(pub TimeSpec, pub TimeType);
impl TimeSpec {
pub fn with_type(self, time_type: TimeType) -> TimeSpecAndType {
TimeSpecAndType(self, time_type)
}
pub fn as_seconds(&self) -> i64 {
match *self {
TimeSpec::Zero => 0,
TimeSpec::Hours(h) => h as i64 * 3600,
TimeSpec::HoursMinutes(h, m) => h as i64 * 3600 + m as i64 * 60,
TimeSpec::HoursMinutesSeconds(h, m, s) => h as i64 * 3600 + m as i64 * 60 + s as i64,
}
}
}
impl FromStr for TimeSpecAndType {
type Err = Error;
fn from_str(input: &str) -> Result<TimeSpecAndType, Self::Err> {
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) = 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) = 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::Fail)
}
}
}
impl FromStr for TimeSpec {
type Err = Error;
fn from_str(input: &str) -> Result<TimeSpec, Self::Err> {
match input.parse() {
Ok(TimeSpecAndType(spec, TimeType::Wall)) => Ok(spec),
Ok(TimeSpecAndType(_ , _ )) => Err(Error::Fail),
Err(e) => Err(e),
}
}
}
fn parse_time_type(c: &str) -> Option<TimeType> {
Some(match c {
"w" => TimeType::Wall,
"s" => TimeType::Standard,
"u" | "g" | "z" => TimeType::UTC,
_ => return None,
})
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum Error {
Fail
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.description())
}
}
impl ErrorTrait for Error {
fn description(&self) -> &str {
"parse error"
}
fn cause(&self) -> Option<&ErrorTrait> {
None
}
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum Line<'line> {
Space,
Zone(Zone<'line>),
Continuation(ZoneInfo<'line>),
Rule(Rule<'line>),
Link(Link<'line>),
}
impl<'line> Line<'line> {
pub fn from_str(input: &str) -> Result<Line, Error> {
if EMPTY_LINE.is_match(input) {
Ok(Line::Space)
}
else if let Ok(zone) = Zone::from_str(input) {
Ok(Line::Zone(zone))
}
else if let Some(caps) = CONTINUATION_LINE.captures(input) {
Ok(Line::Continuation(try!(ZoneInfo::from_captures(caps))))
}
else if let Ok(rule) = Rule::from_str(input) {
Ok(Line::Rule(rule))
}
else if let Ok(link) = Link::from_str(input) {
Ok(Line::Link(link))
}
else {
Err(Error::Fail)
}
}
}
#[cfg(test)]
mod test {
pub use std::str::FromStr;
pub use super::*;
pub use datetime::{Weekday, Month};
macro_rules! test {
($name:ident: $input:expr => $result:expr) => {
#[test]
fn $name() {
assert_eq!(Line::from_str($input), $result);
}
};
}
test!(empty: "" => Ok(Line::Space));
test!(spaces: " " => Ok(Line::Space));
mod rules {
use super::*;
use datetime::zone::TimeType;
test!(rule_1: "Rule US 1967 1973 ‐ Apr lastSun 2:00 1:00 D" => Ok(Line::Rule(Rule {
name: "US",
from_year: YearSpec::Number(1967),
to_year: Some(YearSpec::Number(1973)),
month: MonthSpec(Month::April),
day: DaySpec::Last(WeekdaySpec(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: YearSpec::Number(1976),
to_year: None,
month: MonthSpec(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: YearSpec::Number(1977),
to_year: Some(YearSpec::Number(1980)),
month: MonthSpec(Month::April),
day: DaySpec::FirstOnOrAfter(WeekdaySpec(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::Fail));
test!(bad_month: "Rule EU 1977 1980 - Febtober Sun>=1 1:00u 1:00 S" => Err(Error::Fail));
}
mod zones {
use super::*;
use datetime::zone::TimeType;
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(YearSpec::Number(1971), MonthSpec(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(YearSpec::Number(1971), MonthSpec(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(YearSpec::Number(1943), MonthSpec(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(YearSpec::Number(1919))),
},
})));
#[test]
fn negative_offsets() {
static LINE: &'static str = "Zone Europe/London -0:01:15 - LMT 1847 Dec 1 0:00s";
let zone = Zone::from_str(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 zone = Zone::from_str(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 zone = Zone::from_str(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!(MonthSpec::from_str("Aug"), Ok(MonthSpec(Month::August)));
assert_eq!(MonthSpec::from_str("December"), Ok(MonthSpec(Month::December)));
}
test!(golb: "GOLB" => Err(Error::Fail));
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::Fail));
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",
})));
}