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
18pub(crate) fn now() -> String {
20 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
27pub fn use_test_now() {
30 USE_TEST_TIME.store(true, Ordering::Release);
31}
32
33pub(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 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 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 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 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}