use serde::{Deserialize, Deserializer, Serialize};
use std::cmp::Ordering;
use thiserror::Error;
use time::OffsetDateTime;
pub const MIN_YEAR: i64 = -50000;
pub const MAX_YEAR: i64 = 10000;
#[derive(Error, Debug, Clone)]
pub enum DateError {
#[error("Day `{0}` is not allowed")]
InvalidDay(i64),
#[error("Month `{0}` is not allowed")]
InvalidMonth(i64),
#[error("Month `{0}` is not allowed")]
InvalidYear(i64),
#[error("e.g. can't set day without setting month")]
InvalidFields,
}
#[derive(Serialize, PartialEq, Eq, Clone, Copy, Debug, Hash)]
pub struct Date {
day: Option<Day>,
month: Option<Month>,
year: Year,
}
#[rustfmt::skip]
#[derive(derive_more::Display, Serialize, Eq, PartialEq, Clone, Copy, Debug, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[cfg_attr(feature = "sqlx", sqlx(transparent))]
pub struct Day(u8);
#[rustfmt::skip]
#[derive(derive_more::Display, Serialize, Eq, PartialEq, Clone, Copy, Debug, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[cfg_attr(feature = "sqlx", sqlx(transparent))]
pub struct Month(u8);
#[rustfmt::skip]
#[derive(derive_more::Display, Serialize, Eq, PartialEq, Clone, Copy, Debug, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[cfg_attr(feature = "sqlx", sqlx(transparent))]
pub struct Year(i32);
impl Day {
pub fn value(&self) -> u8 {
self.0
}
pub fn current() -> Self {
Date::today().day().unwrap()
}
}
impl Month {
pub fn value(&self) -> u8 {
self.0
}
pub fn current() -> Self {
Date::today().month().unwrap()
}
}
impl Year {
pub fn value(&self) -> i32 {
self.0
}
pub fn min() -> Self {
Year(MIN_YEAR as i32)
}
pub fn max() -> Self {
Year(MAX_YEAR as i32)
}
pub fn current() -> Self {
Date::today().year()
}
}
impl TryFrom<i64> for Day {
type Error = DateError;
fn try_from(value: i64) -> Result<Self, Self::Error> {
if (1..=31).contains(&value) {
Ok(Day(value as u8))
} else {
Err(DateError::InvalidDay(value))
}
}
}
impl TryFrom<i64> for Month {
type Error = DateError;
fn try_from(value: i64) -> Result<Self, Self::Error> {
if (1..=12).contains(&value) {
Ok(Month(value as u8))
} else {
Err(DateError::InvalidMonth(value))
}
}
}
impl TryFrom<i64> for Year {
type Error = DateError;
fn try_from(value: i64) -> Result<Self, Self::Error> {
if (MIN_YEAR..=MAX_YEAR).contains(&value) {
Ok(Year(value as i32))
} else {
Err(DateError::InvalidYear(value))
}
}
}
impl From<time::Month> for Month {
fn from(month: time::Month) -> Self {
Self(month.into())
}
}
impl From<Month> for time::Month {
fn from(month: Month) -> Self {
Self::try_from(month.value()).unwrap()
}
}
impl<'de> Deserialize<'de> for Day {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = i64::deserialize(deserializer)?;
Day::try_from(value).map_err(|e| serde::de::Error::custom(format!("{:?}", e)))
}
}
impl<'de> Deserialize<'de> for Month {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = i64::deserialize(deserializer)?;
Month::try_from(value).map_err(|e| serde::de::Error::custom(format!("{:?}", e)))
}
}
impl<'de> Deserialize<'de> for Year {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = i64::deserialize(deserializer)?;
Year::try_from(value).map_err(|e| serde::de::Error::custom(format!("{:?}", e)))
}
}
impl Date {
pub fn today() -> Self {
let today = OffsetDateTime::now_utc();
let month: Month = today.month().into();
Self::from(
Some(today.day().into()),
Some(month.value().into()),
today.year().into(),
)
.unwrap()
}
pub fn from(day: Option<i64>, month: Option<i64>, year: i64) -> Result<Date, DateError> {
let mut date = Date {
day: None,
month: None,
year: Year(0),
};
date.set_year(year)?;
date.set_month(month)?;
date.set_day(day)?;
Ok(date)
}
pub fn as_long_date_format(&self) -> String {
let day = match self.day() {
Some(day) => format!("{day}"),
None => String::new(),
};
let month = match self.month() {
Some(month) => match month.value() {
1 => "Jan",
2 => "Feb",
3 => "Mar",
4 => "Apr",
5 => "May",
6 => "Jun",
7 => "Jul",
8 => "Aug",
9 => "Sep",
10 => "Oct",
11 => "Nov",
12 => "Dec",
_ => panic!("Month value must be 1 <= x <= 12"),
},
None => "",
};
let year = self.year();
format!("{day} {month} {year}").trim().to_string()
}
pub fn as_short_date_format(&self) -> String {
let day = match self.day() {
Some(day) => format!("{day}"),
None => String::from("-"),
};
let month = match self.month() {
Some(month) => format!("{month}"),
None => String::from("-"),
};
let year = format!("{}", self.year());
format!("{day} / {month} / {year}")
}
pub fn set_day(&mut self, day: Option<i64>) -> Result<(), DateError> {
match day {
None => self.day = None,
Some(day) => {
let mut new_date = *self;
new_date.day = Some(Day::try_from(day)?);
new_date.is_valid()?;
*self = new_date;
}
}
Ok(())
}
pub fn set_month(&mut self, month: Option<i64>) -> Result<(), DateError> {
match month {
None => self.month = None,
Some(month) => {
let mut new_date = *self;
new_date.month = Some(Month::try_from(month)?);
new_date.is_valid()?;
*self = new_date;
}
}
Ok(())
}
pub fn set_year(&mut self, year: i64) -> Result<(), DateError> {
let mut new_date = *self;
new_date.year = Year::try_from(year)?;
new_date.is_valid()?;
*self = new_date;
Ok(())
}
pub fn day(&self) -> Option<Day> {
self.day
}
pub fn month(&self) -> Option<Month> {
self.month
}
pub fn year(&self) -> Year {
self.year
}
fn is_valid(&self) -> Result<(), DateError> {
match (self.day, self.month, self.year) {
(None, None, _) => Ok(()),
(None, Some(_), _) => Ok(()),
(Some(_), Some(_), _) => Ok(()),
_ => Err(DateError::InvalidFields),
}
}
}
impl PartialOrd for Date {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match self.year.cmp(&other.year) {
Ordering::Less => return Some(Ordering::Less),
Ordering::Greater => return Some(Ordering::Greater),
Ordering::Equal => (),
};
if let (Some(this_month), Some(other_month)) = (self.month, other.month) {
match this_month.cmp(&other_month) {
Ordering::Less => return Some(Ordering::Less),
Ordering::Greater => return Some(Ordering::Greater),
Ordering::Equal => (),
};
} else {
return None;
}
if let (Some(this_day), Some(other_day)) = (self.day, other.day) {
match this_day.cmp(&other_day) {
Ordering::Less => Some(Ordering::Less),
Ordering::Greater => Some(Ordering::Greater),
Ordering::Equal => Some(Ordering::Equal),
}
} else {
None
}
}
}
impl Ord for Date {
fn cmp(&self, other: &Self) -> Ordering {
let this_month = self.month().map(|m| m.value()).unwrap_or(1);
let other_month = other.month().map(|m| m.value()).unwrap_or(1);
let this_day = self.day().map(|d| d.value()).unwrap_or(1);
let other_day = other.day().map(|d| d.value()).unwrap_or(1);
(self.year, this_month, this_day).cmp(&(other.year, other_month, other_day))
}
}
#[derive(Deserialize)]
struct RawDate {
day: Option<i64>,
month: Option<i64>,
year: i64,
}
impl<'de> Deserialize<'de> for Date {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw_date = RawDate::deserialize(deserializer)?;
let date = Date::from(raw_date.day, raw_date.month, raw_date.year);
match date {
Ok(date) => Ok(date),
Err(error) => Err(serde::de::Error::custom(error)),
}
}
}
#[cfg(test)]
mod test {
use super::Date;
#[test]
fn from() {
assert!(Date::from(Some(1), None, 234).is_err());
assert!(Date::from(None, None, 999_999).is_err());
assert!(Date::from(None, None, -999_999).is_err());
assert!(Date::from(Some(0), Some(0), 1234).is_err());
assert!(Date::from(Some(32), Some(13), 1234).is_err());
assert!(Date::from(Some(1), Some(1), 1).is_ok());
}
#[test]
fn cmp() {
let date_1 = Date::from(None, None, 234).unwrap();
let date_2 = Date::from(None, None, 4321).unwrap();
assert!(date_2 > date_1);
assert!(date_1 < date_2);
assert!(date_1 == date_1);
assert!(date_1 != date_2);
let date_1 = Date::from(Some(1), Some(1), 234).unwrap();
let date_2 = Date::from(Some(2), Some(1), 234).unwrap();
assert!(date_2 > date_1);
}
#[test]
fn today() {
let today = Date::today();
println!("today = {}", today.as_long_date_format());
println!("today = {}", today.as_short_date_format());
}
}