use crate::constants::{
FOUR_YEARS_DAYS, LEAP_YEAR_DAYS, LEAP_YEAR_MONTH_DAYS, MONTH_DAYS, MONTHS_IN_YEAR,
REGULAR_YEAR_DAYS, REGULAR_YEAR_MONTH_DAYS, SECONDS_IN_DAY, THREE_REGULAR_YEAR_DAYS,
UNIX_EPOCH_YEAR,
};
use crate::date_error::{DateError, DateErrorKind};
use crate::utils::date_util::{leap_year, month_days, month_index};
use crate::utils::{crossplatform_util, date_util};
use core::{cmp, fmt};
use std::cmp::Ordering;
use std::str::FromStr;
#[derive(Copy, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde-struct", derive(serde::Serialize, serde::Deserialize))]
pub struct Date {
pub year: u64,
pub month: u64,
pub day: u64,
}
#[cfg(all(feature = "serde", not(feature = "serde-struct")))]
impl serde::Serialize for Date {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let seconds = self.to_seconds_from_unix_epoch(false);
serializer.serialize_u64(seconds)
}
}
#[cfg(all(feature = "serde", not(feature = "serde-struct")))]
impl<'de> serde::Deserialize<'de> for Date {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let seconds = u64::deserialize(deserializer)?;
let (date, _) = Date::from_seconds_since_unix_epoch(seconds);
Ok(date)
}
}
impl Date {
pub fn new(year: u64, month: u64, day: u64) -> Self {
let date = Date { year, month, day };
date
}
#[inline]
pub fn leap_year(&self) -> bool {
date_util::leap_year(self.year)
}
#[inline]
pub fn recent_leap_year(&self) -> u64 {
date_util::recent_leap_year(self.year)
}
#[inline]
pub fn next_leap_year(&self) -> u64 {
date_util::next_leap_year(self.year)
}
pub fn year_day(&self) -> u64 {
let is_leap = self.leap_year();
let month_days = if is_leap {
&LEAP_YEAR_MONTH_DAYS
} else {
®ULAR_YEAR_MONTH_DAYS
};
let mut days: u64 = 0;
for i in 0..(self.month_index()) {
days += month_days[i];
}
days + self.day
}
pub fn days_to_next_year(&self) -> u64 {
let total = if self.leap_year() { 366u64 } else { 365 };
total - self.year_day()
}
#[inline]
pub fn month_index(&self) -> usize {
date_util::month_index(self.month)
}
pub fn valid(&self) -> bool {
if self.month < 1 || self.month > 12 || self.day < 1 {
return false;
}
let max_day = MONTH_DAYS[self.month_index()];
self.day <= max_day || (self.month == 2 && self.leap_year() && self.day == 29)
}
pub fn from_ms_dos_date(mut ms_dos_date: u16) -> Self {
let day = ms_dos_date & 0x1f;
ms_dos_date >>= 5;
let month = ms_dos_date & 0xf;
ms_dos_date >>= 4;
Date::new(ms_dos_date as u64 + 1980, month as u64, day as u64)
}
pub fn add_days(&self, days: u64) -> Self {
let mut result_year = self.year;
let mut result_month = self.month;
let mut result_day = self.day + days;
let mut is_leap = date_util::leap_year(result_year);
let mut month_days = if is_leap {
&LEAP_YEAR_MONTH_DAYS
} else {
®ULAR_YEAR_MONTH_DAYS
};
loop {
let days_in_current_month = month_days[(result_month.saturating_sub(1)) as usize];
if result_day <= days_in_current_month {
break;
}
result_day -= days_in_current_month;
result_month += 1;
if result_month > 12 {
result_month = 1;
result_year += 1;
is_leap = date_util::leap_year(result_year);
month_days = if is_leap {
&LEAP_YEAR_MONTH_DAYS
} else {
®ULAR_YEAR_MONTH_DAYS
};
}
}
Date::new(result_year, result_month, result_day)
}
pub fn from_seconds_since_unix_epoch(seconds: u64) -> (Self, u64) {
let days = seconds / SECONDS_IN_DAY;
(
Date::new(UNIX_EPOCH_YEAR, 1, 1).add_days(days),
seconds % SECONDS_IN_DAY,
)
}
pub fn to_seconds_from_unix_epoch(self, included: bool) -> u64 {
let days = self.to_days() - Date::new(UNIX_EPOCH_YEAR, 1, 1).to_days();
(days + included as u64) * SECONDS_IN_DAY
}
pub fn today() -> Self {
let (date, _) = Self::from_seconds_since_unix_epoch(crossplatform_util::now_seconds());
date
}
pub fn quarter(&self) -> usize {
(self.month_index() / 3) + 1
}
pub fn add_months(&self, months: i64) -> Self {
let months = (self.year as i64 * 12 + self.month_index() as i64 + months) as u64;
let month = months % MONTHS_IN_YEAR + 1;
let year = months / MONTHS_IN_YEAR;
let day = cmp::min(
date_util::month_days(month, date_util::leap_year(year)),
self.day,
);
Date { year, month, day }
}
pub fn sub_months(&self, months: i64) -> Self {
self.add_months(-months)
}
pub fn is_month_last_day(&self) -> bool {
self.day == date_util::month_days(self.month, self.leap_year())
}
pub fn month_last_day(&self) -> Self {
Date {
year: self.year,
month: self.month,
day: date_util::month_days(self.month, self.leap_year()),
}
}
pub fn sub_days(&self, days: u64) -> Self {
let mut result_year = self.year;
let mut result_month = self.month;
let mut result_day = self.day;
let mut is_leap = date_util::leap_year(result_year);
let mut month_days = if is_leap {
&LEAP_YEAR_MONTH_DAYS
} else {
®ULAR_YEAR_MONTH_DAYS
};
let mut remaining_days = days;
while remaining_days > 0 {
if result_day > remaining_days {
result_day -= remaining_days;
break;
}
remaining_days -= result_day;
result_month -= 1;
if result_month == 0 {
result_month = 12;
result_year -= 1;
is_leap = date_util::leap_year(result_year);
month_days = if is_leap {
&LEAP_YEAR_MONTH_DAYS
} else {
®ULAR_YEAR_MONTH_DAYS
};
}
result_day = month_days[(result_month.saturating_sub(1)) as usize];
}
Date::new(result_year, result_month, result_day)
}
pub fn to_days(&self) -> u64 {
let year_elapsed = self.year - 1;
let leap_years = year_elapsed / 4;
let regular_years = year_elapsed - leap_years;
let base_days = leap_years * LEAP_YEAR_DAYS + regular_years * REGULAR_YEAR_DAYS;
let is_leap = self.leap_year();
let month_days = if is_leap {
&LEAP_YEAR_MONTH_DAYS
} else {
®ULAR_YEAR_MONTH_DAYS
};
let mut days = 0;
for i in 0..(self.month_index()) {
days += month_days[i];
}
base_days + days + self.day
}
pub fn from_days(days: u64) -> Self {
let days = days - 1;
let quarters = days / FOUR_YEARS_DAYS;
let days = days % FOUR_YEARS_DAYS;
let (years, mut days) = if days / THREE_REGULAR_YEAR_DAYS == 0 {
(days / REGULAR_YEAR_DAYS, days % REGULAR_YEAR_DAYS)
} else {
(3, days % THREE_REGULAR_YEAR_DAYS)
};
let year = (quarters << 2) + years + 1;
let is_leap = date_util::leap_year(year);
let month_days = if is_leap {
&LEAP_YEAR_MONTH_DAYS
} else {
®ULAR_YEAR_MONTH_DAYS
};
let mut month = 1;
for &days_in_month in month_days {
if days < days_in_month {
break;
}
days -= days_in_month;
month += 1;
}
Date::new(year, month, days + 1)
}
#[inline]
fn weekday(&self) -> u8 {
(self.to_days() % 7) as u8
}
pub fn is_monday(&self) -> bool {
self.weekday() == 2
}
pub fn is_tuesday(&self) -> bool {
self.weekday() == 3
}
pub fn is_wednesday(&self) -> bool {
self.weekday() == 4
}
pub fn is_thursday(&self) -> bool {
self.weekday() == 5
}
pub fn is_friday(&self) -> bool {
self.weekday() == 6
}
pub fn is_saturday(&self) -> bool {
self.weekday() == 0
}
pub fn is_sunday(&self) -> bool {
self.weekday() == 1
}
pub fn is_weekend(&self) -> bool {
self.weekday() < 2
}
pub fn is_week_day(&self) -> bool {
self.weekday() > 1
}
pub fn normalize(&self) -> Date {
let month_index = month_index(self.month) as u64;
let year = month_index / 12 + self.year;
let month = month_index as u64 % 12 + 1;
let mut result = Self::new(year, month, self.day);
let max_days = month_days(month, leap_year(year));
if max_days < result.day {
let add = result.day - max_days;
result.day = max_days;
result = result.add_days(add);
}
result
}
}
impl std::ops::Sub for Date {
type Output = u64;
fn sub(self, rhs: Self) -> Self::Output {
self.to_days() - rhs.to_days()
}
}
impl fmt::Display for Date {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}-{:02}-{:02}", self.year, self.month, self.day)
}
}
impl fmt::Debug for Date {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl FromStr for Date {
type Err = DateError;
fn from_str(date_str: &str) -> Result<Self, Self::Err> {
let bytes = date_str.as_bytes();
if bytes.len() != 10 || bytes[4] != b'-' || bytes[7] != b'-' {
return Err(DateErrorKind::WrongDateStringFormat.into());
}
let year = parse_digits(&bytes[0..4])?;
let month = parse_digits(&bytes[5..7])?;
let day = parse_digits(&bytes[8..10])?;
Ok(Date::new(year, month, day))
}
}
#[inline]
fn parse_digits(bytes: &[u8]) -> Result<u64, DateError> {
let mut result = 0u64;
for &byte in bytes {
if byte < b'0' || byte > b'9' {
return Err(DateErrorKind::WrongDateStringFormat.into());
}
result = result * 10 + (byte - b'0') as u64;
}
Ok(result)
}
impl Ord for Date {
fn cmp(&self, other: &Self) -> Ordering {
if self.year != other.year {
return self.year.cmp(&other.year);
}
if self.month != other.month {
return self.month.cmp(&other.month);
}
self.day.cmp(&other.day)
}
}
impl PartialOrd for Date {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_date_year_day() {
let date = Date::new(2020, 1, 1);
assert_eq!(date.year_day(), 1);
let date = Date::new(2018, 2, 28);
assert_eq!(date.year_day(), 59);
let date = Date::new(2016, 12, 31);
assert_eq!(date.year_day(), 366);
}
#[test]
fn test_date_sub() {
let date_1 = Date::new(2020, 1, 1);
let date_2 = Date::new(2019, 12, 31);
assert_eq!(date_1 - date_2, 1);
let date_1 = Date::new(2020, 1, 1);
let date_2 = Date::new(2016, 1, 1);
assert_eq!(date_1 - date_2, 1461);
let date_1 = Date::new(2020, 1, 1);
let date_2 = Date::new(2016, 3, 1);
assert_eq!(date_1 - date_2, 1401);
let date_1 = Date::new(2020, 3, 4);
let date_2 = Date::new(2016, 3, 1);
assert_eq!(date_1 - date_2, 1464);
let date_1 = Date::new(2021, 3, 2);
let date_2 = Date::new(2019, 12, 31);
assert_eq!(date_1 - date_2, 427);
let date_1 = Date::new(2021, 3, 2);
let date_2 = Date::new(2020, 12, 31);
assert_eq!(date_1 - date_2, 61);
let date_1 = Date::new(2021, 3, 2);
let date_2 = Date::new(2020, 1, 15);
assert_eq!(date_1 - date_2, 412);
let date_1 = Date::new(2020, 12, 31);
let date_2 = Date::new(2020, 1, 1);
assert_eq!(date_1 - date_2, 365);
}
#[test]
fn test_date_from_str() -> Result<(), DateError> {
assert_eq!(Date::from_str("2020-02-29")?, Date::new(2020, 2, 29));
Ok(())
}
#[test]
fn test_add_days() {
assert_eq!(Date::new(2019, 12, 31).add_days(1), Date::new(2020, 1, 1));
assert_eq!(
Date::new(2019, 12, 31).add_days(3753),
Date::new(2030, 4, 10)
);
assert_eq!(Date::new(2019, 2, 28).add_days(365), Date::new(2020, 2, 28));
assert_eq!(Date::new(2019, 2, 28).add_days(366), Date::new(2020, 2, 29));
assert_eq!(Date::new(2019, 3, 1).add_days(366), Date::new(2020, 3, 1));
assert_eq!(Date::new(2018, 1, 1).add_days(1198), Date::new(2021, 4, 13));
}
#[test]
fn test_ms_dos_date() {
assert_eq!(Date::from_ms_dos_date(0x354b), Date::new(2006, 10, 11));
}
#[test]
fn test_date_cmp() {
assert!(Date::new(2019, 12, 31) < Date::new(2020, 1, 1));
assert!(Date::new(2020, 2, 1) > Date::new(2020, 1, 31));
assert!(Date::new(2020, 3, 31) > Date::new(2020, 3, 30));
assert_eq!(Date::new(2020, 1, 1), Date::new(2020, 1, 1));
}
#[test]
fn test_add_months() {
assert_eq!(
Date::new(2019, 12, 31).add_months(2),
Date::new(2020, 2, 29)
);
assert_eq!(
Date::new(2019, 12, 31).add_months(26),
Date::new(2022, 2, 28)
);
assert_eq!(
Date::new(2019, 12, 31).add_months(1),
Date::new(2020, 1, 31)
);
assert_eq!(
Date::new(2020, 2, 29).add_months(-2),
Date::new(2019, 12, 29)
);
}
#[test]
fn test_is_month_last_day() {
assert!(Date::new(2019, 12, 31).is_month_last_day());
assert!(!Date::new(2019, 12, 30).is_month_last_day());
assert!(Date::new(2019, 2, 28).is_month_last_day());
assert!(!Date::new(2020, 2, 28).is_month_last_day());
assert!(Date::new(2020, 2, 29).is_month_last_day());
}
#[test]
fn test_month_last_day() {
assert_eq!(
Date::new(2019, 2, 2).month_last_day(),
Date::new(2019, 2, 28)
);
assert_eq!(
Date::new(2020, 2, 2).month_last_day(),
Date::new(2020, 2, 29)
);
}
#[test]
fn test_to_seconds_from_unix_epoch() {
assert_eq!(Date::new(1970, 1, 1).to_seconds_from_unix_epoch(false), 0);
assert_eq!(
Date::new(1970, 1, 1).to_seconds_from_unix_epoch(true),
SECONDS_IN_DAY
);
}
#[test]
fn test_to_days() {
assert_eq!(Date::new(1, 1, 1).to_days(), 1);
assert_eq!(Date::new(1, 12, 31).to_days(), 365);
assert_eq!(Date::new(4, 2, 29).to_days(), 1155);
assert_eq!(Date::new(5, 1, 1).to_days(), 1462);
}
#[test]
fn test_from_days() {
assert_eq!(Date::from_days(1), Date::new(1, 1, 1));
assert_eq!(Date::from_days(365), Date::new(1, 12, 31));
assert_eq!(Date::from_days(1155), Date::new(4, 2, 29));
assert_eq!(Date::from_days(1462), Date::new(5, 1, 1));
}
#[test]
fn test_is_monday() {
assert_eq!(Date::new(2021, 8, 2).is_monday(), true);
assert_eq!(Date::new(2021, 8, 3).is_monday(), false);
}
#[test]
fn test_is_tuesday() {
assert_eq!(Date::new(2021, 8, 3).is_tuesday(), true);
assert_eq!(Date::new(2021, 8, 4).is_tuesday(), false);
}
#[test]
fn test_is_wednesday() {
assert_eq!(Date::new(2021, 8, 4).is_wednesday(), true);
assert_eq!(Date::new(2021, 8, 5).is_wednesday(), false);
}
#[test]
fn test_is_thursday() {
assert_eq!(Date::new(2021, 8, 5).is_thursday(), true);
assert_eq!(Date::new(2021, 8, 6).is_thursday(), false);
}
#[test]
fn test_is_friday() {
assert_eq!(Date::new(2021, 8, 6).is_friday(), true);
assert_eq!(Date::new(2021, 8, 7).is_friday(), false);
}
#[test]
fn test_is_saturday() {
assert_eq!(Date::new(2021, 8, 7).is_saturday(), true);
assert_eq!(Date::new(2021, 8, 8).is_saturday(), false);
}
#[test]
fn test_is_sunday() {
assert_eq!(Date::new(2021, 8, 8).is_sunday(), true);
assert_eq!(Date::new(2021, 8, 9).is_sunday(), false);
}
#[test]
fn test_add_sub_days() {
let a = Date::new(2020, 12, 31);
assert_eq!(a.add_days(1).sub_days(1), a);
}
#[test]
fn test_sub_months() {
assert_eq!(
Date::new(2020, 2, 29).sub_months(2),
Date::new(2019, 12, 29)
);
assert_eq!(Date::new(2020, 4, 30).sub_months(2), Date::new(2020, 2, 29));
assert_eq!(
Date::new(2022, 2, 28).sub_months(26),
Date::new(2019, 12, 28)
);
assert_eq!(
Date::new(2020, 1, 31).sub_months(1),
Date::new(2019, 12, 31)
);
}
#[test]
fn test_normalize() {
assert_eq!(Date::new(2020, 49, 32).normalize(), Date::new(2024, 2, 1));
assert_eq!(Date::new(2020, 49, 60).normalize(), Date::new(2024, 2, 29));
assert_eq!(Date::new(2020, 49, 61).normalize(), Date::new(2024, 3, 1));
}
#[test]
fn test_date_validation() {
assert!(Date::new(2020, 2, 29).valid()); assert!(Date::new(2021, 2, 28).valid()); assert!(Date::new(2020, 12, 31).valid());
assert!(Date::new(2020, 1, 1).valid());
assert!(!Date::new(2021, 2, 29).valid()); assert!(!Date::new(2020, 2, 30).valid()); assert!(!Date::new(2020, 4, 31).valid()); assert!(!Date::new(2020, 6, 31).valid()); assert!(!Date::new(2020, 9, 31).valid()); assert!(!Date::new(2020, 11, 31).valid()); assert!(!Date::new(2020, 0, 1).valid()); assert!(!Date::new(2020, 13, 1).valid()); assert!(!Date::new(2020, 1, 0).valid()); assert!(!Date::new(2020, 1, 32).valid()); }
#[test]
fn test_date_from_str_invalid() {
assert!("invalid".parse::<Date>().is_err());
assert!("2020".parse::<Date>().is_err());
assert!("2020-13".parse::<Date>().is_err());
assert!("not-a-date".parse::<Date>().is_err());
assert!("2020/01/01".parse::<Date>().is_err());
}
#[test]
fn test_edge_cases() {
let date = Date::new(1, 1, 1);
assert!(date.valid());
assert_eq!(date.to_days(), 1);
let date = Date::new(9999, 12, 31);
assert!(date.valid());
assert!(Date::new(2000, 1, 1).leap_year());
assert!(!Date::new(1900, 1, 1).leap_year());
assert!(Date::new(2004, 1, 1).leap_year());
assert!(!Date::new(2001, 1, 1).leap_year());
}
#[test]
fn test_quarter_calculation() {
assert_eq!(Date::new(2020, 1, 1).quarter(), 1);
assert_eq!(Date::new(2020, 3, 31).quarter(), 1);
assert_eq!(Date::new(2020, 4, 1).quarter(), 2);
assert_eq!(Date::new(2020, 6, 30).quarter(), 2);
assert_eq!(Date::new(2020, 7, 1).quarter(), 3);
assert_eq!(Date::new(2020, 9, 30).quarter(), 3);
assert_eq!(Date::new(2020, 10, 1).quarter(), 4);
assert_eq!(Date::new(2020, 12, 31).quarter(), 4);
}
#[test]
fn test_weekend_weekday() {
assert!(Date::new(2021, 8, 7).is_weekend());
assert!(Date::new(2021, 8, 8).is_weekend());
assert!(!Date::new(2021, 8, 9).is_weekend());
assert!(Date::new(2021, 8, 9).is_week_day());
assert!(Date::new(2021, 8, 10).is_week_day());
assert!(!Date::new(2021, 8, 7).is_week_day());
assert!(!Date::new(2021, 8, 8).is_week_day());
}
#[test]
fn test_days_to_next_year() {
assert_eq!(Date::new(2020, 1, 1).days_to_next_year(), 365);
assert_eq!(Date::new(2021, 1, 1).days_to_next_year(), 364);
assert_eq!(Date::new(2020, 12, 31).days_to_next_year(), 0);
assert_eq!(Date::new(2020, 6, 15).days_to_next_year(), 199);
}
#[test]
fn test_year_day() {
assert_eq!(Date::new(2020, 1, 1).year_day(), 1);
assert_eq!(Date::new(2020, 1, 31).year_day(), 31);
assert_eq!(Date::new(2020, 2, 1).year_day(), 32);
assert_eq!(Date::new(2020, 2, 29).year_day(), 60);
assert_eq!(Date::new(2021, 2, 28).year_day(), 59);
assert_eq!(Date::new(2020, 12, 31).year_day(), 366);
assert_eq!(Date::new(2021, 12, 31).year_day(), 365);
}
#[cfg(feature = "serde")]
mod serde_tests {
use super::*;
use serde_json;
#[test]
#[cfg(not(feature = "serde-struct"))]
fn test_serde_unix_epoch() {
let date = Date::new(1970, 1, 1);
let json = serde_json::to_string(&date).unwrap();
assert_eq!(json, "0");
let deserialized: Date = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, date);
let date = Date::new(2024, 1, 15);
let json = serde_json::to_string(&date).unwrap();
let deserialized: Date = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, date);
let date = Date::new(2020, 1, 1);
let expected_seconds = date.to_seconds_from_unix_epoch(false);
let json = serde_json::to_string(&date).unwrap();
assert_eq!(json, expected_seconds.to_string());
let deserialized: Date = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, date);
}
#[test]
#[cfg(feature = "serde-struct")]
fn test_serde_struct() {
let date = Date::new(2024, 1, 15);
let json = serde_json::to_string(&date).unwrap();
assert!(json.contains("\"year\":2024"));
assert!(json.contains("\"month\":1"));
assert!(json.contains("\"day\":15"));
let deserialized: Date = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, date);
let date = Date::new(1970, 1, 1);
let json = serde_json::to_string(&date).unwrap();
let deserialized: Date = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, date);
let date = Date::new(2020, 2, 29); let json = serde_json::to_string(&date).unwrap();
let deserialized: Date = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, date);
}
#[test]
fn test_serde_roundtrip() {
let dates = vec![
Date::new(1970, 1, 1),
Date::new(2020, 1, 1),
Date::new(2024, 1, 15),
Date::new(2020, 2, 29),
Date::new(2021, 12, 31),
];
for date in dates {
let json = serde_json::to_string(&date).unwrap();
let deserialized: Date = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, date, "Failed roundtrip for date: {}", date);
}
}
}
}