Skip to content

Commit 0294f46

Browse files
authored
feat(duration): support fractional values in ISO8601 parsing (#679)
feat(duration): support fractional values in parsing
1 parent 14da9da commit 0294f46

File tree

1 file changed

+136
-35
lines changed

1 file changed

+136
-35
lines changed

src/base/duration.rs

Lines changed: 136 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::f64;
2+
13
use anyhow::{Result, anyhow, bail};
24
use chrono::Duration;
35

@@ -7,13 +9,19 @@ fn parse_components(
79
s: &str,
810
allowed_units: &[char],
911
original_input: &str,
10-
) -> Result<Vec<(i64, char)>> {
12+
) -> Result<Vec<(f64, char)>> {
1113
let mut result = Vec::new();
1214
let mut iter = s.chars().peekable();
1315
while iter.peek().is_some() {
1416
let mut num_str = String::new();
17+
let mut has_decimal = false;
18+
19+
// Parse digits and optional decimal point
1520
while let Some(&c) = iter.peek() {
16-
if c.is_digit(10) {
21+
if c.is_digit(10) || (c == '.' && !has_decimal) {
22+
if c == '.' {
23+
has_decimal = true;
24+
}
1725
num_str.push(iter.next().unwrap());
1826
} else {
1927
break;
@@ -23,7 +31,7 @@ fn parse_components(
2331
bail!("Expected number in: {}", original_input);
2432
}
2533
let num = num_str
26-
.parse()
34+
.parse::<f64>()
2735
.map_err(|_| anyhow!("Invalid number '{}' in: {}", num_str, original_input))?;
2836
if let Some(&unit) = iter.peek() {
2937
if allowed_units.contains(&unit) {
@@ -84,25 +92,37 @@ fn parse_iso8601_duration(s: &str, original_input: &str) -> Result<Duration> {
8492
}
8593

8694
// Accumulate date duration
87-
let date_duration =
88-
date_components
89-
.iter()
90-
.fold(Duration::zero(), |acc, &(num, unit)| match unit {
91-
'Y' => acc + Duration::days(num * 365),
92-
'M' => acc + Duration::days(num * 30),
93-
'W' => acc + Duration::days(num * 7),
94-
'D' => acc + Duration::days(num),
95+
let date_duration = date_components
96+
.iter()
97+
.fold(Duration::zero(), |acc, &(num, unit)| {
98+
let days = match unit {
99+
'Y' => num * 365.0,
100+
'M' => num * 30.0,
101+
'W' => num * 7.0,
102+
'D' => num,
95103
_ => unreachable!("Invalid date unit should be caught by prior validation"),
96-
});
104+
};
105+
let microseconds = (days * 86_400_000_000.0) as i64;
106+
acc + Duration::microseconds(microseconds)
107+
});
97108

98109
// Accumulate time duration
99110
let time_duration =
100111
time_components
101112
.iter()
102113
.fold(Duration::zero(), |acc, &(num, unit)| match unit {
103-
'H' => acc + Duration::hours(num),
104-
'M' => acc + Duration::minutes(num),
105-
'S' => acc + Duration::seconds(num),
114+
'H' => {
115+
let nanoseconds = (num * 3_600_000_000_000.0).round() as i64;
116+
acc + Duration::nanoseconds(nanoseconds)
117+
}
118+
'M' => {
119+
let nanoseconds = (num * 60_000_000_000.0).round() as i64;
120+
acc + Duration::nanoseconds(nanoseconds)
121+
}
122+
'S' => {
123+
let nanoseconds = (num.fract() * 1_000_000_000.0).round() as i64;
124+
acc + Duration::seconds(num as i64) + Duration::nanoseconds(nanoseconds)
125+
}
106126
_ => unreachable!("Invalid time unit should be caught by prior validation"),
107127
});
108128

@@ -239,15 +259,6 @@ mod tests {
239259
check_err_contains(parse_duration("P1S"), "Invalid unit 'S' in: P1S", "\"P1S\"");
240260
}
241261

242-
#[test]
243-
fn test_iso_invalid_number_parse() {
244-
check_err_contains(
245-
parse_duration("PT99999999999999999999H"),
246-
"Invalid number '99999999999999999999' in: PT99999999999999999999H",
247-
"\"PT99999999999999999999H\"",
248-
);
249-
}
250-
251262
#[test]
252263
fn test_iso_invalid_unit() {
253264
check_err_contains(parse_duration("P1X"), "Invalid unit 'X' in: P1X", "\"P1X\"");
@@ -282,20 +293,21 @@ mod tests {
282293
}
283294

284295
#[test]
285-
fn test_iso_trailing_number_without_unit_after_p() {
296+
fn test_iso_invalid_fractional_format() {
286297
check_err_contains(
287-
parse_duration("P1"),
288-
"Missing unit after number '1' in: P1",
289-
"\"P1\"",
298+
parse_duration("PT1..5S"),
299+
"Invalid unit '.' in: PT1..5S",
300+
"\"PT1..5S\"",
290301
);
291-
}
292-
293-
#[test]
294-
fn test_iso_fractional_seconds_fail() {
295302
check_err_contains(
296-
parse_duration("PT1.5S"),
297-
"Invalid unit '.' in: PT1.5S",
298-
"\"PT1.5S\"",
303+
parse_duration("PT1.5.5S"),
304+
"Invalid unit '.' in: PT1.5.5S",
305+
"\"PT1.5.5S\"",
306+
);
307+
check_err_contains(
308+
parse_duration("P1..5D"),
309+
"Invalid unit '.' in: P1..5D",
310+
"\"P1..5D\"",
299311
);
300312
}
301313

@@ -418,6 +430,95 @@ mod tests {
418430
check_ok(parse_duration("PT0H0M0S"), Duration::zero(), "\"PT0H0M0S\"");
419431
}
420432

433+
#[test]
434+
fn test_iso_fractional_seconds() {
435+
check_ok(
436+
parse_duration("PT1.5S"),
437+
Duration::seconds(1) + Duration::milliseconds(500),
438+
"\"PT1.5S\"",
439+
);
440+
check_ok(
441+
parse_duration("PT441010.456123S"),
442+
Duration::seconds(441010) + Duration::microseconds(456123),
443+
"\"PT441010.456123S\"",
444+
);
445+
check_ok(
446+
parse_duration("PT0.000001S"),
447+
Duration::microseconds(1),
448+
"\"PT0.000001S\"",
449+
);
450+
}
451+
452+
#[test]
453+
fn test_iso_fractional_date_units() {
454+
check_ok(
455+
parse_duration("P1.5D"),
456+
Duration::microseconds((1.5 * 86_400_000_000.0) as i64),
457+
"\"P1.5D\"",
458+
);
459+
check_ok(
460+
parse_duration("P1.25Y"),
461+
Duration::microseconds((1.25 * 365.0 * 86_400_000_000.0) as i64),
462+
"\"P1.25Y\"",
463+
);
464+
check_ok(
465+
parse_duration("P2.75M"),
466+
Duration::microseconds((2.75 * 30.0 * 86_400_000_000.0) as i64),
467+
"\"P2.75M\"",
468+
);
469+
check_ok(
470+
parse_duration("P0.5W"),
471+
Duration::microseconds((0.5 * 7.0 * 86_400_000_000.0) as i64),
472+
"\"P0.5W\"",
473+
);
474+
}
475+
476+
#[test]
477+
fn test_iso_negative_fractional_date_units() {
478+
check_ok(
479+
parse_duration("-P1.5D"),
480+
-Duration::microseconds((1.5 * 86_400_000_000.0) as i64),
481+
"\"-P1.5D\"",
482+
);
483+
check_ok(
484+
parse_duration("-P0.25Y"),
485+
-Duration::microseconds((0.25 * 365.0 * 86_400_000_000.0) as i64),
486+
"\"-P0.25Y\"",
487+
);
488+
}
489+
490+
#[test]
491+
fn test_iso_combined_fractional_units() {
492+
check_ok(
493+
parse_duration("P1.5DT2.5H3.5M4.5S"),
494+
Duration::microseconds((1.5 * 86_400_000_000.0) as i64)
495+
+ Duration::microseconds((2.5 * 3_600_000_000.0) as i64)
496+
+ Duration::microseconds((3.5 * 60_000_000.0) as i64)
497+
+ Duration::seconds(4)
498+
+ Duration::milliseconds(500),
499+
"\"1.5DT2.5H3.5M4.5S\"",
500+
);
501+
}
502+
503+
#[test]
504+
fn test_iso_multiple_fractional_time_units() {
505+
check_ok(
506+
parse_duration("PT1.5S2.5S"),
507+
Duration::seconds(1 + 2) + Duration::milliseconds(500) + Duration::milliseconds(500),
508+
"\"PT1.5S2.5S\"",
509+
);
510+
check_ok(
511+
parse_duration("PT1.1H2.2M3.3S"),
512+
Duration::hours(1)
513+
+ Duration::seconds((0.1 * 3600.0) as i64)
514+
+ Duration::minutes(2)
515+
+ Duration::seconds((0.2 * 60.0) as i64)
516+
+ Duration::seconds(3)
517+
+ Duration::milliseconds(300),
518+
"\"PT1.1H2.2M3.3S\"",
519+
);
520+
}
521+
421522
// Human-readable Tests
422523
#[test]
423524
fn test_human_missing_unit() {

0 commit comments

Comments
 (0)