flexitime 0.1.0

A flexible time parsing library for Rust
Documentation
use std::num::ParseIntError;

use chrono::NaiveDate;
use nom::{
    Parser, branch::alt, bytes::complete::take_while_m_n, character::complete::char,
    combinator::map_res,
};

use crate::error::FlexitimeResult2;

#[derive(Debug, Clone, PartialEq, strum_macros::Display)]
pub enum DateComponent {
    Day,
    Month,
    Year,
}

#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum AbsoluteDateError {
    #[error("Could not parse {0} component of date: {1}")]
    UnparseableComponent(DateComponent, ParseIntError),

    #[error("{1} is out of range for date component {0}")]
    OutOfRangeComponent(DateComponent, u16),

    #[error("The provided date is invalid")]
    InvalidDate,
}

fn date_delimiter(input: &str) -> FlexitimeResult2<&str, char> {
    alt((char('-'), char('/'))).parse(input)
}

fn parse_year(input: &str) -> FlexitimeResult2<&str, u16> {
    map_res(
        take_while_m_n(4, 4, |c: char| c.is_ascii_digit()),
        |s: &str| {
            s.parse::<u16>()
                .map_err(|e| AbsoluteDateError::UnparseableComponent(DateComponent::Year, e))
                .and_then(|year| {
                    if year > 3000 {
                        return Err(AbsoluteDateError::OutOfRangeComponent(
                            DateComponent::Year,
                            year,
                        ));
                    }

                    Ok(year)
                })
        },
    )
    .parse(input)
}

fn parse_day(input: &str) -> FlexitimeResult2<&str, u8> {
    map_res(
        take_while_m_n(1, 2, |c: char| c.is_ascii_digit()),
        |s: &str| {
            s.parse::<u8>()
                .map_err(|e| AbsoluteDateError::UnparseableComponent(DateComponent::Day, e))
                .and_then(|day| {
                    if day > 31 {
                        return Err(AbsoluteDateError::OutOfRangeComponent(
                            DateComponent::Day,
                            day.into(),
                        ));
                    }

                    Ok(day)
                })
        },
    )
    .parse(input)
}

fn parse_month(input: &str) -> FlexitimeResult2<&str, u8> {
    map_res(
        take_while_m_n(1, 2, |c: char| c.is_ascii_digit()),
        |s: &str| {
            s.parse::<u8>()
                .map_err(|e| AbsoluteDateError::UnparseableComponent(DateComponent::Month, e))
                .and_then(|month| {
                    if month > 12 {
                        return Err(AbsoluteDateError::OutOfRangeComponent(
                            DateComponent::Month,
                            month.into(),
                        ));
                    }

                    Ok(month)
                })
        },
    )
    .parse(input)
}

pub fn parse_date(input: &str) -> FlexitimeResult2<&str, NaiveDate> {
    alt((parse_day_month_year, parse_year_month_day)).parse(input)
}

fn parse_day_month_year(input: &str) -> FlexitimeResult2<&str, NaiveDate> {
    map_res(
        (
            parse_day,
            date_delimiter,
            parse_month,
            date_delimiter,
            parse_year,
        ),
        |(day, _, month, _, year)| {
            NaiveDate::from_ymd_opt(year.into(), month.into(), day.into())
                .ok_or(AbsoluteDateError::InvalidDate)
        },
    )
    .parse(input)
}

fn parse_year_month_day(input: &str) -> FlexitimeResult2<&str, NaiveDate> {
    map_res(
        (
            parse_year,
            date_delimiter,
            parse_month,
            date_delimiter,
            parse_day,
        ),
        |(year, _, month, _, day)| {
            NaiveDate::from_ymd_opt(year.into(), month.into(), day.into())
                .ok_or(AbsoluteDateError::InvalidDate)
        },
    )
    .parse(input)
}

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

    #[test]
    fn test_parse_ymd() {
        assert_eq!(
            parse_date("2023-01-01"),
            Ok(("", NaiveDate::from_ymd_opt(2023, 1, 1).unwrap()))
        );
        assert_eq!(
            parse_date("2029/01/01"),
            Ok(("", NaiveDate::from_ymd_opt(2029, 1, 1).unwrap()))
        );
    }

    #[test]
    fn test_parse_dmy() {
        assert_eq!(
            parse_date("01-01-2023"),
            Ok(("", NaiveDate::from_ymd_opt(2023, 1, 1).unwrap()))
        );
        assert_eq!(
            parse_date("01/01/2029"),
            Ok(("", NaiveDate::from_ymd_opt(2029, 1, 1).unwrap()))
        );
    }
}