flexitime 0.1.0

A flexible time parsing library for Rust
Documentation
use chrono::NaiveTime;
use nom::{
    Parser,
    branch::alt,
    bytes::complete::{tag_no_case, take_while_m_n},
    character::complete::{char, space0},
    combinator::{map_res, opt, peek, value},
    sequence::preceded,
};
use strum_macros::Display;

use crate::error::FlexitimeResult2;

#[derive(PartialEq, Debug, Clone)]
pub struct WallClockTime {
    pub hour: u8,
    pub minute: u8,
    pub second: u8,
}

impl WallClockTime {
    pub fn to_naive_time(&self) -> NaiveTime {
        NaiveTime::from_hms_opt(self.hour.into(), self.minute.into(), self.second.into()).unwrap()
    }
}

#[derive(PartialEq, Debug, thiserror::Error)]
pub enum WallClockTimeError {
    #[error("Hours value '{0}' is out of range in a 24-hour format")]
    OutOfRangeHours24(u8),

    #[error("Hours value '{0}' is out of range in a 12-hour format")]
    OutOfRangeHours12(u8),

    #[error("Minutes value '{0}' is out of range")]
    OutOfRangeMinutes(u8),

    #[error("Seconds value '{0}' is out of range")]
    OutOfRangeSeconds(u8),
}

impl WallClockTime {
    pub fn new(
        mut hour: u8,
        minute: u8,
        second: u8,
        period: Option<TimePeriod>,
    ) -> Result<Self, WallClockTimeError> {
        if hour > 23 && period.is_none() {
            return Err(WallClockTimeError::OutOfRangeHours24(hour));
        } else if (hour < 1 || hour > 12) && period.is_some() {
            return Err(WallClockTimeError::OutOfRangeHours12(hour));
        }

        if let Some(TimePeriod::Pm) = period {
            hour += 12;

            if hour == 24 {
                hour = 0;
            }
        }

        if minute > 59 {
            return Err(WallClockTimeError::OutOfRangeMinutes(minute));
        }
        if second > 59 {
            return Err(WallClockTimeError::OutOfRangeSeconds(second));
        }

        Ok(WallClockTime {
            hour,
            minute,
            second,
        })
    }
}

fn parse_u8(input: &str) -> Result<u8, std::num::ParseIntError> {
    input.parse()
}

fn parse_hours(input: &str) -> FlexitimeResult2<&str, u8> {
    map_res(take_while_m_n(1, 2, |c: char| c.is_ascii_digit()), parse_u8).parse(input)
}

fn parse_optional_mins_or_secs(input: &str) -> FlexitimeResult2<&str, Option<u8>> {
    if let Ok((_, _)) = peek(char::<&str, nom::error::Error<&str>>(':')).parse(input) {
        let (input, seconds) = preceded(
            char(':'),
            map_res(take_while_m_n(2, 2, |c: char| c.is_ascii_digit()), parse_u8),
        )
        .parse(input)?;

        return Ok((input, Some(seconds)));
    }

    Ok((input, None))
}

#[derive(Debug, Clone, PartialEq)]
pub enum TimePeriod {
    Am,
    Pm,
}

fn parse_am_pm_suffix(input: &str) -> FlexitimeResult2<&str, Option<TimePeriod>> {
    opt(preceded(
        space0,
        alt((
            value(TimePeriod::Am, tag_no_case("am")),
            value(TimePeriod::Pm, tag_no_case("pm")),
        )),
    ))
    .parse(input)
}

pub fn parse_wall_clock_time(input: &str) -> FlexitimeResult2<&str, WallClockTime> {
    map_res(
        (
            parse_hours,
            parse_optional_mins_or_secs,
            parse_optional_mins_or_secs,
            parse_am_pm_suffix,
        ),
        |(hours, minutes, seconds, period)| {
            WallClockTime::new(hours, minutes.unwrap_or(0), seconds.unwrap_or(0), period)
        },
    )
    .parse(input)
}

#[cfg(test)]
mod tests {

    use crate::error::FlexitimeError2;

    use super::*;

    #[test]
    fn test_parse_hours_mins() {
        assert_eq!(
            parse_wall_clock_time("12:05"),
            Ok(("", WallClockTime::new(12, 5, 0, None).unwrap()))
        )
    }

    #[test]
    fn test_parse_hours_leading_zero() {
        assert_eq!(
            parse_wall_clock_time("07:05"),
            Ok(("", WallClockTime::new(7, 5, 0, None).unwrap()))
        )
    }

    #[test]
    fn test_parse_hours_single_digit() {
        assert_eq!(
            parse_wall_clock_time("7:05"),
            Ok(("", WallClockTime::new(7, 5, 0, None).unwrap()))
        )
    }

    #[test]
    fn test_parse_hours_mins_secs() {
        assert_eq!(
            parse_wall_clock_time("12:05:30"),
            Ok(("", WallClockTime::new(12, 5, 30, None).unwrap()))
        )
    }

    #[test]
    fn test_hours_out_of_range() {
        assert_eq!(
            parse_wall_clock_time("25:05:30"),
            Err(nom::Err::Error(FlexitimeError2::WallClockTime(
                WallClockTimeError::OutOfRangeHours24(25)
            )))
        )
    }

    #[test]
    fn test_mins_out_of_range() {
        assert_eq!(
            parse_wall_clock_time("23:65:30"),
            Err(nom::Err::Error(FlexitimeError2::WallClockTime(
                WallClockTimeError::OutOfRangeMinutes(65)
            )))
        )
    }

    #[test]
    fn test_secs_out_of_range() {
        assert_eq!(
            parse_wall_clock_time("23:05:60"),
            Err(nom::Err::Error(FlexitimeError2::WallClockTime(
                WallClockTimeError::OutOfRangeSeconds(60)
            )))
        )
    }

    #[test]
    fn test_parse_time_with_am() {
        assert_eq!(
            parse_wall_clock_time("12:05:30 am"),
            Ok((
                "",
                WallClockTime::new(12, 5, 30, Some(TimePeriod::Am)).unwrap()
            ))
        )
    }

    #[test]
    fn test_parse_time_with_pm() {
        assert_eq!(
            parse_wall_clock_time("8:05:30 pm"),
            Ok((
                "",
                WallClockTime::new(8, 5, 30, Some(TimePeriod::Pm)).unwrap()
            ))
        )
    }

    #[test]
    fn test_parse_time_with_pm_boundary() {
        assert_eq!(
            parse_wall_clock_time("12:05:30 pm"),
            Ok((
                "",
                WallClockTime::new(12, 5, 30, Some(TimePeriod::Pm)).unwrap()
            ))
        )
    }

    #[test]
    fn test_parse_time_with_mixed_case_period() {
        assert_eq!(
            parse_wall_clock_time("8:05:30 pM"),
            Ok((
                "",
                WallClockTime::new(8, 5, 30, Some(TimePeriod::Pm)).unwrap()
            ))
        )
    }
    #[test]
    fn test_parse_time_with_no_space_period() {
        assert_eq!(
            parse_wall_clock_time("3:00pm"),
            Ok((
                "",
                WallClockTime::new(3, 0, 0, Some(TimePeriod::Pm)).unwrap()
            ))
        )
    }

    #[test]
    fn test_parse_time_no_mins() {
        assert_eq!(
            parse_wall_clock_time("3pm"),
            Ok((
                "",
                WallClockTime::new(3, 0, 0, Some(TimePeriod::Pm)).unwrap()
            ))
        )
    }

    #[test]
    fn test_out_of_range_12_hour_overflow() {
        assert_eq!(
            parse_wall_clock_time("15am"),
            Err(nom::Err::Error(FlexitimeError2::WallClockTime(
                WallClockTimeError::OutOfRangeHours12(15)
            )))
        )
    }

    #[test]
    fn test_out_of_range_12_hour_underflow() {
        assert_eq!(
            parse_wall_clock_time("0am"),
            Err(nom::Err::Error(FlexitimeError2::WallClockTime(
                WallClockTimeError::OutOfRangeHours12(0)
            )))
        )
    }
}