public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Dominik Csapak <d.csapak@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [pbs-devel] [PATCH proxmox-backup 10/10] tools/systemd/time: enable dates for calendarevents
Date: Thu,  3 Sep 2020 13:40:00 +0200	[thread overview]
Message-ID: <20200903114000.6932-11-d.csapak@proxmox.com> (raw)
In-Reply-To: <20200903114000.6932-1-d.csapak@proxmox.com>

this implements parsing and calculating calendarevents that have a
basic date component (year-mon-day) with the usual syntax options
(*, ranges, lists)

and some special events:
monthly
yearly/annually (like systemd)
quarterly
semiannually,semi-annually (like systemd)

includes some regression tests

the ~ syntax for days (the last x days of the month) is not yet
implemented

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 src/tools/systemd/parse_time.rs | 77 +++++++++++++++++++++++++++++++--
 src/tools/systemd/time.rs       | 76 +++++++++++++++++++++++++++++++-
 2 files changed, 147 insertions(+), 6 deletions(-)

diff --git a/src/tools/systemd/parse_time.rs b/src/tools/systemd/parse_time.rs
index 810a9883..578e253b 100644
--- a/src/tools/systemd/parse_time.rs
+++ b/src/tools/systemd/parse_time.rs
@@ -186,6 +186,25 @@ fn parse_time_spec(i: &str) -> IResult<&str, (Vec<DateTimeValue>, Vec<DateTimeVa
     }
 }
 
+fn parse_date_spec(i: &str) -> IResult<&str, (Vec<DateTimeValue>, Vec<DateTimeValue>, Vec<DateTimeValue>)> {
+
+    // TODO: implement ~ for days (man systemd.time)
+    if let Ok((i, (year, month, day))) = tuple((
+        parse_date_time_comp_list(2200), // the upper limit for systemd, stay compatible
+        preceded(tag("-"), parse_date_time_comp_list(13)),
+        preceded(tag("-"), parse_date_time_comp_list(32)),
+    ))(i) {
+        Ok((i, (year, month, day)))
+    } else if let Ok((i, (month, day))) = tuple((
+        parse_date_time_comp_list(13),
+        preceded(tag("-"), parse_date_time_comp_list(32)),
+    ))(i) {
+        Ok((i, (Vec::new(), month, day)))
+    } else {
+        Err(parse_error(i, "invalid date spec"))
+    }
+}
+
 pub fn parse_calendar_event(i: &str) -> Result<CalendarEvent, Error> {
     parse_complete_line("calendar event", i, parse_calendar_event_incomplete)
 }
@@ -194,7 +213,7 @@ fn parse_calendar_event_incomplete(mut i: &str) -> IResult<&str, CalendarEvent>
 
     let mut has_dayspec = false;
     let mut has_timespec = false;
-    let has_datespec = false;
+    let mut has_datespec = false;
 
     let mut event = CalendarEvent::default();
 
@@ -231,8 +250,52 @@ fn parse_calendar_event_incomplete(mut i: &str) -> IResult<&str, CalendarEvent>
                     ..Default::default()
                 }));
             }
-            "monthly" | "yearly" | "quarterly" | "semiannually" => {
-                return Err(parse_error(i, "unimplemented date or time specification"));
+            "monthly" => {
+                return Ok(("", CalendarEvent {
+                    hour: vec![DateTimeValue::Single(0)],
+                    minute: vec![DateTimeValue::Single(0)],
+                    second: vec![DateTimeValue::Single(0)],
+                    day: vec![DateTimeValue::Single(1)],
+                    ..Default::default()
+                }));
+            }
+            "yearly" | "annually" => {
+                return Ok(("", CalendarEvent {
+                    hour: vec![DateTimeValue::Single(0)],
+                    minute: vec![DateTimeValue::Single(0)],
+                    second: vec![DateTimeValue::Single(0)],
+                    day: vec![DateTimeValue::Single(1)],
+                    month: vec![DateTimeValue::Single(1)],
+                    ..Default::default()
+                }));
+            }
+            "quarterly" => {
+                return Ok(("", CalendarEvent {
+                    hour: vec![DateTimeValue::Single(0)],
+                    minute: vec![DateTimeValue::Single(0)],
+                    second: vec![DateTimeValue::Single(0)],
+                    day: vec![DateTimeValue::Single(1)],
+                    month: vec![
+                        DateTimeValue::Single(1),
+                        DateTimeValue::Single(4),
+                        DateTimeValue::Single(7),
+                        DateTimeValue::Single(10),
+                    ],
+                    ..Default::default()
+                }));
+            }
+            "semiannually" | "semi-annually" => {
+                return Ok(("", CalendarEvent {
+                    hour: vec![DateTimeValue::Single(0)],
+                    minute: vec![DateTimeValue::Single(0)],
+                    second: vec![DateTimeValue::Single(0)],
+                    day: vec![DateTimeValue::Single(1)],
+                    month: vec![
+                        DateTimeValue::Single(1),
+                        DateTimeValue::Single(7),
+                    ],
+                    ..Default::default()
+                }));
             }
             _ => { /* continue */ }
         }
@@ -249,7 +312,13 @@ fn parse_calendar_event_incomplete(mut i: &str) -> IResult<&str, CalendarEvent>
         for range in range_list  { event.days.insert(range); }
     }
 
-    // todo: support date specs
+    if let (n, Some((year, month, day))) = opt(parse_date_spec)(i)? {
+        event.year = year;
+        event.month = month;
+        event.day = day;
+        has_datespec = true;
+        i = space0(n)?.0;
+    }
 
     if let (n, Some((hour, minute, second))) = opt(parse_time_spec)(i)? {
         event.hour = hour;
diff --git a/src/tools/systemd/time.rs b/src/tools/systemd/time.rs
index 6ce881e7..4993a30c 100644
--- a/src/tools/systemd/time.rs
+++ b/src/tools/systemd/time.rs
@@ -101,14 +101,12 @@ pub struct CalendarEvent {
     pub minute: Vec<DateTimeValue>,
     /// the hour(s) this event should trigger
     pub hour: Vec<DateTimeValue>,
-/* FIXME: TODO
     /// the day(s) in a month this event should trigger
     pub day: Vec<DateTimeValue>,
     /// the month(s) in a year this event should trigger
     pub month: Vec<DateTimeValue>,
     /// the years(s) this event should trigger
     pub year: Vec<DateTimeValue>,
-*/
 }
 
 #[derive(Default)]
@@ -173,6 +171,51 @@ pub fn compute_next_event(
             count += 1;
         }
 
+        if !event.year.is_empty() && t.changes.contains(TMChanges::YEAR) {
+            let year = t.year();
+            if DateTimeValue::list_contains(&event.year, year) {
+                t.changes.remove(TMChanges::YEAR);
+            } else {
+                if let Some(n) = DateTimeValue::find_next(&event.year, year) {
+                    t.add_years(n - year)?;
+                    continue;
+                } else {
+                    // if we have no valid year, we cannot find a correct timestamp
+                    return Ok(None);
+                }
+            }
+        }
+
+        if !event.month.is_empty() && t.changes.contains(TMChanges::MON) {
+            let month = t.month();
+            if DateTimeValue::list_contains(&event.month, month) {
+                t.changes.remove(TMChanges::MON);
+            } else {
+                if let Some(n) = DateTimeValue::find_next(&event.month, month) {
+                    t.add_months(n - month)?;
+                } else {
+                    // if we could not find valid month, retry next year
+                    t.add_years(1)?;
+                }
+                continue;
+            }
+        }
+
+        if !event.day.is_empty() && t.changes.contains(TMChanges::MDAY) {
+            let day = t.day();
+            if DateTimeValue::list_contains(&event.day, day) {
+                t.changes.remove(TMChanges::MDAY);
+            } else {
+                if let Some(n) = DateTimeValue::find_next(&event.day, day) {
+                    t.add_days(n - day)?;
+                } else {
+                    // if we could not find valid mday, retry next month
+                    t.add_months(1)?;
+                }
+                continue;
+            }
+        }
+
         if !all_days && t.changes.contains(TMChanges::WDAY) { // match day first
             let day_num = t.day_num();
             let day = WeekDays::from_bits(1<<day_num).unwrap();
@@ -294,6 +337,18 @@ mod test {
             Ok(expect)
         };
 
+        let test_never = |v: &'static str, last: i64| -> Result<(), Error> {
+            let event = match parse_calendar_event(v) {
+                Ok(event) => event,
+                Err(err) => bail!("parsing '{}' failed - {}", v, err),
+            };
+
+            match compute_next_event(&event, last, true)? {
+                None => Ok(()),
+                Some(next) => bail!("compute next for '{}' succeeded, but expected fail - result {}", v, next),
+            }
+        };
+
         const MIN: i64 = 60;
         const HOUR: i64 = 3600;
         const DAY: i64 = 3600*24;
@@ -361,6 +416,23 @@ mod test {
             n = test_value("1:0", n, THURSDAY_00_00 + i*DAY + HOUR)?;
         }
 
+        // test date functionality
+
+        test_value("2020-07-31", 0, JUL_31_2020)?;
+        test_value("02-28", 0, (31+27)*DAY)?;
+        test_value("02-29", 0, 2*365*DAY + (31+28)*DAY)?; // 1972-02-29
+        test_value("1965/5-01-01", -1, THURSDAY_00_00)?;
+        test_value("2020-7..9-2/2", JUL_31_2020, JUL_31_2020 + 2*DAY)?;
+        test_value("2020,2021-12-31", JUL_31_2020, DEC_31_2020)?;
+
+        test_value("monthly", 0, 31*DAY)?;
+        test_value("quarterly", 0, (31+28+31)*DAY)?;
+        test_value("semiannually", 0, (31+28+31+30+31+30)*DAY)?;
+        test_value("yearly", 0, (365)*DAY)?;
+
+        test_never("2021-02-29", 0)?;
+        test_never("02-30", 0)?;
+
         Ok(())
     }
 
-- 
2.20.1





      parent reply	other threads:[~2020-09-03 11:40 UTC|newest]

Thread overview: 16+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2020-09-03 11:39 [pbs-devel] [PATCH proxmox-backup 00/10] implement " Dominik Csapak
2020-09-03 11:39 ` [pbs-devel] [PATCH proxmox-backup 01/10] tools/systemd/time: let libc normalize time for us Dominik Csapak
2020-09-04  4:52   ` Dietmar Maurer
2020-09-04  4:57   ` Dietmar Maurer
2020-09-03 11:39 ` [pbs-devel] [PATCH proxmox-backup 02/10] tools/systemd/time: move continue out of the if/else Dominik Csapak
2020-09-03 11:39 ` [pbs-devel] [PATCH proxmox-backup 03/10] tools/systemd/time: convert the resulting timestamp into an option Dominik Csapak
2020-09-03 11:39 ` [pbs-devel] [PATCH proxmox-backup 04/10] tools/systemd/tm_editor: remove reset_time from add_days and document it Dominik Csapak
2020-09-04  5:12   ` Dietmar Maurer
2020-09-03 11:39 ` [pbs-devel] [PATCH proxmox-backup 05/10] tools/systemd/parse_time: error out on invalid ranges Dominik Csapak
2020-09-03 11:39 ` [pbs-devel] [PATCH proxmox-backup 06/10] tools/systemd/time: fix selection for multiple options Dominik Csapak
2020-09-03 11:39 ` [pbs-devel] [PATCH proxmox-backup 07/10] tools/systemd/time: use i32 for DateTimeValues instead of u32 Dominik Csapak
2020-09-04  5:31   ` Dietmar Maurer
2020-09-04  5:39   ` Dietmar Maurer
2020-09-03 11:39 ` [pbs-devel] [PATCH proxmox-backup 08/10] tools/systemd/tm_editor: remove conversion of the year Dominik Csapak
2020-09-03 11:39 ` [pbs-devel] [PATCH proxmox-backup 09/10] tools/systemd/tm_editor: add setter/getter for months/years/days Dominik Csapak
2020-09-03 11:40 ` Dominik Csapak [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20200903114000.6932-11-d.csapak@proxmox.com \
    --to=d.csapak@proxmox.com \
    --cc=pbs-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal