fhir_model/
date_time.rs

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