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: &String = &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: i64), None) => year == from,
136 (Number(from: i64), Some(Maximum)) => year >= from,
137 (Number(from: i64), Some(Number(to: i64))) => 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: i64 = 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 = 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: &str) => Saving::Multiple(s.to_owned()),
186 line::Saving::OneOff(t: TimeSpec) => Saving::OneOff(t.as_seconds()),
187 },
188 format: Format::new(template: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> {}
417