hl7_parser/datetime/
date.rs

1use crate::parser::Span;
2use nom::{
3    bytes::complete::take_while_m_n,
4    combinator::{map_res, opt},
5    IResult,
6};
7use std::{fmt::Display, str::FromStr};
8
9use super::DateTimeParseError;
10
11/// A parsed date without a time component
12#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub struct Date {
15    /// The year of the date
16    pub year: u16,
17    /// The month of the date (1-12)
18    pub month: Option<u8>,
19    /// The day of the date (1-31)
20    pub day: Option<u8>,
21}
22
23/// Parse an HL7 date in the format: `YYYY[MM[DD]`
24///
25/// # Arguments
26/// * `s` - The string to parse
27/// * `lenient_trailing_chars` - If true, allow trailing characters after the date, otherwise throw
28///   an error
29///
30/// # Example
31///
32/// ```
33/// use hl7_parser::datetime::{parse_date, Date};
34///
35/// let date: Date = parse_date("20230312", false).expect("can parse date");
36///
37/// assert_eq!(date.year, 2023);
38/// assert_eq!(date.month, Some(3));
39/// assert_eq!(date.day, Some(12));
40/// ```
41pub fn parse_date(s: &str, lenient_trailing_chars: bool) -> Result<Date, DateTimeParseError> {
42    fn is_decimal_digit(c: char) -> bool {
43        c.is_ascii_digit()
44    }
45
46    fn from_digits<F: FromStr>(i: Span) -> Result<F, F::Err> {
47        i.input.parse::<F>()
48    }
49
50    fn digit2<F: FromStr>(input: Span) -> IResult<Span, F> {
51        map_res(take_while_m_n(2, 2, is_decimal_digit), from_digits::<F>)(input)
52    }
53
54    fn digit4<F: FromStr>(input: Span) -> IResult<Span, F> {
55        map_res(take_while_m_n(4, 4, is_decimal_digit), from_digits::<F>)(input)
56    }
57
58    let s = Span::new(s);
59    let (s, year): (Span, u16) =
60        digit4(s).map_err(|_| DateTimeParseError::ParsingFailed("year"))?;
61    let (s, month): (Span, Option<u8>) =
62        opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("month"))?;
63    let (s, day): (Span, Option<u8>) =
64        opt(digit2)(s).map_err(|_| DateTimeParseError::ParsingFailed("day"))?;
65
66    if !lenient_trailing_chars && !s.is_empty() {
67        return Err(DateTimeParseError::UnexpectedCharacter(
68            s.offset,
69            s.input.chars().next().unwrap_or_default(),
70        ));
71    }
72
73    Ok(Date { year, month, day })
74}
75
76/// Implement `FromStr` for `TimeStamp` to allow parsing timestamps from strings
77impl FromStr for Date {
78    type Err = DateTimeParseError;
79
80    /// Synonymous with `parse_timestamp`
81    fn from_str(s: &str) -> Result<Self, Self::Err> {
82        parse_date(s, false)
83    }
84}
85
86/// Implement `Display` for `TimeStamp` to allow formatting timestamps as HL7 strings
87impl Display for Date {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        write!(f, "{:04}", self.year)?;
90        if let Some(month) = self.month {
91            write!(f, "{:02}", month)?;
92            if let Some(day) = self.day {
93                write!(f, "{:02}", day)?;
94            }
95        }
96        Ok(())
97    }
98}
99
100#[cfg(test)]
101mod test {
102    use super::*;
103    use pretty_assertions_sorted::assert_eq;
104
105    #[test]
106    fn can_parse_date() {
107        let date = "20230312";
108        let date = parse_date(date, false).expect("can parse date");
109
110        assert_eq!(date.year, 2023);
111        assert_eq!(date.month, Some(3));
112        assert_eq!(date.day, Some(12));
113    }
114}