tools/systemd/time: enable dates for calendarevents
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>
This commit is contained in:
parent
1dfc09cb6b
commit
44055cac4d
|
@ -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> {
|
pub fn parse_calendar_event(i: &str) -> Result<CalendarEvent, Error> {
|
||||||
parse_complete_line("calendar event", i, parse_calendar_event_incomplete)
|
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_dayspec = false;
|
||||||
let mut has_timespec = false;
|
let mut has_timespec = false;
|
||||||
let has_datespec = false;
|
let mut has_datespec = false;
|
||||||
|
|
||||||
let mut event = CalendarEvent::default();
|
let mut event = CalendarEvent::default();
|
||||||
|
|
||||||
|
@ -231,8 +250,52 @@ fn parse_calendar_event_incomplete(mut i: &str) -> IResult<&str, CalendarEvent>
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
"monthly" | "yearly" | "quarterly" | "semiannually" => {
|
"monthly" => {
|
||||||
return Err(parse_error(i, "unimplemented date or time specification"));
|
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 */ }
|
_ => { /* 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); }
|
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)? {
|
if let (n, Some((hour, minute, second))) = opt(parse_time_spec)(i)? {
|
||||||
event.hour = hour;
|
event.hour = hour;
|
||||||
|
|
|
@ -103,14 +103,12 @@ pub struct CalendarEvent {
|
||||||
pub minute: Vec<DateTimeValue>,
|
pub minute: Vec<DateTimeValue>,
|
||||||
/// the hour(s) this event should trigger
|
/// the hour(s) this event should trigger
|
||||||
pub hour: Vec<DateTimeValue>,
|
pub hour: Vec<DateTimeValue>,
|
||||||
/* FIXME: TODO
|
|
||||||
/// the day(s) in a month this event should trigger
|
/// the day(s) in a month this event should trigger
|
||||||
pub day: Vec<DateTimeValue>,
|
pub day: Vec<DateTimeValue>,
|
||||||
/// the month(s) in a year this event should trigger
|
/// the month(s) in a year this event should trigger
|
||||||
pub month: Vec<DateTimeValue>,
|
pub month: Vec<DateTimeValue>,
|
||||||
/// the years(s) this event should trigger
|
/// the years(s) this event should trigger
|
||||||
pub year: Vec<DateTimeValue>,
|
pub year: Vec<DateTimeValue>,
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -175,6 +173,45 @@ pub fn compute_next_event(
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !event.year.is_empty() {
|
||||||
|
let year: u32 = t.year().try_into()?;
|
||||||
|
if !DateTimeValue::list_contains(&event.year, year) {
|
||||||
|
if let Some(n) = DateTimeValue::find_next(&event.year, year) {
|
||||||
|
t.add_years((n - year).try_into()?)?;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// if we have no valid year, we cannot find a correct timestamp
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !event.month.is_empty() {
|
||||||
|
let month: u32 = t.month().try_into()?;
|
||||||
|
if !DateTimeValue::list_contains(&event.month, month) {
|
||||||
|
if let Some(n) = DateTimeValue::find_next(&event.month, month) {
|
||||||
|
t.add_months((n - month).try_into()?)?;
|
||||||
|
} else {
|
||||||
|
// if we could not find valid month, retry next year
|
||||||
|
t.add_years(1)?;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !event.day.is_empty() {
|
||||||
|
let day: u32 = t.day().try_into()?;
|
||||||
|
if !DateTimeValue::list_contains(&event.day, day) {
|
||||||
|
if let Some(n) = DateTimeValue::find_next(&event.day, day) {
|
||||||
|
t.add_days((n - day).try_into()?)?;
|
||||||
|
} else {
|
||||||
|
// if we could not find valid mday, retry next month
|
||||||
|
t.add_months(1)?;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !all_days { // match day first
|
if !all_days { // match day first
|
||||||
let day_num: u32 = t.day_num().try_into()?;
|
let day_num: u32 = t.day_num().try_into()?;
|
||||||
let day = WeekDays::from_bits(1<<day_num).unwrap();
|
let day = WeekDays::from_bits(1<<day_num).unwrap();
|
||||||
|
@ -288,6 +325,18 @@ mod test {
|
||||||
Ok(expect)
|
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 MIN: i64 = 60;
|
||||||
const HOUR: i64 = 3600;
|
const HOUR: i64 = 3600;
|
||||||
const DAY: i64 = 3600*24;
|
const DAY: i64 = 3600*24;
|
||||||
|
@ -355,6 +404,23 @@ mod test {
|
||||||
n = test_value("1:0", n, THURSDAY_00_00 + i*DAY + HOUR)?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue