diff --git a/icalevents/icalparser.py b/icalevents/icalparser.py index 95eb8c9..f30472e 100644 --- a/icalevents/icalparser.py +++ b/icalevents/icalparser.py @@ -4,6 +4,7 @@ Parse iCal data to Events. # for UID generation from random import randint from datetime import datetime, timedelta, date +from dateutil import relativedelta from icalendar import Calendar from pytz import utc @@ -297,20 +298,29 @@ def create_recurring_events(start, end, component): unfolded.append(current) else: break - if freq == 'MONTHLY': + elif freq == 'MONTHLY': + by_day = rule.get('BYDAY') + while True: - current = current.copy_to(next_month_at(current.start)) + if by_day: + next_date = next_month_byday_delta(current.start, by_day[0]) + current = current.copy_to(next_date) + else: + current = current.copy_to(next_month_at(current.start)) if current.start < limit: unfolded.append(current) else: break - else: - if freq == 'DAILY': - delta = timedelta(days=1) - elif freq == 'WEEKLY': - delta = timedelta(days=7) - else: - return + elif freq == 'DAILY': + delta = timedelta(days=1) + while True: + current = current.copy_to(current.start + delta) + if current.start < limit: + unfolded.append(current) + else: + break + elif freq == 'WEEKLY': + delta = timedelta(days=7) by_day = rule.get('BYDAY') if by_day: @@ -326,6 +336,8 @@ def create_recurring_events(start, end, component): unfolded.append(current) else: break + else: + return return in_range(unfolded, start, end) @@ -361,3 +373,26 @@ def generate_day_deltas_by_weekday(by_day): adjusted_deltas = day_deltas[1:] + [first_hop_count] return dict(zip(selected_weekday_numbers, adjusted_deltas)) + + +def next_month_byday_delta(start_date, by_day): + """ + Get the next event date when a MONTHLY rule contains a BYDAY clause, + e.g. 3SA = "Next third Saturday" + """ + + weekdays = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'] + + number = int(by_day[0]) + weekday = by_day[1:] + + if weekday not in weekdays: + raise ValueError('Invalid weekday: {}'.format(weekday)) + + weekday = weekdays.index(weekday) + weekday_func = relativedelta.weekday(weekday) + + delta = relativedelta.relativedelta(day=1, months=+1, + weekday=weekday_func(number)) + + return start_date + delta diff --git a/test.py b/test.py old mode 100644 new mode 100755 index 337c9f4..03cf293 --- a/test.py +++ b/test.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 import unittest from test.test_icaldownload import * diff --git a/test/test_icalparser.py b/test/test_icalparser.py index bb0bc4f..3108e17 100644 --- a/test/test_icalparser.py +++ b/test/test_icalparser.py @@ -114,3 +114,15 @@ class ICalParserTests(unittest.TestCase): result = icalevents.icalparser.generate_day_deltas_by_weekday(by_day) self.assertEqual(7, result[0], 'Mon to Mon') + + def test_next_month_byday_delta(self): + dt = date(2018, 9, 15) + + result = icalevents.icalparser.next_month_byday_delta(dt, "3SA") + self.assertEqual(date(2018, 10, 20), result, '3rd Saturday next month') + + result = icalevents.icalparser.next_month_byday_delta(dt, "2TU") + self.assertEqual(date(2018, 10, 9), result, '2nd Tuesday next month') + + with self.assertRaises(ValueError, msg='Invalid weekday'): + icalevents.icalparser.next_month_byday_delta(dt, "1ZZ")