Skip to main content

jobberdb/
partial_date_time.rs

1//! Partial date and time like it is entered by the user.
2
3use super::prelude::*;
4use chrono::{Datelike, Local, TimeZone, Timelike, Utc};
5use regex::Regex;
6
7/// Partial date and time in different flavors.
8#[derive(PartialEq, Debug)]
9pub enum PartialDateTime {
10    /// None
11    None,
12    /// Hour and minute without date
13    HM { hour: u32, minute: u32 },
14    /// Month and day without year and time
15    MD { month: u32, day: u32 },
16    /// Complete date and time.
17    YMDHM {
18        year: i32,
19        month: u32,
20        day: u32,
21        hour: u32,
22        minute: u32,
23    },
24    /// Date and time without year.
25    MDHM {
26        month: u32,
27        day: u32,
28        hour: u32,
29        minute: u32,
30    },
31    /// Complete date without time.
32    YMD { year: i32, month: u32, day: u32 },
33}
34
35impl PartialDateTime {
36    /// Parse a partial date from optional string.
37    pub fn parse(dt: Option<String>) -> Result<Self, Error> {
38        if let Some(dt) = dt {
39            match Self::parse_date_time(dt.clone()) {
40                PartialDateTime::None => Err(Error::PartialDateTimeFormat(dt)),
41                pdt => Ok(pdt),
42            }
43        } else {
44            Ok(PartialDateTime::None)
45        }
46    }
47    /// Parse optional partial date from optional string.
48    pub fn parse_opt(dt: Option<String>) -> Self {
49        if let Some(dt) = dt {
50            Self::parse_date_time(dt)
51        } else {
52            Self::None
53        }
54    }
55    /// Take `self` and if this is `None` return `other`.
56    pub fn or(self, other: Self) -> Self {
57        match self {
58            Self::None => other,
59            _ => self,
60        }
61    }
62    /// Enrich `left` partial date with available data from the `right`.
63    fn merge(left: Self, right: Self) -> Self {
64        if let Self::HM { hour, minute } = left {
65            if let Self::YMD { year, month, day } = right {
66                return Self::YMDHM {
67                    year,
68                    month,
69                    day,
70                    hour,
71                    minute,
72                };
73            } else if let Self::MD { month, day } = right {
74                return Self::MDHM {
75                    month,
76                    day,
77                    hour,
78                    minute,
79                };
80            }
81        } else if let Self::HM { hour, minute } = right {
82            if let Self::YMD { year, month, day } = left {
83                return Self::YMDHM {
84                    year,
85                    month,
86                    day,
87                    hour,
88                    minute,
89                };
90            } else if let Self::MD { month, day } = left {
91                return Self::MDHM {
92                    month,
93                    day,
94                    hour,
95                    minute,
96                };
97            }
98        }
99        Self::None
100    }
101    /// Parse date and time from `String`.
102    fn parse_date_time(dt: String) -> Self {
103        let dt: Vec<&str> = dt.split(',').collect();
104        match dt.len() {
105            1 => Self::parse_dmy(dt[0]).or(Self::parse_mdy(dt[0]).or(Self::parse_ymd(dt[0])
106                .or(Self::parse_dm(dt[0]).or(Self::parse_md(dt[0]).or(Self::parse_hm(dt[0])))))),
107            2 => {
108                Self::merge(
109                    Self::parse_dmy(dt[0])
110                        .or(Self::parse_mdy(dt[0]).or(Self::parse_ymd(dt[0])
111                            .or(Self::parse_dm(dt[0]).or(Self::parse_md(dt[0]))))),
112                    Self::parse_hm(dt[1]),
113                )
114                .or(Self::merge(
115                    Self::parse_dmy(dt[1])
116                        .or(Self::parse_mdy(dt[1]).or(Self::parse_ymd(dt[1])
117                            .or(Self::parse_dm(dt[1]).or(Self::parse_md(dt[1]))))),
118                    Self::parse_hm(dt[0]),
119                ))
120            }
121            _ => PartialDateTime::None,
122        }
123    }
124
125    /// Parse time from "HH:MM" format.
126    fn parse_hm(dt: &str) -> Self {
127        let re = Regex::new(r"^(\d{1,2}):(\d{1,2})$").unwrap();
128        if let Some(cap) = re.captures_iter(dt).next() {
129            return Self::HM {
130                hour: cap[1].parse::<u32>().unwrap(),
131                minute: cap[2].parse::<u32>().unwrap(),
132            };
133        }
134        Self::None
135    }
136
137    /// Parse German date without year and time from "dd.mm." format.
138    fn parse_dm(dt: &str) -> Self {
139        let re = Regex::new(r"^(\d{1,2})\.(\d{1,2})\.$").unwrap();
140        if let Some(cap) = re.captures_iter(dt).next() {
141            return Self::MD {
142                month: cap[2].parse::<u32>().unwrap(),
143                day: cap[1].parse::<u32>().unwrap(),
144            };
145        }
146        Self::None
147    }
148
149    /// Parse English date without year and month from "mm/dd" format.
150    fn parse_md(dt: &str) -> Self {
151        let re = Regex::new(r"^(\d{1,2})/(\d{1,2})$").unwrap();
152        if let Some(cap) = re.captures_iter(dt).next() {
153            return Self::MD {
154                month: cap[1].parse::<u32>().unwrap(),
155                day: cap[2].parse::<u32>().unwrap(),
156            };
157        }
158        Self::None
159    }
160    /// Parse German date without year and time from "dd.mm.yyyy" format.
161    fn parse_dmy(dt: &str) -> Self {
162        let re = Regex::new(r"^(\d{1,2})\.(\d{1,2})\.(\d{4})$").unwrap();
163        if let Some(cap) = re.captures_iter(dt).next() {
164            return Self::YMD {
165                year: cap[3].parse::<i32>().unwrap(),
166                month: cap[2].parse::<u32>().unwrap(),
167                day: cap[1].parse::<u32>().unwrap(),
168            };
169        }
170        Self::None
171    }
172    /// Parse German date without year and time from "yyy-mm-dd" format.
173    fn parse_ymd(dt: &str) -> Self {
174        let re = Regex::new(r"^(\d{4})-(\d{1,2})-(\d{1,2})$").unwrap();
175        if let Some(cap) = re.captures_iter(dt).next() {
176            return Self::YMD {
177                year: cap[1].parse::<i32>().unwrap(),
178                month: cap[2].parse::<u32>().unwrap(),
179                day: cap[3].parse::<u32>().unwrap(),
180            };
181        }
182        Self::None
183    }
184    /// Parse German date without year and time from "mm/dd/yyyy" format.
185    fn parse_mdy(dt: &str) -> Self {
186        let re = Regex::new(r"^(\d{1,2})/(\d{1,2})/(\d{4})$").unwrap();
187        if let Some(cap) = re.captures_iter(dt).next() {
188            return Self::YMD {
189                year: cap[3].parse::<i32>().unwrap(),
190                month: cap[1].parse::<u32>().unwrap(),
191                day: cap[2].parse::<u32>().unwrap(),
192            };
193        }
194        Self::None
195    }
196    /// Convert partial date and time into date and time by enriching it with data from `base`.
197    pub fn into(self, base: DateTime) -> DateTime {
198        let base: chrono::DateTime<Local> = base.into();
199        chrono::DateTime::with_timezone(
200            &match self {
201                Self::HM { hour, minute } => Local
202                    .with_ymd_and_hms(base.year(), base.month(), base.day(), hour, minute, 0)
203                    .unwrap(),
204                Self::YMDHM {
205                    year,
206                    month,
207                    day,
208                    hour,
209                    minute,
210                } => Local
211                    .with_ymd_and_hms(year, month, day, hour, minute, 0)
212                    .unwrap(),
213                Self::MDHM {
214                    month,
215                    day,
216                    hour,
217                    minute,
218                } => Local
219                    .with_ymd_and_hms(base.year(), month, day, hour, minute, 0)
220                    .unwrap(),
221                Self::YMD { year, month, day } => {
222                    Local.with_ymd_and_hms(year, month, day, 0, 0, 0).unwrap()
223                }
224                Self::MD { month, day } => Local
225                    .with_ymd_and_hms(base.year(), month, day, 0, 0, 0)
226                    .unwrap(),
227                Self::None => Local
228                    .with_ymd_and_hms(
229                        base.year(),
230                        base.month(),
231                        base.day(),
232                        base.hour(),
233                        base.minute(),
234                        0,
235                    )
236                    .unwrap(),
237            },
238            &Utc,
239        )
240        .into()
241    }
242}
243
244/// Test date and time parsing.
245#[test]
246fn test_parse_date_time() {
247    PartialDateTime::parse_date_time("1.1.,12:00".into());
248}
249
250/// Test time parsing.
251#[test]
252fn test_parse_hm() {
253    assert_eq!(
254        PartialDateTime::parse_hm("01:00"),
255        PartialDateTime::HM { hour: 1, minute: 0 }
256    );
257    assert_eq!(
258        PartialDateTime::parse_hm("1:0"),
259        PartialDateTime::HM { hour: 1, minute: 0 }
260    );
261    assert_ne!(
262        PartialDateTime::parse_hm("1.0"),
263        PartialDateTime::HM { hour: 1, minute: 0 }
264    );
265}
266
267/// Test several date parsings.
268#[test]
269fn test_parse_ymd() {
270    assert_eq!(
271        PartialDateTime::parse_ymd("2023-02-01"),
272        PartialDateTime::YMD {
273            year: 2023,
274            month: 2,
275            day: 1
276        }
277    );
278    assert_eq!(
279        PartialDateTime::parse_ymd("2023-2-1"),
280        PartialDateTime::YMD {
281            year: 2023,
282            month: 2,
283            day: 1
284        }
285    );
286    assert_ne!(
287        PartialDateTime::parse_ymd("2023.2.1"),
288        PartialDateTime::YMD {
289            year: 2023,
290            month: 2,
291            day: 1
292        }
293    );
294}
295
296/// Test several date parsings.
297#[test]
298fn test_parse_mdy() {
299    assert_eq!(
300        PartialDateTime::parse_mdy("02/01/2023"),
301        PartialDateTime::YMD {
302            year: 2023,
303            month: 2,
304            day: 1
305        }
306    );
307    assert_eq!(
308        PartialDateTime::parse_mdy("2/1/2023"),
309        PartialDateTime::YMD {
310            year: 2023,
311            month: 2,
312            day: 1
313        }
314    );
315    assert_ne!(
316        PartialDateTime::parse_mdy("2.1.2023"),
317        PartialDateTime::YMD {
318            year: 2023,
319            month: 2,
320            day: 1
321        }
322    );
323}
324
325/// Test several date parsings.
326#[test]
327fn test_parse_dmy() {
328    assert_eq!(
329        PartialDateTime::parse_dmy("01.02.2023"),
330        PartialDateTime::YMD {
331            year: 2023,
332            month: 2,
333            day: 1
334        }
335    );
336    assert_eq!(
337        PartialDateTime::parse_dmy("1.2.2023"),
338        PartialDateTime::YMD {
339            year: 2023,
340            month: 2,
341            day: 1
342        }
343    );
344    assert_ne!(
345        PartialDateTime::parse_dmy("1-2-2023"),
346        PartialDateTime::YMD {
347            year: 2023,
348            month: 2,
349            day: 1
350        }
351    );
352}
353
354/// Test several date without year parsings.
355#[test]
356fn test_parse_dm() {
357    assert_eq!(
358        PartialDateTime::parse_dm("01.02."),
359        PartialDateTime::MD { month: 2, day: 1 }
360    );
361    assert_eq!(
362        PartialDateTime::parse_dm("1.2."),
363        PartialDateTime::MD { month: 2, day: 1 }
364    );
365    assert_ne!(
366        PartialDateTime::parse_dm("1.2"),
367        PartialDateTime::MD { month: 2, day: 1 }
368    );
369    assert_ne!(
370        PartialDateTime::parse_dm("1-2-"),
371        PartialDateTime::MD { month: 2, day: 1 }
372    );
373}
374
375/// Test several date without year parsings.
376#[test]
377fn test_parse_md() {
378    assert_eq!(
379        PartialDateTime::parse_md("02/01"),
380        PartialDateTime::MD { month: 2, day: 1 }
381    );
382    assert_eq!(
383        PartialDateTime::parse_md("2/1"),
384        PartialDateTime::MD { month: 2, day: 1 }
385    );
386    assert_ne!(
387        PartialDateTime::parse_md("2-1"),
388        PartialDateTime::MD { month: 2, day: 1 }
389    );
390}