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