greg 0.2.5

Simple Unobtrusive Date & Time library
Documentation
use crate::Point;
use super::{
	month::MONTHS,
	DateTime,
	Date,
	Time,
	Utc
};

/// Number of days in a 400-year cycle
const GREG_CYCLE_DAYS: i64 = 146_097;
/// Number of days from 0000-01-01 to 1970-01-01
const GREG_UNIX_DAY: i64 = 719_528;
/// `leap_day_count(1970)`
const UNIX_DAY_LEAP_COUNT: i64 = Utc::leap_day_count(1970);

impl Utc {
	/// Whether the given year is a leap year
	pub const fn is_leap(year: i64) -> bool {
		(year.rem_euclid(4) == 0)
		&& (
			(year.rem_euclid(25) != 0) || (year.rem_euclid(16) == 0)
		)
	}

	/// Difference between the [`leap_day_count`](Self::leap_day_count)s of two years
	pub const fn leap_day_diff(first_year: i64, last_year: i64) -> i64 {
		Self::leap_day_count(last_year) - Self::leap_day_count(first_year)
	}
	const fn leap_day_unix_diff(year: i64) -> i64 {
		Self::leap_day_count(year) - UNIX_DAY_LEAP_COUNT
	}

	/// Count of leap days that occur between `0000-01-01` and the beginning of `year`
	///
	/// For negative `year`s (more precisely years below `-3`), the count will be negative.
	///
	/// This counts the leap days that occurred between the *beginning* of the year and the Gregorian epoch `0000-01-01`.
	/// Perhaps surprisingly, this means that for *negative* (i.e. B.C.) leap years, the returned count includes its leap day, whereas the leap days of *positive* (i.e. A.D.) leap years are only counted at the beginning of the next year.
	/// Additionally, this function is not symmetric around year 0 because it is a leap year:
	/// ```text
	/// year  | -5 | -4 | -3 | -2 | -1 |  0 |  1 |  2 |  3 |  4 |  5
	/// leaps | -1 | -1 |  0 |  0 |  0 |  0 |  1 |  1 |  1 |  1 |  2
	/// ```
	pub const fn leap_day_count(year: i64) -> i64 {
		//TODO: mirror-shift refactor

		let cycle = year / 400;
		let rest = year % 400;

		let century = rest / 100;
		// Years from the beginning of the century
		let years = rest % 100;

		// Century correction: 1, 0, 0, 0
		let is_cent_0 = if century == 0 {1} else {0};

		let is_not_bc = if year < 0 {0} else {1};

		// Count leap years:
		// accrued over previous centuries
		//let accrued = (century * 25) - (century - 1 + is_cent_0);
		let accrued = (century * 25) - (century - century.signum());
		// from this century
		let shift = (-1 + 4 * is_cent_0) * is_not_bc;
		let leaps = (years + shift) / 4;

		// negative years aren't simply the negative of positive years
		// this is because the negative years don't have a "-0" leap year
		let bc_correct = if (cycle <= 0) & (century < 0) {1} else {0};

		(cycle * 97) + accrued + leaps + bc_correct
	}

	const fn count_days(date: Date) -> u16 {
		let mut days = MONTHS[date.m as usize - 1].common_offset
			+ date.d as u16
			- 1;
		let is_leap = Self::is_leap(date.y);
		if is_leap & date.y.is_positive() & (date.m > 2) {days += 1}
		days
	}

	/// Rounds down to midnight January 1st of that year
	pub const fn start_of_year(point: Point) -> Point {
		let DateTime(date, time) = Self::lookup(point);
		let timestamp = point.timestamp
			- Self::count_days(date) as i64 * 86_400
			- time.as_seconds() as i64;
		Point {timestamp}
	}

	/// Rounds down to midnight of the 1st of that month
	pub const fn start_of_month(point: Point) -> Point {
		let DateTime(date, time) = Self::lookup(point);
		let timestamp = point.timestamp
			- (date.d - 1) as i64 * 86_400
			- time.as_seconds() as i64;
		Point {timestamp}
	}

	/// Look up the [`DateTime`] of a [`Point`]
	pub const fn lookup(point: Point) -> DateTime {
		let days = point.timestamp.div_euclid(86_400) + GREG_UNIX_DAY;
		let cycle_number = days.div_euclid(GREG_CYCLE_DAYS);
		let cycle_days = days.rem_euclid(GREG_CYCLE_DAYS);

		let mut date = Date::from_cycle_days(cycle_days as u32);
		date.y += 400 * cycle_number;

		let seconds = point.timestamp.rem_euclid(86_400) as u32;
		let time = Time::from_seconds(seconds);

		DateTime(date, time)
	}

	/// Resolve a [`DateTime`] to the [`Point`] where it occurred
	///
	/// Uses **wrapping arithmetic** internally.
	/// Invalid [`DateTime`]s and ones that cannot be represented as a [`Point`] will fall *somewhere* into the continuum of [`Point`]s, indistinguishably from a valid result.
	/// Keep that in mind when processing untrusted [`DateTime`]s and preferably validate your inputs beforehand.
	///
	/// All [`DateTime`]s from `Utc::lookup(Point::MIN)..=Utc::resolve(Point::MAX)` are resolved accurately, but seeing as that is `-292277022657-01-27 08:29:52` to `292277026596-12-04 15:30:07`, almost 300 billion years in the past & future respectively, you may want to validate against a shorter time frame than that.
	pub const fn resolve(datetime: DateTime) -> Point {
		let DateTime(date, time) = datetime;
		let days = date.y
			.wrapping_sub(1970)
			.wrapping_mul(365)
			.wrapping_add(Self::count_days(date) as i64)
			.wrapping_add(Self::leap_day_unix_diff(date.y));
		let timestamp = days
			.wrapping_mul(86_400)
			.wrapping_add(time.as_seconds() as i64);
		Point {timestamp}
	}

	/// Shorthand for [`resolve`](Self::resolve) with a [`Date`] and [`Time::MIDNIGHT`]
	pub const fn resolve_midnight(date: Date) -> Point {
		Self::resolve(DateTime(date, Time::MIDNIGHT))
	}
}



#[test]
fn format() {
	macro_rules! point_and_fmt {
		($y:literal-$mo:literal-$d:literal $h:literal:$m:literal:$s:literal)
		=> {(
				utc!($y-$mo-$d $h:$m:$s),
				format!(
					"{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
					$y, $mo, $d, $h, $m, $s
				)
			)
		};
	}

	let cases = [
		(point_and_fmt!(1970-01-01 00:00:00),         0i64),
		(point_and_fmt!(2020-01-01 00:00:00),   1577836800),
		(point_and_fmt!(2020-01-01 23:00:00),   1577919600),
		(point_and_fmt!(2020-01-03 00:00:00),   1578009600),
		(point_and_fmt!(2018-01-01 00:00:00),   1514764800),
		(point_and_fmt!(2000-02-29 00:00:00),    951782400),
		(point_and_fmt!(2360-01-01 00:00:00),  12307161600),
		(point_and_fmt!(2020-02-29 00:00:00),   1582934400),
		(point_and_fmt!(2020-03-01 00:00:00),   1583020800),
		(point_and_fmt!(1969-12-31 00:00:00),       -86400),
		(point_and_fmt!(1969-12-31 23:00:00),        -3600),
		(point_and_fmt!(1960-01-01 00:00:00),   -315619200),
		(point_and_fmt!(2022-12-31 00:00:00),   1672444800),
		(point_and_fmt!(2022-07-01 00:00:00),   1656633600),
		(point_and_fmt!(2020-07-01 00:00:00),   1593561600),
		(point_and_fmt!(2020-12-30 00:00:00),   1609286400),
		(point_and_fmt!(2020-12-31 00:00:00),   1609372800),
		(point_and_fmt!(2020-03-01 00:00:00),   1583020800),
		(point_and_fmt!(-100-01-31 00:00:00), -65320300800),
		(point_and_fmt!(-100-03-01 00:00:00), -65317795200),
		(point_and_fmt!(-100-03-03 00:00:00), -65317622400),
		(point_and_fmt!(-100-02-28 00:00:00), -65317881600),
		(point_and_fmt!(-100-02-27 23:59:59), -65317881601),
	];

	for ((point, expected), timestamp) in cases {
		let received = Utc::lookup(point).to_string();
		assert_eq!(dbg!(&expected), dbg!(&received));
		assert_eq!(point.timestamp, timestamp);
	}
}

#[test]
fn max_range() {
	for p in [Point::MIN, Point::MAX] {
		assert_eq!(Utc::resolve(Utc::lookup(p)), p);
	}
}

#[test]
#[ignore = "slow"]
fn every_day() {
	let start = -100_000_000i64;
	let mut prev = Utc::lookup(Point::from_epoch((start - 1) * 86_400)).0;
	for d in start..100_000_000 {
		let p = Point::from_epoch(d * 86_400);
		let DateTime(cur, time) = Utc::lookup(p);

		let check = (time == Time::MIDNIGHT)
			& ((cur.d == (prev.d + 1)) & (cur.m == prev.m) & (cur.y == prev.y))
			| ((cur.d == 1) & (cur.m == prev.m + 1) & (cur.y == prev.y))
			| ((cur.d == 1) & (cur.m == 1) & (cur.y == prev.y + 1));

		assert!(check, "{d}: {prev} → {cur}");
		prev = cur;
	}
}