From a49a849917962c2bbdd0eac83149c66e01659938 Mon Sep 17 00:00:00 2001 From: jude Date: Sat, 10 Dec 2022 15:32:49 +0000 Subject: [PATCH] Support daily intervals Add new database column for interval_days. Update humantime to return days as a separate field. --- Cargo.lock | 2 +- .../05-reminder_variable_intervals_2.sql | 3 + postman/src/sender.rs | 54 ++++----- src/interval_parser.rs | 110 +++++++++++++++--- src/models/reminder/builder.rs | 20 +++- src/models/reminder/mod.rs | 12 +- 6 files changed, 145 insertions(+), 56 deletions(-) create mode 100644 migration/05-reminder_variable_intervals_2.sql diff --git a/Cargo.lock b/Cargo.lock index fc8f196..9ff6d0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2133,7 +2133,7 @@ checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "reminder_rs" -version = "1.6.8" +version = "1.6.9" dependencies = [ "base64", "chrono", diff --git a/migration/05-reminder_variable_intervals_2.sql b/migration/05-reminder_variable_intervals_2.sql new file mode 100644 index 0000000..a6b8047 --- /dev/null +++ b/migration/05-reminder_variable_intervals_2.sql @@ -0,0 +1,3 @@ +USE reminders; + +ALTER TABLE reminders.reminders ADD COLUMN `interval_days` INT UNSIGNED DEFAULT NULL; diff --git a/postman/src/sender.rs b/postman/src/sender.rs index 71c6ef9..22fc91c 100644 --- a/postman/src/sender.rs +++ b/postman/src/sender.rs @@ -1,4 +1,4 @@ -use chrono::{DateTime, Days, Duration, Months, TimeZone}; +use chrono::{DateTime, Days, Duration, Months}; use chrono_tz::Tz; use lazy_static::lazy_static; use log::{error, info, warn}; @@ -253,6 +253,7 @@ pub struct Reminder { restartable: bool, expires: Option>, interval_seconds: Option, + interval_days: Option, interval_months: Option, avatar: Option, @@ -286,6 +287,7 @@ SELECT reminders.`restartable` AS restartable, reminders.`expires` AS 'expires', reminders.`interval_seconds` AS 'interval_seconds', + reminders.`interval_days` AS 'interval_days', reminders.`interval_months` AS 'interval_months', reminders.`avatar` AS avatar, @@ -348,42 +350,30 @@ WHERE let mut updated_reminder_time = self.utc_time.with_timezone(&self.timezone.parse().unwrap_or(Tz::UTC)); - if let Some(interval) = self.interval_months { - updated_reminder_time = updated_reminder_time - .checked_add_months(Months::new(interval)) - .unwrap_or_else(|| { - warn!("Could not add months to a reminder"); + while updated_reminder_time < now { + if let Some(interval) = self.interval_months { + updated_reminder_time = updated_reminder_time + .checked_add_months(Months::new(interval)) + .unwrap_or_else(|| { + warn!("Could not add months to a reminder"); - updated_reminder_time - }); - } - - fn increment_days( - now: DateTime, - mut new_time: DateTime, - interval: u32, - ) -> Option> { - while new_time < now { - new_time = new_time.checked_add_days(Days::new((interval / 86400).into()))?; + updated_reminder_time + }); } - Some(new_time) - } + if let Some(interval) = self.interval_days { + updated_reminder_time = updated_reminder_time + .checked_add_days(Days::new(interval as u64)) + .unwrap_or_else(|| { + warn!("Could not add days to a reminder"); - if let Some(interval) = self.interval_seconds { - if interval.div_rem(&86400).1 == 0 { + updated_reminder_time + }); + } + + if let Some(interval) = self.interval_seconds { updated_reminder_time = - match increment_days(now, updated_reminder_time, interval) { - Some(d) => d, - None => { - warn!("Failed to update days on a reminder."); - updated_reminder_time - } - } - } - - while updated_reminder_time < now { - updated_reminder_time += Duration::seconds(interval as i64); + updated_reminder_time + Duration::seconds(interval as i64); } } diff --git a/src/interval_parser.rs b/src/interval_parser.rs index 8befd57..f0d560b 100644 --- a/src/interval_parser.rs +++ b/src/interval_parser.rs @@ -110,13 +110,14 @@ impl OverflowOp for u64 { #[derive(Copy, Clone)] pub struct Interval { pub month: u64, + pub day: u64, pub sec: u64, } struct Parser<'a> { iter: Chars<'a>, src: &'a str, - current: (u64, u64, u64), + current: (u64, u64, u64, u64), } impl<'a> Parser<'a> { @@ -140,17 +141,17 @@ impl<'a> Parser<'a> { Ok(None) } fn parse_unit(&mut self, n: u64, start: usize, end: usize) -> Result<(), Error> { - let (mut month, mut sec, nsec) = match &self.src[start..end] { - "nanos" | "nsec" | "ns" => (0u64, 0u64, n), - "usec" | "us" => (0, 0u64, n.mul(1000)?), - "millis" | "msec" | "ms" => (0, 0u64, n.mul(1_000_000)?), - "seconds" | "second" | "secs" | "sec" | "s" => (0, n, 0), - "minutes" | "minute" | "min" | "mins" | "m" => (0, n.mul(60)?, 0), - "hours" | "hour" | "hr" | "hrs" | "h" => (0, n.mul(3600)?, 0), - "days" | "day" | "d" => (0, n.mul(86400)?, 0), - "weeks" | "week" | "w" => (0, n.mul(86400 * 7)?, 0), - "months" | "month" | "M" => (n, 0, 0), - "years" | "year" | "y" => (12, 0, 0), + let (mut month, mut day, mut sec, nsec) = match &self.src[start..end] { + "nanos" | "nsec" | "ns" => (0, 0u64, 0u64, n), + "usec" | "us" => (0, 0, 0u64, n.mul(1000)?), + "millis" | "msec" | "ms" => (0, 0, 0u64, n.mul(1_000_000)?), + "seconds" | "second" | "secs" | "sec" | "s" => (0, 0, n, 0), + "minutes" | "minute" | "min" | "mins" | "m" => (0, 0, n.mul(60)?, 0), + "hours" | "hour" | "hr" | "hrs" | "h" => (0, 0, n.mul(3600)?, 0), + "days" | "day" | "d" => (0, n, 0, 0), + "weeks" | "week" | "w" => (0, n.mul(7)?, 0, 0), + "months" | "month" | "M" => (n, 0, 0, 0), + "years" | "year" | "y" => (n.mul(12)?, 0, 0, 0), _ => { return Err(Error::UnknownUnit { start, @@ -160,15 +161,16 @@ impl<'a> Parser<'a> { }); } }; - let mut nsec = self.current.2 + nsec; + let mut nsec = self.current.3 + nsec; if nsec > 1_000_000_000 { sec += nsec / 1_000_000_000; nsec %= 1_000_000_000; } - sec += self.current.1; + sec += self.current.2; + day += self.current.1; month += self.current.0; - self.current = (month, sec, nsec); + self.current = (month, day, sec, nsec); Ok(()) } @@ -215,7 +217,13 @@ impl<'a> Parser<'a> { self.parse_unit(n, start, off)?; n = match self.parse_first_char()? { Some(n) => n, - None => return Ok(Interval { month: self.current.0, sec: self.current.1 }), + None => { + return Ok(Interval { + month: self.current.0, + day: self.current.1, + sec: self.current.2, + }) + } }; } } @@ -247,5 +255,73 @@ impl<'a> Parser<'a> { /// assert_eq!(parse_duration("32ms"), Ok(Duration::new(0, 32_000_000))); /// ``` pub fn parse_duration(s: &str) -> Result { - Parser { iter: s.chars(), src: s, current: (0, 0, 0) }.parse() + Parser { iter: s.chars(), src: s, current: (0, 0, 0, 0) }.parse() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_seconds() { + let interval = parse_duration("10 seconds").unwrap(); + + assert_eq!(interval.sec, 10); + assert_eq!(interval.day, 0); + assert_eq!(interval.month, 0); + } + + #[test] + fn parse_minutes() { + let interval = parse_duration("10 minutes").unwrap(); + + assert_eq!(interval.sec, 600); + assert_eq!(interval.day, 0); + assert_eq!(interval.month, 0); + } + + #[test] + fn parse_hours() { + let interval = parse_duration("10 hours").unwrap(); + + assert_eq!(interval.sec, 36_000); + assert_eq!(interval.day, 0); + assert_eq!(interval.month, 0); + } + + #[test] + fn parse_days() { + let interval = parse_duration("10 days").unwrap(); + + assert_eq!(interval.sec, 0); + assert_eq!(interval.day, 10); + assert_eq!(interval.month, 0); + } + + #[test] + fn parse_weeks() { + let interval = parse_duration("10 weeks").unwrap(); + + assert_eq!(interval.sec, 0); + assert_eq!(interval.day, 70); + assert_eq!(interval.month, 0); + } + + #[test] + fn parse_months() { + let interval = parse_duration("10 months").unwrap(); + + assert_eq!(interval.sec, 0); + assert_eq!(interval.day, 0); + assert_eq!(interval.month, 10); + } + + #[test] + fn parse_years() { + let interval = parse_duration("10 years").unwrap(); + + assert_eq!(interval.sec, 0); + assert_eq!(interval.day, 0); + assert_eq!(interval.month, 120); + } } diff --git a/src/models/reminder/builder.rs b/src/models/reminder/builder.rs index 8420bd9..9fd91a6 100644 --- a/src/models/reminder/builder.rs +++ b/src/models/reminder/builder.rs @@ -53,7 +53,8 @@ pub struct ReminderBuilder { channel: u32, utc_time: NaiveDateTime, timezone: String, - interval_secs: Option, + interval_seconds: Option, + interval_days: Option, interval_months: Option, expires: Option, content: String, @@ -87,6 +88,7 @@ INSERT INTO reminders ( `utc_time`, `timezone`, `interval_seconds`, + `interval_days`, `interval_months`, `expires`, `content`, @@ -106,6 +108,7 @@ INSERT INTO reminders ( ?, ?, ?, + ?, ? ) ", @@ -113,7 +116,8 @@ INSERT INTO reminders ( self.channel, utc_time, self.timezone, - self.interval_secs, + self.interval_seconds, + self.interval_days, self.interval_months, self.expires, self.content, @@ -210,9 +214,14 @@ impl<'a> MultiReminderBuilder<'a> { let mut ok_locs = HashSet::new(); - if self.interval.map_or(false, |i| ((i.sec + i.month * 30 * DAY) as i64) < *MIN_INTERVAL) { + if self + .interval + .map_or(false, |i| ((i.sec + i.day * DAY + i.month * 30 * DAY) as i64) < *MIN_INTERVAL) + { errors.insert(ReminderError::ShortInterval); - } else if self.interval.map_or(false, |i| ((i.sec + i.month * 30 * DAY) as i64) > *MAX_TIME) + } else if self + .interval + .map_or(false, |i| ((i.sec + i.day * DAY + i.month * 30 * DAY) as i64) > *MAX_TIME) { errors.insert(ReminderError::LongInterval); } else { @@ -300,7 +309,8 @@ impl<'a> MultiReminderBuilder<'a> { channel: c, utc_time: self.utc_time, timezone: self.timezone.to_string(), - interval_secs: self.interval.map(|i| i.sec as i64), + interval_seconds: self.interval.map(|i| i.sec as i64), + interval_days: self.interval.map(|i| i.day as i64), interval_months: self.interval.map(|i| i.month as i64), expires: self.expires, content: self.content.content.clone(), diff --git a/src/models/reminder/mod.rs b/src/models/reminder/mod.rs index 423b586..27c601a 100644 --- a/src/models/reminder/mod.rs +++ b/src/models/reminder/mod.rs @@ -26,6 +26,7 @@ pub struct Reminder { pub channel: u64, pub utc_time: DateTime, pub interval_seconds: Option, + pub interval_days: Option, pub interval_months: Option, pub expires: Option, pub enabled: bool, @@ -59,6 +60,7 @@ SELECT channels.channel, reminders.utc_time, reminders.interval_seconds, + reminders.interval_days, reminders.interval_months, reminders.expires, reminders.enabled, @@ -95,6 +97,7 @@ SELECT channels.channel, reminders.utc_time, reminders.interval_seconds, + reminders.interval_days, reminders.interval_months, reminders.expires, reminders.enabled, @@ -138,6 +141,7 @@ SELECT channels.channel, reminders.utc_time, reminders.interval_seconds, + reminders.interval_days, reminders.interval_months, reminders.expires, reminders.enabled, @@ -195,6 +199,7 @@ SELECT channels.channel, reminders.utc_time, reminders.interval_seconds, + reminders.interval_days, reminders.interval_months, reminders.expires, reminders.enabled, @@ -228,6 +233,7 @@ SELECT channels.channel, reminders.utc_time, reminders.interval_seconds, + reminders.interval_days, reminders.interval_months, reminders.expires, reminders.enabled, @@ -262,6 +268,7 @@ SELECT channels.channel, reminders.utc_time, reminders.interval_seconds, + reminders.interval_days, reminders.interval_months, reminders.expires, reminders.enabled, @@ -323,7 +330,10 @@ WHERE TimeDisplayType::Relative => format!("", self.utc_time.timestamp()), }; - if self.interval_seconds.is_some() || self.interval_months.is_some() { + if self.interval_seconds.is_some() + || self.interval_days.is_some() + || self.interval_months.is_some() + { format!( "'{}' *occurs next at* **{}**, repeating (set by {})\n", self.display_content(),