]> git.proxmox.com Git - proxmox.git/blob - proxmox-time/src/time.rs
proxmox-time: added time related fuctions from proxmox-systemd crate.
[proxmox.git] / proxmox-time / src / time.rs
1 use std::convert::TryInto;
2
3 use anyhow::Error;
4 use bitflags::bitflags;
5
6 use crate::TmEditor;
7
8 use crate::{parse_calendar_event, parse_time_span};
9
10 bitflags!{
11 /// Defines one or more days of a week.
12 #[derive(Default)]
13 pub struct WeekDays: u8 {
14 const MONDAY = 1;
15 const TUESDAY = 2;
16 const WEDNESDAY = 4;
17 const THURSDAY = 8;
18 const FRIDAY = 16;
19 const SATURDAY = 32;
20 const SUNDAY = 64;
21 }
22 }
23
24 #[derive(Debug, Clone)]
25 pub(crate) enum DateTimeValue {
26 Single(u32),
27 Range(u32, u32),
28 Repeated(u32, u32),
29 }
30
31 impl DateTimeValue {
32 // Test if the entry contains the value
33 pub fn contains(&self, value: u32) -> bool {
34 match self {
35 DateTimeValue::Single(v) => *v == value,
36 DateTimeValue::Range(start, end) => value >= *start && value <= *end,
37 DateTimeValue::Repeated(start, repetition) => {
38 if value >= *start {
39 if *repetition > 0 {
40 let offset = value - start;
41 offset % repetition == 0
42 } else {
43 *start == value
44 }
45 } else {
46 false
47 }
48 }
49 }
50 }
51
52 pub fn list_contains(list: &[DateTimeValue], value: u32) -> bool {
53 list.iter().any(|spec| spec.contains(value))
54 }
55
56 // Find an return an entry greater than value
57 pub fn find_next(list: &[DateTimeValue], value: u32) -> Option<u32> {
58 let mut next: Option<u32> = None;
59 let mut set_next = |v: u32| {
60 if let Some(n) = next {
61 if v < n { next = Some(v); }
62 } else {
63 next = Some(v);
64 }
65 };
66 for spec in list {
67 match spec {
68 DateTimeValue::Single(v) => {
69 if *v > value { set_next(*v); }
70 }
71 DateTimeValue::Range(start, end) => {
72 if value < *start {
73 set_next(*start);
74 } else {
75 let n = value + 1;
76 if n >= *start && n <= *end {
77 set_next(n);
78 }
79 }
80 }
81 DateTimeValue::Repeated(start, repetition) => {
82 if value < *start {
83 set_next(*start);
84 } else if *repetition > 0 {
85 set_next(start + ((value - start + repetition) / repetition) * repetition);
86 }
87 }
88 }
89 }
90
91 next
92 }
93 }
94
95 /// Calendar events may be used to refer to one or more points in time in a
96 /// single expression. They are designed after the systemd.time Calendar Events
97 /// specification, but are not guaranteed to be 100% compatible.
98 #[derive(Default, Clone, Debug)]
99 pub struct CalendarEvent {
100 /// the days in a week this event should trigger
101 pub(crate) days: WeekDays,
102 /// the second(s) this event should trigger
103 pub(crate) second: Vec<DateTimeValue>, // todo: support float values
104 /// the minute(s) this event should trigger
105 pub(crate) minute: Vec<DateTimeValue>,
106 /// the hour(s) this event should trigger
107 pub(crate) hour: Vec<DateTimeValue>,
108 /// the day(s) in a month this event should trigger
109 pub(crate) day: Vec<DateTimeValue>,
110 /// the month(s) in a year this event should trigger
111 pub(crate) month: Vec<DateTimeValue>,
112 /// the years(s) this event should trigger
113 pub(crate) year: Vec<DateTimeValue>,
114 }
115
116 /// A time spans defines a time duration
117 #[derive(Default, Clone, Debug)]
118 pub struct TimeSpan {
119 pub nsec: u64,
120 pub usec: u64,
121 pub msec: u64,
122 pub seconds: u64,
123 pub minutes: u64,
124 pub hours: u64,
125 pub days: u64,
126 pub weeks: u64,
127 pub months: u64,
128 pub years: u64,
129 }
130
131 impl From<TimeSpan> for f64 {
132 fn from(ts: TimeSpan) -> Self {
133 (ts.seconds as f64) +
134 ((ts.nsec as f64) / 1_000_000_000.0) +
135 ((ts.usec as f64) / 1_000_000.0) +
136 ((ts.msec as f64) / 1_000.0) +
137 ((ts.minutes as f64) * 60.0) +
138 ((ts.hours as f64) * 3600.0) +
139 ((ts.days as f64) * 3600.0 * 24.0) +
140 ((ts.weeks as f64) * 3600.0 * 24.0 * 7.0) +
141 ((ts.months as f64) * 3600.0 * 24.0 * 30.44) +
142 ((ts.years as f64) * 3600.0 * 24.0 * 365.25)
143 }
144 }
145
146 impl From<std::time::Duration> for TimeSpan {
147 fn from(duration: std::time::Duration) -> Self {
148 let mut duration = duration.as_nanos();
149 let nsec = (duration % 1000) as u64;
150 duration /= 1000;
151 let usec = (duration % 1000) as u64;
152 duration /= 1000;
153 let msec = (duration % 1000) as u64;
154 duration /= 1000;
155 let seconds = (duration % 60) as u64;
156 duration /= 60;
157 let minutes = (duration % 60) as u64;
158 duration /= 60;
159 let hours = (duration % 24) as u64;
160 duration /= 24;
161 let years = (duration as f64 / 365.25) as u64;
162 let ydays = (duration as f64 % 365.25) as u64;
163 let months = (ydays as f64 / 30.44) as u64;
164 let mdays = (ydays as f64 % 30.44) as u64;
165 let weeks = mdays / 7;
166 let days = mdays % 7;
167 Self {
168 nsec,
169 usec,
170 msec,
171 seconds,
172 minutes,
173 hours,
174 days,
175 weeks,
176 months,
177 years,
178 }
179 }
180 }
181
182 impl std::fmt::Display for TimeSpan {
183 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
184 let mut first = true;
185 { // block scope for mutable borrows
186 let mut do_write = |v: u64, unit: &str| -> Result<(), std::fmt::Error> {
187 if !first {
188 write!(f, " ")?;
189 }
190 first = false;
191 write!(f, "{}{}", v, unit)
192 };
193 if self.years > 0 {
194 do_write(self.years, "y")?;
195 }
196 if self.months > 0 {
197 do_write(self.months, "m")?;
198 }
199 if self.weeks > 0 {
200 do_write(self.weeks, "w")?;
201 }
202 if self.days > 0 {
203 do_write(self.days, "d")?;
204 }
205 if self.hours > 0 {
206 do_write(self.hours, "h")?;
207 }
208 if self.minutes > 0 {
209 do_write(self.minutes, "min")?;
210 }
211 }
212 if !first {
213 write!(f, " ")?;
214 }
215 let seconds = self.seconds as f64 + (self.msec as f64 / 1000.0);
216 if seconds >= 0.1 {
217 if seconds >= 1.0 || !first {
218 write!(f, "{:.0}s", seconds)?;
219 } else {
220 write!(f, "{:.1}s", seconds)?;
221 }
222 } else if first {
223 write!(f, "<0.1s")?;
224 }
225 Ok(())
226 }
227 }
228
229 /// Verify the format of the [TimeSpan]
230 pub fn verify_time_span(i: &str) -> Result<(), Error> {
231 parse_time_span(i)?;
232 Ok(())
233 }
234
235 /// Verify the format of the [CalendarEvent]
236 pub fn verify_calendar_event(i: &str) -> Result<(), Error> {
237 parse_calendar_event(i)?;
238 Ok(())
239 }
240
241 /// Compute the next event
242 pub fn compute_next_event(
243 event: &CalendarEvent,
244 last: i64,
245 utc: bool,
246 ) -> Result<Option<i64>, Error> {
247
248 let last = last + 1; // at least one second later
249
250 let all_days = event.days.is_empty() || event.days.is_all();
251
252 let mut t = TmEditor::with_epoch(last, utc)?;
253
254 let mut count = 0;
255
256 loop {
257 // cancel after 1000 loops
258 if count > 1000 {
259 return Ok(None);
260 } else {
261 count += 1;
262 }
263
264 if !event.year.is_empty() {
265 let year: u32 = t.year().try_into()?;
266 if !DateTimeValue::list_contains(&event.year, year) {
267 if let Some(n) = DateTimeValue::find_next(&event.year, year) {
268 t.add_years((n - year).try_into()?)?;
269 continue;
270 } else {
271 // if we have no valid year, we cannot find a correct timestamp
272 return Ok(None);
273 }
274 }
275 }
276
277 if !event.month.is_empty() {
278 let month: u32 = t.month().try_into()?;
279 if !DateTimeValue::list_contains(&event.month, month) {
280 if let Some(n) = DateTimeValue::find_next(&event.month, month) {
281 t.add_months((n - month).try_into()?)?;
282 } else {
283 // if we could not find valid month, retry next year
284 t.add_years(1)?;
285 }
286 continue;
287 }
288 }
289
290 if !event.day.is_empty() {
291 let day: u32 = t.day().try_into()?;
292 if !DateTimeValue::list_contains(&event.day, day) {
293 if let Some(n) = DateTimeValue::find_next(&event.day, day) {
294 t.add_days((n - day).try_into()?)?;
295 } else {
296 // if we could not find valid mday, retry next month
297 t.add_months(1)?;
298 }
299 continue;
300 }
301 }
302
303 if !all_days { // match day first
304 let day_num: u32 = t.day_num().try_into()?;
305 let day = WeekDays::from_bits(1<<day_num).unwrap();
306 if !event.days.contains(day) {
307 if let Some(n) = ((day_num+1)..7)
308 .find(|d| event.days.contains(WeekDays::from_bits(1<<d).unwrap()))
309 {
310 // try next day
311 t.add_days((n - day_num).try_into()?)?;
312 } else {
313 // try next week
314 t.add_days((7 - day_num).try_into()?)?;
315 }
316 continue;
317 }
318 }
319
320 // this day
321 if !event.hour.is_empty() {
322 let hour = t.hour().try_into()?;
323 if !DateTimeValue::list_contains(&event.hour, hour) {
324 if let Some(n) = DateTimeValue::find_next(&event.hour, hour) {
325 // test next hour
326 t.set_time(n.try_into()?, 0, 0)?;
327 } else {
328 // test next day
329 t.add_days(1)?;
330 }
331 continue;
332 }
333 }
334
335 // this hour
336 if !event.minute.is_empty() {
337 let minute = t.min().try_into()?;
338 if !DateTimeValue::list_contains(&event.minute, minute) {
339 if let Some(n) = DateTimeValue::find_next(&event.minute, minute) {
340 // test next minute
341 t.set_min_sec(n.try_into()?, 0)?;
342 } else {
343 // test next hour
344 t.set_time(t.hour() + 1, 0, 0)?;
345 }
346 continue;
347 }
348 }
349
350 // this minute
351 if !event.second.is_empty() {
352 let second = t.sec().try_into()?;
353 if !DateTimeValue::list_contains(&event.second, second) {
354 if let Some(n) = DateTimeValue::find_next(&event.second, second) {
355 // test next second
356 t.set_sec(n.try_into()?)?;
357 } else {
358 // test next min
359 t.set_min_sec(t.min() + 1, 0)?;
360 }
361 continue;
362 }
363 }
364
365 let next = t.into_epoch()?;
366 return Ok(Some(next))
367 }
368 }
369
370 #[cfg(test)]
371 mod test {
372
373 use anyhow::bail;
374
375 use super::*;
376
377 fn test_event(v: &'static str) -> Result<(), Error> {
378 match parse_calendar_event(v) {
379 Ok(event) => println!("CalendarEvent '{}' => {:?}", v, event),
380 Err(err) => bail!("parsing '{}' failed - {}", v, err),
381 }
382
383 Ok(())
384 }
385
386 const fn make_test_time(mday: i32, hour: i32, min: i32) -> i64 {
387 (mday*3600*24 + hour*3600 + min*60) as i64
388 }
389
390 #[test]
391 fn test_compute_next_event() -> Result<(), Error> {
392
393 let test_value = |v: &'static str, last: i64, expect: i64| -> Result<i64, Error> {
394 let event = match parse_calendar_event(v) {
395 Ok(event) => event,
396 Err(err) => bail!("parsing '{}' failed - {}", v, err),
397 };
398
399 match compute_next_event(&event, last, true) {
400 Ok(Some(next)) => {
401 if next == expect {
402 println!("next {:?} => {}", event, next);
403 } else {
404 bail!(
405 "next {:?} failed\nnext: {:?}\nexpect: {:?}",
406 event,
407 crate::gmtime(next),
408 crate::gmtime(expect),
409 );
410 }
411 }
412 Ok(None) => bail!("next {:?} failed to find a timestamp", event),
413 Err(err) => bail!("compute next for '{}' failed - {}", v, err),
414 }
415
416 Ok(expect)
417 };
418
419 let test_never = |v: &'static str, last: i64| -> Result<(), Error> {
420 let event = match parse_calendar_event(v) {
421 Ok(event) => event,
422 Err(err) => bail!("parsing '{}' failed - {}", v, err),
423 };
424
425 match compute_next_event(&event, last, true)? {
426 None => Ok(()),
427 Some(next) => bail!("compute next for '{}' succeeded, but expected fail - result {}", v, next),
428 }
429 };
430
431 const MIN: i64 = 60;
432 const HOUR: i64 = 3600;
433 const DAY: i64 = 3600*24;
434
435 const THURSDAY_00_00: i64 = make_test_time(0, 0, 0);
436 const THURSDAY_15_00: i64 = make_test_time(0, 15, 0);
437
438 const JUL_31_2020: i64 = 1596153600; // Friday, 2020-07-31 00:00:00
439 const DEC_31_2020: i64 = 1609372800; // Thursday, 2020-12-31 00:00:00
440
441 test_value("*:0", THURSDAY_00_00, THURSDAY_00_00 + HOUR)?;
442 test_value("*:*", THURSDAY_00_00, THURSDAY_00_00 + MIN)?;
443 test_value("*:*:*", THURSDAY_00_00, THURSDAY_00_00 + 1)?;
444 test_value("*:3:5", THURSDAY_00_00, THURSDAY_00_00 + 3*MIN + 5)?;
445
446 test_value("mon *:*", THURSDAY_00_00, THURSDAY_00_00 + 4*DAY)?;
447 test_value("mon 2:*", THURSDAY_00_00, THURSDAY_00_00 + 4*DAY + 2*HOUR)?;
448 test_value("mon 2:50", THURSDAY_00_00, THURSDAY_00_00 + 4*DAY + 2*HOUR + 50*MIN)?;
449
450 test_value("tue", THURSDAY_00_00, THURSDAY_00_00 + 5*DAY)?;
451 test_value("wed", THURSDAY_00_00, THURSDAY_00_00 + 6*DAY)?;
452 test_value("thu", THURSDAY_00_00, THURSDAY_00_00 + 7*DAY)?;
453 test_value("fri", THURSDAY_00_00, THURSDAY_00_00 + 1*DAY)?;
454 test_value("sat", THURSDAY_00_00, THURSDAY_00_00 + 2*DAY)?;
455 test_value("sun", THURSDAY_00_00, THURSDAY_00_00 + 3*DAY)?;
456
457 // test multiple values for a single field
458 // and test that the order does not matter
459 test_value("5,10:4,8", THURSDAY_00_00, THURSDAY_00_00 + 5*HOUR + 4*MIN)?;
460 test_value("10,5:8,4", THURSDAY_00_00, THURSDAY_00_00 + 5*HOUR + 4*MIN)?;
461 test_value("6,4..10:23,5/5", THURSDAY_00_00, THURSDAY_00_00 + 4*HOUR + 5*MIN)?;
462 test_value("4..10,6:5/5,23", THURSDAY_00_00, THURSDAY_00_00 + 4*HOUR + 5*MIN)?;
463
464 // test month wrapping
465 test_value("sat", JUL_31_2020, JUL_31_2020 + 1*DAY)?;
466 test_value("sun", JUL_31_2020, JUL_31_2020 + 2*DAY)?;
467 test_value("mon", JUL_31_2020, JUL_31_2020 + 3*DAY)?;
468 test_value("tue", JUL_31_2020, JUL_31_2020 + 4*DAY)?;
469 test_value("wed", JUL_31_2020, JUL_31_2020 + 5*DAY)?;
470 test_value("thu", JUL_31_2020, JUL_31_2020 + 6*DAY)?;
471 test_value("fri", JUL_31_2020, JUL_31_2020 + 7*DAY)?;
472
473 // test year wrapping
474 test_value("fri", DEC_31_2020, DEC_31_2020 + 1*DAY)?;
475 test_value("sat", DEC_31_2020, DEC_31_2020 + 2*DAY)?;
476 test_value("sun", DEC_31_2020, DEC_31_2020 + 3*DAY)?;
477 test_value("mon", DEC_31_2020, DEC_31_2020 + 4*DAY)?;
478 test_value("tue", DEC_31_2020, DEC_31_2020 + 5*DAY)?;
479 test_value("wed", DEC_31_2020, DEC_31_2020 + 6*DAY)?;
480 test_value("thu", DEC_31_2020, DEC_31_2020 + 7*DAY)?;
481
482 test_value("daily", THURSDAY_00_00, THURSDAY_00_00 + DAY)?;
483 test_value("daily", THURSDAY_00_00+1, THURSDAY_00_00 + DAY)?;
484
485 let n = test_value("5/2:0", THURSDAY_00_00, THURSDAY_00_00 + 5*HOUR)?;
486 let n = test_value("5/2:0", n, THURSDAY_00_00 + 7*HOUR)?;
487 let n = test_value("5/2:0", n, THURSDAY_00_00 + 9*HOUR)?;
488 test_value("5/2:0", n, THURSDAY_00_00 + 11*HOUR)?;
489
490 let mut n = test_value("*:*", THURSDAY_00_00, THURSDAY_00_00 + MIN)?;
491 for i in 2..100 {
492 n = test_value("*:*", n, THURSDAY_00_00 + i*MIN)?;
493 }
494
495 let mut n = test_value("*:0", THURSDAY_00_00, THURSDAY_00_00 + HOUR)?;
496 for i in 2..100 {
497 n = test_value("*:0", n, THURSDAY_00_00 + i*HOUR)?;
498 }
499
500 let mut n = test_value("1:0", THURSDAY_15_00, THURSDAY_00_00 + DAY + HOUR)?;
501 for i in 2..100 {
502 n = test_value("1:0", n, THURSDAY_00_00 + i*DAY + HOUR)?;
503 }
504
505 // test date functionality
506
507 test_value("2020-07-31", 0, JUL_31_2020)?;
508 test_value("02-28", 0, (31+27)*DAY)?;
509 test_value("02-29", 0, 2*365*DAY + (31+28)*DAY)?; // 1972-02-29
510 test_value("1965/5-01-01", -1, THURSDAY_00_00)?;
511 test_value("2020-7..9-2/2", JUL_31_2020, JUL_31_2020 + 2*DAY)?;
512 test_value("2020,2021-12-31", JUL_31_2020, DEC_31_2020)?;
513
514 test_value("monthly", 0, 31*DAY)?;
515 test_value("quarterly", 0, (31+28+31)*DAY)?;
516 test_value("semiannually", 0, (31+28+31+30+31+30)*DAY)?;
517 test_value("yearly", 0, (365)*DAY)?;
518
519 test_never("2021-02-29", 0)?;
520 test_never("02-30", 0)?;
521
522 Ok(())
523 }
524
525 #[test]
526 fn test_calendar_event_weekday() -> Result<(), Error> {
527 test_event("mon,wed..fri")?;
528 test_event("fri..mon")?;
529
530 test_event("mon")?;
531 test_event("MON")?;
532 test_event("monDay")?;
533 test_event("tue")?;
534 test_event("Tuesday")?;
535 test_event("wed")?;
536 test_event("wednesday")?;
537 test_event("thu")?;
538 test_event("thursday")?;
539 test_event("fri")?;
540 test_event("friday")?;
541 test_event("sat")?;
542 test_event("saturday")?;
543 test_event("sun")?;
544 test_event("sunday")?;
545
546 test_event("mon..fri")?;
547 test_event("mon,tue,fri")?;
548 test_event("mon,tue..wednesday,fri..sat")?;
549
550 Ok(())
551 }
552
553 #[test]
554 fn test_time_span_parser() -> Result<(), Error> {
555
556 let test_value = |ts_str: &str, expect: f64| -> Result<(), Error> {
557 let ts = parse_time_span(ts_str)?;
558 assert_eq!(f64::from(ts), expect, "{}", ts_str);
559 Ok(())
560 };
561
562 test_value("2", 2.0)?;
563 test_value("2s", 2.0)?;
564 test_value("2sec", 2.0)?;
565 test_value("2second", 2.0)?;
566 test_value("2seconds", 2.0)?;
567
568 test_value(" 2s 2 s 2", 6.0)?;
569
570 test_value("1msec 1ms", 0.002)?;
571 test_value("1usec 1us 1µs", 0.000_003)?;
572 test_value("1nsec 1ns", 0.000_000_002)?;
573 test_value("1minutes 1minute 1min 1m", 4.0*60.0)?;
574 test_value("1hours 1hour 1hr 1h", 4.0*3600.0)?;
575 test_value("1days 1day 1d", 3.0*86400.0)?;
576 test_value("1weeks 1 week 1w", 3.0*86400.0*7.0)?;
577 test_value("1months 1month 1M", 3.0*86400.0*30.44)?;
578 test_value("1years 1year 1y", 3.0*86400.0*365.25)?;
579
580 test_value("2h", 7200.0)?;
581 test_value(" 2 h", 7200.0)?;
582 test_value("2hours", 7200.0)?;
583 test_value("48hr", 48.0*3600.0)?;
584 test_value("1y 12month", 365.25*24.0*3600.0 + 12.0*30.44*24.0*3600.0)?;
585 test_value("55s500ms", 55.5)?;
586 test_value("300ms20s 5day", 5.0*24.0*3600.0 + 20.0 + 0.3)?;
587
588 Ok(())
589 }
590 }