@@ -9,7 +9,17 @@ class _RRule {
99 final int interval;
1010 final int count;
1111 final DateTime ? until;
12- _RRule (this .repeatType, this .interval, this .count, this .until);
12+ final List <int > byWeekDays;
13+ final List <int > byMonthDays;
14+
15+ _RRule (
16+ this .repeatType,
17+ this .interval,
18+ this .count,
19+ this .until, {
20+ this .byWeekDays = const [],
21+ this .byMonthDays = const [],
22+ });
1323}
1424
1525class ICalConverter {
@@ -100,7 +110,7 @@ class ICalConverter {
100110 status: c.status,
101111 repeatType: rrule.repeatType,
102112 interval: rrule.interval,
103- variation: 0 ,
113+ variation: _variationFromRRule (rrule) ,
104114 count: rrule.count,
105115 until: rrule.until,
106116 );
@@ -197,6 +207,8 @@ class ICalConverter {
197207 int interval = 1 ;
198208 int count = 0 ;
199209 DateTime ? until;
210+ var byWeekDays = < int > [];
211+ var byMonthDays = < int > [];
200212
201213 final parts = value.split (';' );
202214 for (final part in parts) {
@@ -218,9 +230,70 @@ class ICalConverter {
218230 count = int .tryParse (v) ?? 0 ;
219231 } else if (k == 'UNTIL' ) {
220232 until = _parseDateTime (v);
233+ } else if (k == 'BYDAY' ) {
234+ byWeekDays = _parseByDay (v);
235+ } else if (k == 'BYMONTHDAY' ) {
236+ byMonthDays = _parseByMonthDay (v);
221237 }
222238 }
223- return _RRule (type, interval, count, until);
239+ return _RRule (
240+ type,
241+ interval,
242+ count,
243+ until,
244+ byWeekDays: byWeekDays,
245+ byMonthDays: byMonthDays,
246+ );
247+ }
248+
249+ int _variationFromRRule (_RRule rule) {
250+ switch (rule.repeatType) {
251+ case RepeatType .weekly:
252+ return RepeatingCalendarItem .encodeWeeklyWeekdays (rule.byWeekDays);
253+ case RepeatType .monthly:
254+ return RepeatingCalendarItem .encodeMonthlyMonthDays (rule.byMonthDays);
255+ case RepeatType .daily:
256+ case RepeatType .yearly:
257+ return 0 ;
258+ }
259+ }
260+
261+ List <int > _parseByDay (String value) {
262+ final weekdays = < int > [];
263+ for (final part in value.split (',' )) {
264+ final token = part.trim ().toUpperCase ();
265+ if (token.length < 2 ) continue ;
266+ final dayCode = token.substring (token.length - 2 );
267+ final weekday = _weekdayFromIcs (dayCode);
268+ if (weekday != null ) {
269+ weekdays.add (weekday);
270+ }
271+ }
272+ return weekdays;
273+ }
274+
275+ List <int > _parseByMonthDay (String value) {
276+ final days = < int > [];
277+ for (final part in value.split (',' )) {
278+ final day = int .tryParse (part.trim ());
279+ if (day != null && day >= 1 && day <= 31 ) {
280+ days.add (day);
281+ }
282+ }
283+ return days;
284+ }
285+
286+ int ? _weekdayFromIcs (String day) {
287+ return switch (day) {
288+ 'MO' => DateTime .monday,
289+ 'TU' => DateTime .tuesday,
290+ 'WE' => DateTime .wednesday,
291+ 'TH' => DateTime .thursday,
292+ 'FR' => DateTime .friday,
293+ 'SA' => DateTime .saturday,
294+ 'SU' => DateTime .sunday,
295+ _ => null ,
296+ };
224297 }
225298
226299 EventStatus _parseEventStatus (String value) {
@@ -254,12 +327,49 @@ class ICalConverter {
254327 if (item.location.isNotEmpty) 'LOCATION:${_escape (item .location )}' ,
255328 if (item.start != null ) 'DTSTART:${_formatDateTime (item .start !.toUtc ())}' ,
256329 if (item.end != null ) 'DTEND:${_formatDateTime (item .end !.toUtc ())}' ,
257- if (item is RepeatingCalendarItem )
258- 'RRULE:FREQ=${_formatRepeatType (item .repeatType )}${item .interval > 1 ? ';INTERVAL=${item .interval }' : '' }${item .count > 0 ? ';COUNT=${item .count }' : '' }${item .until != null ? ';UNTIL=${_formatDateTime (item .until !.toUtc ())}' : '' }' ,
330+ if (item is RepeatingCalendarItem ) _formatRRule (item),
259331 'STATUS:${_formatEventStatus (item .status )}' ,
260332 'END:VEVENT' ,
261333 ];
262334
335+ String _formatRRule (RepeatingCalendarItem item) {
336+ final parts = < String > ['FREQ=${_formatRepeatType (item .repeatType )}' ];
337+ if (item.interval > 1 ) {
338+ parts.add ('INTERVAL=${item .interval }' );
339+ }
340+ if (item.count > 0 ) {
341+ parts.add ('COUNT=${item .count }' );
342+ }
343+ if (item.until != null ) {
344+ parts.add ('UNTIL=${_formatDateTime (item .until !.toUtc ())}' );
345+ }
346+ if (item.repeatType == RepeatType .weekly) {
347+ final weekdays = item.weeklyVariationWeekdays;
348+ if (weekdays.isNotEmpty) {
349+ parts.add ('BYDAY=${weekdays .map (_weekdayToIcs ).join (',' )}' );
350+ }
351+ } else if (item.repeatType == RepeatType .monthly) {
352+ final monthDays = item.monthlyVariationMonthDays;
353+ if (monthDays.isNotEmpty) {
354+ parts.add ('BYMONTHDAY=${monthDays .join (',' )}' );
355+ }
356+ }
357+ return 'RRULE:${parts .join (';' )}' ;
358+ }
359+
360+ String _weekdayToIcs (int weekday) {
361+ return switch (weekday) {
362+ DateTime .monday => 'MO' ,
363+ DateTime .tuesday => 'TU' ,
364+ DateTime .wednesday => 'WE' ,
365+ DateTime .thursday => 'TH' ,
366+ DateTime .friday => 'FR' ,
367+ DateTime .saturday => 'SA' ,
368+ DateTime .sunday => 'SU' ,
369+ _ => 'MO' ,
370+ };
371+ }
372+
263373 String _formatDateTime (DateTime dateTime) =>
264374 "${dateTime .year }${dateTime .month .toString ().padLeft (2 , '0' )}${dateTime .day .toString ().padLeft (2 , '0' )}T${dateTime .hour .toString ().padLeft (2 , '0' )}${dateTime .minute .toString ().padLeft (2 , '0' )}00Z" ;
265375
0 commit comments