diff --git a/src/tools/systemd/parse_time.rs b/src/tools/systemd/parse_time.rs index 051c4968..d30a1acc 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, Vec IResult<&str, (Vec, Vec, Vec)> { + + // 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 { 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 7717ecee..c30e86f9 100644 --- a/src/tools/systemd/time.rs +++ b/src/tools/systemd/time.rs @@ -103,14 +103,12 @@ pub struct CalendarEvent { pub minute: Vec, /// the hour(s) this event should trigger pub hour: Vec, -/* FIXME: TODO /// the day(s) in a month this event should trigger pub day: Vec, /// the month(s) in a year this event should trigger pub month: Vec, /// the years(s) this event should trigger pub year: Vec, -*/ } #[derive(Default)] @@ -175,6 +173,45 @@ pub fn compute_next_event( 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 let day_num: u32 = t.day_num().try_into()?; let day = WeekDays::from_bits(1< 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; @@ -355,6 +404,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(()) }