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(*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(
345                            timespan
346                                .format
347                                .format(*dst_offset, 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(
356                            timespan
357                                .format
358                                .format(*dst_offset, 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
369                            .format
370                            .format(earliest_rule.time_to_add, earliest_rule.letters.as_ref()),
371                    },
372                );
373                self.rest.push(t);
374            }
375        }
376    }
377
378    fn build(mut self) -> FixedTimespanSet {
379        self.rest.sort_by(|a, b| a.0.cmp(&b.0));
380
381        let first = match self.first {
382            Some(ft) => ft,
383            None => self
384                .rest
385                .iter()
386                .find(|t| t.1.dst_offset == 0)
387                .unwrap()
388                .1
389                .clone(),
390        };
391
392        let mut zoneset = FixedTimespanSet {
393            first,
394            rest: self.rest,
395        };
396        optimise(&mut zoneset);
397        zoneset
398    }
399}
400
401#[allow(unused_results)] // for remove
402fn optimise(transitions: &mut FixedTimespanSet) {
403    let mut from_i = 0;
404    let mut to_i = 0;
405
406    while from_i < transitions.rest.len() {
407        if to_i > 1 {
408            let from = transitions.rest[from_i].0;
409            let to = transitions.rest[to_i - 1].0;
410            if from + transitions.rest[to_i - 1].1.total_offset()
411                <= to + transitions.rest[to_i - 2].1.total_offset()
412            {
413                transitions.rest[to_i - 1].1 = transitions.rest[from_i].1.clone();
414                from_i += 1;
415                continue;
416            }
417        }
418
419        if to_i == 0 || transitions.rest[to_i - 1].1 != transitions.rest[from_i].1 {
420            transitions.rest[to_i] = transitions.rest[from_i].clone();
421            to_i += 1;
422        }
423
424        from_i += 1
425    }
426
427    transitions.rest.truncate(to_i);
428
429    if !transitions.rest.is_empty() && transitions.first == transitions.rest[0].1 {
430        transitions.rest.remove(0);
431    }
432}
433
434#[cfg(test)]
435mod test {
436    use super::optimise;
437    use super::*;
438
439    // Allow unused results in test code, because the only ‘results’ that
440    // we need to ignore are the ones from inserting and removing from
441    // tables and vectors. And as we set them up ourselves, they’re bound
442    // to be correct, otherwise the tests would fail!
443    #[test]
444    #[allow(unused_results)]
445    fn optimise_macquarie() {
446        let mut transitions = FixedTimespanSet {
447            first: FixedTimespan {
448                utc_offset: 0,
449                dst_offset: 0,
450                name: "zzz".to_owned(),
451            },
452            rest: vec![
453                (
454                    -2_214_259_200,
455                    FixedTimespan {
456                        utc_offset: 36000,
457                        dst_offset: 0,
458                        name: "AEST".to_owned(),
459                    },
460                ),
461                (
462                    -1_680_508_800,
463                    FixedTimespan {
464                        utc_offset: 36000,
465                        dst_offset: 3600,
466                        name: "AEDT".to_owned(),
467                    },
468                ),
469                (
470                    -1_669_892_400,
471                    FixedTimespan {
472                        utc_offset: 36000,
473                        dst_offset: 3600,
474                        name: "AEDT".to_owned(),
475                    },
476                ), // gets removed
477                (
478                    -1_665_392_400,
479                    FixedTimespan {
480                        utc_offset: 36000,
481                        dst_offset: 0,
482                        name: "AEST".to_owned(),
483                    },
484                ),
485                (
486                    -1_601_719_200,
487                    FixedTimespan {
488                        utc_offset: 0,
489                        dst_offset: 0,
490                        name: "zzz".to_owned(),
491                    },
492                ),
493                (
494                    -687_052_800,
495                    FixedTimespan {
496                        utc_offset: 36000,
497                        dst_offset: 0,
498                        name: "AEST".to_owned(),
499                    },
500                ),
501                (
502                    -94_730_400,
503                    FixedTimespan {
504                        utc_offset: 36000,
505                        dst_offset: 0,
506                        name: "AEST".to_owned(),
507                    },
508                ), // also gets removed
509                (
510                    -71_136_000,
511                    FixedTimespan {
512                        utc_offset: 36000,
513                        dst_offset: 3600,
514                        name: "AEDT".to_owned(),
515                    },
516                ),
517                (
518                    -55_411_200,
519                    FixedTimespan {
520                        utc_offset: 36000,
521                        dst_offset: 0,
522                        name: "AEST".to_owned(),
523                    },
524                ),
525                (
526                    -37_267_200,
527                    FixedTimespan {
528                        utc_offset: 36000,
529                        dst_offset: 3600,
530                        name: "AEDT".to_owned(),
531                    },
532                ),
533                (
534                    -25_776_000,
535                    FixedTimespan {
536                        utc_offset: 36000,
537                        dst_offset: 0,
538                        name: "AEST".to_owned(),
539                    },
540                ),
541                (
542                    -5_817_600,
543                    FixedTimespan {
544                        utc_offset: 36000,
545                        dst_offset: 3600,
546                        name: "AEDT".to_owned(),
547                    },
548                ),
549            ],
550        };
551
552        let mut result = transitions.clone();
553        result.rest.remove(6);
554        result.rest.remove(2);
555
556        optimise(&mut transitions);
557        assert_eq!(transitions, result);
558    }
559}