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