1use std::cmp::Ordering;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum DatePrecision {
28 Year,
30 Month,
32 Day,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct DateParts {
44 pub year: i32,
46 pub month: Option<u8>,
48 pub day: Option<u8>,
50}
51
52impl DateParts {
53 #[must_use]
55 pub fn precision(&self) -> DatePrecision {
56 match (self.month, self.day) {
57 (None, _) => DatePrecision::Year,
58 (Some(_), None) => DatePrecision::Month,
59 (Some(_), Some(_)) => DatePrecision::Day,
60 }
61 }
62
63 #[must_use]
66 pub fn parse(s: &str) -> Option<Self> {
67 let mut it = s.split('-');
68 let year: i32 = it.next()?.parse().ok()?;
69 if !(1..=9999).contains(&year) {
70 return None;
71 }
72 let month = match it.next() {
73 Some(m) => {
74 let m: u8 = m.parse().ok()?;
75 if !(1..=12).contains(&m) {
76 return None;
77 }
78 Some(m)
79 }
80 None => None,
81 };
82 let day = match it.next() {
83 Some(d) => {
84 month?; let d: u8 = d.parse().ok()?;
86 if !(1..=31).contains(&d) {
87 return None;
88 }
89 Some(d)
90 }
91 None => None,
92 };
93 if it.next().is_some() {
94 return None; }
96 Some(DateParts { year, month, day })
97 }
98}
99
100impl PartialOrd for DateParts {
101 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
102 if self.year != other.year {
103 return Some(self.year.cmp(&other.year));
104 }
105 indeterminate_or_equal(*self, *other)
106 }
107}
108
109fn indeterminate_or_equal(a: DateParts, b: DateParts) -> Option<Ordering> {
113 match (a.month, b.month) {
114 (None, None) => Some(Ordering::Equal),
115 (Some(_), None) | (None, Some(_)) => None,
116 (Some(m1), Some(m2)) => {
117 if m1 != m2 {
118 return Some(m1.cmp(&m2));
119 }
120 match (a.day, b.day) {
121 (None, None) => Some(Ordering::Equal),
122 (Some(_), None) | (None, Some(_)) => None,
123 (Some(d1), Some(d2)) => Some(d1.cmp(&d2)),
124 }
125 }
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct TimeParts {
132 pub hour: u8,
134 pub minute: u8,
136 pub second: u8,
138 pub fraction: Option<String>,
140}
141
142impl TimeParts {
143 #[must_use]
145 pub fn parse(s: &str) -> Option<Self> {
146 let (hms, fraction) = match s.split_once('.') {
147 Some((h, f)) if f.chars().all(|c| c.is_ascii_digit()) && !f.is_empty() => {
148 (h, Some(f.to_string()))
149 }
150 Some(_) => return None,
151 None => (s, None),
152 };
153 let mut it = hms.split(':');
154 let hour: u8 = it.next()?.parse().ok()?;
155 let minute: u8 = it.next()?.parse().ok()?;
156 let second: u8 = it.next()?.parse().ok()?;
157 if it.next().is_some() || hour > 23 || minute > 59 || second > 60 {
158 return None;
159 }
160 Some(TimeParts {
161 hour,
162 minute,
163 second,
164 fraction,
165 })
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn parse_date_precisions() {
175 assert_eq!(
176 DateParts::parse("2024").unwrap(),
177 DateParts {
178 year: 2024,
179 month: None,
180 day: None
181 }
182 );
183 assert_eq!(
184 DateParts::parse("2024-03").unwrap().precision(),
185 DatePrecision::Month
186 );
187 assert_eq!(
188 DateParts::parse("2024-03-25").unwrap().precision(),
189 DatePrecision::Day
190 );
191 }
192
193 #[test]
194 fn rejects_malformed_dates() {
195 assert!(DateParts::parse("2024-13").is_none()); assert!(DateParts::parse("2024-03-32").is_none()); assert!(DateParts::parse("2024-03-25T00:00").is_none()); assert!(DateParts::parse("").is_none());
199 }
200
201 #[test]
202 fn date_ordering_same_precision() {
203 let a = DateParts::parse("2024-03").unwrap();
204 let b = DateParts::parse("2024-05").unwrap();
205 assert_eq!(a.partial_cmp(&b), Some(Ordering::Less));
206 assert_eq!(b.partial_cmp(&a), Some(Ordering::Greater));
207 assert_eq!(a.partial_cmp(&a), Some(Ordering::Equal));
208 }
209
210 #[test]
211 fn date_ordering_different_precision() {
212 let year = DateParts::parse("2024").unwrap();
213 let month = DateParts::parse("2024-03").unwrap();
214 assert_eq!(year.partial_cmp(&month), None);
216 let other = DateParts::parse("2025-03").unwrap();
218 assert_eq!(year.partial_cmp(&other), Some(Ordering::Less));
219 }
220
221 #[test]
222 fn parse_time() {
223 let t = TimeParts::parse("13:28:17").unwrap();
224 assert_eq!((t.hour, t.minute, t.second), (13, 28, 17));
225 assert_eq!(t.fraction, None);
226 assert_eq!(
227 TimeParts::parse("13:28:17.250").unwrap().fraction,
228 Some("250".into())
229 );
230 assert!(TimeParts::parse("25:00:00").is_none());
231 }
232}