diff --git a/src/calendar_duration.rs b/src/calendar_duration.rs index 35a9f96ef1..ba05f9f3aa 100644 --- a/src/calendar_duration.rs +++ b/src/calendar_duration.rs @@ -1,7 +1,9 @@ use core::fmt; use core::num::NonZeroU32; +use core::str; use core::time::Duration; +use crate::format::{parse_iso8601_duration, ParseError, TOO_LONG}; use crate::{expect, try_opt}; /// ISO 8601 duration type. @@ -117,6 +119,18 @@ impl fmt::Display for CalendarDuration { } } +impl str::FromStr for CalendarDuration { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + let (s, duration) = parse_iso8601_duration(s)?; + if !s.is_empty() { + return Err(TOO_LONG); + } + Ok(duration) + } +} + impl CalendarDuration { /// Create a new duration initialized to `0`. /// diff --git a/src/format/mod.rs b/src/format/mod.rs index 86521243dd..8f50fab384 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -67,6 +67,7 @@ pub use formatting::{format, format_item, DelayedFormat}; pub use locales::Locale; pub(crate) use parse::parse_rfc3339; pub use parse::{parse, parse_and_remainder}; +pub(crate) use parse_iso8601::parse_iso8601_duration; pub use parsed::Parsed; pub use strftime::StrftimeItems; diff --git a/src/format/parse_iso8601.rs b/src/format/parse_iso8601.rs index 835083b92e..3f7c050578 100644 --- a/src/format/parse_iso8601.rs +++ b/src/format/parse_iso8601.rs @@ -1,5 +1,164 @@ use super::scan; use super::{ParseResult, INVALID, OUT_OF_RANGE}; +use crate::CalendarDuration; + +/// Parser for the ISO 8601 duration format with designators. +/// +/// Supported formats: +/// - `Pnn̲Ynn̲Mnn̲DTnn̲Hnn̲Mnn̲S` +/// - `Pnn̲W` +/// +/// Any number-designator pair may be missing when zero, as long as there is at least one pair. +/// The last pair may contain a decimal fraction instead of an integer. +/// +/// - Fractional years will be expressed in months. +/// - Fractional weeks will be expressed in days. +/// - Fractional hours, minutes or seconds will be expressed in minutes, seconds and nanoseconds. +pub(crate) fn parse_iso8601_duration(mut s: &str) -> ParseResult<(&str, CalendarDuration)> { + macro_rules! consume { + ($e:expr) => {{ + $e.map(|(s_, v)| { + s = s_; + v + }) + }}; + } + + s = scan::char(s, b'P')?; + let mut duration = CalendarDuration::new(); + + let mut next = consume!(Decimal::parse(s)).ok(); + if let Some(val) = next { + if s.as_bytes().first() == Some(&b'W') { + s = &s[1..]; + // Nothing is allowed after a week value + return Ok((s, duration.with_days(val.mul(7)?))); + } + if s.as_bytes().first() == Some(&b'Y') { + s = &s[1..]; + duration = duration.with_months(val.mul(12)?); + if val.fraction.is_some() { + return Ok((s, duration)); + } + next = consume!(Decimal::parse(s)).ok(); + } + } + + if let Some(val) = next { + if s.as_bytes().first() == Some(&b'M') { + s = &s[1..]; + let months = duration.months().checked_add(val.integer()?).ok_or(OUT_OF_RANGE)?; + duration = duration.with_months(months); + next = consume!(Decimal::parse(s)).ok(); + } + } + + if let Some(val) = next { + if s.as_bytes().first() == Some(&b'D') { + s = &s[1..]; + duration = duration.with_days(val.integer()?); + next = None; + } + } + + if next.is_some() { + // We have numbers without a matching designator. + return Err(INVALID); + } + + if s.as_bytes().first() == Some(&b'T') { + duration = consume!(parse_iso8601_duration_time(s, duration))? + } + Ok((s, duration)) +} + +/// Parser for the time part of the ISO 8601 duration format with designators. +pub(crate) fn parse_iso8601_duration_time( + mut s: &str, + duration: CalendarDuration, +) -> ParseResult<(&str, CalendarDuration)> { + macro_rules! consume_or_return { + ($e:expr, $return:expr) => {{ + match $e { + Ok((s_, next)) => { + s = s_; + next + } + Err(_) => return $return, + } + }}; + } + fn set_hms_nano( + duration: CalendarDuration, + hours: u32, + minutes: u32, + seconds: u32, + nanoseconds: u32, + ) -> ParseResult { + let duration = match (hours, minutes) { + (0, 0) => duration.with_seconds(seconds), + _ => duration.with_hms(hours, minutes, seconds).ok_or(OUT_OF_RANGE)?, + }; + Ok(duration.with_nanos(nanoseconds).unwrap()) + } + + s = scan::char(s, b'T')?; + let mut hours = 0; + let mut minutes = 0; + let mut incomplete = true; // at least one component is required + + let (s_, mut next) = Decimal::parse(s)?; + s = s_; + if s.as_bytes().first() == Some(&b'H') { + s = &s[1..]; + incomplete = false; + match next.integer() { + Ok(h) => hours = h, + _ => { + let (secs, nanos) = next.mul_with_nanos(3600)?; + let mins = secs / 60; + let secs = (secs % 60) as u32; + let minutes = u32::try_from(mins).map_err(|_| OUT_OF_RANGE)?; + return Ok((s, set_hms_nano(duration, 0, minutes, secs, nanos)?)); + } + } + next = consume_or_return!( + Decimal::parse(s), + Ok((s, set_hms_nano(duration, hours, minutes, 0, 0)?)) + ); + } + + if s.as_bytes().first() == Some(&b'M') { + s = &s[1..]; + incomplete = false; + match next.integer() { + Ok(m) => minutes = m, + _ => { + let (secs, nanos) = next.mul_with_nanos(60)?; + let mins = secs / 60; + let secs = (secs % 60) as u32; + minutes = u32::try_from(mins).map_err(|_| OUT_OF_RANGE)?; + return Ok((s, set_hms_nano(duration, hours, minutes, secs, nanos)?)); + } + } + next = consume_or_return!( + Decimal::parse(s), + Ok((s, set_hms_nano(duration, hours, minutes, 0, 0)?)) + ); + } + + if s.as_bytes().first() == Some(&b'S') { + s = &s[1..]; + let (secs, nanos) = next.mul_with_nanos(1)?; + let secs = u32::try_from(secs).map_err(|_| OUT_OF_RANGE)?; + return Ok((s, set_hms_nano(duration, hours, minutes, secs, nanos)?)); + } + + if incomplete { + return Err(INVALID); + } + Ok((s, set_hms_nano(duration, hours, minutes, 0, 0)?)) +} /// Helper type for parsing decimals (as in an ISO 8601 duration). #[derive(Copy, Clone)] @@ -96,7 +255,7 @@ impl Fraction { let huge = self.0 * unit + div / 2; let whole = huge / POW10[15]; let fraction_as_nanos = (huge % POW10[15]) / div; - dbg!(whole as i64, fraction_as_nanos as i64) + (whole as i64, fraction_as_nanos as i64) } } @@ -121,8 +280,9 @@ const POW10: [u64; 16] = [ #[cfg(test)] mod tests { - use super::Fraction; - use crate::format::INVALID; + use super::{parse_iso8601_duration, parse_iso8601_duration_time, Fraction}; + use crate::format::{INVALID, OUT_OF_RANGE, TOO_SHORT}; + use crate::CalendarDuration; #[test] fn test_parse_fraction() { @@ -138,4 +298,128 @@ mod tests { let (_, fraction) = Fraction::parse(",5").unwrap(); assert_eq!(fraction.mul_with_nanos(1), (0, 500_000_000)); } + + #[test] + fn test_parse_duration_time() { + let parse = parse_iso8601_duration_time; + let d = CalendarDuration::new(); + + assert_eq!(parse("T12H", d), Ok(("", d.with_hms(12, 0, 0).unwrap()))); + assert_eq!(parse("T12.25H", d), Ok(("", d.with_hms(12, 15, 0).unwrap()))); + assert_eq!(parse("T12,25H", d), Ok(("", d.with_hms(12, 15, 0).unwrap()))); + assert_eq!(parse("T34M", d), Ok(("", d.with_hms(0, 34, 0).unwrap()))); + assert_eq!(parse("T34.25M", d), Ok(("", d.with_hms(0, 34, 15).unwrap()))); + assert_eq!(parse("T56S", d), Ok(("", d.with_seconds(56)))); + assert_eq!(parse("T0.789S", d), Ok(("", d.with_millis(789).unwrap()))); + assert_eq!(parse("T0,789S", d), Ok(("", d.with_millis(789).unwrap()))); + assert_eq!(parse("T12H34M", d), Ok(("", d.with_hms(12, 34, 0).unwrap()))); + assert_eq!(parse("T12H34M60S", d), Ok(("", d.with_hms(12, 34, 60).unwrap()))); + assert_eq!( + parse("T12H34M56.789S", d), + Ok(("", d.with_hms(12, 34, 56).unwrap().with_millis(789).unwrap())) + ); + assert_eq!(parse("T12H56S", d), Ok(("", d.with_hms(12, 0, 56).unwrap()))); + assert_eq!(parse("T34M56S", d), Ok(("", d.with_hms(0, 34, 56).unwrap()))); + + // Data after a fraction is ignored + assert_eq!(parse("T12.5H16M", d), Ok(("16M", d.with_hms(12, 30, 0).unwrap()))); + assert_eq!(parse("T12H16.5M30S", d), Ok(("30S", d.with_hms(12, 16, 30).unwrap()))); + + // Zero values + assert_eq!(parse("T0H", d), Ok(("", d))); + assert_eq!(parse("T0M", d), Ok(("", d))); + assert_eq!(parse("T0S", d), Ok(("", d))); + assert_eq!(parse("T0,0S", d), Ok(("", d))); + + // Empty or invalid values + assert_eq!(parse("T", d), Err(TOO_SHORT)); + assert_eq!(parse("TH", d), Err(INVALID)); + assert_eq!(parse("TM", d), Err(INVALID)); + assert_eq!(parse("TS", d), Err(INVALID)); + assert_eq!(parse("T.5S", d), Err(INVALID)); + assert_eq!(parse("T,5S", d), Err(INVALID)); + + // Date components + assert_eq!(parse("T5W", d), Err(INVALID)); + assert_eq!(parse("T5Y", d), Err(INVALID)); + assert_eq!(parse("T5D", d), Err(INVALID)); + + // Max values + assert_eq!(parse("T1118481H", d), Ok(("", d.with_hms(1118481, 0, 0).unwrap()))); + assert_eq!(parse("T1118482H", d), Err(OUT_OF_RANGE)); + assert_eq!(parse("T1118481.05H", d), Ok(("", d.with_hms(1118481, 3, 0).unwrap()))); + assert_eq!(parse("T1118481.5H", d), Err(OUT_OF_RANGE)); + assert_eq!(parse("T67108863M", d), Ok(("", d.with_hms(0, u32::MAX >> 6, 0).unwrap()))); + assert_eq!(parse("T67108864M", d), Err(OUT_OF_RANGE)); + assert_eq!(parse("T67108863.25M", d), Ok(("", d.with_hms(0, u32::MAX >> 6, 15).unwrap()))); + assert_eq!(parse("T4294967295S", d), Ok(("", d.with_seconds(u32::MAX)))); + assert_eq!(parse("T4294967296S", d), Err(OUT_OF_RANGE)); + assert_eq!( + parse("T4294967295.25S", d), + Ok(("", d.with_seconds(u32::MAX).with_millis(250).unwrap())) + ); + assert_eq!( + parse("T4294967295.999999999S", d), + Ok(("", d.with_seconds(u32::MAX).with_nanos(999_999_999).unwrap())) + ); + assert_eq!(parse("T4294967295.9999999995S", d), Err(OUT_OF_RANGE)); + assert_eq!(parse("T12H34M61S", d), Err(OUT_OF_RANGE)); + } + + #[test] + fn test_parse_duration() { + let d = CalendarDuration::new(); + assert_eq!( + parse_iso8601_duration("P12Y"), + Ok(("", d.with_years_and_months(12, 0).unwrap())) + ); + assert_eq!(parse_iso8601_duration("P34M"), Ok(("", d.with_months(34)))); + assert_eq!(parse_iso8601_duration("P56D"), Ok(("", d.with_days(56)))); + assert_eq!(parse_iso8601_duration("P78W"), Ok(("", d.with_weeks_and_days(78, 0).unwrap()))); + + // Fractional date values + assert_eq!( + parse_iso8601_duration("P1.25Y"), + Ok(("", d.with_years_and_months(1, 3).unwrap())) + ); + assert_eq!( + parse_iso8601_duration("P1.99Y"), + Ok(("", d.with_years_and_months(2, 0).unwrap())) + ); + assert_eq!(parse_iso8601_duration("P1.4W"), Ok(("", d.with_days(10)))); + assert_eq!(parse_iso8601_duration("P1.95W"), Ok(("", d.with_days(14)))); + assert_eq!(parse_iso8601_duration("P1.5M"), Err(INVALID)); + assert_eq!(parse_iso8601_duration("P1.5D"), Err(INVALID)); + + // Data after a fraction is ignored + assert_eq!( + parse_iso8601_duration("P1.25Y5D"), + Ok(("5D", d.with_years_and_months(1, 3).unwrap())) + ); + assert_eq!( + parse_iso8601_duration("P1.25YT3H"), + Ok(("T3H", d.with_years_and_months(1, 3).unwrap())) + ); + + // Zero values + assert_eq!(parse_iso8601_duration("P0Y"), Ok(("", d))); + assert_eq!(parse_iso8601_duration("P0M"), Ok(("", d))); + assert_eq!(parse_iso8601_duration("P0W"), Ok(("", d))); + assert_eq!(parse_iso8601_duration("P0D"), Ok(("", d))); + assert_eq!(parse_iso8601_duration("PT0M"), Ok(("", d))); + assert_eq!(parse_iso8601_duration("PT0S"), Ok(("", d))); + + // Invalid designator at a position where another designator can be expected. + assert_eq!(parse_iso8601_duration("P12Y12Y"), Err(INVALID)); + assert_eq!(parse_iso8601_duration("P12M12M"), Err(INVALID)); + assert_eq!(parse_iso8601_duration("P12M12Y"), Err(INVALID)); + + // Trailing data + assert_eq!( + parse_iso8601_duration("P12W34D"), + Ok(("34D", d.with_weeks_and_days(12, 0).unwrap())) + ); + assert_eq!(parse_iso8601_duration("P12D12D"), Ok(("12D", d.with_days(12)))); + assert_eq!(parse_iso8601_duration("P12D12Y"), Ok(("12Y", d.with_days(12)))); + } }