yaml2ical/yaml2ical/recurrence.py
rbowen 4c4ae58575 Report which week a meeting occurs.
Change-Id: I1afcb7e657dd898279aa80c325a3adbfa5f40248
2021-07-15 16:41:52 +00:00

274 lines
9.0 KiB
Python

# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import abc
import calendar
import datetime
WEEKDAYS = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3,
'Friday': 4, 'Saturday': 5, 'Sunday': 6}
class _Recurrence(object, metaclass=abc.ABCMeta):
@abc.abstractmethod
def next_occurence(self, current_date_time, day):
"""Return the date of the next meeting.
:param ref_date: datetime object of meeting
:param day: weekday the meeting is held on
:returns: datetime object of the next meeting time
"""
@abc.abstractmethod
def rrule(self):
"Return dict with data for building ICS recurrence rule."
@abc.abstractmethod
def __str__(self):
"Return string representation of the recurrence rule"
@property
def day_specifier(self):
"""Return string prefix for day.
For example, monthly recurring events may return 'first' to
indicate the first instance of a particular day of the month
(e.g., first Thursday).
"""
# NOTE(dhellmann): This is not an abstract property because
# most of the subclasses will use this concrete
# implementation.
return ''
class WeeklyRecurrence(_Recurrence):
"""Meetings occuring every week."""
def __init__(self):
pass
def next_occurence(self, current_date_time, day):
"""Return the date of the next meeting.
: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'}
def __str__(self):
return "Weekly"
class BiWeeklyRecurrence(_Recurrence):
"""Meetings occuring on alternate weeks.
Can be either on odd weeks or on even weeks
"""
def __init__(self, style='even'):
self.style = style
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 __str__(self):
return "Every two weeks (on %s weeks)" % self.style
class QuadWeeklyRecurrence(_Recurrence):
"""Meetings occuring every 4 weeks.
A week number can be supplied to offset meetings
"""
def __init__(self, week=0):
self.week = week
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] % 4 == self.week:
return nextweek_day
# If week doesn't match rule, skip one week
return self.next_occurence(nextweek_day + datetime.timedelta(7), day)
def rrule(self):
return {'freq': 'weekly', 'interval': 4}
def __str__(self):
return (
"Every four weeks on week %d of the four week rotation"
% self.week)
class AdhocRecurrence(_Recurrence):
"""Meetings occuring as needed.
Effectively this is a noop recurrance as next_occurance is always None
"""
def __init__(self):
pass
def next_occurence(self, current_date, day):
"""Calculate the next adhoc meeting.
:param current_date: the current date
:param day: scheduled day of the meeting
:returns: datetime object of next meeting
"""
return None
def rrule(self):
return {'freq': 'adhoc', 'interval': 0}
def __str__(self):
return "Occurs as needed, no fixed schedule."
class MonthlyRecurrence(_Recurrence):
"""Meetings occuring every month."""
def __init__(self, week, day):
self._week = week
self._day = day
def next_occurence(self, current_date_time, day):
"""Return the date of the next meeting.
:param current_date_time: datetime object of meeting
:param day: weekday the meeting is held on
:returns: datetime object of the next meeting time
"""
weekday = WEEKDAYS[day]
month = current_date_time.month + 1
year = current_date_time.year
if current_date_time.month == 12:
month = 1
year = year + 1
next_month_dates = calendar.monthcalendar(year, month)
# We can't simply index into the dates for the next month
# because we don't know that the first week is full of days
# that actually appear in that month. Therefore we loop
# through them counting down until we've skipped enough weeks.
skip_weeks = self._week - 1
for week in next_month_dates:
day = week[weekday]
# Dates in the week that fall in other months
# are 0 so we want to skip counting those weeks.
if not day:
continue
# If we have skipped all of the weeks we need to,
# we have the day.
if not skip_weeks:
return datetime.datetime(
year, month, day,
current_date_time.hour, current_date_time.minute,
current_date_time.second, current_date_time.microsecond,
)
skip_weeks -= 1
raise ValueError(
'Could not compute week {} of next month for {}'.format(
self._week, current_date_time)
)
def rrule(self):
return {
'freq': 'monthly',
'byday': '{}{}'.format(self._week, self._day[:2].upper()),
}
_ORDINALS = [
'first',
'second',
'third',
'fourth',
'fifth',
]
@property
def day_specifier(self):
return 'the {}'.format(self._ORDINALS[self._week - 1])
def __str__(self):
return "Monthly, in the " + format(
self._ORDINALS[self._week - 1]) + ' week,'
supported_recurrences = {
'weekly': WeeklyRecurrence(),
'biweekly-odd': BiWeeklyRecurrence(style='odd'),
'biweekly-even': BiWeeklyRecurrence(),
'quadweekly': QuadWeeklyRecurrence(week=0),
'quadweekly-week-1': QuadWeeklyRecurrence(week=1),
'quadweekly-week-2': QuadWeeklyRecurrence(week=2),
'quadweekly-week-3': QuadWeeklyRecurrence(week=3),
'quadweekly-alternate': QuadWeeklyRecurrence(week=2),
'adhoc': AdhocRecurrence(),
'first-monday': MonthlyRecurrence(week=1, day='Monday'),
'first-tuesday': MonthlyRecurrence(week=1, day='Tuesday'),
'first-wednesday': MonthlyRecurrence(week=1, day='Wednesday'),
'first-thursday': MonthlyRecurrence(week=1, day='Thursday'),
'first-friday': MonthlyRecurrence(week=1, day='Friday'),
'second-monday': MonthlyRecurrence(week=2, day='Monday'),
'second-tuesday': MonthlyRecurrence(week=2, day='Tuesday'),
'second-wednesday': MonthlyRecurrence(week=2, day='Wednesday'),
'second-thursday': MonthlyRecurrence(week=2, day='Thursday'),
'second-friday': MonthlyRecurrence(week=2, day='Friday'),
'third-monday': MonthlyRecurrence(week=3, day='Monday'),
'third-tuesday': MonthlyRecurrence(week=3, day='Tuesday'),
'third-wednesday': MonthlyRecurrence(week=3, day='Wednesday'),
'third-thursday': MonthlyRecurrence(week=3, day='Thursday'),
'third-friday': MonthlyRecurrence(week=3, day='Friday'),
'fourth-monday': MonthlyRecurrence(week=4, day='Monday'),
'fourth-tuesday': MonthlyRecurrence(week=4, day='Tuesday'),
'fourth-wednesday': MonthlyRecurrence(week=4, day='Wednesday'),
'fourth-thursday': MonthlyRecurrence(week=4, day='Thursday'),
'fourth-friday': MonthlyRecurrence(week=4, day='Friday'),
}