1 | use std::error::Error as StdError; |
2 | use std::fmt; |
3 | use std::str::Chars; |
4 | use std::time::Duration; |
5 | |
6 | /// Error parsing human-friendly duration |
7 | #[derive (Debug, PartialEq, Clone)] |
8 | pub enum Error { |
9 | /// Invalid character during parsing |
10 | /// |
11 | /// More specifically anything that is not alphanumeric is prohibited |
12 | /// |
13 | /// The field is an byte offset of the character in the string. |
14 | InvalidCharacter(usize), |
15 | /// Non-numeric value where number is expected |
16 | /// |
17 | /// This usually means that either time unit is broken into words, |
18 | /// e.g. `m sec` instead of `msec`, or just number is omitted, |
19 | /// for example `2 hours min` instead of `2 hours 1 min` |
20 | /// |
21 | /// The field is an byte offset of the errorneous character |
22 | /// in the string. |
23 | NumberExpected(usize), |
24 | /// Unit in the number is not one of allowed units |
25 | /// |
26 | /// See documentation of `parse_duration` for the list of supported |
27 | /// time units. |
28 | /// |
29 | /// The two fields are start and end (exclusive) of the slice from |
30 | /// the original string, containing errorneous value |
31 | UnknownUnit { |
32 | /// Start of the invalid unit inside the original string |
33 | start: usize, |
34 | /// End of the invalid unit inside the original string |
35 | end: usize, |
36 | /// The unit verbatim |
37 | unit: String, |
38 | /// A number associated with the unit |
39 | value: u64, |
40 | }, |
41 | /// The numeric value is too large |
42 | /// |
43 | /// Usually this means value is too large to be useful. If user writes |
44 | /// data in subsecond units, then the maximum is about 3k years. When |
45 | /// using seconds, or larger units, the limit is even larger. |
46 | NumberOverflow, |
47 | /// The value was an empty string (or consists only whitespace) |
48 | Empty, |
49 | } |
50 | |
51 | impl StdError for Error {} |
52 | |
53 | impl fmt::Display for Error { |
54 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
55 | match self { |
56 | Error::InvalidCharacter(offset) => write!(f, "invalid character at {}" , offset), |
57 | Error::NumberExpected(offset) => write!(f, "expected number at {}" , offset), |
58 | Error::UnknownUnit { unit, value, .. } if &unit == &"" => { |
59 | write!(f, |
60 | "time unit needed, for example {0}sec or {0}ms" , |
61 | value, |
62 | ) |
63 | } |
64 | Error::UnknownUnit { unit, .. } => { |
65 | write!( |
66 | f, |
67 | "unknown time unit {:?}, \ |
68 | supported units: ns, us, ms, sec, min, hours, days, \ |
69 | weeks, months, years (and few variations)" , |
70 | unit |
71 | ) |
72 | } |
73 | Error::NumberOverflow => write!(f, "number is too large" ), |
74 | Error::Empty => write!(f, "value was empty" ), |
75 | } |
76 | } |
77 | } |
78 | |
79 | /// A wrapper type that allows you to Display a Duration |
80 | #[derive (Debug, Clone)] |
81 | pub struct FormattedDuration(Duration); |
82 | |
83 | trait OverflowOp: Sized { |
84 | fn mul(self, other: Self) -> Result<Self, Error>; |
85 | fn add(self, other: Self) -> Result<Self, Error>; |
86 | } |
87 | |
88 | impl OverflowOp for u64 { |
89 | fn mul(self, other: Self) -> Result<Self, Error> { |
90 | self.checked_mul(other).ok_or(err:Error::NumberOverflow) |
91 | } |
92 | fn add(self, other: Self) -> Result<Self, Error> { |
93 | self.checked_add(other).ok_or(err:Error::NumberOverflow) |
94 | } |
95 | } |
96 | |
97 | struct Parser<'a> { |
98 | iter: Chars<'a>, |
99 | src: &'a str, |
100 | current: (u64, u64), |
101 | } |
102 | |
103 | impl<'a> Parser<'a> { |
104 | fn off(&self) -> usize { |
105 | self.src.len() - self.iter.as_str().len() |
106 | } |
107 | |
108 | fn parse_first_char(&mut self) -> Result<Option<u64>, Error> { |
109 | let off = self.off(); |
110 | for c in self.iter.by_ref() { |
111 | match c { |
112 | '0' ..='9' => { |
113 | return Ok(Some(c as u64 - '0' as u64)); |
114 | } |
115 | c if c.is_whitespace() => continue, |
116 | _ => { |
117 | return Err(Error::NumberExpected(off)); |
118 | } |
119 | } |
120 | } |
121 | Ok(None) |
122 | } |
123 | fn parse_unit(&mut self, n: u64, start: usize, end: usize) |
124 | -> Result<(), Error> |
125 | { |
126 | let (mut sec, nsec) = match &self.src[start..end] { |
127 | "nanos" | "nsec" | "ns" => (0u64, n), |
128 | "usec" | "us" => (0u64, n.mul(1000)?), |
129 | "millis" | "msec" | "ms" => (0u64, n.mul(1_000_000)?), |
130 | "seconds" | "second" | "secs" | "sec" | "s" => (n, 0), |
131 | "minutes" | "minute" | "min" | "mins" | "m" |
132 | => (n.mul(60)?, 0), |
133 | "hours" | "hour" | "hr" | "hrs" | "h" => (n.mul(3600)?, 0), |
134 | "days" | "day" | "d" => (n.mul(86400)?, 0), |
135 | "weeks" | "week" | "w" => (n.mul(86400*7)?, 0), |
136 | "months" | "month" | "M" => (n.mul(2_630_016)?, 0), // 30.44d |
137 | "years" | "year" | "y" => (n.mul(31_557_600)?, 0), // 365.25d |
138 | _ => { |
139 | return Err(Error::UnknownUnit { |
140 | start, end, |
141 | unit: self.src[start..end].to_string(), |
142 | value: n, |
143 | }); |
144 | } |
145 | }; |
146 | let mut nsec = self.current.1.add(nsec)?; |
147 | if nsec > 1_000_000_000 { |
148 | sec = sec.add(nsec / 1_000_000_000)?; |
149 | nsec %= 1_000_000_000; |
150 | } |
151 | sec = self.current.0.add(sec)?; |
152 | self.current = (sec, nsec); |
153 | Ok(()) |
154 | } |
155 | |
156 | fn parse(mut self) -> Result<Duration, Error> { |
157 | let mut n = self.parse_first_char()?.ok_or(Error::Empty)?; |
158 | 'outer: loop { |
159 | let mut off = self.off(); |
160 | while let Some(c) = self.iter.next() { |
161 | match c { |
162 | '0' ..='9' => { |
163 | n = n.checked_mul(10) |
164 | .and_then(|x| x.checked_add(c as u64 - '0' as u64)) |
165 | .ok_or(Error::NumberOverflow)?; |
166 | } |
167 | c if c.is_whitespace() => {} |
168 | 'a' ..='z' | 'A' ..='Z' => { |
169 | break; |
170 | } |
171 | _ => { |
172 | return Err(Error::InvalidCharacter(off)); |
173 | } |
174 | } |
175 | off = self.off(); |
176 | } |
177 | let start = off; |
178 | let mut off = self.off(); |
179 | while let Some(c) = self.iter.next() { |
180 | match c { |
181 | '0' ..='9' => { |
182 | self.parse_unit(n, start, off)?; |
183 | n = c as u64 - '0' as u64; |
184 | continue 'outer; |
185 | } |
186 | c if c.is_whitespace() => break, |
187 | 'a' ..='z' | 'A' ..='Z' => {} |
188 | _ => { |
189 | return Err(Error::InvalidCharacter(off)); |
190 | } |
191 | } |
192 | off = self.off(); |
193 | } |
194 | self.parse_unit(n, start, off)?; |
195 | n = match self.parse_first_char()? { |
196 | Some(n) => n, |
197 | None => return Ok( |
198 | Duration::new(self.current.0, self.current.1 as u32)), |
199 | }; |
200 | } |
201 | } |
202 | |
203 | } |
204 | |
205 | /// Parse duration object `1hour 12min 5s` |
206 | /// |
207 | /// The duration object is a concatenation of time spans. Where each time |
208 | /// span is an integer number and a suffix. Supported suffixes: |
209 | /// |
210 | /// * `nsec`, `ns` -- nanoseconds |
211 | /// * `usec`, `us` -- microseconds |
212 | /// * `msec`, `ms` -- milliseconds |
213 | /// * `seconds`, `second`, `sec`, `s` |
214 | /// * `minutes`, `minute`, `min`, `m` |
215 | /// * `hours`, `hour`, `hr`, `h` |
216 | /// * `days`, `day`, `d` |
217 | /// * `weeks`, `week`, `w` |
218 | /// * `months`, `month`, `M` -- defined as 30.44 days |
219 | /// * `years`, `year`, `y` -- defined as 365.25 days |
220 | /// |
221 | /// # Examples |
222 | /// |
223 | /// ``` |
224 | /// use std::time::Duration; |
225 | /// use humantime::parse_duration; |
226 | /// |
227 | /// assert_eq!(parse_duration("2h 37min" ), Ok(Duration::new(9420, 0))); |
228 | /// assert_eq!(parse_duration("32ms" ), Ok(Duration::new(0, 32_000_000))); |
229 | /// ``` |
230 | pub fn parse_duration(s: &str) -> Result<Duration, Error> { |
231 | Parser { |
232 | iter: s.chars(), |
233 | src: s, |
234 | current: (0, 0), |
235 | }.parse() |
236 | } |
237 | |
238 | /// Formats duration into a human-readable string |
239 | /// |
240 | /// Note: this format is guaranteed to have same value when using |
241 | /// parse_duration, but we can change some details of the exact composition |
242 | /// of the value. |
243 | /// |
244 | /// # Examples |
245 | /// |
246 | /// ``` |
247 | /// use std::time::Duration; |
248 | /// use humantime::format_duration; |
249 | /// |
250 | /// let val1 = Duration::new(9420, 0); |
251 | /// assert_eq!(format_duration(val1).to_string(), "2h 37m" ); |
252 | /// let val2 = Duration::new(0, 32_000_000); |
253 | /// assert_eq!(format_duration(val2).to_string(), "32ms" ); |
254 | /// ``` |
255 | pub fn format_duration(val: Duration) -> FormattedDuration { |
256 | FormattedDuration(val) |
257 | } |
258 | |
259 | fn item_plural(f: &mut fmt::Formatter, started: &mut bool, |
260 | name: &str, value: u64) |
261 | -> fmt::Result |
262 | { |
263 | if value > 0 { |
264 | if *started { |
265 | f.write_str(data:" " )?; |
266 | } |
267 | write!(f, " {}{}" , value, name)?; |
268 | if value > 1 { |
269 | f.write_str(data:"s" )?; |
270 | } |
271 | *started = true; |
272 | } |
273 | Ok(()) |
274 | } |
275 | fn item(f: &mut fmt::Formatter, started: &mut bool, name: &str, value: u32) |
276 | -> fmt::Result |
277 | { |
278 | if value > 0 { |
279 | if *started { |
280 | f.write_str(data:" " )?; |
281 | } |
282 | write!(f, " {}{}" , value, name)?; |
283 | *started = true; |
284 | } |
285 | Ok(()) |
286 | } |
287 | |
288 | impl FormattedDuration { |
289 | /// Returns a reference to the [`Duration`][] that is being formatted. |
290 | pub fn get_ref(&self) -> &Duration { |
291 | &self.0 |
292 | } |
293 | } |
294 | |
295 | impl fmt::Display for FormattedDuration { |
296 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
297 | let secs = self.0.as_secs(); |
298 | let nanos = self.0.subsec_nanos(); |
299 | |
300 | if secs == 0 && nanos == 0 { |
301 | f.write_str("0s" )?; |
302 | return Ok(()); |
303 | } |
304 | |
305 | let years = secs / 31_557_600; // 365.25d |
306 | let ydays = secs % 31_557_600; |
307 | let months = ydays / 2_630_016; // 30.44d |
308 | let mdays = ydays % 2_630_016; |
309 | let days = mdays / 86400; |
310 | let day_secs = mdays % 86400; |
311 | let hours = day_secs / 3600; |
312 | let minutes = day_secs % 3600 / 60; |
313 | let seconds = day_secs % 60; |
314 | |
315 | let millis = nanos / 1_000_000; |
316 | let micros = nanos / 1000 % 1000; |
317 | let nanosec = nanos % 1000; |
318 | |
319 | let ref mut started = false; |
320 | item_plural(f, started, "year" , years)?; |
321 | item_plural(f, started, "month" , months)?; |
322 | item_plural(f, started, "day" , days)?; |
323 | item(f, started, "h" , hours as u32)?; |
324 | item(f, started, "m" , minutes as u32)?; |
325 | item(f, started, "s" , seconds as u32)?; |
326 | item(f, started, "ms" , millis)?; |
327 | item(f, started, "us" , micros)?; |
328 | item(f, started, "ns" , nanosec)?; |
329 | Ok(()) |
330 | } |
331 | } |
332 | |
333 | #[cfg (test)] |
334 | mod test { |
335 | use std::time::Duration; |
336 | |
337 | use rand::Rng; |
338 | |
339 | use super::{parse_duration, format_duration}; |
340 | use super::Error; |
341 | |
342 | #[test ] |
343 | #[allow (clippy::cognitive_complexity)] |
344 | fn test_units() { |
345 | assert_eq!(parse_duration("17nsec" ), Ok(Duration::new(0, 17))); |
346 | assert_eq!(parse_duration("17nanos" ), Ok(Duration::new(0, 17))); |
347 | assert_eq!(parse_duration("33ns" ), Ok(Duration::new(0, 33))); |
348 | assert_eq!(parse_duration("3usec" ), Ok(Duration::new(0, 3000))); |
349 | assert_eq!(parse_duration("78us" ), Ok(Duration::new(0, 78000))); |
350 | assert_eq!(parse_duration("31msec" ), Ok(Duration::new(0, 31_000_000))); |
351 | assert_eq!(parse_duration("31millis" ), Ok(Duration::new(0, 31_000_000))); |
352 | assert_eq!(parse_duration("6ms" ), Ok(Duration::new(0, 6_000_000))); |
353 | assert_eq!(parse_duration("3000s" ), Ok(Duration::new(3000, 0))); |
354 | assert_eq!(parse_duration("300sec" ), Ok(Duration::new(300, 0))); |
355 | assert_eq!(parse_duration("300secs" ), Ok(Duration::new(300, 0))); |
356 | assert_eq!(parse_duration("50seconds" ), Ok(Duration::new(50, 0))); |
357 | assert_eq!(parse_duration("1second" ), Ok(Duration::new(1, 0))); |
358 | assert_eq!(parse_duration("100m" ), Ok(Duration::new(6000, 0))); |
359 | assert_eq!(parse_duration("12min" ), Ok(Duration::new(720, 0))); |
360 | assert_eq!(parse_duration("12mins" ), Ok(Duration::new(720, 0))); |
361 | assert_eq!(parse_duration("1minute" ), Ok(Duration::new(60, 0))); |
362 | assert_eq!(parse_duration("7minutes" ), Ok(Duration::new(420, 0))); |
363 | assert_eq!(parse_duration("2h" ), Ok(Duration::new(7200, 0))); |
364 | assert_eq!(parse_duration("7hr" ), Ok(Duration::new(25200, 0))); |
365 | assert_eq!(parse_duration("7hrs" ), Ok(Duration::new(25200, 0))); |
366 | assert_eq!(parse_duration("1hour" ), Ok(Duration::new(3600, 0))); |
367 | assert_eq!(parse_duration("24hours" ), Ok(Duration::new(86400, 0))); |
368 | assert_eq!(parse_duration("1day" ), Ok(Duration::new(86400, 0))); |
369 | assert_eq!(parse_duration("2days" ), Ok(Duration::new(172_800, 0))); |
370 | assert_eq!(parse_duration("365d" ), Ok(Duration::new(31_536_000, 0))); |
371 | assert_eq!(parse_duration("1week" ), Ok(Duration::new(604_800, 0))); |
372 | assert_eq!(parse_duration("7weeks" ), Ok(Duration::new(4_233_600, 0))); |
373 | assert_eq!(parse_duration("52w" ), Ok(Duration::new(31_449_600, 0))); |
374 | assert_eq!(parse_duration("1month" ), Ok(Duration::new(2_630_016, 0))); |
375 | assert_eq!(parse_duration("3months" ), Ok(Duration::new(3*2_630_016, 0))); |
376 | assert_eq!(parse_duration("12M" ), Ok(Duration::new(31_560_192, 0))); |
377 | assert_eq!(parse_duration("1year" ), Ok(Duration::new(31_557_600, 0))); |
378 | assert_eq!(parse_duration("7years" ), Ok(Duration::new(7*31_557_600, 0))); |
379 | assert_eq!(parse_duration("17y" ), Ok(Duration::new(536_479_200, 0))); |
380 | } |
381 | |
382 | #[test ] |
383 | fn test_combo() { |
384 | assert_eq!(parse_duration("20 min 17 nsec " ), Ok(Duration::new(1200, 17))); |
385 | assert_eq!(parse_duration("2h 15m" ), Ok(Duration::new(8100, 0))); |
386 | } |
387 | |
388 | #[test ] |
389 | fn all_86400_seconds() { |
390 | for second in 0..86400 { // scan leap year and non-leap year |
391 | let d = Duration::new(second, 0); |
392 | assert_eq!(d, |
393 | parse_duration(&format_duration(d).to_string()).unwrap()); |
394 | } |
395 | } |
396 | |
397 | #[test ] |
398 | fn random_second() { |
399 | for _ in 0..10000 { |
400 | let sec = rand::thread_rng().gen_range(0, 253_370_764_800); |
401 | let d = Duration::new(sec, 0); |
402 | assert_eq!(d, |
403 | parse_duration(&format_duration(d).to_string()).unwrap()); |
404 | } |
405 | } |
406 | |
407 | #[test ] |
408 | fn random_any() { |
409 | for _ in 0..10000 { |
410 | let sec = rand::thread_rng().gen_range(0, 253_370_764_800); |
411 | let nanos = rand::thread_rng().gen_range(0, 1_000_000_000); |
412 | let d = Duration::new(sec, nanos); |
413 | assert_eq!(d, |
414 | parse_duration(&format_duration(d).to_string()).unwrap()); |
415 | } |
416 | } |
417 | |
418 | #[test ] |
419 | fn test_overlow() { |
420 | // Overflow on subseconds is earlier because of how we do conversion |
421 | // we could fix it, but I don't see any good reason for this |
422 | assert_eq!(parse_duration("100000000000000000000ns" ), |
423 | Err(Error::NumberOverflow)); |
424 | assert_eq!(parse_duration("100000000000000000us" ), |
425 | Err(Error::NumberOverflow)); |
426 | assert_eq!(parse_duration("100000000000000ms" ), |
427 | Err(Error::NumberOverflow)); |
428 | |
429 | assert_eq!(parse_duration("100000000000000000000s" ), |
430 | Err(Error::NumberOverflow)); |
431 | assert_eq!(parse_duration("10000000000000000000m" ), |
432 | Err(Error::NumberOverflow)); |
433 | assert_eq!(parse_duration("1000000000000000000h" ), |
434 | Err(Error::NumberOverflow)); |
435 | assert_eq!(parse_duration("100000000000000000d" ), |
436 | Err(Error::NumberOverflow)); |
437 | assert_eq!(parse_duration("10000000000000000w" ), |
438 | Err(Error::NumberOverflow)); |
439 | assert_eq!(parse_duration("1000000000000000M" ), |
440 | Err(Error::NumberOverflow)); |
441 | assert_eq!(parse_duration("10000000000000y" ), |
442 | Err(Error::NumberOverflow)); |
443 | } |
444 | |
445 | #[test ] |
446 | fn test_nice_error_message() { |
447 | assert_eq!(parse_duration("123" ).unwrap_err().to_string(), |
448 | "time unit needed, for example 123sec or 123ms" ); |
449 | assert_eq!(parse_duration("10 months 1" ).unwrap_err().to_string(), |
450 | "time unit needed, for example 1sec or 1ms" ); |
451 | assert_eq!(parse_duration("10nights" ).unwrap_err().to_string(), |
452 | "unknown time unit \"nights \", supported units: \ |
453 | ns, us, ms, sec, min, hours, days, weeks, months, \ |
454 | years (and few variations)" ); |
455 | } |
456 | } |
457 | |