greg-tz 0.1.1

WIP: greg timezone data
Documentation
//! 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 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
}

/// 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 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 {
	fn revert_to_earliest(&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.start <= reverted && frame.stop > reverted {
				return reverted;
			}
			else if frame.start > reverted {
				//skipped
				return frame.start;
			}

		}
		unreachable!()
	}
}