use crate::Point;
use super::{
month::MONTHS,
DateTime,
Date,
Time,
Utc
};
const GREG_CYCLE_DAYS: i64 = 146_097;
const GREG_UNIX_DAY: i64 = 719_528;
const UNIX_DAY_LEAP_COUNT: i64 = Utc::leap_day_count(1970);
impl Utc {
pub const fn is_leap(year: i64) -> bool {
(year.rem_euclid(4) == 0)
&& (
(year.rem_euclid(25) != 0) || (year.rem_euclid(16) == 0)
)
}
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
}
pub const fn leap_day_count(year: i64) -> i64 {
let cycle = year / 400;
let rest = year % 400;
let century = rest / 100;
let years = rest % 100;
let is_cent_0 = if century == 0 {1} else {0};
let is_not_bc = if year < 0 {0} else {1};
let accrued = (century * 25) - (century - century.signum());
let shift = (-1 + 4 * is_cent_0) * is_not_bc;
let leaps = (years + shift) / 4;
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
}
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}
}
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}
}
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)
}
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}
}
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;
}
}