24 Commits

Author SHA1 Message Date
Sylvestre Ledru bed2d16e67 Release version 0.4.0 2023-06-09 13:43:54 +02:00
Sylvestre Ledru 7ab1ad2f0e document the old name 2023-06-09 13:43:04 +02:00
Sylvestre Ledru c1a2bef4a6 Merge pull request #22 from sylvestre/rename
rename from humantime_to_duration to parse_datetime
2023-06-09 13:41:17 +02:00
Sylvestre Ledru 673d5b600e rename from humantime_to_duration to parse_datetime 2023-06-09 10:16:26 +02:00
Sylvestre Ledru 83e1431db0 Merge pull request #21 from sylvestre/fuzz
Also fuzz fuzz_parse_datetime_from_str for a minute
2023-06-08 11:27:07 +02:00
Sylvestre Ledru 80f8fdf883 README: syntax highlighting 2023-06-07 15:01:25 +02:00
Sylvestre Ledru 22bdd8f49c run the fuzzers in the CI 2023-06-07 13:52:00 +02:00
Sylvestre Ledru dc508c469b add a fuzzer for parse_datetime_from_str 2023-06-07 13:51:40 +02:00
Sylvestre Ledru e503de7559 Merge pull request #12 from Benjscho/main
add datetime parser
2023-06-07 10:03:17 +02:00
Ben Schofield 7ee33d1fb1 cargo fmt 2023-06-06 10:26:05 -07:00
Ben Schofield bc33770075 Update README
Update the README and add a test module to parse_datetime for any
examples presented in the README.
2023-06-06 10:05:14 -07:00
Sylvestre Ledru 70d3e53604 new release 2023-06-06 11:46:45 +02:00
Sylvestre Ledru 1ba787d1c3 Merge pull request #17 from cakebaker/downgrade_libc
Downgrade libc from 0.2.145 to 0.2.144
2023-06-06 08:02:47 +02:00
Daniel Hofstetter 33cefd7db2 Downgrade libc from 0.2.145 to 0.2.144 2023-06-06 07:49:14 +02:00
Sylvestre Ledru d0d913e161 Merge pull request #16 from uutils/renovate/regex-1.x
Update Rust crate regex to 1.8.4
2023-06-05 19:40:31 +02:00
renovate[bot] c784a721ee Update Rust crate regex to 1.8.4 2023-06-05 16:52:25 +00:00
Ben Schofield 29e7a5c335 Update src/parse_datetime.rs
Co-authored-by: Sylvestre Ledru <sledru@mozilla.com>
2023-06-05 08:04:22 -07:00
Ben Schofield 8d797fcbcb typo fix 2023-06-05 08:00:07 -07:00
Ben Schofield c177117428 Fixes for comments 2023-06-05 07:58:44 -07:00
Sylvestre Ledru 03c8e81a10 Merge pull request #15 from Redfire75369/patch-1
Added Syntax Highlighting to Example in ReadME
2023-06-05 14:32:06 +02:00
Redfire 7547f31b76 Added Syntax Highlighting to Example in ReadME 2023-06-05 20:22:57 +08:00
Sylvestre Ledru 1c32733f38 Merge pull request #14 from cakebaker/remove_time_crate
Adapt chrono settings to remove old time crate
2023-06-05 09:53:01 +02:00
Daniel Hofstetter 5278394b7c Adapt chrono settings to remove old time crate 2023-06-05 09:18:54 +02:00
Ben Schofield e53aabd914 add datetime parser
Add a relaxed datetime parser. This datetime parser functions by using `chrono`s
own parsing utilities and a try/succeed approach to parsing.

This implementation of the datetime parser has some drawbacks and some
positives. On the positive side:
- it was easy to implement
- it is easy to add more datetime formats to

In order to add additionally supported formats, a developer can add the
required format string to the `format` mod in `parse_datetime.rs`, and
then add it as a potential format to the relevant `fmts` vec.

On the negative:
- It is not easily customiseable beyond the supported `chrono` parsing
  formats. E.g., `chrono` does not currently support parsing offsets
  without trailing zeros. `from_str("UTC+1")` should return a valid response
  but `chrono` fails to parse this.
- Because it is an attempt driven parser, it is likely not that
  performant. I have not done any performance testing as part of this
change, but I would expect a custom parser to perform much better.
2023-06-03 09:58:36 -07:00
11 changed files with 357 additions and 61 deletions
+24
View File
@@ -164,3 +164,27 @@ jobs:
flags: ${{ steps.vars.outputs.CODECOV_FLAGS }}
name: codecov-umbrella
fail_ci_if_error: false
fuzz:
name: Run the fuzzers
runs-on: ubuntu-latest
env:
RUN_FOR: 60
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@nightly
- name: Install `cargo-fuzz`
run: cargo install cargo-fuzz
- uses: Swatinem/rust-cache@v2
- name: Run from_str for XX seconds
shell: bash
run: |
## Run it
cd fuzz
cargo +nightly fuzz run fuzz_from_str -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
- name: Run fuzz_parse_datetime_from_str for XX seconds
shell: bash
run: |
## Run it
cd fuzz
cargo +nightly fuzz run fuzz_parse_datetime_from_str -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
Generated
+10 -30
View File
@@ -58,10 +58,7 @@ checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"time",
"wasm-bindgen",
"winapi",
]
@@ -71,14 +68,6 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]]
name = "humantime_to_duration"
version = "0.3.0"
dependencies = [
"chrono",
"regex",
]
[[package]]
name = "iana-time-zone"
version = "0.1.56"
@@ -140,9 +129,17 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.17.2"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "parse_datetime"
version = "0.4.0"
dependencies = [
"chrono",
"regex",
]
[[package]]
name = "proc-macro2"
@@ -190,29 +187,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "time"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
dependencies = [
"libc",
"wasi",
"winapi",
]
[[package]]
name = "unicode-ident"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0"
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasm-bindgen"
version = "0.2.86"
+4 -4
View File
@@ -1,12 +1,12 @@
[package]
name = "humantime_to_duration"
name = "parse_datetime"
description = " parsing human-readable relative time strings and converting them to a Duration"
version = "0.3.0"
version = "0.4.0"
edition = "2021"
license = "MIT"
repository = "https://github.com/uutils/humantime_to_duration"
repository = "https://github.com/uutils/parse_datetime"
readme = "README.md"
[dependencies]
regex = "1.8"
chrono = "0.4"
chrono = { version="0.4", default-features=false, features=["std", "alloc", "clock"] }
+32 -9
View File
@@ -1,14 +1,14 @@
# humantime_to_duration
# parse_datetime
[![Crates.io](https://img.shields.io/crates/v/humantime_to_duration.svg)](https://crates.io/crates/humantime_to_duration)
[![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/uutils/humantime_to_duration/blob/main/LICENSE)
[![CodeCov](https://codecov.io/gh/uutils/humantime_to_duration/branch/main/graph/badge.svg)](https://codecov.io/gh/uutils/humantime_to_duration)
[![Crates.io](https://img.shields.io/crates/v/parse_datetime.svg)](https://crates.io/crates/parse_datetime)
[![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/uutils/parse_datetime/blob/main/LICENSE)
[![CodeCov](https://codecov.io/gh/uutils/parse_datetime/branch/main/graph/badge.svg)](https://codecov.io/gh/uutils/parse_datetime)
A Rust crate for parsing human-readable relative time strings and converting them to a `Duration`.
A Rust crate for parsing human-readable relative time strings and converting them to a `Duration`, or parsing human-readable datetime strings and converting them to a `DateTime`.
## Features
- Parses a variety of human-readable time formats.
- Parses a variety of human-readable and standard time formats.
- Supports positive and negative durations.
- Allows for chaining time units (e.g., "1 hour 2 minutes" or "2 days and 2 hours").
- Calculate durations relative to a specified date.
@@ -20,12 +20,12 @@ Add this to your `Cargo.toml`:
```toml
[dependencies]
humantime_to_duration = "0.3.0"
parse_datetime = "0.4.0"
```
Then, import the crate and use the `from_str` and `from_str_at_date` functions:
```
use humantime_to_duration::{from_str, from_str_at_date};
```rs
use parse_datetime::{from_str, from_str_at_date};
use chrono::Duration;
let duration = from_str("+3 days");
@@ -39,6 +39,15 @@ assert_eq!(
);
```
For DateTime parsing, import the `parse_datetime` module:
```rs
use parse_datetime::parse_datetime::from_str;
use chrono::{Local, TimeZone};
let dt = from_str("2021-02-14 06:37:47");
assert_eq!(dt.unwrap(), Local.with_ymd_and_hms(2021, 2, 14, 6, 37, 47).unwrap());
```
### Supported Formats
The `from_str` and `from_str_at_date` functions support the following formats for relative time:
@@ -56,6 +65,8 @@ The `from_str` and `from_str_at_date` functions support the following formats fo
## Return Values
### Duration
The `from_str` and `from_str_at_date` functions return:
- `Ok(Duration)` - If the input string can be parsed as a relative time
@@ -64,6 +75,13 @@ The `from_str` and `from_str_at_date` functions return:
This function will return `Err(ParseDurationError::InvalidInput)` if the input string
cannot be parsed as a relative time.
### parse_datetime
The `from_str` function returns:
- `Ok(DateTime<FixedOffset>)` - If the input string can be prsed as a datetime
- `Err(ParseDurationError::InvalidInput)` - If the input string cannot be parsed
## Fuzzer
To run the fuzzer:
@@ -74,3 +92,8 @@ $ cargo fuzz run fuzz_from_str
## License
This project is licensed under the [MIT License](LICENSE).
## Note
At some point, this crate was called humantime_to_duration.
It has been renamed to cover more cases.
+11 -11
View File
@@ -85,8 +85,8 @@ name = "fuzz_from_str"
version = "0.1.0"
dependencies = [
"chrono",
"humantime_to_duration",
"libfuzzer-sys",
"parse_datetime",
"rand",
"regex",
]
@@ -102,14 +102,6 @@ dependencies = [
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
name = "humantime_to_duration"
version = "0.3.0"
dependencies = [
"chrono",
"regex",
]
[[package]]
name = "iana-time-zone"
version = "0.1.56"
@@ -195,6 +187,14 @@ version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
[[package]]
name = "parse_datetime"
version = "0.4.0"
dependencies = [
"chrono",
"regex",
]
[[package]]
name = "ppv-lite86"
version = "0.2.17"
@@ -251,9 +251,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.8.3"
version = "1.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390"
checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
dependencies = [
"aho-corasick",
"memchr",
+8 -2
View File
@@ -9,10 +9,10 @@ cargo-fuzz = true
[dependencies]
rand = "0.8.5"
libfuzzer-sys = "0.4"
regex = "1.8.3"
regex = "1.8.4"
chrono = "0.4"
[dependencies.humantime_to_duration]
[dependencies.parse_datetime]
path = "../"
[[bin]]
@@ -20,3 +20,9 @@ name = "fuzz_from_str"
path = "fuzz_targets/from_str.rs"
test = false
doc = false
[[bin]]
name = "fuzz_parse_datetime_from_str"
path = "fuzz_targets/parse_datetime_from_str.rs"
test = false
doc = false
+1 -1
View File
@@ -4,5 +4,5 @@ use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
let s = std::str::from_utf8(data).unwrap_or("");
let _ = humantime_to_duration::from_str(s);
let _ = parse_datetime::from_str(s);
});
@@ -0,0 +1,8 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
let s = std::str::from_utf8(data).unwrap_or("");
let _ = parse_datetime::parse_datetime::from_str(s);
});
+6 -3
View File
@@ -1,6 +1,9 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// Expose parse_datetime
pub mod parse_datetime;
use chrono::{Duration, Local, NaiveDate, Utc};
use regex::{Error as RegexError, Regex};
use std::error::Error;
@@ -47,7 +50,7 @@ impl From<RegexError> for ParseDurationError {
///
/// ```
/// use chrono::Duration;
/// let duration = humantime_to_duration::from_str("+3 days");
/// let duration = parse_datetime::from_str("+3 days");
/// assert_eq!(duration.unwrap(), Duration::days(3));
/// ```
///
@@ -82,7 +85,7 @@ impl From<RegexError> for ParseDurationError {
///
/// ```
/// use chrono::Duration;
/// use humantime_to_duration::{from_str, ParseDurationError};
/// use parse_datetime::{from_str, ParseDurationError};
///
/// assert_eq!(from_str("1 hour, 30 minutes").unwrap(), Duration::minutes(90));
/// assert_eq!(from_str("tomorrow").unwrap(), Duration::days(1));
@@ -109,7 +112,7 @@ pub fn from_str(s: &str) -> Result<Duration, ParseDurationError> {
///
/// ```
/// use chrono::{Duration, NaiveDate, Utc, Local};
/// use humantime_to_duration::{from_str_at_date, ParseDurationError};
/// use parse_datetime::{from_str_at_date, ParseDurationError};
/// let today = Local::now().date().naive_local();
/// let yesterday = today - Duration::days(1);
/// assert_eq!(
+252
View File
@@ -0,0 +1,252 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use chrono::{DateTime, FixedOffset, Local, LocalResult, NaiveDateTime, TimeZone};
use crate::ParseDurationError;
/// Formats that parse input can take.
/// Taken from `touch` coreutils
mod format {
pub(crate) const ISO_8601: &str = "%Y-%m-%d";
pub(crate) const ISO_8601_NO_SEP: &str = "%Y%m%d";
pub(crate) const POSIX_LOCALE: &str = "%a %b %e %H:%M:%S %Y";
pub(crate) const YYYYMMDDHHMM_DOT_SS: &str = "%Y%m%d%H%M.%S";
pub(crate) const YYYYMMDDHHMMSS: &str = "%Y-%m-%d %H:%M:%S.%f";
pub(crate) const YYYYMMDDHHMMS: &str = "%Y-%m-%d %H:%M:%S";
pub(crate) const YYYY_MM_DD_HH_MM: &str = "%Y-%m-%d %H:%M";
pub(crate) const YYYYMMDDHHMM: &str = "%Y%m%d%H%M";
pub(crate) const YYYYMMDDHHMM_OFFSET: &str = "%Y%m%d%H%M %z";
pub(crate) const YYYYMMDDHHMM_UTC_OFFSET: &str = "%Y%m%d%H%MUTC%z";
pub(crate) const YYYYMMDDHHMM_ZULU_OFFSET: &str = "%Y%m%d%H%MZ%z";
pub(crate) const YYYYMMDDHHMM_HYPHENATED_OFFSET: &str = "%Y-%m-%d %H:%M %z";
pub(crate) const YYYYMMDDHHMMS_T_SEP: &str = "%Y-%m-%dT%H:%M:%S";
pub(crate) const UTC_OFFSET: &str = "UTC%#z";
pub(crate) const ZULU_OFFSET: &str = "Z%#z";
}
/// Loosely parses a time string and returns a `DateTime` representing the
/// absolute time of the string.
///
/// # Arguments
///
/// * `s` - A string slice representing the time.
///
/// # Examples
///
/// ```
/// use chrono::{DateTime, Utc, TimeZone};
/// let time = parse_datetime::parse_datetime::from_str("2023-06-03 12:00:01Z");
/// assert_eq!(time.unwrap(), Utc.with_ymd_and_hms(2023, 06, 03, 12, 00, 01).unwrap());
/// ```
///
/// # Supported formats
///
/// The function supports the following formats for time:
///
/// * ISO formats
/// * timezone offsets, e.g., "UTC-0100"
///
/// # Returns
///
/// * `Ok(DateTime<FixedOffset>)` - If the input string can be parsed as a time
/// * `Err(ParseDurationError)` - If the input string cannot be parsed as a relative time
///
/// # Errors
///
/// This function will return `Err(ParseDurationError::InvalidInput)` if the input string
/// cannot be parsed as a relative time.
///
pub fn from_str<S: AsRef<str> + Clone>(s: S) -> Result<DateTime<FixedOffset>, ParseDurationError> {
// TODO: Replace with a proper customiseable parsing solution using `nom`, `grmtools`, or
// similar
// Formats with offsets don't require NaiveDateTime workaround
for fmt in [
format::YYYYMMDDHHMM_OFFSET,
format::YYYYMMDDHHMM_HYPHENATED_OFFSET,
format::YYYYMMDDHHMM_UTC_OFFSET,
format::YYYYMMDDHHMM_ZULU_OFFSET,
] {
if let Ok(parsed) = DateTime::parse_from_str(s.as_ref(), fmt) {
return Ok(parsed);
}
}
// Parse formats with no offset, assume local time
for fmt in [
format::YYYYMMDDHHMMS_T_SEP,
format::YYYYMMDDHHMM,
format::YYYYMMDDHHMMS,
format::YYYYMMDDHHMMSS,
format::YYYY_MM_DD_HH_MM,
format::YYYYMMDDHHMM_DOT_SS,
format::POSIX_LOCALE,
] {
if let Ok(parsed) = NaiveDateTime::parse_from_str(s.as_ref(), fmt) {
if let Ok(dt) = naive_dt_to_fixed_offset(parsed) {
return Ok(dt);
}
}
}
// Parse epoch seconds
if s.as_ref().bytes().next() == Some(b'@') {
if let Ok(parsed) = NaiveDateTime::parse_from_str(&s.as_ref()[1..], "%s") {
if let Ok(dt) = naive_dt_to_fixed_offset(parsed) {
return Ok(dt);
}
}
}
let ts = s.as_ref().to_owned() + "0000";
// Parse date only formats - assume midnight local timezone
for fmt in [format::ISO_8601, format::ISO_8601_NO_SEP] {
let f = fmt.to_owned() + "%H%M";
if let Ok(parsed) = NaiveDateTime::parse_from_str(&ts, &f) {
if let Ok(dt) = naive_dt_to_fixed_offset(parsed) {
return Ok(dt);
}
}
}
// Parse offsets. chrono doesn't provide any functionality to parse
// offsets, so instead we replicate parse_date behaviour by getting
// the current date with local, and create a date time string at midnight,
// before trying offset suffixes
let local = Local::now();
let ts = format!("{}", local.format("%Y%m%d")) + "0000" + s.as_ref();
for fmt in [format::UTC_OFFSET, format::ZULU_OFFSET] {
let f = format::YYYYMMDDHHMM.to_owned() + fmt;
if let Ok(parsed) = DateTime::parse_from_str(&ts, &f) {
return Ok(parsed);
}
}
// Default parse and failure
s.as_ref()
.parse()
.map_err(|_| (ParseDurationError::InvalidInput))
}
// Convert NaiveDateTime to DateTime<FixedOffset> by assuming the offset
// is local time
fn naive_dt_to_fixed_offset(dt: NaiveDateTime) -> Result<DateTime<FixedOffset>, ()> {
let now = Local::now();
match now.offset().from_local_datetime(&dt) {
LocalResult::Single(dt) => Ok(dt),
_ => Err(()),
}
}
#[cfg(test)]
mod tests {
static TEST_TIME: i64 = 1613371067;
#[cfg(test)]
mod iso_8601 {
use std::env;
use crate::{
parse_datetime::from_str, parse_datetime::tests::TEST_TIME, ParseDurationError,
};
#[test]
fn test_t_sep() {
env::set_var("TZ", "UTC");
let dt = "2021-02-15T06:37:47";
let actual = from_str(dt);
assert_eq!(actual.unwrap().timestamp(), TEST_TIME);
}
#[test]
fn test_space_sep() {
env::set_var("TZ", "UTC");
let dt = "2021-02-15 06:37:47";
let actual = from_str(dt);
assert_eq!(actual.unwrap().timestamp(), TEST_TIME);
}
#[test]
fn test_space_sep_offset() {
env::set_var("TZ", "UTC");
let dt = "2021-02-14 22:37:47 -0800";
let actual = from_str(dt);
assert_eq!(actual.unwrap().timestamp(), TEST_TIME);
}
#[test]
fn test_t_sep_offset() {
env::set_var("TZ", "UTC");
let dt = "2021-02-14T22:37:47 -0800";
let actual = from_str(dt);
assert_eq!(actual.unwrap().timestamp(), TEST_TIME);
}
#[test]
fn invalid_formats() {
let invalid_dts = vec!["NotADate", "202104", "202104-12T22:37:47"];
for dt in invalid_dts {
assert_eq!(from_str(dt), Err(ParseDurationError::InvalidInput));
}
}
}
#[cfg(test)]
mod offsets {
use chrono::Local;
use crate::{parse_datetime::from_str, ParseDurationError};
#[test]
fn test_positive_offsets() {
let offsets = vec![
"UTC+07:00",
"UTC+0700",
"UTC+07",
"Z+07:00",
"Z+0700",
"Z+07",
];
let expected = format!("{}{}", Local::now().format("%Y%m%d"), "0000+0700");
for offset in offsets {
let actual = from_str(offset).unwrap();
assert_eq!(expected, format!("{}", actual.format("%Y%m%d%H%M%z")));
}
}
#[test]
fn test_partial_offset() {
let offsets = vec!["UTC+00:15", "UTC+0015", "Z+00:15", "Z+0015"];
let expected = format!("{}{}", Local::now().format("%Y%m%d"), "0000+0015");
for offset in offsets {
let actual = from_str(offset).unwrap();
assert_eq!(expected, format!("{}", actual.format("%Y%m%d%H%M%z")));
}
}
#[test]
fn invalid_offset_format() {
let invalid_offsets = vec!["+0700", "UTC+2", "Z-1", "UTC+01005"];
for offset in invalid_offsets {
assert_eq!(from_str(offset), Err(ParseDurationError::InvalidInput));
}
}
}
/// Used to test example code presented in the README.
mod readme_test {
use crate::parse_datetime::from_str;
use chrono::{Local, TimeZone};
#[test]
fn test_readme_code() {
let dt = from_str("2021-02-14 06:37:47");
assert_eq!(
dt.unwrap(),
Local.with_ymd_and_hms(2021, 2, 14, 6, 37, 47).unwrap()
);
}
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
use chrono::{Duration, Utc};
use humantime_to_duration::{from_str, from_str_at_date, ParseDurationError};
use parse_datetime::{from_str, from_str_at_date, ParseDurationError};
#[test]
fn test_invalid_input() {