| 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 | |