1#![deny(warnings)]
16#![deny(missing_docs)]
17
18use std::error::Error;
19use std::fmt::{self, Display, Formatter};
20use std::time::Duration;
21
22const MAX_NANOS_U64: u64 = i64::MAX as u64;
25
26#[derive(Clone, Debug, Eq, PartialEq)]
28pub enum ParseDurationError {
29 Invalid {
31 input: String,
33 reason: String,
35 },
36 Negative,
38 Overflow,
40}
41
42impl Display for ParseDurationError {
43 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
44 match self {
45 ParseDurationError::Invalid { input, reason } => {
46 write!(f, "invalid duration '{}': {}", input, reason)
47 }
48 ParseDurationError::Negative => write!(f, "negative durations are not supported"),
49 ParseDurationError::Overflow => write!(f, "duration value exceeds supported range"),
50 }
51 }
52}
53
54impl Error for ParseDurationError {}
55
56fn invalid(input: &str, reason: impl Into<String>) -> ParseDurationError {
57 ParseDurationError::Invalid {
58 input: input.to_string(),
59 reason: reason.into(),
60 }
61}
62
63pub fn parse_duration(s: &str) -> Result<Duration, ParseDurationError> {
75 let orig = s;
76 let mut rest = s;
77 let mut total_ns: u128 = 0;
78 let mut negative = false;
79
80 if let Some(c) = rest.chars().next() {
81 if c == '+' || c == '-' {
82 negative = c == '-';
83 rest = &rest[1..];
84 }
85 }
86
87 if rest == "0" {
89 return Ok(Duration::ZERO);
90 }
91 if rest.is_empty() {
92 return Err(invalid(orig, "empty duration"));
93 }
94
95 while !rest.is_empty() {
96 let (int_part, after_int) = consume_digits(rest);
97 let had_int = !int_part.is_empty();
98
99 let (frac_part, after_frac) = if let Some(stripped) = after_int.strip_prefix('.') {
100 consume_digits(stripped)
101 } else {
102 ("", after_int)
103 };
104 let consumed_dot = after_int.starts_with('.');
105 let had_frac = consumed_dot && !frac_part.is_empty();
106
107 if !had_int && !had_frac {
108 return Err(invalid(orig, "expected digits"));
109 }
110
111 rest = after_frac;
112
113 let unit_str = consume_unit(rest);
114 if unit_str.is_empty() {
115 return Err(invalid(orig, "missing unit"));
116 }
117 rest = &rest[unit_str.len()..];
118
119 let unit_ns: u128 = match unit_str {
120 "ns" => 1,
121 "us" | "µs" | "μs" => 1_000,
122 "ms" => 1_000_000,
123 "s" => 1_000_000_000,
124 "m" => 60 * 1_000_000_000,
125 "h" => 3_600 * 1_000_000_000,
126 other => return Err(invalid(orig, format!("unknown unit '{}'", other))),
127 };
128
129 let int_val: u128 = if int_part.is_empty() {
130 0
131 } else {
132 int_part
133 .parse::<u128>()
134 .map_err(|_| invalid(orig, "integer overflow"))?
135 };
136
137 let mut ns = int_val.checked_mul(unit_ns).ok_or_else(|| invalid(orig, "overflow"))?;
138
139 if !frac_part.is_empty() {
140 let keep = frac_part.len().min(18);
143 let frac_digits = &frac_part[..keep];
144 let mut scale: u128 = 1;
145 for _ in 0..keep {
146 scale *= 10;
147 }
148 let f: u128 = frac_digits
149 .parse::<u128>()
150 .map_err(|_| invalid(orig, "invalid fractional"))?;
151 let frac_ns = f.checked_mul(unit_ns).ok_or_else(|| invalid(orig, "overflow"))? / scale;
152 ns = ns.checked_add(frac_ns).ok_or_else(|| invalid(orig, "overflow"))?;
153 }
154
155 total_ns = total_ns.checked_add(ns).ok_or_else(|| invalid(orig, "overflow"))?;
156 }
157
158 if negative && total_ns != 0 {
159 return Err(ParseDurationError::Negative);
160 }
161
162 if total_ns > MAX_NANOS_U64 as u128 {
163 return Err(ParseDurationError::Overflow);
164 }
165 Ok(Duration::from_nanos(total_ns as u64))
166}
167
168fn consume_digits(s: &str) -> (&str, &str) {
169 let end = s.bytes().take_while(|b| b.is_ascii_digit()).count();
170 s.split_at(end)
171}
172
173fn consume_unit(s: &str) -> &str {
174 let mut end = 0;
175 for (i, c) in s.char_indices() {
176 if c.is_ascii_alphabetic() || c == 'µ' || c == 'μ' {
177 end = i + c.len_utf8();
178 } else {
179 break;
180 }
181 }
182 &s[..end]
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 const NS: Duration = Duration::from_nanos(1);
190 const MS: Duration = Duration::from_millis(1);
191 const S: Duration = Duration::from_secs(1);
192 const M: Duration = Duration::from_secs(60);
193 const H: Duration = Duration::from_secs(3600);
194
195 #[test]
196 fn supports_go_style_units() {
197 assert_eq!(parse_duration("10s").unwrap(), Duration::from_secs(10));
198 assert_eq!(parse_duration("1m0s").unwrap(), Duration::from_secs(60));
199 assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
200 assert_eq!(
201 parse_duration("1h2m3.5s").unwrap(),
202 Duration::from_secs(3723) + Duration::from_millis(500)
203 );
204 assert_eq!(parse_duration("250us").unwrap(), Duration::from_micros(250));
205 assert_eq!(parse_duration("250µs").unwrap(), Duration::from_micros(250));
206 assert_eq!(parse_duration("250μs").unwrap(), Duration::from_micros(250));
207 }
208
209 #[test]
210 fn supports_signs_zero_and_fractions() {
211 assert_eq!(parse_duration("0").unwrap(), Duration::ZERO);
212 assert_eq!(parse_duration("+0").unwrap(), Duration::ZERO);
213 assert_eq!(parse_duration("-0").unwrap(), Duration::ZERO);
214 assert_eq!(parse_duration("0s").unwrap(), Duration::ZERO);
215 assert_eq!(parse_duration("+5h").unwrap(), 5 * H);
216 assert_eq!(parse_duration(".5s").unwrap(), 500 * MS);
217 assert_eq!(parse_duration("1.5h").unwrap(), 90 * M);
218 assert_eq!(
219 parse_duration("2h45m30.5s").unwrap(),
220 (2 * H) + (45 * M) + (30 * S) + (500 * MS)
221 );
222 assert_eq!(
223 parse_duration("1h1m1s1ms1us1ns").unwrap(),
224 H + M + S + MS + (1000 * NS) + NS
225 );
226 }
227
228 #[test]
229 fn largest_representable_value() {
230 assert_eq!(
231 parse_duration("9223372036854775807ns").unwrap(),
232 Duration::from_nanos(9_223_372_036_854_775_807)
233 );
234 }
235
236 #[test]
237 fn rejects_invalid_and_out_of_range() {
238 assert!(matches!(parse_duration("10"), Err(ParseDurationError::Invalid { .. })));
240 assert!(matches!(parse_duration(""), Err(ParseDurationError::Invalid { .. })));
241 assert!(matches!(parse_duration("abc"), Err(ParseDurationError::Invalid { .. })));
242 assert!(matches!(parse_duration("1d"), Err(ParseDurationError::Invalid { .. })));
243 assert!(matches!(
244 parse_duration("5m32sFOO"),
245 Err(ParseDurationError::Invalid { .. })
246 ));
247 assert!(matches!(parse_duration("-1s"), Err(ParseDurationError::Negative)));
248 assert!(matches!(
249 parse_duration("9223372036854775808ns"),
250 Err(ParseDurationError::Overflow)
251 ));
252 }
253
254 #[test]
255 fn error_display_messages() {
256 assert!(parse_duration("").unwrap_err().to_string().contains("empty duration"));
257 assert!(parse_duration(".s")
258 .unwrap_err()
259 .to_string()
260 .contains("expected digits"));
261 assert!(parse_duration("5ns5").unwrap_err().to_string().contains("missing unit"));
262 assert!(parse_duration("1d")
263 .unwrap_err()
264 .to_string()
265 .contains("unknown unit 'd'"));
266 assert!(parse_duration("-1s").unwrap_err().to_string().contains("negative"));
267 assert!(parse_duration("9223372036854775808ns")
268 .unwrap_err()
269 .to_string()
270 .contains("exceeds"));
271 }
272}