parse_zoneinfo/
transitions.rs

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
104use 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)]
112pub 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)]
132pub 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
143impl 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.
151pub 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
157impl TableTransitions for Table {
158    fn timespans(&self, zone_name: &str) -> Option<FixedTimespanSet> {
159        let mut builder = FixedTimespanSetBuilder::default();
160
161        let zoneset = self.get_zoneset(zone_name)?;
162
163        for (i, zone_info) in zoneset.iter().enumerate() {
164            let mut dst_offset = 0;
165            let use_until = i != zoneset.len() - 1;
166            let utc_offset = zone_info.offset;
167
168            let mut insert_start_transition = i > 0;
169            let mut start_zone_id = None;
170            let mut start_utc_offset = zone_info.offset;
171            let mut start_dst_offset = 0;
172
173            match zone_info.saving {
174                Saving::NoSaving => {
175                    builder.add_fixed_saving(
176                        zone_info,
177                        0,
178                        &mut dst_offset,
179                        utc_offset,
180                        &mut insert_start_transition,
181                        &mut start_zone_id,
182                    );
183                }
184
185                Saving::OneOff(amount) => {
186                    builder.add_fixed_saving(
187                        zone_info,
188                        amount,
189                        &mut dst_offset,
190                        utc_offset,
191                        &mut insert_start_transition,
192                        &mut start_zone_id,
193                    );
194                }
195
196                Saving::Multiple(ref rules) => {
197                    let rules = &self.rulesets[rules];
198                    builder.add_multiple_saving(
199                        zone_info,
200                        rules,
201                        &mut dst_offset,
202                        use_until,
203                        utc_offset,
204                        &mut insert_start_transition,
205                        &mut start_zone_id,
206                        &mut start_utc_offset,
207                        &mut start_dst_offset,
208                    );
209                }
210            }
211
212            if insert_start_transition && start_zone_id.is_some() {
213                let t = (
214                    builder.start_time.expect("Start time"),
215                    FixedTimespan {
216                        utc_offset: start_utc_offset,
217                        dst_offset: start_dst_offset,
218                        name: start_zone_id.clone().expect("Start zone ID"),
219                    },
220                );
221                builder.rest.push(t);
222            }
223
224            if use_until {
225                builder.start_time = Some(
226                    zone_info
227                        .end_time
228                        .expect("End time")
229                        .to_timestamp(utc_offset, dst_offset),
230                );
231            }
232        }
233
234        Some(builder.build())
235    }
236}
237
238#[derive(Debug, Default)]
239struct FixedTimespanSetBuilder {
240    first: Option<FixedTimespan>,
241    rest: Vec<(i64, FixedTimespan)>,
242
243    start_time: Option<i64>,
244    until_time: Option<i64>,
245}
246
247impl 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(utc_offset, *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 = Some(
308                        timespan
309                            .end_time
310                            .unwrap()
311                            .to_timestamp(utc_offset, *dst_offset),
312                    );
313                }
314
315                // Find the minimum rule and its start time based on the current
316                // UTC and DST offsets.
317                let earliest = activated_rules
318                    .iter()
319                    .enumerate()
320                    .map(|(i, r)| (i, r.absolute_datetime(year, utc_offset, *dst_offset)))
321                    .min_by_key(|&(_, time)| time);
322
323                let (pos, earliest_at) = match earliest {
324                    Some((pos, time)) => (pos, time),
325                    None => break,
326                };
327
328                let earliest_rule = activated_rules.remove(pos);
329
330                if use_until && earliest_at >= self.until_time.unwrap() {
331                    break;
332                }
333
334                *dst_offset = earliest_rule.time_to_add;
335
336                if *insert_start_transition && earliest_at == self.start_time.unwrap() {
337                    *insert_start_transition = false;
338                }
339
340                if *insert_start_transition {
341                    if earliest_at < self.start_time.unwrap() {
342                        let _ = replace(start_utc_offset, timespan.offset);
343                        let _ = replace(start_dst_offset, *dst_offset);
344                        let _ = start_zone_id.replace(timespan.format.format(
345                            utc_offset,
346                            *dst_offset,
347                            earliest_rule.letters.as_ref(),
348                        ));
349                        continue;
350                    }
351
352                    if start_zone_id.is_none()
353                        && *start_utc_offset + *start_dst_offset == timespan.offset + *dst_offset
354                    {
355                        let _ = start_zone_id.replace(timespan.format.format(
356                            utc_offset,
357                            *dst_offset,
358                            earliest_rule.letters.as_ref(),
359                        ));
360                    }
361                }
362
363                let t = (
364                    earliest_at,
365                    FixedTimespan {
366                        utc_offset: timespan.offset,
367                        dst_offset: earliest_rule.time_to_add,
368                        name: timespan.format.format(
369                            timespan.offset,
370                            earliest_rule.time_to_add,
371                            earliest_rule.letters.as_ref(),
372                        ),
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
404fn 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)]
437mod 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}