hl7_parser/datetime/
timestamp.rs

1use crate::parser::Span;
2use nom::{
3    bytes::complete::{tag, take_while_m_n},
4    character::complete::one_of,
5    combinator::{map_res, opt},
6    sequence::preceded,
7    IResult,
8};
9use std::{fmt::Display, str::FromStr};
10
11use super::DateTimeParseError;
12
13/// A parsed timezone offset in hours and minutes
14#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub struct TimeStampOffset {
17    /// The hours offset from UTC. Note: if this value is negative, the timezone
18    /// is behind UTC, if positive, it is ahead of UTC.
19    pub hours: i8,
20    /// The minutes offset from UTC
21    pub minutes: u8,
22}
23
24impl Display for TimeStampOffset {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        write!(f, "{:+03}{:02}", self.hours, self.minutes)
27    }
28}
29
30/// The results of parsing a timestamp. Note that the timestamp is not validated,
31/// i.e. it may not be a valid date or time.
32#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34pub struct TimeStamp {
35    /// The year of the timestamp
36    pub year: u16,
37    /// The month of the timestamp (1-12)
38    pub month: Option<u8>,
39    /// The day of the timestamp (1-31)
40    pub day: Option<u8>,
41    /// The hour of the timestamp (0-23)
42    pub hour: Option<u8>,
43    /// The minute of the timestamp (0-59)
44    pub minute: Option<u8>,
45    /// The second of the timestamp (0-59)
46    pub second: Option<u8>,
47    /// The microsecond of the timestamp (0-999_900)
48    pub microsecond: Option<u32>,
49    /// The timezone offset of the timestamp
50    pub offset: Option<TimeStampOffset>,
51}
52
53/// Parse an HL7 timestamp in the format: `YYYY[MM[DD[HH[MM[SS[.S[S[S[S]]]]]]]]][+/-ZZZZ]`
54///
55/// # Arguments
56/// * `s` - The string to parse
57/// * `lenient_trailing_chars` - If true, allow trailing characters after the timestamp, otherwise
58///   throw an error
59///
60/// # Example
61///
62/// ```
63/// use hl7_parser::datetime::{parse_timestamp, TimeStamp, TimeStampOffset};
64///
65/// let ts: TimeStamp = parse_timestamp("20230312195905.1234-0700", false).expect("can parse timestamp");
66///
67/// assert_eq!(ts.year, 2023);
68/// assert_eq!(ts.month, Some(3));
69/// assert_eq!(ts.day, Some(12));
70/// assert_eq!(ts.hour, Some(19));
71/// assert_eq!(ts.minute, Some(59));
72/// assert_eq!(ts.second, Some(5));
73/// assert_eq!(ts.microsecond, Some(123_400));
74/// assert_eq!(ts.offset, Some(TimeStampOffset {
75///     hours: -7,
76///     minutes: 0,
77/// }));
78/// ```
79pub fn parse_timestamp<'s>(
80    s: &'s str,
81    lenient_trailing_chars: bool,
82) -> Result<TimeStamp, DateTimeParseError> {
83    fn is_decimal_digit(c: char) -> bool {
84        c.is_ascii_digit()
85    }
86
87    fn from_digits<F: FromStr>(i: Span) -> Result<F, F::Err> {
88        i.input.parse::<F>()
89    }
90
91    fn digit2<F: FromStr>(input: Span) -> IResult<Span, F> {
92        map_res(take_while_m_n(2, 2, is_decimal_digit), from_digits::<F>)(input)
93    }
94
95    fn digit4<F: FromStr>(input: Span) -> IResult<Span, F> {
96        map_res(take_while_m_n(4, 4, is_decimal_digit), from_digits::<F>)(input)
97    }
98
99    let s = Span::new(s);
100    let (s, year): (Span, u16) =
101        digit4(s).map_err(|_| DateTimeParseError::ParsingFailed("year"))?;
102    let (s, month): (Span, Option<u8>) =
103        opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("month"))?;
104    let (s, day): (Span, Option<u8>) =
105        opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("day"))?;
106    let (s, hour): (Span, Option<u8>) =
107        opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("hour"))?;
108    let (s, minute): (Span, Option<u8>) =
109        opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("minute"))?;
110    let (s, second): (Span, Option<u8>) =
111        opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("second"))?;
112    let (s, second_fracs) = opt(preceded(tag("."), take_while_m_n(1, 4, is_decimal_digit)))(s)
113        .map_err(|_: nom::Err<nom::error::Error<Span<'s>>>| {
114            DateTimeParseError::ParsingFailed("fractional seconds")
115        })?;
116    let (s, offset_dir) =
117        opt(one_of("+-"))(s).map_err(|_: nom::Err<nom::error::Error<Span<'s>>>| {
118            DateTimeParseError::ParsingFailed("offset direction")
119        })?;
120
121    let offset_dir = match offset_dir.unwrap_or('+') {
122        '-' => -1i8,
123        _ => 1i8,
124    };
125    let (s, offset_hours): (Span, Option<i8>) =
126        opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("offset hours"))?;
127    let offset_hours = offset_hours.map(|h| h * offset_dir);
128    let (s, offset_minutes): (Span, Option<u8>) =
129        opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("offset minutes"))?;
130
131    if !lenient_trailing_chars && !s.is_empty() {
132        return Err(DateTimeParseError::UnexpectedCharacter(
133            s.offset,
134            s.input.chars().next().unwrap_or_default(),
135        ));
136    }
137
138    let microsecond = match second_fracs {
139        Some(fracs) => {
140            let fracs_multiplier = match fracs.len() {
141                1 => 100_000,
142                2 => 10_000,
143                3 => 1_000,
144                4 => 100,
145                _ => panic!("second_fracs.len() not in 1..=4"),
146            };
147            Some(
148                fracs
149                    .input
150                    .parse::<u32>()
151                    .expect("can parse fractional seconds as number")
152                    * fracs_multiplier,
153            )
154        }
155        None => None,
156    };
157
158    let offset = match (offset_hours, offset_minutes) {
159        (Some(hours), Some(minutes)) => Some(TimeStampOffset { hours, minutes }),
160        _ => None,
161    };
162
163    Ok(TimeStamp {
164        year,
165        month,
166        day,
167        hour,
168        minute,
169        second,
170        microsecond,
171        offset,
172    })
173}
174
175/// Implement `FromStr` for `TimeStamp` to allow parsing timestamps from strings
176impl FromStr for TimeStamp {
177    type Err = DateTimeParseError;
178
179    /// Synonymous with `parse_timestamp`
180    fn from_str(s: &str) -> Result<Self, Self::Err> {
181        parse_timestamp(s, false)
182    }
183}
184
185/// Implement `Display` for `TimeStamp` to allow formatting timestamps as HL7 strings
186impl Display for TimeStamp {
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188        write!(f, "{:04}", self.year)?;
189        if let Some(month) = self.month {
190            write!(f, "{:02}", month)?;
191            if let Some(day) = self.day {
192                write!(f, "{:02}", day)?;
193                if let Some(hour) = self.hour {
194                    write!(f, "{:02}", hour)?;
195                    if let Some(minute) = self.minute {
196                        write!(f, "{:02}", minute)?;
197                        if let Some(second) = self.second {
198                            write!(f, "{:02}", second)?;
199                            if let Some(microsecond) = self.microsecond {
200                                let microsecond = format!("{:06}", microsecond);
201                                write!(f, ".{}", &microsecond[..4])?;
202                            }
203                        }
204                    }
205                }
206            }
207        }
208        if let Some(offset) = &self.offset {
209            write!(f, "{}", offset)?;
210        }
211        Ok(())
212    }
213}
214
215#[cfg(test)]
216mod test {
217    use super::*;
218    use pretty_assertions_sorted::assert_eq;
219
220    #[test]
221    fn can_parse_time_with_offsets() {
222        let ts = "20230312195905.1234-0700";
223        let ts = parse_timestamp(ts, false).expect("can parse timestamp");
224
225        assert_eq!(ts.year, 2023);
226        assert_eq!(ts.month, Some(3));
227        assert_eq!(ts.day, Some(12));
228        assert_eq!(ts.hour, Some(19));
229        assert_eq!(ts.minute, Some(59));
230        assert_eq!(ts.second, Some(5));
231        assert_eq!(ts.microsecond, Some(123_400));
232        assert_eq!(
233            ts.offset,
234            Some(TimeStampOffset {
235                hours: -7,
236                minutes: 0,
237            })
238        );
239    }
240
241    #[test]
242    fn can_parse_time_without_offsets() {
243        let ts = "20230312195905.1234";
244        let ts = parse_timestamp(ts, false).expect("can parse timestamp");
245
246        assert_eq!(ts.year, 2023);
247        assert_eq!(ts.month, Some(3));
248        assert_eq!(ts.day, Some(12));
249        assert_eq!(ts.hour, Some(19));
250        assert_eq!(ts.minute, Some(59));
251        assert_eq!(ts.second, Some(5));
252        assert_eq!(ts.microsecond, Some(123_400));
253        assert_eq!(ts.offset, None);
254    }
255
256    #[test]
257    fn can_parse_time_without_offsets_or_fractional_seconds() {
258        let ts = "20230312195905";
259        let ts = parse_timestamp(ts, false).expect("can parse timestamp");
260
261        assert_eq!(ts.year, 2023);
262        assert_eq!(ts.month, Some(3));
263        assert_eq!(ts.day, Some(12));
264        assert_eq!(ts.hour, Some(19));
265        assert_eq!(ts.minute, Some(59));
266        assert_eq!(ts.second, Some(5));
267        assert_eq!(ts.microsecond, None);
268        assert_eq!(ts.offset, None);
269    }
270
271    #[test]
272    fn can_parse_time_with_offsets_without_fractional_seconds() {
273        let ts = "20230312195905-0700";
274        let ts = parse_timestamp(ts, false).expect("can parse timestamp");
275
276        assert_eq!(ts.year, 2023);
277        assert_eq!(ts.month, Some(3));
278        assert_eq!(ts.day, Some(12));
279        assert_eq!(ts.hour, Some(19));
280        assert_eq!(ts.minute, Some(59));
281        assert_eq!(ts.second, Some(5));
282        assert_eq!(ts.microsecond, None);
283        assert_eq!(
284            ts.offset,
285            Some(TimeStampOffset {
286                hours: -7,
287                minutes: 0,
288            })
289        );
290    }
291
292    #[test]
293    fn can_parse_time_with_only_year() {
294        let ts = "2023";
295        let ts = parse_timestamp(ts, false).expect("can parse timestamp");
296
297        assert_eq!(ts.year, 2023);
298        assert_eq!(ts.month, None);
299        assert_eq!(ts.day, None);
300        assert_eq!(ts.hour, None);
301        assert_eq!(ts.minute, None);
302        assert_eq!(ts.second, None);
303        assert_eq!(ts.microsecond, None);
304        assert_eq!(ts.offset, None);
305    }
306
307    #[test]
308    fn cant_parse_bad_timestamps() {
309        assert!(parse_timestamp("23", false).is_err());
310        assert!(parse_timestamp("abcd", false).is_err());
311        assert!(parse_timestamp("202303121959051", false).is_err());
312    }
313
314    #[test]
315    fn can_parse_timestamp_fromstr() {
316        let ts: TimeStamp = "20230312195905.1234-0700"
317            .parse()
318            .expect("can parse timestamp");
319
320        assert_eq!(ts.year, 2023);
321        assert_eq!(ts.month, Some(3));
322        assert_eq!(ts.day, Some(12));
323        assert_eq!(ts.hour, Some(19));
324        assert_eq!(ts.minute, Some(59));
325        assert_eq!(ts.second, Some(5));
326        assert_eq!(ts.microsecond, Some(123_400));
327        assert_eq!(
328            ts.offset,
329            Some(TimeStampOffset {
330                hours: -7,
331                minutes: 0,
332            })
333        );
334    }
335
336    #[test]
337    fn can_format_timestamp() {
338        let ts = TimeStamp {
339            year: 2023,
340            month: Some(3),
341            day: Some(12),
342            hour: Some(19),
343            minute: Some(59),
344            second: Some(5),
345            microsecond: Some(123_400),
346            offset: Some(TimeStampOffset {
347                hours: -7,
348                minutes: 0,
349            }),
350        };
351        assert_eq!(ts.to_string(), "20230312195905.1234-0700");
352
353        let ts = TimeStamp {
354            year: 2023,
355            month: Some(3),
356            day: Some(12),
357            hour: Some(19),
358            minute: None,
359            second: None,
360            microsecond: None,
361            offset: Some(TimeStampOffset {
362                hours: -7,
363                minutes: 0,
364            }),
365        };
366        assert_eq!(ts.to_string(), "2023031219-0700");
367    }
368}