greg 0.2.5

Simple Unobtrusive Date & Time library
Documentation
//! Traits defining time zones as fixed or variable offsets from UTC
//!
//! The actual time zones are not provided by this crate (besides [`Utc`]), but are generated from the IANA time zone database in the `greg-tz` crate.
//!
//! This module provides 3 traits for defining 2 types of time zones.
//! Both types implement [`Shift`], time zones with fixed offsets also implement [`Steady`] whereas time zones with variable offsets implement [`Unsteady`].
//!
//! [`Shift`] applies the offset from UTC: this essentially means that for a given [`Point`], which has a particular [`DateTime`] at that offset, the [`Point`] that is the result of applying it will have the same [`DateTime`] *in UTC*.
//! If this is confusing, it may help to remember that for all the ambiguities introduced by time zones, it is never, in any time zone, unclear what the current time is (assuming that it is clear what time zone is in effect).
//! [`Shift`] being implemented by both [`Steady`] and [`Unsteady`] types reflects that fact.
//!
//! [`Steady`] and [`Unsteady`] both serve the same general purpose, which is to *revert* the offset as (would have been) applied by [`Shift`].
//! As such, [`Steady::revert`] is the inverse of [`Shift::apply`]: for a [`Point`] that has a given [`DateTime`] according to UTC, it returns a [`Point`] that has the same [`DateTime`] *in this timezone*.
//! [`Unsteady::revert_lossy`] does this too, essentially, but with some caveats.
//!
//! [`Steady`] can only be implemented by time zones that have a fixed offset from UTC.
//! This means that the clock was never and will never be set forwards or backwards in it, which excludes (almost) all time zones.
//! Not just because many zones observe Daylight Savings Time (DST), but also because the time zone database tracks offset changes (when all the clocks in that timezone have to be set forwards or backwards, called a timezone *transition*) made for administrative and political reasons.
//! Interestingly, even the introduction of the timezone itself usually counts as a transition: it would be inaccurate to simply apply it retroactively.
//! So, instead of that, the offset before the timezone is introduced is retroactively defined according to its geographical location (see [LMT](https://en.wikipedia.org/wiki/Local_mean_time)).
//!
//! [`Unsteady`], then, is implemented by a general "time zone" type.
//! Because many or most time zones do not unambiguously map every [`DateTime`] in UTC, [`Unsteady`] does not provide this guarantee.
//! Rather, it makes explicit the workarounds used to address this.
//! This is better than forcing users to somehow deal with all the annoying edge cases of time zones.
//!
//! To summarize: [`Shift`] allows you to look up the [`DateTime`] for a given [`Point`].
//! [`Steady`] resolves every (valid) [`DateTime`] to a [`Point`], unambiguously.
//! [`Unsteady`] is the same as [`Steady`] except that [`revert_lossy`](Unsteady::revert_lossy) always returns the earliest [`Point`] if ambiguous, or returns the [`Point`] where the transition occurred, if the [`DateTime`] in question never occurred.

use crate::{
	Point,
	real::Scale
};

use super::{
	Utc,
	Calendar,
	DateTime,
	Date,
	Time,
	Weekday,
	Frames,
	State
};

/// Types that apply an offset from UTC
pub trait Shift {
	/// Apply the offset
	///
	/// This means that the output [`Point`] has the same [`DateTime`] according to UTC as the input has according to this time zone.
	fn apply(&self, point: Point) -> Point;
}

/// A *steady* offset from UTC which can be unambiguously reverted
pub trait Steady: Shift {
	/// Revert the offset as (would be) applied by [`Shift::apply`]
	///
	/// For any [`Point`] `a`, `zone.revert(a)` returns a [`Point`] `b`, such that `Calendar(Utc).lookup(a) == Calendar(zone).lookup(b)`.
	/// Simply put: the output [`Point`] has the same [`DateTime`] *in this time zone* as the input [`Point`] has in UTC.
	fn revert(&self, point: Point) -> Point;
}

/// An *unsteady* offset from UTC which cannot (always) be reverted unambiguously
///
/// Around every [`Point`] where the offset from UTC changes, [`Shift::apply`] will not be [bijective].
/// Specifically, there will either be [`Point`]s that are *skipped* (not [surjective]) or [`Point`]s that are *repeated* (not [injective]).
///
/// A [`Point`] is *skipped* if there is no input for [`Shift::apply`] that will produce it as its output.
/// In practical terms, this occurs when the clock in a particular time zone is set forward, and it means that there are [`DateTime`]s that simply do not exist in that time zone.
///
/// A [`Point`] is *repeated* if there are multiple different inputs for [`Shift::apply`] that produce it as its output.
/// This occurs when the clock in a particular time zone is set backwards, which results in some [`DateTime`]s being ambiguous because they occur twice in that time zone.
///
///
/// For example, in the time zone Europe/Berlin, on 2022-03-27 at 02:00:00 local time, clocks were set forward by one hour, such that 02:00:00 - 02:59:59 never occurred.
/// Then, on 2022-10-30 at 03:00:00 local time, clocks were set backward by one hour, so that 02:00:00 - 02:59:59 occurred twice.
///
/// [bijective]: https://en.wikipedia.org/wiki/Bijection,_injection_and_surjection#Bijection
/// [surjective]: https://en.wikipedia.org/wiki/Bijection,_injection_and_surjection#Surjection
/// [injective]: https://en.wikipedia.org/wiki/Bijection,_injection_and_surjection#Injection
pub trait Unsteady: Shift {
	/// Ambiguity that may arise when attempting to revert the offset
	type Ambiguity: std::fmt::Debug;
	/// Attempt to revert [`Shift::apply`], choosing the earliest [`Point`] if ambiguous or settling for the transition [`Point`] if impossible
	///
	/// Doing this is a bit more complicated than [`Shift::apply`] because, essentially, only after the offset has been reverted is it clear whether it had applied ([`Shift::apply`] could have returned a [`Point`] for which a different offset is in effect).
	fn revert_lossy(&self, point: Point) -> Point;
	/// Try to revert [`Shift::apply`] without suppressing ambiguities
	fn try_revert(&self, point: Point) -> Result<Point, Self::Ambiguity>;
}

impl Shift for Utc {
	/// Identity function
	fn apply(&self, point: Point) -> Point {point}
}

impl Steady for Utc {
	/// Identity function
	fn revert(&self, point: Point) -> Point {point}
}


impl<Z: Shift> Calendar<Z> {
	/// Look up the [`DateTime`] of a [`Point`]
	///
	/// This function may be neither injective nor surjective: whenever the clock is set forwards, [`DateTime`]s are skipped and never occur and whenever it gets set backwards, [`DateTime`]s repeat.
	pub fn lookup(&self, point: Point) -> DateTime {
		let shifted = self.0.apply(point);
		Utc::lookup(shifted)
	}

	/// Get the [`Weekday`] of the [`Point`]
	pub fn weekday(&self, point: Point) -> Weekday {
		let in_utc = self.0.apply(point);
		match (in_utc.timestamp / 86_400 - 4).rem_euclid(7) {
			0 => Weekday::Monday,
			1 => Weekday::Tuesday,
			2 => Weekday::Wednesday,
			3 => Weekday::Thursday,
			4 => Weekday::Friday,
			5 => Weekday::Saturday,
			6 => Weekday::Sunday,
			_ => unreachable!()
		}
	}

	/// Format a [`Point`] as `YYYY-MM-DD hh:mm:ss` (i.e. `2022-12-31 12:34:56`)
	///
	/// Same as doing [`Calendar::lookup`] and calling [`ToString::to_string`] on the [`DateTime`].
	pub fn format(&self, point: Point) -> String {
		self.lookup(point).to_string()
	}
}

impl<Z: Steady> Calendar<Z> {
	/// Resolve the [`Point`] in time where the [`DateTime`] occurred *at this offset*.
	pub fn resolve(&self, datetime: DateTime) -> Point {
		let utc_point = Utc::resolve(datetime);
		self.0.revert(utc_point)
	}
	/// Shorthand for [`resolve`](Self::resolve) with a [`Date`] and [`Time::MIDNIGHT`]
	pub fn resolve_midnight(&self, date: Date) -> Point {
		self.resolve(DateTime(date, Time::MIDNIGHT))
	}

	/// Round a [`Point`] down according to the [`Scale`]
	///
	/// Rounding down with [`Scale::Seconds`] is a no-op.
	///
	/// ```
	/// use greg::{
	///     Calendar,
	///     Utc,
	///     real::Scale,
	///     utc
	/// };
	///
	/// let christmas = utc!(2022-12-24 12:34:56);
	///
	/// let cases = [
	///     (Scale::Years,   utc!(2022-01-01 00:00:00)),
	///     (Scale::Months,  utc!(2022-12-01 00:00:00)),
	///     (Scale::Weeks,   utc!(2022-12-19 00:00:00)),
	///     (Scale::Days,    utc!(2022-12-24 00:00:00)),
	///     (Scale::Hours,   utc!(2022-12-24 12:00:00)),
	///     (Scale::Minutes, utc!(2022-12-24 12:34:00)),
	///     (Scale::Seconds, utc!(2022-12-24 12:34:56))
	/// ];
	///
	/// for (scale, expected) in cases {
	///     assert_eq!(Calendar(Utc).date_floor(scale, christmas), expected);
	/// }
	/// ```
	pub fn date_floor(&self, scale: Scale, point: Point) -> Point {
		let point_utc = self.0.apply(point);
		let utc_floor = match scale {
			Scale::Years => Utc::start_of_year(point_utc),
			Scale::Months => Utc::start_of_month(point_utc),
			Scale::Weeks => {
				let rest = (point_utc.timestamp - 4 * 86_400) % (7 * 86_400);
				let timestamp = point_utc.timestamp - rest;
				Point {timestamp}
			},
			Scale::Days |
			Scale::Hours |
			Scale::Minutes |
			Scale::Seconds => {
				let rest = point_utc.timestamp % scale.as_seconds() as i64;
				let timestamp = point_utc.timestamp - rest;
				Point {timestamp}
			}
		};
		self.0.revert(utc_floor)
	}
}

impl<Z: Unsteady> Calendar<Z> {
	/// Finds either the (first) point when the [`DateTime`] occurred *or* when it was skipped
	///
	/// In a (sane) timezone, [`DateTime`]s can occur 0, 1, or 2 times.
	/// Most [`DateTime`]s occur exactly once - these require no explanation.
	///
	/// If they don't occur, they were skipped by setting clocks forward.
	/// In this case, the [`Point`] where transition occurred is returned since it is the best approximation.
	///
	/// If they occur twice, the clock was set backwards.
	/// In this case, this function returns the [`Point`] when it first occurred, before the transition.
	/// This is somewhat arbitrary but at least it's consistent.
	pub fn resolve_lossy(&self, datetime: DateTime) -> Point {
		let utc_point = Utc::resolve(datetime);
		self.0.revert_lossy(utc_point)
	}
	/// Shorthand for [`resolve_lossy`](Self::resolve_lossy) with a [`Date`] and [`Time::MIDNIGHT`]
	pub fn resolve_midnight_lossy(&self, date: Date) -> Point {
		self.resolve_lossy(DateTime(date, Time::MIDNIGHT))
	}

	/// Try to find the point when the [`DateTime`] occurred
	pub fn try_resolve(&self, dt: DateTime) -> Result<Point, Z::Ambiguity> {
		let utc_point = Utc::resolve(dt);
		self.0.try_revert(utc_point)
	}

	/// Round a [`Point`] down according to the [`Scale`]
	///
	/// See the [`Calendar::date_floor`] example for the general principle, but note that this uses [`Unsteady::revert_lossy`], so its caveats propagate.
	/// Basically, this function shifts the [`Point`] to UTC, rounds down the result, and reverts that back to the time zone.
	/// Thus the caveats of [`Unsteady::revert_lossy`] about missing and repeated [`DateTime`]s apply.
	///
	/// For example, rounding down with [`Scale::Seconds`] may no longer be a no-op, because the [`Point`] could be in the period of repeated [`DateTime`]s, after clocks were set back.
	/// Similarly, rounding down by [`Scale::Minutes`] might return a [`Point`] more than 60 seconds before the input.
	pub fn date_floor_lossy(&self, scale: Scale, point: Point) -> Point {
		let point_utc = self.0.apply(point);
		let utc_floor = match scale {
			Scale::Years => Utc::start_of_year(point_utc),
			Scale::Months => Utc::start_of_month(point_utc),
			Scale::Weeks => {
				let rest = (point_utc.timestamp - 4 * 86_400) % (7 * 86_400);
				let timestamp = point_utc.timestamp - rest;
				Point {timestamp}
			},
			Scale::Days |
			Scale::Hours |
			Scale::Minutes => {
				let rest = point_utc.timestamp % scale.as_seconds() as i64;
				let timestamp = point_utc.timestamp - rest;
				Point {timestamp}
			},
			Scale::Seconds => return point
		};
		self.0.revert_lossy(utc_floor)
	}

	/// Iterate over [`Frame`](crate::Frame)s of [`Scale`], starting on or before `start`
	///
	/// The first [`Frame`](crate::Frame) starts at [`Calendar::date_floor_lossy`]`(start)`.
	/// See the documentation on [`Frames`] for more, including important caveats.
	pub fn frames(&self, scale: Scale, start: Point) -> Frames<'_, Z> {
		let first_start = self.date_floor_lossy(scale, start);

		let state = match scale {
			Scale::Years |
			Scale::Months => State::Date (self.lookup(first_start).0),
			Scale::Seconds => State::Seconds,
			_ => State::Utc(self.0.apply(first_start))
		};

		Frames {
			calendar: self,
			scale,
			previous: first_start,
			state
		}
	}
}