[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