#![allow(clippy::type_complexity)]
use crate::whitespace::ws;
use anyhow::{anyhow, Result};
use chrono::{DateTime, Duration, Utc};
use nom::{
branch::alt,
bytes::complete::tag,
character::complete::{digit1, multispace0},
combinator::{map_res, opt},
sequence::tuple,
IResult,
};
use std::sync::atomic::{AtomicBool, Ordering};
const SIMPLIFIED_EXTENDED_ISO_8601: &str = "%Y-%m-%dT%H:%M:%S%.3fZ";
static USE_TEST_TIME: AtomicBool = AtomicBool::new(false);
pub(crate) fn now() -> String {
if USE_TEST_TIME.load(Ordering::Acquire) {
return "2017-01-19T16:27:20.974Z".to_string();
}
format!("{}", Utc::now().format(SIMPLIFIED_EXTENDED_ISO_8601))
}
pub fn use_test_now() {
USE_TEST_TIME.store(true, Ordering::Release);
}
pub(crate) fn from_now(offset: &str, reference: &str) -> Result<String> {
let reference: DateTime<Utc> = reference.parse()?;
let dur = parse_duration(offset)
.ok_or_else(|| anyhow!("String '{}' isn't a time expression", offset))?;
Ok(format!(
"{}",
(reference + dur).format(SIMPLIFIED_EXTENDED_ISO_8601)
))
}
fn int(input: &str) -> IResult<&str, i64> {
fn to_int(input: (&str, &str)) -> Result<i64, ()> {
input.0.parse().map_err(|_| ())
}
map_res(tuple((digit1, multispace0)), to_int)(input)
}
fn sign(input: &str) -> IResult<&str, bool> {
fn to_bool(input: &str) -> Result<bool, ()> {
Ok(input == "-")
}
map_res(ws(alt((tag("-"), tag("+")))), to_bool)(input)
}
fn years(input: &str) -> IResult<&str, Duration> {
fn to_duration(input: (i64, &str)) -> Result<Duration, ()> {
Ok(Duration::days(input.0 * 365))
}
map_res(
tuple((int, alt((tag("years"), tag("year"), tag("yr"), tag("y"))))),
to_duration,
)(input)
}
fn months(input: &str) -> IResult<&str, Duration> {
fn to_duration(input: (i64, &str)) -> Result<Duration, ()> {
Ok(Duration::days(input.0 * 30))
}
map_res(
tuple((int, alt((tag("months"), tag("month"), tag("mo"))))),
to_duration,
)(input)
}
fn weeks(input: &str) -> IResult<&str, Duration> {
fn to_duration(input: (i64, &str)) -> Result<Duration, ()> {
Ok(Duration::weeks(input.0))
}
map_res(
tuple((int, alt((tag("weeks"), tag("week"), tag("wk"), tag("w"))))),
to_duration,
)(input)
}
fn days(input: &str) -> IResult<&str, Duration> {
fn to_duration(input: (i64, &str)) -> Result<Duration, ()> {
Ok(Duration::days(input.0))
}
map_res(
tuple((int, alt((tag("days"), tag("day"), tag("d"))))),
to_duration,
)(input)
}
fn hours(input: &str) -> IResult<&str, Duration> {
fn to_duration(input: (i64, &str)) -> Result<Duration, ()> {
Ok(Duration::hours(input.0))
}
map_res(
tuple((int, alt((tag("hours"), tag("hour"), tag("h"))))),
to_duration,
)(input)
}
fn minutes(input: &str) -> IResult<&str, Duration> {
fn to_duration(input: (i64, &str)) -> Result<Duration, ()> {
Ok(Duration::minutes(input.0))
}
map_res(
tuple((
int,
alt((tag("minutes"), tag("minute"), tag("min"), tag("m"))),
)),
to_duration,
)(input)
}
fn seconds(input: &str) -> IResult<&str, Duration> {
fn to_duration(input: (i64, &str)) -> Result<Duration, ()> {
Ok(Duration::seconds(input.0))
}
map_res(
tuple((
int,
alt((tag("seconds"), tag("second"), tag("sec"), tag("s"))),
)),
to_duration,
)(input)
}
fn duration(input: &str) -> IResult<&str, Duration> {
fn sum_duration(
input: (
&str,
Option<bool>,
Option<Duration>,
Option<Duration>,
Option<Duration>,
Option<Duration>,
Option<Duration>,
Option<Duration>,
Option<Duration>,
),
) -> Result<Duration, ()> {
let mut dur = Duration::zero();
if let Some(d) = input.2 {
dur = dur + d;
}
if let Some(d) = input.3 {
dur = dur + d;
}
if let Some(d) = input.4 {
dur = dur + d;
}
if let Some(d) = input.5 {
dur = dur + d;
}
if let Some(d) = input.6 {
dur = dur + d;
}
if let Some(d) = input.7 {
dur = dur + d;
}
if let Some(d) = input.8 {
dur = dur + d;
}
if input.1 == Some(true) {
dur = -dur;
}
Ok(dur)
}
map_res(
tuple((
multispace0,
ws(opt(sign)),
ws(opt(years)),
ws(opt(months)),
ws(opt(weeks)),
ws(opt(days)),
ws(opt(hours)),
ws(opt(minutes)),
ws(opt(seconds)),
)),
sum_duration,
)(input)
}
fn parse_duration(input: &str) -> Option<Duration> {
match duration(input) {
Ok(("", dur)) => Some(dur),
_ => None,
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_empty_string() {
assert_eq!(parse_duration(""), Some(Duration::zero()));
}
#[test]
fn test_1s() {
assert_eq!(parse_duration("1s"), Some(Duration::seconds(1)));
}
#[test]
fn test_1sec() {
assert_eq!(parse_duration("1sec"), Some(Duration::seconds(1)));
}
#[test]
fn test_1second() {
assert_eq!(parse_duration("1second"), Some(Duration::seconds(1)));
}
#[test]
fn test_2seconds() {
assert_eq!(parse_duration("2seconds"), Some(Duration::seconds(2)));
}
#[test]
fn test_10s() {
assert_eq!(parse_duration("10s"), Some(Duration::seconds(10)));
}
#[test]
fn test_1s_space1() {
assert_eq!(parse_duration(" 1s"), Some(Duration::seconds(1)));
}
#[test]
fn test_1s_space2() {
assert_eq!(parse_duration("1 s"), Some(Duration::seconds(1)));
}
#[test]
fn test_1s_space3() {
assert_eq!(parse_duration("1s "), Some(Duration::seconds(1)));
}
#[test]
fn test_1s_space4() {
assert_eq!(parse_duration(" 1 s "), Some(Duration::seconds(1)));
}
#[test]
fn test_3m() {
assert_eq!(parse_duration("3m"), Some(Duration::minutes(3)));
}
#[test]
fn test_3min() {
assert_eq!(parse_duration("3min"), Some(Duration::minutes(3)));
}
#[test]
fn test_3minute() {
assert_eq!(parse_duration("3minute"), Some(Duration::minutes(3)));
}
#[test]
fn test_3minutes() {
assert_eq!(parse_duration("3minutes"), Some(Duration::minutes(3)));
}
#[test]
fn test_3h() {
assert_eq!(parse_duration("3h"), Some(Duration::hours(3)));
}
#[test]
fn test_4day() {
assert_eq!(parse_duration("4day"), Some(Duration::days(4)));
}
#[test]
fn test_5weeks() {
assert_eq!(parse_duration("5 weeks"), Some(Duration::weeks(5)));
}
#[test]
fn test_6mo() {
assert_eq!(parse_duration("6 months"), Some(Duration::days(6 * 30)));
}
#[test]
fn test_7yr() {
assert_eq!(parse_duration("7 yr"), Some(Duration::days(7 * 365)));
}
#[test]
fn test_all_units() {
assert_eq!(
parse_duration("7y6mo5w4d3h2m1s"),
Some(
Duration::seconds(1)
+ Duration::minutes(2)
+ Duration::hours(3)
+ Duration::days(4)
+ Duration::weeks(5)
+ Duration::days(6 * 30)
+ Duration::days(7 * 365)
)
);
}
#[test]
fn test_all_units_neg() {
assert_eq!(
parse_duration(" - 7y6mo5w4d3h2m1s"),
Some(
-Duration::seconds(1)
- Duration::minutes(2)
- Duration::hours(3)
- Duration::days(4)
- Duration::weeks(5)
- Duration::days(6 * 30)
- Duration::days(7 * 365)
)
);
}
#[test]
fn test_units_wrong_oder() {
assert!(parse_duration("1s 1y").is_none());
}
#[test]
fn test_all_units_space() {
assert_eq!(
parse_duration(" 7 y 6 mo 5 w 4 d 3 h 2 m 1 s "),
Some(
Duration::seconds(1)
+ Duration::minutes(2)
+ Duration::hours(3)
+ Duration::days(4)
+ Duration::weeks(5)
+ Duration::days(6 * 30)
+ Duration::days(7 * 365)
)
);
}
}