diff --git a/CHANGELOG.md b/CHANGELOG.md index 199fe224..3b93a0e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ## [Unreleased](https://github.com/badboy/iso8601/compare/v0.5.0...main) - ReleaseDate +* Fix accepted duration representations + ## [0.5.0](https://github.com/badboy/iso8601/compare/v0.4.2...v0.5.0) - 2022-07-29 * Replace rounding-error prone floating point code with robust integer code ([#36](https://github.com/badboy/iso8601/pull/36) by @plugwash) diff --git a/src/assert.rs b/src/assert.rs index 50e99a92..e6351408 100644 --- a/src/assert.rs +++ b/src/assert.rs @@ -13,8 +13,9 @@ macro_rules! assert_parser { use std::string::ToString; let (rest, parsed) = $parser($line.as_bytes()).unwrap(); - crate::assert::print_result($line, &rest, &parsed); - + if std::env::var("VERBOSE_TEST_OUTPUT").is_ok() { + $crate::assert::print_result($line, &rest, &parsed); + } assert_eq!( parsed, $expectation, "{:?} not parsed as expected (leftover: {:?})", diff --git a/src/lib.rs b/src/lib.rs index 1fc59234..c4647c4c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,6 +80,19 @@ pub struct DateTime { } /// A time duration. + +/// Durations: +/// https://www.rfc-editor.org/rfc/rfc3339#page-13 +/// dur-second = 1*DIGIT "S" +/// dur-minute = 1*DIGIT "M" [dur-second] +/// dur-hour = 1*DIGIT "H" [dur-minute] +/// dur-time = "T" (dur-hour / dur-minute / dur-second) +/// dur-day = 1*DIGIT "D" +/// dur-week = 1*DIGIT "W" +/// dur-month = 1*DIGIT "M" [dur-day] +/// dur-year = 1*DIGIT "Y" [dur-month] +/// dur-date = (dur-day / dur-month / dur-year) [dur-time] +/// duration = "P" (dur-date / dur-time / dur-week) #[derive(Eq, PartialEq, Debug, Copy, Clone)] pub enum Duration { /// A duration specified by year, month, day, hour, minute and second units diff --git a/src/parsers.rs b/src/parsers.rs index 205f9430..187c16cd 100644 --- a/src/parsers.rs +++ b/src/parsers.rs @@ -54,17 +54,6 @@ fn take_n_digits(i: &[u8], n: usize) -> IResult<&[u8], u32> { Ok((i, res)) } -fn take_m_to_n_digits(i: &[u8], m: usize, n: usize) -> IResult<&[u8], u32> { - let (i, digits) = take_while_m_n(m, n, is_digit)(i)?; - - let s = str::from_utf8(digits).expect("Invalid data, expected UTF-8 string"); - let res = s - .parse() - .expect("Invalid string, expected ASCII representation of a number"); - - Ok((i, res)) -} - fn n_digit_in_range( i: &[u8], n: usize, @@ -79,21 +68,6 @@ fn n_digit_in_range( } } -fn m_to_n_digit_in_range( - i: &[u8], - m: usize, - n: usize, - range: impl core::ops::RangeBounds, -) -> IResult<&[u8], u32> { - let (new_i, number) = take_m_to_n_digits(i, m, n)?; - - if range.contains(&number) { - Ok((new_i, number)) - } else { - Err(Err::Error(Error::new(i, nom::error::ErrorKind::Eof))) - } -} - fn sign(i: &[u8]) -> IResult<&[u8], i32> { map(alt((tag(b"-"), tag(b"+"))), |s: &[u8]| match s { b"-" => -1, @@ -273,50 +247,61 @@ pub fn parse_datetime(i: &[u8]) -> IResult<&[u8], DateTime> { // DURATION -// Y[YYY...] +/// dur-year = 1*DIGIT "Y" [dur-month] fn duration_year(i: &[u8]) -> IResult<&[u8], u32> { - take_digits(i) + terminated(take_digits, tag(b"Y"))(i) } -// M[M] +/// dur-month = 1*DIGIT "M" [dur-day] fn duration_month(i: &[u8]) -> IResult<&[u8], u32> { - m_to_n_digit_in_range(i, 1, 2, 0..=12) + terminated(take_digits, tag(b"M"))(i) } -// W[W] +/// dur-week = 1*DIGIT "W" fn duration_week(i: &[u8]) -> IResult<&[u8], u32> { - m_to_n_digit_in_range(i, 1, 2, 0..=52) + terminated(take_digits, tag(b"W"))(i) } -// D[D] +// dur-day = 1*DIGIT "D" fn duration_day(i: &[u8]) -> IResult<&[u8], u32> { - m_to_n_digit_in_range(i, 1, 2, 0..=31) + terminated(take_digits, tag(b"D"))(i) } -// H[H] +/// dur-hour = 1*DIGIT "H" [dur-minute] +/// dur-time = "T" (dur-hour / dur-minute / dur-second) fn duration_hour(i: &[u8]) -> IResult<&[u8], u32> { - m_to_n_digit_in_range(i, 1, 2, 0..=24) + terminated(take_digits, tag(b"H"))(i) } -// M[M] +/// dur-minute = 1*DIGIT "M" [dur-second] fn duration_minute(i: &[u8]) -> IResult<&[u8], u32> { - m_to_n_digit_in_range(i, 1, 2, 0..=60) + terminated(take_digits, tag(b"M"))(i) } -// S[S][[,.][MS]] -fn duration_second_and_millisecond(i: &[u8]) -> IResult<&[u8], (u32, u32)> { - let (i, s) = m_to_n_digit_in_range(i, 1, 2, 0..=60)?; - let (i, ms) = opt(preceded(one_of(",."), fraction_millisecond))(i)?; +//// dur-second = 1*DIGIT "S" +fn duration_second(i: &[u8]) -> IResult<&[u8], u32> { + terminated(take_digits, tag(b"S"))(i) +} - Ok((i, (s, ms.unwrap_or(0)))) +/// dur-second-ext = 1*DIGIT (,|.) 1*DIGIT "S" +fn duration_second_and_millisecond(i: &[u8]) -> IResult<&[u8], (u32, u32)> { + alt(( + // no milliseconds + map(duration_second, |m| (m, 0)), + terminated( + // with milliseconds + separated_pair(take_digits, one_of(",."), fraction_millisecond), + tag(b"S"), + ), + ))(i) } fn duration_time(i: &[u8]) -> IResult<&[u8], (u32, u32, u32, u32)> { map( tuple(( - opt(terminated(duration_hour, tag(b"H"))), - opt(terminated(duration_minute, tag(b"M"))), - opt(terminated(duration_second_and_millisecond, tag(b"S"))), + opt(duration_hour), + opt(duration_minute), + opt(duration_second_and_millisecond), )), |(h, m, s)| { let (s, ms) = s.unwrap_or((0, 0)); @@ -331,9 +316,9 @@ fn duration_ymdhms(i: &[u8]) -> IResult<&[u8], Duration> { preceded( tag(b"P"), tuple(( - opt(terminated(duration_year, tag(b"Y"))), - opt(terminated(duration_month, tag(b"M"))), - opt(terminated(duration_day, tag(b"D"))), + opt(duration_year), + opt(duration_month), + opt(duration_day), opt(preceded(tag(b"T"), duration_time)), )), ), @@ -359,10 +344,7 @@ fn duration_ymdhms(i: &[u8]) -> IResult<&[u8], Duration> { } fn duration_weeks(i: &[u8]) -> IResult<&[u8], Duration> { - map( - preceded(tag(b"P"), terminated(duration_week, tag(b"W"))), - Duration::Weeks, - )(i) + map(preceded(tag(b"P"), duration_week), Duration::Weeks)(i) } // YYYY, no sign diff --git a/src/parsers/tests.rs b/src/parsers/tests.rs index 8c5ed881..65c3d52a 100644 --- a/src/parsers/tests.rs +++ b/src/parsers/tests.rs @@ -162,18 +162,18 @@ fn disallows_notallowed() { #[test] fn test_duration_year() { - assert_eq!(Ok((&[][..], 2019)), duration_year(b"2019")); - assert_eq!(Ok((&[][..], 0)), duration_year(b"0")); - assert_eq!(Ok((&[][..], 10000)), duration_year(b"10000")); + assert_eq!(Ok((&[][..], 2019)), duration_year(b"2019Y")); + assert_eq!(Ok((&[][..], 0)), duration_year(b"0Y")); + assert_eq!(Ok((&[][..], 10000)), duration_year(b"10000Y")); assert!(duration_year(b"abcd").is_err()); assert!(duration_year(b"-1").is_err()); } #[test] fn test_duration_month() { - assert_eq!(Ok((&[][..], 6)), duration_month(b"6")); - assert_eq!(Ok((&[][..], 0)), duration_month(b"0")); - assert_eq!(Ok((&[][..], 12)), duration_month(b"12")); + assert_eq!(Ok((&[][..], 6)), duration_month(b"6M")); + assert_eq!(Ok((&[][..], 0)), duration_month(b"0M")); + assert_eq!(Ok((&[][..], 12)), duration_month(b"12M")); assert!(duration_month(b"ab").is_err()); assert!(duration_month(b"-1").is_err()); assert!(duration_month(b"13").is_err()); @@ -181,9 +181,9 @@ fn test_duration_month() { #[test] fn test_duration_week() { - assert_eq!(Ok((&[][..], 26)), duration_week(b"26")); - assert_eq!(Ok((&[][..], 0)), duration_week(b"0")); - assert_eq!(Ok((&[][..], 52)), duration_week(b"52")); + assert_eq!(Ok((&[][..], 26)), duration_week(b"26W")); + assert_eq!(Ok((&[][..], 0)), duration_week(b"0W")); + assert_eq!(Ok((&[][..], 52)), duration_week(b"52W")); assert!(duration_week(b"ab").is_err()); assert!(duration_week(b"-1").is_err()); assert!(duration_week(b"53").is_err()); @@ -191,9 +191,9 @@ fn test_duration_week() { #[test] fn test_duration_day() { - assert_eq!(Ok((&[][..], 16)), duration_day(b"16")); - assert_eq!(Ok((&[][..], 0)), duration_day(b"0")); - assert_eq!(Ok((&[][..], 31)), duration_day(b"31")); + assert_eq!(Ok((&[][..], 16)), duration_day(b"16D")); + assert_eq!(Ok((&[][..], 0)), duration_day(b"0D")); + assert_eq!(Ok((&[][..], 31)), duration_day(b"31D")); assert!(duration_day(b"ab").is_err()); assert!(duration_day(b"-1").is_err()); assert!(duration_day(b"32").is_err()); @@ -201,9 +201,9 @@ fn test_duration_day() { #[test] fn test_duration_hour() { - assert_eq!(Ok((&[][..], 12)), duration_hour(b"12")); - assert_eq!(Ok((&[][..], 0)), duration_hour(b"0")); - assert_eq!(Ok((&[][..], 24)), duration_hour(b"24")); + assert_eq!(Ok((&[][..], 12)), duration_hour(b"12H")); + assert_eq!(Ok((&[][..], 0)), duration_hour(b"0H")); + assert_eq!(Ok((&[][..], 24)), duration_hour(b"24H")); assert!(duration_hour(b"ab").is_err()); assert!(duration_hour(b"-1").is_err()); assert!(duration_hour(b"25").is_err()); @@ -211,41 +211,44 @@ fn test_duration_hour() { #[test] fn test_duration_minute() { - assert_eq!(Ok((&[][..], 30)), duration_minute(b"30")); - assert_eq!(Ok((&[][..], 0)), duration_minute(b"0")); - assert_eq!(Ok((&[][..], 60)), duration_minute(b"60")); + assert_eq!(Ok((&[][..], 30)), duration_minute(b"30M")); + assert_eq!(Ok((&[][..], 0)), duration_minute(b"0M")); + assert_eq!(Ok((&[][..], 60)), duration_minute(b"60M")); assert!(duration_minute(b"ab").is_err()); assert!(duration_minute(b"-1").is_err()); assert!(duration_minute(b"61").is_err()); } #[test] -fn test_duration_second_and_millisecond() { +fn test_duration_second_and_millisecond1() { assert_eq!( Ok((&[][..], (30, 0))), - duration_second_and_millisecond(b"30") + duration_second_and_millisecond(b"30S") + ); + assert_eq!( + Ok((&[][..], (0, 0))), + duration_second_and_millisecond(b"0S") ); - assert_eq!(Ok((&[][..], (0, 0))), duration_second_and_millisecond(b"0")); assert_eq!( Ok((&[][..], (60, 0))), - duration_second_and_millisecond(b"60") + duration_second_and_millisecond(b"60S") ); assert_eq!( Ok((&[][..], (1, 230))), - duration_second_and_millisecond(b"1,23") + duration_second_and_millisecond(b"1,23S") ); assert_eq!( - Ok((&[][..], (1, 230))), - duration_second_and_millisecond(b"1.23") + Ok((&[][..], (2, 340))), + duration_second_and_millisecond(b"2.34S") ); - assert!(duration_second_and_millisecond(b"ab").is_err()); - assert!(duration_second_and_millisecond(b"-1").is_err()); - assert!(duration_second_and_millisecond(b"61").is_err()); + assert!(duration_second_and_millisecond(b"abS").is_err()); + assert!(duration_second_and_millisecond(b"-1S").is_err()); } #[test] fn test_duration_time() { assert_eq!(Ok((&[][..], (1, 2, 3, 0))), duration_time(b"1H2M3S")); + assert_eq!(Ok((&[][..], (10, 12, 30, 0))), duration_time(b"10H12M30S")); assert_eq!(Ok((&[][..], (1, 0, 3, 0))), duration_time(b"1H3S")); assert_eq!(Ok((&[][..], (0, 2, 0, 0))), duration_time(b"2M")); assert_eq!(Ok((&[][..], (1, 2, 3, 400))), duration_time(b"1H2M3,4S")); @@ -277,6 +280,27 @@ fn test_duration_datetime_error() { assert!(duration_datetime(b"0001-02-03T04:05:06").is_err()); // missing P at start } +#[rustfmt::skip] +#[test] +fn test_duration_second_and_millisecond2() { + assert_parser!( + parse_duration, "PT30S", + Duration::YMDHMS { year: 0, month: 0, day: 0, hour: 0, minute: 0, second: 30, millisecond: 0 } + + ); + + assert_parser!( + parse_duration, "PT30.123S", + Duration::YMDHMS { year: 0, month: 0, day: 0, hour: 0, minute: 0, second: 30, millisecond: 123 } + + ); + + assert_parser!( + parse_duration, "P2021Y11M16DT23H26M59.123S", + Duration::YMDHMS { year: 2021, month: 11, day: 16, hour: 23, minute: 26, second: 59, millisecond: 123 } + ); +} + #[rustfmt::skip] #[test] fn duration_roundtrip() { @@ -330,6 +354,59 @@ fn duration_roundtrip() { ); } +#[rustfmt::skip] +#[test] +fn duration_multi_digit_hour() { + assert_parser!( + parse_duration, "PT12H", + Duration::YMDHMS { year: 0, month: 0, day: 0, hour: 12, minute: 0, second: 0, millisecond: 0 } + ); + assert_parser!( + parse_duration, "PT8760H", + Duration::YMDHMS { year: 0, month: 0, day: 0, hour: 365*24, minute: 0, second: 0, millisecond: 0 } + ); +} + +#[rustfmt::skip] +#[test] +fn duration_multi_digit_minute() { + assert_parser!( + parse_duration, "PT15M", + Duration::YMDHMS { year: 0, month: 0, day: 0, hour: 0, minute: 15, second: 0, millisecond: 0 } + ); + assert_parser!( + parse_duration, "PT600M", + Duration::YMDHMS { year: 0, month: 0, day: 0, hour: 0, minute: 600, second: 0, millisecond: 0 } + ); +} + +#[rustfmt::skip] +#[test] +fn duration_multi_digit_second() { + assert_parser!( + parse_duration, "PT16S", + Duration::YMDHMS { year: 0, month: 0, day: 0, hour: 0, minute: 0, second: 16, millisecond: 0 } + ); + + assert_parser!( + parse_duration, "PT900S", + Duration::YMDHMS { year: 0, month: 0, day: 0, hour: 0, minute: 0, second: 900, millisecond: 0 } + ); +} + +#[rustfmt::skip] +#[test] +fn duration_multi_digit_day() { + assert_parser!( + parse_duration, "P365D", + Duration::YMDHMS { year: 0, month: 0, day: 365, hour: 0, minute: 0, second: 0, millisecond: 0 } + ); + assert_parser!( + parse_duration, "P36500D", + Duration::YMDHMS { year: 0, month: 0, day: 36500, hour: 0, minute: 0, second: 0, millisecond: 0 } + ); +} + // #[test] // fn corner_cases() { // // how to deal with left overs?