parse_zoneinfo/
table.rs

1//! Collecting parsed zoneinfo data lines into a set of time zone data.
2//!
3//! This module provides the `Table` struct, which is able to take parsed
4//! lines of input from the `line` module and coalesce them into a single
5//! set of data.
6//!
7//! It’s not as simple as it seems, because the zoneinfo data lines refer to
8//! each other through strings: lines of the form “link zone A to B” could be
9//! *parsed* successfully but still fail to be *interpreted* successfully if
10//! “B” doesn’t exist. So it has to check every step of the way—nothing wrong
11//! with this, it’s just a consequence of reading data from a text file.
12//!
13//! This module only deals with constructing a table from data: any analysis
14//! of the data is done elsewhere.
15//!
16//!
17//! ## Example
18//!
19//! ```
20//! use parse_zoneinfo::line::{Zone, Line, LineParser, Link};
21//! use parse_zoneinfo::table::{TableBuilder};
22//!
23//! let parser = LineParser::default();
24//! let mut builder = TableBuilder::new();
25//!
26//! let zone = "Zone  Pacific/Auckland  11:39:04  -  LMT  1868  Nov  2";
27//! let link = "Link  Pacific/Auckland  Antarctica/McMurdo";
28//!
29//! for line in [zone, link] {
30//!     match parser.parse_str(&line)? {
31//!         Line::Zone(zone) => builder.add_zone_line(zone).unwrap(),
32//!         Line::Continuation(cont) => builder.add_continuation_line(cont).unwrap(),
33//!         Line::Rule(rule) => builder.add_rule_line(rule).unwrap(),
34//!         Line::Link(link) => builder.add_link_line(link).unwrap(),
35//!         Line::Space => {}
36//!     }
37//! }
38//!
39//! let table = builder.build();
40//!
41//! assert!(table.get_zoneset("Pacific/Auckland").is_some());
42//! assert!(table.get_zoneset("Antarctica/McMurdo").is_some());
43//! assert!(table.get_zoneset("UTC").is_none());
44//! # Ok::<(), parse_zoneinfo::line::Error>(())
45//! ```
46
47use std::collections::hash_map::{Entry, HashMap};
48use std::fmt;
49
50use crate::line::{self, ChangeTime, DaySpec, Month, TimeType, Year};
51
52/// A **table** of all the data in one or more zoneinfo files.
53#[derive(PartialEq, Debug, Default)]
54pub struct Table {
55    /// Mapping of ruleset names to rulesets.
56    pub rulesets: HashMap<String, Vec<RuleInfo>>,
57
58    /// Mapping of zoneset names to zonesets.
59    pub zonesets: HashMap<String, Vec<ZoneInfo>>,
60
61    /// Mapping of link timezone names, to the names they link to.
62    pub links: HashMap<String, String>,
63}
64
65impl Table {
66    /// Tries to find the zoneset with the given name by looking it up in
67    /// either the zonesets map or the links map.
68    pub fn get_zoneset(&self, zone_name: &str) -> Option<&[ZoneInfo]> {
69        if self.zonesets.contains_key(zone_name) {
70            Some(&*self.zonesets[zone_name])
71        } else if self.links.contains_key(zone_name) {
72            let target = &self.links[zone_name];
73            Some(&*self.zonesets[target])
74        } else {
75            None
76        }
77    }
78}
79
80/// An owned rule definition line.
81///
82/// This mimics the `Rule` struct in the `line` module, only its uses owned
83/// Strings instead of string slices, and has had some pre-processing
84/// applied to it.
85#[derive(PartialEq, Debug)]
86pub struct RuleInfo {
87    /// The year that this rule *starts* applying.
88    pub from_year: Year,
89
90    /// The year that this rule *finishes* applying, inclusive, or `None` if
91    /// it applies up until the end of this timespan.
92    pub to_year: Option<Year>,
93
94    /// The month it applies on.
95    pub month: Month,
96
97    /// The day it applies on.
98    pub day: DaySpec,
99
100    /// The exact time it applies on.
101    pub time: i64,
102
103    /// The type of time that time is.
104    pub time_type: TimeType,
105
106    /// The amount of time to save.
107    pub time_to_add: i64,
108
109    /// Any extra letters that should be added to this time zone’s
110    /// abbreviation, in place of `%s`.
111    pub letters: Option<String>,
112}
113
114impl<'line> From<line::Rule<'line>> for RuleInfo {
115    fn from(info: line::Rule) -> RuleInfo {
116        RuleInfo {
117            from_year: info.from_year,
118            to_year: info.to_year,
119            month: info.month,
120            day: info.day,
121            time: info.time.0.as_seconds(),
122            time_type: info.time.1,
123            time_to_add: info.time_to_add.as_seconds(),
124            letters: info.letters.map(str::to_owned),
125        }
126    }
127}
128
129impl RuleInfo {
130    /// Returns whether this rule is in effect during the given year.
131    pub fn applies_to_year(&self, year: i64) -> bool {
132        use line::Year::*;
133
134        match (self.from_year, self.to_year) {
135            (Number(from), None) => year == from,
136            (Number(from), Some(Maximum)) => year >= from,
137            (Number(from), Some(Number(to))) => year >= from && year <= to,
138            _ => unreachable!(),
139        }
140    }
141
142    pub fn absolute_datetime(&self, year: i64, utc_offset: i64, dst_offset: i64) -> i64 {
143        let offset = match self.time_type {
144            TimeType::UTC => 0,
145            TimeType::Standard => utc_offset,
146            TimeType::Wall => utc_offset + dst_offset,
147        };
148
149        let changetime = ChangeTime::UntilDay(Year::Number(year), self.month, self.day);
150        changetime.to_timestamp() + self.time - offset
151    }
152}
153
154/// An owned zone definition line.
155///
156/// This struct mimics the `ZoneInfo` struct in the `line` module, *not* the
157/// `Zone` struct, which is the key name in the map—this is just the value.
158///
159/// As with `RuleInfo`, this struct uses owned Strings rather than string
160/// slices.
161#[derive(PartialEq, Debug)]
162pub struct ZoneInfo {
163    /// The number of seconds that need to be added to UTC to get the
164    /// standard time in this zone.
165    pub offset: i64,
166
167    /// The name of all the rules that should apply in the time zone, or the
168    /// amount of daylight-saving time to add.
169    pub saving: Saving,
170
171    /// The format for time zone abbreviations.
172    pub format: Format,
173
174    /// The time at which the rules change for this time zone, or `None` if
175    /// these rules are in effect until the end of time (!).
176    pub end_time: Option<ChangeTime>,
177}
178
179impl<'line> From<line::ZoneInfo<'line>> for ZoneInfo {
180    fn from(info: line::ZoneInfo) -> ZoneInfo {
181        ZoneInfo {
182            offset: info.utc_offset.as_seconds(),
183            saving: match info.saving {
184                line::Saving::NoSaving => Saving::NoSaving,
185                line::Saving::Multiple(s) => Saving::Multiple(s.to_owned()),
186                line::Saving::OneOff(t) => Saving::OneOff(t.as_seconds()),
187            },
188            format: Format::new(info.format),
189            end_time: info.time,
190        }
191    }
192}
193
194/// The amount of daylight saving time (DST) to apply to this timespan. This
195/// is a special type for a certain field in a zone line, which can hold
196/// different types of value.
197///
198/// This is the owned version of the `Saving` type in the `line` module.
199#[derive(PartialEq, Debug)]
200pub enum Saving {
201    /// Just stick to the base offset.
202    NoSaving,
203
204    /// This amount of time should be saved while this timespan is in effect.
205    /// (This is the equivalent to there being a single one-off rule with the
206    /// given amount of time to save).
207    OneOff(i64),
208
209    /// All rules with the given name should apply while this timespan is in
210    /// effect.
211    Multiple(String),
212}
213
214/// The format string to generate a time zone abbreviation from.
215#[derive(PartialEq, Debug, Clone)]
216pub enum Format {
217    /// A constant format, which remains the same throughout both standard
218    /// and DST timespans.
219    Constant(String),
220
221    /// An alternate format, such as “PST/PDT”, which changes between
222    /// standard and DST timespans.
223    Alternate {
224        /// Abbreviation to use during Standard Time.
225        standard: String,
226
227        /// Abbreviation to use during Summer Time.
228        dst: String,
229    },
230
231    /// A format with a placeholder `%s`, which uses the `letters` field in
232    /// a `RuleInfo` to generate the time zone abbreviation.
233    Placeholder(String),
234}
235
236impl Format {
237    /// Convert the template into one of the `Format` variants. This can’t
238    /// fail, as any syntax that doesn’t match one of the two formats will
239    /// just be a ‘constant’ format.
240    pub fn new(template: &str) -> Format {
241        if let Some(pos) = template.find('/') {
242            Format::Alternate {
243                standard: template[..pos].to_owned(),
244                dst: template[pos + 1..].to_owned(),
245            }
246        } else if template.contains("%s") {
247            Format::Placeholder(template.to_owned())
248        } else {
249            Format::Constant(template.to_owned())
250        }
251    }
252
253    pub fn format(&self, dst_offset: i64, letters: Option<&String>) -> String {
254        let letters = match letters {
255            Some(l) => &**l,
256            None => "",
257        };
258
259        match *self {
260            Format::Constant(ref s) => s.clone(),
261            Format::Placeholder(ref s) => s.replace("%s", letters),
262            Format::Alternate { ref standard, .. } if dst_offset == 0 => standard.clone(),
263            Format::Alternate { ref dst, .. } => dst.clone(),
264        }
265    }
266
267    pub fn format_constant(&self) -> String {
268        if let Format::Constant(ref s) = *self {
269            s.clone()
270        } else {
271            panic!("Expected a constant formatting string");
272        }
273    }
274}
275
276/// A builder for `Table` values based on various line definitions.
277#[derive(PartialEq, Debug)]
278pub struct TableBuilder {
279    /// The table that’s being built up.
280    table: Table,
281
282    /// If the last line was a zone definition, then this holds its name.
283    /// `None` otherwise. This is so continuation lines can be added to the
284    /// same zone as the original zone line.
285    current_zoneset_name: Option<String>,
286}
287
288impl Default for TableBuilder {
289    fn default() -> Self {
290        Self::new()
291    }
292}
293
294impl TableBuilder {
295    /// Creates a new builder with an empty table.
296    pub fn new() -> TableBuilder {
297        TableBuilder {
298            table: Table::default(),
299            current_zoneset_name: None,
300        }
301    }
302
303    /// Adds a new line describing a zone definition.
304    ///
305    /// Returns an error if there’s already a zone with the same name, or the
306    /// zone refers to a ruleset that hasn’t been defined yet.
307    pub fn add_zone_line<'line>(
308        &mut self,
309        zone_line: line::Zone<'line>,
310    ) -> Result<(), Error<'line>> {
311        if let line::Saving::Multiple(ruleset_name) = zone_line.info.saving {
312            if !self.table.rulesets.contains_key(ruleset_name) {
313                return Err(Error::UnknownRuleset(ruleset_name));
314            }
315        }
316
317        let zoneset = match self.table.zonesets.entry(zone_line.name.to_owned()) {
318            Entry::Occupied(_) => return Err(Error::DuplicateZone),
319            Entry::Vacant(e) => e.insert(Vec::new()),
320        };
321
322        zoneset.push(zone_line.info.into());
323        self.current_zoneset_name = Some(zone_line.name.to_owned());
324        Ok(())
325    }
326
327    /// Adds a new line describing the *continuation* of a zone definition.
328    ///
329    /// Returns an error if the builder wasn’t expecting a continuation line
330    /// (meaning, the previous line wasn’t a zone line)
331    pub fn add_continuation_line(
332        &mut self,
333        continuation_line: line::ZoneInfo,
334    ) -> Result<(), Error> {
335        let zoneset = match self.current_zoneset_name {
336            Some(ref name) => self.table.zonesets.get_mut(name).unwrap(),
337            None => return Err(Error::SurpriseContinuationLine),
338        };
339
340        zoneset.push(continuation_line.into());
341        Ok(())
342    }
343
344    /// Adds a new line describing one entry in a ruleset, creating that set
345    /// if it didn’t exist already.
346    pub fn add_rule_line(&mut self, rule_line: line::Rule) -> Result<(), Error> {
347        let ruleset = self
348            .table
349            .rulesets
350            .entry(rule_line.name.to_owned())
351            .or_default();
352
353        ruleset.push(rule_line.into());
354        self.current_zoneset_name = None;
355        Ok(())
356    }
357
358    /// Adds a new line linking one zone to another.
359    ///
360    /// Returns an error if there was already a link with that name.
361    pub fn add_link_line<'line>(
362        &mut self,
363        link_line: line::Link<'line>,
364    ) -> Result<(), Error<'line>> {
365        match self.table.links.entry(link_line.new.to_owned()) {
366            Entry::Occupied(_) => Err(Error::DuplicateLink(link_line.new)),
367            Entry::Vacant(e) => {
368                let _ = e.insert(link_line.existing.to_owned());
369                self.current_zoneset_name = None;
370                Ok(())
371            }
372        }
373    }
374
375    /// Returns the table after it’s finished being built.
376    pub fn build(self) -> Table {
377        self.table
378    }
379}
380
381/// Something that can go wrong while constructing a `Table`.
382#[derive(PartialEq, Debug, Copy, Clone)]
383pub enum Error<'line> {
384    /// A continuation line was passed in, but the previous line wasn’t a zone
385    /// definition line.
386    SurpriseContinuationLine,
387
388    /// A zone definition referred to a ruleset that hadn’t been defined.
389    UnknownRuleset(&'line str),
390
391    /// A link line was passed in, but there’s already a link with that name.
392    DuplicateLink(&'line str),
393
394    /// A zone line was passed in, but there’s already a zone with that name.
395    DuplicateZone,
396}
397
398impl<'line> fmt::Display for Error<'line> {
399    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
400        match self {
401            Error::SurpriseContinuationLine => {
402                write!(
403                    f,
404                    "continuation line follows line that isn't a zone definition line"
405                )
406            }
407            Error::UnknownRuleset(_) => {
408                write!(f, "zone definition refers to a ruleset that isn't defined")
409            }
410            Error::DuplicateLink(_) => write!(f, "link line with name that already exists"),
411            Error::DuplicateZone => write!(f, "zone line with name that already exists"),
412        }
413    }
414}
415
416impl<'line> std::error::Error for Error<'line> {}