aven_core/recurrence/
schedule.rs1use std::fmt;
2
3use chrono::{DateTime, Days, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
4use chrono_tz::{GapInfo, Tz};
5
6use super::rule::RecurrenceRule;
7use super::{RecurrenceDuePolicy, RecurrenceSchedule, RecurrenceSlot};
8
9pub fn is_slot(rule: &RecurrenceRule, start_on: NaiveDate, date: NaiveDate) -> bool {
10 rule.matches(start_on, date)
11}
12
13pub fn next_slot_after(
14 rule: &RecurrenceRule,
15 start_on: NaiveDate,
16 date: NaiveDate,
17) -> Option<NaiveDate> {
18 let next = date.checked_add_days(Days::new(1))?;
19 RecurrenceSlotIter::new(rule, start_on, next).next()
20}
21
22pub fn live_slot_on(
23 rule: &RecurrenceRule,
24 start_on: NaiveDate,
25 now: DateTime<Utc>,
26 timezone: &super::TimeZoneId,
27) -> Option<NaiveDate> {
28 let zone = timezone.timezone();
29 let mut date = now.with_timezone(&zone).date_naive();
30 while date >= start_on {
31 if is_slot(rule, start_on, date)
32 && resolve_local(zone, date.and_time(NaiveTime::MIN)).ok()? <= now
33 {
34 return Some(date);
35 }
36 date = date.checked_sub_days(Days::new(1))?;
37 }
38 None
39}
40
41pub fn projection_slot_at(
42 schedule: &RecurrenceSchedule,
43 at: DateTime<Utc>,
44) -> Result<NaiveDate, RecurrenceScheduleError> {
45 if let Some(slot) = live_slot_on(&schedule.rule, schedule.start_on, at, &schedule.timezone) {
46 return Ok(slot);
47 }
48 schedule
49 .slots_on_or_after(schedule.start_on)
50 .next()
51 .ok_or(RecurrenceScheduleError::DateOverflow)
52}
53
54pub fn slot_cutoff(
55 schedule: &RecurrenceSchedule,
56 slot_on: NaiveDate,
57) -> Result<DateTime<Utc>, RecurrenceScheduleError> {
58 require_slot(schedule, slot_on)?;
59 let next = next_slot_after(&schedule.rule, schedule.start_on, slot_on)
60 .ok_or(RecurrenceScheduleError::DateOverflow)?;
61 resolve_local(schedule.timezone.timezone(), next.and_time(NaiveTime::MIN))
62}
63
64pub fn slot_values(
65 schedule: &RecurrenceSchedule,
66 slot_on: NaiveDate,
67) -> Result<RecurrenceSlot, RecurrenceScheduleError> {
68 require_slot(schedule, slot_on)?;
69 let zone = schedule.timezone.timezone();
70 let boundary = resolve_local(zone, slot_on.and_time(NaiveTime::MIN))?;
71 let available_local_time = schedule.available_local_time.unwrap_or(NaiveTime::MIN);
72 let available_at = resolve_local(zone, slot_on.and_time(available_local_time))?;
73 let due_on = match schedule.due_policy {
74 RecurrenceDuePolicy::SameDay => Some(slot_on.format("%Y-%m-%d").to_string()),
75 RecurrenceDuePolicy::None => None,
76 };
77 Ok(RecurrenceSlot {
78 scheduled_on: slot_on,
79 boundary_at: format_utc(boundary),
80 available_at: format_utc(available_at),
81 due_on,
82 })
83}
84
85#[derive(Debug, Clone)]
86pub struct RecurrenceSlotIter<'a> {
87 rule: &'a RecurrenceRule,
88 start_on: NaiveDate,
89 candidate: Option<NaiveDate>,
90}
91
92impl<'a> RecurrenceSlotIter<'a> {
93 pub(crate) fn new(
94 rule: &'a RecurrenceRule,
95 start_on: NaiveDate,
96 on_or_after: NaiveDate,
97 ) -> Self {
98 Self {
99 rule,
100 start_on,
101 candidate: Some(on_or_after.max(start_on)),
102 }
103 }
104}
105
106impl Iterator for RecurrenceSlotIter<'_> {
107 type Item = NaiveDate;
108
109 fn next(&mut self) -> Option<Self::Item> {
110 let mut candidate = self.candidate?;
111 loop {
112 let following = candidate.checked_add_days(Days::new(1));
113 if is_slot(self.rule, self.start_on, candidate) {
114 self.candidate = following;
115 return Some(candidate);
116 }
117 candidate = following?;
118 }
119 }
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub enum RecurrenceScheduleError {
124 NotScheduledSlot,
125 DateOverflow,
126 UnresolvableLocalTime,
127}
128
129impl fmt::Display for RecurrenceScheduleError {
130 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
131 formatter.write_str(match self {
132 Self::NotScheduledSlot => "date is not a slot in the recurrence schedule",
133 Self::DateOverflow => "recurrence schedule exceeds the supported date range",
134 Self::UnresolvableLocalTime => {
135 "recurrence local time is outside the time-zone rule range"
136 }
137 })
138 }
139}
140
141impl std::error::Error for RecurrenceScheduleError {}
142
143fn require_slot(
144 schedule: &RecurrenceSchedule,
145 slot_on: NaiveDate,
146) -> Result<(), RecurrenceScheduleError> {
147 if is_slot(&schedule.rule, schedule.start_on, slot_on) {
148 Ok(())
149 } else {
150 Err(RecurrenceScheduleError::NotScheduledSlot)
151 }
152}
153
154fn resolve_local(zone: Tz, local: NaiveDateTime) -> Result<DateTime<Utc>, RecurrenceScheduleError> {
155 let zoned = match zone.from_local_datetime(&local) {
156 LocalResult::Single(value) => value,
157 LocalResult::Ambiguous(first, second) => first.min(second),
158 LocalResult::None => GapInfo::new(&local, &zone)
159 .and_then(|gap| gap.end)
160 .ok_or(RecurrenceScheduleError::UnresolvableLocalTime)?,
161 };
162 Ok(zoned.with_timezone(&Utc))
163}
164
165fn format_utc(value: DateTime<Utc>) -> String {
166 value.format("%Y-%m-%dT%H:%M:%SZ").to_string()
167}