1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use super::{
    constants::*,
    types::*,
    utils::*,
};

/// Representation of Calendar without timezones, has awareness of leap years, days in a given month for leap and non-leap years.
/// 
/// Typical usage
/// ```rust
/// use chrono_light::prelude::*;
/// let c = Calendar::create();
/// let now_in_ms: u64 = 1650412800000;  // represents 20/04/2022 00:00:00:000
/// let schedule = Schedule {
///     start: DateTime { year: 2020, month: 4, day: 30, hour: 0, minute: 0, second: 0, ms: 0 },
///     items: vec![(Frequency::Year, 1)],
///     end: Some(DateTime { year: 2025, month: 4, day: 30, hour: 0, minute: 0, second: 0, ms: 0 })
/// };
/// assert!(c.validate_schedule(&schedule).is_ok());
/// assert_eq!(Some(10*24*60*60*1000), c.next_occurrence_ms(&c.from_unixtime(now_in_ms), &schedule));  // triggers in 10 days
/// ```
/// 
/// Beware `c.to_unixtime()` may panic, use `c.validate()` and/or `c.to_unixtime_opt()` to guarantee safety.
pub struct Calendar {
    // values required for the lookup of the years/months, considering leap Februaries
    // - year_offset_ms, taking into account leap/non leap years. store in array with implied index starting at 1970
    // - month_offset_ms, for every year, taking into account leap februaries
    year_ms_offsets:             &'static [u64],
    leap_year_month_offsets:     &'static [u64],
    non_leap_year_month_offsets: &'static [u64],
}

impl Calendar {
    /// Constructor for the calendar.
    pub fn create() -> Self {
        Self {
            year_ms_offsets: YEAR_MS_OFFSETS,
            leap_year_month_offsets: LEAP_YEAR_MONTH_OFFSETS,
            non_leap_year_month_offsets: NON_LEAP_YEAR_MONTH_OFFSETS,
        }
    }

    /// Converts a `&DateTime` to ms from epoch. Note: may panic if invalid `DateTime` specified.
    /// ```rust
    /// # use chrono_light::prelude::*;
    /// let c = Calendar::create();
    /// assert_eq!(c.to_unixtime(&DateTime {year: 2010, month: 10, day: 10, hour: 10, minute: 10, second: 10, ms: 10}), 1286705410010);
    /// ```
    pub fn to_unixtime(&self, dt: &DateTime) -> u64 {
        let year = dt.year as usize - EPOCH_YEAR;
        let year_offset = self.year_ms_offsets[year];
        let month_offset = if LEAP_YEARS.contains(&(dt.year as u16)) {
            self.leap_year_month_offsets[dt.month.checked_sub(1).expect("failed to calc month - 1") as usize]
        } else {
            self.non_leap_year_month_offsets[dt.month.checked_sub(1).expect("failed to calc month - 1") as usize]
        };
        let day_offset = dt.day.checked_sub(1).expect("failed to calc day - 1") as u64 * MS_IN_DAY;
        let hour_offset = dt.hour as u64 * MS_IN_HOUR;
        let minute_offset = dt.minute as u64 * MS_IN_MIN;
        let second_offset = dt.second as u64 * MS_IN_SEC;
        let ms_offset = dt.ms as u64;

        year_offset + month_offset + day_offset + hour_offset + minute_offset + second_offset + ms_offset
    }

    /// Converts a `&DateTime` to ms from epoch, returning `Some()` if supplied `DateTime` was valid, `None` otherwise.
    /// ```rust
    /// # use chrono_light::prelude::*;
    /// let c = Calendar::create();
    /// assert_eq!(c.to_unixtime_res(&DateTime {year: 2010, month: 10, day: 10, hour: 10, minute: 10, second: 10, ms: 10}), Ok(1286705410010));
    /// assert_eq!(c.to_unixtime_res(&DateTime {year: 2010, month:  0, day: 10, hour: 10, minute: 10, second: 10, ms: 10}), Err(ValidationError::Invalid));
    /// assert_eq!(c.to_unixtime_res(&DateTime {year: 2010, month: 10, day:  0, hour: 10, minute: 10, second: 10, ms: 10}), Err(ValidationError::Invalid));
    /// ```
    pub fn to_unixtime_res(&self, dt: &DateTime) -> Result<u64, ValidationError> {
        self.validate_datetime(dt)?;
        Ok(self.to_unixtime(dt))
    }

    /// Converts ms from epoch to `DateTime`.
    pub fn from_unixtime(&self, ts: u64) -> DateTime {
        // find year
        let mut year = CURRENT_YEAR - EPOCH_YEAR;
        if ts > self.year_ms_offsets[year] {
            while ts > self.year_ms_offsets[year+1] {
                year += 1;
            }
        } else {
            year -= 1;
            while ts < self.year_ms_offsets[year] {
                year -= 1;
            }
        }
        let year_offset = ts - self.year_ms_offsets[year];
        let month_offsets = if LEAP_YEARS.contains(&(year as u16 + EPOCH_YEAR as u16)) {
            &self.leap_year_month_offsets
        } else {
            &self.non_leap_year_month_offsets
        };
        
        let mut month = 1_usize;
        if year_offset > 0 {
            while year_offset > month_offsets[month-1] {
                month += 1;
            }
            month -= 1;
        }

        let day_offset = year_offset - month_offsets[month-1];
        let day = day_offset / MS_IN_DAY + 1;
        let hour = (day_offset % MS_IN_DAY) / MS_IN_HOUR;
        let minute = (day_offset % MS_IN_HOUR) / MS_IN_MIN;
        let second = (day_offset % MS_IN_MIN) / MS_IN_SEC;
        let ms = day_offset % MS_IN_SEC;

        DateTime {
            year: (year + EPOCH_YEAR) as u16,
            month: month as u8,
            day: day as u8,
            hour: hour as u8,
            minute: minute as u8,
            second: second as u8,
            ms: ms as u16
        }
    }

    /// Given a `now` `DateTime` and `Schedule`, finds ms delta when the next occurrence should trigger.
    /// If cut of by `Schedule.end`, returns a `None`.
    pub fn next_occurrence_ms(&self, now: &DateTime, schedule: &Schedule) -> Option<u64> /* delta_in_ms */ {
        let now_in_ms = self.to_unixtime(now);
        let start_in_ms = self.to_unixtime(&schedule.start);
        let is_expired = || schedule.end.as_ref().map_or(false, |end_dt| now_in_ms > self.to_unixtime(end_dt));

        if now_in_ms < start_in_ms {
            Some(start_in_ms - now_in_ms)
        } else if is_expired() {
            None
        } else {
            let next_trigger = schedule.items.iter().map(|(freq, multiplier)| {
                match freq {
                    Frequency::Year => {
                        let m_delta = now.month as i64 - schedule.start.month as i64 + i64::from(now.to_day_unixtime() >= schedule.start.to_day_unixtime());
                        let y_delta = now.year as i64 - schedule.start.year as i64 + i64::from(m_delta > 0);
                        let total_y_from_start = ceil_div(y_delta as u32, *multiplier) * multiplier;
                        let next_occurrence = DateTime {
                            year: schedule.start.year + total_y_from_start as u16,
                            month: schedule.start.month,
                            day: schedule.start.day,
                            hour: schedule.start.hour,
                            minute: schedule.start.minute,
                            second: schedule.start.second,
                            ms: schedule.start.ms,
                        };
                        self.to_unixtime(&next_occurrence) - self.to_unixtime(now)
                    }
                    Frequency::Month => {
                        let m_delta = now.month as i64 - schedule.start.month as i64 + i64::from(now.to_day_unixtime() >= schedule.start.to_day_unixtime());
                        let total_m_delta = (now.year as i64 - schedule.start.year as i64) * 12 + m_delta;
                        let total_m_from_start = ceil_div(total_m_delta as u32, *multiplier) * multiplier;
                        let next_occurrence = DateTime {
                            year: schedule.start.year + ((schedule.start.month as u32 + total_m_from_start - 1)/12) as u16,
                            month: ((schedule.start.month as u32 + total_m_from_start - 1) % 12) as u8 + 1,
                            day: schedule.start.day,
                            hour: schedule.start.hour,
                            minute: schedule.start.minute,
                            second: schedule.start.second,
                            ms: schedule.start.ms,
                        };
                        self.to_unixtime(&next_occurrence) - self.to_unixtime(now)
                    }
                    Frequency::Week | Frequency::Day | Frequency::Hour | Frequency::Minute | Frequency::Second | Frequency::Ms => {
                        let freq_in_ms = *freq as u64 * *multiplier as u64;
                        let ms_in_this_period = (now_in_ms - start_in_ms) % freq_in_ms;
                        if ms_in_this_period == 0 {
                            freq_in_ms
                        } else {
                            freq_in_ms - ms_in_this_period
                        }
                    },
                }
            }).min();
            // ensure trigger doesn't exceed end
            match next_trigger {
                Some(trigger) =>
                    match schedule.end.as_ref() {
                        None => Some(trigger),
                        Some(end) if now_in_ms + trigger <= self.to_unixtime(end) => Some(trigger),
                        _ => None
                    },
                _ => None
            }
        }
    }

    pub fn next_occurrence_ms_with_past_triggers(&self, last_run: Option<&DateTime>, now: &DateTime, schedule: &Schedule) -> (Vec<u64>, Option<u64>) /* triggers_in_ms, delta_in_ms */ {
        let t0 = DateTime { year: 1970, month: 1, day: 1, hour: 0, minute: 0, second: 0, ms: 0 };
        let now_ms = self.to_unixtime(&now);
        let mut triggers = Vec::new();
        let mut next_trigger: Option<u64> = None;
        let mut last_run = last_run.map(|x| x.clone()).unwrap_or(t0);
        let mut last_run_ms = self.to_unixtime(&last_run);
        while last_run_ms <= now_ms {
            next_trigger = self.next_occurrence_ms(&last_run, schedule).map(|x| x + last_run_ms);
            if let Some(trigger_ms) = next_trigger {
                let next_trigger = self.from_unixtime(trigger_ms);
                last_run = next_trigger;
                last_run_ms = trigger_ms;
                if trigger_ms <= now_ms {
                    triggers.push(trigger_ms);
                } else {
                    break;
                }
            } else {
                break;
            }
        }
        let next_trigger_delay = next_trigger.map(|x| x - now_ms);
        (triggers, next_trigger_delay)
    }

    /// Finds ms delta between 2 `DateTime`s.
    pub fn ms_between(&self, from: &DateTime, to: &DateTime) -> i64 {
        (self.to_unixtime(to) as i64).checked_sub(self.to_unixtime(from) as i64).expect("failed to calc ms_between")
    }

    /// Validates `DateTime` for correctness of fields, checking in respect to leap years.
    pub fn validate_datetime(&self, dt: &DateTime) -> Result<(), ValidationError> {
        // scope check
        (EPOCH_YEAR..=EPOCH_YEAR+self.year_ms_offsets.len()-1).contains(&(dt.year as usize));
        if !(EPOCH_YEAR..=EPOCH_YEAR+self.year_ms_offsets.len()-1).contains(&(dt.year as usize)) {
            return Err(ValidationError::OutOfScope);
        }

        // static valid check
        if !(1..=12).contains(&dt.month) || !(1..=31).contains(&dt.day) || dt.hour >= 24 || dt.minute >= 60 || dt.second >= 60 || dt.ms >= 1000 {
            return Err(ValidationError::Invalid);
        }

        // leap year check
        let is_leap_year = LEAP_YEARS.contains(&(dt.year as u16));
        if (is_leap_year && dt.day > MONTH_FOR_LEAP_YEAR[dt.month.checked_sub(1).expect("failed to calc month - 1") as usize]) ||
            (!is_leap_year && dt.day > MONTH_FOR_NON_LEAP_YEAR[dt.month.checked_sub(1).expect("failed to calc month - 1") as usize]) {
            return Err(ValidationError::Invalid);
        }
        Ok(())
    }

    pub fn validate_schedule(&self, schedule: &Schedule) -> Result<(), ValidationError> {
        self.validate_datetime(&schedule.start)?;
        if let Some(end) = &schedule.end {
            self.validate_datetime(&end)?;
        }

        let start_before_end = schedule.end.as_ref().map_or(true, |end| schedule.start <= *end);
        let all_freqs_non_zero_multiplier = schedule.items.iter().all(|&(_, x)| x > 0);
        if !start_before_end || !all_freqs_non_zero_multiplier {
            return Err(ValidationError::Invalid);
        }
        Ok(())
    }
}