use crate::prelude::*;
#[derive(
Clone,
Copy,
Debug,
Display,
PartialEq,
Eq,
Hash,
SerializeDisplay,
DeserializeFromStr,
Builder,
Getters,
)]
#[display("{year:04}-{month:02}-{day:02}")]
pub struct Date {
#[getset(get = "pub")]
year: Year,
#[getset(get = "pub")]
month: Month,
#[getset(get = "pub")]
day: Day,
}
impl std::str::FromStr for Date {
type Err = crate::prelude::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parts: Vec<&str> = s.split('-').collect();
if parts.len() != 3 {
return Err(Error::FailedToParseDate {
underlying: "Invalid Format".to_owned(),
});
}
let year = Year::from_str(parts[0])?;
let month = Month::from_str(parts[1])?;
let day = Day::from_str(parts[2])?;
Ok(Self::builder().year(year).month(month).day(day).build())
}
}
fn from_ymd_parts(year: i32, month: u32, day: u32) -> Date {
Date::builder()
.year(year.into())
.month(Month::try_from(month).expect("Invalid month"))
.day(Day::try_from(day).expect("Invalid day"))
.build()
}
impl From<NaiveDate> for Date {
fn from(value: NaiveDate) -> Self {
from_ymd_parts(value.year(), value.month(), value.day())
}
}
impl From<NaiveDateTime> for Date {
fn from(value: NaiveDateTime) -> Self {
from_ymd_parts(value.year(), value.month(), value.day())
}
}
impl Date {
pub fn to_datetime(&self) -> NaiveDateTime {
let naive_date = chrono::NaiveDate::from_ymd_opt(
**self.year() as i32,
**self.month() as u32,
**self.day() as u32,
)
.expect("Invalid date components");
naive_date
.and_hms_opt(0, 0, 0)
.expect("Invalid time components")
}
pub fn advance_days(&self, days: &Day) -> Self {
let datetime = self.to_datetime();
let days: u8 = **days;
let advanced_date = datetime + chrono::Duration::days(days as i64);
Self::from(advanced_date)
}
pub fn advance(&self, terms: &PaymentTerms) -> Self {
match terms {
PaymentTerms::Net(days) => self.advance_days(days.due_in()),
}
}
}
impl HasSample for Date {
fn sample() -> Self {
Self::builder()
.year(2025.into())
.month(Month::May)
.day(Day::try_from(31).expect("LEQ 31 days"))
.build()
}
fn sample_other() -> Self {
Self::builder()
.year(2024.into())
.month(Month::December)
.day(Day::try_from(15).expect("LEQ 31 days"))
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
use test_log::test;
type Sut = Date;
#[test]
fn equality() {
assert_eq!(Sut::sample(), Sut::sample());
assert_eq!(Sut::sample_other(), Sut::sample_other());
}
#[test]
fn inequality() {
assert_ne!(Sut::sample(), Sut::sample_other());
}
#[test]
fn test_date_from_str() {
let sut = Sut::from_str("2025-05-23").unwrap();
assert_eq!(sut.year(), &Year::from(2025));
assert_eq!(sut.month(), &Month::May);
assert_eq!(sut.day(), &Day::try_from(23).unwrap());
}
#[test]
fn test_year_month_from_str() {
let sut = YearAndMonth::from_str("2025-05").unwrap();
assert_eq!(sut.year(), &Year::from(2025));
assert_eq!(sut.month(), &Month::May);
}
#[test]
fn test_from_str_all_reasons_invalid() {
let invalid_dates = [
"2025-05-32", "99999999999-05-32", "2025-13-01", "2025-00-01", "2025-13-01", "2025-05", "2025", "05-23", "2025-05-23-01", ];
for date in invalid_dates {
assert!(Sut::from_str(date).is_err());
}
}
#[test]
fn test_from_naive_date() {
let naive_date = NaiveDate::from_ymd_opt(2025, 5, 23).unwrap();
let date: Date = naive_date.into();
assert_eq!(date.year(), &Year::from(2025));
assert_eq!(date.month(), &Month::May);
assert_eq!(date.day(), &Day::try_from(23).unwrap());
}
}