use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Date {
year: i32,
month: u8,
day: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InvalidDate;
impl fmt::Display for InvalidDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("expected a real date as YYYY-MM-DD")
}
}
impl std::error::Error for InvalidDate {}
const fn is_leap(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
#[must_use]
pub const fn days_in_month(year: i32, month: u8) -> u8 {
match month {
2 if is_leap(year) => 29,
2 => 28,
4 | 6 | 9 | 11 => 30,
_ => 31,
}
}
impl Date {
pub fn new(year: i32, month: u8, day: u8) -> Result<Self, InvalidDate> {
if (1..=9999).contains(&year)
&& (1..=12).contains(&month)
&& day >= 1
&& day <= days_in_month(year, month)
{
Ok(Self { year, month, day })
} else {
Err(InvalidDate)
}
}
pub fn parse(iso: &str) -> Result<Self, InvalidDate> {
let mut parts = iso.split('-');
let (Some(y), Some(m), Some(d), None) =
(parts.next(), parts.next(), parts.next(), parts.next())
else {
return Err(InvalidDate);
};
if y.len() != 4 || m.len() != 2 || d.len() != 2 {
return Err(InvalidDate);
}
let number = |s: &str| s.parse::<u16>().map_err(|_| InvalidDate);
let year = i32::from(number(y)?);
let month = u8::try_from(number(m)?).map_err(|_| InvalidDate)?;
let day = u8::try_from(number(d)?).map_err(|_| InvalidDate)?;
Self::new(year, month, day)
}
#[must_use]
pub const fn year(self) -> i32 {
self.year
}
#[must_use]
pub const fn month(self) -> u8 {
self.month
}
#[must_use]
pub const fn day(self) -> u8 {
self.day
}
#[must_use]
pub fn to_days(self) -> i64 {
let y = i64::from(self.year) - i64::from(self.month <= 2);
let era = y.div_euclid(400);
let yoe = y - era * 400;
let m = i64::from(self.month);
let doy = (153 * (m + if m > 2 { -3 } else { 9 }) + 2) / 5 + i64::from(self.day) - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146_097 + doe - 719_468
}
#[must_use]
pub fn from_days(days: i64) -> Option<Self> {
let z = days.checked_add(719_468)?;
let era = z.div_euclid(146_097);
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let day = doy - (153 * mp + 2) / 5 + 1;
let month = if mp < 10 { mp + 3 } else { mp - 9 };
let year = yoe + era * 400 + i64::from(month <= 2);
Self::new(
i32::try_from(year).ok()?,
u8::try_from(month).ok()?,
u8::try_from(day).ok()?,
)
.ok()
}
#[must_use]
pub fn add_days(self, delta: i64) -> Option<Self> {
Self::from_days(self.to_days().checked_add(delta)?)
}
#[must_use]
pub fn weekday(self) -> u8 {
u8::try_from((self.to_days() + 4).rem_euclid(7)).unwrap_or(0)
}
#[must_use]
pub fn add_months(self, delta: i32) -> Option<Self> {
let index = self
.year
.checked_mul(12)?
.checked_add(i32::from(self.month) - 1)?
.checked_add(delta)?;
let year = index.div_euclid(12);
let month = u8::try_from(index.rem_euclid(12) + 1).ok()?;
let day = self.day.min(days_in_month(year, month));
Self::new(year, month, day).ok()
}
#[must_use]
pub fn first_of_month(self) -> Self {
Self { day: 1, ..self }
}
}
impl fmt::Display for Date {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
}
}
#[must_use]
pub fn leading_blanks(first_of_month: Date, first_day_of_week: u8) -> u8 {
(first_of_month.weekday() + 7 - first_day_of_week % 7) % 7
}
#[must_use]
pub fn key_target(
key: &dioxus::prelude::Key,
shift: bool,
rtl: bool,
current: Date,
first_day_of_week: u8,
) -> Option<Date> {
use dioxus::prelude::Key;
let weekday = i64::from((current.weekday() + 7 - first_day_of_week % 7) % 7);
match key {
Key::ArrowRight => current.add_days(if rtl { -1 } else { 1 }),
Key::ArrowLeft => current.add_days(if rtl { 1 } else { -1 }),
Key::ArrowDown => current.add_days(7),
Key::ArrowUp => current.add_days(-7),
Key::Home => current.add_days(-weekday),
Key::End => current.add_days(6 - weekday),
Key::PageDown => current.add_months(if shift { 12 } else { 1 }),
Key::PageUp => current.add_months(if shift { -12 } else { -1 }),
_ => None,
}
}