1use std::error::Error as StdError;
2use std::fmt;
3use std::str::Chars;
4use std::time::Duration;
5
6/// Error parsing human-friendly duration
7#[derive(Debug, PartialEq, Clone)]
8pub 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
51impl StdError for Error {}
52
53impl 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)]
81pub struct FormattedDuration(Duration);
82
83trait OverflowOp: Sized {
84 fn mul(self, other: Self) -> Result<Self, Error>;
85 fn add(self, other: Self) -> Result<Self, Error>;
86}
87
88impl 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
97struct Parser<'a> {
98 iter: Chars<'a>,
99 src: &'a str,
100 current: (u64, u64),
101}
102
103impl<'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/// ```
230pub 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/// ```
255pub fn format_duration(val: Duration) -> FormattedDuration {
256 FormattedDuration(val)
257}
258
259fn 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}
275fn 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
288impl 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
295impl 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)]
334mod 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