Skip to content

Commit

Permalink
Add parse-datetime crate
Browse files Browse the repository at this point in the history
  • Loading branch information
zmrow committed Mar 27, 2020
1 parent 5e78fc5 commit bbb96c1
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 35 deletions.
1 change: 1 addition & 0 deletions packages/os/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ variant-sensitive = true
source-groups = [
"api",
"bottlerocket-release",
"parse-datetime",
"growpart",
"updater",
"webpki-roots-shim",
Expand Down
80 changes: 45 additions & 35 deletions sources/Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions sources/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ members = [

"models",

"parse-datetime",

"preinit/laika",

"updater/block-party",
Expand Down
14 changes: 14 additions & 0 deletions sources/parse-datetime/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "parse-datetime"
version = "0.1.0"
authors = ["Zac Mrowicki <mrowicki@amazon.com>"]
license = "Apache-2.0 OR MIT"
edition = "2018"
publish = false

[dependencies]
chrono = "0.4.11"
snafu = { version = "0.6.3", features = ["backtraces-impl-backtrace-crate"] }

[build-dependencies]
cargo-readme = "3.1"
25 changes: 25 additions & 0 deletions sources/parse-datetime/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# parse-datetime

Current version: 0.1.0

## Background

This library parses a `DateTime<Utc>` from a string.

The string can be:

* an `RFC3339` formatted date / time
* a string with the form `"in <unsigned integer> <unit(s)>"` where
* `<unsigned integer>` may be any unsigned integer and
* `<unit(s)>` may be either the singular or plural form of the following: `hour | hours`, `day | days`, `week | weeks`

Examples:

* `"in 1 hour"`
* `"in 2 hours"`
* `"in 6 days"`
* `"in 2 weeks"`

## Colophon

This text was generated from `README.tpl` using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/lib.rs`.
9 changes: 9 additions & 0 deletions sources/parse-datetime/README.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# {{crate}}

Current version: {{version}}

{{readme}}

## Colophon

This text was generated from `README.tpl` using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/lib.rs`.
45 changes: 45 additions & 0 deletions sources/parse-datetime/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Automatically generate README.md from rustdoc.

use std::env;
use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, PathBuf};

fn generate_readme() {
// Check for environment variable "SKIP_README". If it is set,
// skip README generation
if env::var_os("SKIP_README").is_some() {
return;
}

let mut source = File::open("src/lib.rs").unwrap();
let mut template = File::open("README.tpl").unwrap();

let content = cargo_readme::generate_readme(
&PathBuf::from("."), // root
&mut source, // source
Some(&mut template), // template
// The "add x" arguments don't apply when using a template.
true, // add title
false, // add badges
false, // add license
true, // indent headings
)
.unwrap();

let mut readme = File::create("README.md").unwrap();
readme.write_all(content.as_bytes()).unwrap();
}

fn generate_constants() {
let out_dir = env::var("OUT_DIR").unwrap();
let arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
let contents = format!("const ARCH: &str = \"{}\";", arch);
let path = Path::new(&out_dir).join("constants.rs");
fs::write(path, contents).unwrap();
}

fn main() {
generate_readme();
generate_constants();
}
130 changes: 130 additions & 0 deletions sources/parse-datetime/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*!
# Background
This library parses a `DateTime<Utc>` from a string.
The string can be:
* an `RFC3339` formatted date / time
* a string with the form `"in <unsigned integer> <unit(s)>"` where
* `<unsigned integer>` may be any unsigned integer and
* `<unit(s)>` may be either the singular or plural form of the following: `hour | hours`, `day | days`, `week | weeks`
Examples:
* `"in 1 hour"`
* `"in 2 hours"`
* `"in 6 days"`
* `"in 2 weeks"`
*/

use chrono::{DateTime, Duration, FixedOffset, Utc};
use snafu::{ensure, ResultExt};

mod error {
use snafu::Snafu;

#[derive(Debug, Snafu)]
#[snafu(visibility = "pub(super)")]
pub enum Error {
#[snafu(display("Date argument '{}' is invalid: {}", input, msg))]
DateArgInvalid { input: String, msg: &'static str },

#[snafu(display(
"Date argument had count '{}' that failed to parse as integer: {}",
input,
source
))]
DateArgCount {
input: String,
source: std::num::ParseIntError,
},
}
}
pub use error::Error;
type Result<T> = std::result::Result<T, error::Error>;

/// Parses a user-specified datetime, either in full RFC 3339 format, or a shorthand like "in 7
/// days"
pub fn parse_datetime(input: &str) -> Result<DateTime<Utc>> {
// If the user gave an absolute date in a standard format, accept it.
let try_dt: std::result::Result<DateTime<FixedOffset>, chrono::format::ParseError> =
DateTime::parse_from_rfc3339(input);
if let Ok(dt) = try_dt {
let utc = dt.into();
return Ok(utc);
}

// Otherwise, pull apart a request like "in 5 days" to get an exact datetime.
let mut parts: Vec<&str> = input.split_whitespace().collect();
ensure!(
parts.len() == 3,
error::DateArgInvalid {
input,
msg: "expected RFC 3339, or something like 'in 7 days'"
}
);
let unit_str = parts.pop().unwrap();
let count_str = parts.pop().unwrap();
let prefix_str = parts.pop().unwrap();

ensure!(
prefix_str == "in",
error::DateArgInvalid {
input,
msg: "expected RFC 3339, or prefix 'in', something like 'in 7 days'",
}
);

let count: u32 = count_str.parse().context(error::DateArgCount { input })?;

let duration = match unit_str {
"hour" | "hours" => Duration::hours(i64::from(count)),
"day" | "days" => Duration::days(i64::from(count)),
"week" | "weeks" => Duration::weeks(i64::from(count)),
_ => {
return error::DateArgInvalid {
input,
msg: "date argument's unit must be hours/days/weeks",
}
.fail();
}
};

let now = Utc::now();
let then = now + duration;
Ok(then)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_acceptable_strings() {
let inputs = vec![
"in 0 hours",
"in 1 hour",
"in 5000000 hours",
"in 0 days",
"in 1 day",
"in 5000000 days",
"in 0 weeks",
"in 1 week",
"in 5000000 weeks",
];

for input in inputs {
assert!(parse_datetime(input).is_ok())
}
}

#[test]
fn test_unacceptable_strings() {
let inputs = vec!["in", "0 hours", "hours", "in 1 month"];

for input in inputs {
assert!(parse_datetime(input).is_err())
}
}
}

0 comments on commit bbb96c1

Please sign in to comment.