1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
//! Time zone data generated at compile time from the IANA timezone database
//!
//! Because the rules for timezone transitions around DST are frequently defined in terms of somewhat arcane rules such as "the last Sunday on or after October 20th", the build step uses the [`parse-zoneinfo`](https://crates.io/crates/parse-zoneinfo) crate to build a series of concrete timestamps and associated offsets for each timezone.
//! Unfortunately, this finite series of concrete timestamps covers only a finite period of time, even though the rules are usually defined for eternity.
//! Since this crate is wholly reliant on `parse-zoneinfo`, it inherits the decision made there to be **restricted to the time from 1800 to 2100**.
//! As a consolation, they offer that "it is not necessarily advisable to rely on offset changes so far into the future".
//! This is fairly reasonable, considering that changes to timezones are occasionally announced just days in advance.
//!
//! Finally, it must be said that without `parse-zoneinfo`, which is part of [`chrono`](https://crates.io/crates/chrono), this crate, and by extension [`greg`] would not exist.
#![allow(clippy::zero_prefixed_literal)]
mod data;
pub use data::ZONES;
#[cfg(test)]
mod tests;
use std::slice::Iter;
use std::ops::RangeBounds;
use greg::{
Point,
Frame,
utc
};
use greg::calendar::{
Utc,
DateTime,
};
use greg::calendar::zone::{
Shift,
Steady,
Unsteady
};
/// An [`Unsteady`] time zone
///
/// *All* the time zones in the IANA database are implemented as associated constants, including the ones that are actually just a fixed [`Offset`].
#[derive(Copy, Clone, Eq, PartialEq)]
pub struct Zone {
name: &'static str,
lmt: Offset,
offsets: &'static [(Point, Offset)]
}
/// A [`Steady`] (i.e. fixed) offset from UTC
///
/// There is a handful of timezones in the IANA database that are always at the same offset (i.e. no DST, no time zone transitions ever), and these are implemented as associated constants here.
/// Most of these are fixed "by definition", that is their name literally is `Etc/GMTPlus3` or something to that effect -- these are unlikely to ever change between versions of the database.
/// However, for the time zones where that is not immediately obvious, a word of caution: if any of these, according to the timezone database, cease being fixed offsets, the associated constants will be removed unceremoniously from the next version of this library.
/// As such, **no semantic versioning guarantees can be made about these constants**.
/// Finally, as a reminder: the timezone database reflects the arbitrary and frequently insane decisions made by politicians.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct Offset {
name: &'static str,
seconds: i64
}
/// Ambiguity that can arise when reverting the [`Unsteady`] [`Zone`] offset
///
/// As described in the documentation for [`Unsteady`], there are two edge cases when trying to revert the offset:
/// - [`Point`]s may be skipped
/// - [`Point`]s may be repeated
///
/// This enum captures both of these.
#[derive(Debug, PartialEq, Eq)]
pub enum Ambiguity {
/// Contains the [`Point`] when the transition occurred that skipped the requested [`Point`] / [`DateTime`]
Skipped(Point),
/// Both [`Point`]s have the same [`DateTime`] (i.e. they "map" to the same [`Point`] in UTC)
Repeated([(Point, Offset); 2])
}
/// An [`Iterator`] yielding all the [`Offset`]s in a [`Zone`] and the [`Frame`]s when they apply
pub struct OffsetFrames {
offsets: Iter<'static, (Point, Offset)>,
current: Option<(Point, Offset)>
}
impl Iterator for OffsetFrames {
type Item = (Frame, Offset);
fn next(&mut self) -> Option<Self::Item> {
match (self.current.take(), self.offsets.next()) {
(Some((curr_p, curr_off)), Some(&(next_p, next_off))) => {
self.current = Some((next_p, next_off));
let frame = Frame {
start: curr_p,
stop: next_p
};
Some((frame, curr_off))
},
(Some((last_p, last_off)), None) => Some((
Frame {start: last_p, stop: Point::from_epoch(i64::MAX)},
last_off
)),
(None, _) => None
}
}
}
impl ExactSizeIterator for OffsetFrames {
fn len(&self) -> usize {
self.offsets.len() + self.current.iter().count()
}
}
/*
* OFFSET
*/
impl Offset {
const fn new(name: &'static str, seconds: i64) -> Self {
Self {name, seconds}
}
/// Name of the [`Offset`] (e.g. `CEST`)
pub const fn name(self) -> &'static str {self.name}
}
impl Shift for Offset {
fn apply(&self, point: Point) -> Point {
let timestamp = point.timestamp + self.seconds;
Point {timestamp}
}
}
impl Steady for Offset {
fn revert(&self, point: Point) -> Point {
let timestamp = point.timestamp - self.seconds;
Point {timestamp}
}
}
/*
* ZONE
*/
impl Zone {
const fn define(
name: &'static str,
lmt: Offset,
offsets: &'static [(Point, Offset)])
-> Self
{
Self {name, lmt, offsets}
}
const fn alias(name: &'static str, alias: Self) -> Self {
Self {name, ..alias}
}
/// Name of the [`Zone`] (e.g. `Europe/Berlin`)
pub const fn name(self) -> &'static str {self.name}
fn contains(point: Point) -> bool {
(utc!(1800-01-01)..utc!(2100-01-01)).contains(&point)
}
/// Format a [`Point`] as `YYYY-MM-DD hh:mm:ss Z` (i.e. `2022-12-31 12:34:56 CET`)
///
/// Same as doing [`greg::Calendar::lookup`] and calling [`ToString::to_string`] on the [`DateTime`], but with the [`Offset`] name appended.
pub fn format_with_name(&self, point: Point) -> String {
let offset = self.offset_at(point);
let DateTime(date, time) = Utc::lookup(offset.apply(point));
format!("{date} {time} {}", offset.name())
}
/// Get the [`Offset`] that applies for a [`Point`]
pub fn offset_at(&self, point: Point) -> Offset {
assert!(
Self::contains(point),
"Point out of valid time zone range 1800..2100"
);
self.offsets.iter()
.rev()
.find(|&&(p, _offset)| p <= point)
.map(|&(_p, offset)| offset)
.unwrap_or(self.lmt)
}
/// Access the array of [`Offset`]s and when they first apply
///
/// Every [`Offset`] applies from the [`Point`] in the tuple until the next one begins.
/// In theory, the last [`Offset`] doesn't have an end [`Point`], but remember that this crate only defines time zones up until the year 2100.
///
/// Note that this doesn't include the base [`Offset`] (LMT).
/// As such, the slice may be empty.
pub fn offsets(&self) -> &'static [(Point, Offset)] {self.offsets}
/// Iterate over all the [`Offset`]s and the [`Frame`] during which they're active
pub fn offset_frames(&self) -> OffsetFrames {
let offsets = self.offsets.iter();
let current = Some((Point::from_epoch(i64::MIN), self.lmt));
OffsetFrames {offsets, current}
}
}
impl Shift for Zone {
fn apply(&self, point: Point) -> Point {
self.offset_at(point).apply(point)
}
}
impl Unsteady for Zone {
type Ambiguity = Ambiguity;
fn revert_lossy(&self, utc_point: Point) -> Point {
assert!(
Self::contains(utc_point),
"Point out of valid time zone range 1800..2100"
);
for (frame, offset) in self.offset_frames() {
let reverted = offset.revert(utc_point);
if frame.contains(&reverted) {
return reverted;
}
else if frame.start > reverted {
//skipped
return frame.start;
}
}
unreachable!()
}
fn try_revert(&self, utc_point: Point) -> Result<Point, Ambiguity> {
assert!(
Self::contains(utc_point),
"Point out of valid time zone range 1800..2100"
);
let mut found = None;
for (frame, offset) in self.offset_frames() {
let reverted = offset.revert(utc_point);
match frame.contains(&reverted) {
true => match found {
Some(previous) => return Err(Ambiguity::Repeated(
[previous, (reverted, offset)]
)),
None => found = Some((reverted, offset))
},
false => match found {
Some((point, _)) => return Ok(point),
None if frame.start > reverted => return Err(
Ambiguity::Skipped(frame.start)
),
None => continue
}
}
}
unreachable!()
}
}