blob: d8b38057cf6c4ee4be33d0847f8c6bd9ba70c5fd [file] [log] [blame] [edit]
//! Generating timespan sets from a built Table.
//!
//! Once a table has been fully built, it needs to be turned into several
//! *fixed timespan sets*: a series of spans of time where the local time
//! offset remains the same throughout. One set is generated for each named
//! time zone. These timespan sets can then be iterated over to produce
//! *transitions*: when the local time changes from one offset to another.
//!
//! These sets are returned as `FixedTimespanSet` values, rather than
//! iterators, because the generation logic does not output the timespans
//! in any particular order, meaning they need to be sorted before they’re
//! returned—so we may as well just return the vector, rather than an
//! iterator over the vector.
//!
//! Similarly, there is a fixed set of years that is iterated over
//! (currently 1800..2100), rather than having an iterator that produces
//! timespans indefinitely. Not only do we need a complete set of timespans
//! for sorting, but it is not necessarily advisable to rely on offset
//! changes so far into the future!
//!
//! ### Example
//!
//! The complete definition of the `Indian/Mauritius` time zone, as
//! specified in the `africa` file in my version of the tz database, has
//! two Zone definitions, one of which refers to four Rule definitions:
//!
//! ```tz
//! # Rule NAME FROM TO TYPE IN ON AT SAVE LETTER/S
//! Rule Mauritius 1982 only - Oct 10 0:00 1:00 S
//! Rule Mauritius 1983 only - Mar 21 0:00 0 -
//! Rule Mauritius 2008 only - Oct lastSun 2:00 1:00 S
//! Rule Mauritius 2009 only - Mar lastSun 2:00 0 -
//!
//! # Zone NAME GMTOFF RULES FORMAT [UNTIL]
//! Zone Indian/Mauritius 3:50:00 - LMT 1907 # Port Louis
//! 4:00 Mauritius MU%sT # Mauritius Time
//! ```
//!
//! To generate a fixed timespan set for this timezone, we examine each of the
//! Zone definitions, generating at least one timespan for each definition.
//!
//! * The first timespan describes the *local mean time* (LMT) in Mauritius,
//! calculated by the geographical position of Port Louis, its capital.
//! Although it’s common to have a timespan set begin with a city’s local mean
//! time, it is by no means necessary. This timespan has a fixed offset of
//! three hours and fifty minutes ahead of UTC, and lasts until the beginning
//! of 1907, at which point the second timespan kicks in.
//! * The second timespan has no ‘until’ date, so it’s in effect indefinitely.
//! Instead of having a fixed offset, it refers to the set of rules under the
//! name “Mauritius”, which we’ll have to consult to compute the timespans.
//! * The first two rules refer to a summer time transition that began on
//! the 10th of October 1982, and lasted until the 21st of March 1983. But
//! before we get onto that, we need to add a timespan beginning at the
//! time the last one ended (1907), up until the point Summer Time kicks
//! in (1982), reflecting that it was four hours ahead of UTC.
//! * After this, we add another timespan for Summer Time, when Mauritius
//! was an extra hour ahead, bringing the total offset for that time to
//! *five* hours.
//! * The next (and last) two rules refer to another summer time
//! transition from the last Sunday of October 2008 to the last Sunday of
//! March 2009, this time at 2am local time instead of midnight. But, as
//! before, we need to add a *standard* time timespan beginning at the
//! time Summer Time ended (1983) up until the point the next span of
//! Summer Time kicks in (2008), again reflecting that it was four hours
//! ahead of UTC again.
//! * Next, we add the Summer Time timespan, again bringing the total
//! offset to five hours. We need to calculate when the last Sundays of
//! the months are to get the dates correct.
//! * Finally, we add one last standard time timespan, lasting from 2009
//! indefinitely, as the Mauritian authorities decided not to change to
//! Summer Time again.
//!
//! All this calculation results in the following six timespans to be added:
//!
//! | Timespan start | Abbreviation | UTC offset | DST? |
//! |:--------------------------|:-------------|:-------------------|:-----|
//! | *no start* | LMT | 3 hours 50 minutes | No |
//! | 1906-11-31 T 20:10:00 UTC | MUT | 4 hours | No |
//! | 1982-09-09 T 20:00:00 UTC | MUST | 5 hours | Yes |
//! | 1983-02-20 T 19:00:00 UTC | MUT | 4 hours | No |
//! | 2008-09-25 T 22:00:00 UTC | MUST | 5 hours | Yes |
//! | 2009-02-28 T 21:00:00 UTC | MUT | 4 hours | No |
//!
//! There are a few final things of note:
//!
//! Firstly, this library records the times that timespans *begin*, while
//! the tz data files record the times that timespans *end*. Pay attention to
//! this if the timestamps aren’t where you expect them to be! For example, in
//! the data file, the first zone rule has an ‘until’ date and the second has
//! none, whereas in the list of timespans, the last timespan has a ‘start’
//! date and the *first* has none.
//!
//! Secondly, although local mean time in Mauritius lasted until 1907, the
//! timespan is recorded as ending in 1906! Why is this? It’s because the
//! transition occurred at midnight *at the local time*, which in this case,
//! was three hours fifty minutes ahead of UTC. So that time has to be
//! *subtracted* from the date, resulting in twenty hours and ten minutes on
//! the last day of the year. Similar things happen on the rest of the
//! transitions, being either four or five hours ahead of UTC.
//!
//! The logic in this file is based off of `zic.c`, which comes with the
//! zoneinfo files and is in the public domain.
use crate::table::{RuleInfo, Saving, Table, ZoneInfo};
/// A set of timespans, separated by the instances at which the timespans
/// change over. There will always be one more timespan than transitions.
///
/// This mimics the `FixedTimespanSet` struct in `datetime::cal::zone`,
/// except it uses owned `Vec`s instead of slices.
#[derive(PartialEq, Debug, Clone)]
pub struct FixedTimespanSet {
/// The first timespan, which is assumed to have been in effect up until
/// the initial transition instant (if any). Each set has to have at
/// least one timespan.
pub first: FixedTimespan,
/// The rest of the timespans, as a vector of tuples, each containing:
///
/// 1. A transition instant at which the previous timespan ends and the
/// next one begins, stored as a Unix timestamp;
/// 2. The actual timespan to transition into.
pub rest: Vec<(i64, FixedTimespan)>,
}
/// An individual timespan with a fixed offset.
///
/// This mimics the `FixedTimespan` struct in `datetime::cal::zone`, except
/// instead of “total offset” and “is DST” fields, it has separate UTC and
/// DST fields. Also, the name is an owned `String` here instead of a slice.
#[derive(PartialEq, Debug, Clone)]
pub struct FixedTimespan {
/// The number of seconds offset from UTC during this timespan.
pub utc_offset: i64,
/// The number of *extra* daylight-saving seconds during this timespan.
pub dst_offset: i64,
/// The abbreviation in use during this timespan.
pub name: String,
}
impl FixedTimespan {
/// The total offset in effect during this timespan.
pub fn total_offset(&self) -> i64 {
self.utc_offset + self.dst_offset
}
}
/// Trait to put the `timespans` method on Tables.
pub trait TableTransitions {
/// Computes a fixed timespan set for the timezone with the given name.
/// Returns `None` if the table doesn’t contain a time zone with that name.
fn timespans(&self, zone_name: &str) -> Option<FixedTimespanSet>;
}
impl TableTransitions for Table {
fn timespans(&self, zone_name: &str) -> Option<FixedTimespanSet> {
let mut builder = FixedTimespanSetBuilder::default();
let zoneset = match self.get_zoneset(zone_name) {
Some(zones) => zones,
None => return None,
};
for (i, zone_info) in zoneset.iter().enumerate() {
let mut dst_offset = 0;
let use_until = i != zoneset.len() - 1;
let utc_offset = zone_info.offset;
let mut insert_start_transition = i > 0;
let mut start_zone_id = None;
let mut start_utc_offset = zone_info.offset;
let mut start_dst_offset = 0;
match zone_info.saving {
Saving::NoSaving => {
builder.add_fixed_saving(
zone_info,
0,
&mut dst_offset,
utc_offset,
&mut insert_start_transition,
&mut start_zone_id,
);
}
Saving::OneOff(amount) => {
builder.add_fixed_saving(
zone_info,
amount,
&mut dst_offset,
utc_offset,
&mut insert_start_transition,
&mut start_zone_id,
);
}
Saving::Multiple(ref rules) => {
let rules = &self.rulesets[rules];
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,
);
}
}
if insert_start_transition && start_zone_id.is_some() {
let t = (
builder.start_time.expect("Start time"),
FixedTimespan {
utc_offset: start_utc_offset,
dst_offset: start_dst_offset,
name: start_zone_id.clone().expect("Start zone ID"),
},
);
builder.rest.push(t);
}
if use_until {
builder.start_time = Some(
zone_info.end_time.expect("End time").to_timestamp() - utc_offset - dst_offset,
);
}
}
Some(builder.build())
}
}
#[derive(Debug, Default)]
struct FixedTimespanSetBuilder {
first: Option<FixedTimespan>,
rest: Vec<(i64, FixedTimespan)>,
start_time: Option<i64>,
until_time: Option<i64>,
}
impl FixedTimespanSetBuilder {
fn add_fixed_saving(
&mut self,
timespan: &ZoneInfo,
amount: i64,
dst_offset: &mut i64,
utc_offset: i64,
insert_start_transition: &mut bool,
start_zone_id: &mut Option<String>,
) {
*dst_offset = amount;
*start_zone_id = Some(timespan.format.format(*dst_offset, None));
if *insert_start_transition {
let time = self.start_time.unwrap();
let timespan = FixedTimespan {
utc_offset: timespan.offset,
dst_offset: *dst_offset,
name: start_zone_id.clone().unwrap_or_default(),
};
self.rest.push((time, timespan));
*insert_start_transition = false;
} else {
self.first = Some(FixedTimespan {
utc_offset,
dst_offset: *dst_offset,
name: start_zone_id.clone().unwrap_or_default(),
});
}
}
#[allow(unused_results)]
#[allow(clippy::too_many_arguments)]
fn add_multiple_saving(
&mut self,
timespan: &ZoneInfo,
rules: &[RuleInfo],
dst_offset: &mut i64,
use_until: bool,
utc_offset: i64,
insert_start_transition: &mut bool,
start_zone_id: &mut Option<String>,
start_utc_offset: &mut i64,
start_dst_offset: &mut i64,
) {
use std::mem::replace;
for year in 1800..2100 {
if use_until && year > timespan.end_time.unwrap().year() {
break;
}
let mut activated_rules = rules
.iter()
.filter(|r| r.applies_to_year(year))
.collect::<Vec<_>>();
loop {
if use_until {
self.until_time =
Some(timespan.end_time.unwrap().to_timestamp() - utc_offset - *dst_offset);
}
// Find the minimum rule and its start time based on the current
// UTC and DST offsets.
let earliest = activated_rules
.iter()
.enumerate()
.map(|(i, r)| (i, r.absolute_datetime(year, utc_offset, *dst_offset)))
.min_by_key(|&(_, time)| time);
let (pos, earliest_at) = match earliest {
Some((pos, time)) => (pos, time),
None => break,
};
let earliest_rule = activated_rules.remove(pos);
if use_until && earliest_at >= self.until_time.unwrap() {
break;
}
*dst_offset = earliest_rule.time_to_add;
if *insert_start_transition && earliest_at == self.start_time.unwrap() {
*insert_start_transition = false;
}
if *insert_start_transition {
if earliest_at < self.start_time.unwrap() {
let _ = replace(start_utc_offset, timespan.offset);
let _ = replace(start_dst_offset, *dst_offset);
let _ = replace(
start_zone_id,
Some(
timespan
.format
.format(*dst_offset, earliest_rule.letters.as_ref()),
),
);
continue;
}
if start_zone_id.is_none()
&& *start_utc_offset + *start_dst_offset == timespan.offset + *dst_offset
{
let _ = replace(
start_zone_id,
Some(
timespan
.format
.format(*dst_offset, earliest_rule.letters.as_ref()),
),
);
}
}
let t = (
earliest_at,
FixedTimespan {
utc_offset: timespan.offset,
dst_offset: earliest_rule.time_to_add,
name: timespan
.format
.format(earliest_rule.time_to_add, earliest_rule.letters.as_ref()),
},
);
self.rest.push(t);
}
}
}
fn build(mut self) -> FixedTimespanSet {
self.rest.sort_by(|a, b| a.0.cmp(&b.0));
let first = match self.first {
Some(ft) => ft,
None => self
.rest
.iter()
.find(|t| t.1.dst_offset == 0)
.unwrap()
.1
.clone(),
};
let mut zoneset = FixedTimespanSet {
first,
rest: self.rest,
};
optimise(&mut zoneset);
zoneset
}
}
#[allow(unused_results)] // for remove
fn optimise(transitions: &mut FixedTimespanSet) {
let mut from_i = 0;
let mut to_i = 0;
while from_i < transitions.rest.len() {
if to_i > 1 {
let from = transitions.rest[from_i].0;
let to = transitions.rest[to_i - 1].0;
if from + transitions.rest[to_i - 1].1.total_offset()
<= to + transitions.rest[to_i - 2].1.total_offset()
{
transitions.rest[to_i - 1].1 = transitions.rest[from_i].1.clone();
from_i += 1;
continue;
}
}
if to_i == 0 || transitions.rest[to_i - 1].1 != transitions.rest[from_i].1 {
transitions.rest[to_i] = transitions.rest[from_i].clone();
to_i += 1;
}
from_i += 1
}
transitions.rest.truncate(to_i);
if !transitions.rest.is_empty() && transitions.first == transitions.rest[0].1 {
transitions.rest.remove(0);
}
}
#[cfg(test)]
mod test {
use super::optimise;
use super::*;
// Allow unused results in test code, because the only ‘results’ that
// we need to ignore are the ones from inserting and removing from
// tables and vectors. And as we set them up ourselves, they’re bound
// to be correct, otherwise the tests would fail!
#[test]
#[allow(unused_results)]
fn optimise_macquarie() {
let mut transitions = FixedTimespanSet {
first: FixedTimespan {
utc_offset: 0,
dst_offset: 0,
name: "zzz".to_owned(),
},
rest: vec![
(
-2_214_259_200,
FixedTimespan {
utc_offset: 36000,
dst_offset: 0,
name: "AEST".to_owned(),
},
),
(
-1_680_508_800,
FixedTimespan {
utc_offset: 36000,
dst_offset: 3600,
name: "AEDT".to_owned(),
},
),
(
-1_669_892_400,
FixedTimespan {
utc_offset: 36000,
dst_offset: 3600,
name: "AEDT".to_owned(),
},
), // gets removed
(
-1_665_392_400,
FixedTimespan {
utc_offset: 36000,
dst_offset: 0,
name: "AEST".to_owned(),
},
),
(
-1_601_719_200,
FixedTimespan {
utc_offset: 0,
dst_offset: 0,
name: "zzz".to_owned(),
},
),
(
-687_052_800,
FixedTimespan {
utc_offset: 36000,
dst_offset: 0,
name: "AEST".to_owned(),
},
),
(
-94_730_400,
FixedTimespan {
utc_offset: 36000,
dst_offset: 0,
name: "AEST".to_owned(),
},
), // also gets removed
(
-71_136_000,
FixedTimespan {
utc_offset: 36000,
dst_offset: 3600,
name: "AEDT".to_owned(),
},
),
(
-55_411_200,
FixedTimespan {
utc_offset: 36000,
dst_offset: 0,
name: "AEST".to_owned(),
},
),
(
-37_267_200,
FixedTimespan {
utc_offset: 36000,
dst_offset: 3600,
name: "AEDT".to_owned(),
},
),
(
-25_776_000,
FixedTimespan {
utc_offset: 36000,
dst_offset: 0,
name: "AEST".to_owned(),
},
),
(
-5_817_600,
FixedTimespan {
utc_offset: 36000,
dst_offset: 3600,
name: "AEDT".to_owned(),
},
),
],
};
let mut result = transitions.clone();
result.rest.remove(6);
result.rest.remove(2);
optimise(&mut transitions);
assert_eq!(transitions, result);
}
}