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 | |
47 | use std::collections::hash_map::{Entry, HashMap}; |
48 | use std::fmt; |
49 | |
50 | use 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)] |
54 | pub 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 | |
65 | impl 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)] |
86 | pub 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 | |
114 | impl<'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 | |
129 | impl 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)] |
162 | pub 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 | |
179 | impl<'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)] |
200 | pub 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)] |
216 | pub 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 | |
236 | impl 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)] |
278 | pub 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 | |
288 | impl Default for TableBuilder { |
289 | fn default() -> Self { |
290 | Self::new() |
291 | } |
292 | } |
293 | |
294 | impl 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)] |
383 | pub 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 | |
398 | impl<'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 | |
416 | impl<'line> std::error::Error for Error<'line> {} |
417 | |