use std::collections::hash_map::{Entry, HashMap};
use std::error::Error as ErrorTrait;
use std::fmt;
use line::{self, ChangeTime, DaySpec, Month, TimeType, Year};
#[derive(PartialEq, Debug, Default)]
pub struct Table {
pub rulesets: HashMap<String, Vec<RuleInfo>>,
pub zonesets: HashMap<String, Vec<ZoneInfo>>,
pub links: HashMap<String, String>,
}
impl Table {
pub fn get_zoneset(&self, zone_name: &str) -> Option<&[ZoneInfo]> {
if self.zonesets.contains_key(zone_name) {
Some(&*self.zonesets[zone_name])
} else if self.links.contains_key(zone_name) {
let target = &self.links[zone_name];
Some(&*self.zonesets[&*target])
} else {
None
}
}
}
#[derive(PartialEq, Debug)]
pub struct RuleInfo {
pub from_year: Year,
pub to_year: Option<Year>,
pub month: Month,
pub day: DaySpec,
pub time: i64,
pub time_type: TimeType,
pub time_to_add: i64,
pub letters: Option<String>,
}
impl<'line> From<line::Rule<'line>> for RuleInfo {
fn from(info: line::Rule) -> RuleInfo {
RuleInfo {
from_year: info.from_year,
to_year: info.to_year,
month: info.month,
day: info.day,
time: info.time.0.as_seconds(),
time_type: info.time.1,
time_to_add: info.time_to_add.as_seconds(),
letters: info.letters.map(str::to_owned),
}
}
}
impl RuleInfo {
pub fn applies_to_year(&self, year: i64) -> bool {
use line::Year::*;
match (self.from_year, self.to_year) {
(Number(from), None) => year == from,
(Number(from), Some(Maximum)) => year >= from,
(Number(from), Some(Number(to))) => year >= from && year <= to,
_ => unreachable!(),
}
}
pub fn absolute_datetime(&self, year: i64, utc_offset: i64, dst_offset: i64) -> i64 {
let offset = match self.time_type {
TimeType::UTC => 0,
TimeType::Standard => utc_offset,
TimeType::Wall => utc_offset + dst_offset,
};
let changetime = ChangeTime::UntilDay(Year::Number(year), self.month, self.day);
changetime.to_timestamp() + self.time - offset
}
}
#[derive(PartialEq, Debug)]
pub struct ZoneInfo {
pub offset: i64,
pub saving: Saving,
pub format: Format,
pub end_time: Option<ChangeTime>,
}
impl<'line> From<line::ZoneInfo<'line>> for ZoneInfo {
fn from(info: line::ZoneInfo) -> ZoneInfo {
ZoneInfo {
offset: info.utc_offset.as_seconds(),
saving: match info.saving {
line::Saving::NoSaving => Saving::NoSaving,
line::Saving::Multiple(s) => Saving::Multiple(s.to_owned()),
line::Saving::OneOff(t) => Saving::OneOff(t.as_seconds()),
},
format: Format::new(info.format),
end_time: info.time,
}
}
}
#[derive(PartialEq, Debug)]
pub enum Saving {
NoSaving,
OneOff(i64),
Multiple(String),
}
#[derive(PartialEq, Debug, Clone)]
pub enum Format {
Constant(String),
Alternate {
standard: String,
dst: String,
},
Placeholder(String),
}
impl Format {
pub fn new(template: &str) -> Format {
if let Some(pos) = template.find('/') {
Format::Alternate {
standard: template[..pos].to_owned(),
dst: template[pos + 1..].to_owned(),
}
} else if template.contains("%s") {
Format::Placeholder(template.to_owned())
} else {
Format::Constant(template.to_owned())
}
}
pub fn format(&self, dst_offset: i64, letters: Option<&String>) -> String {
let letters = match letters {
Some(l) => &**l,
None => "",
};
match *self {
Format::Constant(ref s) => s.clone(),
Format::Placeholder(ref s) => s.replace("%s", letters),
Format::Alternate { ref standard, .. } if dst_offset == 0 => standard.clone(),
Format::Alternate { ref dst, .. } => dst.clone(),
}
}
pub fn format_constant(&self) -> String {
if let Format::Constant(ref s) = *self {
s.clone()
} else {
panic!("Expected a constant formatting string");
}
}
}
#[derive(PartialEq, Debug)]
pub struct TableBuilder {
table: Table,
current_zoneset_name: Option<String>,
}
impl TableBuilder {
pub fn new() -> TableBuilder {
TableBuilder {
table: Table::default(),
current_zoneset_name: None,
}
}
pub fn add_zone_line<'line>(
&mut self,
zone_line: line::Zone<'line>,
) -> Result<(), Error<'line>> {
if let line::Saving::Multiple(ruleset_name) = zone_line.info.saving {
if !self.table.rulesets.contains_key(ruleset_name) {
return Err(Error::UnknownRuleset(ruleset_name));
}
}
let zoneset: &mut _ = match self.table.zonesets.entry(zone_line.name.to_owned()) {
Entry::Occupied(_) => return Err(Error::DuplicateZone),
Entry::Vacant(e) => e.insert(Vec::new()),
};
zoneset.push(zone_line.info.into());
self.current_zoneset_name = Some(zone_line.name.to_owned());
Ok(())
}
pub fn add_continuation_line(
&mut self,
continuation_line: line::ZoneInfo,
) -> Result<(), Error> {
let zoneset: &mut _ = match self.current_zoneset_name {
Some(ref name) => self.table.zonesets.get_mut(name).unwrap(),
None => return Err(Error::SurpriseContinuationLine),
};
zoneset.push(continuation_line.into());
Ok(())
}
pub fn add_rule_line(&mut self, rule_line: line::Rule) -> Result<(), Error> {
let ruleset = self
.table
.rulesets
.entry(rule_line.name.to_owned())
.or_insert_with(Vec::new);
ruleset.push(rule_line.into());
self.current_zoneset_name = None;
Ok(())
}
pub fn add_link_line<'line>(
&mut self,
link_line: line::Link<'line>,
) -> Result<(), Error<'line>> {
match self.table.links.entry(link_line.new.to_owned()) {
Entry::Occupied(_) => Err(Error::DuplicateLink(link_line.new)),
Entry::Vacant(e) => {
let _ = e.insert(link_line.existing.to_owned());
self.current_zoneset_name = None;
Ok(())
}
}
}
pub fn build(self) -> Table {
self.table
}
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum Error<'line> {
SurpriseContinuationLine,
UnknownRuleset(&'line str),
DuplicateLink(&'line str),
DuplicateZone,
}
impl<'line> fmt::Display for Error<'line> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.description())
}
}
impl<'line> ErrorTrait for Error<'line> {
fn description(&self) -> &str {
"interpretation error"
}
#[allow(bare_trait_objects)]
fn cause(&self) -> Option<&ErrorTrait> {
None
}
}