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}