hl7_parser/datetime/
chrono.rs

1//! All implementations here are implemented as `TryFrom` and `From` traits
2//! between the `TimeStamp` struct and various `chrono` types. This allows for
3//! easy conversion between the two types. The `TryFrom` implementations will
4//! return an error if the conversion is not possible, such as if the date or
5//! time components are invalid. The `From` implementations will always succeed
6//! and will set missing components to zero or the epoch if necessary.
7//!
8//! View the `TimeStamp` struct's documentation for more information on exactly
9//! which traits are implemented.
10//!
11//! # Examples
12//!
13//! ```
14//! use hl7_parser::datetime::{TimeStamp, TimeStampOffset};
15//! use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, Utc, Datelike, Timelike};
16//!
17//! let ts = TimeStamp {
18//!    year: 2023,
19//!    month: Some(3),
20//!    day: Some(12),
21//!    hour: Some(19),
22//!    minute: Some(59),
23//!    second: Some(5),
24//!    microsecond: Some(1234),
25//!    offset: Some(TimeStampOffset {
26//!        hours: -7,
27//!        minutes: 0,
28//!    })
29//! };
30//!
31//! let datetime: DateTime<FixedOffset> = ts.try_into().unwrap();
32//! assert_eq!(datetime.year(), 2023);
33//! assert_eq!(datetime.month(), 3);
34//! assert_eq!(datetime.day(), 12);
35//! assert_eq!(datetime.hour(), 19);
36//! assert_eq!(datetime.minute(), 59);
37//! assert_eq!(datetime.second(), 5);
38//! assert_eq!(datetime.nanosecond(), 1234 * 1000);
39//! assert_eq!(datetime.offset().local_minus_utc() / 3600, -7);
40//! assert_eq!(datetime.offset().local_minus_utc() % 3600, 0);
41//! ```
42//!
43//! ```
44//! use hl7_parser::datetime::{TimeStamp, TimeStampOffset};
45//! use chrono::{DateTime, Utc, NaiveDate, TimeZone};
46//!
47//! let datetime = Utc.from_utc_datetime(
48//!     &NaiveDate::from_ymd_opt(2023, 3, 12).unwrap()
49//!     .and_hms_opt(19, 59, 5).unwrap(),
50//! );
51//!
52//! let ts: TimeStamp = datetime.into();
53//! assert_eq!(ts.year, 2023);
54//! assert_eq!(ts.month, Some(3));
55//! assert_eq!(ts.day, Some(12));
56//! assert_eq!(ts.hour, Some(19));
57//! assert_eq!(ts.minute, Some(59));
58//! assert_eq!(ts.second, Some(5));
59//! assert_eq!(ts.microsecond, Some(0));
60//! assert_eq!(ts.offset, Some(TimeStampOffset {
61//!    hours: 0,
62//!    minutes: 0,
63//! }));
64//! ```
65
66use super::{Date, DateTimeParseError, ErroredDateTimeComponent, Time, TimeStamp, TimeStampOffset};
67use chrono::{
68    offset::LocalResult, DateTime, Datelike, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime,
69    TimeZone, Timelike,
70};
71
72/// Attempt to convert a `TimeStamp` into a `NaiveDate`. If the `TimeStamp` is
73/// missing date components, those components will be set to `1`.
74impl TryFrom<TimeStamp> for NaiveDate {
75    type Error = DateTimeParseError;
76
77    fn try_from(value: TimeStamp) -> Result<Self, Self::Error> {
78        let TimeStamp {
79            year, month, day, ..
80        } = value;
81
82        let month = month.unwrap_or(1);
83        let day = day.unwrap_or(1);
84
85        let date = NaiveDate::from_ymd_opt(year as i32, month as u32, day as u32).ok_or(
86            DateTimeParseError::InvalidComponentRange(ErroredDateTimeComponent::Date),
87        )?;
88        Ok(date)
89    }
90}
91
92/// Attempt to convert a `Date` into a `NaiveDate`. If the `Date` is missing
93/// date components, those components will be set to `1`.
94impl TryFrom<Date> for NaiveDate {
95    type Error = DateTimeParseError;
96
97    fn try_from(value: Date) -> Result<Self, Self::Error> {
98        let Date { year, month, day } = value;
99
100        let month = month.unwrap_or(1);
101        let day = day.unwrap_or(1);
102
103        let date = NaiveDate::from_ymd_opt(year as i32, month as u32, day as u32).ok_or(
104            DateTimeParseError::InvalidComponentRange(ErroredDateTimeComponent::Date),
105        )?;
106        Ok(date)
107    }
108}
109
110/// Attempt to convert a `TimeStamp` into a `NaiveTime`. If the `TimeStamp` is
111/// missing time components, those components will be set to zero.
112impl TryFrom<Time> for NaiveTime {
113    type Error = DateTimeParseError;
114
115    fn try_from(value: Time) -> Result<Self, Self::Error> {
116        let Time {
117            hour,
118            minute,
119            second,
120            microsecond,
121            ..
122        } = value;
123
124        let minute = minute.unwrap_or(0);
125        let second = second.unwrap_or(0);
126        let microsecond = microsecond.unwrap_or(0);
127
128        let time =
129            NaiveTime::from_hms_micro_opt(hour as u32, minute as u32, second as u32, microsecond)
130                .ok_or(DateTimeParseError::InvalidComponentRange(
131                ErroredDateTimeComponent::Time,
132            ))?;
133        Ok(time)
134    }
135}
136
137/// Convert a `NaiveDate` into a `TimeStamp`. The `TimeStamp` will have the
138/// date components set to the `NaiveDate`'s components and the time components
139/// set to `None`.
140impl From<NaiveDate> for TimeStamp {
141    fn from(value: NaiveDate) -> Self {
142        let year = value.year() as u16;
143        let month = Some(value.month() as u8);
144        let day = Some(value.day() as u8);
145        let hour = None;
146        let minute = None;
147        let second = None;
148        let microsecond = None;
149        let offset = None;
150        TimeStamp {
151            year,
152            month,
153            day,
154            hour,
155            minute,
156            second,
157            microsecond,
158            offset,
159        }
160    }
161}
162
163/// Convert a `NaiveDate` into a `Date`. The `Date` will have the date
164/// components set to the `NaiveDate`'s components
165impl From<NaiveDate> for Date {
166    fn from(value: NaiveDate) -> Self {
167        let year = value.year() as u16;
168        let month = Some(value.month() as u8);
169        let day = Some(value.day() as u8);
170        Date { year, month, day }
171    }
172}
173
174/// Convert a `NaiveTime` into a `Time`. The `Time` will have the time components
175/// set to the `NaiveTime`'s components and the offset components set to `None`.
176impl From<NaiveTime> for Time {
177    fn from(value: NaiveTime) -> Self {
178        let hour = value.hour() as u8;
179        let minute = Some(value.minute() as u8);
180        let second = Some(value.second() as u8);
181        let microsecond = Some(value.nanosecond() / 1000);
182        Time {
183            hour,
184            minute,
185            second,
186            microsecond,
187            offset: None,
188        }
189    }
190}
191
192/// Attempt to convert a `TimeStamp` into a `NaiveDateTime`. If the `TimeStamp`
193/// is missing time components, those components will be set to zero.
194impl TryFrom<TimeStamp> for NaiveDateTime {
195    type Error = DateTimeParseError;
196
197    fn try_from(value: TimeStamp) -> Result<Self, Self::Error> {
198        let date = NaiveDate::try_from(value)?;
199        let time = NaiveTime::from_hms_micro_opt(
200            value.hour.unwrap_or(0) as u32,
201            value.minute.unwrap_or(0) as u32,
202            value.second.unwrap_or(0) as u32,
203            value.microsecond.unwrap_or(0),
204        )
205        .ok_or(DateTimeParseError::InvalidComponentRange(
206            ErroredDateTimeComponent::Time,
207        ))?;
208        Ok(NaiveDateTime::new(date, time))
209    }
210}
211
212/// Convert a `NaiveDateTime` into a `TimeStamp`. The `TimeStamp` will have the
213/// date and time components set to the `NaiveDateTime`'s components and the
214/// offset components set to `None`.
215impl From<NaiveDateTime> for TimeStamp {
216    fn from(value: NaiveDateTime) -> Self {
217        let year = value.year() as u16;
218        let month = Some(value.month() as u8);
219        let day = Some(value.day() as u8);
220        let hour = Some(value.hour() as u8);
221        let minute = Some(value.minute() as u8);
222        let second = Some(value.second() as u8);
223        let microsecond = Some(value.nanosecond() / 1000);
224        let offset = None;
225        TimeStamp {
226            year,
227            month,
228            day,
229            hour,
230            minute,
231            second,
232            microsecond,
233            offset,
234        }
235    }
236}
237
238/// Attempt to convert a `TimeStamp` into a `DateTime<FixedOffset>`. If the
239/// `TimeStamp` is missing date components, those components will be set to `1`.
240/// If the `TimeStamp` is missing time components, those components will be set
241/// to zero. If the `TimeStamp` is missing offset components, those components
242/// will be set to zero.
243impl TryFrom<TimeStamp> for LocalResult<DateTime<FixedOffset>> {
244    type Error = DateTimeParseError;
245
246    fn try_from(value: TimeStamp) -> Result<Self, Self::Error> {
247        let TimeStamp {
248            year,
249            month,
250            day,
251            hour,
252            minute,
253            second,
254            microsecond,
255            offset,
256        } = value;
257
258        let month = month.unwrap_or(1);
259        let day = day.unwrap_or(1);
260        let date = NaiveDate::from_ymd_opt(year as i32, month as u32, day as u32).ok_or(
261            DateTimeParseError::InvalidComponentRange(ErroredDateTimeComponent::Date),
262        )?;
263
264        let hour = hour.unwrap_or(0);
265        let minute = minute.unwrap_or(0);
266        let second = second.unwrap_or(0);
267        let microsecond = microsecond.unwrap_or(0);
268
269        let time =
270            NaiveTime::from_hms_micro_opt(hour as u32, minute as u32, second as u32, microsecond)
271                .ok_or(DateTimeParseError::InvalidComponentRange(
272                ErroredDateTimeComponent::Time,
273            ))?;
274
275        let offset = offset.unwrap_or_default();
276        let offset_hours = offset.hours as i32;
277        let offset_minutes = offset.minutes as i32;
278        let offset = FixedOffset::east_opt(offset_hours * 3600 + offset_minutes * 60).ok_or(
279            DateTimeParseError::InvalidComponentRange(ErroredDateTimeComponent::Offset),
280        )?;
281
282        let datetime = NaiveDateTime::new(date, time);
283        let datetime = datetime.and_local_timezone(offset);
284        Ok(datetime)
285    }
286}
287
288/// Attempt to convert a `TimeStamp` into a `DateTime<Tz>`. If the `TimeStamp` is
289/// missing date components, those components will be set to `1`. If the
290/// `TimeStamp` is missing time components, those components will be set to zero.
291/// If the `TimeStamp` is missing offset components, those components will be set
292/// to zero.
293///
294/// Note that this implementation will return an error if the `TimeStamp` is
295/// ambiguous or does not exist.
296impl<Tz> TryFrom<TimeStamp> for DateTime<Tz>
297where
298    Tz: TimeZone,
299    DateTime<Tz>: From<DateTime<FixedOffset>>,
300{
301    type Error = DateTimeParseError;
302
303    fn try_from(value: TimeStamp) -> Result<Self, Self::Error> {
304        let datetime: LocalResult<DateTime<FixedOffset>> = LocalResult::try_from(value)?;
305        match datetime {
306            LocalResult::Single(datetime) => Ok(datetime.into()),
307            LocalResult::Ambiguous(earliest, latest) => Err(DateTimeParseError::AmbiguousTime(
308                earliest.to_rfc3339(),
309                latest.to_rfc3339(),
310            )),
311            LocalResult::None => Err(DateTimeParseError::InvalidComponentRange(
312                ErroredDateTimeComponent::DateTime,
313            )),
314        }
315    }
316}
317
318/// Convert a `DateTime` into a `TimeStamp`. The `TimeStamp` will have the date
319/// and time components set to the `DateTime`'s components and the offset
320/// components set to the `DateTime`'s offset components.
321impl<Tz> From<DateTime<Tz>> for TimeStamp
322where
323    Tz: TimeZone,
324    DateTime<Tz>: Into<DateTime<FixedOffset>>,
325{
326    fn from(value: DateTime<Tz>) -> Self {
327        let datetime: DateTime<FixedOffset> = value.into();
328
329        let year = datetime.year() as u16;
330        let month = Some(datetime.month() as u8);
331        let day = Some(datetime.day() as u8);
332        let hour = Some(datetime.hour() as u8);
333        let minute = Some(datetime.minute() as u8);
334        let second = Some(datetime.second() as u8);
335        let microsecond = Some(datetime.nanosecond() / 1000);
336        let offset = Some(TimeStampOffset {
337            hours: (datetime.offset().local_minus_utc() / 3600) as i8,
338            minutes: (datetime.offset().local_minus_utc() % 3600) as u8,
339        });
340
341        TimeStamp {
342            year,
343            month,
344            day,
345            hour,
346            minute,
347            second,
348            microsecond,
349            offset,
350        }
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    use crate::datetime::TimeStampOffset;
357    use chrono::{Timelike, Utc};
358
359    use super::*;
360
361    #[test]
362    fn can_convert_timestamp_to_date() {
363        let ts = TimeStamp {
364            year: 2023,
365            month: Some(3),
366            day: Some(12),
367            hour: Some(19),
368            minute: Some(59),
369            second: None,
370            microsecond: None,
371            offset: None,
372        };
373        let actual = NaiveDate::try_from(ts).unwrap();
374        assert_eq!(actual.year(), 2023);
375        assert_eq!(actual.month(), 3);
376        assert_eq!(actual.day(), 12);
377    }
378
379    #[test]
380    fn can_convert_timestamp_to_datetime_with_fixed_offset() {
381        let ts = TimeStamp {
382            year: 2023,
383            month: Some(3),
384            day: Some(12),
385            hour: Some(19),
386            minute: Some(59),
387            second: Some(5),
388            microsecond: Some(1234),
389            offset: Some(TimeStampOffset {
390                hours: -7,
391                minutes: 0,
392            }),
393        };
394        let actual = DateTime::<FixedOffset>::try_from(ts).unwrap();
395        assert_eq!(actual.year(), 2023);
396        assert_eq!(actual.month(), 3);
397        assert_eq!(actual.day(), 12);
398        assert_eq!(actual.hour(), 19);
399        assert_eq!(actual.minute(), 59);
400        assert_eq!(actual.second(), 5);
401        assert_eq!(actual.nanosecond(), 1234 * 1000);
402        assert_eq!(actual.offset().local_minus_utc() / 3600, -7);
403        assert_eq!(actual.offset().local_minus_utc() % 3600, 0);
404    }
405
406    #[test]
407    fn can_convert_timestamp_datetime_with_utc_offset() {
408        let ts = TimeStamp {
409            year: 2023,
410            month: Some(3),
411            day: Some(12),
412            hour: Some(19),
413            minute: Some(59),
414            second: Some(5),
415            microsecond: Some(1234),
416            offset: Some(TimeStampOffset {
417                hours: -7,
418                minutes: 0,
419            }),
420        };
421        let actual = DateTime::<Utc>::try_from(ts).unwrap();
422
423        assert_eq!(actual.year(), 2023);
424        assert_eq!(actual.month(), 3);
425        assert_eq!(actual.day(), 13);
426        assert_eq!(actual.hour(), 2);
427        assert_eq!(actual.minute(), 59);
428        assert_eq!(actual.second(), 5);
429        assert_eq!(actual.nanosecond(), 1234 * 1000);
430    }
431
432    #[test]
433    fn can_convert_datetime_to_timestamp() {
434        let datetime = Utc.from_utc_datetime(
435            &NaiveDate::from_ymd_opt(2023, 3, 12)
436                .unwrap()
437                .and_hms_opt(19, 59, 5)
438                .unwrap(),
439        );
440        let actual = TimeStamp::from(datetime);
441        assert_eq!(actual.year, 2023);
442        assert_eq!(actual.month, Some(3));
443        assert_eq!(actual.day, Some(12));
444        assert_eq!(actual.hour, Some(19));
445        assert_eq!(actual.minute, Some(59));
446        assert_eq!(actual.second, Some(5));
447        assert_eq!(actual.microsecond, Some(0));
448        assert_eq!(
449            actual.offset,
450            Some(TimeStampOffset {
451                hours: 0,
452                minutes: 0
453            })
454        );
455    }
456}