Skip to content

Commit

Permalink
Initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
tailhook committed May 20, 2016
0 parents commit d1f32f7
Show file tree
Hide file tree
Showing 5 changed files with 313 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/Cargo.lock
/.vagga
/target
23 changes: 23 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"

242 changes: 242 additions & 0 deletions src/duration.rs
Original file line number Diff line number Diff line change
@@ -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<Self, Error>;
fn add(self, other: Self) -> Result<Self, Error>;
}

impl OverflowOp for u64 {
fn mul(self, other: Self) -> Result<Self, Error> {
self.checked_mul(other).ok_or(Error::NumberOverflow)
}
fn add(self, other: Self) -> Result<Self, Error> {
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<Option<u64>, 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<Duration, Error> {
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<Duration, Error> {
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));
}
10 changes: 10 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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};
35 changes: 35 additions & 0 deletions vagga.yaml
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit d1f32f7

Please sign in to comment.