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
17pub(crate) fn now() -> String {
19 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
26pub fn use_test_now() {
29 USE_TEST_TIME.store(true, Ordering::Release);
30}
31
32pub(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 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 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 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 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}