<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[13176] PyCalendar/trunk</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/13176">13176</a></dd>
<dt>Author</dt> <dd>cdaboo@apple.com</dd>
<dt>Date</dt> <dd>2014-04-07 08:59:12 -0700 (Mon, 07 Apr 2014)</dd>
</dl>

<h3>Log Message</h3>
<pre>Properly handle invalid dates during recurrence.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#PyCalendartrunkpydevproject">PyCalendar/trunk/.pydevproject</a></li>
<li><a href="#PyCalendartrunksrcpycalendardatetimepy">PyCalendar/trunk/src/pycalendar/datetime.py</a></li>
<li><a href="#PyCalendartrunksrcpycalendaricalendarrecurrencepy">PyCalendar/trunk/src/pycalendar/icalendar/recurrence.py</a></li>
<li><a href="#PyCalendartrunksrcpycalendaricalendarteststest_recurrencepy">PyCalendar/trunk/src/pycalendar/icalendar/tests/test_recurrence.py</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="PyCalendartrunkpydevproject"></a>
<div class="modfile"><h4>Modified: PyCalendar/trunk/.pydevproject (13175 => 13176)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/trunk/.pydevproject        2014-04-05 19:11:46 UTC (rev 13175)
+++ PyCalendar/trunk/.pydevproject        2014-04-07 15:59:12 UTC (rev 13176)
</span><span class="lines">@@ -1,10 +1,8 @@
</span><span class="cx"> &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;
</span><del>-&lt;?eclipse-pydev version=&quot;1.0&quot;?&gt;
-
-&lt;pydev_project&gt;
</del><ins>+&lt;?eclipse-pydev version=&quot;1.0&quot;?&gt;&lt;pydev_project&gt;
+&lt;pydev_property name=&quot;org.python.pydev.PYTHON_PROJECT_INTERPRETER&quot;&gt;Default&lt;/pydev_property&gt;
+&lt;pydev_property name=&quot;org.python.pydev.PYTHON_PROJECT_VERSION&quot;&gt;python 2.7&lt;/pydev_property&gt;
</ins><span class="cx"> &lt;pydev_pathproperty name=&quot;org.python.pydev.PROJECT_SOURCE_PATH&quot;&gt;
</span><del>-&lt;path&gt;/${PROJECT_DIR_NAME}&lt;/path&gt;
</del><ins>+&lt;path&gt;/${PROJECT_DIR_NAME}/src&lt;/path&gt;
</ins><span class="cx"> &lt;/pydev_pathproperty&gt;
</span><del>-&lt;pydev_property name=&quot;org.python.pydev.PYTHON_PROJECT_VERSION&quot;&gt;python 2.7&lt;/pydev_property&gt;
-&lt;pydev_property name=&quot;org.python.pydev.PYTHON_PROJECT_INTERPRETER&quot;&gt;Default&lt;/pydev_property&gt;
</del><span class="cx"> &lt;/pydev_project&gt;
</span></span></pre></div>
<a id="PyCalendartrunksrcpycalendardatetimepy"></a>
<div class="modfile"><h4>Modified: PyCalendar/trunk/src/pycalendar/datetime.py (13175 => 13176)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -354,11 +354,19 @@
</span><span class="cx">         self.normalise()
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def setYearDay(self, day):
</del><ins>+    def setYearDay(self, day, allow_invalid=False):
</ins><span class="cx">         # 1 .. 366 offset from start, or
</span><span class="cx">         # -1 .. -366 offset from end
</span><span class="cx"> 
</span><del>-        if day &gt; 0:
</del><ins>+        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 &gt; 0:
</ins><span class="cx">             # Offset current date to 1st January of current year
</span><span class="cx">             self.mMonth = 1
</span><span class="cx">             self.mDay = 1
</span><span class="lines">@@ -366,26 +374,26 @@
</span><span class="cx">             # Increment day
</span><span class="cx">             self.mDay += day - 1
</span><span class="cx"> 
</span><del>-            # Normalise to get proper month/day values
-            self.normalise()
</del><span class="cx">         elif day &lt; 0:
</span><span class="cx">             # Offset current date to 1st January of next year
</span><del>-            self.mYear += 1
-            self.mMonth = 1
-            self.mDay = 1
</del><ins>+            self.mMonth = 12
+            self.mDay = 31
</ins><span class="cx"> 
</span><span class="cx">             # Decrement day
</span><del>-            self.mDay += day
</del><ins>+            self.mDay += day + 1
</ins><span class="cx"> 
</span><ins>+        if not allow_invalid:
</ins><span class="cx">             # Normalise to get proper year/month/day values
</span><span class="cx">             self.normalise()
</span><ins>+        else:
+            self.changed()
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def getYearDay(self):
</span><span class="cx">         return self.mDay + utils.daysUptoMonth(self.mMonth, self.mYear)
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def setMonthDay(self, day):
</del><ins>+    def setMonthDay(self, day, allow_invalid=False):
</ins><span class="cx">         # 1 .. 31 offset from start, or
</span><span class="cx">         # -1 .. -31 offset from end
</span><span class="cx"> 
</span><span class="lines">@@ -396,18 +404,18 @@
</span><span class="cx">             # Increment day
</span><span class="cx">             self.mDay += day - 1
</span><span class="cx"> 
</span><del>-            # Normalise to get proper month/day values
-            self.normalise()
</del><span class="cx">         elif day &lt; 0:
</span><del>-            # Offset current date to 1st of next month
-            self.mMonth += 1
-            self.mDay = 1
</del><ins>+            # Offset current date to last of month
+            self.mDay = utils.daysInMonth(self.mMonth, self.mYear)
</ins><span class="cx"> 
</span><span class="cx">             # Decrement day
</span><del>-            self.mDay += day
</del><ins>+            self.mDay += day + 1
</ins><span class="cx"> 
</span><ins>+        if not allow_invalid:
</ins><span class="cx">             # Normalise to get proper year/month/day values
</span><span class="cx">             self.normalise()
</span><ins>+        else:
+            self.changed()
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def isMonthDay(self, day):
</span><span class="lines">@@ -525,7 +533,7 @@
</span><span class="cx">         self.normalise()
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def setDayOfWeekInMonth(self, offset, day):
</del><ins>+    def setDayOfWeekInMonth(self, offset, day, allow_invalid=False):
</ins><span class="cx">         # Set to first day in month
</span><span class="cx">         self.mDay = 1
</span><span class="cx"> 
</span><span class="lines">@@ -549,7 +557,10 @@
</span><span class="cx">                 cycle += 7
</span><span class="cx">             self.mDay = days_in_month - cycle
</span><span class="cx"> 
</span><del>-        self.normalise()
</del><ins>+        if not allow_invalid:
+            self.normalise()
+        else:
+            self.changed()
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def setNextDayOfWeek(self, start, day):
</span><span class="lines">@@ -784,8 +795,9 @@
</span><span class="cx">         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)
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def recur(self, freq, interval):
</del><ins>+    def recur(self, freq, interval, allow_invalid=False):
</ins><span class="cx">         # Add appropriate interval
</span><ins>+        normalize = True
</ins><span class="cx">         if freq == definitions.eRecurrence_SECONDLY:
</span><span class="cx">             self.mSeconds += interval
</span><span class="cx">         elif freq == definitions.eRecurrence_MINUTELY:
</span><span class="lines">@@ -803,15 +815,32 @@
</span><span class="cx">             # or 1/2 May, or 31 March or what? We choose to find the next month with
</span><span class="cx">             # the same day number as the current one.
</span><span class="cx">             self.mMonth += interval
</span><del>-            self.normalise()
-            while self.mDay &gt; utils.daysInMonth(self.mMonth, self.mYear):
-                self.mMonth += interval
</del><ins>+
+            # Normalise month
+            normalised_month = ((self.mMonth - 1) % 12) + 1
+            adjustment_year = (self.mMonth - 1) / 12
+            if (normalised_month - 1) &lt; 0:
+                normalised_month += 12
+                adjustment_year -= 1
+            self.mMonth = normalised_month
+            self.mYear += adjustment_year
+
+            if not allow_invalid:
</ins><span class="cx">                 self.normalise()
</span><ins>+                while self.mDay &gt; utils.daysInMonth(self.mMonth, self.mYear):
+                    self.mMonth += interval
+                    self.normalise()
+            normalize = False
</ins><span class="cx">         elif freq == definitions.eRecurrence_YEARLY:
</span><span class="cx">             self.mYear += interval
</span><ins>+            if allow_invalid:
+                normalize = False
</ins><span class="cx"> 
</span><del>-        # Normalise to standard date-time ranges
-        self.normalise()
</del><ins>+        if normalize:
+            # Normalise to standard date-time ranges
+            self.normalise()
+        else:
+            self.changed()
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def getLocaleDate(self, locale):
</span><span class="lines">@@ -1111,6 +1140,20 @@
</span><span class="cx">         jobject.append(self.getJSONText())
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    def invalid(self):
+        &quot;&quot;&quot;
+        Are any of the current fields invalid.
+        &quot;&quot;&quot;
+
+        # 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 &lt;= 0 or self.mDay &gt; utils.daysInMonth(self.mMonth, self.mYear):
+            return True
+
+        return False
+
+
</ins><span class="cx">     def normalise(self):
</span><span class="cx">         # Normalise seconds
</span><span class="cx">         normalised_secs = self.mSeconds % 60
</span></span></pre></div>
<a id="PyCalendartrunksrcpycalendaricalendarrecurrencepy"></a>
<div class="modfile"><h4>Modified: PyCalendar/trunk/src/pycalendar/icalendar/recurrence.py (13175 => 13176)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -876,6 +876,10 @@
</span><span class="cx"> 
</span><span class="cx">     def expand(self, start, range, items, float_offset=0):
</span><span class="cx"> 
</span><ins>+        # 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()
+
</ins><span class="cx">         # Must have recurrence list at this point
</span><span class="cx">         if self.mRecurrences is None:
</span><span class="cx">             self.mRecurrences = []
</span><span class="lines">@@ -937,7 +941,9 @@
</span><span class="cx">             items.append(start_iter.duplicate())
</span><span class="cx"> 
</span><span class="cx">             # Get next item
</span><del>-            start_iter.recur(self.mFreq, self.mInterval)
</del><ins>+            start_iter.recur(self.mFreq, self.mInterval, allow_invalid=True)
+            while start_iter.invalid():
+                start_iter.recur(self.mFreq, self.mInterval, allow_invalid=True)
</ins><span class="cx"> 
</span><span class="cx">             # Check limits
</span><span class="cx">             if self.mUseCount:
</span><span class="lines">@@ -995,6 +1001,9 @@
</span><span class="cx">             elif self.mFreq == definitions.eRecurrence_YEARLY:
</span><span class="cx">                 self.generateYearlySet(start_iter, set_items)
</span><span class="cx"> 
</span><ins>+            # Ignore if it is invalid
+            set_items = filter(lambda x: not x.invalid(), set_items)
+
</ins><span class="cx">             # Always sort the set as BYxxx rules may not be sorted
</span><span class="cx">             #set_items.sort(cmp=DateTime.sort)
</span><span class="cx">             set_items.sort(key=lambda x: x.getPosixTime())
</span><span class="lines">@@ -1040,7 +1049,7 @@
</span><span class="cx">                 return False
</span><span class="cx"> 
</span><span class="cx">             # Get next item
</span><del>-            start_iter.recur(self.mFreq, self.mInterval)
</del><ins>+            start_iter.recur(self.mFreq, self.mInterval, allow_invalid=True)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def clear(self):
</span><span class="lines">@@ -1428,7 +1437,7 @@
</span><span class="cx">             # and insert into output
</span><span class="cx">             for iter2 in self.mByYearDay:
</span><span class="cx">                 temp = iter1.duplicate()
</span><del>-                temp.setYearDay(iter2)
</del><ins>+                temp.setYearDay(iter2, allow_invalid=True)
</ins><span class="cx">                 output.append(temp)
</span><span class="cx"> 
</span><span class="cx">         return output
</span><span class="lines">@@ -1442,7 +1451,7 @@
</span><span class="cx">             # and insert into output
</span><span class="cx">             for iter2 in self.mByMonthDay:
</span><span class="cx">                 temp = iter1.duplicate()
</span><del>-                temp.setMonthDay(iter2)
</del><ins>+                temp.setMonthDay(iter2, allow_invalid=True)
</ins><span class="cx">                 output.append(temp)
</span><span class="cx"> 
</span><span class="cx">         return output
</span><span class="lines">@@ -1481,13 +1490,13 @@
</span><span class="cx">                 # Numeric value means specific instance
</span><span class="cx">                 if iter2[0] != 0:
</span><span class="cx">                     temp = iter1.duplicate()
</span><del>-                    temp.setDayOfWeekInMonth(iter2[0], iter2[1])
</del><ins>+                    temp.setDayOfWeekInMonth(iter2[0], iter2[1], allow_invalid=True)
</ins><span class="cx">                     output.append(temp)
</span><span class="cx">                 else:
</span><span class="cx">                     # Every matching day in the month
</span><span class="cx">                     for i in range(1, 7):
</span><span class="cx">                         temp = iter1.duplicate()
</span><del>-                        temp.setDayOfWeekInMonth(i, iter2[1])
</del><ins>+                        temp.setDayOfWeekInMonth(i, iter2[1], allow_invalid=True)
</ins><span class="cx">                         if temp.getMonth() == iter1.getMonth():
</span><span class="cx">                             output.append(temp)
</span><span class="cx"> 
</span></span></pre></div>
<a id="PyCalendartrunksrcpycalendaricalendarteststest_recurrencepy"></a>
<div class="modfile"><h4>Modified: PyCalendar/trunk/src/pycalendar/icalendar/tests/test_recurrence.py (13175 => 13176)</h4>
<pre class="diff"><span>
<span class="info">--- 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)
</span><span class="lines">@@ -18,6 +18,7 @@
</span><span class="cx"> from pycalendar.period import Period
</span><span class="cx"> from pycalendar.icalendar.recurrence import Recurrence
</span><span class="cx"> import unittest
</span><ins>+from pycalendar.timezone import Timezone
</ins><span class="cx"> 
</span><span class="cx"> class TestRecurrence(unittest.TestCase):
</span><span class="cx"> 
</span><span class="lines">@@ -139,6 +140,214 @@
</span><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    def testMonthlyInvalidStart(self):
+
+        recur = Recurrence()
+        recur.parse(&quot;FREQ=MONTHLY&quot;)
+        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(&quot;FREQ=MONTHLY&quot;)
+        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=&quot;America/New_York&quot;)), range, items)
+        self.assertEqual(
+            items,
+            [
+                DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(tzid=&quot;America/New_York&quot;)),
+                DateTime(2014, 2, 1, 12, 0, 0, tzid=Timezone(tzid=&quot;America/New_York&quot;)),
+                DateTime(2014, 3, 1, 12, 0, 0, tzid=Timezone(tzid=&quot;America/New_York&quot;)),
+                DateTime(2014, 4, 1, 12, 0, 0, tzid=Timezone(tzid=&quot;America/New_York&quot;)),
+                DateTime(2014, 5, 1, 12, 0, 0, tzid=Timezone(tzid=&quot;America/New_York&quot;)),
+                DateTime(2014, 6, 1, 12, 0, 0, tzid=Timezone(tzid=&quot;America/New_York&quot;)),
+                DateTime(2014, 7, 1, 12, 0, 0, tzid=Timezone(tzid=&quot;America/New_York&quot;)),
+                DateTime(2014, 8, 1, 12, 0, 0, tzid=Timezone(tzid=&quot;America/New_York&quot;)),
+                DateTime(2014, 9, 1, 12, 0, 0, tzid=Timezone(tzid=&quot;America/New_York&quot;)),
+                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(&quot;FREQ=MONTHLY&quot;)
+        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(&quot;FREQ=MONTHLY;BYMONTHDAY=31&quot;)
+        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(&quot;FREQ=MONTHLY;BYMONTHDAY=-31&quot;)
+        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(&quot;FREQ=MONTHLY;BYDAY=-1FR&quot;)
+        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(&quot;FREQ=MONTHLY;BYDAY=5FR&quot;)
+        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(&quot;FREQ=YEARLY&quot;)
+        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(&quot;FREQ=YEARLY;BYYEARDAY=366&quot;)
+        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),
+            ],
+        )
+
+
</ins><span class="cx">     def testClearOnChange(self):
</span><span class="cx"> 
</span><span class="cx">         recur = Recurrence()
</span></span></pre>
</div>
</div>

</body>
</html>