1use anyhow::Error;
48use std::convert::TryFrom;
49use std::str::FromStr;
50use thiserror::Error;
51
52#[cfg(test)]
53mod tests;
54
55#[derive(Debug, PartialEq, Clone)]
56pub struct Datetime {
57 pub date: YearMonthDay,
58 pub time: HourMinuteSecond,
59}
60
61impl FromStr for Datetime {
62 type Err = DateTimeParseError;
63
64 fn from_str(s: &str) -> Result<Self, Self::Err> {
65 let mut parts = s.split('T');
66
67 let date = YearMonthDay::from_str(parts.next().ok_or_else(|| DateTimeParseError {
68 component: Component::Date,
69 found: "".to_string(),
70 kind: DateTimeParseErrorKind::ValueMissing,
71 })?)?;
72
73 let time = HourMinuteSecond::from_str(parts.next().ok_or_else(|| DateTimeParseError {
74 component: Component::Time,
75 found: "".to_string(),
76 kind: DateTimeParseErrorKind::ValueMissing,
77 })?)?;
78
79 Ok(Datetime { date, time })
80 }
81}
82
83#[derive(Debug, Error)]
84#[error("Failed to parse {component}'s value `{found}`: {kind}")]
85pub struct DateTimeParseError {
86 component: Component,
87 found: String,
88 kind: DateTimeParseErrorKind,
89}
90
91#[derive(Debug, Error)]
92pub enum DateTimeParseErrorKind {
93 #[error(transparent)]
94 InvalidNumber(Error),
95 #[error("The value is missing")]
96 ValueMissing,
97 #[error("The value must be at least {min} and at most {max}")]
98 OutOfRange { min: i32, max: i32 },
99}
100
101#[derive(Debug, PartialEq, Clone, strum::Display)]
102pub enum Component {
103 Year,
104 Month,
105 Day,
106 Hour,
107 Minute,
108 Second,
109
110 Date,
111 Time,
112}
113
114#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
115pub struct Year(i32);
116
117#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
118pub struct Month(u8);
119
120#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
121pub struct Day(u8);
122
123#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
124pub struct Hour(u8);
125
126#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
127pub struct Minute(u8);
128
129#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
130pub struct Second(f32);
131
132#[derive(Debug, PartialEq, Clone)]
133pub struct YearMonthDay {
134 year: Year,
135 month: Month,
136 day: Day,
137}
138
139#[derive(Debug, PartialEq, Clone)]
140pub struct HourMinuteSecond {
141 hour: Hour,
142 minute: Minute,
143 second: Second,
144}
145
146macro_rules! impl_parse_numeric {
147 ($component:tt, $inner:ty, $min:expr, $max:expr) => {
148 impl TryFrom<$inner> for $component {
149 type Error = DateTimeParseError;
150
151 fn try_from(value: $inner) -> Result<Self, Self::Error> {
152 if !(($min as $inner)..=($max as $inner)).contains(&value) {
153 return Err(DateTimeParseError {
154 component: Component::$component,
155 found: value.to_string(),
156 kind: DateTimeParseErrorKind::OutOfRange {
157 min: $min,
158 max: ($max - 1),
159 },
160 });
161 }
162
163 Ok(Self(value))
164 }
165 }
166
167 impl FromStr for $component {
168 type Err = DateTimeParseError;
169
170 fn from_str(value: &str) -> Result<Self, Self::Err> {
171 let inner =
172 <$inner as FromStr>::from_str(value).map_err(|source| DateTimeParseError {
173 component: Component::$component,
174 found: value.to_string(),
175 kind: DateTimeParseErrorKind::InvalidNumber(source.into()),
176 })?;
177
178 Self::try_from(inner)
179 }
180 }
181 };
182}
183
184impl_parse_numeric!(Year, i32, i32::MIN, i32::MAX);
185impl_parse_numeric!(Month, u8, 1, 13);
186impl_parse_numeric!(Day, u8, 1, 32);
187impl_parse_numeric!(Hour, u8, 0, 24);
188impl_parse_numeric!(Minute, u8, 0, 60);
189impl_parse_numeric!(Second, f32, 0, 60);
190
191impl FromStr for YearMonthDay {
192 type Err = DateTimeParseError;
193
194 fn from_str(value: &str) -> Result<Self, Self::Err> {
195 let parts: Vec<&str> = value.split('-').collect();
196
197 let year = parts.first().ok_or_else(|| DateTimeParseError {
198 found: "".to_string(),
199 component: Component::Year,
200 kind: DateTimeParseErrorKind::ValueMissing,
201 })?;
202 let month = parts.get(1).ok_or_else(|| DateTimeParseError {
203 found: "".to_string(),
204 component: Component::Month,
205 kind: DateTimeParseErrorKind::ValueMissing,
206 })?;
207 let day = parts.get(2).ok_or_else(|| DateTimeParseError {
208 found: "".to_string(),
209 component: Component::Day,
210 kind: DateTimeParseErrorKind::ValueMissing,
211 })?;
212
213 let year = Year::from_str(year)?;
214 let month = Month::from_str(month)?;
215 let day = Day::from_str(day)?;
216
217 Self::from_components(year, month, day)
218 }
219}
220
221impl YearMonthDay {
222 pub fn from_components(year: Year, month: Month, day: Day) -> Result<Self, DateTimeParseError> {
223 if !is_valid_day(year, month, day) {
224 return Err(DateTimeParseError {
225 kind: DateTimeParseErrorKind::OutOfRange {
226 min: 1,
227 max: day_in_month(year, month) as i32,
228 },
229 found: day.0.to_string(),
230 component: Component::Day,
231 });
232 }
233
234 Ok(YearMonthDay { year, month, day })
235 }
236}
237
238fn is_valid_day(year: Year, month: Month, day: Day) -> bool {
240 day.0 <= day_in_month(year, month)
241}
242
243fn day_in_month(year: Year, month: Month) -> u8 {
245 match month.0 {
246 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
247 4 | 6 | 9 | 11 => 30,
248 2 if is_leap_year(year.0) => 29,
249 2 => 28,
250 _ => unreachable!("The Month type guards against values that aren't in range (1..=12)"),
251 }
252}
253
254fn is_leap_year(year: i32) -> bool {
256 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
257}
258
259impl FromStr for HourMinuteSecond {
260 type Err = DateTimeParseError;
261
262 fn from_str(value: &str) -> Result<Self, Self::Err> {
263 let parts: Vec<&str> = value.split(':').collect();
264
265 let hour = parts.first().ok_or_else(|| DateTimeParseError {
266 component: Component::Hour,
267 found: value.to_string(),
268 kind: DateTimeParseErrorKind::ValueMissing,
269 })?;
270 let minute = parts.get(1).ok_or_else(|| DateTimeParseError {
271 component: Component::Minute,
272 found: value.to_string(),
273 kind: DateTimeParseErrorKind::ValueMissing,
274 })?;
275
276 let second = parts.get(2).unwrap_or(&"0");
277
278 Ok(HourMinuteSecond {
279 hour: Hour::from_str(hour)?,
280 minute: Minute::from_str(minute)?,
281 second: Second::from_str(second)?,
282 })
283 }
284}