| 1 | //! Generating timespan sets from a built Table. |
| 2 | //! |
| 3 | //! Once a table has been fully built, it needs to be turned into several |
| 4 | //! *fixed timespan sets*: a series of spans of time where the local time |
| 5 | //! offset remains the same throughout. One set is generated for each named |
| 6 | //! time zone. These timespan sets can then be iterated over to produce |
| 7 | //! *transitions*: when the local time changes from one offset to another. |
| 8 | //! |
| 9 | //! These sets are returned as `FixedTimespanSet` values, rather than |
| 10 | //! iterators, because the generation logic does not output the timespans |
| 11 | //! in any particular order, meaning they need to be sorted before they’re |
| 12 | //! returned—so we may as well just return the vector, rather than an |
| 13 | //! iterator over the vector. |
| 14 | //! |
| 15 | //! Similarly, there is a fixed set of years that is iterated over |
| 16 | //! (currently 1800..2100), rather than having an iterator that produces |
| 17 | //! timespans indefinitely. Not only do we need a complete set of timespans |
| 18 | //! for sorting, but it is not necessarily advisable to rely on offset |
| 19 | //! changes so far into the future! |
| 20 | //! |
| 21 | //! ### Example |
| 22 | //! |
| 23 | //! The complete definition of the `Indian/Mauritius` time zone, as |
| 24 | //! specified in the `africa` file in my version of the tz database, has |
| 25 | //! two Zone definitions, one of which refers to four Rule definitions: |
| 26 | //! |
| 27 | //! ```tz |
| 28 | //! # Rule NAME FROM TO TYPE IN ON AT SAVE LETTER/S |
| 29 | //! Rule Mauritius 1982 only - Oct 10 0:00 1:00 S |
| 30 | //! Rule Mauritius 1983 only - Mar 21 0:00 0 - |
| 31 | //! Rule Mauritius 2008 only - Oct lastSun 2:00 1:00 S |
| 32 | //! Rule Mauritius 2009 only - Mar lastSun 2:00 0 - |
| 33 | //! |
| 34 | //! # Zone NAME GMTOFF RULES FORMAT [UNTIL] |
| 35 | //! Zone Indian/Mauritius 3:50:00 - LMT 1907 # Port Louis |
| 36 | //! 4:00 Mauritius MU%sT # Mauritius Time |
| 37 | //! ``` |
| 38 | //! |
| 39 | //! To generate a fixed timespan set for this timezone, we examine each of the |
| 40 | //! Zone definitions, generating at least one timespan for each definition. |
| 41 | //! |
| 42 | //! * The first timespan describes the *local mean time* (LMT) in Mauritius, |
| 43 | //! calculated by the geographical position of Port Louis, its capital. |
| 44 | //! Although it’s common to have a timespan set begin with a city’s local mean |
| 45 | //! time, it is by no means necessary. This timespan has a fixed offset of |
| 46 | //! three hours and fifty minutes ahead of UTC, and lasts until the beginning |
| 47 | //! of 1907, at which point the second timespan kicks in. |
| 48 | //! * The second timespan has no ‘until’ date, so it’s in effect indefinitely. |
| 49 | //! Instead of having a fixed offset, it refers to the set of rules under the |
| 50 | //! name “Mauritius”, which we’ll have to consult to compute the timespans. |
| 51 | //! * The first two rules refer to a summer time transition that began on |
| 52 | //! the 10th of October 1982, and lasted until the 21st of March 1983. But |
| 53 | //! before we get onto that, we need to add a timespan beginning at the |
| 54 | //! time the last one ended (1907), up until the point Summer Time kicks |
| 55 | //! in (1982), reflecting that it was four hours ahead of UTC. |
| 56 | //! * After this, we add another timespan for Summer Time, when Mauritius |
| 57 | //! was an extra hour ahead, bringing the total offset for that time to |
| 58 | //! *five* hours. |
| 59 | //! * The next (and last) two rules refer to another summer time |
| 60 | //! transition from the last Sunday of October 2008 to the last Sunday of |
| 61 | //! March 2009, this time at 2am local time instead of midnight. But, as |
| 62 | //! before, we need to add a *standard* time timespan beginning at the |
| 63 | //! time Summer Time ended (1983) up until the point the next span of |
| 64 | //! Summer Time kicks in (2008), again reflecting that it was four hours |
| 65 | //! ahead of UTC again. |
| 66 | //! * Next, we add the Summer Time timespan, again bringing the total |
| 67 | //! offset to five hours. We need to calculate when the last Sundays of |
| 68 | //! the months are to get the dates correct. |
| 69 | //! * Finally, we add one last standard time timespan, lasting from 2009 |
| 70 | //! indefinitely, as the Mauritian authorities decided not to change to |
| 71 | //! Summer Time again. |
| 72 | //! |
| 73 | //! All this calculation results in the following six timespans to be added: |
| 74 | //! |
| 75 | //! | Timespan start | Abbreviation | UTC offset | DST? | |
| 76 | //! |:--------------------------|:-------------|:-------------------|:-----| |
| 77 | //! | *no start* | LMT | 3 hours 50 minutes | No | |
| 78 | //! | 1906-11-31 T 20:10:00 UTC | MUT | 4 hours | No | |
| 79 | //! | 1982-09-09 T 20:00:00 UTC | MUST | 5 hours | Yes | |
| 80 | //! | 1983-02-20 T 19:00:00 UTC | MUT | 4 hours | No | |
| 81 | //! | 2008-09-25 T 22:00:00 UTC | MUST | 5 hours | Yes | |
| 82 | //! | 2009-02-28 T 21:00:00 UTC | MUT | 4 hours | No | |
| 83 | //! |
| 84 | //! There are a few final things of note: |
| 85 | //! |
| 86 | //! Firstly, this library records the times that timespans *begin*, while |
| 87 | //! the tz data files record the times that timespans *end*. Pay attention to |
| 88 | //! this if the timestamps aren’t where you expect them to be! For example, in |
| 89 | //! the data file, the first zone rule has an ‘until’ date and the second has |
| 90 | //! none, whereas in the list of timespans, the last timespan has a ‘start’ |
| 91 | //! date and the *first* has none. |
| 92 | //! |
| 93 | //! Secondly, although local mean time in Mauritius lasted until 1907, the |
| 94 | //! timespan is recorded as ending in 1906! Why is this? It’s because the |
| 95 | //! transition occurred at midnight *at the local time*, which in this case, |
| 96 | //! was three hours fifty minutes ahead of UTC. So that time has to be |
| 97 | //! *subtracted* from the date, resulting in twenty hours and ten minutes on |
| 98 | //! the last day of the year. Similar things happen on the rest of the |
| 99 | //! transitions, being either four or five hours ahead of UTC. |
| 100 | //! |
| 101 | //! The logic in this file is based off of `zic.c`, which comes with the |
| 102 | //! zoneinfo files and is in the public domain. |
| 103 | |
| 104 | use crate::table::{RuleInfo, Saving, Table, ZoneInfo}; |
| 105 | |
| 106 | /// A set of timespans, separated by the instances at which the timespans |
| 107 | /// change over. There will always be one more timespan than transitions. |
| 108 | /// |
| 109 | /// This mimics the `FixedTimespanSet` struct in `datetime::cal::zone`, |
| 110 | /// except it uses owned `Vec`s instead of slices. |
| 111 | #[derive (PartialEq, Debug, Clone)] |
| 112 | pub struct FixedTimespanSet { |
| 113 | /// The first timespan, which is assumed to have been in effect up until |
| 114 | /// the initial transition instant (if any). Each set has to have at |
| 115 | /// least one timespan. |
| 116 | pub first: FixedTimespan, |
| 117 | |
| 118 | /// The rest of the timespans, as a vector of tuples, each containing: |
| 119 | /// |
| 120 | /// 1. A transition instant at which the previous timespan ends and the |
| 121 | /// next one begins, stored as a Unix timestamp; |
| 122 | /// 2. The actual timespan to transition into. |
| 123 | pub rest: Vec<(i64, FixedTimespan)>, |
| 124 | } |
| 125 | |
| 126 | /// An individual timespan with a fixed offset. |
| 127 | /// |
| 128 | /// This mimics the `FixedTimespan` struct in `datetime::cal::zone`, except |
| 129 | /// instead of “total offset” and “is DST” fields, it has separate UTC and |
| 130 | /// DST fields. Also, the name is an owned `String` here instead of a slice. |
| 131 | #[derive (PartialEq, Debug, Clone)] |
| 132 | pub struct FixedTimespan { |
| 133 | /// The number of seconds offset from UTC during this timespan. |
| 134 | pub utc_offset: i64, |
| 135 | |
| 136 | /// The number of *extra* daylight-saving seconds during this timespan. |
| 137 | pub dst_offset: i64, |
| 138 | |
| 139 | /// The abbreviation in use during this timespan. |
| 140 | pub name: String, |
| 141 | } |
| 142 | |
| 143 | impl FixedTimespan { |
| 144 | /// The total offset in effect during this timespan. |
| 145 | pub fn total_offset(&self) -> i64 { |
| 146 | self.utc_offset + self.dst_offset |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | /// Trait to put the `timespans` method on Tables. |
| 151 | pub trait TableTransitions { |
| 152 | /// Computes a fixed timespan set for the timezone with the given name. |
| 153 | /// Returns `None` if the table doesn’t contain a time zone with that name. |
| 154 | fn timespans(&self, zone_name: &str) -> Option<FixedTimespanSet>; |
| 155 | } |
| 156 | |
| 157 | impl TableTransitions for Table { |
| 158 | fn timespans(&self, zone_name: &str) -> Option<FixedTimespanSet> { |
| 159 | let mut builder = FixedTimespanSetBuilder::default(); |
| 160 | |
| 161 | let zoneset = match self.get_zoneset(zone_name) { |
| 162 | Some(zones) => zones, |
| 163 | None => return None, |
| 164 | }; |
| 165 | |
| 166 | for (i, zone_info) in zoneset.iter().enumerate() { |
| 167 | let mut dst_offset = 0; |
| 168 | let use_until = i != zoneset.len() - 1; |
| 169 | let utc_offset = zone_info.offset; |
| 170 | |
| 171 | let mut insert_start_transition = i > 0; |
| 172 | let mut start_zone_id = None; |
| 173 | let mut start_utc_offset = zone_info.offset; |
| 174 | let mut start_dst_offset = 0; |
| 175 | |
| 176 | match zone_info.saving { |
| 177 | Saving::NoSaving => { |
| 178 | builder.add_fixed_saving( |
| 179 | zone_info, |
| 180 | 0, |
| 181 | &mut dst_offset, |
| 182 | utc_offset, |
| 183 | &mut insert_start_transition, |
| 184 | &mut start_zone_id, |
| 185 | ); |
| 186 | } |
| 187 | |
| 188 | Saving::OneOff(amount) => { |
| 189 | builder.add_fixed_saving( |
| 190 | zone_info, |
| 191 | amount, |
| 192 | &mut dst_offset, |
| 193 | utc_offset, |
| 194 | &mut insert_start_transition, |
| 195 | &mut start_zone_id, |
| 196 | ); |
| 197 | } |
| 198 | |
| 199 | Saving::Multiple(ref rules) => { |
| 200 | let rules = &self.rulesets[rules]; |
| 201 | builder.add_multiple_saving( |
| 202 | zone_info, |
| 203 | rules, |
| 204 | &mut dst_offset, |
| 205 | use_until, |
| 206 | utc_offset, |
| 207 | &mut insert_start_transition, |
| 208 | &mut start_zone_id, |
| 209 | &mut start_utc_offset, |
| 210 | &mut start_dst_offset, |
| 211 | ); |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | if insert_start_transition && start_zone_id.is_some() { |
| 216 | let t = ( |
| 217 | builder.start_time.expect("Start time" ), |
| 218 | FixedTimespan { |
| 219 | utc_offset: start_utc_offset, |
| 220 | dst_offset: start_dst_offset, |
| 221 | name: start_zone_id.clone().expect("Start zone ID" ), |
| 222 | }, |
| 223 | ); |
| 224 | builder.rest.push(t); |
| 225 | } |
| 226 | |
| 227 | if use_until { |
| 228 | builder.start_time = Some( |
| 229 | zone_info.end_time.expect("End time" ).to_timestamp() - utc_offset - dst_offset, |
| 230 | ); |
| 231 | } |
| 232 | } |
| 233 | |
| 234 | Some(builder.build()) |
| 235 | } |
| 236 | } |
| 237 | |
| 238 | #[derive (Debug, Default)] |
| 239 | struct FixedTimespanSetBuilder { |
| 240 | first: Option<FixedTimespan>, |
| 241 | rest: Vec<(i64, FixedTimespan)>, |
| 242 | |
| 243 | start_time: Option<i64>, |
| 244 | until_time: Option<i64>, |
| 245 | } |
| 246 | |
| 247 | impl FixedTimespanSetBuilder { |
| 248 | fn add_fixed_saving( |
| 249 | &mut self, |
| 250 | timespan: &ZoneInfo, |
| 251 | amount: i64, |
| 252 | dst_offset: &mut i64, |
| 253 | utc_offset: i64, |
| 254 | insert_start_transition: &mut bool, |
| 255 | start_zone_id: &mut Option<String>, |
| 256 | ) { |
| 257 | *dst_offset = amount; |
| 258 | *start_zone_id = Some(timespan.format.format(*dst_offset, None)); |
| 259 | |
| 260 | if *insert_start_transition { |
| 261 | let time = self.start_time.unwrap(); |
| 262 | let timespan = FixedTimespan { |
| 263 | utc_offset: timespan.offset, |
| 264 | dst_offset: *dst_offset, |
| 265 | name: start_zone_id.clone().unwrap_or_default(), |
| 266 | }; |
| 267 | |
| 268 | self.rest.push((time, timespan)); |
| 269 | *insert_start_transition = false; |
| 270 | } else { |
| 271 | self.first = Some(FixedTimespan { |
| 272 | utc_offset, |
| 273 | dst_offset: *dst_offset, |
| 274 | name: start_zone_id.clone().unwrap_or_default(), |
| 275 | }); |
| 276 | } |
| 277 | } |
| 278 | |
| 279 | #[allow (unused_results)] |
| 280 | #[allow (clippy::too_many_arguments)] |
| 281 | fn add_multiple_saving( |
| 282 | &mut self, |
| 283 | timespan: &ZoneInfo, |
| 284 | rules: &[RuleInfo], |
| 285 | dst_offset: &mut i64, |
| 286 | use_until: bool, |
| 287 | utc_offset: i64, |
| 288 | insert_start_transition: &mut bool, |
| 289 | start_zone_id: &mut Option<String>, |
| 290 | start_utc_offset: &mut i64, |
| 291 | start_dst_offset: &mut i64, |
| 292 | ) { |
| 293 | use std::mem::replace; |
| 294 | |
| 295 | for year in 1800..2100 { |
| 296 | if use_until && year > timespan.end_time.unwrap().year() { |
| 297 | break; |
| 298 | } |
| 299 | |
| 300 | let mut activated_rules = rules |
| 301 | .iter() |
| 302 | .filter(|r| r.applies_to_year(year)) |
| 303 | .collect::<Vec<_>>(); |
| 304 | |
| 305 | loop { |
| 306 | if use_until { |
| 307 | self.until_time = |
| 308 | Some(timespan.end_time.unwrap().to_timestamp() - utc_offset - *dst_offset); |
| 309 | } |
| 310 | |
| 311 | // Find the minimum rule and its start time based on the current |
| 312 | // UTC and DST offsets. |
| 313 | let earliest = activated_rules |
| 314 | .iter() |
| 315 | .enumerate() |
| 316 | .map(|(i, r)| (i, r.absolute_datetime(year, utc_offset, *dst_offset))) |
| 317 | .min_by_key(|&(_, time)| time); |
| 318 | |
| 319 | let (pos, earliest_at) = match earliest { |
| 320 | Some((pos, time)) => (pos, time), |
| 321 | None => break, |
| 322 | }; |
| 323 | |
| 324 | let earliest_rule = activated_rules.remove(pos); |
| 325 | |
| 326 | if use_until && earliest_at >= self.until_time.unwrap() { |
| 327 | break; |
| 328 | } |
| 329 | |
| 330 | *dst_offset = earliest_rule.time_to_add; |
| 331 | |
| 332 | if *insert_start_transition && earliest_at == self.start_time.unwrap() { |
| 333 | *insert_start_transition = false; |
| 334 | } |
| 335 | |
| 336 | if *insert_start_transition { |
| 337 | if earliest_at < self.start_time.unwrap() { |
| 338 | let _ = replace(start_utc_offset, timespan.offset); |
| 339 | let _ = replace(start_dst_offset, *dst_offset); |
| 340 | let _ = replace( |
| 341 | start_zone_id, |
| 342 | Some( |
| 343 | timespan |
| 344 | .format |
| 345 | .format(*dst_offset, earliest_rule.letters.as_ref()), |
| 346 | ), |
| 347 | ); |
| 348 | continue; |
| 349 | } |
| 350 | |
| 351 | if start_zone_id.is_none() |
| 352 | && *start_utc_offset + *start_dst_offset == timespan.offset + *dst_offset |
| 353 | { |
| 354 | let _ = replace( |
| 355 | start_zone_id, |
| 356 | Some( |
| 357 | timespan |
| 358 | .format |
| 359 | .format(*dst_offset, earliest_rule.letters.as_ref()), |
| 360 | ), |
| 361 | ); |
| 362 | } |
| 363 | } |
| 364 | |
| 365 | let t = ( |
| 366 | earliest_at, |
| 367 | FixedTimespan { |
| 368 | utc_offset: timespan.offset, |
| 369 | dst_offset: earliest_rule.time_to_add, |
| 370 | name: timespan |
| 371 | .format |
| 372 | .format(earliest_rule.time_to_add, earliest_rule.letters.as_ref()), |
| 373 | }, |
| 374 | ); |
| 375 | self.rest.push(t); |
| 376 | } |
| 377 | } |
| 378 | } |
| 379 | |
| 380 | fn build(mut self) -> FixedTimespanSet { |
| 381 | self.rest.sort_by(|a, b| a.0.cmp(&b.0)); |
| 382 | |
| 383 | let first = match self.first { |
| 384 | Some(ft) => ft, |
| 385 | None => self |
| 386 | .rest |
| 387 | .iter() |
| 388 | .find(|t| t.1.dst_offset == 0) |
| 389 | .unwrap() |
| 390 | .1 |
| 391 | .clone(), |
| 392 | }; |
| 393 | |
| 394 | let mut zoneset = FixedTimespanSet { |
| 395 | first, |
| 396 | rest: self.rest, |
| 397 | }; |
| 398 | optimise(&mut zoneset); |
| 399 | zoneset |
| 400 | } |
| 401 | } |
| 402 | |
| 403 | #[allow (unused_results)] // for remove |
| 404 | fn optimise(transitions: &mut FixedTimespanSet) { |
| 405 | let mut from_i = 0; |
| 406 | let mut to_i = 0; |
| 407 | |
| 408 | while from_i < transitions.rest.len() { |
| 409 | if to_i > 1 { |
| 410 | let from = transitions.rest[from_i].0; |
| 411 | let to = transitions.rest[to_i - 1].0; |
| 412 | if from + transitions.rest[to_i - 1].1.total_offset() |
| 413 | <= to + transitions.rest[to_i - 2].1.total_offset() |
| 414 | { |
| 415 | transitions.rest[to_i - 1].1 = transitions.rest[from_i].1.clone(); |
| 416 | from_i += 1; |
| 417 | continue; |
| 418 | } |
| 419 | } |
| 420 | |
| 421 | if to_i == 0 || transitions.rest[to_i - 1].1 != transitions.rest[from_i].1 { |
| 422 | transitions.rest[to_i] = transitions.rest[from_i].clone(); |
| 423 | to_i += 1; |
| 424 | } |
| 425 | |
| 426 | from_i += 1 |
| 427 | } |
| 428 | |
| 429 | transitions.rest.truncate(to_i); |
| 430 | |
| 431 | if !transitions.rest.is_empty() && transitions.first == transitions.rest[0].1 { |
| 432 | transitions.rest.remove(0); |
| 433 | } |
| 434 | } |
| 435 | |
| 436 | #[cfg (test)] |
| 437 | mod test { |
| 438 | use super::optimise; |
| 439 | use super::*; |
| 440 | |
| 441 | // Allow unused results in test code, because the only ‘results’ that |
| 442 | // we need to ignore are the ones from inserting and removing from |
| 443 | // tables and vectors. And as we set them up ourselves, they’re bound |
| 444 | // to be correct, otherwise the tests would fail! |
| 445 | #[test ] |
| 446 | #[allow (unused_results)] |
| 447 | fn optimise_macquarie() { |
| 448 | let mut transitions = FixedTimespanSet { |
| 449 | first: FixedTimespan { |
| 450 | utc_offset: 0, |
| 451 | dst_offset: 0, |
| 452 | name: "zzz" .to_owned(), |
| 453 | }, |
| 454 | rest: vec![ |
| 455 | ( |
| 456 | -2_214_259_200, |
| 457 | FixedTimespan { |
| 458 | utc_offset: 36000, |
| 459 | dst_offset: 0, |
| 460 | name: "AEST" .to_owned(), |
| 461 | }, |
| 462 | ), |
| 463 | ( |
| 464 | -1_680_508_800, |
| 465 | FixedTimespan { |
| 466 | utc_offset: 36000, |
| 467 | dst_offset: 3600, |
| 468 | name: "AEDT" .to_owned(), |
| 469 | }, |
| 470 | ), |
| 471 | ( |
| 472 | -1_669_892_400, |
| 473 | FixedTimespan { |
| 474 | utc_offset: 36000, |
| 475 | dst_offset: 3600, |
| 476 | name: "AEDT" .to_owned(), |
| 477 | }, |
| 478 | ), // gets removed |
| 479 | ( |
| 480 | -1_665_392_400, |
| 481 | FixedTimespan { |
| 482 | utc_offset: 36000, |
| 483 | dst_offset: 0, |
| 484 | name: "AEST" .to_owned(), |
| 485 | }, |
| 486 | ), |
| 487 | ( |
| 488 | -1_601_719_200, |
| 489 | FixedTimespan { |
| 490 | utc_offset: 0, |
| 491 | dst_offset: 0, |
| 492 | name: "zzz" .to_owned(), |
| 493 | }, |
| 494 | ), |
| 495 | ( |
| 496 | -687_052_800, |
| 497 | FixedTimespan { |
| 498 | utc_offset: 36000, |
| 499 | dst_offset: 0, |
| 500 | name: "AEST" .to_owned(), |
| 501 | }, |
| 502 | ), |
| 503 | ( |
| 504 | -94_730_400, |
| 505 | FixedTimespan { |
| 506 | utc_offset: 36000, |
| 507 | dst_offset: 0, |
| 508 | name: "AEST" .to_owned(), |
| 509 | }, |
| 510 | ), // also gets removed |
| 511 | ( |
| 512 | -71_136_000, |
| 513 | FixedTimespan { |
| 514 | utc_offset: 36000, |
| 515 | dst_offset: 3600, |
| 516 | name: "AEDT" .to_owned(), |
| 517 | }, |
| 518 | ), |
| 519 | ( |
| 520 | -55_411_200, |
| 521 | FixedTimespan { |
| 522 | utc_offset: 36000, |
| 523 | dst_offset: 0, |
| 524 | name: "AEST" .to_owned(), |
| 525 | }, |
| 526 | ), |
| 527 | ( |
| 528 | -37_267_200, |
| 529 | FixedTimespan { |
| 530 | utc_offset: 36000, |
| 531 | dst_offset: 3600, |
| 532 | name: "AEDT" .to_owned(), |
| 533 | }, |
| 534 | ), |
| 535 | ( |
| 536 | -25_776_000, |
| 537 | FixedTimespan { |
| 538 | utc_offset: 36000, |
| 539 | dst_offset: 0, |
| 540 | name: "AEST" .to_owned(), |
| 541 | }, |
| 542 | ), |
| 543 | ( |
| 544 | -5_817_600, |
| 545 | FixedTimespan { |
| 546 | utc_offset: 36000, |
| 547 | dst_offset: 3600, |
| 548 | name: "AEDT" .to_owned(), |
| 549 | }, |
| 550 | ), |
| 551 | ], |
| 552 | }; |
| 553 | |
| 554 | let mut result = transitions.clone(); |
| 555 | result.rest.remove(6); |
| 556 | result.rest.remove(2); |
| 557 | |
| 558 | optimise(&mut transitions); |
| 559 | assert_eq!(transitions, result); |
| 560 | } |
| 561 | } |
| 562 | |