Skip to main content

haste_fhir_model/r4/datetime/
mod.rs

1use regex::Regex;
2use std::sync::LazyLock;
3
4mod reflect;
5mod serialize;
6
7#[derive(Debug, Clone, PartialEq)]
8pub enum DateTime {
9    Year(u16),
10    YearMonth(u16, u8),
11    YearMonthDay(u16, u8, u8),
12    Iso8601(chrono::DateTime<chrono::Utc>),
13}
14
15impl ToString for DateTime {
16    fn to_string(&self) -> String {
17        match self {
18            DateTime::Year(year) => year.to_string(),
19            DateTime::YearMonth(year, month) => format!("{:04}-{:02}", year, month),
20            DateTime::YearMonthDay(year, month, day) => {
21                format!("{:04}-{:02}-{:02}", year, month, day)
22            }
23            DateTime::Iso8601(dt) => dt.to_rfc3339(),
24        }
25    }
26}
27
28impl TryFrom<DateTime> for chrono::DateTime<chrono::Utc> {
29    type Error = ParseError;
30
31    fn try_from(value: DateTime) -> Result<Self, Self::Error> {
32        match value {
33            // "1996-12-19T16:39:57-08:00"
34            DateTime::Year(year) => {
35                let datetime = chrono::DateTime::parse_from_rfc3339(
36                    format!("{}-01-01T00:00:00Z", year).as_str(),
37                )
38                .map_err(|_| ParseError::InvalidFormat)?;
39
40                Ok(datetime.with_timezone(&chrono::Utc))
41            }
42            DateTime::YearMonth(year, month) => {
43                let datetime = chrono::DateTime::parse_from_rfc3339(
44                    format!("{}-{:02}-01T00:00:00Z", year, month).as_str(),
45                )
46                .map_err(|_| ParseError::InvalidFormat)?;
47
48                Ok(datetime.with_timezone(&chrono::Utc))
49            }
50            DateTime::YearMonthDay(year, month, day) => {
51                let datetime = chrono::DateTime::parse_from_rfc3339(
52                    format!("{}-{:02}-{:02}T00:00:00Z", year, month, day).as_str(),
53                )
54                .map_err(|_| ParseError::InvalidFormat)?;
55
56                Ok(datetime.with_timezone(&chrono::Utc))
57            }
58            DateTime::Iso8601(dt) => Ok(dt),
59        }
60    }
61}
62
63#[derive(Debug, Clone, PartialEq)]
64pub enum Date {
65    Year(u16),
66    YearMonth(u16, u8),
67    YearMonthDay(u16, u8, u8),
68}
69
70impl ToString for Date {
71    fn to_string(&self) -> String {
72        match self {
73            Date::Year(year) => year.to_string(),
74            Date::YearMonth(year, month) => format!("{:04}-{:02}", year, month),
75            Date::YearMonthDay(year, month, day) => {
76                format!("{:04}-{:02}-{:02}", year, month, day)
77            }
78        }
79    }
80}
81
82#[derive(Debug, Clone, PartialEq)]
83pub enum Instant {
84    Iso8601(chrono::DateTime<chrono::Utc>),
85}
86
87impl Instant {
88    pub fn format(&self, fmt: &str) -> String {
89        match self {
90            Instant::Iso8601(dt) => dt.to_utc().format(fmt).to_string(),
91        }
92    }
93}
94
95impl ToString for Instant {
96    fn to_string(&self) -> String {
97        match self {
98            Instant::Iso8601(dt) => dt.to_rfc3339(),
99        }
100    }
101}
102
103#[derive(Debug, Clone, PartialEq)]
104pub struct Time(chrono::NaiveTime);
105
106impl ToString for Time {
107    fn to_string(&self) -> String {
108        self.0.format("%H:%M:%S%.f").to_string()
109    }
110}
111
112#[derive(Debug)]
113pub enum ParseError {
114    InvalidFormat,
115}
116
117pub static DATE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
118    Regex::new(
119        r"^(?<year>[0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(?<month>0[1-9]|1[0-2])(-(?<day>0[1-9]|[1-2][0-9]|3[0-1]))?)?$",
120    ).unwrap()
121});
122
123pub static DATETIME_REGEX: LazyLock<Regex> = LazyLock::new(|| {
124    Regex::new(
125        r"^(?<year>[0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(?<month>0[1-9]|1[0-2])(-(?<day>0[1-9]|[1-2][0-9]|3[0-1])(?<time>T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?$",
126    ).unwrap()
127});
128
129pub static INSTANT_REGEX: LazyLock<Regex> = LazyLock::new(|| {
130    Regex::new(
131        r"^([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))$",
132    ).unwrap()
133});
134
135pub static TIME_REGEX: LazyLock<Regex> = LazyLock::new(|| {
136    Regex::new(r"^([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\.[0-9]+)?$").unwrap()
137});
138
139pub fn parse_instant(instant_string: &str) -> Result<Instant, ParseError> {
140    if INSTANT_REGEX.is_match(instant_string) {
141        let datetime = chrono::DateTime::parse_from_rfc3339(instant_string)
142            .map_err(|_| ParseError::InvalidFormat)?;
143        Ok(Instant::Iso8601(datetime.with_timezone(&chrono::Utc)))
144    } else {
145        Err(ParseError::InvalidFormat)
146    }
147}
148
149pub fn parse_time(time_string: &str) -> Result<Time, ParseError> {
150    if TIME_REGEX.is_match(time_string) {
151        let time = Time(
152            chrono::NaiveTime::parse_from_str(time_string, "%H:%M:%S%.f")
153                .map_err(|_| ParseError::InvalidFormat)?,
154        );
155        Ok(time)
156    } else {
157        Err(ParseError::InvalidFormat)
158    }
159}
160
161pub fn parse_datetime(datetime_string: &str) -> Result<DateTime, ParseError> {
162    if let Some(captures) = DATETIME_REGEX.captures(datetime_string) {
163        match (
164            captures.name("year"),
165            captures.name("month"),
166            captures.name("day"),
167            captures.name("time"),
168        ) {
169            (Some(year), None, None, None) => {
170                let year = year.as_str().parse::<u16>().unwrap();
171                Ok(DateTime::Year(year))
172            }
173            (Some(year), Some(month), None, None) => {
174                let year = year.as_str().parse::<u16>().unwrap();
175                let month = month.as_str().parse::<u8>().unwrap();
176                Ok(DateTime::YearMonth(year, month))
177            }
178            (Some(year), Some(month), Some(day), None) => {
179                let year = year.as_str().parse::<u16>().unwrap();
180                let month = month.as_str().parse::<u8>().unwrap();
181                let day = day.as_str().parse::<u8>().unwrap();
182                Ok(DateTime::YearMonthDay(year, month, day))
183            }
184            _ => {
185                let datetime = chrono::DateTime::parse_from_rfc3339(datetime_string)
186                    .map_err(|_| ParseError::InvalidFormat)?;
187                Ok(DateTime::Iso8601(datetime.with_timezone(&chrono::Utc)))
188            }
189        }
190    } else {
191        Err(ParseError::InvalidFormat)
192    }
193}
194
195pub fn parse_date(date_string: &str) -> Result<Date, ParseError> {
196    if let Some(captures) = DATE_REGEX.captures(date_string) {
197        match (
198            captures.name("year"),
199            captures.name("month"),
200            captures.name("day"),
201        ) {
202            (Some(year), None, None) => {
203                let year = year.as_str().parse::<u16>().unwrap();
204                Ok(Date::Year(year))
205            }
206            (Some(year), Some(month), None) => {
207                let year = year.as_str().parse::<u16>().unwrap();
208                let month = month.as_str().parse::<u8>().unwrap();
209                Ok(Date::YearMonth(year, month))
210            }
211            (Some(year), Some(month), Some(day)) => {
212                let year = year.as_str().parse::<u16>().unwrap();
213                let month = month.as_str().parse::<u8>().unwrap();
214                let day = day.as_str().parse::<u8>().unwrap();
215                Ok(Date::YearMonthDay(year, month, day))
216            }
217            _ => Err(ParseError::InvalidFormat),
218        }
219    } else {
220        Err(ParseError::InvalidFormat)
221    }
222}
223
224pub enum DateKind {
225    DateTime,
226    Date,
227    Time,
228    Instant,
229}
230
231pub enum DateResult {
232    DateTime(DateTime),
233    Date(Date),
234    Time(Time),
235    Instant(Instant),
236}
237
238pub fn parse(kind: DateKind, input: &str) -> Result<DateResult, ParseError> {
239    match kind {
240        DateKind::DateTime => Ok(DateResult::DateTime(parse_datetime(input)?)),
241        DateKind::Date => Ok(DateResult::Date(parse_date(input)?)),
242        DateKind::Time => Ok(DateResult::Time(parse_time(input)?)),
243        DateKind::Instant => Ok(DateResult::Instant(parse_instant(input)?)),
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn test_parse_time() {
253        assert!(parse_time("12:34:56").is_ok());
254        assert!(parse_time("23:59:59").is_ok());
255        assert!(parse_time("23:59:59.232").is_ok());
256        assert_eq!(
257            parse_time("23:59:59.232").unwrap(),
258            Time(chrono::NaiveTime::from_hms_milli_opt(23, 59, 59, 232).unwrap())
259        );
260    }
261
262    #[test]
263    fn test_parse_instant() {
264        assert!(parse_instant("2015-02-07T13:28:17.239+02:00").is_ok());
265        assert!(parse_instant("2017-01-01T00:00:00Z").is_ok());
266    }
267
268    #[test]
269    fn test_parse_date() {
270        assert_eq!(parse_date("2023").unwrap(), Date::Year(2023));
271        assert_eq!(parse_date("2023-01").unwrap(), Date::YearMonth(2023, 1));
272        assert_eq!(
273            parse_date("2023-01-01").unwrap(),
274            Date::YearMonthDay(2023, 1, 1)
275        );
276
277        assert_eq!(
278            Date::YearMonthDay(2023, 1, 19),
279            parse_date("2023-01-19").unwrap()
280        );
281
282        assert!(parse_date("2023-01-33").is_err());
283        assert!(parse_date("2023-13-30").is_err());
284        assert!(parse_date("2023-01-01T12:00:00Z").is_err());
285    }
286    #[test]
287    fn test_parse_datetime() {
288        assert_eq!(parse_datetime("2023").unwrap(), DateTime::Year(2023));
289        assert_eq!(
290            parse_datetime("2023-01").unwrap(),
291            DateTime::YearMonth(2023, 1)
292        );
293        assert_eq!(
294            parse_datetime("2023-01-01").unwrap(),
295            DateTime::YearMonthDay(2023, 1, 1)
296        );
297
298        assert_eq!(
299            DateTime::YearMonthDay(2023, 1, 19),
300            parse_datetime("2023-01-19").unwrap()
301        );
302
303        // Invalid day won't parse.
304        assert!(parse_datetime("2023-01-42").is_err());
305
306        assert_eq!(
307            parse_datetime("2023-01-01T12:00:00Z").unwrap(),
308            DateTime::Iso8601(
309                chrono::DateTime::parse_from_rfc3339("2023-01-01T12:00:00Z")
310                    .unwrap()
311                    .with_timezone(&chrono::Utc)
312            )
313        );
314        assert!(parse_datetime("2023-01-01T12:00:00+00:00").is_ok());
315        assert!(parse_datetime("2023-01-01T12:00:00+01:00").is_ok());
316        assert!(parse_datetime("2023-01-01T12:00:00-01:00").is_ok());
317        assert!(parse_datetime("2023-01-01T12:00:00+02:00").is_ok());
318        assert!(parse_datetime("2023-01-01T12:00:00-02:00").is_ok());
319        assert!(parse_datetime("2023-01-01T12:00:00+14:00").is_ok());
320    }
321}