1use chrono::{Datelike, Duration, NaiveDate, Weekday};
2use std::mem;
3
4use crate::{terr, tfilter};
5
6pub const NO_CHANGE: &str = "no change";
9const DAYS_PER_WEEK: u32 = 7;
10const FAR_PAST: i64 = -100 * 365; type HumanResult = Result<NaiveDate, String>;
13
14#[derive(Debug, Clone, PartialEq, Eq, Copy)]
16pub enum CalendarRangeType {
17 Days(i8),
18 Weeks(i8),
19 Months(i8),
20 Years(i8),
21 DayRange(i8, i8),
22 WeekRange(i8, i8),
23 MonthRange(i8, i8),
24 YearRange(i8, i8),
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Copy)]
29pub struct CalendarRange {
30 pub strict: bool,
31 pub rng: CalendarRangeType,
32}
33
34impl Default for CalendarRange {
35 fn default() -> CalendarRange {
36 CalendarRange { strict: false, rng: CalendarRangeType::Days(1) }
37 }
38}
39
40fn parse_int(s: &str) -> (&str, String) {
41 let mut res = String::new();
42 for c in s.chars() {
43 if !c.is_ascii_digit() {
44 break;
45 }
46 res.push(c);
47 }
48 (&s[res.len()..], res)
49}
50
51impl CalendarRange {
52 pub fn parse(s: &str) -> Result<CalendarRange, terr::TodoError> {
54 if s.contains("..") || s.contains(':') { CalendarRange::parse_range(s) } else { CalendarRange::parse_single(s) }
55 }
56
57 fn parse_single_num(s_in: &str) -> Result<(&str, i8, bool), terr::TodoError> {
58 let (s, strict) = if s_in.starts_with('+') { (&s_in["+".len()..], true) } else { (s_in, false) };
59 let (s, sgn) = if s.starts_with('-') { (&s["-".len()..], -1i8) } else { (s, 1i8) };
60 let (s, num_str) = parse_int(s);
61 let num = if num_str.is_empty() {
62 1
63 } else {
64 match num_str.parse::<i8>() {
65 Ok(n) => n,
66 Err(_) => {
67 return Err(terr::TodoError::InvalidValue(s_in.to_string(), "calendar range value".to_string()));
68 }
69 }
70 };
71 let num = num * sgn;
72 match s {
73 "" | "d" | "D" => {
74 if num.abs() > 100 {
75 return Err(terr::TodoError::InvalidValue(
76 s_in.to_string(),
77 "number of days(range -100..100)".to_string(),
78 ));
79 }
80 }
81 "w" | "W" => {
82 if num.abs() > 16 {
83 return Err(terr::TodoError::InvalidValue(
84 s_in.to_string(),
85 "number of weeks(range -16..16)".to_string(),
86 ));
87 }
88 }
89 "m" | "M" => {
90 if num.abs() > 24 {
91 return Err(terr::TodoError::InvalidValue(
92 s_in.to_string(),
93 "number of months(range -24..24)".to_string(),
94 ));
95 }
96 }
97 "y" | "Y" => {
98 if num.abs() > 2 {
99 return Err(terr::TodoError::InvalidValue(
100 s_in.to_string(),
101 "number of years(range -2..2)".to_string(),
102 ));
103 }
104 }
105 _ => return Err(terr::TodoError::InvalidValue(s_in.to_string(), "calendar range type".to_string())),
106 }
107 Ok((s, num, strict))
108 }
109
110 fn parse_range(s: &str) -> Result<CalendarRange, terr::TodoError> {
111 let ends: Vec<&str> = if s.contains("..") { s.split("..").collect() } else { s.split(':').collect() };
112 if ends.len() > 2 {
113 return Err(terr::TodoError::InvalidValue(
114 s.to_string(),
115 "calendar range cannot contain more than 2 values".to_string(),
116 ));
117 }
118 let (ltp, lnum, lstrict) = CalendarRange::parse_single_num(ends[0])?;
119 let (rtp, rnum, rstrict) = CalendarRange::parse_single_num(ends[1])?;
120 if ltp != rtp {
121 return Err(terr::TodoError::InvalidValue(
122 s.to_string(),
123 "both range ends must use the same dimensions".to_string(),
124 ));
125 }
126 let (lnum, rnum) = if lnum > rnum { (rnum, lnum) } else { (lnum, rnum) };
127 let rng = CalendarRange {
128 strict: lstrict || rstrict,
129 rng: match ltp {
130 "" | "d" | "D" => CalendarRangeType::DayRange(lnum, rnum),
131 "w" | "W" => CalendarRangeType::WeekRange(lnum, rnum),
132 "m" | "M" => CalendarRangeType::MonthRange(lnum, rnum),
133 "y" | "Y" => CalendarRangeType::YearRange(lnum, rnum),
134 _ => {
135 return Err(terr::TodoError::InvalidValue(ltp.to_string(), "date range type".to_string()));
136 }
137 },
138 };
139 Ok(rng)
140 }
141
142 fn parse_single(s: &str) -> Result<CalendarRange, terr::TodoError> {
143 let (tp, num, strict) = CalendarRange::parse_single_num(s)?;
144 let rng = CalendarRange {
145 strict,
146 rng: match tp {
147 "" | "d" | "D" => CalendarRangeType::Days(num),
148 "w" | "W" => CalendarRangeType::Weeks(num),
149 "m" | "M" => CalendarRangeType::Months(num),
150 "y" | "Y" => CalendarRangeType::Years(num),
151 _ => {
152 return Err(terr::TodoError::InvalidValue(tp.to_string(), "date range type".to_string()));
153 }
154 },
155 };
156 Ok(rng)
157 }
158}
159
160fn days_in_month(y: i32, m: u32) -> u32 {
161 match m {
162 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
163 2 => {
164 if y % 4 == 0 {
165 if y % 100 == 0 && y % 400 != 0 { 28 } else { 29 }
166 } else {
167 28
168 }
169 }
170 _ => 30,
171 }
172}
173
174pub fn add_months(dt: NaiveDate, num: u32, back: bool) -> NaiveDate {
181 let mut y = dt.year();
182 let mut m = dt.month();
183 let mut d = dt.day();
184 let mxd = days_in_month(y, m);
185 if back {
186 let full_years = num / 12;
187 let num = num % 12;
188 y -= full_years as i32;
189 m = if m > num {
190 m - num
191 } else {
192 y -= 1;
193 m + 12 - num
194 };
195 } else {
196 m += num;
197 if m > 12 {
198 m -= 1;
199 y += (m / 12) as i32;
200 m = (m % 12) + 1;
201 }
202 }
203 let new_mxd = days_in_month(y, m);
204 if mxd > d || d == mxd {
205 if d == mxd || new_mxd < d {
206 d = new_mxd
207 }
208 NaiveDate::from_ymd_opt(y, m, d).unwrap_or(dt)
209 } else {
210 NaiveDate::from_ymd_opt(y, m, new_mxd).unwrap_or(dt)
211 }
212}
213
214pub fn add_years(dt: NaiveDate, num: u32, back: bool) -> NaiveDate {
217 let mut y = dt.year();
218 let m = dt.month();
219 let mut d = dt.day();
220 if back {
221 y -= num as i32;
222 } else {
223 y += num as i32;
224 }
225 if d > days_in_month(y, m) {
226 d = days_in_month(y, m);
227 }
228 NaiveDate::from_ymd_opt(y, m, d).unwrap_or(dt)
229}
230
231fn abs_time_diff(base: NaiveDate, human: &str, back: bool) -> HumanResult {
232 let mut num = 0u32;
233 let mut dt = base;
234
235 for c in human.chars() {
236 match c.to_digit(10) {
237 None => {
238 if num != 0 {
239 match c {
240 'd' => {
241 let dur = if back { Duration::days(-(num as i64)) } else { Duration::days(num as i64) };
242 dt += dur;
243 }
244 'w' => {
245 let dur = if back { Duration::weeks(-(num as i64)) } else { Duration::weeks(num as i64) };
246 dt += dur;
247 }
248 'm' => {
249 dt = add_months(dt, num, back);
250 }
251 'y' => {
252 let mut y = dt.year();
253 let m = dt.month();
254 let mut d = dt.day();
255 let mxd = days_in_month(y, m);
256 if back {
257 y -= num as i32;
258 } else {
259 y += num as i32;
260 };
261 let new_mxd = days_in_month(y, m);
262 if mxd > d || d == mxd {
263 if new_mxd < d || d == mxd {
264 d = new_mxd;
265 }
266 dt = NaiveDate::from_ymd_opt(y, m, d).unwrap_or(base);
267 } else {
268 dt = NaiveDate::from_ymd_opt(y, m, new_mxd).unwrap_or(base);
269 }
270 }
271 _ => {}
272 }
273 num = 0;
274 }
275 }
276 Some(i) => num = num * 10 + i,
277 }
278 }
279 if base == dt {
280 return Err(format!("invalid date '{human}'"));
282 }
283 Ok(dt)
284}
285
286pub fn next_weekday(base: NaiveDate, wd: Weekday) -> NaiveDate {
292 let base_wd = base.weekday();
293 let (bn, wn) = (base_wd.number_from_monday(), wd.number_from_monday());
294 if bn < wn {
295 base + Duration::days((wn - bn) as i64)
297 } else {
298 base + Duration::days((DAYS_PER_WEEK + wn - bn) as i64)
300 }
301}
302
303pub fn prev_weekday(base: NaiveDate, wd: Weekday) -> NaiveDate {
309 let base_wd = base.weekday();
310 let (bn, wn) = (base_wd.number_from_monday(), wd.number_from_monday());
311 if bn > wn {
312 base - Duration::days(bn as i64 - wn as i64)
314 } else {
315 base + Duration::days(wn as i64 - bn as i64 - DAYS_PER_WEEK as i64)
317 }
318}
319
320fn day_of_first_month(base: NaiveDate, human: &str) -> HumanResult {
326 match human.parse::<u32>() {
327 Err(e) => Err(format!("invalid day of month: {e:?}")),
328 Ok(n) => {
329 if n == 0 || n > 31 {
330 Err(format!("Day number too big: {n}"))
331 } else {
332 let mut m = base.month();
333 let mut y = base.year();
334 let mut d = base.day();
335 let bdays = days_in_month(y, m);
336 if d >= n {
337 if m == 12 {
338 m = 1;
339 y += 1;
340 } else {
341 m += 1;
342 }
343 }
344 d = if n >= days_in_month(y, m) || n >= bdays { days_in_month(y, m) } else { n };
345 Ok(NaiveDate::from_ymd_opt(y, m, d).unwrap_or(base))
346 }
347 }
348 }
349}
350
351fn no_year_date(base: NaiveDate, human: &str) -> HumanResult {
352 let parts: Vec<_> = human.split('-').collect();
353 if parts.len() != 2 {
354 return Err("expected date in format MONTH-DAY".to_string());
355 }
356 let y = base.year();
357 let m = match parts[0].parse::<u32>() {
358 Err(_) => return Err(format!("invalid month number: {}", parts[0])),
359 Ok(n) => {
360 if !(1..=12).contains(&n) {
361 return Err(format!("month number must be between 1 and 12 ({n})"));
362 }
363 n
364 }
365 };
366 let d = match parts[1].parse::<u32>() {
367 Err(_) => return Err(format!("invalid day number: {}", parts[1])),
368 Ok(n) => {
369 if !(1..=31).contains(&n) {
370 return Err(format!("day number must be between 1 and 31 ({n})"));
371 }
372 let mx = days_in_month(y, m);
373 if n > mx { mx } else { n }
374 }
375 };
376 let dt = NaiveDate::from_ymd_opt(y, m, d).unwrap_or(base);
377 if dt < base {
378 let y = y + 1;
379 let mx = days_in_month(y, m);
380 let d = if mx < d { mx } else { d };
381 Ok(NaiveDate::from_ymd_opt(y, m, d).unwrap_or(base))
382 } else {
383 Ok(dt)
384 }
385}
386
387fn is_absolute(name: &str) -> bool {
390 matches!(name, "today" | "tomorrow" | "tmr" | "tm" | "yesterday" | "overdue")
391}
392
393fn month_to_index(name: &str) -> u32 {
394 match name {
395 "jan" | "january" => 1,
396 "feb" | "february" => 2,
397 "mar" | "march" => 3,
398 "apr" | "april" => 4,
399 "may" => 5,
400 "jun" | "june" => 6,
401 "jul" | "july" => 7,
402 "aug" | "august" => 8,
403 "sep" | "september" => 9,
404 "oct" | "october" => 10,
405 "nov" | "november" => 11,
406 "dec" | "december" => 12,
407 _ => unreachable!(),
408 }
409}
410
411fn special_time_point(base: NaiveDate, human: &str, back: bool, soon_days: u8) -> HumanResult {
412 let s = human.replace(&['-', '_'][..], "").to_lowercase();
413 if back && is_absolute(human) {
414 return Err(format!("'{human}' cannot be back"));
415 }
416 match s.as_str() {
417 "today" => Ok(base),
418 "tomorrow" | "tmr" | "tm" => Ok(base.succ_opt().unwrap_or(base)),
419 "yesterday" => Ok(base.pred_opt().unwrap_or(base)),
420 "overdue" => Ok(base + Duration::days(FAR_PAST)),
421 "soon" => {
422 let dur = Duration::days(soon_days as i64);
423 Ok(if back { base - dur } else { base + dur })
424 }
425 "jan" | "january" | "feb" | "february" | "mar" | "march" | "apr" | "april" | "may" | "jun" | "june" | "jul"
426 | "july" | "aug" | "august" | "sep" | "september" | "oct" | "october" | "nov" | "november" | "dec"
427 | "december" => {
428 let y = base.year();
429 let m_idx = month_to_index(&s);
430 if back {
431 if base.month() == m_idx && base.day() > 1 {
432 Ok(NaiveDate::from_ymd_opt(y, m_idx, 1).unwrap_or(base))
433 } else {
434 Ok(NaiveDate::from_ymd_opt(y - 1, m_idx, 1).unwrap_or(base))
435 }
436 } else if base.month() < m_idx {
437 Ok(NaiveDate::from_ymd_opt(y, m_idx, 1).unwrap_or(base))
438 } else {
439 Ok(NaiveDate::from_ymd_opt(y + 1, m_idx, 1).unwrap_or(base))
440 }
441 }
442 "first" => {
443 let mut y = base.year();
444 let mut m = base.month();
445 let d = base.day();
446 if !back {
447 if m < 12 {
448 m += 1;
449 } else {
450 y += 1;
451 m = 1;
452 }
453 } else if d == 1 {
454 if m == 1 {
455 m = 12;
456 y -= 1;
457 } else {
458 m -= 1;
459 }
460 }
461 Ok(NaiveDate::from_ymd_opt(y, m, 1).unwrap_or(base))
462 }
463 "last" => {
464 let mut y = base.year();
465 let mut m = base.month();
466 let mut d = base.day();
467 let last_day = days_in_month(y, m);
468 if back {
469 if m == 1 {
470 m = 12;
471 y -= 1;
472 } else {
473 m -= 1;
474 }
475 } else if d == last_day {
476 if m < 12 {
477 m += 1;
478 } else {
479 m = 1;
480 y += 1;
481 }
482 }
483 d = days_in_month(y, m);
484 Ok(NaiveDate::from_ymd_opt(y, m, d).unwrap_or(base))
485 }
486 "monday" | "mon" | "mo" => {
487 if back {
488 Ok(prev_weekday(base, Weekday::Mon))
489 } else {
490 Ok(next_weekday(base, Weekday::Mon))
491 }
492 }
493 "tuesday" | "tue" | "tu" => {
494 if back {
495 Ok(prev_weekday(base, Weekday::Tue))
496 } else {
497 Ok(next_weekday(base, Weekday::Tue))
498 }
499 }
500 "wednesday" | "wed" | "we" => {
501 if back {
502 Ok(prev_weekday(base, Weekday::Wed))
503 } else {
504 Ok(next_weekday(base, Weekday::Wed))
505 }
506 }
507 "thursday" | "thu" | "th" => {
508 if back {
509 Ok(prev_weekday(base, Weekday::Thu))
510 } else {
511 Ok(next_weekday(base, Weekday::Thu))
512 }
513 }
514 "friday" | "fri" | "fr" => {
515 if back {
516 Ok(prev_weekday(base, Weekday::Fri))
517 } else {
518 Ok(next_weekday(base, Weekday::Fri))
519 }
520 }
521 "saturday" | "sat" | "sa" => {
522 if back {
523 Ok(prev_weekday(base, Weekday::Sat))
524 } else {
525 Ok(next_weekday(base, Weekday::Sat))
526 }
527 }
528 "sunday" | "sun" | "su" => {
529 if back {
530 Ok(prev_weekday(base, Weekday::Sun))
531 } else {
532 Ok(next_weekday(base, Weekday::Sun))
533 }
534 }
535 _ => Err(format!("invalid date '{human}'")),
536 }
537}
538
539pub fn human_to_date(base: NaiveDate, human: &str, soon_days: u8) -> HumanResult {
543 if human.is_empty() {
544 return Err("empty date".to_string());
545 }
546 let back = human.starts_with('-');
547 let human = if back { &human[1..] } else { human };
548
549 if human.find(|c: char| !c.is_ascii_digit()).is_none() {
550 if back {
551 return Err("negative day of month".to_string());
552 }
553 return day_of_first_month(base, human);
554 }
555 if human.find(|c: char| !c.is_ascii_digit() && c != '-').is_none() {
556 if back {
557 return Err("negative absolute date".to_string());
558 }
559 if human.matches('-').count() == 1 {
560 return no_year_date(base, human);
562 }
563 return Err(NO_CHANGE.to_string());
565 }
566 if human.find(|c: char| c < '0' || (c > '9' && c != 'd' && c != 'm' && c != 'w' && c != 'y')).is_none() {
567 return abs_time_diff(base, human, back);
568 }
569
570 special_time_point(base, human, back, soon_days)
572}
573
574pub fn fix_date(base: NaiveDate, orig: &str, look_for: &str, soon_days: u8) -> Option<String> {
577 if orig.is_empty() || look_for.is_empty() {
578 return None;
579 }
580 let spaced = " ".to_string() + look_for;
581 let start = if orig.starts_with(look_for) {
582 0
583 } else if let Some(p) = orig.find(&spaced) {
584 p + " ".len()
585 } else {
586 return None;
587 };
588 let substr = &orig[start + look_for.len()..];
589 let human = if let Some(p) = substr.find(' ') { &substr[..p] } else { substr };
590 match human_to_date(base, human, soon_days) {
591 Err(_) => None,
592 Ok(new_date) => {
593 let what = look_for.to_string() + human;
594 let with = look_for.to_string() + new_date.format("%Y-%m-%d").to_string().as_str();
595 Some(orig.replace(what.as_str(), with.as_str()))
596 }
597 }
598}
599
600pub fn is_range_with_none(human: &str) -> bool {
606 if !is_range(human) {
607 return false;
608 }
609 human.starts_with("none..") || human.ends_with("..none") || human.starts_with("none:") || human.ends_with(":none")
610}
611
612pub fn human_to_range_with_none(
615 base: NaiveDate,
616 human: &str,
617 soon_days: u8,
618) -> Result<tfilter::DateRange, terr::TodoError> {
619 let parts: Vec<&str> = if human.find(':').is_none() {
620 human.split("..").filter(|s| !s.is_empty()).collect()
621 } else {
622 human.split(':').filter(|s| !s.is_empty()).collect()
623 };
624 if parts.len() > 2 {
625 return Err(range_error(human));
626 }
627 if parts[1] == "none" {
628 match human_to_date(base, parts[0], soon_days) {
629 Err(e) => Err(range_error(&e)),
630 Ok(d) => Ok(tfilter::DateRange {
631 days: tfilter::ValueRange { high: tfilter::INCLUDE_NONE, low: (d - base).num_days() },
632 span: tfilter::ValueSpan::Range,
633 }),
634 }
635 } else if parts[0] == "none" {
636 match human_to_date(base, parts[1], soon_days) {
637 Err(e) => Err(range_error(&e)),
638 Ok(d) => Ok(tfilter::DateRange {
639 days: tfilter::ValueRange { low: tfilter::INCLUDE_NONE, high: (d - base).num_days() },
640 span: tfilter::ValueSpan::Range,
641 }),
642 }
643 } else {
644 Err(range_error(human))
645 }
646}
647
648pub fn is_range(human: &str) -> bool {
654 human.contains("..") || human.contains(':')
655}
656
657fn range_error(msg: &str) -> terr::TodoError {
658 terr::TodoError::InvalidValue(msg.to_string(), "date range".to_string())
659}
660
661pub fn human_to_range(base: NaiveDate, human: &str, soon_days: u8) -> Result<tfilter::DateRange, terr::TodoError> {
666 let parts: Vec<&str> = if human.find(':').is_none() {
667 human.split("..").filter(|s| !s.is_empty()).collect()
668 } else {
669 human.split(':').filter(|s| !s.is_empty()).collect()
670 };
671 if parts.len() > 2 {
672 return Err(range_error(human));
673 }
674 let left_open = human.starts_with(':') || human.starts_with("..");
675 if parts.len() == 2 {
676 let mut begin = match human_to_date(base, parts[0], soon_days) {
677 Ok(d) => d,
678 Err(e) => return Err(range_error(&e)),
679 };
680 let mut end = match human_to_date(base, parts[1], soon_days) {
681 Ok(d) => d,
682 Err(e) => return Err(range_error(&e)),
683 };
684 if begin > end {
685 mem::swap(&mut begin, &mut end);
686 }
687 return Ok(tfilter::DateRange {
688 days: tfilter::ValueRange { low: (begin - base).num_days(), high: (end - base).num_days() },
689 span: tfilter::ValueSpan::Range,
690 });
691 }
692 if left_open {
693 let end = match human_to_date(base, parts[0], soon_days) {
694 Ok(d) => d,
695 Err(e) => return Err(range_error(&e)),
696 };
697 let diff = (end - base).num_days() + 1;
698 return Ok(tfilter::DateRange {
699 days: tfilter::ValueRange { low: diff, high: 0 },
700 span: tfilter::ValueSpan::Lower,
701 });
702 }
703 match human_to_date(base, parts[0], soon_days) {
704 Ok(begin) => {
705 let diff = (begin - base).num_days() - 1;
706 Ok(tfilter::DateRange {
707 days: tfilter::ValueRange { low: 0, high: diff },
708 span: tfilter::ValueSpan::Higher,
709 })
710 }
711 Err(e) => Err(range_error(&e)),
712 }
713}
714
715pub fn calendar_first_day(today: NaiveDate, rng: &CalendarRange, first_sunday: bool) -> NaiveDate {
724 match rng.rng {
725 CalendarRangeType::Days(n) => {
726 if n >= 0 {
727 today
728 } else {
729 let diff = n + 1;
730 today.checked_add_signed(Duration::days(diff.into())).unwrap_or(today)
731 }
732 }
733 CalendarRangeType::DayRange(n, _) => today.checked_add_signed(Duration::days(n.into())).unwrap_or(today),
734 CalendarRangeType::Weeks(n) => {
735 let is_first =
736 (today.weekday() == Weekday::Sun && first_sunday) || (today.weekday() == Weekday::Mon && !first_sunday);
737 let today = if rng.strict || is_first {
738 today
739 } else {
740 match first_sunday {
741 true => prev_weekday(today, Weekday::Sun),
742 false => prev_weekday(today, Weekday::Mon),
743 }
744 };
745 if rng.strict || n >= -1 {
746 return today;
747 }
748 let diff = if rng.strict {
749 n
750 } else if n > 0 {
751 n - 1
752 } else {
753 n + 1
754 };
755 today.checked_add_signed(Duration::weeks(diff.into())).unwrap_or(today)
756 }
757 CalendarRangeType::WeekRange(n, _) => {
758 let diff = if rng.strict {
759 n
760 } else if n > 0 {
761 n - 1
762 } else {
763 n + 1
764 };
765 today.checked_add_signed(Duration::weeks(diff.into())).unwrap_or(today)
766 }
767 CalendarRangeType::Months(n) => {
768 if n >= 0 {
769 if rng.strict {
770 return today;
771 }
772 return NaiveDate::from_ymd_opt(today.year(), today.month(), 1).unwrap_or(today);
773 }
774 let (today, diff) = if rng.strict {
775 (today, -n)
776 } else {
777 (NaiveDate::from_ymd_opt(today.year(), today.month(), 1).unwrap_or(today), -n - 1)
778 };
779 let today = add_months(today, diff as u32, true);
780 if rng.strict {
781 return today.checked_add_signed(Duration::days(1)).unwrap_or(today);
782 }
783 today
784 }
785 CalendarRangeType::MonthRange(n, _) => {
786 let (today, diff) = if rng.strict {
787 (today, n)
788 } else {
789 (
790 NaiveDate::from_ymd_opt(today.year(), today.month(), 1).unwrap_or(today),
791 if n > 0 { n - 1 } else { n + 1 },
792 )
793 };
794 add_months(today, diff.unsigned_abs() as u32, n < 0)
795 }
796 CalendarRangeType::Years(n) => {
797 if n >= 0 {
798 if rng.strict {
799 return today;
800 }
801 return NaiveDate::from_ymd_opt(today.year(), 1, 1).unwrap_or(today);
802 }
803 let (today, diff) = if rng.strict {
804 (today, -n)
805 } else {
806 (NaiveDate::from_ymd_opt(today.year(), 1, 1).unwrap_or(today), -n - 1)
807 };
808 add_years(today, diff as u32, n < 0)
809 }
810 CalendarRangeType::YearRange(n, _) => {
811 let (today, diff) =
812 if rng.strict { (today, n) } else { (NaiveDate::from_ymd_opt(today.year(), 1, 1).unwrap_or(today), n) };
813 add_years(today, diff.unsigned_abs() as u32, n < 0)
814 }
815 }
816}
817
818pub fn calendar_last_day(today: NaiveDate, rng: &CalendarRange, first_sunday: bool) -> NaiveDate {
827 match rng.rng {
828 CalendarRangeType::Days(n) => {
829 if n <= 0 {
830 return today;
831 }
832 let n = n - 1;
833 today.checked_add_signed(Duration::days(n.into())).unwrap_or(today)
834 }
835 CalendarRangeType::DayRange(_, n) => today.checked_add_signed(Duration::days(n.into())).unwrap_or(today),
836 CalendarRangeType::Weeks(n) => {
837 if rng.strict {
838 if n <= 0 {
839 return today;
840 }
841 return match today.checked_add_signed(Duration::weeks(n.into())) {
842 None => today,
843 Some(d) => d.checked_add_signed(Duration::days(-1)).unwrap_or(d),
844 };
845 }
846 let today = match first_sunday {
847 true => next_weekday(today, Weekday::Sat),
848 false => next_weekday(today, Weekday::Sun),
849 };
850 if n <= 1 {
851 return today;
852 }
853 let n = n - 1;
854 today.checked_add_signed(Duration::weeks(n.into())).unwrap_or(today)
855 }
856 CalendarRangeType::WeekRange(_, n) => today.checked_add_signed(Duration::weeks(n.into())).unwrap_or(today),
857 CalendarRangeType::Months(n) => {
858 if rng.strict {
859 if n <= 0 {
860 return today;
861 }
862 let today = add_months(today, n.unsigned_abs() as u32, n < 0);
863 return today.checked_add_signed(Duration::days(-1)).unwrap_or(today);
864 }
865 let last = days_in_month(today.year(), today.month());
866 let today = NaiveDate::from_ymd_opt(today.year(), today.month(), last).unwrap_or(today);
867 if n <= 1 {
868 return today;
869 }
870 let diff = n - 1;
871 add_months(today, diff.unsigned_abs() as u32, diff < 0)
872 }
873 CalendarRangeType::MonthRange(_, n) => {
874 let dt = add_months(today, n.unsigned_abs() as u32, n < 0);
875 if rng.strict {
876 dt
877 } else {
878 let y = dt.year();
879 let m = dt.month();
880 let d = days_in_month(y, m);
881 NaiveDate::from_ymd_opt(y, m, d).unwrap_or(dt)
882 }
883 }
884 CalendarRangeType::Years(n) => {
885 if rng.strict {
886 if n <= 0 {
887 return today;
888 }
889 return add_years(today, n as u32, false);
890 }
891 let dt = NaiveDate::from_ymd_opt(today.year(), 12, 31).unwrap_or(today);
892 if n <= 1 { dt } else { add_years(dt, (n - 1) as u32, false) }
893 }
894 CalendarRangeType::YearRange(_, n) => {
895 if rng.strict {
896 return add_years(today, n.unsigned_abs() as u32, n < 0);
897 }
898 let dt = add_years(today, n.unsigned_abs() as u32, n < 0);
899 NaiveDate::from_ymd_opt(dt.year(), 12, 31).unwrap_or(today)
900 }
901 }
902}
903
904#[cfg(test)]
905mod humandate_test {
906 use super::*;
907 use chrono::Local;
908
909 struct Test {
910 txt: &'static str,
911 val: NaiveDate,
912 }
913 struct TestRange {
914 txt: &'static str,
915 val: tfilter::DateRange,
916 }
917
918 #[test]
919 fn no_change() {
920 let dt = Local::now().date_naive();
921 let res = human_to_date(dt, "2010-10-10", 0);
922 let must = Err(NO_CHANGE.to_string());
923 assert_eq!(res, must)
924 }
925
926 #[test]
927 fn month_day() {
928 let dt = NaiveDate::from_ymd_opt(2020, 7, 9).unwrap();
929 let tests: Vec<Test> = vec![
930 Test { txt: "7", val: NaiveDate::from_ymd_opt(2020, 8, 7).unwrap() },
931 Test { txt: "11", val: NaiveDate::from_ymd_opt(2020, 7, 11).unwrap() },
932 Test { txt: "31", val: NaiveDate::from_ymd_opt(2020, 7, 31).unwrap() },
933 ];
934 for test in tests.iter() {
935 let nm = human_to_date(dt, test.txt, 0);
936 assert_eq!(nm, Ok(test.val), "{}", test.txt);
937 }
938
939 let dt = NaiveDate::from_ymd_opt(2020, 6, 9).unwrap();
940 let nm = human_to_date(dt, "31", 0);
941 assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 6, 30).unwrap()));
942 let dt = NaiveDate::from_ymd_opt(2020, 2, 4).unwrap();
943 let nm = human_to_date(dt, "31", 0);
944 assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 2, 29).unwrap()));
945 let dt = NaiveDate::from_ymd_opt(2020, 2, 29).unwrap();
946 let nm = human_to_date(dt, "29", 0);
947 assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 3, 31).unwrap()));
948
949 let nm = human_to_date(dt, "32", 0);
950 assert!(nm.is_err());
951 let nm = human_to_date(dt, "0", 0);
952 assert!(nm.is_err());
953 }
954
955 #[test]
956 fn month_and_day() {
957 let dt = NaiveDate::from_ymd_opt(2020, 7, 9).unwrap();
958 let nm = human_to_date(dt, "07-08", 0);
959 assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2021, 7, 8).unwrap()));
960 let nm = human_to_date(dt, "07-11", 0);
961 assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 7, 11).unwrap()));
962 let nm = human_to_date(dt, "02-31", 0);
963 assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2021, 2, 28).unwrap()));
964 }
965
966 #[test]
967 fn absolute() {
968 let dt = NaiveDate::from_ymd_opt(2020, 7, 9).unwrap();
969 let tests: Vec<Test> = vec![
970 Test { txt: "1w", val: NaiveDate::from_ymd_opt(2020, 7, 16).unwrap() },
971 Test { txt: "3d4d", val: NaiveDate::from_ymd_opt(2020, 7, 16).unwrap() },
972 Test { txt: "1y", val: NaiveDate::from_ymd_opt(2021, 7, 9).unwrap() },
973 Test { txt: "2w2d1m", val: NaiveDate::from_ymd_opt(2020, 8, 25).unwrap() },
974 Test { txt: "-1w", val: NaiveDate::from_ymd_opt(2020, 7, 2).unwrap() },
975 Test { txt: "-3d4d", val: NaiveDate::from_ymd_opt(2020, 7, 2).unwrap() },
976 Test { txt: "-1y", val: NaiveDate::from_ymd_opt(2019, 7, 9).unwrap() },
977 Test { txt: "-2w2d1m", val: NaiveDate::from_ymd_opt(2020, 5, 23).unwrap() },
978 ];
979 for test in tests.iter() {
980 let nm = human_to_date(dt, test.txt, 0);
981 assert_eq!(nm, Ok(test.val), "{}", test.txt);
982 }
983
984 let dt = NaiveDate::from_ymd_opt(2021, 2, 28).unwrap();
985 let tests: Vec<Test> = vec![
986 Test { txt: "1m", val: NaiveDate::from_ymd_opt(2021, 3, 31).unwrap() },
987 Test { txt: "1y", val: NaiveDate::from_ymd_opt(2022, 2, 28).unwrap() },
988 Test { txt: "3y", val: NaiveDate::from_ymd_opt(2024, 2, 29).unwrap() },
989 Test { txt: "-1m", val: NaiveDate::from_ymd_opt(2021, 1, 31).unwrap() },
990 Test { txt: "-1y", val: NaiveDate::from_ymd_opt(2020, 2, 29).unwrap() },
991 Test { txt: "-3y", val: NaiveDate::from_ymd_opt(2018, 2, 28).unwrap() },
992 ];
993 for test in tests.iter() {
994 let nm = human_to_date(dt, test.txt, 0);
995 assert_eq!(nm, Ok(test.val), "{}", test.txt);
996 }
997 }
998
999 #[test]
1000 fn special() {
1001 let dt = NaiveDate::from_ymd_opt(2020, 2, 29).unwrap();
1002 let nm = human_to_date(dt, "last", 0);
1003 assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 3, 31).unwrap()));
1004 let nm = human_to_date(dt, "-last", 0);
1005 assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 1, 31).unwrap()));
1006
1007 let dt = NaiveDate::from_ymd_opt(2020, 2, 10).unwrap();
1008 let nm = human_to_date(dt, "last", 0);
1009 assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 2, 29).unwrap()));
1010 let nm = human_to_date(dt, "-last", 0);
1011 assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 1, 31).unwrap()));
1012
1013 let dt = NaiveDate::from_ymd_opt(2020, 2, 1).unwrap();
1014 let nm = human_to_date(dt, "first", 0);
1015 assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 3, 1).unwrap()));
1016 let nm = human_to_date(dt, "-first", 0);
1017 assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()));
1018
1019 let dt = NaiveDate::from_ymd_opt(2020, 2, 10).unwrap();
1020 let nm = human_to_date(dt, "first", 0);
1021 assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 3, 1).unwrap()));
1022 let nm = human_to_date(dt, "-first", 0);
1023 assert_eq!(nm, Ok(NaiveDate::from_ymd_opt(2020, 2, 1).unwrap()));
1024
1025 let dt = NaiveDate::from_ymd_opt(2020, 7, 9).unwrap(); let tests: Vec<Test> = vec![
1027 Test { txt: "tmr", val: NaiveDate::from_ymd_opt(2020, 7, 10).unwrap() },
1028 Test { txt: "tm", val: NaiveDate::from_ymd_opt(2020, 7, 10).unwrap() },
1029 Test { txt: "tomorrow", val: NaiveDate::from_ymd_opt(2020, 7, 10).unwrap() },
1030 Test { txt: "today", val: NaiveDate::from_ymd_opt(2020, 7, 9).unwrap() },
1031 Test { txt: "first", val: NaiveDate::from_ymd_opt(2020, 8, 1).unwrap() },
1032 Test { txt: "last", val: NaiveDate::from_ymd_opt(2020, 7, 31).unwrap() },
1033 Test { txt: "mon", val: NaiveDate::from_ymd_opt(2020, 7, 13).unwrap() },
1034 Test { txt: "tu", val: NaiveDate::from_ymd_opt(2020, 7, 14).unwrap() },
1035 Test { txt: "wed", val: NaiveDate::from_ymd_opt(2020, 7, 15).unwrap() },
1036 Test { txt: "thursday", val: NaiveDate::from_ymd_opt(2020, 7, 16).unwrap() },
1037 Test { txt: "fri", val: NaiveDate::from_ymd_opt(2020, 7, 10).unwrap() },
1038 Test { txt: "sa", val: NaiveDate::from_ymd_opt(2020, 7, 11).unwrap() },
1039 Test { txt: "sunday", val: NaiveDate::from_ymd_opt(2020, 7, 12).unwrap() },
1040 Test { txt: "yesterday", val: NaiveDate::from_ymd_opt(2020, 7, 8).unwrap() },
1041 Test { txt: "-mon", val: NaiveDate::from_ymd_opt(2020, 7, 6).unwrap() },
1042 Test { txt: "-tu", val: NaiveDate::from_ymd_opt(2020, 7, 7).unwrap() },
1043 Test { txt: "-wed", val: NaiveDate::from_ymd_opt(2020, 7, 8).unwrap() },
1044 Test { txt: "-thursday", val: NaiveDate::from_ymd_opt(2020, 7, 2).unwrap() },
1045 Test { txt: "-fri", val: NaiveDate::from_ymd_opt(2020, 7, 3).unwrap() },
1046 Test { txt: "-sa", val: NaiveDate::from_ymd_opt(2020, 7, 4).unwrap() },
1047 Test { txt: "-sunday", val: NaiveDate::from_ymd_opt(2020, 7, 5).unwrap() },
1048 ];
1049 for test in tests.iter() {
1050 let nm = human_to_date(dt, test.txt, 0);
1051 assert_eq!(nm, Ok(test.val), "{}", test.txt);
1052 }
1053 }
1054
1055 #[test]
1056 fn range_test() {
1057 let dt = NaiveDate::from_ymd_opt(2020, 7, 9).unwrap();
1058 let tests: Vec<TestRange> = vec![
1059 TestRange {
1060 txt: "..tue",
1061 val: tfilter::DateRange {
1062 days: tfilter::ValueRange { low: 6, high: 0 },
1063 span: tfilter::ValueSpan::Lower,
1064 },
1065 },
1066 TestRange {
1067 txt: ":2d",
1068 val: tfilter::DateRange {
1069 days: tfilter::ValueRange { low: 3, high: 0 },
1070 span: tfilter::ValueSpan::Lower,
1071 },
1072 },
1073 TestRange {
1074 txt: "tue..",
1075 val: tfilter::DateRange {
1076 days: tfilter::ValueRange { low: 0, high: 4 },
1077 span: tfilter::ValueSpan::Higher,
1078 },
1079 },
1080 TestRange {
1081 txt: "3d:",
1082 val: tfilter::DateRange {
1083 days: tfilter::ValueRange { low: 0, high: 2 },
1084 span: tfilter::ValueSpan::Higher,
1085 },
1086 },
1087 TestRange {
1088 txt: "-tue..we",
1089 val: tfilter::DateRange {
1090 days: tfilter::ValueRange { low: -2, high: 6 },
1091 span: tfilter::ValueSpan::Range,
1092 },
1093 },
1094 TestRange {
1095 txt: "we..-tue",
1096 val: tfilter::DateRange {
1097 days: tfilter::ValueRange { low: -2, high: 6 },
1098 span: tfilter::ValueSpan::Range,
1099 },
1100 },
1101 TestRange {
1102 txt: "-tue..-wed",
1103 val: tfilter::DateRange {
1104 days: tfilter::ValueRange { low: -2, high: -1 },
1105 span: tfilter::ValueSpan::Range,
1106 },
1107 },
1108 TestRange {
1109 txt: "-1w:today",
1110 val: tfilter::DateRange {
1111 days: tfilter::ValueRange { low: -7, high: 0 },
1112 span: tfilter::ValueSpan::Range,
1113 },
1114 },
1115 TestRange {
1116 txt: "..soon",
1117 val: tfilter::DateRange {
1118 days: tfilter::ValueRange { low: 7, high: 0 },
1119 span: tfilter::ValueSpan::Lower,
1120 },
1121 },
1122 TestRange {
1123 txt: "soon..",
1124 val: tfilter::DateRange {
1125 days: tfilter::ValueRange { low: 0, high: 5 },
1126 span: tfilter::ValueSpan::Higher,
1127 },
1128 },
1129 TestRange {
1130 txt: "-soon..soon",
1131 val: tfilter::DateRange {
1132 days: tfilter::ValueRange { low: -6, high: 6 },
1133 span: tfilter::ValueSpan::Range,
1134 },
1135 },
1136 ];
1137 for test in tests.iter() {
1138 let rng = human_to_range(dt, test.txt, 6).unwrap();
1139 assert_eq!(rng, test.val, "{}", test.txt);
1140 }
1141 }
1142
1143 #[test]
1144 fn date_replace() {
1145 let dt = NaiveDate::from_ymd_opt(2020, 7, 9).unwrap();
1146 let s = fix_date(dt, "error due:xxxx next week", "due:", 0);
1147 assert_eq!(s, None);
1148 let s = fix_date(dt, "due: next week", "due:", 0);
1149 assert_eq!(s, None);
1150
1151 let s = fix_date(dt, "due:1w next week", "due:", 0);
1152 assert_eq!(s, Some("due:2020-07-16 next week".to_string()));
1153 let s = fix_date(dt, "next day due:1d", "due:", 0);
1154 assert_eq!(s, Some("next day due:2020-07-10".to_string()));
1155 let s = fix_date(dt, "special due:sat in the middle", "due:", 0);
1156 assert_eq!(s, Some("special due:2020-07-11 in the middle".to_string()));
1157 }
1158
1159 #[test]
1160 fn parse_calendar() {
1161 struct TestCal {
1162 txt: &'static str,
1163 err: bool,
1164 val: Option<CalendarRange>,
1165 }
1166 let tests: Vec<TestCal> = vec![
1167 TestCal {
1168 txt: "",
1169 err: false,
1170 val: Some(CalendarRange { strict: false, rng: CalendarRangeType::Days(1) }),
1171 },
1172 TestCal {
1173 txt: "12",
1174 err: false,
1175 val: Some(CalendarRange { strict: false, rng: CalendarRangeType::Days(12) }),
1176 },
1177 TestCal {
1178 txt: "w",
1179 err: false,
1180 val: Some(CalendarRange { strict: false, rng: CalendarRangeType::Weeks(1) }),
1181 },
1182 TestCal {
1183 txt: "+m",
1184 err: false,
1185 val: Some(CalendarRange { strict: true, rng: CalendarRangeType::Months(1) }),
1186 },
1187 TestCal {
1188 txt: "+-3d",
1189 err: false,
1190 val: Some(CalendarRange { strict: true, rng: CalendarRangeType::Days(-3) }),
1191 },
1192 TestCal { txt: "zzz", err: true, val: None },
1193 TestCal { txt: "*2d", err: true, val: None },
1194 TestCal { txt: "10r", err: true, val: None },
1195 TestCal { txt: "100m", err: true, val: None },
1196 ];
1197 for test in tests.iter() {
1198 let res = CalendarRange::parse(test.txt);
1199 if test.err {
1200 assert!(res.is_err(), "{}", test.txt);
1201 } else {
1202 assert!(!res.is_err(), "{}", test.txt);
1203 assert_eq!(res.unwrap(), test.val.unwrap(), "{}", test.txt);
1204 }
1205 }
1206 }
1207 #[test]
1208 fn calendar_first_date() {
1209 struct TestCal {
1210 td: NaiveDate,
1211 rng: CalendarRange,
1212 sunday: bool,
1213 res: NaiveDate,
1214 }
1215 let tests: Vec<TestCal> = vec![
1216 TestCal {
1217 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(-1) },
1219 sunday: true,
1220 res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1221 },
1222 TestCal {
1223 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1224 rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(-1) },
1225 sunday: false,
1226 res: NaiveDate::from_ymd_opt(2022, 06, 27).unwrap(),
1227 },
1228 TestCal {
1229 td: NaiveDate::from_ymd_opt(2022, 07, 04).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(-1) },
1231 sunday: true,
1232 res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1233 },
1234 TestCal {
1235 td: NaiveDate::from_ymd_opt(2022, 07, 04).unwrap(),
1236 rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(-1) },
1237 sunday: false,
1238 res: NaiveDate::from_ymd_opt(2022, 07, 04).unwrap(),
1239 },
1240 TestCal {
1242 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(1) },
1244 sunday: true,
1245 res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1246 },
1247 TestCal {
1248 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1249 rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(1) },
1250 sunday: false,
1251 res: NaiveDate::from_ymd_opt(2022, 06, 27).unwrap(),
1252 },
1253 TestCal {
1254 td: NaiveDate::from_ymd_opt(2022, 07, 04).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(1) },
1256 sunday: true,
1257 res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1258 },
1259 TestCal {
1260 td: NaiveDate::from_ymd_opt(2022, 07, 04).unwrap(),
1261 rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(1) },
1262 sunday: false,
1263 res: NaiveDate::from_ymd_opt(2022, 07, 04).unwrap(),
1264 },
1265 TestCal {
1267 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: true, rng: CalendarRangeType::Weeks(1) },
1269 sunday: true,
1270 res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1271 },
1272 TestCal {
1273 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1274 rng: CalendarRange { strict: true, rng: CalendarRangeType::Weeks(1) },
1275 sunday: false,
1276 res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1277 },
1278 TestCal {
1280 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: true, rng: CalendarRangeType::Weeks(2) },
1282 sunday: true,
1283 res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1284 },
1285 TestCal {
1286 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1287 rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(2) },
1288 sunday: false,
1289 res: NaiveDate::from_ymd_opt(2022, 06, 27).unwrap(),
1290 },
1291 TestCal {
1292 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1293 rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(2) },
1294 sunday: true,
1295 res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1296 },
1297 TestCal {
1299 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Days(15) },
1301 sunday: true,
1302 res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1303 },
1304 TestCal {
1305 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1306 rng: CalendarRange { strict: false, rng: CalendarRangeType::Days(15) },
1307 sunday: false,
1308 res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1309 },
1310 TestCal {
1311 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: true, rng: CalendarRangeType::Days(15) },
1313 sunday: true,
1314 res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1315 },
1316 TestCal {
1317 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1318 rng: CalendarRange { strict: true, rng: CalendarRangeType::Days(15) },
1319 sunday: false,
1320 res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1321 },
1322 TestCal {
1324 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Days(-5) },
1326 sunday: true,
1327 res: NaiveDate::from_ymd_opt(2022, 06, 29).unwrap(),
1328 },
1329 TestCal {
1330 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1331 rng: CalendarRange { strict: false, rng: CalendarRangeType::Days(-5) },
1332 sunday: false,
1333 res: NaiveDate::from_ymd_opt(2022, 06, 29).unwrap(),
1334 },
1335 TestCal {
1336 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: true, rng: CalendarRangeType::Days(-5) },
1338 sunday: true,
1339 res: NaiveDate::from_ymd_opt(2022, 06, 29).unwrap(),
1340 },
1341 TestCal {
1342 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1343 rng: CalendarRange { strict: true, rng: CalendarRangeType::Days(-5) },
1344 sunday: false,
1345 res: NaiveDate::from_ymd_opt(2022, 06, 29).unwrap(),
1346 },
1347 TestCal {
1349 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: true, rng: CalendarRangeType::Months(2) },
1351 sunday: true,
1352 res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1353 },
1354 TestCal {
1355 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1356 rng: CalendarRange { strict: true, rng: CalendarRangeType::Months(2) },
1357 sunday: false,
1358 res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1359 },
1360 TestCal {
1361 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Months(2) },
1363 sunday: true,
1364 res: NaiveDate::from_ymd_opt(2022, 07, 01).unwrap(),
1365 },
1366 TestCal {
1367 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1368 rng: CalendarRange { strict: false, rng: CalendarRangeType::Months(2) },
1369 sunday: false,
1370 res: NaiveDate::from_ymd_opt(2022, 07, 01).unwrap(),
1371 },
1372 TestCal {
1374 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: true, rng: CalendarRangeType::Months(-2) },
1376 sunday: true,
1377 res: NaiveDate::from_ymd_opt(2022, 05, 04).unwrap(),
1378 },
1379 TestCal {
1380 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1381 rng: CalendarRange { strict: true, rng: CalendarRangeType::Months(-2) },
1382 sunday: false,
1383 res: NaiveDate::from_ymd_opt(2022, 05, 04).unwrap(),
1384 },
1385 TestCal {
1386 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Months(-2) },
1388 sunday: true,
1389 res: NaiveDate::from_ymd_opt(2022, 06, 01).unwrap(),
1390 },
1391 TestCal {
1392 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1393 rng: CalendarRange { strict: false, rng: CalendarRangeType::Months(-2) },
1394 sunday: false,
1395 res: NaiveDate::from_ymd_opt(2022, 06, 01).unwrap(),
1396 },
1397 TestCal {
1399 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1400 rng: CalendarRange { strict: false, rng: CalendarRangeType::Years(-1) },
1401 sunday: false,
1402 res: NaiveDate::from_ymd_opt(2022, 01, 01).unwrap(),
1403 },
1404 ];
1405 for test in tests.iter() {
1406 let res = calendar_first_day(test.td, &test.rng, test.sunday);
1407 assert_eq!(res, test.res, "{} - SUN: {}, RANGE: {:?}", test.td, test.sunday, test.rng);
1408 }
1409 }
1410 #[test]
1411 fn calendar_last_date() {
1412 struct TestCal {
1413 td: NaiveDate,
1414 rng: CalendarRange,
1415 sunday: bool,
1416 res: NaiveDate,
1417 }
1418 let tests: Vec<TestCal> = vec![
1419 TestCal {
1420 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(-1) },
1422 sunday: true,
1423 res: NaiveDate::from_ymd_opt(2022, 07, 09).unwrap(),
1424 },
1425 TestCal {
1426 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1427 rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(-1) },
1428 sunday: false,
1429 res: NaiveDate::from_ymd_opt(2022, 07, 10).unwrap(),
1430 },
1431 TestCal {
1432 td: NaiveDate::from_ymd_opt(2022, 07, 04).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(-1) },
1434 sunday: true,
1435 res: NaiveDate::from_ymd_opt(2022, 07, 09).unwrap(),
1436 },
1437 TestCal {
1438 td: NaiveDate::from_ymd_opt(2022, 07, 04).unwrap(),
1439 rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(-1) },
1440 sunday: false,
1441 res: NaiveDate::from_ymd_opt(2022, 07, 10).unwrap(),
1442 },
1443 TestCal {
1445 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(1) },
1447 sunday: true,
1448 res: NaiveDate::from_ymd_opt(2022, 07, 09).unwrap(),
1449 },
1450 TestCal {
1451 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1452 rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(1) },
1453 sunday: false,
1454 res: NaiveDate::from_ymd_opt(2022, 07, 10).unwrap(),
1455 },
1456 TestCal {
1457 td: NaiveDate::from_ymd_opt(2022, 07, 04).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(1) },
1459 sunday: true,
1460 res: NaiveDate::from_ymd_opt(2022, 07, 09).unwrap(),
1461 },
1462 TestCal {
1463 td: NaiveDate::from_ymd_opt(2022, 07, 04).unwrap(),
1464 rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(1) },
1465 sunday: false,
1466 res: NaiveDate::from_ymd_opt(2022, 07, 10).unwrap(),
1467 },
1468 TestCal {
1470 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: true, rng: CalendarRangeType::Weeks(1) },
1472 sunday: true,
1473 res: NaiveDate::from_ymd_opt(2022, 07, 09).unwrap(),
1474 },
1475 TestCal {
1476 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1477 rng: CalendarRange { strict: true, rng: CalendarRangeType::Weeks(1) },
1478 sunday: false,
1479 res: NaiveDate::from_ymd_opt(2022, 07, 09).unwrap(),
1480 },
1481 TestCal {
1483 td: NaiveDate::from_ymd_opt(2022, 07, 05).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Weeks(2) },
1485 sunday: true,
1486 res: NaiveDate::from_ymd_opt(2022, 07, 16).unwrap(),
1487 },
1488 TestCal {
1489 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1490 rng: CalendarRange { strict: true, rng: CalendarRangeType::Weeks(2) },
1491 sunday: false,
1492 res: NaiveDate::from_ymd_opt(2022, 07, 16).unwrap(),
1493 },
1494 TestCal {
1496 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Days(15) },
1498 sunday: true,
1499 res: NaiveDate::from_ymd_opt(2022, 07, 17).unwrap(),
1500 },
1501 TestCal {
1502 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1503 rng: CalendarRange { strict: false, rng: CalendarRangeType::Days(15) },
1504 sunday: false,
1505 res: NaiveDate::from_ymd_opt(2022, 07, 17).unwrap(),
1506 },
1507 TestCal {
1508 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: true, rng: CalendarRangeType::Days(15) },
1510 sunday: true,
1511 res: NaiveDate::from_ymd_opt(2022, 07, 17).unwrap(),
1512 },
1513 TestCal {
1514 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1515 rng: CalendarRange { strict: true, rng: CalendarRangeType::Days(15) },
1516 sunday: false,
1517 res: NaiveDate::from_ymd_opt(2022, 07, 17).unwrap(),
1518 },
1519 TestCal {
1521 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Days(-5) },
1523 sunday: true,
1524 res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1525 },
1526 TestCal {
1527 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1528 rng: CalendarRange { strict: false, rng: CalendarRangeType::Days(-5) },
1529 sunday: false,
1530 res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1531 },
1532 TestCal {
1533 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: true, rng: CalendarRangeType::Days(-5) },
1535 sunday: true,
1536 res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1537 },
1538 TestCal {
1539 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1540 rng: CalendarRange { strict: true, rng: CalendarRangeType::Days(-5) },
1541 sunday: false,
1542 res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1543 },
1544 TestCal {
1546 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: true, rng: CalendarRangeType::Months(2) },
1548 sunday: true,
1549 res: NaiveDate::from_ymd_opt(2022, 09, 02).unwrap(),
1550 },
1551 TestCal {
1552 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1553 rng: CalendarRange { strict: true, rng: CalendarRangeType::Months(2) },
1554 sunday: false,
1555 res: NaiveDate::from_ymd_opt(2022, 09, 02).unwrap(),
1556 },
1557 TestCal {
1558 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Months(2) },
1560 sunday: true,
1561 res: NaiveDate::from_ymd_opt(2022, 08, 31).unwrap(),
1562 },
1563 TestCal {
1564 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1565 rng: CalendarRange { strict: false, rng: CalendarRangeType::Months(2) },
1566 sunday: false,
1567 res: NaiveDate::from_ymd_opt(2022, 08, 31).unwrap(),
1568 },
1569 TestCal {
1571 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: true, rng: CalendarRangeType::Months(-2) },
1573 sunday: true,
1574 res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1575 },
1576 TestCal {
1577 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1578 rng: CalendarRange { strict: true, rng: CalendarRangeType::Months(-2) },
1579 sunday: false,
1580 res: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1581 },
1582 TestCal {
1583 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), rng: CalendarRange { strict: false, rng: CalendarRangeType::Months(-2) },
1585 sunday: true,
1586 res: NaiveDate::from_ymd_opt(2022, 07, 31).unwrap(),
1587 },
1588 TestCal {
1589 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1590 rng: CalendarRange { strict: false, rng: CalendarRangeType::Months(-2) },
1591 sunday: false,
1592 res: NaiveDate::from_ymd_opt(2022, 07, 31).unwrap(),
1593 },
1594 TestCal {
1596 td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(),
1597 rng: CalendarRange { strict: false, rng: CalendarRangeType::Years(-1) },
1598 sunday: false,
1599 res: NaiveDate::from_ymd_opt(2022, 12, 31).unwrap(),
1600 },
1601 ];
1602 for test in tests.iter() {
1603 let res = calendar_last_day(test.td, &test.rng, test.sunday);
1604 assert_eq!(res, test.res, "{} - SUN: {}, RANGE: {:?}", test.td, test.sunday, test.rng);
1605 }
1606 }
1607 #[test]
1608 fn months_test() {
1609 struct TestCal {
1610 val: &'static str,
1611 base: &'static str,
1612 res: &'static str,
1613 }
1614 let tests: Vec<TestCal> = vec![
1615 TestCal { val: "jan", base: "2001-02-03", res: "2002-01-01" },
1616 TestCal { val: "january", base: "2001-01-01", res: "2002-01-01" },
1617 TestCal { val: "feb", base: "2001-02-03", res: "2002-02-01" },
1618 TestCal { val: "february", base: "2001-01-12", res: "2001-02-01" },
1619 TestCal { val: "mar", base: "2001-03-04", res: "2002-03-01" },
1620 TestCal { val: "march", base: "2001-01-12", res: "2001-03-01" },
1621 TestCal { val: "apr", base: "2001-04-04", res: "2002-04-01" },
1622 TestCal { val: "april", base: "2001-01-12", res: "2001-04-01" },
1623 TestCal { val: "may", base: "2001-05-04", res: "2002-05-01" },
1624 TestCal { val: "may", base: "2001-01-12", res: "2001-05-01" },
1625 TestCal { val: "jun", base: "2001-06-04", res: "2002-06-01" },
1626 TestCal { val: "june", base: "2001-01-12", res: "2001-06-01" },
1627 TestCal { val: "jul", base: "2001-07-04", res: "2002-07-01" },
1628 TestCal { val: "july", base: "2001-01-12", res: "2001-07-01" },
1629 TestCal { val: "aug", base: "2001-08-04", res: "2002-08-01" },
1630 TestCal { val: "august", base: "2001-01-12", res: "2001-08-01" },
1631 TestCal { val: "sep", base: "2001-09-04", res: "2002-09-01" },
1632 TestCal { val: "september", base: "2001-01-12", res: "2001-09-01" },
1633 TestCal { val: "oct", base: "2001-10-04", res: "2002-10-01" },
1634 TestCal { val: "october", base: "2001-01-12", res: "2001-10-01" },
1635 TestCal { val: "nov", base: "2001-11-04", res: "2002-11-01" },
1636 TestCal { val: "november", base: "2001-01-12", res: "2001-11-01" },
1637 TestCal { val: "dec", base: "2001-12-04", res: "2002-12-01" },
1638 TestCal { val: "december", base: "2001-01-12", res: "2001-12-01" },
1639 ];
1640 for test in tests.iter() {
1641 let base = NaiveDate::parse_from_str(test.base, "%Y-%m-%d").unwrap();
1642 let expect = NaiveDate::parse_from_str(test.res, "%Y-%m-%d").unwrap();
1643 let res = special_time_point(base, test.val, false, 0).unwrap();
1644 assert_eq!(res, expect, "{} for base {} should be {}, got {res}", test.val, test.base, test.res);
1645 }
1646 }
1647}