1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
use {
std::{
fmt,
num::ParseIntError,
},
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum DateParseError {
#[error("unexpected end")]
UnexpectedEnd,
#[error("invalid day {0:?}")]
InvalidDay(u8),
#[error("date is ambiguous in context {0:?}")]
AmbiguousDate(String),
#[error("invalid month {0:?}")]
InvalidMonth(u8),
#[error("unrecognized month {0:?}")]
UnrecognizedMonth(String),
#[error("expected int")]
IntExpected(#[from] ParseIntError),
}
static MONTHS_3_LETTERS: &[&str] = &[
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Date {
pub year: u16,
pub month: u8,
pub day: u8,
}
impl Date {
pub fn new(year: u16, month: u8, day: u8) -> Result<Self, DateParseError> {
if day < 1 || day > 31 {
return Err(DateParseError::InvalidDay(day));
}
if month < 1 || month > 12 {
return Err(DateParseError::InvalidMonth(month));
}
Ok(Self { year, month, day })
}
pub fn from_nginx(s: &str) -> Result<Self, DateParseError> {
if s.len()<11 {
return Err(DateParseError::UnexpectedEnd);
}
let day = s[0..2].parse()?;
let year = s[7..11].parse()?;
let month = &s[3..6];
let month = MONTHS_3_LETTERS
.iter()
.position(|&m| m == month)
.ok_or_else(|| DateParseError::UnrecognizedMonth(s.to_string()))?;
let month = (month + 1) as u8;
Self::new(year, month, day)
}
pub fn with_implicit(
s: &str,
default_year: Option<u16>,
default_month: Option<u8>,
) -> Result<Self, DateParseError> {
let mut t = s.split('/');
match (t.next(), t.next(), t.next()) {
(Some(year), Some(month), Some(day)) => {
Date::new(year.parse()?, month.parse()?, day.parse()?)
}
(Some(month), Some(day), None) => {
if let Some(year) = default_year {
Date::new(year, month.parse()?, day.parse()?)
} else {
Err(DateParseError::AmbiguousDate(s.to_string()))
}
}
(Some(day), None, None) => {
if let (Some(year), Some(month)) = (default_year, default_month) {
Date::new(year, month, day.parse()?)
} else {
Err(DateParseError::AmbiguousDate(s.to_string()))
}
}
_ => unsafe { std::hint::unreachable_unchecked() },
}
}
}
impl fmt::Display for Date {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{:0>2}/{:0>2}", self.year, self.month, self.day)
}
}
#[cfg(test)]
mod date_parsing_tests {
use super::*;
#[test]
fn parse_nginx_date() {
assert_eq!(
Date::from_nginx("10/Jan/2021:10:27:01 +0000").unwrap(),
Date::new(2021, 1, 10).unwrap(),
);
}
}