use std::fmt; use std::str::Chars; use std::time::Duration; use std::error::Error as StdError; quick_error! { /// Error parsing human-friendly duration #[derive(Debug, PartialEq, Clone, Copy)] pub enum Error { /// Invalid character during parsing /// /// More specifically anything that is not alphanumeric is prohibited /// /// The field is an byte offset of the character in the string. InvalidCharacter(offset: usize) { display("invalid character at {}", offset) description("invalid character") } /// Non-numeric value where number is expected /// /// This usually means that either time unit is broken into words, /// e.g. `m sec` instead of `msec`, or just number is omitted, /// for example `2 hours min` instead of `2 hours 1 min` /// /// The field is an byte offset of the errorneous character /// in the string. NumberExpected(offset: usize) { display("expected number at {}", offset) description("expected number") } /// Unit in the number is not one of allowed units /// /// See documentation of `parse_duration` for the list of supported /// time units. /// /// The two fields are start and end (exclusive) of the slice from /// the original string, containing errorneous value UnknownUnit(start: usize, end: usize) { display("unknown unit at {}-{}", start, end) description("unknown unit") } /// The numeric value is too large /// /// Usually this means value is too large to be useful. If user writes /// data in subsecond units, then the maximum is about 3k years. When /// using seconds, or larger units, the limit is even larger. NumberOverflow { display(self_) -> ("{}", self_.description()) description("number is too large") } /// The value was an empty string (or consists only whitespace) Empty { display(self_) -> ("{}", self_.description()) description("value was empty") } } } /// A wrapper type that allows you to Display a Duration #[derive(Debug, Clone)] pub struct FormattedDuration(Duration); trait OverflowOp: Sized { fn mul(self, other: Self) -> Result; fn add(self, other: Self) -> Result; } impl OverflowOp for u64 { fn mul(self, other: Self) -> Result { self.checked_mul(other).ok_or(Error::NumberOverflow) } fn add(self, other: Self) -> Result { self.checked_add(other).ok_or(Error::NumberOverflow) } } struct Parser<'a> { iter: Chars<'a>, src: &'a str, current: (u64, u64), } impl<'a> Parser<'a> { fn off(&self) -> usize { self.src.len() - self.iter.as_str().len() } fn parse_first_char(&mut self) -> Result, Error> { let off = self.off(); for c in self.iter.by_ref() { match c { '0'...'9' => { return Ok(Some(c as u64 - '0' as u64)); } c if c.is_whitespace() => continue, _ => { return Err(Error::NumberExpected(off)); } } } return Ok(None); } fn parse_unit(&mut self, n: u64, start: usize, end: usize) -> Result<(), Error> { let (mut sec, nsec) = match &self.src[start..end] { "nanos" | "nsec" | "ns" => (0u64, n), "usec" | "us" => (0u64, try!(n.mul(1000))), "millis" | "msec" | "ms" => (0u64, try!(n.mul(1000_000))), "seconds" | "second" | "secs" | "sec" | "s" => (n, 0), "minutes" | "minute" | "min" | "mins" | "m" => (try!(n.mul(60)), 0), "hours" | "hour" | "hr" | "hrs" | "h" => (try!(n.mul(3600)), 0), "days" | "day" | "d" => (try!(n.mul(86400)), 0), "weeks" | "week" | "w" => (try!(n.mul(86400*7)), 0), "months" | "month" | "M" => (try!(n.mul(2630016)), 0), // 30.44d "years" | "year" | "y" => (try!(n.mul(31557600)), 0), // 365.25d _ => return Err(Error::UnknownUnit(start, end)), }; let mut nsec = try!(self.current.1.add(nsec)); if nsec > 1000_000_000 { sec = try!(sec.add(nsec / 1000_000_000)); nsec %= 1000_000_000; } sec = try!(self.current.0.add(sec)); self.current = (sec, nsec); Ok(()) } fn parse(mut self) -> Result { let mut n = try!(try!(self.parse_first_char()).ok_or(Error::Empty)); 'outer: loop { let mut off = self.off(); while let Some(c) = self.iter.next() { match c { '0'...'9' => { n = try!(n.checked_mul(10) .and_then(|x| x.checked_add(c as u64 - '0' as u64)) .ok_or(Error::NumberOverflow)); } c if c.is_whitespace() => {} 'a'...'z' | 'A'...'Z' => { break; } _ => { return Err(Error::InvalidCharacter(off)); } } off = self.off(); } let start = off; let mut off = self.off(); while let Some(c) = self.iter.next() { match c { '0'...'9' => { try!(self.parse_unit(n, start, off)); n = c as u64 - '0' as u64; continue 'outer; } c if c.is_whitespace() => break, 'a'...'z' | 'A'...'Z' => {} _ => { return Err(Error::InvalidCharacter(off)); } } off = self.off(); } try!(self.parse_unit(n, start, off)); n = match try!(self.parse_first_char()) { Some(n) => n, None => return Ok( Duration::new(self.current.0, self.current.1 as u32)), }; } } } /// Parse duration object `1hour 12min 5s` /// /// The duration object is a concatenation of time spans. Where each time /// span is an integer number and a suffix. Supported suffixes: /// /// * `nsec`, `ns` -- microseconds /// * `usec`, `us` -- microseconds /// * `msec`, `ms` -- milliseconds /// * `seconds`, `second`, `sec`, `s` /// * `minutes`, `minute`, `min`, `m` /// * `hours`, `hour`, `hr`, `h` /// * `days`, `day`, `d` /// * `weeks`, `week`, `w` /// * `months`, `month`, `M` -- defined as 30.44 days /// * `years`, `year`, `y` -- defined as 365.25 days /// /// # Examples /// /// ``` /// use std::time::Duration; /// use humantime::parse_duration; /// /// assert_eq!(parse_duration("2h 37min"), Ok(Duration::new(9420, 0))); /// assert_eq!(parse_duration("32ms"), Ok(Duration::new(0, 32_000_000))); /// ``` pub fn parse_duration(s: &str) -> Result { Parser { iter: s.chars(), src: s, current: (0, 0), }.parse() } /// Formats duration into a human-readable string /// /// Note: this format is guaranteed to have same value when using /// parse_duration, but we can change some details of the exact composition /// of the value. /// /// # Examples /// /// ``` /// use std::time::Duration; /// use humantime::format_duration; /// /// let val1 = Duration::new(9420, 0); /// assert_eq!(format_duration(val1).to_string(), "2h 37m"); /// let val2 = Duration::new(0, 32_000_000); /// assert_eq!(format_duration(val2).to_string(), "32ms"); /// ``` pub fn format_duration(val: Duration) -> FormattedDuration { FormattedDuration(val) } fn item_plural(f: &mut fmt::Formatter, started: &mut bool, name: &str, value: u64) -> fmt::Result { if value > 0 { if *started { f.write_str(" ")?; } write!(f, "{}{}", value, name)?; if value > 1 { f.write_str("s")?; } *started = true; } Ok(()) } fn item(f: &mut fmt::Formatter, started: &mut bool, name: &str, value: u32) -> fmt::Result { if value > 0 { if *started { f.write_str(" ")?; } write!(f, "{}{}", value, name)?; *started = true; } Ok(()) } impl fmt::Display for FormattedDuration { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let secs = self.0.as_secs(); let nanos = self.0.subsec_nanos(); if secs == 0 && nanos == 0 { f.write_str("0s")?; return Ok(()); } let years = secs / 31557600; // 365.25d let ydays = secs % 31557600; let months = ydays / 2630016; // 30.44d let mdays = ydays % 2630016; let days = mdays / 86400; let day_secs = mdays % 86400; let hours = day_secs / 3600; let minutes = day_secs % 3600 / 60; let seconds = day_secs % 60; let millis = nanos / 1_000_000; let micros = nanos / 1000 % 1000; let nanosec = nanos % 1000; let ref mut started = false; item_plural(f, started, "year", years)?; item_plural(f, started, "month", months)?; item_plural(f, started, "day", days)?; item(f, started, "h", hours as u32)?; item(f, started, "m", minutes as u32)?; item(f, started, "s", seconds as u32)?; item(f, started, "ms", millis)?; item(f, started, "us", micros)?; item(f, started, "ns", nanosec)?; Ok(()) } } #[cfg(test)] mod test { extern crate rand; use std::time::Duration; use self::rand::Rng; use super::{parse_duration, format_duration}; use super::Error; #[test] fn test_units() { assert_eq!(parse_duration("17nsec"), Ok(Duration::new(0, 17))); assert_eq!(parse_duration("17nanos"), Ok(Duration::new(0, 17))); assert_eq!(parse_duration("33ns"), Ok(Duration::new(0, 33))); assert_eq!(parse_duration("3usec"), Ok(Duration::new(0, 3000))); assert_eq!(parse_duration("78us"), Ok(Duration::new(0, 78000))); assert_eq!(parse_duration("31msec"), Ok(Duration::new(0, 31000000))); assert_eq!(parse_duration("31millis"), Ok(Duration::new(0, 31000000))); assert_eq!(parse_duration("6ms"), Ok(Duration::new(0, 6000000))); assert_eq!(parse_duration("3000s"), Ok(Duration::new(3000, 0))); assert_eq!(parse_duration("300sec"), Ok(Duration::new(300, 0))); assert_eq!(parse_duration("300secs"), Ok(Duration::new(300, 0))); assert_eq!(parse_duration("50seconds"), Ok(Duration::new(50, 0))); assert_eq!(parse_duration("1second"), Ok(Duration::new(1, 0))); assert_eq!(parse_duration("100m"), Ok(Duration::new(6000, 0))); assert_eq!(parse_duration("12min"), Ok(Duration::new(720, 0))); assert_eq!(parse_duration("12mins"), Ok(Duration::new(720, 0))); assert_eq!(parse_duration("1minute"), Ok(Duration::new(60, 0))); assert_eq!(parse_duration("7minutes"), Ok(Duration::new(420, 0))); assert_eq!(parse_duration("2h"), Ok(Duration::new(7200, 0))); assert_eq!(parse_duration("7hr"), Ok(Duration::new(25200, 0))); assert_eq!(parse_duration("7hrs"), Ok(Duration::new(25200, 0))); assert_eq!(parse_duration("1hour"), Ok(Duration::new(3600, 0))); assert_eq!(parse_duration("24hours"), Ok(Duration::new(86400, 0))); assert_eq!(parse_duration("1day"), Ok(Duration::new(86400, 0))); assert_eq!(parse_duration("2days"), Ok(Duration::new(172800, 0))); assert_eq!(parse_duration("365d"), Ok(Duration::new(31536000, 0))); assert_eq!(parse_duration("1week"), Ok(Duration::new(604800, 0))); assert_eq!(parse_duration("7weeks"), Ok(Duration::new(4233600, 0))); assert_eq!(parse_duration("52w"), Ok(Duration::new(31449600, 0))); assert_eq!(parse_duration("1month"), Ok(Duration::new(2630016, 0))); assert_eq!(parse_duration("3months"), Ok(Duration::new(3*2630016, 0))); assert_eq!(parse_duration("12M"), Ok(Duration::new(31560192, 0))); assert_eq!(parse_duration("1year"), Ok(Duration::new(31557600, 0))); assert_eq!(parse_duration("7years"), Ok(Duration::new(7*31557600, 0))); assert_eq!(parse_duration("17y"), Ok(Duration::new(536479200, 0))); } #[test] fn test_combo() { assert_eq!(parse_duration("20 min 17 nsec "), Ok(Duration::new(1200, 17))); assert_eq!(parse_duration("2h 15m"), Ok(Duration::new(8100, 0))); } #[test] fn all_86400_seconds() { for second in 0..86400 { // scan leap year and non-leap year let d = Duration::new(second, 0); assert_eq!(d, parse_duration(&format_duration(d).to_string()).unwrap()); } } #[test] fn random_second() { for _ in 0..10000 { let sec = rand::thread_rng().gen_range(0, 253370764800); let d = Duration::new(sec, 0); assert_eq!(d, parse_duration(&format_duration(d).to_string()).unwrap()); } } #[test] fn random_any() { for _ in 0..10000 { let sec = rand::thread_rng().gen_range(0, 253370764800); let nanos = rand::thread_rng().gen_range(0, 1_000_000_000); let d = Duration::new(sec, nanos); assert_eq!(d, parse_duration(&format_duration(d).to_string()).unwrap()); } } #[test] fn test_overlow() { // Overflow on subseconds is earlier because of how we do conversion // we could fix it, but I don't see any good reason for this assert_eq!(parse_duration("100000000000000000000ns"), Err(Error::NumberOverflow)); assert_eq!(parse_duration("100000000000000000us"), Err(Error::NumberOverflow)); assert_eq!(parse_duration("100000000000000ms"), Err(Error::NumberOverflow)); assert_eq!(parse_duration("100000000000000000000s"), Err(Error::NumberOverflow)); assert_eq!(parse_duration("10000000000000000000m"), Err(Error::NumberOverflow)); assert_eq!(parse_duration("1000000000000000000h"), Err(Error::NumberOverflow)); assert_eq!(parse_duration("100000000000000000d"), Err(Error::NumberOverflow)); assert_eq!(parse_duration("10000000000000000w"), Err(Error::NumberOverflow)); assert_eq!(parse_duration("1000000000000000M"), Err(Error::NumberOverflow)); assert_eq!(parse_duration("10000000000000y"), Err(Error::NumberOverflow)); } }