iso8601-duration 0.2.0

Parse ISO8601 duration format.
Documentation
use std::time::Duration as StdDuration;
use std::{fmt, str::FromStr};

use nom::{
    branch::alt,
    bytes::complete::tag,
    character::complete::digit1,
    combinator::{all_consuming, map_res, opt},
    error::{ErrorKind, ParseError},
    number::complete::float,
    sequence::{preceded, separated_pair, terminated, tuple},
    Err, Finish, IResult,
};

#[derive(Debug, PartialEq, Clone, Copy)]
pub struct Duration {
    pub year: f32,
    pub month: f32,
    pub day: f32,
    pub hour: f32,
    pub minute: f32,
    pub second: f32,
}

impl Duration {
    /// Create a new duration
    pub fn new(year: f32, month: f32, day: f32, hour: f32, minute: f32, second: f32) -> Self {
        Duration {
            year,
            month,
            day,
            hour,
            minute,
            second,
        }
    }

    /// Return number of years in the duration
    ///
    /// This method will return `None` is `Duration` contains
    /// `second`, `minute` or `hour`.
    pub fn num_years(&self) -> Option<f32> {
        if self.second > 0.0 || self.minute > 0.0 || self.hour > 0.0 {
            return None;
        }

        Some(self.year + self.month / 12.)
    }

    /// Return number of months in the duration
    ///
    /// This method will return `None` is `Duration` contains
    /// `second`, `minute` or `hour`.
    pub fn num_months(&self) -> Option<f32> {
        if self.second > 0.0 || self.minute > 0.0 || self.hour > 0.0 {
            return None;
        }

        Some(self.year * 12. + self.month)
    }

    /// Return number of weeks in the duration
    ///
    /// This method will return `None` is `Duration` contains
    /// `year` or `month`.
    pub fn num_weeks(&self) -> Option<f32> {
        if self.month > 0.0 || self.year > 0.0 {
            return None;
        }

        Some(
            self.second / 60. / 60. / 24. / 7.
                + self.minute / 60. / 24. / 7.
                + self.hour / 24. / 7.
                + self.day / 7.,
        )
    }

    /// Return number of days in the duration
    ///
    /// This method will return `None` is `Duration` contains
    /// `year` or `month`.
    pub fn num_days(&self) -> Option<f32> {
        if self.month > 0.0 || self.year > 0.0 {
            return None;
        }

        Some(self.second / 60. / 60. / 24. + self.minute / 60. / 24. + self.hour / 24. + self.day)
    }

    /// Return number of hours in the duration
    ///
    /// This method will return `None` is `Duration` contains
    /// `year` or `month`.
    pub fn num_hours(&self) -> Option<f32> {
        if self.month > 0.0 || self.year > 0.0 {
            return None;
        }

        Some(self.second / 60. / 60. + self.minute / 60. + self.hour + self.day * 24.)
    }

    /// Return number of minutes in the duration
    ///
    /// This method will return `None` is `Duration` contains
    /// `year` or `month`.
    pub fn num_minutes(&self) -> Option<f32> {
        if self.month > 0.0 || self.year > 0.0 {
            return None;
        }

        Some(self.second / 60. + self.minute + self.hour * 60. + self.day * 60. * 24.)
    }

    /// Return number of seconds in the duration
    ///
    /// This method will return `None` is `Duration` contains
    /// `year` or `month`.
    pub fn num_seconds(&self) -> Option<f32> {
        if self.month > 0.0 || self.year > 0.0 {
            return None;
        }

        Some(self.second + self.minute * 60. + self.hour * 60. * 60. + self.day * 60. * 60. * 24.)
    }

    /// Convert duration to `std::time::Duration`.
    ///
    /// This method will return `None` is `Duration` contains
    /// `year` or `month`.
    ///
    /// See to know how to convert a `Duration` contains
    /// `year` or `month`.
    pub fn to_std(&self) -> Option<StdDuration> {
        self.num_seconds().map(StdDuration::from_secs_f32)
    }

    /// Parse given string into Duration
    pub fn parse(input: &str) -> Result<Duration, ParseDurationError> {
        all_consuming(preceded(
            tag("P"),
            alt((parse_week_format, parse_basic_format)),
        ))(input)
        .finish()
        .map(|(_, duration)| duration)
        .map_err(|err| ParseDurationError::new(input, err))
    }
}

#[derive(PartialEq, Eq)]
pub struct ParseDurationError {
    pub input: String,
    pub position: usize,
    pub kind: ErrorKind,
}

impl ParseDurationError {
    fn new(input: &str, err: nom::error::Error<&str>) -> Self {
        ParseDurationError {
            input: input.to_string(),
            position: input.len() - err.input.len(),
            kind: err.code,
        }
    }
}

impl fmt::Debug for ParseDurationError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Parse error: {:?} in {:?} at position {}",
            self.kind, self.input, self.position
        )
    }
}

impl FromStr for Duration {
    type Err = ParseDurationError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Duration::parse(s)
    }
}

impl fmt::Display for Duration {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("P")?;
        if self.year > 0.0 {
            write!(f, "{}Y", self.year)?;
        }
        if self.month > 0.0 {
            write!(f, "{}M", self.month)?;
        }
        if self.day > 0.0 {
            write!(f, "{}D", self.day)?;
        }
        if self.hour > 0.0 || self.minute > 0.0 || self.second > 0.0 {
            f.write_str("T")?;
        }
        if self.hour > 0.0 {
            write!(f, "{}H", self.hour)?;
        }
        if self.minute > 0.0 {
            write!(f, "{}M", self.minute)?;
        }
        if self.second > 0.0 {
            write!(f, "{}S", self.second)?;
        }
        Ok(())
    }
}

fn decimal_comma_number(input: &str) -> IResult<&str, f32> {
    map_res(separated_pair(digit1, tag(","), digit1), |(a, b)| {
        f32::from_str(&format!("{}.{}", a, b))
    })(input)
}

fn value_with_designator(designator: &str) -> impl Fn(&str) -> IResult<&str, f32> + '_ {
    move |input| {
        terminated(
            alt((float, decimal_comma_number, map_res(digit1, f32::from_str))),
            tag(designator),
        )(input)
    }
}

fn parse_basic_format(input: &str) -> IResult<&str, Duration> {
    let (input, (year, month, day)) = tuple((
        opt(value_with_designator("Y")),
        opt(value_with_designator("M")),
        opt(value_with_designator("D")),
    ))(input)?;

    let (input, time) = opt(preceded(
        tag("T"),
        tuple((
            opt(value_with_designator("H")),
            opt(value_with_designator("M")),
            opt(value_with_designator("S")),
        )),
    ))(input)?;

    let (hour, minute, second) = time.unwrap_or_default();

    if year.is_none()
        && month.is_none()
        && day.is_none()
        && hour.is_none()
        && minute.is_none()
        && second.is_none()
    {
        Err(Err::Error(ParseError::from_error_kind(
            input,
            ErrorKind::Verify,
        )))
    } else {
        Ok((
            input,
            Duration {
                year: year.unwrap_or_default(),
                month: month.unwrap_or_default(),
                day: day.unwrap_or_default(),
                hour: hour.unwrap_or_default(),
                minute: minute.unwrap_or_default(),
                second: second.unwrap_or_default(),
            },
        ))
    }
}

fn parse_week_format(input: &str) -> IResult<&str, Duration> {
    let (input, week) = value_with_designator("W")(input)?;

    Ok((
        input,
        Duration {
            year: 0.,
            month: 0.,
            day: week * 7.,
            hour: 0.,
            minute: 0.,
            second: 0.,
        },
    ))
}

fn _parse_extended_format(_input: &str) -> IResult<&str, Duration> {
    unimplemented!()
}