Skip to content

Commit 7e8e29f

Browse files
committed
Add ISO 8601 parser for duration format with designators
1 parent 373913f commit 7e8e29f

File tree

3 files changed

+302
-3
lines changed

3 files changed

+302
-3
lines changed

src/calendar_duration.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use core::fmt;
22
use core::num::NonZeroU32;
3+
use core::str;
34
use core::time::Duration;
45

6+
use crate::format::{parse_iso8601_duration, ParseError, TOO_LONG};
57
use crate::{expect, try_opt};
68

79
/// ISO 8601 duration type.
@@ -117,6 +119,18 @@ impl fmt::Display for CalendarDuration {
117119
}
118120
}
119121

122+
impl str::FromStr for CalendarDuration {
123+
type Err = ParseError;
124+
125+
fn from_str(s: &str) -> Result<CalendarDuration, ParseError> {
126+
let (s, duration) = parse_iso8601_duration(s)?;
127+
if !s.is_empty() {
128+
return Err(TOO_LONG);
129+
}
130+
Ok(duration)
131+
}
132+
}
133+
120134
impl CalendarDuration {
121135
/// Create a new duration initialized to `0`.
122136
///

src/format/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ pub use locales::Locale;
7272
pub(crate) use locales::Locale;
7373
pub(crate) use parse::parse_rfc3339;
7474
pub use parse::{parse, parse_and_remainder};
75+
pub(crate) use parse_iso8601::parse_iso8601_duration;
7576
pub use parsed::Parsed;
7677
pub use strftime::StrftimeItems;
7778

src/format/parse_iso8601.rs

Lines changed: 287 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,164 @@
11
use super::scan;
22
use super::{ParseResult, INVALID, OUT_OF_RANGE};
3+
use crate::CalendarDuration;
4+
5+
/// Parser for the ISO 8601 duration format with designators.
6+
///
7+
/// Supported formats:
8+
/// - `Pnn̲Ynn̲Mnn̲DTnn̲Hnn̲Mnn̲S`
9+
/// - `Pnn̲W`
10+
///
11+
/// Any number-designator pair may be missing when zero, as long as there is at least one pair.
12+
/// The last pair may contain a decimal fraction instead of an integer.
13+
///
14+
/// - Fractional years will be expressed in months.
15+
/// - Fractional weeks will be expressed in days.
16+
/// - Fractional hours, minutes or seconds will be expressed in minutes, seconds and nanoseconds.
17+
pub(crate) fn parse_iso8601_duration(mut s: &str) -> ParseResult<(&str, CalendarDuration)> {
18+
macro_rules! consume {
19+
($e:expr) => {{
20+
$e.map(|(s_, v)| {
21+
s = s_;
22+
v
23+
})
24+
}};
25+
}
26+
27+
s = scan::char(s, b'P')?;
28+
let mut duration = CalendarDuration::new();
29+
30+
let mut next = consume!(Decimal::parse(s)).ok();
31+
if let Some(val) = next {
32+
if s.as_bytes().first() == Some(&b'W') {
33+
s = &s[1..];
34+
// Nothing is allowed after a week value
35+
return Ok((s, duration.with_days(val.mul(7)?)));
36+
}
37+
if s.as_bytes().first() == Some(&b'Y') {
38+
s = &s[1..];
39+
duration = duration.with_months(val.mul(12)?);
40+
if val.fraction.is_some() {
41+
return Ok((s, duration));
42+
}
43+
next = consume!(Decimal::parse(s)).ok();
44+
}
45+
}
46+
47+
if let Some(val) = next {
48+
if s.as_bytes().first() == Some(&b'M') {
49+
s = &s[1..];
50+
let months = duration.months().checked_add(val.integer()?).ok_or(OUT_OF_RANGE)?;
51+
duration = duration.with_months(months);
52+
next = consume!(Decimal::parse(s)).ok();
53+
}
54+
}
55+
56+
if let Some(val) = next {
57+
if s.as_bytes().first() == Some(&b'D') {
58+
s = &s[1..];
59+
duration = duration.with_days(val.integer()?);
60+
next = None;
61+
}
62+
}
63+
64+
if next.is_some() {
65+
// We have numbers without a matching designator.
66+
return Err(INVALID);
67+
}
68+
69+
if s.as_bytes().first() == Some(&b'T') {
70+
duration = consume!(parse_iso8601_duration_time(s, duration))?
71+
}
72+
Ok((s, duration))
73+
}
74+
75+
/// Parser for the time part of the ISO 8601 duration format with designators.
76+
pub(crate) fn parse_iso8601_duration_time(
77+
mut s: &str,
78+
duration: CalendarDuration,
79+
) -> ParseResult<(&str, CalendarDuration)> {
80+
macro_rules! consume_or_return {
81+
($e:expr, $return:expr) => {{
82+
match $e {
83+
Ok((s_, next)) => {
84+
s = s_;
85+
next
86+
}
87+
Err(_) => return $return,
88+
}
89+
}};
90+
}
91+
fn set_hms_nano(
92+
duration: CalendarDuration,
93+
hours: u32,
94+
minutes: u32,
95+
seconds: u32,
96+
nanoseconds: u32,
97+
) -> ParseResult<CalendarDuration> {
98+
let duration = match (hours, minutes) {
99+
(0, 0) => duration.with_seconds(seconds),
100+
_ => duration.with_hms(hours, minutes, seconds).ok_or(OUT_OF_RANGE)?,
101+
};
102+
Ok(duration.with_nanos(nanoseconds).unwrap())
103+
}
104+
105+
s = scan::char(s, b'T')?;
106+
let mut hours = 0;
107+
let mut minutes = 0;
108+
let mut incomplete = true; // at least one component is required
109+
110+
let (s_, mut next) = Decimal::parse(s)?;
111+
s = s_;
112+
if s.as_bytes().first() == Some(&b'H') {
113+
s = &s[1..];
114+
incomplete = false;
115+
match next.integer() {
116+
Ok(h) => hours = h,
117+
_ => {
118+
let (secs, nanos) = next.mul_with_nanos(3600)?;
119+
let mins = secs / 60;
120+
let secs = (secs % 60) as u32;
121+
let minutes = u32::try_from(mins).map_err(|_| OUT_OF_RANGE)?;
122+
return Ok((s, set_hms_nano(duration, 0, minutes, secs, nanos)?));
123+
}
124+
}
125+
next = consume_or_return!(
126+
Decimal::parse(s),
127+
Ok((s, set_hms_nano(duration, hours, minutes, 0, 0)?))
128+
);
129+
}
130+
131+
if s.as_bytes().first() == Some(&b'M') {
132+
s = &s[1..];
133+
incomplete = false;
134+
match next.integer() {
135+
Ok(m) => minutes = m,
136+
_ => {
137+
let (secs, nanos) = next.mul_with_nanos(60)?;
138+
let mins = secs / 60;
139+
let secs = (secs % 60) as u32;
140+
minutes = u32::try_from(mins).map_err(|_| OUT_OF_RANGE)?;
141+
return Ok((s, set_hms_nano(duration, hours, minutes, secs, nanos)?));
142+
}
143+
}
144+
next = consume_or_return!(
145+
Decimal::parse(s),
146+
Ok((s, set_hms_nano(duration, hours, minutes, 0, 0)?))
147+
);
148+
}
149+
150+
if s.as_bytes().first() == Some(&b'S') {
151+
s = &s[1..];
152+
let (secs, nanos) = next.mul_with_nanos(1)?;
153+
let secs = u32::try_from(secs).map_err(|_| OUT_OF_RANGE)?;
154+
return Ok((s, set_hms_nano(duration, hours, minutes, secs, nanos)?));
155+
}
156+
157+
if incomplete {
158+
return Err(INVALID);
159+
}
160+
Ok((s, set_hms_nano(duration, hours, minutes, 0, 0)?))
161+
}
3162

4163
/// Helper type for parsing decimals (as in an ISO 8601 duration).
5164
#[derive(Copy, Clone)]
@@ -96,7 +255,7 @@ impl Fraction {
96255
let huge = self.0 * unit + div / 2;
97256
let whole = huge / POW10[15];
98257
let fraction_as_nanos = (huge % POW10[15]) / div;
99-
dbg!(whole as i64, fraction_as_nanos as i64)
258+
(whole as i64, fraction_as_nanos as i64)
100259
}
101260
}
102261

@@ -121,8 +280,9 @@ const POW10: [u64; 16] = [
121280

122281
#[cfg(test)]
123282
mod tests {
124-
use super::Fraction;
125-
use crate::format::INVALID;
283+
use super::{parse_iso8601_duration, parse_iso8601_duration_time, Fraction};
284+
use crate::format::{INVALID, OUT_OF_RANGE, TOO_SHORT};
285+
use crate::CalendarDuration;
126286

127287
#[test]
128288
fn test_parse_fraction() {
@@ -138,4 +298,128 @@ mod tests {
138298
let (_, fraction) = Fraction::parse(",5").unwrap();
139299
assert_eq!(fraction.mul_with_nanos(1), (0, 500_000_000));
140300
}
301+
302+
#[test]
303+
fn test_parse_duration_time() {
304+
let parse = parse_iso8601_duration_time;
305+
let d = CalendarDuration::new();
306+
307+
assert_eq!(parse("T12H", d), Ok(("", d.with_hms(12, 0, 0).unwrap())));
308+
assert_eq!(parse("T12.25H", d), Ok(("", d.with_hms(12, 15, 0).unwrap())));
309+
assert_eq!(parse("T12,25H", d), Ok(("", d.with_hms(12, 15, 0).unwrap())));
310+
assert_eq!(parse("T34M", d), Ok(("", d.with_hms(0, 34, 0).unwrap())));
311+
assert_eq!(parse("T34.25M", d), Ok(("", d.with_hms(0, 34, 15).unwrap())));
312+
assert_eq!(parse("T56S", d), Ok(("", d.with_seconds(56))));
313+
assert_eq!(parse("T0.789S", d), Ok(("", d.with_millis(789).unwrap())));
314+
assert_eq!(parse("T0,789S", d), Ok(("", d.with_millis(789).unwrap())));
315+
assert_eq!(parse("T12H34M", d), Ok(("", d.with_hms(12, 34, 0).unwrap())));
316+
assert_eq!(parse("T12H34M60S", d), Ok(("", d.with_hms(12, 34, 60).unwrap())));
317+
assert_eq!(
318+
parse("T12H34M56.789S", d),
319+
Ok(("", d.with_hms(12, 34, 56).unwrap().with_millis(789).unwrap()))
320+
);
321+
assert_eq!(parse("T12H56S", d), Ok(("", d.with_hms(12, 0, 56).unwrap())));
322+
assert_eq!(parse("T34M56S", d), Ok(("", d.with_hms(0, 34, 56).unwrap())));
323+
324+
// Data after a fraction is ignored
325+
assert_eq!(parse("T12.5H16M", d), Ok(("16M", d.with_hms(12, 30, 0).unwrap())));
326+
assert_eq!(parse("T12H16.5M30S", d), Ok(("30S", d.with_hms(12, 16, 30).unwrap())));
327+
328+
// Zero values
329+
assert_eq!(parse("T0H", d), Ok(("", d)));
330+
assert_eq!(parse("T0M", d), Ok(("", d)));
331+
assert_eq!(parse("T0S", d), Ok(("", d)));
332+
assert_eq!(parse("T0,0S", d), Ok(("", d)));
333+
334+
// Empty or invalid values
335+
assert_eq!(parse("T", d), Err(TOO_SHORT));
336+
assert_eq!(parse("TH", d), Err(INVALID));
337+
assert_eq!(parse("TM", d), Err(INVALID));
338+
assert_eq!(parse("TS", d), Err(INVALID));
339+
assert_eq!(parse("T.5S", d), Err(INVALID));
340+
assert_eq!(parse("T,5S", d), Err(INVALID));
341+
342+
// Date components
343+
assert_eq!(parse("T5W", d), Err(INVALID));
344+
assert_eq!(parse("T5Y", d), Err(INVALID));
345+
assert_eq!(parse("T5D", d), Err(INVALID));
346+
347+
// Max values
348+
assert_eq!(parse("T1118481H", d), Ok(("", d.with_hms(1118481, 0, 0).unwrap())));
349+
assert_eq!(parse("T1118482H", d), Err(OUT_OF_RANGE));
350+
assert_eq!(parse("T1118481.05H", d), Ok(("", d.with_hms(1118481, 3, 0).unwrap())));
351+
assert_eq!(parse("T1118481.5H", d), Err(OUT_OF_RANGE));
352+
assert_eq!(parse("T67108863M", d), Ok(("", d.with_hms(0, u32::MAX >> 6, 0).unwrap())));
353+
assert_eq!(parse("T67108864M", d), Err(OUT_OF_RANGE));
354+
assert_eq!(parse("T67108863.25M", d), Ok(("", d.with_hms(0, u32::MAX >> 6, 15).unwrap())));
355+
assert_eq!(parse("T4294967295S", d), Ok(("", d.with_seconds(u32::MAX))));
356+
assert_eq!(parse("T4294967296S", d), Err(OUT_OF_RANGE));
357+
assert_eq!(
358+
parse("T4294967295.25S", d),
359+
Ok(("", d.with_seconds(u32::MAX).with_millis(250).unwrap()))
360+
);
361+
assert_eq!(
362+
parse("T4294967295.999999999S", d),
363+
Ok(("", d.with_seconds(u32::MAX).with_nanos(999_999_999).unwrap()))
364+
);
365+
assert_eq!(parse("T4294967295.9999999995S", d), Err(OUT_OF_RANGE));
366+
assert_eq!(parse("T12H34M61S", d), Err(OUT_OF_RANGE));
367+
}
368+
369+
#[test]
370+
fn test_parse_duration() {
371+
let d = CalendarDuration::new();
372+
assert_eq!(
373+
parse_iso8601_duration("P12Y"),
374+
Ok(("", d.with_years_and_months(12, 0).unwrap()))
375+
);
376+
assert_eq!(parse_iso8601_duration("P34M"), Ok(("", d.with_months(34))));
377+
assert_eq!(parse_iso8601_duration("P56D"), Ok(("", d.with_days(56))));
378+
assert_eq!(parse_iso8601_duration("P78W"), Ok(("", d.with_weeks_and_days(78, 0).unwrap())));
379+
380+
// Fractional date values
381+
assert_eq!(
382+
parse_iso8601_duration("P1.25Y"),
383+
Ok(("", d.with_years_and_months(1, 3).unwrap()))
384+
);
385+
assert_eq!(
386+
parse_iso8601_duration("P1.99Y"),
387+
Ok(("", d.with_years_and_months(2, 0).unwrap()))
388+
);
389+
assert_eq!(parse_iso8601_duration("P1.4W"), Ok(("", d.with_days(10))));
390+
assert_eq!(parse_iso8601_duration("P1.95W"), Ok(("", d.with_days(14))));
391+
assert_eq!(parse_iso8601_duration("P1.5M"), Err(INVALID));
392+
assert_eq!(parse_iso8601_duration("P1.5D"), Err(INVALID));
393+
394+
// Data after a fraction is ignored
395+
assert_eq!(
396+
parse_iso8601_duration("P1.25Y5D"),
397+
Ok(("5D", d.with_years_and_months(1, 3).unwrap()))
398+
);
399+
assert_eq!(
400+
parse_iso8601_duration("P1.25YT3H"),
401+
Ok(("T3H", d.with_years_and_months(1, 3).unwrap()))
402+
);
403+
404+
// Zero values
405+
assert_eq!(parse_iso8601_duration("P0Y"), Ok(("", d)));
406+
assert_eq!(parse_iso8601_duration("P0M"), Ok(("", d)));
407+
assert_eq!(parse_iso8601_duration("P0W"), Ok(("", d)));
408+
assert_eq!(parse_iso8601_duration("P0D"), Ok(("", d)));
409+
assert_eq!(parse_iso8601_duration("PT0M"), Ok(("", d)));
410+
assert_eq!(parse_iso8601_duration("PT0S"), Ok(("", d)));
411+
412+
// Invalid designator at a position where another designator can be expected.
413+
assert_eq!(parse_iso8601_duration("P12Y12Y"), Err(INVALID));
414+
assert_eq!(parse_iso8601_duration("P12M12M"), Err(INVALID));
415+
assert_eq!(parse_iso8601_duration("P12M12Y"), Err(INVALID));
416+
417+
// Trailing data
418+
assert_eq!(
419+
parse_iso8601_duration("P12W34D"),
420+
Ok(("34D", d.with_weeks_and_days(12, 0).unwrap()))
421+
);
422+
assert_eq!(parse_iso8601_duration("P12D12D"), Ok(("12D", d.with_days(12))));
423+
assert_eq!(parse_iso8601_duration("P12D12Y"), Ok(("12Y", d.with_days(12))));
424+
}
141425
}

0 commit comments

Comments
 (0)