greg_tz/
lib.rs

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