hl7_parser/datetime/
time.rs

1use super::{DateTimeParseError, TimeStampOffset};
2use crate::parser::Span;
3use nom::{
4    bytes::complete::{tag, take_while_m_n},
5    character::complete::one_of,
6    combinator::{map_res, opt},
7    sequence::preceded,
8    IResult,
9};
10use std::{fmt::Display, str::FromStr};
11
12/// The results of parsing a timestamp. Note that the timestamp is not validated,
13/// i.e. it may not be a valid date or time.
14#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub struct Time {
17    /// The hour of the time (0-23)
18    pub hour: u8,
19    /// The minute of the time (0-59)
20    pub minute: Option<u8>,
21    /// The second of the time (0-59)
22    pub second: Option<u8>,
23    /// The microsecond of the time (0-999_900)
24    pub microsecond: Option<u32>,
25    /// The timezone offset of the time
26    pub offset: Option<TimeStampOffset>,
27}
28
29/// Parse an HL7 time in the format: `HH[MM[SS[.S[S[S[S]]]]]][+/-ZZZZ]`
30///
31/// # Arguments
32/// * `s` - The string to parse
33/// * `lenient_trailing_chars` - If true, allow trailing characters after the timestamp, otherwise
34///   throw an error
35///
36/// # Example
37///
38/// ```
39/// use hl7_parser::datetime::{parse_time, Time, TimeStampOffset};
40///
41/// let time: Time = parse_time("195905.1234-0700", false).expect("can parse time");
42///
43/// assert_eq!(time.hour, 19);
44/// assert_eq!(time.minute, Some(59));
45/// assert_eq!(time.second, Some(5));
46/// assert_eq!(time.microsecond, Some(123_400));
47/// assert_eq!(time.offset, Some(TimeStampOffset {
48///     hours: -7,
49///     minutes: 0,
50/// }));
51/// ```
52pub fn parse_time<'s>(
53    s: &'s str,
54    lenient_trailing_chars: bool,
55) -> Result<Time, DateTimeParseError> {
56    fn is_decimal_digit(c: char) -> bool {
57        c.is_ascii_digit()
58    }
59
60    fn from_digits<F: FromStr>(i: Span) -> Result<F, F::Err> {
61        i.input.parse::<F>()
62    }
63
64    fn digit2<F: FromStr>(input: Span) -> IResult<Span, F> {
65        map_res(take_while_m_n(2, 2, is_decimal_digit), from_digits::<F>)(input)
66    }
67
68    let s = Span::new(s);
69    let (s, hour): (Span, u8) = digit2(s).map_err(|_| DateTimeParseError::ParsingFailed("hour"))?;
70    let (s, minute): (Span, Option<u8>) =
71        opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("minute"))?;
72    let (s, second): (Span, Option<u8>) =
73        opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("second"))?;
74    let (s, second_fracs) = opt(preceded(tag("."), take_while_m_n(1, 4, is_decimal_digit)))(s)
75        .map_err(|_: nom::Err<nom::error::Error<Span<'s>>>| {
76            DateTimeParseError::ParsingFailed("fractional seconds")
77        })?;
78    let (s, offset_dir) =
79        opt(one_of("+-"))(s).map_err(|_: nom::Err<nom::error::Error<Span<'s>>>| {
80            DateTimeParseError::ParsingFailed("offset direction")
81        })?;
82
83    let offset_dir = match offset_dir.unwrap_or('+') {
84        '-' => -1i8,
85        _ => 1i8,
86    };
87    let (s, offset_hours): (Span, Option<i8>) =
88        opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("offset hours"))?;
89    let offset_hours = offset_hours.map(|h| h * offset_dir);
90    let (s, offset_minutes): (Span, Option<u8>) =
91        opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("offset minutes"))?;
92
93    if !lenient_trailing_chars && !s.is_empty() {
94        return Err(DateTimeParseError::UnexpectedCharacter(
95            s.offset,
96            s.input.chars().next().unwrap_or_default(),
97        ));
98    }
99
100    let microsecond = match second_fracs {
101        Some(fracs) => {
102            let fracs_multiplier = match fracs.len() {
103                1 => 100_000,
104                2 => 10_000,
105                3 => 1_000,
106                4 => 100,
107                _ => panic!("second_fracs.len() not in 1..=4"),
108            };
109            Some(
110                fracs
111                    .input
112                    .parse::<u32>()
113                    .expect("can parse fractional seconds as number")
114                    * fracs_multiplier,
115            )
116        }
117        None => None,
118    };
119
120    let offset = match (offset_hours, offset_minutes) {
121        (Some(hours), Some(minutes)) => Some(TimeStampOffset { hours, minutes }),
122        _ => None,
123    };
124
125    Ok(Time {
126        hour,
127        minute,
128        second,
129        microsecond,
130        offset,
131    })
132}
133
134/// Implement `FromStr` for `Time` to allow parsing timestamps from strings
135impl FromStr for Time {
136    type Err = DateTimeParseError;
137
138    /// Synonymous with `parse_time`
139    fn from_str(s: &str) -> Result<Self, Self::Err> {
140        parse_time(s, false)
141    }
142}
143
144/// Implement `Display` for `Time` to allow formatting timestamps as HL7 strings
145impl Display for Time {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        write!(f, "{:02}", self.hour)?;
148        if let Some(minute) = self.minute {
149            write!(f, "{:02}", minute)?;
150            if let Some(second) = self.second {
151                write!(f, "{:02}", second)?;
152                if let Some(microsecond) = self.microsecond {
153                    let microsecond = format!("{:06}", microsecond);
154                    write!(f, ".{}", &microsecond[..4])?;
155                }
156            }
157        }
158        if let Some(offset) = &self.offset {
159            write!(f, "{}", offset)?;
160        }
161        Ok(())
162    }
163}
164
165#[cfg(test)]
166mod test {
167    use super::*;
168    use pretty_assertions_sorted::assert_eq;
169
170    #[test]
171    fn can_parse_time_with_offsets() {
172        let ts = "195905.1234-0700";
173        let ts = parse_time(ts, false).expect("can parse time");
174
175        assert_eq!(ts.hour, 19);
176        assert_eq!(ts.minute, Some(59));
177        assert_eq!(ts.second, Some(5));
178        assert_eq!(ts.microsecond, Some(123_400));
179        assert_eq!(
180            ts.offset,
181            Some(TimeStampOffset {
182                hours: -7,
183                minutes: 0,
184            })
185        );
186    }
187}