Skip to main content

fhir_model/
date_time.rs

1//! FHIR Time, Date, DateTime and Instant types.
2
3use ::serde::{Deserialize, Serialize};
4use ::std::{cmp::Ordering, str::FromStr};
5use ::time::{OffsetDateTime, error::Parse, format_description::well_known::Rfc3339};
6
7use crate::error::DateFormatError;
8
9/// FHIR instant type: <https://hl7.org/fhir/datatypes.html#instant>
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
11#[serde(transparent)]
12pub struct Instant(#[serde(with = "time::serde::rfc3339")] pub OffsetDateTime);
13
14/// FHIR date type: <https://hl7.org/fhir/datatypes.html#date>
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub enum Date {
17	/// Date in the format of YYYY
18	Year(i32),
19	/// Date in the format of YYYY-MM
20	YearMonth(i32, time::Month),
21	/// Date in the format of YYYY-MM-DD
22	Date(time::Date),
23}
24
25/// FHIR dateTime type: <https://hl7.org/fhir/datatypes.html#dateTime>
26#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
27#[serde(untagged)]
28pub enum DateTime {
29	/// Date that does not contain time or timezone
30	Date(Date),
31	/// Full date and time
32	DateTime(Instant),
33}
34
35/// FHIR time type: <https://hl7.org/fhir/datatypes.html#time>
36#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
37#[serde(transparent)]
38pub struct Time(#[serde(with = "serde_time")] pub time::Time);
39
40/// Serde module for serialize and deserialize function for the type.
41mod serde_time {
42	use serde::{Deserialize, Serialize};
43	use time::{format_description::FormatItem, macros::format_description};
44
45	/// Time format `hh:mm:ss`.
46	const TIME_FORMAT: &[FormatItem<'_>] = format_description!("[hour]:[minute]:[second]");
47	/// Time format for `hh:mm:ss[.SSS]`.
48	const TIME_FORMAT_SUBSEC: &[FormatItem<'_>] = fhir_time_format();
49
50	/// Time format with optional subseconds.
51	const fn fhir_time_format() -> &'static [FormatItem<'static>] {
52		/// Optional subseconds.
53		const OPTIONAL_SUB_SECONDS: FormatItem<'_> =
54			FormatItem::Optional(&FormatItem::Compound(format_description!(".[subsecond]")));
55		&[FormatItem::Compound(TIME_FORMAT), OPTIONAL_SUB_SECONDS]
56	}
57
58	/// Serialize time, using subseconds iff appropriate.
59	#[allow(clippy::trivially_copy_pass_by_ref, reason = "Parameter types are set by serde")]
60	pub fn serialize<S>(time: &time::Time, serializer: S) -> Result<S::Ok, S::Error>
61	where
62		S: serde::Serializer,
63	{
64		let format = if time.nanosecond() == 0 { TIME_FORMAT } else { TIME_FORMAT_SUBSEC };
65		time.format(format).map_err(serde::ser::Error::custom)?.serialize(serializer)
66	}
67
68	/// Deserialize time, subseconds optional.
69	pub fn deserialize<'de, D>(deserializer: D) -> Result<time::Time, D::Error>
70	where
71		D: serde::Deserializer<'de>,
72	{
73		let string = String::deserialize(deserializer)?;
74		time::Time::parse(&string, TIME_FORMAT_SUBSEC).map_err(serde::de::Error::custom)
75	}
76}
77
78impl Serialize for Date {
79	/// Serialize date.
80	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
81	where
82		S: serde::Serializer,
83	{
84		match &self {
85			// Serialize YYYY
86			Date::Year(year) => {
87				if (1000 .. 10000).contains(year) {
88					year.to_string().serialize(serializer)
89				} else {
90					Err(serde::ser::Error::custom("Year is not 4 digits long"))
91				}
92			}
93			// Serialize YYYY-MM
94			Date::YearMonth(year, month) => {
95				if (1000 .. 10000).contains(year) {
96					serializer.serialize_str(&format!("{year}-{:02}", *month as u8))
97				} else {
98					Err(serde::ser::Error::custom("Year is not 4 digits long"))
99				}
100			}
101			// Serialize YYYY-MM-DD
102			Date::Date(date) => {
103				/// Full date format
104				const FORMAT: &[time::format_description::FormatItem<'_>] =
105					time::macros::format_description!("[year]-[month]-[day]");
106				date.format(FORMAT).map_err(serde::ser::Error::custom)?.serialize(serializer)
107			}
108		}
109	}
110}
111
112impl FromStr for Date {
113	type Err = DateFormatError;
114
115	fn from_str(s: &str) -> Result<Self, Self::Err> {
116		// Split date into parts
117		// YYYY(1)-MM(2)-DD(3)
118		match s.split('-').count() {
119			1 => Ok(Date::Year(s.parse::<i32>()?)),
120			2 => {
121				let (year, month) = s.split_once('-').ok_or(DateFormatError::StringSplit)?;
122				// Convert strings into integers
123				let year = year.parse::<i32>()?;
124				let month = month.parse::<u8>()?;
125
126				Ok(Date::YearMonth(year, month.try_into()?))
127			}
128			3 => {
129				/// Full date format
130				const FORMAT: &[time::format_description::FormatItem<'_>] =
131					time::macros::format_description!("[year]-[month]-[day]");
132				Ok(Date::Date(time::Date::parse(s, FORMAT)?))
133			}
134			_ => Err(DateFormatError::InvalidDate),
135		}
136	}
137}
138
139impl<'de> Deserialize<'de> for Date {
140	/// Deserialize date.
141	fn deserialize<D>(deserializer: D) -> Result<Date, D::Error>
142	where
143		D: serde::Deserializer<'de>,
144	{
145		let string = String::deserialize(deserializer)?;
146		Date::from_str(&string).map_err(serde::de::Error::custom)
147	}
148}
149
150impl FromStr for DateTime {
151	type Err = DateFormatError;
152
153	fn from_str(s: &str) -> Result<Self, Self::Err> {
154		if s.contains('T') {
155			let instant = Instant::from_str(s)?;
156			Ok(DateTime::DateTime(instant))
157		} else {
158			let date = Date::from_str(s)?;
159			Ok(DateTime::Date(date))
160		}
161	}
162}
163
164impl<'de> Deserialize<'de> for DateTime {
165	/// Deserialize datetime.
166	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
167	where
168		D: serde::Deserializer<'de>,
169	{
170		let string = String::deserialize(deserializer)?;
171		Self::from_str(&string).map_err(serde::de::Error::custom)
172	}
173}
174
175impl FromStr for Instant {
176	type Err = Parse;
177
178	fn from_str(s: &str) -> Result<Self, Self::Err> {
179		Ok(Instant(OffsetDateTime::parse(s, &Rfc3339)?))
180	}
181}
182
183impl PartialOrd for Date {
184	fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
185		match (self, other) {
186			(Date::Date(ld), r) => ld.partial_cmp(r),
187			(l, Date::Date(rd)) => l.partial_cmp(rd),
188			(Date::Year(ly), Date::Year(ry)) => ly.partial_cmp(ry),
189			(Date::Year(ly), Date::YearMonth(ry, _rm)) => ly.partial_cmp(ry),
190			(Date::YearMonth(ly, _lm), Date::Year(ry)) => ly.partial_cmp(ry),
191			(Date::YearMonth(ly, lm), Date::YearMonth(ry, rm)) => match ly.partial_cmp(ry)? {
192				Ordering::Less => Some(Ordering::Less),
193				Ordering::Greater => Some(Ordering::Greater),
194				Ordering::Equal => (*lm as u8).partial_cmp(&(*rm as u8)),
195			},
196		}
197	}
198}
199
200impl PartialOrd for DateTime {
201	fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
202		match (self, other) {
203			(DateTime::Date(ld), DateTime::Date(rd)) => ld.partial_cmp(rd),
204			(DateTime::Date(ld), DateTime::DateTime(Instant(rdtm))) => ld.partial_cmp(&rdtm.date()),
205			(DateTime::DateTime(Instant(ldtm)), DateTime::Date(rd)) => ldtm.date().partial_cmp(rd),
206			(DateTime::DateTime(ldtm), DateTime::DateTime(rdtm)) => ldtm.partial_cmp(rdtm),
207		}
208	}
209}
210
211impl PartialEq<time::Date> for Date {
212	fn eq(&self, other: &time::Date) -> bool {
213		match self {
214			Self::Year(year) => *year == other.year(),
215			Self::YearMonth(year, month) => *year == other.year() && *month == other.month(),
216			Self::Date(date) => date == other,
217		}
218	}
219}
220
221impl PartialEq<Date> for time::Date {
222	fn eq(&self, other: &Date) -> bool {
223		match other {
224			Date::Year(year) => self.year() == *year,
225			Date::YearMonth(year, month) => self.year() == *year && self.month() == *month,
226			Date::Date(date) => self == date,
227		}
228	}
229}
230
231impl PartialOrd<time::Date> for Date {
232	fn partial_cmp(&self, other: &time::Date) -> Option<Ordering> {
233		match self {
234			Date::Year(year) => year.partial_cmp(&other.year()),
235			Date::YearMonth(year, month) => match year.partial_cmp(&other.year())? {
236				Ordering::Less => Some(Ordering::Less),
237				Ordering::Greater => Some(Ordering::Greater),
238				Ordering::Equal => (*month as u8).partial_cmp(&(other.month() as u8)),
239			},
240			Date::Date(date) => date.partial_cmp(other),
241		}
242	}
243}
244
245impl PartialOrd<Date> for time::Date {
246	fn partial_cmp(&self, other: &Date) -> Option<Ordering> {
247		match other {
248			Date::Year(year) => self.year().partial_cmp(year),
249			Date::YearMonth(year, month) => match self.year().partial_cmp(year)? {
250				Ordering::Less => Some(Ordering::Less),
251				Ordering::Greater => Some(Ordering::Greater),
252				Ordering::Equal => (self.month() as u8).partial_cmp(&(*month as u8)),
253			},
254			Date::Date(date) => self.partial_cmp(date),
255		}
256	}
257}
258
259impl PartialEq<OffsetDateTime> for DateTime {
260	fn eq(&self, other: &OffsetDateTime) -> bool {
261		match self {
262			Self::Date(date) => *date == other.date(),
263			Self::DateTime(Instant(datetime)) => datetime == other,
264		}
265	}
266}
267
268impl PartialEq<DateTime> for OffsetDateTime {
269	fn eq(&self, other: &DateTime) -> bool {
270		match other {
271			DateTime::Date(date) => self.date() == *date,
272			DateTime::DateTime(Instant(datetime)) => self == datetime,
273		}
274	}
275}
276
277impl PartialOrd<OffsetDateTime> for DateTime {
278	fn partial_cmp(&self, other: &OffsetDateTime) -> Option<Ordering> {
279		match self {
280			DateTime::Date(date) => date.partial_cmp(&other.date()),
281			DateTime::DateTime(Instant(datetime)) => datetime.partial_cmp(other),
282		}
283	}
284}
285
286impl PartialOrd<DateTime> for OffsetDateTime {
287	fn partial_cmp(&self, other: &DateTime) -> Option<Ordering> {
288		match other {
289			DateTime::Date(date) => self.date().partial_cmp(date),
290			DateTime::DateTime(Instant(datetime)) => self.partial_cmp(datetime),
291		}
292	}
293}