]> git.proxmox.com Git - proxmox.git/blob - proxmox-time/src/calendar_event.rs
proxmox-time: calendar-events: make compute_next_event a method
[proxmox.git] / proxmox-time / src / calendar_event.rs
1 use std::convert::TryInto;
2
3 use anyhow::Error;
4 use nom::{
5 bytes::complete::tag,
6 character::complete::space0,
7 combinator::opt,
8 error::context,
9 multi::separated_nonempty_list,
10 sequence::{preceded, terminated, tuple},
11 };
12
13 use crate::date_time_value::DateTimeValue;
14 use crate::parse_helpers::{parse_complete_line, parse_error, parse_time_comp, IResult};
15 use crate::{parse_weekdays_range, TmEditor, WeekDays};
16
17 /// Calendar events may be used to refer to one or more points in time in a
18 /// single expression. They are designed after the systemd.time Calendar Events
19 /// specification, but are not guaranteed to be 100% compatible.
20 #[derive(Default, Clone, Debug)]
21 pub struct CalendarEvent {
22 /// the days in a week this event should trigger
23 pub(crate) days: WeekDays,
24 /// the second(s) this event should trigger
25 pub(crate) second: Vec<DateTimeValue>, // todo: support float values
26 /// the minute(s) this event should trigger
27 pub(crate) minute: Vec<DateTimeValue>,
28 /// the hour(s) this event should trigger
29 pub(crate) hour: Vec<DateTimeValue>,
30 /// the day(s) in a month this event should trigger
31 pub(crate) day: Vec<DateTimeValue>,
32 /// the month(s) in a year this event should trigger
33 pub(crate) month: Vec<DateTimeValue>,
34 /// the years(s) this event should trigger
35 pub(crate) year: Vec<DateTimeValue>,
36 }
37
38 impl CalendarEvent {
39 /// Computes the next timestamp after `last`. If `utc` is false, the local
40 /// timezone will be used for the calculation.
41 pub fn compute_next_event(&self, last: i64, utc: bool) -> Result<Option<i64>, Error> {
42 let last = last + 1; // at least one second later
43
44 let all_days = self.days.is_empty() || self.days.is_all();
45
46 let mut t = TmEditor::with_epoch(last, utc)?;
47
48 let mut count = 0;
49
50 loop {
51 // cancel after 1000 loops
52 if count > 1000 {
53 return Ok(None);
54 } else {
55 count += 1;
56 }
57
58 if !self.year.is_empty() {
59 let year: u32 = t.year().try_into()?;
60 if !DateTimeValue::list_contains(&self.year, year) {
61 if let Some(n) = DateTimeValue::find_next(&self.year, year) {
62 t.add_years((n - year).try_into()?)?;
63 continue;
64 } else {
65 // if we have no valid year, we cannot find a correct timestamp
66 return Ok(None);
67 }
68 }
69 }
70
71 if !self.month.is_empty() {
72 let month: u32 = t.month().try_into()?;
73 if !DateTimeValue::list_contains(&self.month, month) {
74 if let Some(n) = DateTimeValue::find_next(&self.month, month) {
75 t.add_months((n - month).try_into()?)?;
76 } else {
77 // if we could not find valid month, retry next year
78 t.add_years(1)?;
79 }
80 continue;
81 }
82 }
83
84 if !self.day.is_empty() {
85 let day: u32 = t.day().try_into()?;
86 if !DateTimeValue::list_contains(&self.day, day) {
87 if let Some(n) = DateTimeValue::find_next(&self.day, day) {
88 t.add_days((n - day).try_into()?)?;
89 } else {
90 // if we could not find valid mday, retry next month
91 t.add_months(1)?;
92 }
93 continue;
94 }
95 }
96
97 if !all_days {
98 // match day first
99 let day_num: u32 = t.day_num().try_into()?;
100 let day = WeekDays::from_bits(1 << day_num).unwrap();
101 if !self.days.contains(day) {
102 if let Some(n) = ((day_num + 1)..7)
103 .find(|d| self.days.contains(WeekDays::from_bits(1 << d).unwrap()))
104 {
105 // try next day
106 t.add_days((n - day_num).try_into()?)?;
107 } else {
108 // try next week
109 t.add_days((7 - day_num).try_into()?)?;
110 }
111 continue;
112 }
113 }
114
115 // this day
116 if !self.hour.is_empty() {
117 let hour = t.hour().try_into()?;
118 if !DateTimeValue::list_contains(&self.hour, hour) {
119 if let Some(n) = DateTimeValue::find_next(&self.hour, hour) {
120 // test next hour
121 t.set_time(n.try_into()?, 0, 0)?;
122 } else {
123 // test next day
124 t.add_days(1)?;
125 }
126 continue;
127 }
128 }
129
130 // this hour
131 if !self.minute.is_empty() {
132 let minute = t.min().try_into()?;
133 if !DateTimeValue::list_contains(&self.minute, minute) {
134 if let Some(n) = DateTimeValue::find_next(&self.minute, minute) {
135 // test next minute
136 t.set_min_sec(n.try_into()?, 0)?;
137 } else {
138 // test next hour
139 t.set_time(t.hour() + 1, 0, 0)?;
140 }
141 continue;
142 }
143 }
144
145 // this minute
146 if !self.second.is_empty() {
147 let second = t.sec().try_into()?;
148 if !DateTimeValue::list_contains(&self.second, second) {
149 if let Some(n) = DateTimeValue::find_next(&self.second, second) {
150 // test next second
151 t.set_sec(n.try_into()?)?;
152 } else {
153 // test next min
154 t.set_min_sec(t.min() + 1, 0)?;
155 }
156 continue;
157 }
158 }
159
160 let next = t.into_epoch()?;
161 return Ok(Some(next));
162 }
163 }
164 }
165
166 /// Verify the format of the [CalendarEvent]
167 pub fn verify_calendar_event(i: &str) -> Result<(), Error> {
168 parse_calendar_event(i)?;
169 Ok(())
170 }
171
172 /// Compute the next event. Use [CalendarEvent::compute_next_event] instead.
173 #[deprecated="use method 'compute_next_event' of CalendarEvent instead"]
174 pub fn compute_next_event(
175 event: &CalendarEvent,
176 last: i64,
177 utc: bool,
178 ) -> Result<Option<i64>, Error> {
179 event.compute_next_event(last, utc)
180 }
181
182 /// Parse a [CalendarEvent]
183 pub fn parse_calendar_event(i: &str) -> Result<CalendarEvent, Error> {
184 parse_complete_line("calendar event", i, parse_calendar_event_incomplete)
185 }
186
187 fn parse_calendar_event_incomplete(mut i: &str) -> IResult<&str, CalendarEvent> {
188 let mut has_dayspec = false;
189 let mut has_timespec = false;
190 let mut has_datespec = false;
191
192 let mut event = CalendarEvent::default();
193
194 if i.starts_with(|c: char| char::is_ascii_alphabetic(&c)) {
195 match i {
196 "minutely" => {
197 return Ok((
198 "",
199 CalendarEvent {
200 second: vec![DateTimeValue::Single(0)],
201 ..Default::default()
202 },
203 ));
204 }
205 "hourly" => {
206 return Ok((
207 "",
208 CalendarEvent {
209 minute: vec![DateTimeValue::Single(0)],
210 second: vec![DateTimeValue::Single(0)],
211 ..Default::default()
212 },
213 ));
214 }
215 "daily" => {
216 return Ok((
217 "",
218 CalendarEvent {
219 hour: vec![DateTimeValue::Single(0)],
220 minute: vec![DateTimeValue::Single(0)],
221 second: vec![DateTimeValue::Single(0)],
222 ..Default::default()
223 },
224 ));
225 }
226 "weekly" => {
227 return Ok((
228 "",
229 CalendarEvent {
230 hour: vec![DateTimeValue::Single(0)],
231 minute: vec![DateTimeValue::Single(0)],
232 second: vec![DateTimeValue::Single(0)],
233 days: WeekDays::MONDAY,
234 ..Default::default()
235 },
236 ));
237 }
238 "monthly" => {
239 return Ok((
240 "",
241 CalendarEvent {
242 hour: vec![DateTimeValue::Single(0)],
243 minute: vec![DateTimeValue::Single(0)],
244 second: vec![DateTimeValue::Single(0)],
245 day: vec![DateTimeValue::Single(1)],
246 ..Default::default()
247 },
248 ));
249 }
250 "yearly" | "annually" => {
251 return Ok((
252 "",
253 CalendarEvent {
254 hour: vec![DateTimeValue::Single(0)],
255 minute: vec![DateTimeValue::Single(0)],
256 second: vec![DateTimeValue::Single(0)],
257 day: vec![DateTimeValue::Single(1)],
258 month: vec![DateTimeValue::Single(1)],
259 ..Default::default()
260 },
261 ));
262 }
263 "quarterly" => {
264 return Ok((
265 "",
266 CalendarEvent {
267 hour: vec![DateTimeValue::Single(0)],
268 minute: vec![DateTimeValue::Single(0)],
269 second: vec![DateTimeValue::Single(0)],
270 day: vec![DateTimeValue::Single(1)],
271 month: vec![
272 DateTimeValue::Single(1),
273 DateTimeValue::Single(4),
274 DateTimeValue::Single(7),
275 DateTimeValue::Single(10),
276 ],
277 ..Default::default()
278 },
279 ));
280 }
281 "semiannually" | "semi-annually" => {
282 return Ok((
283 "",
284 CalendarEvent {
285 hour: vec![DateTimeValue::Single(0)],
286 minute: vec![DateTimeValue::Single(0)],
287 second: vec![DateTimeValue::Single(0)],
288 day: vec![DateTimeValue::Single(1)],
289 month: vec![DateTimeValue::Single(1), DateTimeValue::Single(7)],
290 ..Default::default()
291 },
292 ));
293 }
294 _ => { /* continue */ }
295 }
296
297 let (n, range_list) = context(
298 "weekday range list",
299 separated_nonempty_list(tag(","), parse_weekdays_range),
300 )(i)?;
301
302 has_dayspec = true;
303
304 i = space0(n)?.0;
305
306 for range in range_list {
307 event.days.insert(range);
308 }
309 }
310
311 if let (n, Some(date)) = opt(parse_date_spec)(i)? {
312 event.year = date.year;
313 event.month = date.month;
314 event.day = date.day;
315 has_datespec = true;
316 i = space0(n)?.0;
317 }
318
319 if let (n, Some(time)) = opt(parse_time_spec)(i)? {
320 event.hour = time.hour;
321 event.minute = time.minute;
322 event.second = time.second;
323 has_timespec = true;
324 i = n;
325 } else {
326 event.hour = vec![DateTimeValue::Single(0)];
327 event.minute = vec![DateTimeValue::Single(0)];
328 event.second = vec![DateTimeValue::Single(0)];
329 }
330
331 if !(has_dayspec || has_timespec || has_datespec) {
332 return Err(parse_error(i, "date or time specification"));
333 }
334
335 Ok((i, event))
336 }
337
338 struct TimeSpec {
339 hour: Vec<DateTimeValue>,
340 minute: Vec<DateTimeValue>,
341 second: Vec<DateTimeValue>,
342 }
343
344 struct DateSpec {
345 year: Vec<DateTimeValue>,
346 month: Vec<DateTimeValue>,
347 day: Vec<DateTimeValue>,
348 }
349
350 fn parse_date_time_comp(max: usize) -> impl Fn(&str) -> IResult<&str, DateTimeValue> {
351 move |i: &str| {
352 let (i, value) = parse_time_comp(max)(i)?;
353
354 if let (i, Some(end)) = opt(preceded(tag(".."), parse_time_comp(max)))(i)? {
355 if value > end {
356 return Err(parse_error(i, "range start is bigger than end"));
357 }
358 if let Some(time) = i.strip_prefix('/') {
359 let (time, repeat) = parse_time_comp(max)(time)?;
360 return Ok((time, DateTimeValue::Repeated(value, repeat, Some(end))));
361 }
362 return Ok((i, DateTimeValue::Range(value, end)));
363 }
364
365 if let Some(time) = i.strip_prefix('/') {
366 let (time, repeat) = parse_time_comp(max)(time)?;
367 Ok((time, DateTimeValue::Repeated(value, repeat, None)))
368 } else {
369 Ok((i, DateTimeValue::Single(value)))
370 }
371 }
372 }
373
374 fn parse_date_time_comp_list(
375 start: u32,
376 max: usize,
377 ) -> impl Fn(&str) -> IResult<&str, Vec<DateTimeValue>> {
378 move |i: &str| {
379 if let Some(rest) = i.strip_prefix('*') {
380 if let Some(time) = rest.strip_prefix('/') {
381 let (n, repeat) = parse_time_comp(max)(time)?;
382 if repeat > 0 {
383 return Ok((n, vec![DateTimeValue::Repeated(start, repeat, None)]));
384 }
385 }
386 return Ok((rest, Vec::new()));
387 }
388
389 separated_nonempty_list(tag(","), parse_date_time_comp(max))(i)
390 }
391 }
392
393 fn parse_time_spec(i: &str) -> IResult<&str, TimeSpec> {
394 let (i, (opt_hour, minute, opt_second)) = tuple((
395 opt(terminated(parse_date_time_comp_list(0, 24), tag(":"))),
396 parse_date_time_comp_list(0, 60),
397 opt(preceded(tag(":"), parse_date_time_comp_list(0, 60))),
398 ))(i)?;
399
400 let hour = opt_hour.unwrap_or_else(Vec::new);
401 let second = opt_second.unwrap_or_else(|| vec![DateTimeValue::Single(0)]);
402
403 Ok((
404 i,
405 TimeSpec {
406 hour,
407 minute,
408 second,
409 },
410 ))
411 }
412
413 fn parse_date_spec(i: &str) -> IResult<&str, DateSpec> {
414 // TODO: implement ~ for days (man systemd.time)
415 if let Ok((i, (year, month, day))) = tuple((
416 parse_date_time_comp_list(0, 2200), // the upper limit for systemd, stay compatible
417 preceded(tag("-"), parse_date_time_comp_list(1, 13)),
418 preceded(tag("-"), parse_date_time_comp_list(1, 32)),
419 ))(i)
420 {
421 Ok((i, DateSpec { year, month, day }))
422 } else if let Ok((i, (month, day))) = tuple((
423 parse_date_time_comp_list(1, 13),
424 preceded(tag("-"), parse_date_time_comp_list(1, 32)),
425 ))(i)
426 {
427 Ok((
428 i,
429 DateSpec {
430 year: Vec::new(),
431 month,
432 day,
433 },
434 ))
435 } else {
436 Err(parse_error(i, "invalid date spec"))
437 }
438 }