Implement duration formatting (fixes #1)

This commit is contained in:
Paul Colomiets 2018-02-16 02:00:32 +02:00
parent 3684288f3d
commit 28d451af66
4 changed files with 209 additions and 66 deletions

View File

@ -28,6 +28,7 @@ enum Precision {
Nanos,
}
/// A wrapper type that allows you to Display a SystemTime
#[derive(Debug)]
pub struct Rfc3339Timestamp(SystemTime, Precision);

View File

@ -1,3 +1,4 @@
use std::fmt;
use std::str::Chars;
use std::time::Duration;
use std::error::Error as StdError;
@ -53,8 +54,13 @@ quick_error! {
description("value was empty")
}
}
}
/// A wrapper type that allows you to Display a Duration
#[derive(Debug)]
pub struct FormattedDuration(Duration);
trait OverflowOp: Sized {
fn mul(self, other: Self) -> Result<Self, Error>;
fn add(self, other: Self) -> Result<Self, Error>;
@ -204,74 +210,202 @@ pub fn parse_duration(s: &str) -> Result<Duration, Error> {
}.parse()
}
#[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)));
/// 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)
}
#[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)));
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(())
}
#[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));
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();
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));
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));
}
}

View File

@ -3,6 +3,7 @@
//! Features:
//!
//! * Parses durations in free form like `15days 2min 2s`
//! * Formats durations in similar form `2years 2min 12us`
//! * Parses and formats timestamp in `rfc3339` format: `2018-01-01T12:53:00Z`
//! * Parses timestamps in a weaker format: `2018-01-01 12:53:00`
//!
@ -22,6 +23,8 @@ mod wrapper;
mod date;
pub use duration::{parse_duration, Error as DurationError};
pub use duration::{format_duration, FormattedDuration};
pub use wrapper::{Duration, Timestamp};
pub use date::{parse_rfc3339, parse_rfc3339_weak, Error as TimestampError};
pub use date::{format_rfc3339, format_rfc3339_seconds, format_rfc3339_nanos};
pub use date::{Rfc3339Timestamp};

View File

@ -3,7 +3,7 @@ use std::ops::Deref;
use std::fmt;
use std::time::{Duration as StdDuration, SystemTime};
use duration::{self, parse_duration};
use duration::{self, parse_duration, format_duration};
use date::{self, parse_rfc3339_weak, format_rfc3339};
/// A wrapper for duration that has `FromStr` implementation
@ -70,6 +70,11 @@ impl FromStr for Duration {
}
}
impl fmt::Display for Duration {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
format_duration(self.0).fmt(f)
}
}
impl AsRef<SystemTime> for Timestamp {
fn as_ref(&self) -> &SystemTime { &self.0 }