[CalendarServer-changes] [13176] PyCalendar/trunk
source_changes at macosforge.org
source_changes at macosforge.org
Mon Apr 7 08:59:12 PDT 2014
Revision: 13176
http://trac.calendarserver.org//changeset/13176
Author: cdaboo at apple.com
Date: 2014-04-07 08:59:12 -0700 (Mon, 07 Apr 2014)
Log Message:
-----------
Properly handle invalid dates during recurrence.
Modified Paths:
--------------
PyCalendar/trunk/.pydevproject
PyCalendar/trunk/src/pycalendar/datetime.py
PyCalendar/trunk/src/pycalendar/icalendar/recurrence.py
PyCalendar/trunk/src/pycalendar/icalendar/tests/test_recurrence.py
Modified: PyCalendar/trunk/.pydevproject
===================================================================
--- PyCalendar/trunk/.pydevproject 2014-04-05 19:11:46 UTC (rev 13175)
+++ PyCalendar/trunk/.pydevproject 2014-04-07 15:59:12 UTC (rev 13176)
@@ -1,10 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<?eclipse-pydev version="1.0"?>
-
-<pydev_project>
+<?eclipse-pydev version="1.0"?><pydev_project>
+<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
+<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
-<path>/${PROJECT_DIR_NAME}</path>
+<path>/${PROJECT_DIR_NAME}/src</path>
</pydev_pathproperty>
-<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
-<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
</pydev_project>
Modified: PyCalendar/trunk/src/pycalendar/datetime.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/datetime.py 2014-04-05 19:11:46 UTC (rev 13175)
+++ PyCalendar/trunk/src/pycalendar/datetime.py 2014-04-07 15:59:12 UTC (rev 13176)
@@ -354,11 +354,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
@@ -366,26 +374,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
@@ -396,18 +404,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):
@@ -525,7 +533,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
@@ -549,7 +557,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):
@@ -784,8 +795,9 @@
return DateTime(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:
@@ -803,15 +815,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):
@@ -1111,6 +1140,20 @@
jobject.append(self.getJSONText())
+ 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/trunk/src/pycalendar/icalendar/recurrence.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/icalendar/recurrence.py 2014-04-05 19:11:46 UTC (rev 13175)
+++ PyCalendar/trunk/src/pycalendar/icalendar/recurrence.py 2014-04-07 15:59:12 UTC (rev 13176)
@@ -876,6 +876,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 = []
@@ -937,7 +941,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:
@@ -995,6 +1001,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=DateTime.sort)
set_items.sort(key=lambda x: x.getPosixTime())
@@ -1040,7 +1049,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):
@@ -1428,7 +1437,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
@@ -1442,7 +1451,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
@@ -1481,13 +1490,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/trunk/src/pycalendar/icalendar/tests/test_recurrence.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/icalendar/tests/test_recurrence.py 2014-04-05 19:11:46 UTC (rev 13175)
+++ PyCalendar/trunk/src/pycalendar/icalendar/tests/test_recurrence.py 2014-04-07 15:59:12 UTC (rev 13176)
@@ -18,6 +18,7 @@
from pycalendar.period import Period
from pycalendar.icalendar.recurrence import Recurrence
import unittest
+from pycalendar.timezone import Timezone
class TestRecurrence(unittest.TestCase):
@@ -139,6 +140,214 @@
)
+ def testMonthlyInvalidStart(self):
+
+ recur = Recurrence()
+ recur.parse("FREQ=MONTHLY")
+ start = DateTime(2014, 1, 40, 12, 0, 0)
+ end = DateTime(2015, 1, 1, 0, 0, 0)
+ items = []
+ range = Period(start, end)
+ recur.expand(start, range, items)
+ self.assertEqual(
+ items,
+ [
+ DateTime(2014, 2, 9, 12, 0, 0),
+ DateTime(2014, 3, 9, 12, 0, 0),
+ DateTime(2014, 4, 9, 12, 0, 0),
+ DateTime(2014, 5, 9, 12, 0, 0),
+ DateTime(2014, 6, 9, 12, 0, 0),
+ DateTime(2014, 7, 9, 12, 0, 0),
+ DateTime(2014, 8, 9, 12, 0, 0),
+ DateTime(2014, 9, 9, 12, 0, 0),
+ DateTime(2014, 10, 9, 12, 0, 0),
+ DateTime(2014, 11, 9, 12, 0, 0),
+ DateTime(2014, 12, 9, 12, 0, 0),
+ ],
+ )
+
+
+ def testMonthlyInUTC(self):
+
+ recur = Recurrence()
+ recur.parse("FREQ=MONTHLY")
+ start = DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(utc=True))
+ end = DateTime(2015, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
+ items = []
+ range = Period(start, end)
+ recur.expand(DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), range, items)
+ self.assertEqual(
+ items,
+ [
+ DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
+ DateTime(2014, 2, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
+ DateTime(2014, 3, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
+ DateTime(2014, 4, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
+ DateTime(2014, 5, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
+ DateTime(2014, 6, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
+ DateTime(2014, 7, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
+ DateTime(2014, 8, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
+ DateTime(2014, 9, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
+ DateTime(2014, 10, 1, 12, 0, 0),
+ DateTime(2014, 11, 1, 12, 0, 0),
+ DateTime(2014, 12, 1, 12, 0, 0),
+ ],
+ )
+
+
+ def testMonthlyStart31st(self):
+
+ recur = Recurrence()
+ recur.parse("FREQ=MONTHLY")
+ start = DateTime(2014, 1, 31, 12, 0, 0)
+ end = DateTime(2015, 1, 1, 0, 0, 0)
+ items = []
+ range = Period(start, end)
+ recur.expand(start, range, items)
+ self.assertEqual(
+ items,
+ [
+ DateTime(2014, 1, 31, 12, 0, 0),
+ DateTime(2014, 3, 31, 12, 0, 0),
+ DateTime(2014, 5, 31, 12, 0, 0),
+ DateTime(2014, 7, 31, 12, 0, 0),
+ DateTime(2014, 8, 31, 12, 0, 0),
+ DateTime(2014, 10, 31, 12, 0, 0),
+ DateTime(2014, 12, 31, 12, 0, 0),
+ ],
+ )
+
+
+ def testMonthlyByMonthDay31(self):
+
+ recur = Recurrence()
+ recur.parse("FREQ=MONTHLY;BYMONTHDAY=31")
+ start = DateTime(2014, 1, 31, 12, 0, 0)
+ end = DateTime(2015, 1, 1, 0, 0, 0)
+ items = []
+ range = Period(start, end)
+ recur.expand(start, range, items)
+ self.assertEqual(
+ items,
+ [
+ DateTime(2014, 1, 31, 12, 0, 0),
+ DateTime(2014, 3, 31, 12, 0, 0),
+ DateTime(2014, 5, 31, 12, 0, 0),
+ DateTime(2014, 7, 31, 12, 0, 0),
+ DateTime(2014, 8, 31, 12, 0, 0),
+ DateTime(2014, 10, 31, 12, 0, 0),
+ DateTime(2014, 12, 31, 12, 0, 0),
+ ],
+ )
+
+
+ def testMonthlyByMonthDayMinus31(self):
+
+ recur = Recurrence()
+ recur.parse("FREQ=MONTHLY;BYMONTHDAY=-31")
+ start = DateTime(2014, 1, 1, 12, 0, 0)
+ end = DateTime(2015, 1, 1, 0, 0, 0)
+ items = []
+ range = Period(start, end)
+ recur.expand(start, range, items)
+ self.assertEqual(
+ items,
+ [
+ DateTime(2014, 1, 1, 12, 0, 0),
+ DateTime(2014, 3, 1, 12, 0, 0),
+ DateTime(2014, 5, 1, 12, 0, 0),
+ DateTime(2014, 7, 1, 12, 0, 0),
+ DateTime(2014, 8, 1, 12, 0, 0),
+ DateTime(2014, 10, 1, 12, 0, 0),
+ DateTime(2014, 12, 1, 12, 0, 0),
+ ],
+ )
+
+
+ def testMonthlyByLastFridayExpand(self):
+
+ recur = Recurrence()
+ recur.parse("FREQ=MONTHLY;BYDAY=-1FR")
+ start = DateTime(2014, 1, 31, 12, 0, 0)
+ end = DateTime(2015, 1, 1, 0, 0, 0)
+ items = []
+ range = Period(start, end)
+ recur.expand(start, range, items)
+ self.assertEqual(
+ items,
+ [
+ DateTime(2014, 1, 31, 12, 0, 0),
+ DateTime(2014, 2, 28, 12, 0, 0),
+ DateTime(2014, 3, 28, 12, 0, 0),
+ DateTime(2014, 4, 25, 12, 0, 0),
+ DateTime(2014, 5, 30, 12, 0, 0),
+ DateTime(2014, 6, 27, 12, 0, 0),
+ DateTime(2014, 7, 25, 12, 0, 0),
+ DateTime(2014, 8, 29, 12, 0, 0),
+ DateTime(2014, 9, 26, 12, 0, 0),
+ DateTime(2014, 10, 31, 12, 0, 0),
+ DateTime(2014, 11, 28, 12, 0, 0),
+ DateTime(2014, 12, 26, 12, 0, 0),
+ ],
+ )
+
+
+ def testMonthlyByFifthFridayExpand(self):
+
+ recur = Recurrence()
+ recur.parse("FREQ=MONTHLY;BYDAY=5FR")
+ start = DateTime(2014, 1, 31, 12, 0, 0)
+ end = DateTime(2015, 1, 1, 0, 0, 0)
+ items = []
+ range = Period(start, end)
+ recur.expand(start, range, items)
+ self.assertEqual(
+ items,
+ [
+ DateTime(2014, 1, 31, 12, 0, 0),
+ DateTime(2014, 5, 30, 12, 0, 0),
+ DateTime(2014, 8, 29, 12, 0, 0),
+ DateTime(2014, 10, 31, 12, 0, 0),
+ ],
+ )
+
+
+ def testYearlyLeapDay(self):
+
+ recur = Recurrence()
+ recur.parse("FREQ=YEARLY")
+ start = DateTime(2012, 2, 29, 12, 0, 0)
+ end = DateTime(2020, 1, 1, 0, 0, 0)
+ items = []
+ range = Period(start, end)
+ recur.expand(start, range, items)
+ self.assertEqual(
+ items,
+ [
+ DateTime(2012, 2, 29, 12, 0, 0),
+ DateTime(2016, 2, 29, 12, 0, 0),
+ ],
+ )
+
+
+ def testYearlyYearDay(self):
+
+ recur = Recurrence()
+ recur.parse("FREQ=YEARLY;BYYEARDAY=366")
+ start = DateTime(2012, 12, 31, 12, 0, 0)
+ end = DateTime(2020, 1, 1, 0, 0, 0)
+ items = []
+ range = Period(start, end)
+ recur.expand(start, range, items)
+ self.assertEqual(
+ items,
+ [
+ DateTime(2012, 12, 31, 12, 0, 0),
+ DateTime(2016, 12, 31, 12, 0, 0),
+ ],
+ )
+
+
def testClearOnChange(self):
recur = Recurrence()
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140407/6344e73f/attachment-0001.html>
More information about the calendarserver-changes
mailing list