Skip to content

Commit 94abd28

Browse files
committed
Implement workday recurrence
1 parent 45d20bf commit 94abd28

File tree

5 files changed

+101
-9
lines changed

5 files changed

+101
-9
lines changed

README.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,9 @@ npm run tauri build --release
7777

7878
The built binary & installers will be created in `src-tauri/target/release`.
7979

80-
## TODO
81-
82-
- Calendar units for recurrence
83-
8480
## Future Releases
8581

86-
- GUI paging
82+
- GUI paging to support displaying more than 30 upcoming reminders
8783
- Update paging so that all generated instances are not held in memory and do not grow with each new page
8884
- Only hold the current page and check if it's needed to regenerate upon every request for a page of tasks
8985
- MacOS release

src-tauri/src/datetime.rs

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use chrono::{DateTime, Datelike, TimeZone, Timelike, Utc};
1+
use chrono::{DateTime, Datelike, Duration, TimeZone, Timelike, Utc, Weekday};
22

33
pub fn add_months(dt: DateTime<Utc>, months: u32) -> DateTime<Utc> {
44
let mut year = dt.year();
@@ -23,3 +23,83 @@ pub fn days_in_month(year: i32, month: u32) -> u32 {
2323
.unwrap();
2424
(next_first_day - first_day).num_days() as u32
2525
}
26+
27+
pub fn add_workdays(mut date: DateTime<Utc>, mut n: i64) -> DateTime<Utc> {
28+
while n > 0 {
29+
date = date + Duration::days(1);
30+
match date.weekday() {
31+
Weekday::Sat | Weekday::Sun => continue,
32+
_ => n -= 1,
33+
}
34+
}
35+
date
36+
}
37+
38+
#[cfg(test)]
39+
mod tests {
40+
use super::*;
41+
use chrono::TimeZone;
42+
43+
#[test]
44+
fn test_days_in_month() {
45+
assert_eq!(days_in_month(2024, 2), 29); // Leap year
46+
assert_eq!(days_in_month(2023, 2), 28);
47+
assert_eq!(days_in_month(2023, 1), 31);
48+
assert_eq!(days_in_month(2023, 4), 30);
49+
}
50+
51+
#[test]
52+
fn test_add_months_basic() {
53+
let date = Utc.with_ymd_and_hms(2023, 1, 15, 12, 0, 0).unwrap();
54+
let result = add_months(date, 1);
55+
assert_eq!(result, Utc.with_ymd_and_hms(2023, 2, 15, 12, 0, 0).unwrap());
56+
}
57+
58+
#[test]
59+
fn test_add_months_wrap_year() {
60+
let date = Utc.with_ymd_and_hms(2023, 11, 30, 9, 0, 0).unwrap();
61+
let result = add_months(date, 2);
62+
assert_eq!(result, Utc.with_ymd_and_hms(2024, 1, 30, 9, 0, 0).unwrap());
63+
}
64+
65+
#[test]
66+
fn test_add_months_day_adjustment() {
67+
let date = Utc.with_ymd_and_hms(2023, 1, 31, 8, 0, 0).unwrap();
68+
let result = add_months(date, 1);
69+
// February 2023 only has 28 days
70+
assert_eq!(result, Utc.with_ymd_and_hms(2023, 2, 28, 8, 0, 0).unwrap());
71+
}
72+
73+
#[test]
74+
fn test_add_workdays_simple() {
75+
let monday = Utc.with_ymd_and_hms(2023, 7, 24, 10, 0, 0).unwrap(); // Monday
76+
let result = add_workdays(monday, 5);
77+
assert_eq!(result.weekday(), Weekday::Mon);
78+
assert_eq!(
79+
result.date_naive(),
80+
monday.date_naive() + chrono::Days::new(7)
81+
);
82+
}
83+
84+
#[test]
85+
fn test_add_workdays_over_weekend() {
86+
let friday = Utc.with_ymd_and_hms(2023, 7, 21, 10, 0, 0).unwrap(); // Friday
87+
let result = add_workdays(friday, 1);
88+
assert_eq!(result.weekday(), Weekday::Mon);
89+
assert_eq!(
90+
result.date_naive(),
91+
friday.date_naive() + chrono::Days::new(3)
92+
);
93+
}
94+
95+
#[test]
96+
fn test_add_workdays_multiple_weeks() {
97+
let tuesday = Utc.with_ymd_and_hms(2023, 7, 18, 10, 0, 0).unwrap(); // Tuesday
98+
let result = add_workdays(tuesday, 10);
99+
assert_eq!(result.weekday(), Weekday::Tue);
100+
assert_eq!(
101+
result.date_naive(),
102+
tuesday.date_naive() + chrono::Days::new(14)
103+
);
104+
}
105+
}

src-tauri/src/tasks.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::fs;
88
use uuid::Uuid;
99

1010
use crate::config;
11-
use crate::datetime::add_months;
11+
use crate::datetime::{add_months, add_workdays};
1212

1313
#[derive(Serialize, Deserialize, Clone, Debug)]
1414
pub enum RecurrenceInterval {
@@ -18,6 +18,7 @@ pub enum RecurrenceInterval {
1818
Weekly(i64),
1919
Monthly(i64),
2020
Yearly(i64),
21+
Workdays(i64),
2122
}
2223

2324
#[derive(Serialize, Deserialize, Clone, Debug)]
@@ -183,41 +184,54 @@ impl TaskReminder {
183184
d.start + Duration::minutes(i * number)
184185
}
185186
}
187+
186188
RecurrenceInterval::Hourly(number) => {
187189
if let Some(last) = last_recurrence {
188190
*last + Duration::hours((i + 1) * number)
189191
} else {
190192
d.start + Duration::hours(i * number)
191193
}
192194
}
195+
193196
RecurrenceInterval::Daily(number) => {
194197
if let Some(last) = last_recurrence {
195198
*last + Duration::days((i + 1) * number)
196199
} else {
197200
d.start + Duration::days(i * number)
198201
}
199202
}
203+
200204
RecurrenceInterval::Weekly(number) => {
201205
if let Some(last) = last_recurrence {
202206
*last + Duration::weeks((i + 1) * number)
203207
} else {
204208
d.start + Duration::weeks(i * number)
205209
}
206210
}
211+
207212
RecurrenceInterval::Monthly(number) => {
208213
let mut t = last_recurrence.unwrap_or(d.start);
209214
for _ in 0..=(i as u32) {
210215
t = add_months(t, *number as u32);
211216
}
212217
t
213218
}
219+
214220
RecurrenceInterval::Yearly(number) => {
215221
let mut t = last_recurrence.unwrap_or(d.start);
216222
for _ in 0..=(i as u32) {
217223
t = add_months(t, 12 * (*number as u32));
218224
}
219225
t
220226
}
227+
228+
RecurrenceInterval::Workdays(number) => {
229+
let mut t = last_recurrence.unwrap_or(d.start);
230+
for _ in 0..(i + 1) {
231+
t = add_workdays(t, *number);
232+
}
233+
t
234+
}
221235
},
222236
None => d.start,
223237
};

src/components/TaskForm.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,15 +209,16 @@ import {
209209
import { pluralizeUnit, timeUnitToMinutesMap } from '../helpers/datetime'
210210
import PIcon from './PIcon.vue'
211211
212-
type IntervalKey = 'Minutes' | 'Hourly' | 'Daily' | 'Weekly' | 'Monthly' | 'Yearly'
212+
type IntervalKey = 'Minutes' | 'Hourly' | 'Daily' | 'Weekly' | 'Monthly' | 'Yearly' | 'Workdays'
213213
214214
const intervalMap = {
215215
minute: 'Minutes',
216216
hour: 'Hourly',
217217
day: 'Daily',
218218
week: 'Weekly',
219219
month: 'Monthly',
220-
year: 'Yearly'
220+
year: 'Yearly',
221+
workday: 'Workdays'
221222
} as const satisfies Record<string, IntervalKey>
222223
223224
const props = withDefaults(

src/composables/useTasks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export type Interval =
88
| { Minutes: number }
99
| { Hourly: number }
1010
| { Daily: number }
11+
| { Workdays: number }
1112
| { Weekly: number }
1213
| { Monthly: number }
1314
| { Yearly: number }

0 commit comments

Comments
 (0)