iso15924 0.1.0

ISO 15924 data.
Documentation
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 {
    /// The day was missing from the provided date.
    Day,
    /// The month was missing from the provided date.
    Month,
    /// The year was missing from the provided date.
    Year,
}

/// Error enum when parsing a [`ScriptDate`] fails.
///
/// [`ScriptDate`]: struct.ScriptDate.html
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ScriptDateError {
    /// When a `day` is provided that is greater than `31`.
    DayTooLarge(u8),
    /// When a `day` is provided that is less than `1`.
    DayTooSmall(u8),
    /// The provided date was in the wrong format.
    InvalidFormat {
        /// The part that couldn't be parsed.
        part: ScriptDateErrorPart,
        /// The error itself from the failed parsing.
        source: ParseIntError,
        /// The value that couldn't be parsed.
        value: String,
    },
    /// A part of the date is missing.
    ///
    /// The name of the part is provided.
    MissingPart(ScriptDateErrorPart),
    /// A `month` is provided that is greater than `12`.
    MonthTooLarge(u8),
    /// A `month` is provided that is less than `1`.
    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",
        }
    }
}

/// Representation of a date of introduction for a script code. This contains
/// `year`, `month`, and `day` public fields of `u16` type.
///
/// # Examples
///
/// Create a new valid `ScriptDate`:
///
/// ```rust
/// use iso15924::ScriptDate;
///
/// let _ = ScriptDate::new(1976, 7, 2);
/// ```
///
/// Check if a `ScriptDate` is equal to, less than, etc. another `ScriptDate`:
///
/// ```rust
/// use iso15924::ScriptDate;
///
/// let date1 = ScriptDate::new(1997, 7, 7);
/// let date2 = ScriptDate::new(2005, 5, 5);
///
/// // Check that `date1` is not equal to `date2`.
/// assert_ne!(date1, date2);
///
/// // Check that `date1` is less than `date2`.
/// assert!(date1 < date2);
///
/// // Check that `date2` is greater than `date1`.
/// assert!(date2 > date1);
/// ```
#[derive(Clone, Copy, Debug, Eq, Hash, PartialOrd, Ord)]
pub struct ScriptDate {
    /// The year of the date that a code was introduced.
    pub year: u16,
    /// The month of the date that a code was introduced.
    pub month: u8,
    /// The day of the date that a code was introduced.
    pub day: u8,
}

impl ScriptDate {
    /// Creates a new `ScriptDate` from the given `year`, `month`, and `day`, if
    /// possible.
    ///
    /// This doesn't check the input for a valid month and day. If you need
    /// that, use the `TryFrom` impl.
    ///
    /// # Examples
    ///
    /// Create a new valid `ScriptDate`:
    ///
    /// ```rust
    /// use iso15924::ScriptDate;
    ///
    /// let _ = ScriptDate::new(2000, 1, 1);
    /// ```
    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(())
    }
}