fdate 0.2.2

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

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

pub(super) fn parse_relative_day_of_month(input: &str) -> IResult<&str, RelativeDayOfMonth> {
    opt(common::parse_relative_direction
        .and(multispace1)
        .map(|(direction, _)| direction))
    .and(
        common::parse_ordinal_day
            .and(not(peek(multispace1.and(common::parse_month))))
            .map(|(day, _)| day),
    )
    .map(|(direction, day)| RelativeDayOfMonth { direction, day })
    .parse(input)
}

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

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

    #[test]
    fn parses_bare_relative_days_of_month() {
        assert_eq!(
            parse_relative_day_of_month("14"),
            Ok((
                "",
                RelativeDayOfMonth {
                    direction: None,
                    day: 14,
                },
            ))
        );
        assert_eq!(
            parse_relative_day_of_month("14th"),
            Ok((
                "",
                RelativeDayOfMonth {
                    direction: None,
                    day: 14,
                },
            ))
        );
    }

    #[test]
    fn parses_relative_days_of_month_with_direction() {
        assert_eq!(
            parse_relative_day_of_month("next 14th"),
            Ok((
                "",
                RelativeDayOfMonth {
                    direction: Some(RelativeDirection::Future),
                    day: 14,
                },
            ))
        );
        assert_eq!(
            parse_relative_day_of_month("last 14"),
            Ok((
                "",
                RelativeDayOfMonth {
                    direction: Some(RelativeDirection::Past),
                    day: 14,
                },
            ))
        );
    }

    #[test]
    fn parses_relative_days_of_month_case_insensitively() {
        assert_eq!(
            parse_relative_day_of_month("Next 1st"),
            Ok((
                "",
                RelativeDayOfMonth {
                    direction: Some(RelativeDirection::Future),
                    day: 1,
                },
            ))
        );
        assert_eq!(
            parse_relative_day_of_month("LAST 22ND"),
            Ok((
                "",
                RelativeDayOfMonth {
                    direction: Some(RelativeDirection::Past),
                    day: 22,
                },
            ))
        );
    }

    #[test]
    fn rejects_partial_dates() {
        assert!(parse_relative_day_of_month("14th april").is_err());
        assert!(parse_relative_day_of_month("next 1st December").is_err());
        assert!(parse_relative_day_of_month("next April 14th").is_err());
        assert!(parse_relative_day_of_month("last 14 April").is_err());
        assert!(parse_relative_day_of_month("last April 14").is_err());
    }

    #[test]
    fn leaves_remaining_input_for_larger_parsers() {
        assert_eq!(
            parse_relative_day_of_month("14th please"),
            Ok((
                " please",
                RelativeDayOfMonth {
                    direction: None,
                    day: 14,
                },
            ))
        );
    }

    #[test]
    fn rejects_invalid_relative_days_of_month() {
        assert!(parse_relative_day_of_month("next").is_err());
        assert!(parse_relative_day_of_month("0th").is_err());
        assert!(parse_relative_day_of_month("32").is_err());
        assert!(parse_relative_day_of_month("hello").is_err());
        assert!(parse_relative_day_of_month("next smarch").is_err());
    }
}