[CalendarServer-changes] [13177] PyCalendar/branches/CalendarServer-5.2/src/pycalendar
source_changes at macosforge.org
source_changes at macosforge.org
Mon Apr 7 09:00:00 PDT 2014
Revision: 13177
http://trac.calendarserver.org//changeset/13177
Author: cdaboo at apple.com
Date: 2014-04-07 09:00:00 -0700 (Mon, 07 Apr 2014)
Log Message:
-----------
Properly handle invalid dates during recurrence.
Modified Paths:
--------------
PyCalendar/branches/CalendarServer-5.2/src/pycalendar/datetime.py
PyCalendar/branches/CalendarServer-5.2/src/pycalendar/recurrence.py
PyCalendar/branches/CalendarServer-5.2/src/pycalendar/tests/test_recurrence.py
Modified: PyCalendar/branches/CalendarServer-5.2/src/pycalendar/datetime.py
===================================================================
--- PyCalendar/branches/CalendarServer-5.2/src/pycalendar/datetime.py 2014-04-07 15:59:12 UTC (rev 13176)
+++ PyCalendar/branches/CalendarServer-5.2/src/pycalendar/datetime.py 2014-04-07 16:00:00 UTC (rev 13177)
@@ -347,11 +347,19 @@
self.normalise()
- def setYearDay(self, day):
+ def setYearDay(self, day, allow_invalid=False):
# 1 .. 366 offset from start, or
# -1 .. -366 offset from end
- if day > 0:
+ if day == 366:
+ self.mMonth = 12
+ self.mDay = 31 if utils.isLeapYear(self.mYear) else 32
+
+ elif day == -366:
+ self.mMonth = 1 if utils.isLeapYear(self.mYear) else 1
+ self.mDay = 1 if utils.isLeapYear(self.mYear) else 0
+
+ elif day > 0:
# Offset current date to 1st January of current year
self.mMonth = 1
self.mDay = 1
@@ -359,26 +367,26 @@
# Increment day
self.mDay += day - 1
- # Normalise to get proper month/day values
- self.normalise()
elif day < 0:
# Offset current date to 1st January of next year
- self.mYear += 1
- self.mMonth = 1
- self.mDay = 1
+ self.mMonth = 12
+ self.mDay = 31
# Decrement day
- self.mDay += day
+ self.mDay += day + 1
+ if not allow_invalid:
# Normalise to get proper year/month/day values
self.normalise()
+ else:
+ self.changed()
def getYearDay(self):
return self.mDay + utils.daysUptoMonth(self.mMonth, self.mYear)
- def setMonthDay(self, day):
+ def setMonthDay(self, day, allow_invalid=False):
# 1 .. 31 offset from start, or
# -1 .. -31 offset from end
@@ -389,18 +397,18 @@
# Increment day
self.mDay += day - 1
- # Normalise to get proper month/day values
- self.normalise()
elif day < 0:
- # Offset current date to 1st of next month
- self.mMonth += 1
- self.mDay = 1
+ # Offset current date to last of month
+ self.mDay = utils.daysInMonth(self.mMonth, self.mYear)
# Decrement day
- self.mDay += day
+ self.mDay += day + 1
+ if not allow_invalid:
# Normalise to get proper year/month/day values
self.normalise()
+ else:
+ self.changed()
def isMonthDay(self, day):
@@ -518,7 +526,7 @@
self.normalise()
- def setDayOfWeekInMonth(self, offset, day):
+ def setDayOfWeekInMonth(self, offset, day, allow_invalid=False):
# Set to first day in month
self.mDay = 1
@@ -542,7 +550,10 @@
cycle += 7
self.mDay = days_in_month - cycle
- self.normalise()
+ if not allow_invalid:
+ self.normalise()
+ else:
+ self.changed()
def setNextDayOfWeek(self, start, day):
@@ -777,8 +788,9 @@
return PyCalendarDateTime(year=now_tm.tm_year, month=now_tm.tm_mon, day=now_tm.tm_mday, hours=now_tm.tm_hour, minutes=now_tm.tm_min, seconds=now_tm.tm_sec, tzid=tzid)
- def recur(self, freq, interval):
+ def recur(self, freq, interval, allow_invalid=False):
# Add appropriate interval
+ normalize = True
if freq == definitions.eRecurrence_SECONDLY:
self.mSeconds += interval
elif freq == definitions.eRecurrence_MINUTELY:
@@ -796,15 +808,32 @@
# or 1/2 May, or 31 March or what? We choose to find the next month with
# the same day number as the current one.
self.mMonth += interval
- self.normalise()
- while self.mDay > utils.daysInMonth(self.mMonth, self.mYear):
- self.mMonth += interval
+
+ # Normalise month
+ normalised_month = ((self.mMonth - 1) % 12) + 1
+ adjustment_year = (self.mMonth - 1) / 12
+ if (normalised_month - 1) < 0:
+ normalised_month += 12
+ adjustment_year -= 1
+ self.mMonth = normalised_month
+ self.mYear += adjustment_year
+
+ if not allow_invalid:
self.normalise()
+ while self.mDay > utils.daysInMonth(self.mMonth, self.mYear):
+ self.mMonth += interval
+ self.normalise()
+ normalize = False
elif freq == definitions.eRecurrence_YEARLY:
self.mYear += interval
+ if allow_invalid:
+ normalize = False
- # Normalise to standard date-time ranges
- self.normalise()
+ if normalize:
+ # Normalise to standard date-time ranges
+ self.normalise()
+ else:
+ self.changed()
def getLocaleDate(self, locale):
@@ -1095,6 +1124,20 @@
value.text = self.getXMLText()
+ def invalid(self):
+ """
+ Are any of the current fields invalid.
+ """
+
+ # Right now we only care about invalid days of the month (e.g. February 30th). In the
+ # future we may also want to look for invalid times during a DST transition.
+
+ if self.mDay <= 0 or self.mDay > utils.daysInMonth(self.mMonth, self.mYear):
+ return True
+
+ return False
+
+
def normalise(self):
# Normalise seconds
normalised_secs = self.mSeconds % 60
Modified: PyCalendar/branches/CalendarServer-5.2/src/pycalendar/recurrence.py
===================================================================
--- PyCalendar/branches/CalendarServer-5.2/src/pycalendar/recurrence.py 2014-04-07 15:59:12 UTC (rev 13176)
+++ PyCalendar/branches/CalendarServer-5.2/src/pycalendar/recurrence.py 2014-04-07 16:00:00 UTC (rev 13177)
@@ -803,6 +803,10 @@
def expand(self, start, range, items, float_offset=0):
+ # Have to normalize this to be very sure we are starting with a valid date, as otherwise
+ # we could end up looping forever when doing recurrence.
+ start.normalise()
+
# Must have recurrence list at this point
if self.mRecurrences is None:
self.mRecurrences = []
@@ -864,7 +868,9 @@
items.append(start_iter.duplicate())
# Get next item
- start_iter.recur(self.mFreq, self.mInterval)
+ start_iter.recur(self.mFreq, self.mInterval, allow_invalid=True)
+ while start_iter.invalid():
+ start_iter.recur(self.mFreq, self.mInterval, allow_invalid=True)
# Check limits
if self.mUseCount:
@@ -922,6 +928,9 @@
elif self.mFreq == definitions.eRecurrence_YEARLY:
self.generateYearlySet(start_iter, set_items)
+ # Ignore if it is invalid
+ set_items = filter(lambda x: not x.invalid(), set_items)
+
# Always sort the set as BYxxx rules may not be sorted
#set_items.sort(cmp=PyCalendarDateTime.sort)
set_items.sort(key=lambda x: x.getPosixTime())
@@ -967,7 +976,7 @@
return False
# Get next item
- start_iter.recur(self.mFreq, self.mInterval)
+ start_iter.recur(self.mFreq, self.mInterval, allow_invalid=True)
def clear(self):
@@ -1355,7 +1364,7 @@
# and insert into output
for iter2 in self.mByYearDay:
temp = iter1.duplicate()
- temp.setYearDay(iter2)
+ temp.setYearDay(iter2, allow_invalid=True)
output.append(temp)
return output
@@ -1369,7 +1378,7 @@
# and insert into output
for iter2 in self.mByMonthDay:
temp = iter1.duplicate()
- temp.setMonthDay(iter2)
+ temp.setMonthDay(iter2, allow_invalid=True)
output.append(temp)
return output
@@ -1408,13 +1417,13 @@
# Numeric value means specific instance
if iter2[0] != 0:
temp = iter1.duplicate()
- temp.setDayOfWeekInMonth(iter2[0], iter2[1])
+ temp.setDayOfWeekInMonth(iter2[0], iter2[1], allow_invalid=True)
output.append(temp)
else:
# Every matching day in the month
for i in range(1, 7):
temp = iter1.duplicate()
- temp.setDayOfWeekInMonth(i, iter2[1])
+ temp.setDayOfWeekInMonth(i, iter2[1], allow_invalid=True)
if temp.getMonth() == iter1.getMonth():
output.append(temp)
Modified: PyCalendar/branches/CalendarServer-5.2/src/pycalendar/tests/test_recurrence.py
===================================================================
--- PyCalendar/branches/CalendarServer-5.2/src/pycalendar/tests/test_recurrence.py 2014-04-07 15:59:12 UTC (rev 13176)
+++ PyCalendar/branches/CalendarServer-5.2/src/pycalendar/tests/test_recurrence.py 2014-04-07 16:00:00 UTC (rev 13177)
@@ -17,6 +17,7 @@
from pycalendar.datetime import PyCalendarDateTime
from pycalendar.period import PyCalendarPeriod
from pycalendar.recurrence import PyCalendarRecurrence
+from pycalendar.timezone import PyCalendarTimezone
import unittest
class TestRecurrence(unittest.TestCase):
@@ -137,9 +138,216 @@
PyCalendarDateTime(2016, 1, 15, 0, 0, 0),
],
)
- print items
+ def testMonthlyInvalidStart(self):
+
+ recur = PyCalendarRecurrence()
+ recur.parse("FREQ=MONTHLY")
+ start = PyCalendarDateTime(2014, 1, 40, 12, 0, 0)
+ end = PyCalendarDateTime(2015, 1, 1, 0, 0, 0)
+ items = []
+ range = PyCalendarPeriod(start, end)
+ recur.expand(start, range, items)
+ self.assertEqual(
+ items,
+ [
+ PyCalendarDateTime(2014, 2, 9, 12, 0, 0),
+ PyCalendarDateTime(2014, 3, 9, 12, 0, 0),
+ PyCalendarDateTime(2014, 4, 9, 12, 0, 0),
+ PyCalendarDateTime(2014, 5, 9, 12, 0, 0),
+ PyCalendarDateTime(2014, 6, 9, 12, 0, 0),
+ PyCalendarDateTime(2014, 7, 9, 12, 0, 0),
+ PyCalendarDateTime(2014, 8, 9, 12, 0, 0),
+ PyCalendarDateTime(2014, 9, 9, 12, 0, 0),
+ PyCalendarDateTime(2014, 10, 9, 12, 0, 0),
+ PyCalendarDateTime(2014, 11, 9, 12, 0, 0),
+ PyCalendarDateTime(2014, 12, 9, 12, 0, 0),
+ ],
+ )
+
+
+ def testMonthlyInUTC(self):
+
+ recur = PyCalendarRecurrence()
+ recur.parse("FREQ=MONTHLY")
+ start = PyCalendarDateTime(2014, 1, 1, 12, 0, 0, tzid=PyCalendarTimezone(utc=True))
+ end = PyCalendarDateTime(2015, 1, 1, 0, 0, 0, tzid=PyCalendarTimezone(utc=True))
+ items = []
+ range = PyCalendarPeriod(start, end)
+ recur.expand(PyCalendarDateTime(2014, 1, 1, 12, 0, 0, tzid=PyCalendarTimezone(tzid="America/New_York")), range, items)
+ self.assertEqual(
+ items,
+ [
+ PyCalendarDateTime(2014, 1, 1, 12, 0, 0, tzid=PyCalendarTimezone(tzid="America/New_York")),
+ PyCalendarDateTime(2014, 2, 1, 12, 0, 0, tzid=PyCalendarTimezone(tzid="America/New_York")),
+ PyCalendarDateTime(2014, 3, 1, 12, 0, 0, tzid=PyCalendarTimezone(tzid="America/New_York")),
+ PyCalendarDateTime(2014, 4, 1, 12, 0, 0, tzid=PyCalendarTimezone(tzid="America/New_York")),
+ PyCalendarDateTime(2014, 5, 1, 12, 0, 0, tzid=PyCalendarTimezone(tzid="America/New_York")),
+ PyCalendarDateTime(2014, 6, 1, 12, 0, 0, tzid=PyCalendarTimezone(tzid="America/New_York")),
+ PyCalendarDateTime(2014, 7, 1, 12, 0, 0, tzid=PyCalendarTimezone(tzid="America/New_York")),
+ PyCalendarDateTime(2014, 8, 1, 12, 0, 0, tzid=PyCalendarTimezone(tzid="America/New_York")),
+ PyCalendarDateTime(2014, 9, 1, 12, 0, 0, tzid=PyCalendarTimezone(tzid="America/New_York")),
+ PyCalendarDateTime(2014, 10, 1, 12, 0, 0),
+ PyCalendarDateTime(2014, 11, 1, 12, 0, 0),
+ PyCalendarDateTime(2014, 12, 1, 12, 0, 0),
+ ],
+ )
+
+
+ def testMonthlyStart31st(self):
+
+ recur = PyCalendarRecurrence()
+ recur.parse("FREQ=MONTHLY")
+ start = PyCalendarDateTime(2014, 1, 31, 12, 0, 0)
+ end = PyCalendarDateTime(2015, 1, 1, 0, 0, 0)
+ items = []
+ range = PyCalendarPeriod(start, end)
+ recur.expand(start, range, items)
+ self.assertEqual(
+ items,
+ [
+ PyCalendarDateTime(2014, 1, 31, 12, 0, 0),
+ PyCalendarDateTime(2014, 3, 31, 12, 0, 0),
+ PyCalendarDateTime(2014, 5, 31, 12, 0, 0),
+ PyCalendarDateTime(2014, 7, 31, 12, 0, 0),
+ PyCalendarDateTime(2014, 8, 31, 12, 0, 0),
+ PyCalendarDateTime(2014, 10, 31, 12, 0, 0),
+ PyCalendarDateTime(2014, 12, 31, 12, 0, 0),
+ ],
+ )
+
+
+ def testMonthlyByMonthDay31(self):
+
+ recur = PyCalendarRecurrence()
+ recur.parse("FREQ=MONTHLY;BYMONTHDAY=31")
+ start = PyCalendarDateTime(2014, 1, 31, 12, 0, 0)
+ end = PyCalendarDateTime(2015, 1, 1, 0, 0, 0)
+ items = []
+ range = PyCalendarPeriod(start, end)
+ recur.expand(start, range, items)
+ self.assertEqual(
+ items,
+ [
+ PyCalendarDateTime(2014, 1, 31, 12, 0, 0),
+ PyCalendarDateTime(2014, 3, 31, 12, 0, 0),
+ PyCalendarDateTime(2014, 5, 31, 12, 0, 0),
+ PyCalendarDateTime(2014, 7, 31, 12, 0, 0),
+ PyCalendarDateTime(2014, 8, 31, 12, 0, 0),
+ PyCalendarDateTime(2014, 10, 31, 12, 0, 0),
+ PyCalendarDateTime(2014, 12, 31, 12, 0, 0),
+ ],
+ )
+
+
+ def testMonthlyByMonthDayMinus31(self):
+
+ recur = PyCalendarRecurrence()
+ recur.parse("FREQ=MONTHLY;BYMONTHDAY=-31")
+ start = PyCalendarDateTime(2014, 1, 1, 12, 0, 0)
+ end = PyCalendarDateTime(2015, 1, 1, 0, 0, 0)
+ items = []
+ range = PyCalendarPeriod(start, end)
+ recur.expand(start, range, items)
+ self.assertEqual(
+ items,
+ [
+ PyCalendarDateTime(2014, 1, 1, 12, 0, 0),
+ PyCalendarDateTime(2014, 3, 1, 12, 0, 0),
+ PyCalendarDateTime(2014, 5, 1, 12, 0, 0),
+ PyCalendarDateTime(2014, 7, 1, 12, 0, 0),
+ PyCalendarDateTime(2014, 8, 1, 12, 0, 0),
+ PyCalendarDateTime(2014, 10, 1, 12, 0, 0),
+ PyCalendarDateTime(2014, 12, 1, 12, 0, 0),
+ ],
+ )
+
+
+ def testMonthlyByLastFridayExpand(self):
+
+ recur = PyCalendarRecurrence()
+ recur.parse("FREQ=MONTHLY;BYDAY=-1FR")
+ start = PyCalendarDateTime(2014, 1, 31, 12, 0, 0)
+ end = PyCalendarDateTime(2015, 1, 1, 0, 0, 0)
+ items = []
+ range = PyCalendarPeriod(start, end)
+ recur.expand(start, range, items)
+ self.assertEqual(
+ items,
+ [
+ PyCalendarDateTime(2014, 1, 31, 12, 0, 0),
+ PyCalendarDateTime(2014, 2, 28, 12, 0, 0),
+ PyCalendarDateTime(2014, 3, 28, 12, 0, 0),
+ PyCalendarDateTime(2014, 4, 25, 12, 0, 0),
+ PyCalendarDateTime(2014, 5, 30, 12, 0, 0),
+ PyCalendarDateTime(2014, 6, 27, 12, 0, 0),
+ PyCalendarDateTime(2014, 7, 25, 12, 0, 0),
+ PyCalendarDateTime(2014, 8, 29, 12, 0, 0),
+ PyCalendarDateTime(2014, 9, 26, 12, 0, 0),
+ PyCalendarDateTime(2014, 10, 31, 12, 0, 0),
+ PyCalendarDateTime(2014, 11, 28, 12, 0, 0),
+ PyCalendarDateTime(2014, 12, 26, 12, 0, 0),
+ ],
+ )
+
+
+ def testMonthlyByFifthFridayExpand(self):
+
+ recur = PyCalendarRecurrence()
+ recur.parse("FREQ=MONTHLY;BYDAY=5FR")
+ start = PyCalendarDateTime(2014, 1, 31, 12, 0, 0)
+ end = PyCalendarDateTime(2015, 1, 1, 0, 0, 0)
+ items = []
+ range = PyCalendarPeriod(start, end)
+ recur.expand(start, range, items)
+ self.assertEqual(
+ items,
+ [
+ PyCalendarDateTime(2014, 1, 31, 12, 0, 0),
+ PyCalendarDateTime(2014, 5, 30, 12, 0, 0),
+ PyCalendarDateTime(2014, 8, 29, 12, 0, 0),
+ PyCalendarDateTime(2014, 10, 31, 12, 0, 0),
+ ],
+ )
+
+
+ def testYearlyLeapDay(self):
+
+ recur = PyCalendarRecurrence()
+ recur.parse("FREQ=YEARLY")
+ start = PyCalendarDateTime(2012, 2, 29, 12, 0, 0)
+ end = PyCalendarDateTime(2020, 1, 1, 0, 0, 0)
+ items = []
+ range = PyCalendarPeriod(start, end)
+ recur.expand(start, range, items)
+ self.assertEqual(
+ items,
+ [
+ PyCalendarDateTime(2012, 2, 29, 12, 0, 0),
+ PyCalendarDateTime(2016, 2, 29, 12, 0, 0),
+ ],
+ )
+
+
+ def testYearlyYearDay(self):
+
+ recur = PyCalendarRecurrence()
+ recur.parse("FREQ=YEARLY;BYYEARDAY=366")
+ start = PyCalendarDateTime(2012, 12, 31, 12, 0, 0)
+ end = PyCalendarDateTime(2020, 1, 1, 0, 0, 0)
+ items = []
+ range = PyCalendarPeriod(start, end)
+ recur.expand(start, range, items)
+ self.assertEqual(
+ items,
+ [
+ PyCalendarDateTime(2012, 12, 31, 12, 0, 0),
+ PyCalendarDateTime(2016, 12, 31, 12, 0, 0),
+ ],
+ )
+
+
def testClearOnChange(self):
recur = PyCalendarRecurrence()
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140407/29177c94/attachment-0001.html>
More information about the calendarserver-changes
mailing list