1+ use std:: f64;
2+
13use anyhow:: { Result , anyhow, bail} ;
24use 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