#![allow(clippy::integer_division)]
use crate::{
DAY_IN_SECONDS,
HOUR_IN_SECONDS,
MINUTE_IN_SECONDS,
Utc2k,
};
use std::ops::{
Add,
AddAssign,
};
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
pub(super) struct Abacus {
y: u32,
m: u32,
d: u32,
hh: u32,
mm: u32,
ss: u32,
}
impl Add<u32> for Abacus {
type Output = Self;
fn add(self, other: u32) -> Self {
let mut out = Self {
y: self.y,
m: self.m,
d: self.d,
hh: self.hh,
mm: self.mm,
ss: self.ss.saturating_add(other),
};
out.rebalance();
out
}
}
impl AddAssign<u32> for Abacus {
#[inline]
fn add_assign(&mut self, other: u32) {
self.ss = self.ss.saturating_add(other);
self.rebalance();
}
}
impl From<Utc2k> for Abacus {
fn from(src: Utc2k) -> Self {
let (y, m, d, hh, mm, ss) = src.parts();
Self::new(y, m, d, hh, mm, ss)
}
}
impl Abacus {
#[must_use]
pub(super) fn new(y: u16, m: u8, d: u8, hh: u8, mm: u8, ss: u8) -> Self {
let mut out = Self {
y: y.into(),
m: m.into(),
d: d.into(),
hh: hh.into(),
mm: mm.into(),
ss: ss.into(),
};
out.rebalance();
out
}
#[allow(clippy::cast_possible_truncation)] #[must_use]
pub(super) const fn parts(&self) -> (u8, u8, u8, u8, u8, u8) {
if self.y < 2000 { (0, 1, 1, 0, 0, 0) }
else if 2099 < self.y { (99, 12, 31, 23, 59, 59) }
else {
(
(self.y - 2000) as u8,
self.m as u8,
self.d as u8,
self.hh as u8,
self.mm as u8,
self.ss as u8,
)
}
}
}
impl Abacus {
fn rebalance(&mut self) {
if 23 < self.hh || 59 < self.mm || 59 < self.ss {
self.rebalance_ss();
self.rebalance_mm();
self.rebalance_hh();
}
if
0 == self.m || 12 < self.m || 0 == self.d ||
(28 < self.d && self.month_days() < self.d)
{
self.rebalance_date();
}
}
fn rebalance_ss(&mut self) {
if self.ss >= DAY_IN_SECONDS {
let div = self.ss / DAY_IN_SECONDS;
self.d += div;
self.ss -= div * DAY_IN_SECONDS;
}
if self.ss >= HOUR_IN_SECONDS {
let div = (self.ss * 0x91A3) >> 27;
self.hh += div;
self.ss -= div * HOUR_IN_SECONDS;
}
if self.ss >= MINUTE_IN_SECONDS {
let div = (self.ss * 0x889) >> 17;
self.mm += div;
self.ss -= div * MINUTE_IN_SECONDS;
}
}
fn rebalance_mm(&mut self) {
if self.mm > 59 {
let div = (self.mm * 0x889) >> 17;
self.hh += div;
self.mm -= div * 60;
}
}
fn rebalance_hh(&mut self) {
if self.hh > 23 {
let div = self.hh / 24;
self.d += div;
self.hh -= div * 24;
}
}
fn rebalance_date(&mut self) {
if self.y < 1500 {
self.y = 1500;
self.m = 1;
self.d = 1;
return;
}
if self.m == 0 {
self.y -= 1;
self.m = 12;
}
else if 12 < self.m {
let div = (self.m - 1) / 12;
self.y += div;
self.m -= div * 12;
}
if self.d == 0 {
if self.m == 1 {
self.y -= 1;
self.m = 12;
self.d = 31;
}
else {
self.m -= 1;
self.d = self.month_days();
}
}
else if 28 < self.d {
let size = self.month_days();
if size < self.d {
self.m += 1;
self.d -= size;
self.rebalance_date();
}
}
}
const fn month_days(&self) -> u32 {
match self.m {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if self.y.trailing_zeros() >= 2 && ((self.y % 100) != 0 || (self.y % 400) == 0) => 29,
_ => 28,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn addition() {
macro_rules! add {
($($start:ident + $num:literal = ($y2:literal, $m2:literal, $d2:literal, $hh2:literal, $mm2:literal, $ss2:literal)),+) => ($(
assert_eq!(
($start + $num).parts(),
($y2, $m2, $d2, $hh2, $mm2, $ss2)
);
{
let mut tmp = $start;
tmp += $num;
assert_eq!($start + $num, tmp);
}
)+);
}
let start = Abacus::new(2000, 1, 1, 0, 0, 0);
add!(
start + 0 = (0, 1, 1, 0, 0, 0),
start + 1 = (0, 1, 1, 0, 0, 1),
start + 60 = (0, 1, 1, 0, 1, 0),
start + 3600 = (0, 1, 1, 1, 0, 0),
start + 3661 = (0, 1, 1, 1, 1, 1),
start + 31_622_400 = (1, 1, 1, 0, 0, 0),
start + 4_294_967_295 = (99, 12, 31, 23, 59, 59)
);
}
#[test]
fn carries() {
macro_rules! carry {
($(($y:literal, $m:literal, $d:literal, $hh:literal, $mm:literal, $ss:literal) ($y2:literal, $m2:literal, $d2:literal, $hh2:literal, $mm2:literal, $ss2:literal) $fail:literal),+) => ($(
assert_eq!(
Abacus::new($y, $m, $d, $hh, $mm, $ss).parts(),
($y2, $m2, $d2, $hh2, $mm2, $ss2),
$fail
);
)+);
}
carry!(
(2000, 13, 32, 24, 60, 60) (01, 2, 2, 1, 1, 0) "Overage of one everywhere.",
(2000, 25, 99, 1, 1, 1) (02, 4, 9, 1, 1, 1) "Large month/day overages.",
(2000, 1, 1, 99, 99, 99) (00, 1, 5, 4, 40, 39) "Large time overflows.",
(2000, 255, 255, 255, 255, 255) (21, 11, 20, 19, 19, 15) "Max overflows.",
(1970, 25, 99, 1, 1, 1) (00, 1, 1, 0, 0, 0) "Saturating low.",
(3000, 25, 99, 1, 1, 1) (99, 12, 31, 23, 59, 59) "Saturating high #1.",
(2099, 25, 99, 1, 1, 1) (99, 12, 31, 23, 59, 59) "Saturating high #2.",
(2010, 0, 0, 1, 1, 1) (09, 11, 30, 1, 1, 1) "Zero month, zero day.",
(2010, 0, 32, 1, 1, 1) (10, 1, 1, 1, 1, 1) "Zero month, overflowing day.",
(2010, 1, 0, 1, 1, 1) (09, 12, 31, 1, 1, 1) "Zero day into zero month.",
(2010, 2, 30, 1, 1, 1) (10, 3, 2, 1, 1, 1) "Too many days for month.",
(2010, 24, 1, 1, 1, 1) (11, 12, 1, 1, 1, 1) "Exactly 24 months."
);
}
#[test]
fn shifting() {
fn divvy(mut ss: u32) -> (u32, u32, u32) {
let mut hh = 0;
let mut mm = 0;
if ss >= HOUR_IN_SECONDS {
let div = ss / HOUR_IN_SECONDS;
hh += div;
ss -= div * HOUR_IN_SECONDS;
}
if ss >= MINUTE_IN_SECONDS {
let div = ss / MINUTE_IN_SECONDS;
mm += div;
ss -= div * MINUTE_IN_SECONDS;
}
(hh, mm, ss)
}
fn shifty(mut ss: u32) -> (u32, u32, u32) {
let mut hh = 0;
let mut mm = 0;
if ss >= HOUR_IN_SECONDS {
let div = (ss * 0x91A3) >> 27;
hh += div;
ss -= div * HOUR_IN_SECONDS;
}
if ss >= MINUTE_IN_SECONDS {
let div = (ss * 0x889) >> 17;
mm += div;
ss -= div * MINUTE_IN_SECONDS;
}
(hh, mm, ss)
}
for i in 0..DAY_IN_SECONDS {
assert_eq!(divvy(i), shifty(i));
}
}
}