diff --git a/proxmox-time/Cargo.toml b/proxmox-time/Cargo.toml index 691cd82a..eaaed9a0 100644 --- a/proxmox-time/Cargo.toml +++ b/proxmox-time/Cargo.toml @@ -9,4 +9,8 @@ description = "time utilities and TmEditor" exclude = [ "debian" ] [dependencies] +anyhow = "1.0" +bitflags = "1.2.1" +lazy_static = "1.4" libc = { version = "0.2", features = [ "extra_traits" ] } +nom = "5.1" diff --git a/proxmox-time/src/daily_duration.rs b/proxmox-time/src/daily_duration.rs new file mode 100644 index 00000000..2f4b03df --- /dev/null +++ b/proxmox-time/src/daily_duration.rs @@ -0,0 +1,173 @@ +use std::cmp::{Ordering, PartialOrd}; +use std::convert::{TryFrom, TryInto}; + +use anyhow::Error; + +use crate::{TmEditor, WeekDays}; + +pub use super::parse_time::parse_daily_duration; + +/// Time of Day (hour with minute) +#[derive(Default, PartialEq, Clone, Debug)] +pub struct HmTime { + pub hour: u32, + pub minute: u32, +} + +impl PartialOrd for HmTime { + fn partial_cmp(&self, other: &Self) -> Option { + let mut order = self.hour.cmp(&other.hour); + if order == Ordering::Equal { + order = self.minute.cmp(&other.minute); + } + Some(order) + } +} + +/// Defines a period of time for on or more [WeekDays] +#[derive(Default, Clone, Debug)] +pub struct DailyDuration { + /// the days in a week this duration should trigger + pub days: WeekDays, + pub start: HmTime, + pub end: HmTime, +} + +impl DailyDuration { + + /// Test it time is within this frame + pub fn time_match(&self, epoch: i64, utc: bool) -> Result { + + let t = TmEditor::with_epoch(epoch, utc)?; + + Ok(self.time_match_with_tm_editor(&t)) + } + + /// Like time_match, but use [TmEditor] to specify the time + /// + /// Note: This function returns bool (not Result). It + /// simply returns ''false' if passed time 't' contains invalid values. + pub fn time_match_with_tm_editor(&self, t: &TmEditor) -> bool { + let all_days = self.days.is_empty() || self.days.is_all(); + + if !all_days { // match day first + match u32::try_from(t.day_num()) { + Ok(day_num) => { + match WeekDays::from_bits(1< { + if !self.days.contains(day) { + return false; + } + } + None => return false, + } + } + Err(_) => return false, + } + } + + let hour = t.hour().try_into(); + let minute = t.min().try_into(); + + match (hour, minute) { + (Ok(hour), Ok(minute)) => { + let ctime = HmTime { hour, minute }; + ctime >= self.start && ctime < self.end + } + _ => false, + } + } +} + +#[cfg(test)] +mod test { + + use anyhow::{bail, Error}; + + use super::*; + + fn test_parse( + duration_str: &str, + start_h: u32, start_m: u32, + end_h: u32, end_m: u32, + days: &[usize], + ) -> Result<(), Error> { + let mut day_bits = 0; + for day in days { day_bits |= 1< i64 { + (mday*3600*24 + hour*3600 + min*60) as i64 + } + + #[test] + fn test_daily_duration_parser() -> Result<(), Error> { + + assert!(parse_daily_duration("").is_err()); + assert!(parse_daily_duration(" 8-12").is_err()); + assert!(parse_daily_duration("8:60-12").is_err()); + assert!(parse_daily_duration("8-25").is_err()); + assert!(parse_daily_duration("12-8").is_err()); + + test_parse("8-12", 8, 0, 12, 0, &[])?; + test_parse("8:0-12:0", 8, 0, 12, 0, &[])?; + test_parse("8:00-12:00", 8, 0, 12, 0, &[])?; + test_parse("8:05-12:20", 8, 5, 12, 20, &[])?; + test_parse("8:05 - 12:20", 8, 5, 12, 20, &[])?; + + test_parse("mon 8-12", 8, 0, 12, 0, &[0])?; + test_parse("tue..fri 8-12", 8, 0, 12, 0, &[1,2,3,4])?; + test_parse("sat,tue..thu,fri 8-12", 8, 0, 12, 0, &[1,2,3,4,5])?; + + Ok(()) + } + + #[test] + fn test_time_match() -> Result<(), Error> { + const THURSDAY_80_00: i64 = make_test_time(0, 8, 0); + const THURSDAY_12_00: i64 = make_test_time(0, 12, 0); + const DAY: i64 = 3600*24; + + let duration = parse_daily_duration("thu..fri 8:05-12")?; + + assert!(!duration.time_match(THURSDAY_80_00, true)?); + assert!(!duration.time_match(THURSDAY_80_00 + DAY, true)?); + assert!(!duration.time_match(THURSDAY_80_00 + 2*DAY, true)?); + + assert!(duration.time_match(THURSDAY_80_00 + 5*60, true)?); + assert!(duration.time_match(THURSDAY_80_00 + 5*60 + DAY, true)?); + assert!(!duration.time_match(THURSDAY_80_00 + 5*60 + 2*DAY, true)?); + + assert!(duration.time_match(THURSDAY_12_00 - 1, true)?); + assert!(duration.time_match(THURSDAY_12_00 - 1 + DAY, true)?); + assert!(!duration.time_match(THURSDAY_12_00 - 1 + 2*DAY, true)?); + + assert!(!duration.time_match(THURSDAY_12_00, true)?); + assert!(!duration.time_match(THURSDAY_12_00 + DAY, true)?); + assert!(!duration.time_match(THURSDAY_12_00 + 2*DAY, true)?); + + Ok(()) + } +} diff --git a/proxmox-time/src/lib.rs b/proxmox-time/src/lib.rs index 62e68d7f..bfa696e9 100644 --- a/proxmox-time/src/lib.rs +++ b/proxmox-time/src/lib.rs @@ -5,6 +5,15 @@ use std::io; mod tm_editor; pub use tm_editor::*; +mod parse_time; +pub use parse_time::*; + +mod time; +pub use time::*; + +mod daily_duration; +pub use daily_duration::*; + #[derive(Debug)] pub struct Error { msg: String, diff --git a/proxmox-time/src/parse_time.rs b/proxmox-time/src/parse_time.rs new file mode 100644 index 00000000..89d147e7 --- /dev/null +++ b/proxmox-time/src/parse_time.rs @@ -0,0 +1,513 @@ +use std::collections::HashMap; + +use anyhow::{bail, Error}; +use lazy_static::lazy_static; + +use super::time::*; +use super::daily_duration::*; + +use nom::{ + error::{context, ParseError, VerboseError}, + bytes::complete::{tag, take_while1}, + combinator::{map_res, all_consuming, opt, recognize}, + sequence::{pair, preceded, tuple}, + character::complete::{alpha1, space0, digit1}, + multi::separated_nonempty_list, +}; + +type IResult> = Result<(I, O), nom::Err>; + +fn parse_error<'a>(i: &'a str, context: &'static str) -> nom::Err> { + let err = VerboseError { errors: Vec::new() }; + let err = VerboseError::add_context(i, context, err); + nom::Err::Error(err) +} + +// Parse a 64 bit unsigned integer +fn parse_u64(i: &str) -> IResult<&str, u64> { + map_res(recognize(digit1), str::parse)(i) +} + +// Parse complete input, generate simple error message (use this for sinple line input). +fn parse_complete_line<'a, F, O>(what: &str, i: &'a str, parser: F) -> Result + where F: Fn(&'a str) -> IResult<&'a str, O>, +{ + match all_consuming(parser)(i) { + Err(nom::Err::Error(VerboseError { errors })) | + Err(nom::Err::Failure(VerboseError { errors })) => { + if errors.is_empty() { + bail!("unable to parse {}", what); + } else { + bail!("unable to parse {} at '{}' - {:?}", what, errors[0].0, errors[0].1); + } + } + Err(err) => { + bail!("unable to parse {} - {}", what, err); + } + Ok((_, data)) => Ok(data), + } +} + +lazy_static! { + static ref TIME_SPAN_UNITS: HashMap<&'static str, f64> = { + let mut map = HashMap::new(); + + let second = 1.0; + + map.insert("seconds", second); + map.insert("second", second); + map.insert("sec", second); + map.insert("s", second); + + let msec = second / 1000.0; + + map.insert("msec", msec); + map.insert("ms", msec); + + let usec = msec / 1000.0; + + map.insert("usec", usec); + map.insert("us", usec); + map.insert("µs", usec); + + let nsec = usec / 1000.0; + + map.insert("nsec", nsec); + map.insert("ns", nsec); + + let minute = second * 60.0; + + map.insert("minutes", minute); + map.insert("minute", minute); + map.insert("min", minute); + map.insert("m", minute); + + let hour = minute * 60.0; + + map.insert("hours", hour); + map.insert("hour", hour); + map.insert("hr", hour); + map.insert("h", hour); + + let day = hour * 24.0 ; + + map.insert("days", day); + map.insert("day", day); + map.insert("d", day); + + let week = day * 7.0; + + map.insert("weeks", week); + map.insert("week", week); + map.insert("w", week); + + let month = 30.44 * day; + + map.insert("months", month); + map.insert("month", month); + map.insert("M", month); + + let year = 365.25 * day; + + map.insert("years", year); + map.insert("year", year); + map.insert("y", year); + + map + }; +} + +struct TimeSpec { + hour: Vec, + minute: Vec, + second: Vec, +} + +struct DateSpec { + year: Vec, + month: Vec, + day: Vec, +} + +fn parse_time_comp(max: usize) -> impl Fn(&str) -> IResult<&str, u32> { + move |i: &str| { + let (i, v) = map_res(recognize(digit1), str::parse)(i)?; + if (v as usize) >= max { + return Err(parse_error(i, "time value too large")); + } + Ok((i, v)) + } +} + +fn parse_weekday(i: &str) -> IResult<&str, WeekDays> { + let (i, text) = alpha1(i)?; + + match text.to_ascii_lowercase().as_str() { + "monday" | "mon" => Ok((i, WeekDays::MONDAY)), + "tuesday" | "tue" => Ok((i, WeekDays::TUESDAY)), + "wednesday" | "wed" => Ok((i, WeekDays::WEDNESDAY)), + "thursday" | "thu" => Ok((i, WeekDays::THURSDAY)), + "friday" | "fri" => Ok((i, WeekDays::FRIDAY)), + "saturday" | "sat" => Ok((i, WeekDays::SATURDAY)), + "sunday" | "sun" => Ok((i, WeekDays::SUNDAY)), + _ => return Err(parse_error(text, "weekday")), + } +} + +fn parse_weekdays_range(i: &str) -> IResult<&str, WeekDays> { + let (i, startday) = parse_weekday(i)?; + + let generate_range = |start, end| { + let mut res = 0; + let mut pos = start; + loop { + res |= pos; + if pos >= end { break; } + pos <<= 1; + } + WeekDays::from_bits(res).unwrap() + }; + + if let (i, Some((_, endday))) = opt(pair(tag(".."),parse_weekday))(i)? { + let start = startday.bits(); + let end = endday.bits(); + if start > end { + let set1 = generate_range(start, WeekDays::SUNDAY.bits()); + let set2 = generate_range(WeekDays::MONDAY.bits(), end); + Ok((i, set1 | set2)) + } else { + Ok((i, generate_range(start, end))) + } + } else { + Ok((i, startday)) + } +} + +fn parse_date_time_comp(max: usize) -> impl Fn(&str) -> IResult<&str, DateTimeValue> { + move |i: &str| { + let (i, value) = parse_time_comp(max)(i)?; + + if let (i, Some(end)) = opt(preceded(tag(".."), parse_time_comp(max)))(i)? { + if value > end { + return Err(parse_error(i, "range start is bigger than end")); + } + return Ok((i, DateTimeValue::Range(value, end))) + } + + if let Some(time) = i.strip_prefix('/') { + let (time, repeat) = parse_time_comp(max)(time)?; + Ok((time, DateTimeValue::Repeated(value, repeat))) + } else { + Ok((i, DateTimeValue::Single(value))) + } + } +} + +fn parse_date_time_comp_list(start: u32, max: usize) -> impl Fn(&str) -> IResult<&str, Vec> { + move |i: &str| { + if let Some(rest) = i.strip_prefix('*') { + if let Some(time) = rest.strip_prefix('/') { + let (n, repeat) = parse_time_comp(max)(time)?; + if repeat > 0 { + return Ok((n, vec![DateTimeValue::Repeated(start, repeat)])); + } + } + return Ok((rest, Vec::new())); + } + + separated_nonempty_list(tag(","), parse_date_time_comp(max))(i) + } +} + +fn parse_time_spec(i: &str) -> IResult<&str, TimeSpec> { + + let (i, (hour, minute, opt_second)) = tuple(( + parse_date_time_comp_list(0, 24), + preceded(tag(":"), parse_date_time_comp_list(0, 60)), + opt(preceded(tag(":"), parse_date_time_comp_list(0, 60))), + ))(i)?; + + if let Some(second) = opt_second { + Ok((i, TimeSpec { hour, minute, second })) + } else { + Ok((i, TimeSpec { hour, minute, second: vec![DateTimeValue::Single(0)] })) + } +} + +fn parse_date_spec(i: &str) -> IResult<&str, DateSpec> { + + // TODO: implement ~ for days (man systemd.time) + if let Ok((i, (year, month, day))) = tuple(( + parse_date_time_comp_list(0, 2200), // the upper limit for systemd, stay compatible + preceded(tag("-"), parse_date_time_comp_list(1, 13)), + preceded(tag("-"), parse_date_time_comp_list(1, 32)), + ))(i) { + Ok((i, DateSpec { year, month, day })) + } else if let Ok((i, (month, day))) = tuple(( + parse_date_time_comp_list(1, 13), + preceded(tag("-"), parse_date_time_comp_list(1, 32)), + ))(i) { + Ok((i, DateSpec { year: Vec::new(), month, day })) + } else { + Err(parse_error(i, "invalid date spec")) + } +} + +/// Parse a [CalendarEvent] +pub fn parse_calendar_event(i: &str) -> Result { + parse_complete_line("calendar event", i, parse_calendar_event_incomplete) +} + +fn parse_calendar_event_incomplete(mut i: &str) -> IResult<&str, CalendarEvent> { + + let mut has_dayspec = false; + let mut has_timespec = false; + let mut has_datespec = false; + + let mut event = CalendarEvent::default(); + + if i.starts_with(|c: char| char::is_ascii_alphabetic(&c)) { + + match i { + "minutely" => { + return Ok(("", CalendarEvent { + second: vec![DateTimeValue::Single(0)], + ..Default::default() + })); + } + "hourly" => { + return Ok(("", CalendarEvent { + minute: vec![DateTimeValue::Single(0)], + second: vec![DateTimeValue::Single(0)], + ..Default::default() + })); + } + "daily" => { + return Ok(("", CalendarEvent { + hour: vec![DateTimeValue::Single(0)], + minute: vec![DateTimeValue::Single(0)], + second: vec![DateTimeValue::Single(0)], + ..Default::default() + })); + } + "weekly" => { + return Ok(("", CalendarEvent { + hour: vec![DateTimeValue::Single(0)], + minute: vec![DateTimeValue::Single(0)], + second: vec![DateTimeValue::Single(0)], + days: WeekDays::MONDAY, + ..Default::default() + })); + } + "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 */ } + } + + let (n, range_list) = context( + "weekday range list", + separated_nonempty_list(tag(","), parse_weekdays_range) + )(i)?; + + has_dayspec = true; + + i = space0(n)?.0; + + for range in range_list { event.days.insert(range); } + } + + if let (n, Some(date)) = opt(parse_date_spec)(i)? { + event.year = date.year; + event.month = date.month; + event.day = date.day; + has_datespec = true; + i = space0(n)?.0; + } + + if let (n, Some(time)) = opt(parse_time_spec)(i)? { + event.hour = time.hour; + event.minute = time.minute; + event.second = time.second; + has_timespec = true; + i = n; + } else { + event.hour = vec![DateTimeValue::Single(0)]; + event.minute = vec![DateTimeValue::Single(0)]; + event.second = vec![DateTimeValue::Single(0)]; + } + + if !(has_dayspec || has_timespec || has_datespec) { + return Err(parse_error(i, "date or time specification")); + } + + Ok((i, event)) +} + +fn parse_time_unit(i: &str) -> IResult<&str, &str> { + let (n, text) = take_while1(|c: char| char::is_ascii_alphabetic(&c) || c == 'µ')(i)?; + if TIME_SPAN_UNITS.contains_key(&text) { + Ok((n, text)) + } else { + Err(parse_error(text, "time unit")) + } +} + + +/// Parse a [TimeSpan] +pub fn parse_time_span(i: &str) -> Result { + parse_complete_line("time span", i, parse_time_span_incomplete) +} + +fn parse_time_span_incomplete(mut i: &str) -> IResult<&str, TimeSpan> { + + let mut ts = TimeSpan::default(); + + loop { + i = space0(i)?.0; + if i.is_empty() { break; } + let (n, num) = parse_u64(i)?; + i = space0(n)?.0; + + if let (n, Some(unit)) = opt(parse_time_unit)(i)? { + i = n; + match unit { + "seconds" | "second" | "sec" | "s" => { + ts.seconds += num; + } + "msec" | "ms" => { + ts.msec += num; + } + "usec" | "us" | "µs" => { + ts.usec += num; + } + "nsec" | "ns" => { + ts.nsec += num; + } + "minutes" | "minute" | "min" | "m" => { + ts.minutes += num; + } + "hours" | "hour" | "hr" | "h" => { + ts.hours += num; + } + "days" | "day" | "d" => { + ts.days += num; + } + "weeks" | "week" | "w" => { + ts.weeks += num; + } + "months" | "month" | "M" => { + ts.months += num; + } + "years" | "year" | "y" => { + ts.years += num; + } + _ => return Err(parse_error(unit, "internal error")), + } + } else { + ts.seconds += num; + } + } + + Ok((i, ts)) +} + +/// Parse a [DailyDuration] +pub fn parse_daily_duration(i: &str) -> Result { + parse_complete_line("daily duration", i, parse_daily_duration_incomplete) +} + +fn parse_daily_duration_incomplete(mut i: &str) -> IResult<&str, DailyDuration> { + + let mut duration = DailyDuration::default(); + + if i.starts_with(|c: char| char::is_ascii_alphabetic(&c)) { + + let (n, range_list) = context( + "weekday range list", + separated_nonempty_list(tag(","), parse_weekdays_range) + )(i)?; + + i = space0(n)?.0; + + for range in range_list { duration.days.insert(range); } + } + + let (i, start) = parse_hm_time(i)?; + + let i = space0(i)?.0; + + let (i, _) = tag("-")(i)?; + + let i = space0(i)?.0; + + let end_time_start = i; + + let (i, end) = parse_hm_time(i)?; + + if start > end { + return Err(parse_error(end_time_start, "end time before start time")); + } + + duration.start = start; + duration.end = end; + + Ok((i, duration)) +} + +fn parse_hm_time(i: &str) -> IResult<&str, HmTime> { + + let (i, (hour, opt_minute)) = tuple(( + parse_time_comp(24), + opt(preceded(tag(":"), parse_time_comp(60))), + ))(i)?; + + match opt_minute { + Some(minute) => Ok((i, HmTime { hour, minute })), + None => Ok((i, HmTime { hour, minute: 0})), + } +} diff --git a/proxmox-time/src/time.rs b/proxmox-time/src/time.rs new file mode 100644 index 00000000..8e09e977 --- /dev/null +++ b/proxmox-time/src/time.rs @@ -0,0 +1,590 @@ +use std::convert::TryInto; + +use anyhow::Error; +use bitflags::bitflags; + +use crate::TmEditor; + +use crate::{parse_calendar_event, parse_time_span}; + +bitflags!{ + /// Defines one or more days of a week. + #[derive(Default)] + pub struct WeekDays: u8 { + const MONDAY = 1; + const TUESDAY = 2; + const WEDNESDAY = 4; + const THURSDAY = 8; + const FRIDAY = 16; + const SATURDAY = 32; + const SUNDAY = 64; + } +} + +#[derive(Debug, Clone)] +pub(crate) enum DateTimeValue { + Single(u32), + Range(u32, u32), + Repeated(u32, u32), +} + +impl DateTimeValue { + // Test if the entry contains the value + pub fn contains(&self, value: u32) -> bool { + match self { + DateTimeValue::Single(v) => *v == value, + DateTimeValue::Range(start, end) => value >= *start && value <= *end, + DateTimeValue::Repeated(start, repetition) => { + if value >= *start { + if *repetition > 0 { + let offset = value - start; + offset % repetition == 0 + } else { + *start == value + } + } else { + false + } + } + } + } + + pub fn list_contains(list: &[DateTimeValue], value: u32) -> bool { + list.iter().any(|spec| spec.contains(value)) + } + + // Find an return an entry greater than value + pub fn find_next(list: &[DateTimeValue], value: u32) -> Option { + let mut next: Option = None; + let mut set_next = |v: u32| { + if let Some(n) = next { + if v < n { next = Some(v); } + } else { + next = Some(v); + } + }; + for spec in list { + match spec { + DateTimeValue::Single(v) => { + if *v > value { set_next(*v); } + } + DateTimeValue::Range(start, end) => { + if value < *start { + set_next(*start); + } else { + let n = value + 1; + if n >= *start && n <= *end { + set_next(n); + } + } + } + DateTimeValue::Repeated(start, repetition) => { + if value < *start { + set_next(*start); + } else if *repetition > 0 { + set_next(start + ((value - start + repetition) / repetition) * repetition); + } + } + } + } + + next + } +} + +/// Calendar events may be used to refer to one or more points in time in a +/// single expression. They are designed after the systemd.time Calendar Events +/// specification, but are not guaranteed to be 100% compatible. +#[derive(Default, Clone, Debug)] +pub struct CalendarEvent { + /// the days in a week this event should trigger + pub(crate) days: WeekDays, + /// the second(s) this event should trigger + pub(crate) second: Vec, // todo: support float values + /// the minute(s) this event should trigger + pub(crate) minute: Vec, + /// the hour(s) this event should trigger + pub(crate) hour: Vec, + /// the day(s) in a month this event should trigger + pub(crate) day: Vec, + /// the month(s) in a year this event should trigger + pub(crate) month: Vec, + /// the years(s) this event should trigger + pub(crate) year: Vec, +} + +/// A time spans defines a time duration +#[derive(Default, Clone, Debug)] +pub struct TimeSpan { + pub nsec: u64, + pub usec: u64, + pub msec: u64, + pub seconds: u64, + pub minutes: u64, + pub hours: u64, + pub days: u64, + pub weeks: u64, + pub months: u64, + pub years: u64, +} + +impl From for f64 { + fn from(ts: TimeSpan) -> Self { + (ts.seconds as f64) + + ((ts.nsec as f64) / 1_000_000_000.0) + + ((ts.usec as f64) / 1_000_000.0) + + ((ts.msec as f64) / 1_000.0) + + ((ts.minutes as f64) * 60.0) + + ((ts.hours as f64) * 3600.0) + + ((ts.days as f64) * 3600.0 * 24.0) + + ((ts.weeks as f64) * 3600.0 * 24.0 * 7.0) + + ((ts.months as f64) * 3600.0 * 24.0 * 30.44) + + ((ts.years as f64) * 3600.0 * 24.0 * 365.25) + } +} + +impl From for TimeSpan { + fn from(duration: std::time::Duration) -> Self { + let mut duration = duration.as_nanos(); + let nsec = (duration % 1000) as u64; + duration /= 1000; + let usec = (duration % 1000) as u64; + duration /= 1000; + let msec = (duration % 1000) as u64; + duration /= 1000; + let seconds = (duration % 60) as u64; + duration /= 60; + let minutes = (duration % 60) as u64; + duration /= 60; + let hours = (duration % 24) as u64; + duration /= 24; + let years = (duration as f64 / 365.25) as u64; + let ydays = (duration as f64 % 365.25) as u64; + let months = (ydays as f64 / 30.44) as u64; + let mdays = (ydays as f64 % 30.44) as u64; + let weeks = mdays / 7; + let days = mdays % 7; + Self { + nsec, + usec, + msec, + seconds, + minutes, + hours, + days, + weeks, + months, + years, + } + } +} + +impl std::fmt::Display for TimeSpan { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + let mut first = true; + { // block scope for mutable borrows + let mut do_write = |v: u64, unit: &str| -> Result<(), std::fmt::Error> { + if !first { + write!(f, " ")?; + } + first = false; + write!(f, "{}{}", v, unit) + }; + if self.years > 0 { + do_write(self.years, "y")?; + } + if self.months > 0 { + do_write(self.months, "m")?; + } + if self.weeks > 0 { + do_write(self.weeks, "w")?; + } + if self.days > 0 { + do_write(self.days, "d")?; + } + if self.hours > 0 { + do_write(self.hours, "h")?; + } + if self.minutes > 0 { + do_write(self.minutes, "min")?; + } + } + if !first { + write!(f, " ")?; + } + let seconds = self.seconds as f64 + (self.msec as f64 / 1000.0); + if seconds >= 0.1 { + if seconds >= 1.0 || !first { + write!(f, "{:.0}s", seconds)?; + } else { + write!(f, "{:.1}s", seconds)?; + } + } else if first { + write!(f, "<0.1s")?; + } + Ok(()) + } +} + +/// Verify the format of the [TimeSpan] +pub fn verify_time_span(i: &str) -> Result<(), Error> { + parse_time_span(i)?; + Ok(()) +} + +/// Verify the format of the [CalendarEvent] +pub fn verify_calendar_event(i: &str) -> Result<(), Error> { + parse_calendar_event(i)?; + Ok(()) +} + +/// Compute the next event +pub fn compute_next_event( + event: &CalendarEvent, + last: i64, + utc: bool, +) -> Result, Error> { + + let last = last + 1; // at least one second later + + let all_days = event.days.is_empty() || event.days.is_all(); + + let mut t = TmEditor::with_epoch(last, utc)?; + + let mut count = 0; + + loop { + // cancel after 1000 loops + if count > 1000 { + return Ok(None); + } else { + 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> { + match parse_calendar_event(v) { + Ok(event) => println!("CalendarEvent '{}' => {:?}", v, event), + Err(err) => bail!("parsing '{}' failed - {}", v, err), + } + + Ok(()) + } + + const fn make_test_time(mday: i32, hour: i32, min: i32) -> i64 { + (mday*3600*24 + hour*3600 + min*60) as i64 + } + + #[test] + fn test_compute_next_event() -> Result<(), Error> { + + let test_value = |v: &'static str, last: i64, expect: i64| -> Result { + 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(Some(next)) => { + if next == expect { + println!("next {:?} => {}", event, next); + } else { + bail!( + "next {:?} failed\nnext: {:?}\nexpect: {:?}", + event, + crate::gmtime(next), + crate::gmtime(expect), + ); + } + } + Ok(None) => bail!("next {:?} failed to find a timestamp", event), + Err(err) => bail!("compute next for '{}' failed - {}", v, err), + } + + 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; + + const THURSDAY_00_00: i64 = make_test_time(0, 0, 0); + const THURSDAY_15_00: i64 = make_test_time(0, 15, 0); + + const JUL_31_2020: i64 = 1596153600; // Friday, 2020-07-31 00:00:00 + const DEC_31_2020: i64 = 1609372800; // Thursday, 2020-12-31 00:00:00 + + test_value("*:0", THURSDAY_00_00, THURSDAY_00_00 + HOUR)?; + test_value("*:*", THURSDAY_00_00, THURSDAY_00_00 + MIN)?; + test_value("*:*:*", THURSDAY_00_00, THURSDAY_00_00 + 1)?; + test_value("*:3:5", THURSDAY_00_00, THURSDAY_00_00 + 3*MIN + 5)?; + + 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)?; + + test_value("tue", THURSDAY_00_00, THURSDAY_00_00 + 5*DAY)?; + test_value("wed", THURSDAY_00_00, THURSDAY_00_00 + 6*DAY)?; + test_value("thu", THURSDAY_00_00, THURSDAY_00_00 + 7*DAY)?; + test_value("fri", THURSDAY_00_00, THURSDAY_00_00 + 1*DAY)?; + test_value("sat", THURSDAY_00_00, THURSDAY_00_00 + 2*DAY)?; + test_value("sun", THURSDAY_00_00, THURSDAY_00_00 + 3*DAY)?; + + // test multiple values for a single field + // and test that the order does not matter + test_value("5,10:4,8", THURSDAY_00_00, THURSDAY_00_00 + 5*HOUR + 4*MIN)?; + test_value("10,5:8,4", THURSDAY_00_00, THURSDAY_00_00 + 5*HOUR + 4*MIN)?; + test_value("6,4..10:23,5/5", THURSDAY_00_00, THURSDAY_00_00 + 4*HOUR + 5*MIN)?; + test_value("4..10,6:5/5,23", THURSDAY_00_00, THURSDAY_00_00 + 4*HOUR + 5*MIN)?; + + // test month wrapping + test_value("sat", JUL_31_2020, JUL_31_2020 + 1*DAY)?; + test_value("sun", JUL_31_2020, JUL_31_2020 + 2*DAY)?; + test_value("mon", JUL_31_2020, JUL_31_2020 + 3*DAY)?; + test_value("tue", JUL_31_2020, JUL_31_2020 + 4*DAY)?; + test_value("wed", JUL_31_2020, JUL_31_2020 + 5*DAY)?; + test_value("thu", JUL_31_2020, JUL_31_2020 + 6*DAY)?; + test_value("fri", JUL_31_2020, JUL_31_2020 + 7*DAY)?; + + // test year wrapping + test_value("fri", DEC_31_2020, DEC_31_2020 + 1*DAY)?; + test_value("sat", DEC_31_2020, DEC_31_2020 + 2*DAY)?; + test_value("sun", DEC_31_2020, DEC_31_2020 + 3*DAY)?; + test_value("mon", DEC_31_2020, DEC_31_2020 + 4*DAY)?; + test_value("tue", DEC_31_2020, DEC_31_2020 + 5*DAY)?; + test_value("wed", DEC_31_2020, DEC_31_2020 + 6*DAY)?; + test_value("thu", DEC_31_2020, DEC_31_2020 + 7*DAY)?; + + test_value("daily", THURSDAY_00_00, THURSDAY_00_00 + DAY)?; + test_value("daily", THURSDAY_00_00+1, THURSDAY_00_00 + DAY)?; + + let n = test_value("5/2:0", THURSDAY_00_00, THURSDAY_00_00 + 5*HOUR)?; + let n = test_value("5/2:0", n, THURSDAY_00_00 + 7*HOUR)?; + let n = test_value("5/2:0", n, THURSDAY_00_00 + 9*HOUR)?; + test_value("5/2:0", n, THURSDAY_00_00 + 11*HOUR)?; + + 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)?; + } + + // 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(()) + } + + #[test] + fn test_calendar_event_weekday() -> Result<(), Error> { + test_event("mon,wed..fri")?; + test_event("fri..mon")?; + + test_event("mon")?; + test_event("MON")?; + test_event("monDay")?; + test_event("tue")?; + test_event("Tuesday")?; + test_event("wed")?; + test_event("wednesday")?; + test_event("thu")?; + test_event("thursday")?; + test_event("fri")?; + test_event("friday")?; + test_event("sat")?; + test_event("saturday")?; + test_event("sun")?; + test_event("sunday")?; + + test_event("mon..fri")?; + test_event("mon,tue,fri")?; + test_event("mon,tue..wednesday,fri..sat")?; + + Ok(()) + } + + #[test] + fn test_time_span_parser() -> Result<(), Error> { + + let test_value = |ts_str: &str, expect: f64| -> Result<(), Error> { + let ts = parse_time_span(ts_str)?; + assert_eq!(f64::from(ts), expect, "{}", ts_str); + Ok(()) + }; + + test_value("2", 2.0)?; + test_value("2s", 2.0)?; + test_value("2sec", 2.0)?; + test_value("2second", 2.0)?; + test_value("2seconds", 2.0)?; + + test_value(" 2s 2 s 2", 6.0)?; + + test_value("1msec 1ms", 0.002)?; + test_value("1usec 1us 1µs", 0.000_003)?; + test_value("1nsec 1ns", 0.000_000_002)?; + test_value("1minutes 1minute 1min 1m", 4.0*60.0)?; + test_value("1hours 1hour 1hr 1h", 4.0*3600.0)?; + test_value("1days 1day 1d", 3.0*86400.0)?; + test_value("1weeks 1 week 1w", 3.0*86400.0*7.0)?; + test_value("1months 1month 1M", 3.0*86400.0*30.44)?; + test_value("1years 1year 1y", 3.0*86400.0*365.25)?; + + test_value("2h", 7200.0)?; + test_value(" 2 h", 7200.0)?; + test_value("2hours", 7200.0)?; + test_value("48hr", 48.0*3600.0)?; + test_value("1y 12month", 365.25*24.0*3600.0 + 12.0*30.44*24.0*3600.0)?; + test_value("55s500ms", 55.5)?; + test_value("300ms20s 5day", 5.0*24.0*3600.0 + 20.0 + 0.3)?; + + Ok(()) + } +}