1#![allow(dead_code)]
2
3pub mod interval;
4
5use std::time::SystemTime;
6
7use chrono::Datelike;
8use chumsky::prelude::*;
9
10use crate::component::date::smart::date;
11use crate::component::period::interval::{interval, Interval};
12use crate::component::whitespace::whitespace;
13use crate::state::State;
14
15#[derive(Debug, Clone, PartialEq)]
16pub struct Period {
17 pub interval: Option<Interval>,
18 pub begin: Option<chrono::NaiveDate>,
19 pub end: Option<chrono::NaiveDate>,
20}
21
22pub fn period<'a>() -> impl Parser<'a, &'a str, Period, extra::Full<Rich<'a, char>, State, ()>> {
23 let interval_begin_end = interval()
24 .then_ignore(
25 whitespace()
26 .repeated()
27 .at_least(1)
28 .ignore_then(just("in"))
29 .ignore_then(whitespace().repeated().at_least(1))
30 .or(whitespace().repeated().at_least(1)),
31 )
32 .then(
33 quarter()
34 .or(year_quarter())
35 .or(begin_end())
36 .or(just_end())
37 .or(begin())
38 .or(year_month_day())
39 .or(year_month())
40 .or(year()),
41 )
42 .map(|(interval, (begin, end))| Period {
43 interval: Some(interval),
44 begin,
45 end,
46 });
47
48 let interval = interval().map(|interval| Period {
49 interval: Some(interval),
50 begin: None,
51 end: None,
52 });
53
54 let begin_end = quarter()
55 .or(year_quarter())
56 .or(begin_end())
57 .or(just_end())
58 .or(begin())
59 .or(year_month_day())
60 .or(year_month())
61 .or(year())
62 .map(|(begin, end)| Period {
63 interval: None,
64 begin,
65 end,
66 });
67
68 interval_begin_end.or(interval).or(begin_end)
69}
70
71fn today() -> chrono::NaiveDate {
73 let current_time = SystemTime::now();
74 let datetime: chrono::DateTime<chrono::Local> = current_time.into();
75 datetime.date_naive()
76}
77
78fn year_quarter<'a>() -> impl Parser<
80 'a,
81 &'a str,
82 (Option<chrono::NaiveDate>, Option<chrono::NaiveDate>),
83 extra::Full<Rich<'a, char>, State, ()>,
84> {
85 any()
86 .filter(|c: &char| c.is_ascii_digit())
87 .repeated()
88 .exactly(4)
89 .collect::<String>()
90 .from_str::<i32>()
91 .unwrapped()
92 .then(
93 one_of("qQ")
94 .ignore_then(one_of("1234"))
95 .map(|s: char| s.to_string())
96 .from_str::<u32>()
97 .unwrapped(),
98 )
99 .map(|(year, q)| {
100 (
101 chrono::NaiveDate::from_ymd_opt(year, (q - 1) * 3 + 1, 1),
102 chrono::NaiveDate::from_ymd_opt(year, (q - 1) * 3 + 4, 1),
103 )
104 })
105}
106
107fn quarter<'a>() -> impl Parser<
109 'a,
110 &'a str,
111 (Option<chrono::NaiveDate>, Option<chrono::NaiveDate>),
112 extra::Full<Rich<'a, char>, State, ()>,
113> {
114 one_of("qQ")
115 .ignore_then(one_of("1234"))
116 .map(|s: char| s.to_string())
117 .from_str::<u32>()
118 .unwrapped()
119 .map(|q| {
120 (
121 today().with_month((q - 1) * 3 + 1).unwrap().with_day(1),
122 today().with_month((q - 1) * 3 + 4).unwrap().with_day(1),
123 )
124 })
125}
126
127fn begin_end<'a>() -> impl Parser<
128 'a,
129 &'a str,
130 (Option<chrono::NaiveDate>, Option<chrono::NaiveDate>),
131 extra::Full<Rich<'a, char>, State, ()>,
132> {
133 just("from")
134 .or(just("since"))
135 .ignore_then(whitespace().repeated().at_least(1))
136 .or_not()
137 .ignore_then(date())
138 .then_ignore(
139 whitespace()
140 .repeated()
141 .then(just("to").or(just("..")).or(just("-")))
142 .or_not(),
143 )
144 .then_ignore(whitespace().repeated())
145 .then(date())
146 .map(|(begin, end)| (Some(begin), Some(end)))
147}
148
149fn just_end<'a>() -> impl Parser<
151 'a,
152 &'a str,
153 (Option<chrono::NaiveDate>, Option<chrono::NaiveDate>),
154 extra::Full<Rich<'a, char>, State, ()>,
155> {
156 just("to")
157 .then(whitespace().repeated())
158 .ignore_then(date())
159 .map(|end| (None, Some(end)))
160}
161
162fn begin<'a>() -> impl Parser<
164 'a,
165 &'a str,
166 (Option<chrono::NaiveDate>, Option<chrono::NaiveDate>),
167 extra::Full<Rich<'a, char>, State, ()>,
168> {
169 just("from")
170 .or(just("since"))
171 .ignore_then(whitespace().repeated().at_least(1))
172 .ignore_then(date())
173 .map(|begin| (Some(begin), None))
174}
175
176fn year<'a>() -> impl Parser<
178 'a,
179 &'a str,
180 (Option<chrono::NaiveDate>, Option<chrono::NaiveDate>),
181 extra::Full<Rich<'a, char>, State, ()>,
182> {
183 any()
184 .filter(|c: &char| c.is_ascii_digit())
185 .repeated()
186 .exactly(4)
187 .collect::<String>()
188 .from_str::<i32>()
189 .unwrapped()
190 .map(|year| {
191 (
192 chrono::NaiveDate::from_ymd_opt(year, 1, 1),
193 chrono::NaiveDate::from_ymd_opt(year + 1, 1, 1),
194 )
195 })
196}
197
198fn year_month<'a>() -> impl Parser<
200 'a,
201 &'a str,
202 (Option<chrono::NaiveDate>, Option<chrono::NaiveDate>),
203 extra::Full<Rich<'a, char>, State, ()>,
204> {
205 any()
206 .filter(|c: &char| c.is_ascii_digit())
207 .repeated()
208 .exactly(4)
209 .collect::<String>()
210 .from_str::<i32>()
211 .unwrapped()
212 .then_ignore(one_of("-/."))
213 .then(
214 any()
215 .filter(|c: &char| c.is_ascii_digit())
216 .repeated()
217 .at_least(1)
218 .at_most(2)
219 .collect::<String>()
220 .from_str::<u32>()
221 .unwrapped(),
222 )
223 .map(|(year, month)| {
224 (
225 chrono::NaiveDate::from_ymd_opt(year, month, 1),
226 chrono::NaiveDate::from_ymd_opt(year, month + 1, 1),
227 )
228 })
229}
230
231fn year_month_day<'a>() -> impl Parser<
233 'a,
234 &'a str,
235 (Option<chrono::NaiveDate>, Option<chrono::NaiveDate>),
236 extra::Full<Rich<'a, char>, State, ()>,
237> {
238 let year_month_day = |sep: char| {
239 any()
240 .filter(|c: &char| c.is_ascii_digit())
241 .repeated()
242 .at_least(1)
243 .at_most(4)
244 .collect::<String>()
245 .from_str::<i32>()
246 .unwrapped()
247 .then_ignore(just(sep))
248 .then(
249 any()
250 .filter(|c: &char| c.is_ascii_digit())
251 .repeated()
252 .at_least(1)
253 .at_most(2)
254 .collect::<String>()
255 .from_str::<u32>()
256 .unwrapped(),
257 )
258 .then_ignore(just(sep))
259 .then(
260 any()
261 .filter(|c: &char| c.is_ascii_digit())
262 .repeated()
263 .at_least(1)
264 .at_most(2)
265 .collect::<String>()
266 .from_str::<u32>()
267 .unwrapped(),
268 )
269 .map(|((year, month), day)| {
270 (
271 chrono::NaiveDate::from_ymd_opt(year, month, day),
272 chrono::NaiveDate::from_ymd_opt(year, month, day + 1),
273 )
274 })
275 };
276 year_month_day('.')
277 .or(year_month_day('/'))
278 .or(year_month_day('-'))
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn from_to() {
287 let result = period()
288 .then_ignore(end())
289 .parse("from 2009/1/1 to 2009/4/1")
290 .into_result();
291 assert_eq!(
292 result,
293 Ok(Period {
294 interval: None,
295 begin: Some(chrono::NaiveDate::from_ymd_opt(2009, 1, 1).unwrap()),
296 end: Some(chrono::NaiveDate::from_ymd_opt(2009, 4, 1).unwrap()),
297 })
298 );
299 }
300
301 #[test]
302 fn dots() {
303 let result = period()
304 .then_ignore(end())
305 .parse("2009/1/1..2009/4/1")
306 .into_result();
307 assert_eq!(
308 result,
309 Ok(Period {
310 interval: None,
311 begin: Some(chrono::NaiveDate::from_ymd_opt(2009, 1, 1).unwrap()),
312 end: Some(chrono::NaiveDate::from_ymd_opt(2009, 4, 1).unwrap()),
313 })
314 );
315 }
316
317 #[test]
318 fn only_begin() {
319 let result = period()
320 .then_ignore(end())
321 .parse("since 2009/1")
322 .into_result();
323 assert_eq!(
324 result,
325 Ok(Period {
326 interval: None,
327 begin: Some(chrono::NaiveDate::from_ymd_opt(2009, 1, 1).unwrap()),
328 end: None,
329 })
330 );
331 }
332
333 #[test]
334 fn only_end() {
335 let result = period().then_ignore(end()).parse("to 2009").into_result();
336 assert_eq!(
337 result,
338 Ok(Period {
339 interval: None,
340 begin: None,
341 end: Some(chrono::NaiveDate::from_ymd_opt(2009, 1, 1).unwrap()),
342 })
343 );
344 }
345
346 #[test]
347 fn year() {
348 let result = period().then_ignore(end()).parse("2009").into_result();
349 assert_eq!(
350 result,
351 Ok(Period {
352 interval: None,
353 begin: Some(chrono::NaiveDate::from_ymd_opt(2009, 1, 1).unwrap()),
354 end: Some(chrono::NaiveDate::from_ymd_opt(2010, 1, 1).unwrap()),
355 })
356 );
357 }
358
359 #[test]
360 fn month() {
361 let result = period().then_ignore(end()).parse("2009/1").into_result();
362 assert_eq!(
363 result,
364 Ok(Period {
365 interval: None,
366 begin: Some(chrono::NaiveDate::from_ymd_opt(2009, 1, 1).unwrap()),
367 end: Some(chrono::NaiveDate::from_ymd_opt(2009, 2, 1).unwrap()),
368 })
369 );
370 }
371
372 #[test]
373 fn day() {
374 let result = period().then_ignore(end()).parse("2009/1/1").into_result();
375 assert_eq!(
376 result,
377 Ok(Period {
378 interval: None,
379 begin: Some(chrono::NaiveDate::from_ymd_opt(2009, 1, 1).unwrap()),
380 end: Some(chrono::NaiveDate::from_ymd_opt(2009, 1, 2).unwrap()),
381 })
382 );
383 }
384
385 #[test]
386 fn quarter() {
387 let result = period().then_ignore(end()).parse("q3").into_result();
388 assert_eq!(
389 result,
390 Ok(Period {
391 interval: None,
392 begin: today().with_month(7).unwrap().with_day(1),
393 end: today().with_month(10).unwrap().with_day(1),
394 })
395 );
396 }
397
398 #[test]
399 fn year_quarter() {
400 let result = period().then_ignore(end()).parse("2009Q3").into_result();
401 assert_eq!(
402 result,
403 Ok(Period {
404 interval: None,
405 begin: chrono::NaiveDate::from_ymd_opt(2009, 7, 1),
406 end: chrono::NaiveDate::from_ymd_opt(2009, 10, 1),
407 })
408 );
409 }
410
411 #[test]
412 fn with_in_interval() {
413 let result = period()
414 .then_ignore(end())
415 .parse("every 2 weeks in 2008")
416 .into_result();
417 assert_eq!(
418 result,
419 Ok(Period {
420 interval: Some(Interval::NthWeek(2)),
421 begin: chrono::NaiveDate::from_ymd_opt(2008, 1, 1),
422 end: chrono::NaiveDate::from_ymd_opt(2009, 1, 1),
423 })
424 );
425 }
426
427 #[test]
428 fn with_interval() {
429 let result = period()
430 .then_ignore(end())
431 .parse("weekly from 2009/1/1 to 2009/4/1")
432 .into_result();
433 assert_eq!(
434 result,
435 Ok(Period {
436 interval: Some(Interval::NthWeek(1)),
437 begin: chrono::NaiveDate::from_ymd_opt(2009, 1, 1),
438 end: chrono::NaiveDate::from_ymd_opt(2009, 4, 1),
439 })
440 );
441 }
442
443 #[test]
444 fn just_interval() {
445 let result = period().then_ignore(end()).parse("monthly").into_result();
446 assert_eq!(
447 result,
448 Ok(Period {
449 interval: Some(Interval::NthMonth(1)),
450 begin: None,
451 end: None,
452 })
453 );
454 }
455}