json_e/
fromnow.rs

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