src/tools/systemd/time.rs: implement compute_next_event
This commit is contained in:
		@ -4,6 +4,8 @@ use anyhow::{bail, Error};
 | 
			
		||||
use lazy_static::lazy_static;
 | 
			
		||||
use bitflags::bitflags;
 | 
			
		||||
 | 
			
		||||
use proxmox::tools::time::*;
 | 
			
		||||
 | 
			
		||||
use nom::{
 | 
			
		||||
    error::{context, ParseError, VerboseError},
 | 
			
		||||
    bytes::complete::{tag, take_while1},
 | 
			
		||||
@ -472,6 +474,163 @@ pub fn verify_calendar_event(i: &str) -> Result<(), Error> {
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
fn is_leap_year(year: libc::c_int) -> bool {
 | 
			
		||||
    if year % 4 != 0  { return false; }
 | 
			
		||||
    if year % 100 != 0 { return true; }
 | 
			
		||||
    if year % 400 != 0  { return false; }
 | 
			
		||||
    return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn days_in_month(mon: libc::c_int, year: libc::c_int) -> libc::c_int {
 | 
			
		||||
 | 
			
		||||
    let mon = mon % 12;
 | 
			
		||||
 | 
			
		||||
    static MAP: &[libc::c_int] = &[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
 | 
			
		||||
 | 
			
		||||
    if mon == 1 && is_leap_year(year) { return 29; }
 | 
			
		||||
 | 
			
		||||
    MAP[mon as usize]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn wrap_time(t: &mut libc::tm) {
 | 
			
		||||
 | 
			
		||||
    // sec: 0..59
 | 
			
		||||
    if t.tm_sec >= 60 {
 | 
			
		||||
        t.tm_min += t.tm_sec / 60;
 | 
			
		||||
        t.tm_sec %= 60;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // min: 0..59
 | 
			
		||||
    if t.tm_min >= 60 {
 | 
			
		||||
        t.tm_hour += t.tm_min / 60;
 | 
			
		||||
        t.tm_min %= 60;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // hour: 0..23
 | 
			
		||||
    if t.tm_hour >= 24 {
 | 
			
		||||
        t.tm_mday += t.tm_hour / 24;
 | 
			
		||||
        t.tm_wday += t.tm_hour / 24;
 | 
			
		||||
        t.tm_hour %= 24;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Translate to 0..($days_in_mon-1)
 | 
			
		||||
    t.tm_mday -= 1;
 | 
			
		||||
    loop {
 | 
			
		||||
	let days_in_mon = days_in_month(t.tm_mon, t.tm_year);
 | 
			
		||||
	if t.tm_mday < days_in_mon { break; }
 | 
			
		||||
	// Wrap one month
 | 
			
		||||
	t.tm_mday -= days_in_mon;
 | 
			
		||||
        t.tm_wday += 7 - (days_in_mon % 7);
 | 
			
		||||
	t.tm_mon += 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Translate back to 1..$days_in_mon
 | 
			
		||||
    t.tm_mday += 1;
 | 
			
		||||
 | 
			
		||||
    // mon: 0..11
 | 
			
		||||
    if t.tm_mon >= 12 {
 | 
			
		||||
        t.tm_year += t.tm_mon / 12;
 | 
			
		||||
        t.tm_mon %= 12;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    t.tm_wday %= 7;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn time_add_days(t: &mut libc::tm, days: libc::c_int) {
 | 
			
		||||
    t.tm_mday += days;
 | 
			
		||||
    t.tm_wday += days;
 | 
			
		||||
    wrap_time(t);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn compute_next_event(
 | 
			
		||||
    event: &CalendarEvent,
 | 
			
		||||
    last: i64,
 | 
			
		||||
    utc: bool,
 | 
			
		||||
) -> Result<i64, Error> {
 | 
			
		||||
 | 
			
		||||
    let last = last + 60; // at least one minute later
 | 
			
		||||
 | 
			
		||||
    let all_days = event.days.is_empty() || event.days.is_all();
 | 
			
		||||
 | 
			
		||||
    let mut t = if utc { gmtime(last)? } else { localtime(last)? };
 | 
			
		||||
    t.tm_sec = 0; // we're not interested in seconds, actually
 | 
			
		||||
    t.tm_year += 1900; // real years for clarity
 | 
			
		||||
 | 
			
		||||
    let mut count = 0;
 | 
			
		||||
 | 
			
		||||
    loop {
 | 
			
		||||
        if count > 1000 { // should not happen
 | 
			
		||||
            bail!("unable to compute next calendar event");
 | 
			
		||||
        } else {
 | 
			
		||||
            count += 1;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if !all_days { // match day first
 | 
			
		||||
            // Note: tm_wday (0-6, Sunday = 0) => convert to Sunday = 6
 | 
			
		||||
            let day_num = (t.tm_wday + 6) % 7;
 | 
			
		||||
            let day = WeekDays::from_bits(1<<day_num).unwrap();
 | 
			
		||||
            if !event.days.contains(day) {
 | 
			
		||||
                if let Some(n) = (day_num+1..6)
 | 
			
		||||
                    .map(|d| WeekDays::from_bits(1<<d).unwrap())
 | 
			
		||||
                    .find(|d| event.days.contains(*d))
 | 
			
		||||
                {
 | 
			
		||||
                    // try next day
 | 
			
		||||
                    t.tm_sec = 0; t.tm_min = 0; t.tm_hour = 0;
 | 
			
		||||
                    time_add_days(&mut t, (n.bits() as i32) - day_num);
 | 
			
		||||
                    continue;
 | 
			
		||||
                } else {
 | 
			
		||||
                    // try next week
 | 
			
		||||
                    t.tm_sec = 0; t.tm_min = 0; t.tm_hour = 0;
 | 
			
		||||
                    time_add_days(&mut t, 7 - day_num);
 | 
			
		||||
                    continue;
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // this day
 | 
			
		||||
        if !event.hour.is_empty() {
 | 
			
		||||
            let hour = t.tm_hour as u32;
 | 
			
		||||
            if event.hour.iter().find(|hspec| hspec.contains(hour)).is_none() {
 | 
			
		||||
                if let Some(n) = DateTimeValue::find_next(&event.hour, hour) {
 | 
			
		||||
                    // test next hour
 | 
			
		||||
                    t.tm_sec = 0; t.tm_min = 0; t.tm_hour += n as libc::c_int;
 | 
			
		||||
                    wrap_time(&mut t);
 | 
			
		||||
                    continue;
 | 
			
		||||
                } else {
 | 
			
		||||
                    // test next day
 | 
			
		||||
                    t.tm_sec = 0; t.tm_min = 0; t.tm_hour = 0;
 | 
			
		||||
                    time_add_days(&mut t, 1);
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // this hour
 | 
			
		||||
        if !event.minute.is_empty() {
 | 
			
		||||
            let minute = t.tm_min as u32;
 | 
			
		||||
            if event.minute.iter().find(|hspec| hspec.contains(minute)).is_none() {
 | 
			
		||||
                if let Some(n) = DateTimeValue::find_next(&event.minute, minute) {
 | 
			
		||||
                    // test next minute
 | 
			
		||||
                    t.tm_sec = 0; t.tm_min += n as libc::c_int;
 | 
			
		||||
                    wrap_time(&mut t);
 | 
			
		||||
                    continue;
 | 
			
		||||
                } else {
 | 
			
		||||
                   // test next hour
 | 
			
		||||
                    t.tm_sec = 0; t.tm_min = 0; t.tm_hour += 1;
 | 
			
		||||
                    wrap_time(&mut t);
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        t.tm_year -= 1900;
 | 
			
		||||
        let next = if utc { timegm(t)? } else { timelocal(t)? };
 | 
			
		||||
        return Ok(next)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod test {
 | 
			
		||||
 | 
			
		||||
@ -486,6 +645,65 @@ mod test {
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const fn make_test_time(mday: i32, hour: i32, min: i32) -> libc::time_t {
 | 
			
		||||
        (mday*3600*24 + hour*3600 + min*60) as libc::time_t
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_compute_next_event() -> Result<(), Error> {
 | 
			
		||||
 | 
			
		||||
        let test_value = |v: &'static str, last: i64, expect: i64| -> Result<i64, 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) {
 | 
			
		||||
                Ok(next) => {
 | 
			
		||||
                    if next == expect {
 | 
			
		||||
                        println!("next {:?} => {}", event, next);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        bail!("next {:?} failed\nnext:  {:?}\nexpect: {:?}",
 | 
			
		||||
                              event, gmtime(next), gmtime(expect));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                Err(err) => bail!("compute next for '{}' failed - {}", v, err),
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Ok(expect)
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const MIN: i64 = 60;
 | 
			
		||||
        const HOUR: i64 = 3600;
 | 
			
		||||
        const DAY: i64 = 3600*24;
 | 
			
		||||
 | 
			
		||||
        const THURSDAY_00_00: i64 = make_test_time(0, 0, 0);
 | 
			
		||||
        const THURSDAY_15_00: i64 = make_test_time(0, 15, 0);
 | 
			
		||||
 | 
			
		||||
        test_value("*:*", THURSDAY_00_00, THURSDAY_00_00 + MIN)?;
 | 
			
		||||
 | 
			
		||||
        test_value("mon *:*", THURSDAY_00_00, THURSDAY_00_00 + 4*DAY)?;
 | 
			
		||||
        test_value("mon 2:*", THURSDAY_00_00, THURSDAY_00_00 + 4*DAY + 2*HOUR)?;
 | 
			
		||||
        test_value("mon 2:50", THURSDAY_00_00, THURSDAY_00_00 + 4*DAY + 2*HOUR + 50*MIN)?;
 | 
			
		||||
 | 
			
		||||
        let mut n = test_value("*:*", THURSDAY_00_00, THURSDAY_00_00 + MIN)?;
 | 
			
		||||
        for i in 2..100 {
 | 
			
		||||
            n = test_value("*:*", n, THURSDAY_00_00 + i*MIN)?;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let mut n = test_value("*:0", THURSDAY_00_00, THURSDAY_00_00 + HOUR)?;
 | 
			
		||||
        for i in 2..100 {
 | 
			
		||||
            n = test_value("*:0", n, THURSDAY_00_00 + i*HOUR)?;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let mut n = test_value("1:0", THURSDAY_15_00, THURSDAY_00_00 + DAY + HOUR)?;
 | 
			
		||||
        for i in 2..100 {
 | 
			
		||||
            n = test_value("1:0", n, THURSDAY_00_00 + i*DAY + HOUR)?;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_calendar_event_weekday() -> Result<(), Error> {
 | 
			
		||||
        test_event("mon,wed..fri")?;
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user