aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJoris2022-02-26 18:35:24 +0100
committerJoris2022-02-26 18:35:24 +0100
commit2d80413609130f1c121dcae39a150a27dd9f02ea (patch)
treea8c807b7e95d5049ea43a1757d292b5cb745524b /src
parent1445e23a26c6581ad0c3f5b5016e47e95d224e9f (diff)
Show repeated events
Diffstat (limited to 'src')
-rw-r--r--src/app/app.rs21
-rw-r--r--src/app/calendar.rs44
-rw-r--r--src/app/form/mod.rs4
-rw-r--r--src/app/form/repetition.rs34
-rw-r--r--src/app/update.rs91
-rw-r--r--src/db/migrations/1-init.sql16
-rw-r--r--src/db/mod.rs42
-rw-r--r--src/model/event.rs20
-rw-r--r--src/model/repetition.rs152
9 files changed, 347 insertions, 77 deletions
diff --git a/src/app/app.rs b/src/app/app.rs
index b1ee395..58240af 100644
--- a/src/app/app.rs
+++ b/src/app/app.rs
@@ -1,7 +1,7 @@
use gtk4 as gtk;
use async_channel::Sender;
-use chrono::{Datelike, NaiveDate, Weekday};
+use chrono::{Datelike, Duration, NaiveDate, Weekday};
use gtk::glib::signal::Inhibit;
use gtk::prelude::*;
use rusqlite::Connection;
@@ -16,8 +16,10 @@ pub struct App {
pub window: Rc<gtk::ApplicationWindow>,
pub grid: gtk::Grid,
pub events: Vec<Event>,
+ pub repeated_events: Vec<Event>,
pub today: NaiveDate,
pub start_date: NaiveDate,
+ pub end_date: NaiveDate,
pub tx: Sender<Msg>,
}
@@ -36,8 +38,19 @@ impl App {
let today = chrono::offset::Local::today().naive_utc();
let start_date =
NaiveDate::from_isoywd(today.year(), today.iso_week().week(), Weekday::Mon);
- let events = db::list(&conn).unwrap_or(vec![]);
- let grid = calendar::create(tx.clone(), &today, &start_date, &events);
+ let end_date = start_date + Duration::days(7 * 4 - 1);
+
+ let events = db::list_non_repeated_between(&conn, start_date, end_date).unwrap_or(vec![]);
+ let repeated_events = db::list_repeated(&conn).unwrap_or(vec![]);
+
+ let grid = calendar::create(
+ tx.clone(),
+ today,
+ start_date,
+ end_date,
+ &events,
+ &repeated_events,
+ );
window.set_child(Some(&grid));
@@ -53,8 +66,10 @@ impl App {
window,
grid,
events,
+ repeated_events,
today,
start_date,
+ end_date,
tx,
}
}
diff --git a/src/app/calendar.rs b/src/app/calendar.rs
index fa4ebe6..11eb893 100644
--- a/src/app/calendar.rs
+++ b/src/app/calendar.rs
@@ -4,8 +4,9 @@ use async_channel::Sender;
use chrono::{Datelike, NaiveDate};
use gtk::glib;
use gtk::prelude::*;
+use std::collections::HashMap;
-use crate::{app::update, app::update::Msg, app::App, model::event::Event};
+use crate::{app::update, app::update::Msg, app::App, model::event, model::event::Event};
static DAYS: [&str; 7] = ["LUN", "MAR", "MER", "JEU", "VEN", "SAM", "DIM"];
static MONTHES: [&str; 12] = [
@@ -14,9 +15,11 @@ static MONTHES: [&str; 12] = [
pub fn create(
tx: Sender<Msg>,
- today: &NaiveDate,
- start_date: &NaiveDate,
+ today: NaiveDate,
+ start_date: NaiveDate,
+ end_date: NaiveDate,
events: &Vec<Event>,
+ repeated_events: &Vec<Event>,
) -> gtk::Grid {
let grid = gtk::Grid::builder().build();
@@ -24,7 +27,8 @@ pub fn create(
grid.attach(&day_title(col), col, 0, 1, 1);
}
- attach_days(tx, &grid, &start_date, &today, &events);
+ let repetitions = event::repetitions_between(repeated_events, start_date, end_date);
+ attach_days(tx, &grid, start_date, today, events, &repetitions);
grid
}
@@ -32,27 +36,34 @@ pub fn create(
fn attach_days(
tx: Sender<Msg>,
grid: &gtk::Grid,
- start_date: &NaiveDate,
- today: &NaiveDate,
+ start_date: NaiveDate,
+ today: NaiveDate,
events: &Vec<Event>,
+ repetitions: &HashMap<NaiveDate, Vec<Event>>,
) {
- let mut d = *start_date;
+ let mut d = start_date;
for row in 1..5 {
for col in 0..7 {
- grid.attach(&day_entry(tx.clone(), &d, &today, &events), col, row, 1, 1);
+ grid.attach(
+ &day_entry(tx.clone(), d, today, events, repetitions),
+ col,
+ row,
+ 1,
+ 1,
+ );
d = d.succ();
}
}
}
-pub fn refresh_date(app: &App, date: NaiveDate) {
+pub fn refresh_date(app: &App, date: NaiveDate, repetitions: &HashMap<NaiveDate, Vec<Event>>) {
let d = date.signed_duration_since(app.start_date).num_days();
let col = (d % 7) as i32;
let row = 1 + (d / 7) as i32;
app.grid.attach(
- &day_entry(app.tx.clone(), &date, &app.today, &app.events),
+ &day_entry(app.tx.clone(), date, app.today, &app.events, repetitions),
col,
row,
1,
@@ -76,9 +87,10 @@ fn day_title(col: i32) -> gtk::Box {
pub fn day_entry(
tx: Sender<Msg>,
- date: &NaiveDate,
- today: &NaiveDate,
+ date: NaiveDate,
+ today: NaiveDate,
events: &Vec<Event>,
+ repetitions: &HashMap<NaiveDate, Vec<Event>>,
) -> gtk::ScrolledWindow {
let vbox = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
@@ -87,7 +99,7 @@ pub fn day_entry(
vbox.add_css_class("g-Calendar__Day");
let gesture = gtk::GestureClick::new();
- gesture.connect_pressed(glib::clone!(@strong date, @strong tx => move |_, n, _, _| {
+ gesture.connect_pressed(glib::clone!(@strong tx => move |_, n, _, _| {
if n == 2 {
update::send(tx.clone(), Msg::ShowAddForm { date });
}
@@ -102,8 +114,10 @@ pub fn day_entry(
let mut events = events
.iter()
- .filter(|e| e.date == *date)
+ .filter(|e| e.date == date)
.collect::<Vec<&Event>>();
+ let repeated_events = repetitions.get(&date).map(|e| e.clone()).unwrap_or(vec![]);
+ events.extend(repeated_events.iter());
events.sort_by_key(|e| e.start);
if !events.is_empty() {
@@ -120,7 +134,7 @@ pub fn day_entry(
scrolled_window
}
-fn day_label(date: &NaiveDate) -> gtk::Label {
+fn day_label(date: NaiveDate) -> gtk::Label {
let label = gtk::Label::builder()
.label(&format!(
"{} {}",
diff --git a/src/app/form/mod.rs b/src/app/form/mod.rs
index 5c60bc5..9cb6ba7 100644
--- a/src/app/form/mod.rs
+++ b/src/app/form/mod.rs
@@ -85,10 +85,10 @@ pub async fn show(app: &App, event: Event, is_new: bool) {
update::send(tx.clone(), msg);
dialog.close()
},
- Err(err) => println!("Error when upserting event: {err}")
+ Err(err) => eprintln!("Error when upserting event: {err}")
}
},
- None => println!("Event is not valid: {event:?}")
+ None => eprintln!("Event is not valid: {event:?}")
}
}));
diff --git a/src/app/form/repetition.rs b/src/app/form/repetition.rs
index ac56479..87c8d84 100644
--- a/src/app/form/repetition.rs
+++ b/src/app/form/repetition.rs
@@ -7,7 +7,7 @@ use crate::{
model::event::Event,
model::{
repetition,
- repetition::{MonthFrequency, Repetition},
+ repetition::{DayOfMonth, Repetition},
},
};
@@ -19,14 +19,14 @@ static WEEKDAYS: [Weekday; 7] = [Mon, Tue, Wed, Thu, Fri, Sat, Sun];
pub struct Model {
pub view: gtk::Box,
- pub no_radio: gtk::CheckButton,
- pub day_interval_radio: gtk::CheckButton,
- pub day_interval_entry: gtk::Entry,
- pub monthly_radio: gtk::CheckButton,
- pub monthly_entry: gtk::Entry,
- pub first_day_radio: gtk::CheckButton,
- pub first_day_dropdown: gtk::DropDown,
- pub yearly_radio: gtk::CheckButton,
+ no_radio: gtk::CheckButton,
+ day_interval_radio: gtk::CheckButton,
+ day_interval_entry: gtk::Entry,
+ monthly_radio: gtk::CheckButton,
+ monthly_entry: gtk::Entry,
+ first_day_radio: gtk::CheckButton,
+ first_day_dropdown: gtk::DropDown,
+ yearly_radio: gtk::CheckButton,
}
pub fn view(event: &Event) -> Model {
@@ -44,7 +44,7 @@ pub fn view(event: &Event) -> Model {
view.append(&no_radio);
let default = match event.repetition {
- Some(Repetition::Daily { frequency }) => frequency.to_string(),
+ Some(Repetition::Daily { period }) => period.to_string(),
_ => "".to_string(),
};
let day_interval_entry = gtk::Entry::builder().text(&default).build();
@@ -58,7 +58,7 @@ pub fn view(event: &Event) -> Model {
let default = match event.repetition {
Some(Repetition::Monthly {
- frequency: MonthFrequency::Day { day },
+ day: DayOfMonth::Day { day },
}) => day.to_string(),
_ => "".to_string(),
};
@@ -69,8 +69,8 @@ pub fn view(event: &Event) -> Model {
let (active, default) = match event.repetition {
Some(Repetition::Monthly {
- frequency: MonthFrequency::FirstDay { day },
- }) => (true, day),
+ day: DayOfMonth::Weekday { weekday },
+ }) => (true, weekday),
_ => (false, Mon),
};
let first_day_dropdown = gtk::DropDown::from_strings(&WEEKDAYS_STR);
@@ -131,17 +131,17 @@ pub fn validate(model: &Model) -> Option<Repetition> {
None
} else if model.day_interval_radio.is_active() {
repetition::validate_day(&model.day_interval_entry.buffer().text())
- .map(|d| Repetition::Daily { frequency: d })
+ .map(|d| Repetition::Daily { period: d })
} else if model.monthly_radio.is_active() {
repetition::validate_day(&model.monthly_entry.buffer().text()).map(|d| {
Repetition::Monthly {
- frequency: MonthFrequency::Day { day: d },
+ day: DayOfMonth::Day { day: d },
}
})
} else if model.first_day_radio.is_active() {
- let day = WEEKDAYS[model.first_day_dropdown.selected() as usize];
+ let weekday = WEEKDAYS[model.first_day_dropdown.selected() as usize];
Some(Repetition::Monthly {
- frequency: MonthFrequency::FirstDay { day },
+ day: DayOfMonth::Weekday { weekday },
})
} else if model.yearly_radio.is_active() {
Some(Repetition::Yearly)
diff --git a/src/app/update.rs b/src/app/update.rs
index 4e21050..4ef1eb1 100644
--- a/src/app/update.rs
+++ b/src/app/update.rs
@@ -1,5 +1,6 @@
use async_channel::{Receiver, Sender};
use chrono::NaiveDate;
+use std::collections::HashSet;
use crate::{
app::{calendar, form, utils, App},
@@ -30,31 +31,75 @@ pub async fn event_handler(rx: Receiver<Msg>, mut app: App) {
form::show(&app, event, false).await;
}
Msg::AddEvent { new } => {
- let date = new.date.clone();
- app.events.push(new);
- calendar::refresh_date(&app, date);
+ let refresh_dates = add(&mut app, &new);
+ refresh(&app, &refresh_dates)
}
Msg::UpdateEvent { old, new } => {
- let new_date = new.date.clone();
- match app.events.iter().position(|e| e.id == new.id) {
- Some(index) => {
- app.events.remove(index);
- app.events.push(new);
- calendar::refresh_date(&app, new_date);
- if old.date != new_date {
- calendar::refresh_date(&app, old.date.clone())
- }
- }
- None => println!("Event not found when updating from {:?} to {:?}", old, new),
- }
- }
- Msg::DeleteEvent { event } => match app.events.iter().position(|e| e.id == event.id) {
- Some(index) => {
- app.events.remove(index);
- calendar::refresh_date(&app, event.date);
- }
- None => println!("Event not found when trying to delete {:?}", event),
- },
+ let refresh_dates_1 = remove(&mut app, &old);
+ let refresh_dates_2 = add(&mut app, &new);
+ refresh(&app, &refresh_dates_1.union(&refresh_dates_2).map(|d| *d).collect::<HashSet<NaiveDate>>())
+ }
+ Msg::DeleteEvent { event } => {
+ let refresh_dates = remove(&mut app, &event);
+ refresh(&app, &refresh_dates)
+ }
+ }
+ }
+}
+
+/// Remove event and return dates that should be refreshed.
+fn remove(app: &mut App, event: &Event) -> HashSet<NaiveDate> {
+ if event.repetition.is_some() {
+ match app.repeated_events.iter().position(|e| e.id == event.id) {
+ Some(index) => {
+ app.repeated_events.remove(index);
+ let mut dates = repetition_dates(&app, event);
+ dates.insert(event.date);
+ dates
+ }
+ None => {
+ eprintln!("Event not found when trying to delete {:?}", event);
+ HashSet::new()
+ }
+ }
+ } else {
+ match app.events.iter().position(|e| e.id == event.id) {
+ Some(index) => {
+ app.events.remove(index);
+ HashSet::from([event.date])
+ }
+ None => {
+ eprintln!("Event not found when trying to delete {:?}", event);
+ HashSet::new()
+ }
}
}
}
+
+/// Add event and return dates that should be refreshed.
+fn add(app: &mut App, event: &Event) -> HashSet<NaiveDate> {
+ if event.repetition.is_some() {
+ app.repeated_events.push(event.clone());
+ let mut dates = repetition_dates(&app, event);
+ dates.insert(event.date);
+ dates
+ } else {
+ app.events.push(event.clone());
+ HashSet::from([event.date])
+ }
+}
+
+/// Repetition dates of a repetead event.
+fn repetition_dates(app: &App, event: &Event) -> HashSet<NaiveDate> {
+ let event_reps = event::repetitions_between(&vec!(event.clone()), app.start_date, app.end_date);
+ HashSet::from_iter(event_reps.keys().map(|d| *d))
+}
+
+/// Refresh app for the given dates.
+fn refresh(app: &App, dates: &HashSet<NaiveDate>) {
+ let repetitions = event::repetitions_between(&app.repeated_events, app.start_date, app.end_date);
+
+ for date in dates {
+ calendar::refresh_date(app, *date, &repetitions)
+ }
+}
diff --git a/src/db/migrations/1-init.sql b/src/db/migrations/1-init.sql
index a7db8b8..7e49764 100644
--- a/src/db/migrations/1-init.sql
+++ b/src/db/migrations/1-init.sql
@@ -1,10 +1,10 @@
CREATE TABLE IF NOT EXISTS "events" (
- "id" VARCHAR PRIMARY KEY,
- "date" VARCHAR NOT NULL,
- "start" VARCHAR NULL,
- "end" VARCHAR NULL,
- "name" VARCHAR NOT NULL,
- "repetition" VARCHAR NULL,
- "created" TIMESTAMP NOT NULL,
- "updated" TIMESTAMP NOT NULL
+ "id" TEXT PRIMARY KEY, /* UUID */
+ "date" TEXT NOT NULL, /* DATE */
+ "start" TEXT NULL, /* TIME */
+ "end" TEXT NULL, /* TIME */
+ "name" TEXT NOT NULL,
+ "repetition" TEXT NULL, /* JSON */
+ "created" TEXT NOT NULL, /* DATETIME */
+ "updated" TEXT NOT NULL /* DATETIME */
);
diff --git a/src/db/mod.rs b/src/db/mod.rs
index 2cac0d2..b27226b 100644
--- a/src/db/mod.rs
+++ b/src/db/mod.rs
@@ -1,4 +1,5 @@
use anyhow::Result;
+use chrono::NaiveDate;
use rusqlite::{params, Connection};
use rusqlite_migration::{Migrations, M};
use uuid::Uuid;
@@ -50,8 +51,13 @@ pub fn delete(conn: &Connection, id: &Uuid) -> Result<()> {
}
// TODO: Don’t use unwrap
-pub fn list(conn: &Connection) -> Result<Vec<Event>> {
- let mut stmt = conn.prepare("SELECT id, date, start, end, name, repeated FROM events")?;
+pub fn list_repeated(conn: &Connection) -> Result<Vec<Event>> {
+ let mut stmt = conn.prepare(
+ "
+ SELECT id, date, start, end, name, repetition
+ FROM events
+ WHERE repetition IS NOT NULL",
+ )?;
let iter = stmt.query_map([], |row| {
let uuid: String = row.get(0)?;
@@ -68,3 +74,35 @@ pub fn list(conn: &Connection) -> Result<Vec<Event>> {
Ok(iter.map(|r| r.unwrap()).collect())
}
+
+// TODO: Don’t use unwrap
+pub fn list_non_repeated_between(
+ conn: &Connection,
+ start: NaiveDate,
+ end: NaiveDate,
+) -> Result<Vec<Event>> {
+ let mut stmt = conn.prepare(
+ "
+ SELECT id, date, start, end, name
+ FROM events
+ WHERE
+ repetition IS NULL
+ AND date >= ?
+ AND date <= ?
+ ",
+ )?;
+
+ let iter = stmt.query_map([start, end], |row| {
+ let uuid: String = row.get(0)?;
+ Ok(Event {
+ id: Uuid::parse_str(&uuid).unwrap(),
+ date: row.get(1)?,
+ start: row.get(2)?,
+ end: row.get(3)?,
+ name: row.get(4)?,
+ repetition: None,
+ })
+ })?;
+
+ Ok(iter.map(|r| r.unwrap()).collect())
+}
diff --git a/src/model/event.rs b/src/model/event.rs
index 3765fec..b18d811 100644
--- a/src/model/event.rs
+++ b/src/model/event.rs
@@ -1,5 +1,6 @@
use chrono::Timelike;
use chrono::{NaiveDate, NaiveTime};
+use std::collections::HashMap;
use uuid::Uuid;
use crate::model::repetition::Repetition;
@@ -43,6 +44,25 @@ impl Event {
}
}
+/// Repeated events in an included date range
+pub fn repetitions_between(
+ events: &Vec<Event>,
+ start: NaiveDate,
+ end: NaiveDate,
+) -> HashMap<NaiveDate, Vec<Event>> {
+ let mut res: HashMap<NaiveDate, Vec<Event>> = HashMap::new();
+
+ for event in events {
+ for repetition in event.repetition.as_ref() {
+ for date in repetition.between(event.date, start, end) {
+ res.entry(date).or_insert(vec![]).push(event.clone())
+ }
+ }
+ }
+
+ res
+}
+
pub fn pprint_time(t: NaiveTime) -> String {
if t.minute() == 0 {
format!("{}h", t.hour())
diff --git a/src/model/repetition.rs b/src/model/repetition.rs
index 80387d9..ceb903b 100644
--- a/src/model/repetition.rs
+++ b/src/model/repetition.rs
@@ -1,21 +1,19 @@
-use chrono::Weekday;
+use chrono::{Datelike, Duration, NaiveDate, Weekday};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Repetition {
- Daily { frequency: u8 },
- Monthly { frequency: MonthFrequency },
+ Daily { period: u8 },
+ Monthly { day: DayOfMonth },
Yearly,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
-pub enum MonthFrequency {
+pub enum DayOfMonth {
Day { day: u8 },
- FirstDay { day: Weekday },
+ Weekday { weekday: Weekday },
}
-// Validation
-
pub fn validate_day(str: &str) -> Option<u8> {
let n = str.parse::<u8>().ok()?;
if n >= 1 && n <= 31 {
@@ -24,3 +22,143 @@ pub fn validate_day(str: &str) -> Option<u8> {
None
}
}
+
+impl Repetition {
+ pub fn between(&self, event: NaiveDate, start: NaiveDate, end: NaiveDate) -> Vec<NaiveDate> {
+ let repeat = |mut date, next: Box<dyn Fn(NaiveDate) -> NaiveDate>| {
+ let mut repetitions = vec![];
+ while date <= end {
+ if date >= event && date >= start {
+ repetitions.push(date)
+ }
+ date = next(date)
+ }
+ repetitions
+ };
+
+ match self {
+ Repetition::Daily { period } => {
+ let n = start.signed_duration_since(event).num_days() % (*period as i64);
+ let duration = Duration::days(*period as i64);
+ repeat(start - Duration::days(n), Box::new(|d| d + duration))
+ }
+ Repetition::Monthly {
+ day: DayOfMonth::Day { day },
+ } => match start.with_day(*day as u32) {
+ Some(first_repetition) => repeat(first_repetition, Box::new(next_month)),
+ None => vec![],
+ },
+ Repetition::Monthly {
+ day: DayOfMonth::Weekday { weekday },
+ } => repeat(
+ first_weekday_of_month(start, *weekday),
+ Box::new(|d| first_weekday_of_month(next_month(d), *weekday)),
+ ),
+ Repetition::Yearly => repeat(
+ NaiveDate::from_ymd(start.year(), event.month(), event.day()),
+ Box::new(|d| NaiveDate::from_ymd(d.year() + 1, d.month(), d.day())),
+ ),
+ }
+ }
+}
+
+fn first_weekday_of_month(date: NaiveDate, weekday: Weekday) -> NaiveDate {
+ NaiveDate::from_weekday_of_month(date.year(), date.month(), weekday, 1)
+}
+
+fn next_month(date: NaiveDate) -> NaiveDate {
+ if date.month() == 12 {
+ NaiveDate::from_ymd(date.year() + 1, 1, date.day())
+ } else {
+ NaiveDate::from_ymd(date.year(), date.month() + 1, date.day())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn every_day_event_before() {
+ let repetition = Repetition::Daily { period: 1 };
+ assert_eq!(
+ repetition.between(d(2022, 6, 1), d(2022, 7, 1), d(2022, 8, 31)),
+ d(2022, 7, 1)
+ .iter_days()
+ .take(62)
+ .collect::<Vec<NaiveDate>>()
+ )
+ }
+
+ #[test]
+ fn every_day_event_between() {
+ let repetition = Repetition::Daily { period: 1 };
+ assert_eq!(
+ repetition.between(d(2022, 8, 10), d(2022, 7, 1), d(2022, 8, 31)),
+ d(2022, 8, 10)
+ .iter_days()
+ .take(22)
+ .collect::<Vec<NaiveDate>>()
+ )
+ }
+
+ #[test]
+ fn every_day_event_after() {
+ let repetition = Repetition::Daily { period: 1 };
+ assert!(repetition
+ .between(d(2022, 9, 1), d(2022, 7, 1), d(2022, 8, 31))
+ .is_empty())
+ }
+
+ #[test]
+ fn every_three_days() {
+ let repetition = Repetition::Daily { period: 3 };
+ assert_eq!(
+ repetition.between(d(2022, 2, 16), d(2022, 2, 21), d(2022, 3, 6)),
+ vec!(
+ d(2022, 2, 22),
+ d(2022, 2, 25),
+ d(2022, 2, 28),
+ d(2022, 3, 3),
+ d(2022, 3, 6)
+ )
+ )
+ }
+
+ #[test]
+ fn day_of_month() {
+ let repetition = Repetition::Monthly {
+ day: DayOfMonth::Day { day: 8 },
+ };
+ assert_eq!(
+ repetition.between(d(2022, 2, 7), d(2022, 1, 1), d(2022, 4, 7)),
+ vec!(d(2022, 2, 8), d(2022, 3, 8))
+ )
+ }
+
+ #[test]
+ fn weekday_of_month() {
+ let repetition = Repetition::Monthly {
+ day: DayOfMonth::Weekday {
+ weekday: Weekday::Tue,
+ },
+ };
+ assert_eq!(
+ repetition.between(d(2022, 1, 5), d(2022, 1, 1), d(2022, 4, 4)),
+ vec!(d(2022, 2, 1), d(2022, 3, 1))
+ )
+ }
+
+ #[test]
+ fn yearly() {
+ let repetition = Repetition::Yearly;
+ assert_eq!(
+ repetition.between(d(2020, 5, 5), d(2018, 1, 1), d(2022, 5, 5)),
+ vec!(d(2020, 5, 5), d(2021, 5, 5), d(2022, 5, 5))
+ )
+ }
+
+ fn d(y: i32, m: u32, d: u32) -> NaiveDate {
+ NaiveDate::from_ymd(y, m, d)
+ }
+}