time-parse 0.2.2

Parse iso8601 periods
Documentation
use std::iter::Peekable;
use std::str::FromStr;
use std::time::Duration;

use anyhow::Result;
use anyhow::anyhow;
use anyhow::bail;
use anyhow::ensure;

struct Parts<'s> {
    inner: &'s str,
}

impl<'s> Parts<'s> {
    fn new(inner: &str) -> Parts<'_> {
        Parts { inner }
    }
}

impl<'s> Iterator for Parts<'s> {
    type Item = (&'s str, char);

    fn next(&mut self) -> Option<(&'s str, char)> {
        self.inner
            .find(|c: char| c.is_ascii_alphabetic())
            .map(|next| {
                let (init, point) = self.inner.split_at(next);
                self.inner = &point[1..];
                (init, point.as_bytes()[0].to_ascii_uppercase() as char)
            })
    }
}

fn maybe_take(parts: &mut Peekable<Parts>, token: char, mul: u64) -> Result<u64> {
    Ok(match parts.peek().cloned() {
        Some((body, found_token)) if found_token == token => {
            parts.next().unwrap();
            u64::from_str(body)? * mul
        }
        _ => 0,
    })
}

fn take_empty(parts: &mut Peekable<Parts>, token: char) -> Result<()> {
    match parts.next() {
        Some(("", avail)) if avail == token => Ok(()),
        Some((head, avail)) if avail == token => {
            bail!("invalid data before '{}': {:?}", token, head)
        }
        other => bail!("expected '{}', not {:?}", token, other),
    }
}

pub fn parse(input: &str) -> Result<Duration> {
    let mut parts = Parts::new(input).peekable();

    let mut seconds = 0u64;
    let mut nanos = 0u32;

    take_empty(&mut parts, 'P')?;

    seconds += maybe_take(&mut parts, 'W', super::SECS_PER_WEEK)?;
    seconds += maybe_take(&mut parts, 'D', super::SECS_PER_DAY)?;

    take_empty(&mut parts, 'T')?;

    seconds += maybe_take(&mut parts, 'H', super::SECS_PER_HOUR)?;
    seconds += maybe_take(&mut parts, 'M', super::SECS_PER_MINUTE)?;

    if let Some(&(mut body, 'S')) = parts.peek() {
        parts.next().unwrap();

        if let Some(first_point) = body.find('.') {
            let (main, after) = body.split_at(first_point);
            body = main;
            nanos = super::to_nanos(&after[1..]).ok_or(anyhow!("invalid nanos"))?;
        }

        seconds += u64::from_str(body)?;
    }

    ensure!(
        parts.peek().is_none(),
        "unexpected trailing data: {:?}",
        parts.next().unwrap(),
    );

    Ok(Duration::new(seconds, nanos))
}

#[cfg(test)]
mod tests {
    use std::time::Duration;

    #[test]
    fn duration() {
        use super::parse;
        assert_eq!(Duration::new(7, 0), parse("PT7S").unwrap());
        assert_eq!(Duration::new(7, 5_000_000), parse("PT7.005S").unwrap());
        assert_eq!(Duration::new(2 * 60, 0), parse("PT2M").unwrap());
        assert_eq!(
            Duration::new((2 * 24 + 1) * 60 * 60, 0),
            parse("P2DT1H").unwrap()
        );
    }

    #[test]
    fn parts() {
        let mut p = super::Parts::new("1D23M");
        assert_eq!(Some(("1", 'D')), p.next());
        assert_eq!(Some(("23", 'M')), p.next());
    }
}