rust-version 0.1.0

Crate for parsing Rust versions.
Documentation
use std::char::from_digit;
use std::convert::TryFrom;
use std::error::Error;
use std::fmt;
use std::str::from_utf8_unchecked;

use parse::{parse_uint, ParseUintError, Parseable};

/// Rust release date.
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Date {
    year: u32,
    month: u8,
    day: u8,
}
impl Date {
    /// The triple of (year, month, day).
    pub fn triple(self) -> (u32, u8, u8) {
        (self.year, self.month, self.day)
    }

    /// The pair of (month, day).
    pub fn pair(self) -> (u8, u8) {
        (self.month, self.day)
    }

    /// The year.
    pub fn year(self) -> u32 {
        self.year
    }

    /// The month, from 1 to 12
    pub fn month(self) -> u8 {
        self.month
    }

    /// The day, from 1 to 31.
    pub fn day(self) -> u8 {
        self.day
    }

    /// Writes the date to a buffer.
    pub(crate) fn write_to(&self, buf: &mut [u8]) -> usize {
        let len = self.year.write_to(buf);
        buf[len] = b'-';
        buf[len + 1] = from_digit(u32::from(self.month / 10), 10).unwrap() as u8;
        buf[len + 2] = from_digit(u32::from(self.month % 10), 10).unwrap() as u8;
        buf[len + 3] = b'-';
        buf[len + 4] = from_digit(u32::from(self.day / 10), 10).unwrap() as u8;
        buf[len + 5] = from_digit(u32::from(self.day % 10), 10).unwrap() as u8;
        len + 6
    }
}

/// The first representable date (2015-11-07).
impl Default for Date {
    fn default() -> Date {
        Date {
            year: 2014,
            month: 11,
            day: 7,
        }
    }
}

impl TryFrom<(u32, u8, u8)> for Date {
    type Error = ParseDateError<'static>;
    fn try_from((year, month, day): (u32, u8, u8)) -> Result<Date, ParseDateError<'static>> {
        // disallow dates before Rust had nightlies
        if (year, month, day) < (2015, 11, 7) {
            return Err(ParseDateError::BeforeRust { year, month, day });
        }

        // validate months and days
        match (month, day) {
            // remove days that never exist
            (2, 30) | (2, 31) | (4, 31) | (6, 31) | (9, 31) | (11, 31) => {
                Err(ParseDateError::MonthDay { month, day })
            }

            // remove leap day on non-leap-years
            (2, 29) if year % 4 != 0 || (year % 100 == 0 && year % 400 != 0) => {
                Err(ParseDateError::LeapDay { year })
            }

            // ensure valid month/day
            (1...12, 1...31) => Ok(Date { year, month, day }),

            _ => Err(ParseDateError::MonthDay { month, day }),
        }
    }
}
impl fmt::Debug for Date {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        fmt::Display::fmt(&self, f)
    }
}
impl fmt::Display for Date {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let mut buf = *b"4294967295-12-31";
        let len = self.write_to(&mut buf);
        f.pad(unsafe { from_utf8_unchecked(&buf[..len]) })
    }
}

impl<'a> TryFrom<&'a [u8]> for Date {
    type Error = ParseDateError<'a>;
    fn try_from(bytes: &'a [u8]) -> Result<Date, ParseDateError<'a>> {
        if let Some(idx) = bytes.iter().position(|b| *b == b'-') {
            let (year, rest) = bytes.split_at(idx);
            let rest = &rest[1..];
            if let Some(idx) = rest.iter().position(|b| *b == b'-') {
                let (month, rest) = rest.split_at(idx);
                if rest[1..].contains(&b'-') {
                    return Err(ParseDateError::Format(bytes));
                }

                let day = &rest[1..];
                let year = match parse_uint(year) {
                    Err(ParseUintError::Empty) => Err(ParseDateError::Format(bytes)),
                    Err(ParseUintError::BadByte(_)) => Err(ParseDateError::Number(year)),
                    Err(ParseUintError::Overflow) => Err(ParseDateError::OverflowYear(year)),
                    Ok(n) => Ok(n),
                }?;
                let month = match parse_uint(month) {
                    Err(ParseUintError::Empty) => Err(ParseDateError::Format(bytes)),
                    Err(ParseUintError::BadByte(_)) => Err(ParseDateError::Number(month)),
                    Err(ParseUintError::Overflow) => Err(ParseDateError::OverflowMonthDay(month)),
                    Ok(n) => Ok(n),
                }?;
                let day = match parse_uint(day) {
                    Err(ParseUintError::Empty) => Err(ParseDateError::Format(bytes)),
                    Err(ParseUintError::BadByte(_)) => Err(ParseDateError::Number(day)),
                    Err(ParseUintError::Overflow) => Err(ParseDateError::OverflowMonthDay(day)),
                    Ok(n) => Ok(n),
                }?;
                return Date::try_from((year, month, day));
            }
        }
        Err(ParseDateError::Format(bytes))
    }
}
impl<'a> TryFrom<&'a str> for Date {
    type Error = ParseDateError<'a>;
    fn try_from(s: &'a str) -> Result<Date, ParseDateError<'a>> {
        Date::try_from(s.as_bytes())
    }
}

/// Error encountered when parsing a [`Date`].
///
/// [`Date`]: struct.Date.html
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
pub enum ParseDateError<'a> {
    /// The string wasn't in the format `"Y-M-D"`.
    Format(&'a [u8]),

    /// The given number was not valid.
    Number(&'a [u8]),

    /// The given number was too large to be a proper year.
    OverflowYear(&'a [u8]),

    /// The given number was too large to be a proper month or day.
    OverflowMonthDay(&'a [u8]),

    /// The given month/day pair will never occur.
    MonthDay { month: u8, day: u8 },

    /// February 29th doesn't exist on the given year.
    LeapDay { year: u32 },

    /// The given date occurred before the first release of Rust.
    ///
    /// It might not even be an actual date.
    BeforeRust { year: u32, month: u8, day: u8 },
}
impl<'a> fmt::Debug for ParseDateError<'a> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            ParseDateError::Format(bytes) => {
                f.debug_tuple("ParseDateError::Format")
                    .field(&String::from_utf8_lossy(bytes))
                    .finish()
            }
            ParseDateError::Number(bytes) => {
                f.debug_tuple("ParseDateError::Number")
                    .field(&String::from_utf8_lossy(bytes))
                    .finish()
            }
            ParseDateError::OverflowYear(bytes) => {
                f.debug_tuple("ParseDateError::OverflowYear")
                    .field(&String::from_utf8_lossy(bytes))
                    .finish()
            }
            ParseDateError::OverflowMonthDay(bytes) => {
                f.debug_tuple("ParseDateError::OverflowMonthDay")
                    .field(&String::from_utf8_lossy(bytes))
                    .finish()
            }
            ParseDateError::MonthDay { month, day } => {
                f.debug_struct("ParseDateError::MonthDay")
                    .field("month", &month)
                    .field("day", &day)
                    .finish()
            }
            ParseDateError::LeapDay { year } => {
                f.debug_struct("ParseDateError::LeapDay")
                    .field("year", &year)
                    .finish()
            }
            ParseDateError::BeforeRust { year, month, day } => {
                f.debug_struct("ParseDateError::BeforeRust")
                    .field("year", &year)
                    .field("month", &month)
                    .field("day", &day)
                    .finish()
            }
        }
    }
}
impl<'a> fmt::Display for ParseDateError<'a> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            ParseDateError::Format(bytes) => {
                write!(
                    f,
                    "could not parse {:?} as \"Y-M-D\"",
                    String::from_utf8_lossy(bytes)
                )
            }
            ParseDateError::Number(bytes) => {
                write!(
                    f,
                    "could not parse {:?} as a non-negative number",
                    String::from_utf8_lossy(bytes)
                )
            }
            ParseDateError::OverflowYear(bytes) => {
                write!(
                    f,
                    "could not parse {:?}; was greater than 2^32-1",
                    String::from_utf8_lossy(bytes)
                )
            }
            ParseDateError::OverflowMonthDay(bytes) => {
                write!(
                    f,
                    "could not parse {:?}; was greater than 255",
                    String::from_utf8_lossy(bytes)
                )
            }
            ParseDateError::MonthDay { month, day } => {
                write!(f, "{:02}-{:02} is not a valid month-day pair", month, day)
            }
            ParseDateError::LeapDay { year } => write!(f, "{:?} was not a leap year", year),
            ParseDateError::BeforeRust { year, month, day } => {
                write!(
                    f,
                    "{:04}-{:02}-{:02} was before Rust existed",
                    year,
                    month,
                    day
                )
            }
        }
    }
}
impl<'a> Error for ParseDateError<'a> {
    fn description(&self) -> &str {
        match *self {
            ParseDateError::Format(_) => "could not parse as \"Y-M-D\"",
            ParseDateError::Number(_) => {
                "could not parse \"Y-M-D\" with Y, M, and D as non-negative numbers "
            }
            ParseDateError::OverflowYear(_) => "could not parse year; was greater than 2^32-1",
            ParseDateError::OverflowMonthDay(_) => {
                "could not parse month or day; was greater than 255"
            }
            ParseDateError::MonthDay { .. } => "month-day pair is invalid",
            ParseDateError::LeapDay { .. } => "leap day does not exist on given year",
            ParseDateError::BeforeRust { .. } => "date given was before Rust existed",
        }
    }
}

#[cfg(test)]
mod tests {
    use std::convert::TryFrom;

    use super::{Date, ParseDateError};

    #[test]
    fn format() {
        assert_eq!(
            Date::try_from("2017-01"),
            Err(ParseDateError::Format(b"2017-01"))
        );
        assert_eq!(
            Date::try_from("2017-01-01-01"),
            Err(ParseDateError::Format(b"2017-01-01-01"))
        );
        assert_eq!(Date::try_from("2017"), Err(ParseDateError::Format(b"2017")));
    }

    #[test]
    fn number() {
        assert_eq!(
            Date::try_from("oops-01-01"),
            Err(ParseDateError::Number(b"oops"))
        );
        assert_eq!(
            Date::try_from("2017-oops-01"),
            Err(ParseDateError::Number(b"oops"))
        );
        assert_eq!(
            Date::try_from("2017-01-oops"),
            Err(ParseDateError::Number(b"oops"))
        );
    }

    #[test]
    fn overflow() {
        assert_eq!(
            Date::try_from("12345678912345-01-01"),
            Err(ParseDateError::OverflowYear(b"12345678912345"))
        );
        assert_eq!(
            Date::try_from("2017-300-01"),
            Err(ParseDateError::OverflowMonthDay(b"300"))
        );
        assert_eq!(
            Date::try_from("2017-01-400"),
            Err(ParseDateError::OverflowMonthDay(b"400"))
        );
    }

    #[test]
    fn month_day() {
        assert_eq!(
            Date::try_from("2017-02-30"),
            Err(ParseDateError::MonthDay { month: 2, day: 30 })
        );
        assert_eq!(
            Date::try_from("2017-02-31"),
            Err(ParseDateError::MonthDay { month: 2, day: 31 })
        );
        assert_eq!(
            Date::try_from("2017-04-31"),
            Err(ParseDateError::MonthDay { month: 4, day: 31 })
        );
        assert_eq!(
            Date::try_from("2017-06-31"),
            Err(ParseDateError::MonthDay { month: 6, day: 31 })
        );
        assert_eq!(
            Date::try_from("2017-09-31"),
            Err(ParseDateError::MonthDay { month: 9, day: 31 })
        );
        assert_eq!(
            Date::try_from("2017-11-31"),
            Err(ParseDateError::MonthDay { month: 11, day: 31 })
        );
        assert_eq!(
            Date::try_from("2017-02-17"),
            Ok(Date {
                year: 2017,
                month: 2,
                day: 17,
            })
        );
    }

    #[test]
    fn leap_day() {
        assert_eq!(
            Date::try_from("2017-02-29"),
            Err(ParseDateError::LeapDay { year: 2017 })
        );
        assert_eq!(
            Date::try_from("2016-02-29"),
            Ok(Date {
                year: 2016,
                month: 2,
                day: 29,
            })
        );
    }

    #[test]
    fn before_rust() {
        assert_eq!(
            Date::try_from("1234-56-78"),
            Err(ParseDateError::BeforeRust {
                year: 1234,
                month: 56,
                day: 78,
            })
        );
        assert_eq!(
            Date::try_from("2000-01-01"),
            Err(ParseDateError::BeforeRust {
                year: 2000,
                month: 1,
                day: 1,
            })
        );
    }

    #[test]
    fn parse_display() {
        assert_eq!(
            Date::try_from("4444444-02-29").unwrap().to_string(),
            "4444444-02-29"
        );
        assert_eq!(
            Date::try_from("2017-12-01").unwrap().to_string(),
            "2017-12-01"
        );
    }

}