haste_fhir_model/r4/datetime/
mod.rs1use 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 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 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}