use std::{
convert::TryFrom,
error::Error,
fmt::{Display, Formatter, Result as FmtResult},
num::ParseIntError,
str::FromStr,
};
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub enum ScriptDateErrorPart {
Day,
Month,
Year,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ScriptDateError {
DayTooLarge(u8),
DayTooSmall(u8),
InvalidFormat {
part: ScriptDateErrorPart,
source: ParseIntError,
value: String,
},
MissingPart(ScriptDateErrorPart),
MonthTooLarge(u8),
MonthTooSmall(u8),
}
impl Display for ScriptDateError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
f.write_str(self.description())
}
}
impl Error for ScriptDateError {
fn description(&self) -> &str {
use self::ScriptDateError::*;
match self {
DayTooLarge(_) => "The provided day was too large, must be 1-31",
DayTooSmall(_) => "The provided day was 0, must be 1-31",
InvalidFormat { .. } => "One of the date parts was not an integer",
MissingPart(_) => "A part of the date format was missing",
MonthTooLarge(_) => "The provided month was too large, must be 1-12",
MonthTooSmall(_) => "The provided month was 0, must be 1-12",
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialOrd, Ord)]
pub struct ScriptDate {
pub year: u16,
pub month: u8,
pub day: u8,
}
impl ScriptDate {
pub const fn new(year: u16, month: u8, day: u8) -> Self {
Self { day, month, year }
}
}
impl PartialEq for ScriptDate {
fn eq(&self, other: &Self) -> bool {
(self.year, self.month, self.day) == (other.year, other.month, other.day)
}
}
impl TryFrom<(u16, u8, u8)> for ScriptDate {
type Error = ScriptDateError;
fn try_from((year, month, day): (u16, u8, u8)) -> Result<Self, Self::Error> {
if month > 12 {
Err(ScriptDateError::MonthTooLarge(month))
} else if month == 0 {
Err(ScriptDateError::MonthTooSmall(month))
} else if day > 31 {
Err(ScriptDateError::DayTooLarge(day))
} else if day == 0 {
Err(ScriptDateError::DayTooSmall(day))
} else {
Ok(Self { day, month, year })
}
}
}
impl FromStr for ScriptDate {
type Err = ScriptDateError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use self::{
ScriptDateError::*,
ScriptDateErrorPart::*,
};
let mut parts = s.split('-');
let year = parts.next().ok_or(MissingPart(Year))?;
let year = year.parse().map_err(|source| InvalidFormat {
part: Year,
source,
value: year.to_owned(),
})?;
let month = parts.next().ok_or(MissingPart(Month))?;
let month = month.parse().map_err(|source| InvalidFormat {
part: Month,
source,
value: month.to_owned(),
})?;
if month == 0 {
return Err(MonthTooSmall(month));
} else if month > 12 {
return Err(MonthTooLarge(month));
}
let day_str = parts.next().ok_or(MissingPart(Day))?;
let day = day_str.parse().map_err(|source| InvalidFormat {
part: Day,
source,
value: day_str.to_owned(),
})?;
if day == 0 {
return Err(DayTooSmall(day));
} else if day > 31 {
return Err(DayTooLarge(day));
}
Ok(Self { day, month, year })
}
}
#[cfg(test)]
mod tests {
use std::{
convert::TryFrom,
error::Error,
};
use super::ScriptDate;
#[test]
fn test_inits() {
assert!(ScriptDate::try_from((2000, 1, 1)).is_ok());
assert!(ScriptDate::try_from((2000, 0, 1)).is_err());
assert!(ScriptDate::try_from((2000, 13, 1)).is_err());
assert!(ScriptDate::try_from((2000, 1, 0)).is_err());
assert!(ScriptDate::try_from((2000, 1, 32)).is_err());
}
#[test]
fn test_eqs() -> Result<(), Box<dyn Error>> {
let date1 = ScriptDate::try_from((2000, 1, 1))?;
let date2 = ScriptDate::try_from((2000, 1, 2))?;
assert_ne!(date1, date2);
assert_eq!(date1, ScriptDate::try_from((2000, 1, 1))?);
assert!(date1 < date2);
assert!(date1 <= date2);
assert!(date2 > date1);
assert!(date2 >= date1);
Ok(())
}
}