fdate 0.2.2

Natural date input parsing
Documentation
use chrono::Month;
use nom::{IResult, Parser, character::complete::multispace1, combinator::opt};

use super::common::{self, RelativeDirection};

pub(super) fn parse_relative_partial_date(input: &str) -> IResult<&str, RelativePartialDate> {
    opt(common::parse_relative_direction
        .and(multispace1)
        .map(|(direction, _)| direction))
    .and(parse_partial_date_body)
    .map(|(direction, (day, month))| RelativePartialDate {
        direction,
        day,
        month,
    })
    .parse(input)
}

pub(super) fn parse_partial_date_body(input: &str) -> IResult<&str, (u32, Month)> {
    (common::parse_ordinal_day, multispace1, common::parse_month)
        .map(|(day, _, month)| (day, month))
        .or(
            (common::parse_month, multispace1, common::parse_ordinal_day)
                .map(|(month, _, day)| (day, month)),
        )
        .parse(input)
}

#[derive(Debug, PartialEq, Eq)]
pub(crate) struct RelativePartialDate {
    pub direction: Option<RelativeDirection>,
    pub day: u32,
    pub month: Month,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_partial_date_body() {
        assert_eq!(
            parse_partial_date_body("14th april"),
            Ok(("", (14, Month::April)))
        );
        assert_eq!(
            parse_partial_date_body("april 14th"),
            Ok(("", (14, Month::April)))
        );
        assert!(parse_partial_date_body("14th").is_err());
        assert!(parse_partial_date_body("next 14th april").is_err());
    }

    #[test]
    fn parses_relative_partial_dates() {
        assert_eq!(
            parse_relative_partial_date("14th april"),
            Ok((
                "",
                RelativePartialDate {
                    direction: None,
                    day: 14,
                    month: Month::April,
                },
            ))
        );
        assert_eq!(
            parse_relative_partial_date("next 1st December"),
            Ok((
                "",
                RelativePartialDate {
                    direction: Some(RelativeDirection::Future),
                    day: 1,
                    month: Month::December,
                },
            ))
        );
        assert_eq!(
            parse_relative_partial_date("next April 14th"),
            Ok((
                "",
                RelativePartialDate {
                    direction: Some(RelativeDirection::Future),
                    day: 14,
                    month: Month::April,
                },
            ))
        );
        assert_eq!(
            parse_relative_partial_date("last 14 April"),
            Ok((
                "",
                RelativePartialDate {
                    direction: Some(RelativeDirection::Past),
                    day: 14,
                    month: Month::April,
                },
            ))
        );
        assert_eq!(
            parse_relative_partial_date("last April 14"),
            Ok((
                "",
                RelativePartialDate {
                    direction: Some(RelativeDirection::Past),
                    day: 14,
                    month: Month::April,
                },
            ))
        );
    }

    #[test]
    fn parses_partial_dates_case_insensitively() {
        assert_eq!(
            parse_relative_partial_date("Next 14th APRIL"),
            Ok((
                "",
                RelativePartialDate {
                    direction: Some(RelativeDirection::Future),
                    day: 14,
                    month: Month::April,
                },
            ))
        );
    }

    #[test]
    fn leaves_remaining_input_for_larger_parsers() {
        assert_eq!(
            parse_relative_partial_date("14th april please"),
            Ok((
                " please",
                RelativePartialDate {
                    direction: None,
                    day: 14,
                    month: Month::April,
                },
            ))
        );
    }

    #[test]
    fn rejects_bare_days_of_month() {
        assert!(parse_relative_partial_date("14th").is_err());
        assert!(parse_relative_partial_date("next 14").is_err());
        assert!(parse_relative_partial_date("hello").is_err());
    }
}