From d1f32f797233ca05329cfc0059565d98a42f654b Mon Sep 17 00:00:00 2001 From: Paul Colomiets Date: Sat, 21 May 2016 00:01:11 +0300 Subject: [PATCH] Initial implementation --- .gitignore | 3 + Cargo.toml | 23 +++++ src/duration.rs | 242 ++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 10 ++ vagga.yaml | 35 +++++++ 5 files changed, 313 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/duration.rs create mode 100644 src/lib.rs create mode 100644 vagga.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a445b2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/Cargo.lock +/.vagga +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..883a9fd --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "humantime" +description = """ + A human friendly format parser for std::time::Duration. +""" +license = "MIT/Apache-2.0" +readme = "README.rst" +keywords = ["linux", "container", "namespace", "docker", "process"] +homepage = "http://github.com/tailhook/humantime" +documentation = "http://tailhook.github.io/humantime/" +version = "0.1.0" +authors = ["paul@colomiets.name"] + +[dependencies] +quick-error = "1.0.0" + +[dev-dependencies] +argparse = "0.2.1" + +[lib] +name = "humantime" +path = "src/lib.rs" + diff --git a/src/duration.rs b/src/duration.rs new file mode 100644 index 0000000..0e0094c --- /dev/null +++ b/src/duration.rs @@ -0,0 +1,242 @@ +use std::str::Chars; +use std::time::Duration; +use std::error::Error as StdError; + +quick_error! { + #[derive(Debug, PartialEq)] + pub enum Error { + InvalidCharacter(offset: usize) { + display("invalid character at {}", offset) + description("invalid character") + } + NumberExpected(offset: usize) { + display("expected number at {}", offset) + description("expected number") + } + UnknownUnit(start: usize, end: usize) { + display("unknown unit at {}-{}", start, end) + description("unknown unit") + } + NumberOverflow { + display(self_) -> ("{}", self_.description()) + description("number is too large") + } + Empty { + display(self_) -> ("{}", self_.description()) + description("value was empty") + } + } +} + +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] { + "nsec" | "ns" => (0u64, n), + "usec" | "us" => (0u64, try!(n.mul(1000))), + "msec" | "ms" => (0u64, try!(n.mul(1000_000))), + "seconds" | "second" | "sec" | "s" => (n, 0), + "minutes" | "minute" | "min" | "m" => (try!(n.mul(60)), 0), + "hours" | "hour" | "hr" | "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 +/// +/// 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))); +/// ``` +pub fn parse_duration(s: &str) -> Result { + Parser { + iter: s.chars(), + src: s, + current: (0, 0), + }.parse() +} + +#[test] +fn test_units() { + assert_eq!(parse_duration("17nsec"), 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("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("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("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("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 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)); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d3c9f17 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,10 @@ +//! Human-friendly time parser +//! +//! Currently this only currently implements parsing of duration. Relative and +//! absolute times may be added in future. + +#[macro_use] extern crate quick_error; + +mod duration; + +pub use duration::{parse_duration, Error as DurationError}; diff --git a/vagga.yaml b/vagga.yaml new file mode 100644 index 0000000..a41f006 --- /dev/null +++ b/vagga.yaml @@ -0,0 +1,35 @@ +commands: + + cargo: !Command + description: Run any cargo command + container: ubuntu + run: [cargo] + + make: !Command + description: Build the library + container: ubuntu + run: [cargo, build] + + test: !Command + description: Test the library + container: ubuntu + environ: { RUST_BACKTRACE: 1 } + run: [cargo, test] + +containers: + + ubuntu: + setup: + - !Ubuntu trusty + - !UbuntuUniverse ~ + - !Install [make, checkinstall, wget, ca-certificates, + libssl-dev, build-essential] + + - !TarInstall + url: "http://static.rust-lang.org/dist/rust-1.8.0-x86_64-unknown-linux-gnu.tar.gz" + script: "./install.sh --prefix=/usr \ + --components=rustc,rust-std-x86_64-unknown-linux-gnu,cargo" + + environ: + HOME: /work/target + USER: pc