Abstract recurrence styles as classes

Abstract recurrence styles as classes in recurrence.py, to make it
easier to add other recurrence styles.

Fix "biweekly" recurrences so that they use ISO week numbers (instead of
week number in the month, which prevents alternating meetings).

Change-Id: Ie786a545276c1aaf829daa93bc93245642f308a6
This commit is contained in:
Thierry Carrez 2015-02-23 15:41:46 +01:00
parent 3434d6294b
commit d6963905f1
4 changed files with 76 additions and 76 deletions

View File

@ -17,7 +17,6 @@ import os
import pytz
from yaml2ical import meeting
from yaml2ical import recurrence
class Yaml2IcalCalendar(icalendar.Calendar):
@ -53,15 +52,7 @@ class Yaml2IcalCalendar(icalendar.Calendar):
# get starting date
start_date = datetime.datetime.utcnow()
if sch.freq.startswith('biweekly'):
meet_on_even = sch.freq.endswith('even')
next_meeting = recurrence.next_biweekly_meeting(
start_date,
sch.day,
meet_on_even=meet_on_even)
else:
next_meeting = recurrence.next_weekday(start_date, sch.day)
next_meeting = sch.recurrence.next_occurence(start_date, sch.day)
next_meeting_date = datetime.datetime(next_meeting.year,
next_meeting.month,
next_meeting.day,
@ -71,24 +62,7 @@ class Yaml2IcalCalendar(icalendar.Calendar):
event.add('dtstart', next_meeting_date)
# add recurrence rule
if sch.freq.startswith('biweekly'):
cadence = ()
# NOTE(lbragstad): Setting the `cadence` for the schedule
# will allow for alternating meetings. Typically there are
# only 4 weeks in a month but adding `5` and `6` allow for
# cases where there are 5 meetings in a month, which would
# otherwise be unsupported if only setting `cadence` to
# either (1, 3) or (2, 4).
if sch.freq == 'biweekly-odd':
cadence = (1, 3, 5)
elif sch.freq == 'biweekly-even':
cadence = (2, 4, 6)
rule_dict = {'freq': 'monthly',
'byday': sch.day[0:2],
'bysetpos': cadence}
event.add('rrule', rule_dict)
else:
event.add('rrule', {'freq': sch.freq})
event.add('rrule', sch.recurrence.rrule())
# add meeting length
# TODO(jotan): determine duration to use for OpenStack meetings

View File

@ -14,6 +14,8 @@ import datetime
import os
import yaml
from yaml2ical.recurrence import supported_recurrences
class Schedule(object):
"""A meeting schedule."""
@ -26,7 +28,7 @@ class Schedule(object):
self.time = datetime.datetime.strptime(sched_yaml['time'], '%H%M')
self.day = sched_yaml['day']
self.irc = sched_yaml['irc']
self.freq = sched_yaml['frequency']
self.recurrence = supported_recurrences[sched_yaml['frequency']]
def __eq__(self, other):
#TODO(ttx): This is a bit overzealous (it will report as conflict

View File

@ -17,48 +17,63 @@ WEEKDAYS = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3,
'Friday': 4, 'Saturday': 5, 'Sunday': 6}
def next_weekday(ref_date, day):
"""Return the date of the next meeting.
class WeeklyRecurrence(object):
"""Meetings occuring every week."""
def __init__(self):
pass
:param ref_date: datetime object of meeting
:param day: weekday the meeting is held on
def next_occurence(self, current_date_time, day):
"""Return the date of the next meeting.
:returns: datetime object of the next meeting time
:param ref_date: datetime object of meeting
:param day: weekday the meeting is held on
:returns: datetime object of the next meeting time
"""
weekday = WEEKDAYS[day]
days_ahead = weekday - current_date_time.weekday()
if days_ahead <= 0: # target day already happened this week
days_ahead += 7
return current_date_time + datetime.timedelta(days_ahead)
def rrule(self):
return {'freq': 'weekly'}
class BiWeeklyRecurrence(object):
"""Meetings occuring on alternate weeks.
Can be either on odd weeks or on even weeks
"""
def __init__(self, style='even'):
self.style = style
weekday = WEEKDAYS[day]
days_ahead = weekday - ref_date.weekday()
if days_ahead <= 0: # target day already happened this week
days_ahead += 7
return ref_date + datetime.timedelta(days_ahead)
def next_occurence(self, current_date, day):
"""Calculate the next biweekly meeting.
:param current_date: the current date
:param day: scheduled day of the meeting
:returns: datetime object of next meeting
"""
nextweek_day = WeeklyRecurrence().next_occurence(current_date, day)
if nextweek_day.isocalendar()[1] % 2:
## ISO week is odd
if self.style == 'odd':
return nextweek_day
else:
## ISO week is even
if self.style == 'even':
return nextweek_day
# If week doesn't match rule, skip one week
return nextweek_day + datetime.timedelta(7)
def rrule(self):
return {'freq': 'weekly', 'interval': 2}
def next_biweekly_meeting(current_date_time, day, meet_on_even=False):
"""Calculate the next biweekly meeting.
:param current_date_time: the current datetime object
:param day: scheduled day of the meeting
:param meet_on_even: True if meeting on even weeks and False if meeting
on odd weeks
:returns: datetime object of next meeting
"""
weekday = WEEKDAYS[day]
first_day_of_mo = current_date_time.replace(day=1)
day_of_week = first_day_of_mo.strftime("%w")
adjustment = (8 - int(day_of_week)) % (7 - weekday)
if meet_on_even:
adjustment += 7
next_meeting = first_day_of_mo + datetime.timedelta(adjustment)
if current_date_time > next_meeting:
next_meeting = next_meeting + datetime.timedelta(14)
if current_date_time > next_meeting:
current_date_time = current_date_time.replace(
month=current_date_time.month + 1, day=1)
first_wday_next_mo = next_weekday(current_date_time, day)
if meet_on_even:
next_meeting = first_wday_next_mo + datetime.timedelta(7)
else:
next_meeting = first_wday_next_mo
return next_meeting
supported_recurrences = {
'weekly': WeeklyRecurrence(),
'biweekly-odd': BiWeeklyRecurrence(style='odd'),
'biweekly-even': BiWeeklyRecurrence(),
}

View File

@ -18,13 +18,22 @@ from yaml2ical import recurrence
class RecurrenceTestCase(unittest.TestCase):
def test_calculate_next_biweekly_meeting_meet_on_even(self):
def next_meeting(self, to_test):
test_time = datetime.datetime(2014, 10, 5, 2, 47, 28, 832666)
test_weekday = 'Wednesday'
meet_on_even = True
next_meeting = recurrence.next_biweekly_meeting(
test_time,
test_weekday,
meet_on_even=meet_on_even)
expected_meeting = datetime.datetime(2014, 10, 8, 2, 47, 28, 832666)
self.assertEqual(expected_meeting, next_meeting)
return to_test.next_occurence(test_time, test_weekday)
def test_next_weekly(self):
self.assertEqual(
datetime.datetime(2014, 10, 8, 2, 47, 28, 832666),
self.next_meeting(recurrence.WeeklyRecurrence()))
def test_next_biweekly_odd(self):
self.assertEqual(
datetime.datetime(2014, 10, 8, 2, 47, 28, 832666),
self.next_meeting(recurrence.BiWeeklyRecurrence(style='odd')))
def test_next_biweekly_even(self):
self.assertEqual(
datetime.datetime(2014, 10, 15, 2, 47, 28, 832666),
self.next_meeting(recurrence.BiWeeklyRecurrence(style='even')))