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(Error::NumberOverflow) 91 } 92 fn add(self, other: Self) -> Result<Self, Error> { 93 self.checked_add(other).ok_or(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(" ")?; 266 } 267 write!(f, "{}{}", value, name)?; 268 if value > 1 { 269 f.write_str("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(" ")?; 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