use crate::MILLIS_PER_DAY;
const SECONDS_PER_DAY: i32 = 86400;
const DAYS_PER_CYCLE: i32 = 146097;
const DAYS_0000_TO_1970: i32 = (DAYS_PER_CYCLE * 5) - (30 * 365 + 7);
const SUPPORT_NEGATIVE_YEAR: bool = false;
static DAYS_PER_MONTH: [[i32; 12]; 2] = [
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
];
#[inline]
fn is_leap_year(year: i32) -> bool {
((year % 4) == 0) & ((year % 100) != 0) | ((year % 400) == 0)
}
#[inline]
fn days_per_month(year: i32, zero_based_month: i32) -> i32 {
let is_leap = is_leap_year(year);
let is_feb = zero_based_month == 1;
let mut days = 30 + ((zero_based_month % 2) != (zero_based_month <= 6) as i32) as i32;
days -= (2 - is_leap as i32 ) * (is_feb as i32);
days
}
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub struct EpochDays(i32);
impl EpochDays {
#[inline]
pub fn new(epoch_days: i32) -> Self {
Self(epoch_days)
}
#[inline]
pub fn days(&self) -> i32 {
self.0
}
#[inline]
pub fn from_ymd(year: i32, month: i32, day: i32) -> Self {
let y = year;
let m = month;
let mut total = 365 * y;
total += if y < 0 && SUPPORT_NEGATIVE_YEAR {
-(y / -4 - y / -100 + y / -400)
} else {
(y + 3) / 4 - (y + 99) / 100 + (y + 399) / 400
};
total += ((367 * m - 362) / 12);
total += day - 1;
total -= 0_i32.wrapping_sub((m > 2) as i32) & (1 + (!is_leap_year(year) as i32));
Self(total - DAYS_0000_TO_1970)
}
#[inline]
pub fn to_ymd(&self) -> (i32, i32, i32) {
let epoch_days = self.0;
let mut zero_day = epoch_days + DAYS_0000_TO_1970;
zero_day -= 60; let mut adjust = 0;
if zero_day < 0 && SUPPORT_NEGATIVE_YEAR {
let adjust_cycles = (zero_day + 1) / DAYS_PER_CYCLE - 1;
adjust = adjust_cycles * 400;
zero_day += -adjust_cycles * DAYS_PER_CYCLE;
}
let mut year_est = (400 * zero_day + 591) / DAYS_PER_CYCLE;
if !SUPPORT_NEGATIVE_YEAR {
year_est &= i32::MAX;
}
let mut doy_est =
zero_day - (365 * year_est + year_est / 4 - year_est / 100 + year_est / 400);
year_est -= (doy_est < 0) as i32;
if !SUPPORT_NEGATIVE_YEAR {
year_est &= i32::MAX;
}
doy_est = zero_day - (365 * year_est + year_est / 4 - year_est / 100 + year_est / 400);
year_est += adjust; let march_doy0 = doy_est;
let march_month0 = (march_doy0 * 5 + 2) / 153;
let month = (march_month0 + 2) % 12 + 1;
let dom = march_doy0 - (march_month0 * 306 + 5) / 10 + 1;
year_est += march_month0 / 10;
(year_est, month, dom)
}
#[inline]
pub fn from_timestamp_millis(ts: i64) -> Self {
Self::from_timestamp_millis_float(ts as f64)
}
#[inline]
pub fn from_timestamp_millis_float(ts: f64) -> Self {
let epoch_days = (ts * (1.0 / MILLIS_PER_DAY as f64)).floor();
Self(unsafe { epoch_days.to_int_unchecked() })
}
#[inline]
pub fn to_timestamp_millis(&self) -> i64 {
(self.0 as i64) * MILLIS_PER_DAY
}
#[inline]
pub fn to_timestamp_millis_float(&self) -> f64 {
(self.0 as f64) * (MILLIS_PER_DAY as f64)
}
#[inline]
pub fn add_months(&self, months: i32) -> Self {
let (mut y, mut m, mut d) = self.to_ymd();
let mut m0 = m - 1;
m0 += months;
y += m0.div_euclid(12);
m0 = m0.rem_euclid(12);
d = d.min(days_per_month(y, m0));
m = m0 + 1;
Self::from_ymd(y, m, d)
}
#[inline]
pub fn add_years(&self, years: i32) -> Self {
let (mut y, m, mut d) = self.to_ymd();
y += years;
d = d.min(days_per_month(y, m-1));
Self::from_ymd(y, m, d)
}
#[inline]
pub fn diff_months(&self, other: EpochDays) -> i32 {
let (y0, m0, d0) = self.to_ymd();
let (y1, m1, d1) = other.to_ymd();
(y1 * 12 + m1) - (y0 * 12 + m0) - (d1 < d0) as i32
}
#[inline]
pub fn diff_years(&self, other: EpochDays) -> i32 {
let (y0, m0, d0) = self.to_ymd();
let (y1, m1, d1) = other.to_ymd();
y1 - y0 - ((m1, d1) < (m0, d0)) as i32
}
#[inline]
pub fn date_trunc_month(&self) -> Self {
let (y, m, d) = self.to_ymd();
Self::from_ymd(y, m, 1)
}
#[inline]
pub fn date_trunc_year(&self) -> Self {
let (y, m, d) = self.to_ymd();
Self::from_ymd(y, 1, 1)
}
#[inline]
pub fn date_trunc_quarter(&self) -> Self {
let (y, m, d) = self.to_ymd();
Self::from_ymd(y, (m - 1) / 3 * 3 + 1, 1)
}
#[inline]
pub fn extract_year(&self) -> i32 {
self.to_ymd().0
}
#[inline]
pub fn extract_month(&self) -> i32 {
self.to_ymd().1
}
#[inline]
pub fn extract_quarter(&self) -> i32 {
(self.to_ymd().1 - 1) / 3 + 1
}
#[inline]
pub fn extract_day_of_month(&self) -> i32 {
self.to_ymd().2
}
}
#[cfg(test)]
mod tests {
use crate::epoch_days::{days_per_month, DAYS_PER_MONTH, is_leap_year};
use crate::EpochDays;
#[test]
fn test_is_leap_year() {
assert!(!is_leap_year(1900));
assert!(!is_leap_year(1999));
assert!(is_leap_year(2000));
assert!(!is_leap_year(2001));
assert!(!is_leap_year(2002));
assert!(!is_leap_year(2003));
assert!(is_leap_year(2004));
assert!(is_leap_year(2020));
}
#[test]
fn test_days_per_month() {
for i in 0..12 {
assert_eq!(days_per_month(2023, i as i32), DAYS_PER_MONTH[0][i], "non-leap: {i}");
}
for i in 0..12 {
assert_eq!(days_per_month(2020, i as i32), DAYS_PER_MONTH[1][i], "leap: {i}");
}
}
#[test]
fn test_to_epoch_day() {
assert_eq!(0, EpochDays::from_ymd(1970, 1, 1).0);
assert_eq!(1, EpochDays::from_ymd(1970, 1, 2).0);
assert_eq!(365, EpochDays::from_ymd(1971, 1, 1).0);
assert_eq!(365 * 2, EpochDays::from_ymd(1972, 1, 1).0);
assert_eq!(365 * 2 + 366, EpochDays::from_ymd(1973, 1, 1).0);
assert_eq!(18998, EpochDays::from_ymd(2022, 1, 6).0);
assert_eq!(19198, EpochDays::from_ymd(2022, 7, 25).0);
}
#[test]
fn test_date_trunc_year_epoch_days() {
assert_eq!(18993, EpochDays::new(19198).date_trunc_year().days());
}
#[test]
fn test_date_trunc_month_epoch_days() {
assert_eq!(19174, EpochDays::new(19198).date_trunc_month().days());
}
#[test]
fn test_date_diff_month_epoch_days() {
assert_eq!(
EpochDays::from_ymd(2023, 10, 1).diff_months(EpochDays::from_ymd(2023, 11, 1)),
1
);
assert_eq!(
EpochDays::from_ymd(2023, 10, 1).diff_months(EpochDays::from_ymd(2023, 12, 1)),
2
);
assert_eq!(
EpochDays::from_ymd(2023, 10, 1).diff_months(EpochDays::from_ymd(2023, 12, 31)),
2
);
assert_eq!(
EpochDays::from_ymd(2023, 10, 22).diff_months(EpochDays::from_ymd(2023, 11, 22)),
1
);
assert_eq!(
EpochDays::from_ymd(2023, 10, 22).diff_months(EpochDays::from_ymd(2023, 11, 21)),
0
);
assert_eq!(
EpochDays::from_ymd(2023, 10, 31).diff_months(EpochDays::from_ymd(2023, 11, 30)),
0
);
}
#[test]
fn test_date_diff_month_epoch_days_negative() {
assert_eq!(
EpochDays::from_ymd(2023, 11, 1).diff_months(EpochDays::from_ymd(2023, 10, 1)),
-1
);
}
#[test]
fn test_date_diff_year_epoch_days() {
assert_eq!(
EpochDays::from_ymd(2023, 10, 1).diff_years(EpochDays::from_ymd(2023, 10, 1)),
0
);
assert_eq!(
EpochDays::from_ymd(2023, 10, 1).diff_years(EpochDays::from_ymd(2023, 11, 1)),
0
);
assert_eq!(
EpochDays::from_ymd(2023, 1, 1).diff_years(EpochDays::from_ymd(2024, 1, 1)),
1
);
assert_eq!(
EpochDays::from_ymd(2023, 2, 28).diff_years(EpochDays::from_ymd(2024, 2, 28)),
1
);
assert_eq!(
EpochDays::from_ymd(2023, 2, 28).diff_years(EpochDays::from_ymd(2024, 2, 29)),
1
);
assert_eq!(
EpochDays::from_ymd(2023, 6, 15).diff_years(EpochDays::from_ymd(2024, 6, 14)),
0
);
assert_eq!(
EpochDays::from_ymd(2023, 6, 15).diff_years(EpochDays::from_ymd(2025, 6, 14)),
1
);
assert_eq!(
EpochDays::from_ymd(2023, 6, 15).diff_years(EpochDays::from_ymd(2025, 6, 16)),
2
);
}
#[test]
fn test_extract_year() {
assert_eq!(2022, EpochDays::from_ymd(2022, 1, 1).extract_year());
assert_eq!(2022, EpochDays::from_ymd(2022, 8, 24).extract_year());
assert_eq!(2022, EpochDays::from_ymd(2022, 12, 31).extract_year());
}
#[test]
fn test_extract_month() {
assert_eq!(1, EpochDays::from_ymd(2000, 1, 1).extract_month());
assert_eq!(2, EpochDays::from_ymd(2000, 2, 1).extract_month());
assert_eq!(2, EpochDays::from_ymd(2000, 2, 29).extract_month());
assert_eq!(1, EpochDays::from_ymd(2022, 1, 1).extract_month());
assert_eq!(8, EpochDays::from_ymd(2022, 8, 24).extract_month());
assert_eq!(12, EpochDays::from_ymd(2022, 12, 31).extract_month());
}
#[test]
fn test_extract_day() {
assert_eq!(1, EpochDays::from_ymd(2000, 1, 1).extract_day_of_month());
assert_eq!(1, EpochDays::from_ymd(2000, 2, 1).extract_day_of_month());
assert_eq!(29, EpochDays::from_ymd(2000, 2, 29).extract_day_of_month());
assert_eq!(1, EpochDays::from_ymd(2000, 3, 1).extract_day_of_month());
}
#[test]
fn test_extract_quarter() {
assert_eq!(1, EpochDays::from_ymd(2000, 1, 1).extract_quarter());
assert_eq!(1, EpochDays::from_ymd(2000, 2, 1).extract_quarter());
assert_eq!(1, EpochDays::from_ymd(2000, 3, 31).extract_quarter());
assert_eq!(2, EpochDays::from_ymd(2000, 4, 1).extract_quarter());
assert_eq!(3, EpochDays::from_ymd(2000, 7, 1).extract_quarter());
assert_eq!(4, EpochDays::from_ymd(2000, 10, 1).extract_quarter());
assert_eq!(4, EpochDays::from_ymd(2000, 12, 31).extract_quarter());
}
}