1#![forbid(unsafe_code)]
60use std::fmt;
62use std::time::Duration;
63
64#[derive(Clone, Copy, Debug, Eq, PartialEq)]
66pub enum OverflowBehavior {
67 Error,
69 Saturate,
71}
72
73#[derive(Clone, Copy, Debug)]
75pub struct ParseOptions {
76 overflow: OverflowBehavior,
77}
78
79impl ParseOptions {
80 pub fn strict() -> Self {
82 Self {
83 overflow: OverflowBehavior::Error,
84 }
85 }
86 pub fn saturating(mut self) -> Self {
88 self.overflow = OverflowBehavior::Saturate;
89 self
90 }
91}
92
93pub fn parse(input: &str) -> Result<Duration, ParseError> {
106 parse_with(input, &ParseOptions::strict())
107}
108
109pub fn parse_with(input: &str, opts: &ParseOptions) -> Result<Duration, ParseError> {
120 let s = normalize_input(input)?;
121 Parser::new(&s, *opts).parse()
122}
123
124pub fn format(d: Duration) -> String {
138 format_with(d, &FormatOptions::mixed())
139}
140
141#[derive(Clone, Copy, Debug)]
143pub enum FormatStyle {
144 Mixed,
147 LargestUnitDecimal,
151}
152
153#[derive(Clone, Copy, Debug)]
155pub struct FormatOptions {
156 style: FormatStyle,
157 max_frac_digits: u8, }
159
160impl FormatOptions {
161 pub fn mixed() -> Self {
163 Self {
164 style: FormatStyle::Mixed,
165 max_frac_digits: 9,
166 }
167 }
168 pub fn largest_unit_decimal() -> Self {
170 Self {
171 style: FormatStyle::LargestUnitDecimal,
172 max_frac_digits: 9,
173 }
174 }
175 pub fn with_max_frac_digits(mut self, digits: u8) -> Self {
177 self.max_frac_digits = digits.min(9);
178 self
179 }
180}
181
182pub fn format_with(d: Duration, opts: &FormatOptions) -> String {
195 match opts.style {
196 FormatStyle::Mixed => format_mixed(d, opts.max_frac_digits),
197 FormatStyle::LargestUnitDecimal => format_largest_unit_decimal(d, opts.max_frac_digits),
198 }
199}
200
201#[derive(Clone, Debug, Eq, PartialEq)]
203pub enum ParseError {
204 Empty,
206 InvalidChar(usize),
208 InvalidNumber(usize),
210 InvalidUnit(usize),
212 OutOfOrderUnit {
214 prev: Unit,
216 next: Unit,
218 index: usize,
220 },
221 DuplicateUnit {
223 unit: Unit,
225 index: usize,
227 },
228 DecimalNotLast(usize),
230 TooPreciseFraction {
232 digits: usize,
234 index: usize,
236 },
237 Overflow,
239}
240
241impl fmt::Display for ParseError {
242 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243 use ParseError::*;
244 match self {
245 Empty => write!(f, "empty duration"),
246 InvalidChar(i) => write!(f, "invalid character at byte index {}", i),
247 InvalidNumber(i) => write!(f, "invalid number at byte index {}", i),
248 InvalidUnit(i) => write!(f, "invalid or missing unit at byte index {}", i),
249 OutOfOrderUnit { prev, next, index } => write!(
250 f,
251 "out-of-order unit '{}' followed by '{}' at byte index {}",
252 prev.as_str(),
253 next.as_str(),
254 index
255 ),
256 DuplicateUnit { unit, index } => write!(
257 f,
258 "duplicate unit '{}' at byte index {}",
259 unit.as_str(),
260 index
261 ),
262 DecimalNotLast(i) => write!(f, "decimal segment must be last (index {})", i),
263 TooPreciseFraction { digits, index } => write!(
264 f,
265 "fractional part has {} digits (max 9) at byte index {}",
266 digits, index
267 ),
268 Overflow => write!(f, "duration overflowed maximum representable span"),
269 }
270 }
271}
272
273impl std::error::Error for ParseError {}
274
275#[derive(Clone, Copy, Debug, Eq, PartialEq)]
277pub enum Unit {
278 D,
280 H,
282 M,
284 S,
286 Ms,
288 Us,
290 Ns,
292}
293
294impl Unit {
295 fn as_str(self) -> &'static str {
296 match self {
297 Unit::D => "d",
298 Unit::H => "h",
299 Unit::M => "m",
300 Unit::S => "s",
301 Unit::Ms => "ms",
302 Unit::Us => "us",
303 Unit::Ns => "ns",
304 }
305 }
306 fn rank(self) -> u8 {
307 match self {
308 Unit::D => 6,
309 Unit::H => 5,
310 Unit::M => 4,
311 Unit::S => 3,
312 Unit::Ms => 2,
313 Unit::Us => 1,
314 Unit::Ns => 0,
315 }
316 }
317 fn nanos(self) -> u128 {
318 match self {
319 Unit::D => 86_400_000_000_000,
320 Unit::H => 3_600_000_000_000,
321 Unit::M => 60_000_000_000,
322 Unit::S => 1_000_000_000,
323 Unit::Ms => 1_000_000,
324 Unit::Us => 1_000,
325 Unit::Ns => 1,
326 }
327 }
328}
329
330struct Parser<'a> {
331 s: &'a str,
332 opts: ParseOptions,
333 i: usize,
334 len: usize,
335}
336
337impl<'a> Parser<'a> {
338 fn new(s: &'a str, opts: ParseOptions) -> Self {
339 Self {
340 s,
341 opts,
342 i: 0,
343 len: s.len(),
344 }
345 }
346
347 fn parse(&mut self) -> Result<Duration, ParseError> {
348 if self.s.is_empty() {
349 return Err(ParseError::Empty);
350 }
351
352 let mut total_nanos: u128 = 0;
353 let max_nanos: u128 =
354 (u128::from(u64::MAX) * 1_000_000_000u128) + (1_000_000_000u128 - 1u128);
355
356 let mut prev_rank: Option<u8> = None;
357 let mut seen_mask: u8 = 0;
358 let mut decimal_used = false;
359 let mut segments = 0usize;
360
361 while self.i < self.len {
362 let start_num = self.i;
363 let (int_part, frac_part) = self.parse_number()?;
365 segments += 1;
366
367 if frac_part.is_some() && self.i < self.len {
369 decimal_used = true;
372 }
373
374 let unit_start = self.i;
375 let unit = self
376 .parse_unit()
377 .map_err(|_| ParseError::InvalidUnit(unit_start))?;
378
379 let rank = unit.rank();
381 if let Some(prev) = prev_rank {
382 if rank >= prev {
383 return Err(ParseError::OutOfOrderUnit {
384 prev: rank_to_unit(prev),
385 next: unit,
386 index: unit_start,
387 });
388 }
389 }
390 prev_rank = Some(rank);
391
392 let bit = 1u8 << rank;
394 if (seen_mask & bit) != 0 {
395 return Err(ParseError::DuplicateUnit {
396 unit,
397 index: unit_start,
398 });
399 }
400 seen_mask |= bit;
401
402 if decimal_used && self.i < self.len {
404 return Err(ParseError::DecimalNotLast(start_num));
405 }
406
407 let unit_nanos = unit.nanos();
409
410 if int_part > 0 {
412 let add = (int_part as u128)
413 .checked_mul(unit_nanos)
414 .ok_or(ParseError::Overflow)?;
415 total_nanos = match total_nanos.checked_add(add) {
416 Some(v) => v,
417 None => {
418 if self.opts.overflow == OverflowBehavior::Saturate {
419 return Ok(duration_max());
420 } else {
421 return Err(ParseError::Overflow);
422 }
423 }
424 };
425 if total_nanos > max_nanos {
426 if self.opts.overflow == OverflowBehavior::Saturate {
427 return Ok(duration_max());
428 } else {
429 return Err(ParseError::Overflow);
430 }
431 }
432 }
433
434 if let Some(frac) = frac_part {
436 let digits = frac.len();
437 if digits == 0 {
438 return Err(ParseError::InvalidNumber(start_num));
439 }
440 if digits > 9 {
441 return Err(ParseError::TooPreciseFraction {
442 digits,
443 index: start_num,
444 });
445 }
446 let frac_value = frac
447 .bytes()
448 .try_fold(0u128, |acc, b: u8| {
449 if b.is_ascii_digit() {
450 Some(acc * 10 + u128::from(b - b'0'))
451 } else {
452 None
453 }
454 })
455 .ok_or(ParseError::InvalidNumber(start_num))?;
456
457 let denom = 10u128.pow(digits as u32);
459 let add = unit_nanos
460 .checked_mul(frac_value)
461 .ok_or(ParseError::Overflow)?
462 / denom;
463
464 total_nanos = match total_nanos.checked_add(add) {
465 Some(v) => v,
466 None => {
467 if self.opts.overflow == OverflowBehavior::Saturate {
468 return Ok(duration_max());
469 } else {
470 return Err(ParseError::Overflow);
471 }
472 }
473 };
474 if total_nanos > max_nanos {
475 if self.opts.overflow == OverflowBehavior::Saturate {
476 return Ok(duration_max());
477 } else {
478 return Err(ParseError::Overflow);
479 }
480 }
481 }
482 }
483
484 if segments == 0 {
485 return Err(ParseError::Empty);
486 }
487
488 Ok(nanos_to_duration(total_nanos))
489 }
490
491 fn parse_number(&mut self) -> Result<(u64, Option<&'a str>), ParseError> {
492 let start = self.i;
493 let bytes = self.s.as_bytes();
494
495 if start >= self.len {
496 return Err(ParseError::InvalidNumber(start));
497 }
498
499 let mut saw_digit = false;
500 let mut int_end = start;
501 while int_end < self.len {
502 let b = bytes[int_end];
503 if b.is_ascii_digit() {
504 saw_digit = true;
505 int_end += 1;
506 } else {
507 break;
508 }
509 }
510
511 if !saw_digit {
512 return Err(ParseError::InvalidNumber(start));
513 }
514
515 let mut frac: Option<&'a str> = None;
516 let mut pos = int_end;
517 if pos < self.len && bytes[pos] == b'.' {
518 pos += 1;
520 let frac_start = pos;
521 let mut frac_end = pos;
522 while frac_end < self.len {
523 let b = bytes[frac_end];
524 if b.is_ascii_digit() {
525 frac_end += 1;
526 } else {
527 break;
528 }
529 }
530 if frac_end == frac_start {
531 return Err(ParseError::InvalidNumber(start));
532 }
533 frac = Some(&self.s[frac_start..frac_end]);
534 pos = frac_end;
535 }
536
537 let int_str = &self.s[start..int_end];
539 let int_val = int_str
540 .bytes()
541 .try_fold(0u64, |acc, b| {
542 acc.checked_mul(10)?.checked_add(u64::from(b - b'0'))
543 })
544 .ok_or(ParseError::InvalidNumber(start))?;
545
546 self.i = pos;
547 Ok((int_val, frac))
548 }
549
550 fn parse_unit(&mut self) -> Result<Unit, ()> {
551 let rest = &self.s[self.i..];
553
554 let try_take =
555 |s: &str, u: Unit| -> Option<Unit> { if rest.starts_with(s) { Some(u) } else { None } };
556
557 let unit = try_take("ms", Unit::Ms)
558 .or_else(|| try_take("us", Unit::Us))
559 .or_else(|| try_take("ns", Unit::Ns))
560 .or_else(|| try_take("d", Unit::D))
561 .or_else(|| try_take("h", Unit::H))
562 .or_else(|| try_take("m", Unit::M))
563 .or_else(|| try_take("s", Unit::S));
564
565 if let Some(u) = unit {
566 self.i += u.as_str().len();
567 Ok(u)
568 } else {
569 Err(())
570 }
571 }
572}
573
574fn nanos_to_duration(nanos: u128) -> Duration {
577 let secs = (nanos / 1_000_000_000) as u64;
578 let sub = (nanos % 1_000_000_000) as u32;
579 Duration::new(secs, sub)
580}
581
582fn duration_max() -> Duration {
583 nanos_to_duration((u128::from(u64::MAX) * 1_000_000_000u128) + 999_999_999u128)
585}
586
587fn rank_to_unit(rank: u8) -> Unit {
588 match rank {
589 6 => Unit::D,
590 5 => Unit::H,
591 4 => Unit::M,
592 3 => Unit::S,
593 2 => Unit::Ms,
594 1 => Unit::Us,
595 _ => Unit::Ns,
596 }
597}
598
599fn normalize_input(input: &str) -> Result<String, ParseError> {
600 #[cfg(feature = "loose")]
601 {
602 let mut s = String::with_capacity(input.len());
603 for (i, ch) in input.chars().enumerate() {
604 if ch == ' ' || ch == '_' {
605 continue;
606 }
607 if ch.is_ascii() {
608 s.push(ch.to_ascii_lowercase());
609 } else {
610 return Err(ParseError::InvalidChar(i));
611 }
612 }
613 if s.is_empty() {
614 return Err(ParseError::Empty);
615 }
616 Ok(s)
617 }
618 #[cfg(not(feature = "loose"))]
619 {
620 if input.is_empty() {
622 return Err(ParseError::Empty);
623 }
624 for (i, b) in input.bytes().enumerate() {
625 if !b.is_ascii() {
626 return Err(ParseError::InvalidChar(i));
627 }
628 if b == b' ' || b == b'_' || b.is_ascii_uppercase() {
629 return Err(ParseError::InvalidChar(i));
630 }
631 }
632 Ok(input.to_string())
633 }
634}
635
636fn format_mixed(d: Duration, max_frac_digits: u8) -> String {
639 let mut rem_secs = d.as_secs();
640 let rem_nanos = d.subsec_nanos();
641
642 let mut out = String::new();
643
644 let days = rem_secs / 86_400;
645 if days > 0 {
646 out.push_str(&format!("{}d", days));
647 rem_secs %= 86_400;
648 }
649 let hours = rem_secs / 3_600;
650 if hours > 0 {
651 out.push_str(&format!("{}h", hours));
652 rem_secs %= 3_600;
653 }
654 let mins = rem_secs / 60;
655 if mins > 0 {
656 out.push_str(&format!("{}m", mins));
657 rem_secs %= 60;
658 }
659
660 if rem_secs > 0 || rem_nanos > 0 {
662 if rem_nanos > 0 {
663 let s = format_fraction(rem_secs, rem_nanos, max_frac_digits);
664 out.push_str(&format!("{}s", s));
665 } else {
666 out.push_str(&format!("{}s", rem_secs));
667 }
668 }
669
670 if out.is_empty() {
672 out.push_str("0s");
673 }
674 out
675}
676
677fn format_largest_unit_decimal(d: Duration, max_frac_digits: u8) -> String {
678 let total_nanos = (d.as_secs() as u128) * 1_000_000_000u128 + (d.subsec_nanos() as u128);
679
680 if total_nanos == 0 {
681 return "0s".to_string();
682 }
683
684 let candidates = [
685 Unit::D,
686 Unit::H,
687 Unit::M,
688 Unit::S,
689 Unit::Ms,
690 Unit::Us,
691 Unit::Ns,
692 ];
693
694 for &u in &candidates {
695 let u_nanos = u.nanos();
696 if total_nanos >= u_nanos {
697 let whole = total_nanos / u_nanos;
699 let rem = total_nanos % u_nanos;
700 if rem == 0 {
701 return format!("{}{}", whole, u.as_str());
702 } else {
703 let frac = rem * 10u128.pow(max_frac_digits as u32) / u_nanos;
705 let mut frac_str = format!("{:0width$}", frac, width = max_frac_digits as usize);
707 while frac_str.ends_with('0') && frac_str.len() > 1 {
708 frac_str.pop();
709 }
710 return format!("{}.{}{}", whole, frac_str, u.as_str());
711 }
712 }
713 }
714 "0s".to_string()
716}
717
718fn format_fraction(secs: u64, nanos: u32, max_frac_digits: u8) -> String {
719 if nanos == 0 || max_frac_digits == 0 {
720 return format!("{}.", secs).trim_end_matches('.').to_string();
721 }
722 let scale = 10u32.pow(max_frac_digits as u32);
724 let frac = (nanos as u128 * scale as u128) / 1_000_000_000u128;
725 let mut frac_str = format!("{:0width$}", frac, width = max_frac_digits as usize);
726 while frac_str.ends_with('0') && frac_str.len() > 1 {
728 frac_str.pop();
729 }
730 format!("{}.{}", secs, frac_str)
731}
732
733#[cfg(feature = "serde")]
737#[derive(Clone, Copy, Debug, Eq, PartialEq)]
738pub struct DurationStr(pub Duration);
739
740#[cfg(feature = "serde")]
741impl serde::Serialize for DurationStr {
742 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
743 where
744 S: serde::Serializer,
745 {
746 let s = format(self.0);
747 serializer.serialize_str(&s)
748 }
749}
750
751#[cfg(feature = "serde")]
752impl<'de> serde::Deserialize<'de> for DurationStr {
753 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
754 where
755 D: serde::Deserializer<'de>,
756 {
757 struct V;
758 impl<'de> serde::de::Visitor<'de> for V {
759 type Value = DurationStr;
760 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
761 f.write_str("a strict human duration string (e.g., \"2d3h4m\", \"90s\", \"1.5h\")")
762 }
763 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
764 where
765 E: serde::de::Error,
766 {
767 parse(v)
768 .map(DurationStr)
769 .map_err(|e| E::custom(format!("invalid duration: {}", e)))
770 }
771 }
772 deserializer.deserialize_str(V)
773 }
774}
775
776#[cfg(test)]
779mod tests {
780 use super::*;
781
782 #[test]
783 fn basic_parse() {
784 assert_eq!(parse("90s").unwrap(), Duration::from_secs(90));
785 assert_eq!(parse("1.5h").unwrap(), Duration::from_secs(5400));
786 assert_eq!(
787 parse("2d3h4m").unwrap(),
788 Duration::from_secs(2 * 86_400 + 3 * 3600 + 4 * 60)
789 );
790 assert_eq!(parse("250ms").unwrap(), Duration::from_millis(250));
791 assert_eq!(parse("750us").unwrap(), Duration::from_micros(750));
792 assert_eq!(parse("10ns").unwrap(), Duration::new(0, 10));
793 assert_eq!(parse("1m30s").unwrap(), Duration::from_secs(90));
794 assert_eq!(
795 parse("1m30.5s").unwrap(),
796 Duration::from_secs(90) + Duration::from_millis(500)
797 );
798 }
799
800 #[test]
801 fn ordering_and_duplicates() {
802 assert!(parse("h1m").is_err());
803 assert!(parse("1m1m").is_err());
804 assert!(parse("1s2m").is_err());
805 assert!(parse("1ms2s").is_err());
806 }
807
808 #[test]
809 fn decimal_rules() {
810 assert!(parse("1.5h10m").is_err()); assert!(parse("1.1234567890s").is_err()); assert!(parse("1.s").is_err());
813 assert!(parse(".5h").is_err());
814 }
815
816 #[test]
817 fn zero_and_format() {
818 assert_eq!(parse("0s").unwrap(), Duration::from_secs(0));
819 assert_eq!(format(Duration::from_secs(0)), "0s");
820
821 let d = Duration::from_secs(2 * 86_400 + 3 * 3600 + 4 * 60) + Duration::from_millis(250);
822 let s = format(d);
823 assert_eq!(s, "2d3h4m0.25s");
824 }
825
826 #[test]
827 fn roundtrip_mixed() {
828 let cases = [
829 "2d3h4m",
830 "90s",
831 "1.5h",
832 "250ms",
833 "1m30s",
834 "1m30.5s",
835 "999ms",
836 "1001ms",
837 "3h15m45.123456789s",
838 ];
839 for &c in &cases {
840 let d = parse(c).unwrap();
841 let s = format(d);
842 let d2 = parse(&s).unwrap();
843 assert_eq!(d, d2, "roundtrip failed for {}", c);
844 }
845 }
846
847 #[test]
848 fn largest_unit_decimal_format() {
849 let d = Duration::from_secs(5400);
850 let s = format_with(
851 d,
852 &FormatOptions::largest_unit_decimal().with_max_frac_digits(3),
853 );
854 assert_eq!(s, "1.5h");
856 }
857
858 #[test]
859 fn overflow_behavior() {
860 let huge = format!("{}d", u64::MAX);
862 let err = parse_with(&huge, &ParseOptions::strict()).unwrap_err();
863 assert!(matches!(err, ParseError::Overflow));
864
865 let saturated = parse_with(&huge, &ParseOptions::strict().saturating()).unwrap();
866 assert_eq!(saturated, super::duration_max());
867 }
868
869 #[cfg(feature = "loose")]
870 #[test]
871 fn loose_mode() {
872 assert_eq!(
873 super::parse("1H 30M").unwrap(),
874 std::time::Duration::from_secs(5400)
875 );
876 assert_eq!(
877 super::parse("1h_250ms").unwrap(),
878 std::time::Duration::from_millis(3_600_250)
879 );
880 }
881}