zoneinfo_parse/
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 table::{Table, Saving, RuleInfo, ZoneInfo};
105use datetime::LocalDateTime;
106
107
108/// A set of timespans, separated by the instances at which the timespans
109/// change over. There will always be one more timespan than transitions.
110///
111/// This mimics the `FixedTimespanSet` struct in `datetime::cal::zone`,
112/// except it uses owned `Vec`s instead of slices.
113#[derive(PartialEq, Debug, Clone)]
114pub struct FixedTimespanSet {
115
116    /// The first timespan, which is assumed to have been in effect up until
117    /// the initial transition instant (if any). Each set has to have at
118    /// least one timespan.
119    pub first: FixedTimespan,
120
121    /// The rest of the timespans, as a vector of tuples, each containing:
122    ///
123    /// 1. A transition instant at which the previous timespan ends and the
124    ///    next one begins, stored as a Unix timestamp;
125    /// 2. The actual timespan to transition into.
126    pub rest: Vec<(i64, FixedTimespan)>,
127}
128
129
130/// An individual timespan with a fixed offset.
131///
132/// This mimics the `FixedTimespan` struct in `datetime::cal::zone`, except
133/// instead of “total offset” and “is DST” fields, it has separate UTC and
134/// DST fields. Also, the name is an owned `String` here instead of a slice.
135#[derive(PartialEq, Debug, Clone)]
136pub struct FixedTimespan {
137
138    /// The number of seconds offset from UTC during this timespan.
139    pub utc_offset: i64,
140
141    /// The number of *extra* daylight-saving seconds during this timespan.
142    pub dst_offset: i64,
143
144    /// The abbreviation in use during this timespan.
145    pub name: String,
146}
147
148impl FixedTimespan {
149
150    /// The total offset in effect during this timespan.
151    pub fn total_offset(&self) -> i64 {
152        self.utc_offset + self.dst_offset
153    }
154}
155
156
157/// Trait to put the `timespans` method on Tables.
158pub trait TableTransitions {
159
160    /// Computes a fixed timespan set for the timezone with the given name.
161    /// Returns `None` if the table doesn’t contain a time zone with that name.
162    fn timespans(&self, zone_name: &str) -> Option<FixedTimespanSet>;
163}
164
165
166impl TableTransitions for Table {
167
168    fn timespans(&self, zone_name: &str) -> Option<FixedTimespanSet> {
169        let mut builder = FixedTimespanSetBuilder::default();
170
171        let zoneset = match self.get_zoneset(zone_name) {
172            Some(zones) => zones,
173            None => return None,
174        };
175
176        for (i, zone_info) in zoneset.iter().enumerate() {
177            let mut dst_offset = 0;
178            let use_until      = i != zoneset.len() - 1;
179            let utc_offset     = zone_info.offset;
180
181            let mut insert_start_transition = i > 0;
182            let mut start_zone_id = None;
183            let mut start_utc_offset = zone_info.offset;
184            let mut start_dst_offset = 0;
185
186            match zone_info.saving {
187                Saving::NoSaving => {
188                    builder.add_fixed_saving(zone_info, 0, &mut dst_offset, utc_offset, &mut insert_start_transition, &mut start_zone_id);
189                },
190
191                Saving::OneOff(amount) => {
192                    builder.add_fixed_saving(zone_info, amount, &mut dst_offset, utc_offset, &mut insert_start_transition, &mut start_zone_id);
193                },
194
195                Saving::Multiple(ref rules) => {
196                    let rules = &self.rulesets[&*rules];
197                    builder.add_multiple_saving(zone_info, &*rules, &mut dst_offset, use_until, utc_offset, &mut insert_start_transition, &mut start_zone_id, &mut start_utc_offset, &mut start_dst_offset);
198                }
199            }
200
201            if insert_start_transition && start_zone_id.is_some() {
202                let t = (builder.start_time.expect("Start time"), FixedTimespan {
203                    utc_offset: start_utc_offset,
204                    dst_offset: start_dst_offset,
205                    name:       start_zone_id.clone().expect("Start zone ID"),
206                });
207                builder.rest.push(t);
208            }
209
210            if use_until {
211                builder.start_time = Some(zone_info.end_time.expect("End time").to_timestamp() - utc_offset - dst_offset);
212            }
213        }
214
215        Some(builder.build())
216    }
217}
218
219#[derive(Debug, Default)]
220struct FixedTimespanSetBuilder {
221    first: Option<FixedTimespan>,
222    rest: Vec<(i64, FixedTimespan)>,
223
224    start_time: Option<i64>,
225    until_time: Option<i64>,
226}
227
228impl FixedTimespanSetBuilder {
229    fn add_fixed_saving(&mut self, timespan: &ZoneInfo, amount: i64,
230            dst_offset: &mut i64, utc_offset: i64, insert_start_transition: &mut bool,
231            start_zone_id: &mut Option<String>)
232    {
233        *dst_offset = amount;
234        *start_zone_id = Some(timespan.format.format(*dst_offset, None));
235
236        if *insert_start_transition {
237            let time = self.start_time.unwrap();
238            let timespan = FixedTimespan {
239                utc_offset: timespan.offset,
240                dst_offset: *dst_offset,
241                name:       start_zone_id.clone().unwrap_or("".to_owned()),
242            };
243
244            self.rest.push((time, timespan));
245            *insert_start_transition = false;
246        }
247        else {
248            self.first = Some(FixedTimespan {
249                utc_offset: utc_offset,
250                dst_offset: *dst_offset,
251                name:       start_zone_id.clone().unwrap_or("".to_owned()),
252            });
253        }
254    }
255
256    #[allow(unused_results)]
257    fn add_multiple_saving(&mut self, timespan: &ZoneInfo, rules: &[RuleInfo],
258            dst_offset: &mut i64, use_until: bool, utc_offset: i64, insert_start_transition: &mut bool,
259            start_zone_id: &mut Option<String>, start_utc_offset: &mut i64, start_dst_offset: &mut i64)
260    {
261        use std::mem::replace;
262        use datetime::DatePiece;
263
264        for year in 1800..2100 {
265            if use_until && year > LocalDateTime::at(timespan.end_time.unwrap().to_timestamp()).year() {
266                break;
267            }
268
269            let mut activated_rules = rules.iter()
270                                           .filter(|r| r.applies_to_year(year))
271                                           .collect::<Vec<_>>();
272
273            loop {
274                if use_until {
275                    self.until_time = Some(timespan.end_time.unwrap().to_timestamp() - utc_offset - *dst_offset);
276                }
277
278                // Find the minimum rule based on the current UTC and DST offsets.
279                // (this can be replaced with min_by when it stabilises):
280                //.min_by(|r| r.1.absolute_datetime(year, utc_offset, dst_offset));
281                let pos = {
282                    let earliest = activated_rules.iter().enumerate()
283                        .map(|(i, r)| (r.absolute_datetime(year, utc_offset, *dst_offset), i))
284                        .min()
285                        .map(|(_, i)| i);
286
287                    match earliest {
288                        Some(p) => p,
289                        None    => break,
290                    }
291                };
292
293                let earliest_rule = activated_rules.remove(pos);
294                let earliest_at = earliest_rule.absolute_datetime(year, utc_offset, *dst_offset).to_instant().seconds();
295
296                if use_until && earliest_at >= self.until_time.unwrap() {
297                    break;
298                }
299
300                *dst_offset = earliest_rule.time_to_add;
301
302                if *insert_start_transition && earliest_at == self.start_time.unwrap() {
303                    *insert_start_transition = false;
304                }
305
306                if *insert_start_transition {
307                    if earliest_at < self.start_time.unwrap() {
308                        replace(start_utc_offset, timespan.offset);
309                        replace(start_dst_offset, *dst_offset);
310                        replace(start_zone_id, Some(timespan.format.format(*dst_offset, earliest_rule.letters.as_ref())));
311                        continue;
312                    }
313
314                    if start_zone_id.is_none() && *start_utc_offset + *start_dst_offset == timespan.offset + *dst_offset {
315                        replace(start_zone_id, Some(timespan.format.format(*dst_offset, earliest_rule.letters.as_ref())));
316                    }
317                }
318
319                let t = (earliest_at, FixedTimespan {
320                    utc_offset: timespan.offset,
321                    dst_offset: earliest_rule.time_to_add,
322                    name:       timespan.format.format(earliest_rule.time_to_add, earliest_rule.letters.as_ref()),
323                });
324                self.rest.push(t);
325            }
326        }
327
328    }
329
330    fn build(mut self) -> FixedTimespanSet {
331        self.rest.sort_by(|a, b| a.0.cmp(&b.0));
332
333        let first = match self.first {
334            Some(ft) => ft,
335            None     => self.rest.iter().find(|t| t.1.dst_offset == 0).unwrap().1.clone(),
336        };
337
338        let mut zoneset = FixedTimespanSet {
339            first: first,
340            rest:  self.rest,
341        };
342        optimise(&mut zoneset);
343        zoneset
344    }
345}
346
347#[allow(unused_results)]  // for remove
348fn optimise(transitions: &mut FixedTimespanSet) {
349    let mut from_i = 0;
350    let mut to_i = 0;
351
352    while from_i < transitions.rest.len() {
353        if to_i > 1 {
354            let from = transitions.rest[from_i].0;
355            let to = transitions.rest[to_i - 1].0;
356            if from + transitions.rest[to_i - 1].1.total_offset() <= to + transitions.rest[to_i - 2].1.total_offset() {
357                transitions.rest[to_i - 1].1 = transitions.rest[from_i].1.clone();
358                from_i += 1;
359                continue;
360            }
361        }
362
363        if to_i == 0 || transitions.rest[to_i - 1].1 != transitions.rest[from_i].1 {
364            transitions.rest[to_i] = transitions.rest[from_i].clone();
365            to_i += 1;
366        }
367
368        from_i += 1
369    }
370
371    transitions.rest.truncate(to_i);
372
373    if !transitions.rest.is_empty() && transitions.first == transitions.rest[0].1 {
374        transitions.rest.remove(0);
375    }
376}
377
378
379#[cfg(test)]
380mod test {
381    use super::*;
382    use super::optimise;
383
384    // Allow unused results in test code, because the only ‘results’ that
385    // we need to ignore are the ones from inserting and removing from
386    // tables and vectors. And as we set them up ourselves, they’re bound
387    // to be correct, otherwise the tests would fail!
388    #[test]
389    #[allow(unused_results)]
390    fn optimise_macquarie() {
391        let mut transitions = FixedTimespanSet {
392            first: FixedTimespan { utc_offset:     0, dst_offset:    0, name:  "zzz".to_owned() },
393            rest: vec![
394                (-2_214_259_200, FixedTimespan { utc_offset: 36000,  dst_offset:    0,  name: "AEST".to_owned() }),
395                (-1_680_508_800, FixedTimespan { utc_offset: 36000,  dst_offset: 3600,  name: "AEDT".to_owned() }),
396                (-1_669_892_400, FixedTimespan { utc_offset: 36000,  dst_offset: 3600,  name: "AEDT".to_owned() }),  // gets removed
397                (-1_665_392_400, FixedTimespan { utc_offset: 36000,  dst_offset:    0,  name: "AEST".to_owned() }),
398                (-1_601_719_200, FixedTimespan { utc_offset:     0,  dst_offset:    0,  name:  "zzz".to_owned() }),
399                (  -687_052_800, FixedTimespan { utc_offset: 36000,  dst_offset:    0,  name: "AEST".to_owned() }),
400                (   -94_730_400, FixedTimespan { utc_offset: 36000,  dst_offset:    0,  name: "AEST".to_owned() }),  // also gets removed
401                (   -71_136_000, FixedTimespan { utc_offset: 36000,  dst_offset: 3600,  name: "AEDT".to_owned() }),
402                (   -55_411_200, FixedTimespan { utc_offset: 36000,  dst_offset:    0,  name: "AEST".to_owned() }),
403                (   -37_267_200, FixedTimespan { utc_offset: 36000,  dst_offset: 3600,  name: "AEDT".to_owned() }),
404                (   -25_776_000, FixedTimespan { utc_offset: 36000,  dst_offset:    0,  name: "AEST".to_owned() }),
405                (    -5_817_600, FixedTimespan { utc_offset: 36000,  dst_offset: 3600,  name: "AEDT".to_owned() }),
406            ],
407        };
408
409        let mut result = transitions.clone();
410        result.rest.remove(6);
411        result.rest.remove(2);
412
413        optimise(&mut transitions);
414        assert_eq!(transitions, result);
415    }
416}