use crate::{
Date, GregorianDate, JulianDate, Month,
errors::{InvalidDayOfYear, InvalidDayOfYearCount, InvalidHistoricDate},
};
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct HistoricDate {
year: i32,
month: Month,
day: u8,
}
impl HistoricDate {
pub const fn new(year: i32, month: Month, day: u8) -> Result<Self, InvalidHistoricDate> {
if Self::is_valid_date(year, month, day) {
Ok(Self { year, month, day })
} else {
Err(InvalidHistoricDate { year, month, day })
}
}
pub const fn from_ordinal_date(year: i32, day_of_year: u16) -> Result<Self, InvalidDayOfYear> {
let is_leap_year = Self::is_leap_year(year);
let (month, day) = match month_day_from_ordinal_date(year, day_of_year, is_leap_year) {
Ok((month, day)) => (month, day),
Err(error) => return Err(error),
};
match Self::new(year, month, day) {
Ok(date) => Ok(date),
Err(err) => Err(InvalidDayOfYear::InvalidHistoricDate(err)),
}
}
pub const fn from_date(date: Date<i32>) -> Self {
const GREGORIAN_REFORM: Date<i32> = match GregorianDate::new(1582, Month::October, 15) {
Ok(date) => date.into_date(),
Err(_) => unreachable!(),
};
let is_gregorian =
date.time_since_epoch().count() >= GREGORIAN_REFORM.time_since_epoch().count();
if is_gregorian {
let date = GregorianDate::from_date(date);
Self {
year: date.year(),
month: date.month(),
day: date.day(),
}
} else {
let date = JulianDate::from_date(date);
Self {
year: date.year(),
month: date.month(),
day: date.day(),
}
}
}
pub const fn into_date(self) -> Date<i32> {
let HistoricDate { year, month, day } = self;
if self.is_gregorian() {
match GregorianDate::new(year, month, day) {
Ok(date) => date.into_date(),
Err(_) => unreachable!(),
}
} else {
match JulianDate::new(year, month, day) {
Ok(date) => date.into_date(),
Err(_) => unreachable!(),
}
}
}
pub const fn year(&self) -> i32 {
self.year
}
pub const fn month(&self) -> Month {
self.month
}
pub const fn day(&self) -> u8 {
self.day
}
pub const fn day_of_year(&self) -> u16 {
let k = if Self::is_leap_year(self.year) { 1 } else { 2 };
let m = self.month() as u16;
let d = self.day() as u16;
((275 * m) / 9) - k * ((m + 9) / 12) + d - 30
}
pub const fn is_gregorian(&self) -> bool {
self.year > 1582
|| (self.year == 1582
&& (self.month as u8 > Month::October as u8
|| (self.month as u8 == Month::October as u8 && self.day >= 15)))
}
pub const fn days_in_month(year: i32, month: Month) -> u8 {
use crate::Month::*;
match month {
January | March | May | July | August | October | December => 31,
April | June | September | November => 30,
February => {
if Self::is_leap_year(year) {
29
} else {
28
}
}
}
}
const fn is_leap_year(year: i32) -> bool {
if year > 1582 {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
} else {
year % 4 == 0
}
}
const fn is_valid_date(year: i32, month: Month, day: u8) -> bool {
day != 0
&& day <= Self::days_in_month(year, month)
&& !Self::falls_during_gregorian_reform(year, month, day)
}
const fn falls_during_gregorian_reform(year: i32, month: Month, day: u8) -> bool {
year == 1582 && month as u8 == Month::October as u8 && day > 4 && day < 15
}
}
pub(crate) const fn month_day_from_ordinal_date(
year: i32,
day_of_year: u16,
is_leap_year: bool,
) -> Result<(Month, u8), InvalidDayOfYear> {
if day_of_year == 0 || day_of_year > 366 || (day_of_year == 366 && !is_leap_year) {
return Err(InvalidDayOfYear::InvalidDayOfYearCount(
InvalidDayOfYearCount { year, day_of_year },
));
}
let k = if is_leap_year { 1 } else { 2 };
let month = if day_of_year < 32 {
1
} else {
(9 * (k + day_of_year as i32) + 269) / 275
};
let day = day_of_year as i32 - (275 * month) / 9 + k * ((month + 9) / 12) + 30;
let day = match day {
0..=32 => day as u8,
_ => unreachable!(),
};
let month = match Month::try_from(month as u8) {
Ok(month) => month,
Err(_) => unreachable!(),
};
Ok((month, day))
}
impl From<HistoricDate> for Date<i32> {
fn from(value: HistoricDate) -> Self {
value.into_date()
}
}
impl From<Date<i32>> for HistoricDate {
fn from(value: Date<i32>) -> Self {
Self::from_date(value)
}
}
impl core::fmt::Display for HistoricDate {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}-{:02}-{:02}", self.year, self.month as u8, self.day)
}
}
#[test]
fn day_of_year() {
let date1 = HistoricDate::new(1978, Month::November, 14).unwrap();
assert_eq!(date1.day_of_year(), 318);
let date2 = HistoricDate::new(1988, Month::April, 22).unwrap();
assert_eq!(date2.day_of_year(), 113);
let date3 = HistoricDate::from_ordinal_date(1978, 318).unwrap();
assert_eq!(date3, date1);
let date4 = HistoricDate::from_ordinal_date(1988, 113).unwrap();
assert_eq!(date4, date2);
}
#[test]
fn gregorian_reform() {
use crate::Days;
use crate::Month::*;
let date1 = Date::from_historic_date(1582, October, 4).unwrap();
let date2 = Date::from_historic_date(1582, October, 15).unwrap();
assert_eq!(date1 + Days::new(1), date2);
}
#[cfg(kani)]
impl kani::Arbitrary for HistoricDate {
fn any() -> Self {
let mut year: i32 = kani::any();
let month: Month = kani::any();
let mut day: u8 = kani::any::<u8>() % 32u8;
if !Self::is_valid_date(year, month, day) {
day = 1;
year = 1;
}
Self { year, month, day }
}
}
#[cfg(kani)]
mod proof_harness {
use super::*;
#[kani::proof]
fn construction_never_panics() {
let year: i32 = kani::any();
let month: Month = kani::any();
let day: u8 = kani::any();
let _ = HistoricDate::new(year, month, day);
}
#[kani::proof]
fn day_of_year_never_panics() {
let year: i32 = kani::any();
let day_of_year: u16 = kani::any();
let _ = HistoricDate::from_ordinal_date(year, day_of_year);
}
#[kani::proof]
fn day_of_year_roundtrip() {
let date: HistoricDate = kani::any();
let year = date.year();
let day_of_year = date.day_of_year();
let reconstructed = HistoricDate::from_ordinal_date(year, day_of_year).unwrap();
assert_eq!(date, reconstructed);
}
#[kani::proof]
fn date_conversion_well_defined() {
let date: Date<i32> = kani::any();
let historic_date = HistoricDate::from_date(date);
let _ = historic_date.into_date();
}
}