haste_fhir_model/r4/datetime/
mod.rs

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