date_duration 0.1.0

Library for parsing and serializing time intervals
Documentation
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct DateDuration {
    pub months: u32,
    pub days: u32,
    pub seconds: u32,
    pub nanoseconds: u32,
}

impl DateDuration {
    pub fn parse_iso8601(src: &str) -> Result<DateDuration, ParseError> {
        let mut rest = src.strip_prefix('P').ok_or(ParseError::MissingPrefix)?;

        let months;
        let days;

        {
            const DATE_SUFFIXES: [char; 4] = ['Y', 'M', 'W', 'D'];
            let mut date_values = [0u32; DATE_SUFFIXES.len()];

            let mut segment_idx = 0;
            loop {
                if rest.is_empty() || rest.starts_with('T') {
                    break;
                }

                let end_idx = rest
                    .char_indices()
                    .take_while(|(_, chr)| chr.is_ascii_digit())
                    .last()
                    .ok_or(ParseError::TrailingCharacters)?
                    .0
                    + 1;
                let segment_num = &rest[..end_idx];
                rest = &rest[end_idx..];
                loop {
                    let suffix = DATE_SUFFIXES[segment_idx];
                    if rest.starts_with(suffix) {
                        date_values[segment_idx] =
                            segment_num.parse().map_err(|_| ParseError::InvalidNumber)?;
                        rest = &rest[1..];
                        break;
                    }
                    segment_idx += 1;

                    if segment_idx >= DATE_SUFFIXES.len() {
                        return Err(ParseError::MissingDivider);
                    }
                }
            }
            months = date_values[0] * 12 + date_values[1];
            days = date_values[2] * 7 + date_values[3];
        }

        if rest.is_empty() {
            Ok(DateDuration {
                months,
                days,
                seconds: 0,
                nanoseconds: 0,
            })
        } else {
            // skip T, we know it's there
            rest = &rest[1..];

            const TIME_SUFFIXES: [char; 2] = ['H', 'M'];
            let mut time_values = [0u32; TIME_SUFFIXES.len()];

            let mut segment_idx = 0;
            loop {
                if rest.is_empty() {
                    return Ok(DateDuration {
                        months,
                        days,
                        seconds: (time_values[0] * 60 + time_values[1]) * 60,
                        nanoseconds: 0,
                    });
                }

                let end_idx = rest
                    .char_indices()
                    .take_while(|(_, chr)| chr.is_ascii_digit())
                    .last()
                    .ok_or(ParseError::TrailingCharacters)?
                    .0
                    + 1;
                let segment_num = &rest[..end_idx];
                rest = &rest[end_idx..];
                loop {
                    let suffix = TIME_SUFFIXES[segment_idx];
                    if rest.starts_with(suffix) {
                        time_values[segment_idx] =
                            segment_num.parse().map_err(|_| ParseError::InvalidNumber)?;
                        rest = &rest[1..];
                        break;
                    }
                    segment_idx += 1;

                    if segment_idx >= TIME_SUFFIXES.len() {
                        // after minute, look for seconds/nanoseconds

                        let mut seconds: u32 = (time_values[0] * 60 + time_values[1]) * 60;
                        let mut nanoseconds = 0;
                        if rest.starts_with('.') || rest.starts_with(',') {
                            rest = &rest[1..];
                            let end_idx = rest
                                .char_indices()
                                .take_while(|(_, chr)| chr.is_ascii_digit())
                                .last()
                                .ok_or(ParseError::TrailingCharacters)?
                                .0
                                + 1;

                            let nano_segment_num = &rest[..end_idx];
                            rest = &rest[end_idx..];

                            if !rest.starts_with('S') {
                                return Err(ParseError::TrailingCharacters);
                            }

                            const NANOS_DIGITS: u32 = 9;

                            if nano_segment_num.len() > (NANOS_DIGITS as usize) {
                                return Err(ParseError::InvalidNumber);
                            }

                            seconds += segment_num
                                .parse::<u32>()
                                .map_err(|_| ParseError::InvalidNumber)?;

                            let added_places = NANOS_DIGITS - (nano_segment_num.len() as u32);
                            let nano_segment_num_parsed: u32 = nano_segment_num
                                .parse()
                                .map_err(|_| ParseError::InvalidNumber)?;
                            nanoseconds = nano_segment_num_parsed * (10u32).pow(added_places);
                        } else if rest.starts_with('S') {
                            seconds += segment_num
                                .parse::<u32>()
                                .map_err(|_| ParseError::InvalidNumber)?;
                        } else {
                            return Err(ParseError::TrailingCharacters);
                        }

                        if rest.len() > 1 {
                            return Err(ParseError::TrailingCharacters);
                        }

                        return Ok(DateDuration {
                            months,
                            days,
                            seconds,
                            nanoseconds,
                        });
                    }
                }
            }
        }
    }

    pub fn to_iso8601_long(&self) -> String {
        let years = self.months / 12;
        let months = self.months % 12;
        let weeks = self.days / 7;
        let days = self.days % 7;
        let hours = self.seconds / 60 / 60;
        let minutes = (self.seconds / 60) % 60;
        let seconds = self.seconds % 60;

        format!(
            "P{}Y{}M{}W{}DT{}H{}M{}.{:09}S",
            years, months, weeks, days, hours, minutes, seconds, self.nanoseconds
        )
    }
}

#[derive(Debug, PartialEq, Clone)]
pub enum ParseError {
    MissingPrefix,
    MissingDivider,
    InvalidNumber,
    TrailingCharacters,
}

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

    #[test]
    fn test_parse() {
        assert_eq!(
            DateDuration::parse_iso8601("P2Y10M15DT10H30M20S"),
            Ok(DateDuration {
                months: 34,
                days: 15,
                seconds: 37820,
                nanoseconds: 0,
            })
        );

        assert_eq!(
            DateDuration::parse_iso8601("P1Y2M15DT12H30M0S"),
            Ok(DateDuration {
                months: 14,
                days: 15,
                seconds: 45000,
                nanoseconds: 0,
            })
        );

        assert_eq!(
            DateDuration::parse_iso8601("P6W"),
            Ok(DateDuration {
                months: 0,
                days: 42,
                seconds: 0,
                nanoseconds: 0,
            })
        );
    }

    #[test]
    fn test_parse_nanos() {
        assert_eq!(
            DateDuration::parse_iso8601("P1DT0.5S"),
            Ok(DateDuration {
                months: 0,
                days: 1,
                seconds: 0,
                nanoseconds: 500_000_000,
            })
        );

        assert_eq!(
            DateDuration::parse_iso8601("P10YT4.000000001S"),
            Ok(DateDuration {
                months: 120,
                days: 0,
                seconds: 4,
                nanoseconds: 1,
            })
        );
    }

    #[test]
    fn test_serialize() {
        assert_eq!(
            &DateDuration {
                months: 0,
                days: 1,
                seconds: 0,
                nanoseconds: 0,
            }
            .to_iso8601_long(),
            "P0Y0M0W1DT0H0M0.000000000S",
        );

        assert_eq!(
            &DateDuration {
                months: 0,
                days: 42,
                seconds: 0,
                nanoseconds: 0,
            }
            .to_iso8601_long(),
            "P0Y0M6W0DT0H0M0.000000000S",
        );

        assert_eq!(
            &DateDuration {
                months: 14,
                days: 15,
                seconds: 45000,
                nanoseconds: 0,
            }
            .to_iso8601_long(),
            "P1Y2M2W1DT12H30M0.000000000S",
        );

        assert_eq!(
            &DateDuration {
                months: 0,
                days: 1,
                seconds: 0,
                nanoseconds: 500_000_000,
            }
            .to_iso8601_long(),
            "P0Y0M0W1DT0H0M0.500000000S",
        );

        assert_eq!(
            &DateDuration {
                months: 120,
                days: 0,
                seconds: 4,
                nanoseconds: 1,
            }
            .to_iso8601_long(),
            "P10Y0M0W0DT0H0M4.000000001S",
        );
    }
}