11/// Utility class for calculating next recurrence dates.
2+ ///
3+ /// Supports all standard Taskwarrior recurrence keywords plus arbitrary
4+ /// Nd / Nw / Nm / Ny patterns.
25class RecurrenceEngine {
36 /// Given [oldDate] and a [recur] pattern string, returns the next occurrence date.
4- /// Supports: daily/1d, weekly/1w, monthly/1m, yearly/1y.
57 static DateTime ? calculateNextDate (DateTime oldDate, String recur) {
68 final r = recur.toLowerCase ().trim ();
79 switch (r) {
10+ // ── Daily ────────────────────────────────────────────────────────────────
811 case 'daily' :
912 case '1d' :
1013 return oldDate.add (const Duration (days: 1 ));
14+
15+ // ── Weekdays (Mon–Fri only) ───────────────────────────────────────────
16+ case 'weekdays' :
17+ return _nextWeekday (oldDate);
18+
19+ // ── Weekly ───────────────────────────────────────────────────────────
1120 case 'weekly' :
1221 case '1w' :
22+ case 'sennight' :
1323 return oldDate.add (const Duration (days: 7 ));
24+
25+ // ── Biweekly / Fortnight ─────────────────────────────────────────────
26+ case 'biweekly' :
27+ case 'fortnight' :
28+ case '2w' :
29+ return oldDate.add (const Duration (days: 14 ));
30+
31+ // ── Monthly ──────────────────────────────────────────────────────────
1432 case 'monthly' :
1533 case '1m' :
16- return DateTime (
17- oldDate.year,
18- oldDate.month + 1 ,
19- oldDate.day,
20- oldDate.hour,
21- oldDate.minute,
22- oldDate.second,
23- );
34+ return _addMonths (oldDate, 1 );
35+
36+ // ── Bimonthly ────────────────────────────────────────────────────────
37+ case 'bimonthly' :
38+ case '2m' :
39+ return _addMonths (oldDate, 2 );
40+
41+ // ── Quarterly ────────────────────────────────────────────────────────
42+ case 'quarterly' :
43+ case '3m' :
44+ return _addMonths (oldDate, 3 );
45+
46+ // ── Semi-annual ──────────────────────────────────────────────────────
47+ case 'semiannual' :
48+ case '6m' :
49+ return _addMonths (oldDate, 6 );
50+
51+ // ── Yearly / Annual ──────────────────────────────────────────────────
2452 case 'yearly' :
53+ case 'annual' :
2554 case '1y' :
26- return DateTime (
27- oldDate.year + 1 ,
28- oldDate.month,
29- oldDate.day,
30- oldDate.hour,
31- oldDate.minute,
32- oldDate.second,
33- );
55+ return _addYears (oldDate, 1 );
56+
3457 default :
35- // Try to parse patterns like "2d", "3w", "6m", "2y"
58+ // Try to parse generic patterns like "2d", "3w", "6m", "2y"
3659 final match = RegExp (r'^(\d+)([dwmy])$' ).firstMatch (r);
3760 if (match != null ) {
3861 final count = int .parse (match.group (1 )! );
@@ -43,26 +66,44 @@ class RecurrenceEngine {
4366 case 'w' :
4467 return oldDate.add (Duration (days: count * 7 ));
4568 case 'm' :
46- return DateTime (
47- oldDate.year,
48- oldDate.month + count,
49- oldDate.day,
50- oldDate.hour,
51- oldDate.minute,
52- oldDate.second,
53- );
69+ return _addMonths (oldDate, count);
5470 case 'y' :
55- return DateTime (
56- oldDate.year + count,
57- oldDate.month,
58- oldDate.day,
59- oldDate.hour,
60- oldDate.minute,
61- oldDate.second,
62- );
71+ return _addYears (oldDate, count);
6372 }
6473 }
6574 return null ;
6675 }
6776 }
77+
78+ // ── Private helpers ────────────────────────────────────────────────────────
79+
80+ /// Advance [date] by [months] , clamping to valid day-of-month.
81+ static DateTime _addMonths (DateTime date, int months) {
82+ int newMonth = date.month + months;
83+ int newYear = date.year + (newMonth - 1 ) ~ / 12 ;
84+ newMonth = ((newMonth - 1 ) % 12 ) + 1 ;
85+ final lastDay = DateTime (newYear, newMonth + 1 , 0 ).day;
86+ final day = date.day.clamp (1 , lastDay);
87+ return DateTime (
88+ newYear, newMonth, day, date.hour, date.minute, date.second);
89+ }
90+
91+ /// Advance [date] by [years] , clamping to valid day-of-month (Feb 29 → Feb 28).
92+ static DateTime _addYears (DateTime date, int years) {
93+ final newYear = date.year + years;
94+ final lastDay = DateTime (newYear, date.month + 1 , 0 ).day;
95+ final day = date.day.clamp (1 , lastDay);
96+ return DateTime (
97+ newYear, date.month, day, date.hour, date.minute, date.second);
98+ }
99+
100+ /// Return the next weekday (Mon–Fri) strictly after [date] .
101+ static DateTime _nextWeekday (DateTime date) {
102+ DateTime next = date.add (const Duration (days: 1 ));
103+ while (
104+ next.weekday == DateTime .saturday || next.weekday == DateTime .sunday) {
105+ next = next.add (const Duration (days: 1 ));
106+ }
107+ return next;
108+ }
68109}
0 commit comments