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};
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Date {
year: u32,
month: u8,
day: u8,
}
impl Date {
pub fn triple(self) -> (u32, u8, u8) {
(self.year, self.month, self.day)
}
pub fn pair(self) -> (u8, u8) {
(self.month, self.day)
}
pub fn year(self) -> u32 {
self.year
}
pub fn month(self) -> u8 {
self.month
}
pub fn day(self) -> u8 {
self.day
}
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
}
}
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>> {
if (year, month, day) < (2015, 11, 7) {
return Err(ParseDateError::BeforeRust { year, month, day });
}
match (month, day) {
(2, 30) | (2, 31) | (4, 31) | (6, 31) | (9, 31) | (11, 31) => {
Err(ParseDateError::MonthDay { month, day })
}
(2, 29) if year % 4 != 0 || (year % 100 == 0 && year % 400 != 0) => {
Err(ParseDateError::LeapDay { year })
}
(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())
}
}
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
pub enum ParseDateError<'a> {
Format(&'a [u8]),
Number(&'a [u8]),
OverflowYear(&'a [u8]),
OverflowMonthDay(&'a [u8]),
MonthDay { month: u8, day: u8 },
LeapDay { year: u32 },
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"
);
}
}