todo_lib/
human_date.rs

1use chrono::{Datelike, Duration, NaiveDate, Weekday};
2use std::mem;
3
4use crate::{terr, tfilter};
5
6/// Date converting function returns this value if passed argument and new value are the same.
7/// In other words, the value means that after conversion the value was not changed.
8pub const NO_CHANGE: &str = "no change";
9const DAYS_PER_WEEK: u32 = 7;
10const FAR_PAST: i64 = -100 * 365; // far in the past
11
12type HumanResult = Result<NaiveDate, String>;
13
14/// Calendar date range.
15#[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/// Date ranges for calendar.
28#[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    /// Creates calendar range from a string.
53    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
174/// Add to or subtract from a date `num` months.
175/// `back`: `true' = subtract months, `false` = add months.
176/// A special case:
177///     if `dt` is the last day of a month, the resulting value is also the last day.
178///     Example for `dt`=`2023-02-28`:
179///         add_month(dt, 1, false) ==> `2023-03-31`
180pub 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
214/// Add to or subtract from a date `num` years.
215/// `back`: `true' = subtract years, `false` = add years.
216pub 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        // bad due date
281        return Err(format!("invalid date '{human}'"));
282    }
283    Ok(dt)
284}
285
286/// Find the date of the next week day that is later than `base`.
287/// Examples:
288///     Base date is Saturday: `dt` = `2024-11-16`
289///     next_weekday(dt, Weekday::Saturday) ==> `2024-11-23`
290///     next_weekday(dt, Weekday::Sunday) ==> `2024-11-17`
291pub 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        // this week
296        base + Duration::days((wn - bn) as i64)
297    } else {
298        // next week
299        base + Duration::days((DAYS_PER_WEEK + wn - bn) as i64)
300    }
301}
302
303/// Find the date of the previous week day that was earlier than `base`.
304/// Examples:
305///     Base date is Saturday: `dt` = `2024-11-16`
306///     prev_weekday(dt, Weekday::Saturday) ==> `2024-11-09`
307///     next_weekday(dt, Weekday::Friday) ==> `2024-11-15`
308pub 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        // this week
313        base - Duration::days(bn as i64 - wn as i64)
314    } else {
315        // week before
316        base + Duration::days(wn as i64 - bn as i64 - DAYS_PER_WEEK as i64)
317    }
318}
319
320// Converts "human" which is a string contains a number to a date.
321// "human" is a day of a date. If today's day is less than "human", the function returns the
322// "human" date of the next month, otherwise of this month.
323// E.g: today=2022-06-20, human="24" --> 2022-06-24
324//      today=2022-06-20, human="19" --> 2022-07-19
325fn 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
387// Returns if a special day is always either in the future or in the past. E.g., `today` cannot be in
388// the past and `yesterday` cannot be in the future, so the function returns `true` for both.
389fn 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
539/// Converts human-readable date to an absolute date in todo-txt format.
540/// If the date is already an absolute value, the function returns None.
541/// In case of any error None is returned as well.
542pub 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            // month-day case
561            return no_year_date(base, human);
562        }
563        // normal date, nothing to fix
564        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    // some "special" word like "tomorrow", "tue"
571    special_time_point(base, human, back, soon_days)
572}
573
574/// Replace a special word in due date with a real date.
575/// E.g, "due:sat" ==> "due:2022-07-09" for today between 2022-07-03 and 2022-07-09
576pub 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
600/// Returns true if any of ends of the range is open.
601/// Example:
602///     is_range_with_none("today..tomorrow") ==> false
603///     is_range_with_none("..tomorrow") ==> true
604///     is_range_with_none("today..") ==> true
605pub 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
612/// Converts a human-readable date range with an open end to an internal range.
613/// `soon_days` is a value for a special case when a range end equals `soon`.
614pub 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
648/// Returns true if the passed value is a date range.
649/// Range can be defined with two dots or semicolon.
650/// Examples:
651///     is_range("today..tomorrow") ==> true
652///     is_range("today:tomorrow") ==> true
653pub 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
661/// Converts a string to a date range.
662/// Special cases:
663///     `base` is used for range ends defined with relative days like `today` or `tomorrow`;
664///     `soon_days` is for range ends defined with a word `soon`.
665pub 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
715/// Returns the first date of a calendar depending on the calendar range `rng`.
716/// The value `first_sunday` defines the first day of a week:
717///     `true` => Weekday::Sunday
718///     `false` => Weekday::Monday
719/// For day ranges like `5d` the date is exact.
720/// For week ranges the date is always the first day of a week
721/// For month ranges the date is always the first day of a month
722/// For year ranges the date is always the first day of a year
723pub 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
818/// Returns the last date of a calendar depending on the calendar range `rng`.
819/// The value `first_sunday` defines the first day of a week:
820///     `true` => Weekday::Sunday
821///     `false` => Weekday::Monday
822/// For day ranges like `5d` the date is exact.
823/// For week ranges the date is always the last day of a week
824/// For month ranges the date is always the last day of a month
825/// For year ranges the date is always the last day of a year
826pub 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(); // thursday
1026        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(), // Sunday
1218                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(), // Monday
1230                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            // ---
1241            TestCal {
1242                td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), // Sunday
1243                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(), // Monday
1255                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            // ---
1266            TestCal {
1267                td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), // Sunday
1268                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            // ---
1279            TestCal {
1280                td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), // Sunday
1281                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            // ---
1298            TestCal {
1299                td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), // Sunday
1300                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(), // Sunday
1312                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            // ---
1323            TestCal {
1324                td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), // Sunday
1325                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(), // Sunday
1337                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            // ---
1348            TestCal {
1349                td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), // Sunday
1350                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(), // Sunday
1362                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            // ---
1373            TestCal {
1374                td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), // Sunday
1375                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(), // Sunday
1387                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            // ---
1398            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(), // Sunday
1421                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(), // Monday
1433                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            // ---
1444            TestCal {
1445                td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), // Sunday
1446                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(), // Monday
1458                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            // ---
1469            TestCal {
1470                td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), // Sunday
1471                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            // ---
1482            TestCal {
1483                td: NaiveDate::from_ymd_opt(2022, 07, 05).unwrap(), // Sunday
1484                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            // ---
1495            TestCal {
1496                td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), // Sunday
1497                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(), // Sunday
1509                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            // ---
1520            TestCal {
1521                td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), // Sunday
1522                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(), // Sunday
1534                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            // ---
1545            TestCal {
1546                td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), // Sunday
1547                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(), // Sunday
1559                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            // ---
1570            TestCal {
1571                td: NaiveDate::from_ymd_opt(2022, 07, 03).unwrap(), // Sunday
1572                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(), // Sunday
1584                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            // ---
1595            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}