ex-cli 1.20.1

Command line tool to find, filter, sort and list files.
Documentation
use chrono::{DateTime, Datelike, NaiveDate, NaiveTime, TimeDelta, TimeZone, Utc};
use std::ops::AddAssign;

pub struct Calendar {
    year: i32,
    month: u32,
    day: u32,
    time: NaiveTime,
}

// noinspection RsLift
impl Calendar {
    pub fn from_time<Tz: TimeZone>(time: &DateTime<Utc>, zone: &Tz) -> Self {
        let time = time.with_timezone(zone);
        let date = time.date_naive();
        let time = time.time();
        Self { year: date.year(), month: date.month(), day: date.day(), time }
    }

    pub fn num_months_to(&self, calendar: &Self) -> Option<i64> {
        let years = calendar.year as i64 - self.year as i64;
        let mut months = calendar.month as i64 - self.month as i64 + years * 12;
        if (calendar.day, calendar.time) < (self.day, self.time) {
            months -= 1;
        }
        if months > 0 {
            Some(months)
        } else {
            None
        }
    }

    pub fn num_years_to(&self, calendar: &Self) -> Option<i64> {
        let mut years = calendar.year as i64 - self.year as i64;
        if (calendar.month, calendar.day, calendar.time) < (self.month, self.day, self.time) {
            years -= 1;
        }
        if years > 0 {
            Some(years)
        } else {
            None
        }
    }

    pub fn subtract_month<Tz: TimeZone>(&mut self, count: i64, zone: &Tz) -> DateTime<Utc> {
        for _ in 0..count {
            self.month -= 1;
            if self.month == 0 {
                self.month = 12;
                self.year -= 1;
            }
        }
        self.create_time(zone)
    }

    pub fn subtract_year<Tz: TimeZone>(&mut self, count: i64, zone: &Tz) -> DateTime<Utc> {
        for _ in 0..count {
            self.year -= 1;
        }
        self.create_time(zone)
    }

    fn create_time<Tz: TimeZone>(&mut self, zone: &Tz) -> DateTime<Utc> {
        loop {
            if let Some(date) = NaiveDate::from_ymd_opt(self.year, self.month, self.day) {
                let time = date.and_time(self.time);
                let time = time.and_local_timezone(zone.clone());
                if let Some(time) = time.latest() {
                    return time.to_utc();
                } else {
                    self.increment_hour();
                }
            } else {
                self.increment_day();
            }
        }
    }

    fn increment_hour(&mut self) {
        self.time.add_assign(TimeDelta::hours(1));
    }

    fn increment_day(&mut self) {
        self.day += 1;
        if self.day > 31 {
            self.day = 1;
            self.month += 1;
            if self.month > 12 {
                self.month = 1;
                self.year += 1;
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::util::calendar::Calendar;
    use chrono::{DateTime, Datelike, TimeZone, Timelike, Utc};
    use chrono_tz::America;
    use pretty_assertions::assert_eq;

    #[test]
    fn test_subtracts_month_with_carried_year() {
        let func = |calendar: &mut Calendar| { calendar.subtract_month(1, &America::New_York) };
        assert_calendar(func, 2024, 1, 29, 12, 0, 0, 2023, 12, 29, 12, 0, 0);
        assert_calendar(func, 2024, 1, 30, 12, 0, 0, 2023, 12, 30, 12, 0, 0);
        assert_calendar(func, 2024, 2, 1, 12, 0, 0, 2024, 1, 1, 12, 0, 0);
        assert_calendar(func, 2024, 2, 2, 12, 0, 0, 2024, 1, 2, 12, 0, 0);
    }

    #[test]
    fn test_subtracts_month_with_shorter_month() {
        let func = |calendar: &mut Calendar| { calendar.subtract_month(10, &America::New_York) };
        assert_calendar(func, 2023, 12, 26, 12, 0, 0, 2023, 2, 26, 12, 0, 0);
        assert_calendar(func, 2023, 12, 27, 12, 0, 0, 2023, 2, 27, 12, 0, 0);
        assert_calendar(func, 2023, 12, 28, 12, 0, 0, 2023, 2, 28, 12, 0, 0);
        assert_calendar(func, 2023, 12, 29, 12, 0, 0, 2023, 3, 1, 12, 0, 0);
        assert_calendar(func, 2023, 12, 30, 12, 0, 0, 2023, 3, 1, 12, 0, 0);
        assert_calendar(func, 2023, 12, 31, 12, 0, 0, 2023, 3, 1, 12, 0, 0);
        assert_calendar(func, 2024, 1, 1, 12, 0, 0, 2023, 3, 1, 12, 0, 0);
        assert_calendar(func, 2024, 1, 2, 12, 0, 0, 2023, 3, 2, 12, 0, 0);
        assert_calendar(func, 2024, 1, 3, 12, 0, 0, 2023, 3, 3, 12, 0, 0);
    }

    #[test]
    fn test_subtracts_month_with_clocks_forward() {
        let func = |calendar: &mut Calendar| { calendar.subtract_month(1, &America::New_York) };
        assert_calendar(func, 2024, 4, 10, 4, 30, 0, 2024, 3, 10, 5, 30, 0); // 00:30 EDT => 00:30 EST
        assert_calendar(func, 2024, 4, 10, 5, 30, 0, 2024, 3, 10, 6, 30, 0); // 01:30 EDT => 01:30 EST
        assert_calendar(func, 2024, 4, 10, 6, 30, 0, 2024, 3, 10, 7, 30, 0); // 02:30 EDT => 03:30 EDT
        assert_calendar(func, 2024, 4, 10, 7, 30, 0, 2024, 3, 10, 7, 30, 0); // 03:30 EDT => 03:30 EDT
        assert_calendar(func, 2024, 4, 10, 8, 30, 0, 2024, 3, 10, 8, 30, 0); // 04:30 EDT => 04:30 EDT
    }

    #[test]
    fn test_subtracts_month_with_clocks_backward() {
        let func = |calendar: &mut Calendar| { calendar.subtract_month(1, &America::New_York) };
        assert_calendar(func, 2024, 12, 3, 5, 30, 0, 2024, 11, 3, 4, 30, 0); // 00:30 EST => 00:30 EDT
        assert_calendar(func, 2024, 12, 3, 6, 30, 0, 2024, 11, 3, 6, 30, 0); // 01:30 EST => 01:30 EDT
        assert_calendar(func, 2024, 12, 3, 7, 30, 0, 2024, 11, 3, 7, 30, 0); // 02:30 EST => 02:30 EDT
        assert_calendar(func, 2024, 12, 3, 8, 30, 0, 2024, 11, 3, 8, 30, 0); // 03:30 EST => 03:30 EST
        assert_calendar(func, 2024, 12, 3, 9, 30, 0, 2024, 11, 3, 9, 30, 0); // 04:30 EST => 04:30 EST
    }

    fn assert_calendar<F>(
        mut func: F,
        initial_year: i32,
        initial_month: u32,
        initial_day: u32,
        initial_hour: u32,
        initial_min: u32,
        initial_sec: u32,
        expect_year: i32,
        expect_month: u32,
        expect_day: u32,
        expect_hour: u32,
        expect_min: u32,
        expect_sec: u32,
    ) where F: FnMut(&mut Calendar) -> DateTime<Utc> {
        let time = Utc.with_ymd_and_hms(
            initial_year,
            initial_month,
            initial_day,
            initial_hour,
            initial_min,
            initial_sec,
        ).unwrap();
        let mut calendar = Calendar::from_time(&time, &America::New_York);
        let time = func(&mut calendar);
        assert_eq!(expect_year, time.year(), "year");
        assert_eq!(expect_month, time.month(), "month");
        assert_eq!(expect_day, time.day(), "day");
        assert_eq!(expect_hour, time.hour(), "hour");
        assert_eq!(expect_min, time.minute(), "minute");
        assert_eq!(expect_sec, time.second(), "second");
    }
}