Skip to main content

json_e/
fromnow.rs

1#![allow(clippy::type_complexity)]
2use crate::whitespace::ws;
3use anyhow::{anyhow, Result};
4use chrono::{DateTime, Duration, Utc};
5use nom::{
6    branch::alt,
7    bytes::complete::tag,
8    character::complete::{digit1, multispace0},
9    combinator::{map_res, opt},
10    sequence::tuple,
11    IResult,
12};
13use std::sync::atomic::{AtomicBool, Ordering};
14
15const SIMPLIFIED_EXTENDED_ISO_8601: &str = "%Y-%m-%dT%H:%M:%S%.3fZ";
16static USE_TEST_TIME: AtomicBool = AtomicBool::new(false);
17
18/// Get the current time, as a properly-formatted string
19pub(crate) fn now() -> String {
20    // when testing, we use a fixed value for "now"
21    if USE_TEST_TIME.load(Ordering::Acquire) {
22        return "2017-01-19T16:27:20.974Z".to_string();
23    }
24    format!("{}", Utc::now().format(SIMPLIFIED_EXTENDED_ISO_8601))
25}
26
27/// Use the test time (2017-01-19T16:27:20.974Z) as the current time for all
28/// subsequent operations.  This is only useful in testing this library.
29pub fn use_test_now() {
30    USE_TEST_TIME.store(true, Ordering::Release);
31}
32
33/// Calculate a time offset from a reference time.
34///
35/// Date-times are are specified in simplified extended ISO format (ISO 8601) with zero timezone offset;
36/// this is the format used by the JS `Date.toISOString()` function, and has the form
37/// `YYYY-MM-DDTHH:mm:ss(.sss)?Z`, where the decimal portion of the seconds is optional.
38pub(crate) fn from_now(offset: &str, reference: &str) -> Result<String> {
39    let reference: DateTime<Utc> = reference.parse()?;
40    let dur = parse_duration(offset)
41        .ok_or_else(|| anyhow!("String '{}' isn't a time expression", offset))?;
42    Ok(format!(
43        "{}",
44        (reference + dur).format(SIMPLIFIED_EXTENDED_ISO_8601)
45    ))
46}
47
48fn int(input: &str) -> IResult<&str, i64> {
49    fn to_int(input: (&str, &str)) -> Result<i64, ()> {
50        input.0.parse().map_err(|_| ())
51    }
52    map_res(tuple((digit1, multispace0)), to_int)(input)
53}
54
55fn sign(input: &str) -> IResult<&str, bool> {
56    fn to_bool(input: &str) -> Result<bool, ()> {
57        Ok(input == "-")
58    }
59    map_res(ws(alt((tag("-"), tag("+")))), to_bool)(input)
60}
61
62fn years(input: &str) -> IResult<&str, Duration> {
63    fn to_duration(input: (i64, &str)) -> Result<Duration, ()> {
64        // "a year" is not a precise length of time, but fromNow assumes 365 days
65        Ok(Duration::days(input.0 * 365))
66    }
67    map_res(
68        tuple((int, alt((tag("years"), tag("year"), tag("yr"), tag("y"))))),
69        to_duration,
70    )(input)
71}
72
73fn months(input: &str) -> IResult<&str, Duration> {
74    fn to_duration(input: (i64, &str)) -> Result<Duration, ()> {
75        // "a month" is not a precise length of time, but fromNow assumes 30 days
76        Ok(Duration::days(input.0 * 30))
77    }
78    map_res(
79        tuple((int, alt((tag("months"), tag("month"), tag("mo"))))),
80        to_duration,
81    )(input)
82}
83
84fn weeks(input: &str) -> IResult<&str, Duration> {
85    fn to_duration(input: (i64, &str)) -> Result<Duration, ()> {
86        Ok(Duration::weeks(input.0))
87    }
88    map_res(
89        tuple((int, alt((tag("weeks"), tag("week"), tag("wk"), tag("w"))))),
90        to_duration,
91    )(input)
92}
93
94fn days(input: &str) -> IResult<&str, Duration> {
95    fn to_duration(input: (i64, &str)) -> Result<Duration, ()> {
96        Ok(Duration::days(input.0))
97    }
98    map_res(
99        tuple((int, alt((tag("days"), tag("day"), tag("d"))))),
100        to_duration,
101    )(input)
102}
103
104fn hours(input: &str) -> IResult<&str, Duration> {
105    fn to_duration(input: (i64, &str)) -> Result<Duration, ()> {
106        Ok(Duration::hours(input.0))
107    }
108    map_res(
109        tuple((int, alt((tag("hours"), tag("hour"), tag("h"))))),
110        to_duration,
111    )(input)
112}
113
114fn minutes(input: &str) -> IResult<&str, Duration> {
115    fn to_duration(input: (i64, &str)) -> Result<Duration, ()> {
116        Ok(Duration::minutes(input.0))
117    }
118    map_res(
119        tuple((
120            int,
121            alt((tag("minutes"), tag("minute"), tag("min"), tag("m"))),
122        )),
123        to_duration,
124    )(input)
125}
126
127fn seconds(input: &str) -> IResult<&str, Duration> {
128    fn to_duration(input: (i64, &str)) -> Result<Duration, ()> {
129        Ok(Duration::seconds(input.0))
130    }
131    map_res(
132        tuple((
133            int,
134            alt((tag("seconds"), tag("second"), tag("sec"), tag("s"))),
135        )),
136        to_duration,
137    )(input)
138}
139
140fn duration(input: &str) -> IResult<&str, Duration> {
141    // This looks a little silly, in that it's just adding the components, but this
142    // enforces that each component appears once and in the proper order.
143    fn sum_duration(
144        input: (
145            &str,
146            Option<bool>,
147            Option<Duration>,
148            Option<Duration>,
149            Option<Duration>,
150            Option<Duration>,
151            Option<Duration>,
152            Option<Duration>,
153            Option<Duration>,
154        ),
155    ) -> Result<Duration, ()> {
156        let mut dur = Duration::zero();
157        if let Some(d) = input.2 {
158            dur = dur + d;
159        }
160        if let Some(d) = input.3 {
161            dur = dur + d;
162        }
163        if let Some(d) = input.4 {
164            dur = dur + d;
165        }
166        if let Some(d) = input.5 {
167            dur = dur + d;
168        }
169        if let Some(d) = input.6 {
170            dur = dur + d;
171        }
172        if let Some(d) = input.7 {
173            dur = dur + d;
174        }
175        if let Some(d) = input.8 {
176            dur = dur + d;
177        }
178        // input.1 is true if there was a `-` in the offset
179        if input.1 == Some(true) {
180            dur = -dur;
181        }
182        Ok(dur)
183    }
184    map_res(
185        tuple((
186            multispace0,
187            ws(opt(sign)),
188            ws(opt(years)),
189            ws(opt(months)),
190            ws(opt(weeks)),
191            ws(opt(days)),
192            ws(opt(hours)),
193            ws(opt(minutes)),
194            ws(opt(seconds)),
195        )),
196        sum_duration,
197    )(input)
198}
199
200fn parse_duration(input: &str) -> Option<Duration> {
201    match duration(input) {
202        Ok(("", dur)) => Some(dur),
203        _ => None,
204    }
205}
206
207#[cfg(test)]
208mod test {
209    use super::*;
210
211    #[test]
212    fn test_empty_string() {
213        assert_eq!(parse_duration(""), Some(Duration::zero()));
214    }
215
216    #[test]
217    fn test_1s() {
218        assert_eq!(parse_duration("1s"), Some(Duration::seconds(1)));
219    }
220
221    #[test]
222    fn test_1sec() {
223        assert_eq!(parse_duration("1sec"), Some(Duration::seconds(1)));
224    }
225
226    #[test]
227    fn test_1second() {
228        assert_eq!(parse_duration("1second"), Some(Duration::seconds(1)));
229    }
230
231    #[test]
232    fn test_2seconds() {
233        assert_eq!(parse_duration("2seconds"), Some(Duration::seconds(2)));
234    }
235
236    #[test]
237    fn test_10s() {
238        assert_eq!(parse_duration("10s"), Some(Duration::seconds(10)));
239    }
240
241    #[test]
242    fn test_1s_space1() {
243        assert_eq!(parse_duration("  1s"), Some(Duration::seconds(1)));
244    }
245
246    #[test]
247    fn test_1s_space2() {
248        assert_eq!(parse_duration("1  s"), Some(Duration::seconds(1)));
249    }
250
251    #[test]
252    fn test_1s_space3() {
253        assert_eq!(parse_duration("1s  "), Some(Duration::seconds(1)));
254    }
255
256    #[test]
257    fn test_1s_space4() {
258        assert_eq!(parse_duration(" 1   s  "), Some(Duration::seconds(1)));
259    }
260
261    #[test]
262    fn test_3m() {
263        assert_eq!(parse_duration("3m"), Some(Duration::minutes(3)));
264    }
265
266    #[test]
267    fn test_3min() {
268        assert_eq!(parse_duration("3min"), Some(Duration::minutes(3)));
269    }
270
271    #[test]
272    fn test_3minute() {
273        assert_eq!(parse_duration("3minute"), Some(Duration::minutes(3)));
274    }
275
276    #[test]
277    fn test_3minutes() {
278        assert_eq!(parse_duration("3minutes"), Some(Duration::minutes(3)));
279    }
280
281    #[test]
282    fn test_3h() {
283        assert_eq!(parse_duration("3h"), Some(Duration::hours(3)));
284    }
285
286    #[test]
287    fn test_4day() {
288        assert_eq!(parse_duration("4day"), Some(Duration::days(4)));
289    }
290
291    #[test]
292    fn test_5weeks() {
293        assert_eq!(parse_duration("5 weeks"), Some(Duration::weeks(5)));
294    }
295
296    #[test]
297    fn test_6mo() {
298        assert_eq!(parse_duration("6 months"), Some(Duration::days(6 * 30)));
299    }
300
301    #[test]
302    fn test_7yr() {
303        assert_eq!(parse_duration("7 yr"), Some(Duration::days(7 * 365)));
304    }
305
306    #[test]
307    fn test_all_units() {
308        assert_eq!(
309            parse_duration("7y6mo5w4d3h2m1s"),
310            Some(
311                Duration::seconds(1)
312                    + Duration::minutes(2)
313                    + Duration::hours(3)
314                    + Duration::days(4)
315                    + Duration::weeks(5)
316                    + Duration::days(6 * 30)
317                    + Duration::days(7 * 365)
318            )
319        );
320    }
321
322    #[test]
323    fn test_all_units_neg() {
324        assert_eq!(
325            parse_duration(" - 7y6mo5w4d3h2m1s"),
326            Some(
327                -Duration::seconds(1)
328                    - Duration::minutes(2)
329                    - Duration::hours(3)
330                    - Duration::days(4)
331                    - Duration::weeks(5)
332                    - Duration::days(6 * 30)
333                    - Duration::days(7 * 365)
334            )
335        );
336    }
337
338    #[test]
339    fn test_units_wrong_oder() {
340        assert!(parse_duration("1s 1y").is_none());
341    }
342
343    #[test]
344    fn test_all_units_space() {
345        assert_eq!(
346            parse_duration(" 7 y 6 mo 5 w 4 d 3 h 2 m 1 s "),
347            Some(
348                Duration::seconds(1)
349                    + Duration::minutes(2)
350                    + Duration::hours(3)
351                    + Duration::days(4)
352                    + Duration::weeks(5)
353                    + Duration::days(6 * 30)
354                    + Duration::days(7 * 365)
355            )
356        );
357    }
358}