| 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 | |