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}