Skip to main content

haste_fhirpath/
partial_datetime.rs

1//! `FHIRPath` `DateTime` values that FHIR's `dateTime` cannot hold. FHIR requires seconds and a
2//! timezone offset whenever a `dateTime` has a time, while a `FHIRPath` `DateTime` may stop at
3//! the hour or minute and may have no offset (`@2015-02-04T14:34`, `@2015-02-04T14:34:28`).
4//! Only `FHIRPath` makes these, so they live here and not in the FHIR model; every other
5//! `DateTime` the engine makes is a FHIR `dateTime`.
6
7use chrono::{FixedOffset, NaiveDate, NaiveDateTime, NaiveTime};
8use haste_fhir_model::r4::datetime::{
9    Date, DateTime, Time, parse_date, parse_datetime, parse_partial_time,
10};
11use haste_reflect::MetaValue;
12use regex::Regex;
13use std::{any::Any, fmt, sync::LazyLock};
14
15/// What follows a `DateTime`'s `T`: a time, then `Z` or an offset in FHIR's range.
16static TIME_AND_OFFSET_REGEX: LazyLock<Regex> = LazyLock::new(|| {
17    Regex::new(r"^(?<time>[0-9][0-9:.]*?)(?<offset>Z|(\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))?$")
18        .unwrap()
19});
20
21/// A `FHIRPath` `DateTime` whose time FHIR's `dateTime` cannot hold.
22#[derive(Debug, Clone, PartialEq)]
23pub struct PartialDateTime {
24    pub date: NaiveDate,
25    pub time: Time,
26    /// `None` when the value has no timezone offset.
27    pub offset: Option<FixedOffset>,
28}
29
30/// A `FHIRPath` `DateTime`: a FHIR `dateTime` when FHIR's type can hold it.
31pub enum AnyDateTime {
32    Fhir(DateTime),
33    Partial(PartialDateTime),
34}
35
36/// `text` as a `FHIRPath` `DateTime`, or `None` when it is not one.
37pub fn parse_any_datetime(text: &str) -> Option<AnyDateTime> {
38    match parse_datetime(text) {
39        Ok(value) => Some(AnyDateTime::Fhir(value)),
40        Err(_) => PartialDateTime::parse(text).map(AnyDateTime::Partial),
41    }
42}
43
44/// The clock time a Time names, the parts below its precision at zero.
45pub fn naive_time(time: &Time) -> NaiveTime {
46    match time {
47        Time::Hour(hour) => NaiveTime::from_hms_opt(u32::from(*hour), 0, 0),
48        Time::HourMinute(hour, minute) => {
49            NaiveTime::from_hms_opt(u32::from(*hour), u32::from(*minute), 0)
50        }
51        Time::HourMinuteSecond(time) => Some(*time),
52    }
53    .unwrap_or_default()
54}
55
56impl PartialDateTime {
57    /// `text` as a `DateTime` with a time, a full date before it, or `None`.
58    fn parse(text: &str) -> Option<Self> {
59        let (date, time) = text.split_once('T')?;
60        let captures = TIME_AND_OFFSET_REGEX.captures(time)?;
61        let Date::YearMonthDay(year, month, day) = parse_date(date).ok()? else {
62            return None;
63        };
64        // chrono's offset parser does not take `Z`; the regex keeps the rest in FHIR's range.
65        let offset = match captures.name("offset").map(|offset| offset.as_str()) {
66            None => None,
67            Some("Z") => Some(FixedOffset::east_opt(0)?),
68            Some(offset) => Some(offset.parse::<FixedOffset>().ok()?),
69        };
70        Some(PartialDateTime {
71            date: NaiveDate::from_ymd_opt(i32::from(year), u32::from(month), u32::from(day))?,
72            time: parse_partial_time(&captures["time"]).ok()?,
73            offset,
74        })
75    }
76
77    /// The date and clock time as written, the parts below the precision at zero.
78    #[must_use]
79    pub fn local(&self) -> NaiveDateTime {
80        self.date.and_time(naive_time(&self.time))
81    }
82
83    /// The date and clock time in UTC, or as written when there is no offset.
84    #[must_use]
85    pub fn utc(&self) -> Option<NaiveDateTime> {
86        match self.offset {
87            Some(offset) => self.local().checked_sub_offset(offset),
88            None => Some(self.local()),
89        }
90    }
91}
92
93impl fmt::Display for PartialDateTime {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        write!(
96            f,
97            "{}T{}",
98            self.date.format("%Y-%m-%d"),
99            self.time.to_string()
100        )?;
101        match self.offset {
102            Some(offset) => write!(f, "{offset}"),
103            None => Ok(()),
104        }
105    }
106}
107
108/// A `FHIRPath` `System.DateTime`: there is no FHIR type for it.
109impl MetaValue for PartialDateTime {
110    fn fields(&self) -> Vec<&'static str> {
111        vec![]
112    }
113
114    fn get_field<'a>(&'a self, _field: &str) -> Option<&'a dyn MetaValue> {
115        None
116    }
117
118    fn get_field_mut<'a>(&'a mut self, _field: &str) -> Option<&'a mut dyn MetaValue> {
119        None
120    }
121
122    fn get_index(&self, _index: usize) -> Option<&dyn MetaValue> {
123        None
124    }
125
126    fn get_index_mut(&mut self, _index: usize) -> Option<&mut dyn MetaValue> {
127        None
128    }
129
130    fn flatten(&self) -> Vec<&dyn MetaValue> {
131        vec![self]
132    }
133
134    fn as_any(&self) -> &dyn Any {
135        self
136    }
137
138    fn fhir_type(&self) -> &'static str {
139        "http://hl7.org/fhirpath/System.DateTime"
140    }
141
142    fn is_many(&self) -> bool {
143        false
144    }
145}