[CalendarServer-changes] [14191] PyCalendar/trunk

source_changes at macosforge.org source_changes at macosforge.org
Mon Dec 1 07:15:13 PST 2014


Revision: 14191
          http://trac.calendarserver.org//changeset/14191
Author:   cdaboo at apple.com
Date:     2014-12-01 07:15:13 -0800 (Mon, 01 Dec 2014)
Log Message:
-----------
Initial RSCALE support using cffi to interface with the ICU library.

Modified Paths:
--------------
    PyCalendar/trunk/.project
    PyCalendar/trunk/src/pycalendar/datetime.py
    PyCalendar/trunk/src/pycalendar/icalendar/definitions.py
    PyCalendar/trunk/src/pycalendar/icalendar/recurrence.py
    PyCalendar/trunk/src/pycalendar/icalendar/tests/test_recurrence.py
    PyCalendar/trunk/src/pycalendar/icalendar/xmldefinitions.py

Added Paths:
-----------
    PyCalendar/trunk/src/pycalendar/icalendar/icudatetime.py
    PyCalendar/trunk/src/pycalendar/icalendar/recuriter.py
    PyCalendar/trunk/src/pycalendar/icalendar/tests/test_icudatetime.py
    PyCalendar/trunk/src/pycalendar/icalendar/tests/test_recuriter.py

Modified: PyCalendar/trunk/.project
===================================================================
--- PyCalendar/trunk/.project	2014-12-01 15:13:17 UTC (rev 14190)
+++ PyCalendar/trunk/.project	2014-12-01 15:15:13 UTC (rev 14191)
@@ -3,6 +3,7 @@
 	<name>pycalendar</name>
 	<comment></comment>
 	<projects>
+		<project>cffi</project>
 	</projects>
 	<buildSpec>
 		<buildCommand>

Modified: PyCalendar/trunk/src/pycalendar/datetime.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/datetime.py	2014-12-01 15:13:17 UTC (rev 14190)
+++ PyCalendar/trunk/src/pycalendar/datetime.py	2014-12-01 15:15:13 UTC (rev 14191)
@@ -301,7 +301,7 @@
         self.changed()
 
 
-    def setYYMMDD(self, year, month, days):
+    def setYYMMDD(self, year, month, days, isleapmonth=False):
         if (self.mYear != year) or (self.mMonth != month) or (self.mDay != days):
             self.mYear = year
             self.mMonth = month
@@ -321,14 +321,18 @@
 
     def offsetYear(self, diff_year):
         self.mYear += diff_year
-        self.normalise()
 
+        # Do special normalization for this case to do a skip backwards if
+        # the new date is invalid
+        if self.mDay > utils.daysInMonth(self.mMonth, self.mYear):
+            self.mDay = utils.daysInMonth(self.mMonth, self.mYear)
 
+
     def getMonth(self):
         return self.mMonth
 
 
-    def setMonth(self, month):
+    def setMonth(self, month, isleapmonth=False):
         if self.mMonth != month:
             self.mMonth = month
             self.changed()
@@ -336,9 +340,26 @@
 
     def offsetMonth(self, diff_month):
         self.mMonth += diff_month
-        self.normalise()
 
+        # 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
 
+        # Do special normalization for this case to do a skip backwards if
+        # the new date is invalid
+        if self.mDay > utils.daysInMonth(self.mMonth, self.mYear):
+            self.mDay = utils.daysInMonth(self.mMonth, self.mYear)
+
+
+    def getLeapMonth(self):
+        return False
+
+
     def getDay(self):
         return self.mDay
 
@@ -444,16 +465,22 @@
         # What day does the current year start on, and diff that with the current day
         temp = DateTime(year=self.mYear, month=1, day=1)
         first_day = temp.getDayOfWeek()
+        if first_day == 0:
+            first_day = 7
         current_day = self.getDayOfWeek()
+        if current_day == 0:
+            current_day = 7
 
         # Calculate and set yearday for start of week. The first week is the one that contains at least
         # four days (with week start defaulting to MONDAY), so that means the 1st of January would fall
         # on MO, TU, WE, TH.
         if first_day in (DateTime.MONDAY, DateTime.TUESDAY, DateTime.WEDNESDAY, DateTime.THURSDAY):
-            year_day = (weekno - 1) * 7 + current_day - first_day
+            offset = 0
         else:
-            year_day = weekno * 7 + current_day - first_day
+            offset = 1
 
+        year_day = (weekno - 1 + offset) * 7 + current_day - first_day
+
         # It is possible we have a negative offset which means go back to the prior year as part of
         # week #1 exists at the end of that year.
         if year_day < 0:
@@ -1143,6 +1170,16 @@
         jobject.append(self.getJSONText())
 
 
+    # When doing recurrence iteration we sometimes need to preserve an invalid value for
+    # either day or month (though month is never invalid for Gregorian calendars it can
+    # be for non-Gregorian). For this class we simply set the stored attributes to their
+    # invalid values.
+    def setInvalid(self, year, month, day, isleapmonth=False):
+        self.mYear = year
+        self.mMonth = month
+        self.mDay = day
+
+
     def invalid(self):
         """
         Are any of the current fields invalid.
@@ -1157,6 +1194,32 @@
         return False
 
 
+    def invalidSkip(self, skip):
+        """
+        If this is an invalid value skip backward or forward or not at all.
+
+        @param skip: the skip mode (yes, backward, forward)
+        @type skip: L{int}
+        """
+
+        if self.invalid():
+            if skip == definitions.eRecurrence_SKIP_YES:
+                # Leave it as invalid
+                pass
+            elif skip == definitions.eRecurrence_SKIP_BACKWARD:
+                if self.mDay <= 0:
+                    self.mDay = 1
+                    self.offsetDay(-1)
+                else:
+                    self.mDay = utils.daysInMonth(self.mMonth, self.mYear)
+            elif skip == definitions.eRecurrence_SKIP_FORWARD:
+                if self.mDay <= 0:
+                    self.mDay = 1
+                else:
+                    self.mDay = utils.daysInMonth(self.mMonth, self.mYear)
+                    self.offsetDay(1)
+
+
     def normalise(self):
         # Normalise seconds
         normalised_secs = self.mSeconds % 60

Modified: PyCalendar/trunk/src/pycalendar/icalendar/definitions.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/icalendar/definitions.py	2014-12-01 15:13:17 UTC (rev 14190)
+++ PyCalendar/trunk/src/pycalendar/icalendar/definitions.py	2014-12-01 15:15:13 UTC (rev 14191)
@@ -230,6 +230,8 @@
 eRecurrence_BYMONTH = 11
 eRecurrence_BYSETPOS = 12
 eRecurrence_WKST = 13
+eRecurrence_RSCALE = 14
+eRecurrence_SKIP = 15
 
 cICalValue_RECUR_FREQ = "FREQ"
 cICalValue_RECUR_FREQ_LEN = 5
@@ -256,6 +258,8 @@
 cICalValue_RECUR_BYMONTH = "BYMONTH"
 cICalValue_RECUR_BYSETPOS = "BYSETPOS"
 cICalValue_RECUR_WKST = "WKST"
+cICalValue_RECUR_RSCALE = "RSCALE"
+cICalValue_RECUR_SKIP = "SKIP"
 
 eRecurrence_WEEKDAY_SU = 0
 eRecurrence_WEEKDAY_MO = 1
@@ -273,6 +277,14 @@
 cICalValue_RECUR_WEEKDAY_FR = "FR"
 cICalValue_RECUR_WEEKDAY_SA = "SA"
 
+eRecurrence_SKIP_YES = 0
+eRecurrence_SKIP_BACKWARD = 1
+eRecurrence_SKIP_FORWARD = 2
+
+cICalValue_RECUR_SKIP_YES = "YES"
+cICalValue_RECUR_SKIP_BACKWARD = "BACKWARD"
+cICalValue_RECUR_SKIP_FORWARD = "FORWARD"
+
 #     5545 Section 3.8.1.11
 eStatus_VEvent_None = 0
 eStatus_VEvent_Confirmed = 1

Added: PyCalendar/trunk/src/pycalendar/icalendar/icudatetime.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/icalendar/icudatetime.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/icalendar/icudatetime.py	2014-12-01 15:15:13 UTC (rev 14191)
@@ -0,0 +1,1005 @@
+##
+#    Copyright (c) 2014 Cyrus Daboo. All rights reserved.
+#
+#    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.
+##
+
+from cffi import FFI
+from __builtin__ import classmethod
+from pycalendar.datetime import DateTime
+from pycalendar.icalendar import definitions
+
+# Use cffi to get access to libicucore functions and constants
+ffi = FFI()
+hdr = """
+    //#define U_FAILURE(x) ((x)>U_ZERO_ERROR)
+
+    typedef double UDate;
+    UDate     ucal_getNow (void);
+
+    typedef void *        UCalendar;
+    typedef uint16_t    UChar;
+    enum      UCalendarType { UCAL_TRADITIONAL=0, UCAL_DEFAULT=0, UCAL_GREGORIAN, ... };
+    typedef enum UCalendarType UCalendarType;
+    enum UErrorCode {
+        U_ZERO_ERROR = 0
+    };
+    typedef enum UErrorCode UErrorCode;
+
+    enum UCalendarDaysOfWeek {
+      /** Sunday */
+      UCAL_SUNDAY = 1,
+      /** Monday */
+      UCAL_MONDAY,
+      /** Tuesday */
+      UCAL_TUESDAY,
+      /** Wednesday */
+      UCAL_WEDNESDAY,
+      /** Thursday */
+      UCAL_THURSDAY,
+      /** Friday */
+      UCAL_FRIDAY,
+      /** Saturday */
+      UCAL_SATURDAY
+    };
+
+    typedef enum UCalendarDaysOfWeek UCalendarDaysOfWeek;
+
+    enum UCalendarDateFields {
+      UCAL_ERA,
+      UCAL_YEAR,
+      UCAL_MONTH,
+      UCAL_WEEK_OF_YEAR,
+      UCAL_WEEK_OF_MONTH,
+      UCAL_DATE,
+      UCAL_DAY_OF_YEAR,
+      UCAL_DAY_OF_WEEK,
+      UCAL_DAY_OF_WEEK_IN_MONTH,
+      UCAL_AM_PM,
+      UCAL_HOUR,
+      UCAL_HOUR_OF_DAY,
+      UCAL_MINUTE,
+      UCAL_SECOND,
+      UCAL_MILLISECOND,
+      UCAL_ZONE_OFFSET,
+      UCAL_DST_OFFSET,
+      UCAL_YEAR_WOY,
+      UCAL_DOW_LOCAL,
+      UCAL_EXTENDED_YEAR,
+      UCAL_JULIAN_DAY,
+      UCAL_MILLISECONDS_IN_DAY,
+      UCAL_IS_LEAP_MONTH,
+      UCAL_FIELD_COUNT,
+      UCAL_DAY_OF_MONTH=5
+      };
+
+    typedef enum UCalendarDateFields UCalendarDateFields;
+
+    enum UCalendarMonths {
+      /** January */
+      UCAL_JANUARY,
+      /** February */
+      UCAL_FEBRUARY,
+      /** March */
+      UCAL_MARCH,
+      /** April */
+      UCAL_APRIL,
+      /** May */
+      UCAL_MAY,
+      /** June */
+      UCAL_JUNE,
+      /** July */
+      UCAL_JULY,
+      /** August */
+      UCAL_AUGUST,
+      /** September */
+      UCAL_SEPTEMBER,
+      /** October */
+      UCAL_OCTOBER,
+      /** November */
+      UCAL_NOVEMBER,
+      /** December */
+      UCAL_DECEMBER,
+      /** Value of the <code>UCAL_MONTH</code> field indicating the
+        * thirteenth month of the year. Although the Gregorian calendar
+        * does not use this value, lunar calendars do.
+        */
+      UCAL_UNDECIMBER
+    };
+    typedef enum UCalendarMonths UCalendarMonths;
+
+    UCalendar * ucal_open(const UChar *zoneID, int32_t len, const char *locale, UCalendarType type, UErrorCode *status);
+    void        ucal_close(UCalendar *cal);
+    UCalendar * ucal_clone(const UCalendar* cal, UErrorCode* status);
+    void        ucal_setDate(UCalendar *cal, int32_t year, int32_t month, int32_t date, UErrorCode *status);
+
+    const char* ucal_getTZDataVersion (UErrorCode *status);
+
+    UDate ucal_getMillis(const UCalendar* cal,
+               UErrorCode* status);
+    void  ucal_setMillis(UCalendar* cal,
+               UDate        dateTime,
+               UErrorCode*  status);
+
+    int32_t ucal_get(const UCalendar* cal,
+         UCalendarDateFields  field,
+         UErrorCode*          status);
+    void ucal_set(UCalendar*  cal,
+         UCalendarDateFields  field,
+         int32_t              value);
+    void ucal_add(UCalendar*  cal,
+         UCalendarDateFields  field,
+         int32_t              amount,
+         UErrorCode*          status);
+
+    enum UCalendarLimitType {
+      /** Minimum value */
+      UCAL_MINIMUM,
+      /** Maximum value */
+      UCAL_MAXIMUM,
+      /** Greatest minimum value */
+      UCAL_GREATEST_MINIMUM,
+      /** Leaest maximum value */
+      UCAL_LEAST_MAXIMUM,
+      /** Actual minimum value */
+      UCAL_ACTUAL_MINIMUM,
+      /** Actual maximum value */
+      UCAL_ACTUAL_MAXIMUM
+    };
+
+    typedef enum UCalendarLimitType UCalendarLimitType;
+
+    int32_t ucal_getLimit(const UCalendar*     cal,
+                  UCalendarDateFields  field,
+                  UCalendarLimitType   type,
+                  UErrorCode*          status);
+"""
+
+ffi.cdef(hdr)
+ffi.verify(hdr.replace(", ...", ""))
+
+ICU = ffi.dlopen("libicucore")
+
+class ICUDateTime(object):
+    """
+    An ICU-based L{DateTime} like class that supports non-Gregorian date-time values and arithmetic.
+    """
+
+    RSCALE_GREGORIAN = "gregorian"
+    RSCALE_HEBREW = "hebrew"
+
+    RSCALE_CALCODE = {
+        "gregorian": "",
+        "chinese": "C",
+        "islamic-civil": "I",
+        "hebrew": "H",
+        "ethiopic": "E",
+    }
+
+    def __init__(self, rscale, ucal):
+        """
+        Initialize using an ICU C{ucal} object and the name of the calendar scale.
+
+        @param rscale: calendar scale being used
+        @type rscale: L{str}
+        @param ucal: ICU ucal object
+        @type ucal: L{ICU.UCalendar*}
+        """
+        self.rscale = rscale
+        self.ucal = ucal
+
+        self.mHours = 0
+        self.mMinutes = 0
+        self.mSeconds = 0
+
+        self.mDateOnly = True
+
+        self.mTZUTC = False
+        self.mTZID = None
+        self.mTZOffset = None
+
+        self.mInvalid = None
+
+
+    def __del__(self):
+        """
+        Always close the ICU C{ucal} object.
+        """
+        ICU.ucal_close(self.ucal)
+        self.ucal = None
+
+
+    def duplicate(self):
+        """
+        Duplicate this object.
+        """
+
+        error = ffi.new("UErrorCode *", 0)
+        clone = ICU.ucal_clone(self.ucal, error)
+        dup = ICUDateTime(self.rscale, clone)
+        dup._transferHHMMSS(self, dup)
+        dup.mInvalid = self.mInvalid
+
+        return dup
+
+
+    def __repr__(self):
+        return "ICUDateTime: %s" % (self.getText(),)
+
+
+    def __hash__(self):
+        return hash(self.getPosixTime())
+
+
+    @classmethod
+    def fromDateTime(cls, dt, rscale):
+        """
+        Convert from a regular L{DateTime} to the specified calendar scale.
+
+        @param dt: the regular value to convert from
+        @type dt: L{DateTime}
+        @param rscale: the calendar scale to convert to
+        @type rscale: L{str}
+
+        @return: the new ICU object
+        @rtyope: L{ICUDateTime}
+        """
+
+        # Try to create the ICU object that represents this date
+        gregorian = cls.fromDateComponents(cls.RSCALE_GREGORIAN, dt.getYear(), dt.getMonth(), dt.getDay())
+        cls._transferHHMMSS(dt, gregorian)
+        return gregorian.convertTo(rscale)
+
+
+    def toDateTime(self):
+        """
+        Convert to a regular L{DateTime}.
+
+        @return: the converted object
+        @rtype: L{DateTime}
+        """
+
+        # Try to create the ICU object that represents this date
+        gregorian = self if self.rscale.lower() == self.RSCALE_GREGORIAN else self.convertTo(self.RSCALE_GREGORIAN)
+        dt = DateTime(gregorian.getYear(), gregorian.getMonth(), gregorian.getDay())
+        self._transferHHMMSS(self, dt)
+        return dt
+
+
+    @classmethod
+    def _newUcal(cls, rscale):
+        """
+        Create an ICU C{ucal} object for the specified calendar scale.
+
+        @param rscale: calendar scale to use
+        @type rscale: L{str}
+
+        @return: the ICU ucal object
+        @rtype: L{ICU.UCalendar*}
+        """
+        calsystem = "*@calendar={}".format(rscale)
+        error = ffi.new("UErrorCode *", 0)
+        ucal = ICU.ucal_open(ffi.NULL, -1, ffi.new("char[]", calsystem), ICU.UCAL_DEFAULT, error)
+        if error[0] != ICU.U_ZERO_ERROR:
+            raise ValueError("Unable to create ICU calendar for rscale '{}', code: {}".format(rscale, error))
+        return ucal
+
+
+    @classmethod
+    def fromDateComponents(cls, rscale, year, month, day, isleapmonth=False):
+        """
+        Create ICU calendar for the specified calendar scale with the specified components.
+
+        @param dt: the regular value to convert from
+        @type dt: L{DateTime}
+        @param rscale: the calendar scale to convert to
+        @type rscale: L{str}
+        @param year: the year component
+        @type year: L{int}
+        @param month: the month component
+        @type month: L{int}
+        @param day: the day component
+        @type day: L{int}
+        @param isleapmonth: the leap month component
+        @type isleapmonth: L{bool}
+
+        @return: the new object
+        @rtype: L{ICUDateTime}
+        """
+
+        # Try to create the ICU object that represents this date
+        ucal = cls._newUcal(rscale)
+
+        month, isleapmonth = cls._adjustToICULeapMonth(rscale, month, isleapmonth)
+
+        ICU.ucal_set(ucal, ICU.UCAL_EXTENDED_YEAR, year)
+        ICU.ucal_set(ucal, ICU.UCAL_MONTH, cls._numericMonthToICU(month))
+        ICU.ucal_set(ucal, ICU.UCAL_DAY_OF_MONTH, day)
+        ICU.ucal_set(ucal, ICU.UCAL_IS_LEAP_MONTH, isleapmonth)
+
+        return ICUDateTime(rscale, ucal)
+
+
+    @classmethod
+    def _numericMonthToICU(cls, month):
+        """
+        Map our month numbers (1..13) to ICU constants.
+
+        @param month: the month to map
+        @type month: L{int}
+
+        @return: the ICU constant
+        @rtype: L{ICU.UCalendarMonths}
+        """
+        return {
+            1: ICU.UCAL_JANUARY,
+            2: ICU.UCAL_FEBRUARY,
+            3: ICU.UCAL_MARCH,
+            4: ICU.UCAL_APRIL,
+            5: ICU.UCAL_MAY,
+            6: ICU.UCAL_JUNE,
+            7: ICU.UCAL_JULY,
+            8: ICU.UCAL_AUGUST,
+            9: ICU.UCAL_SEPTEMBER,
+            10: ICU.UCAL_OCTOBER,
+            11: ICU.UCAL_NOVEMBER,
+            12: ICU.UCAL_DECEMBER,
+            13: ICU.UCAL_UNDECIMBER,
+        }[month]
+
+
+    @classmethod
+    def _icuToNumericMonth(cls, month):
+        """
+        Map ICU constants to our month numbers (1..13).
+
+        @param month: the ICU constant to map
+        @type month: L{ICU.UCalendarMonths}
+
+        @return: the month
+        @rtype: L{int}
+        """
+        return {
+            ICU.UCAL_JANUARY: 1,
+            ICU.UCAL_FEBRUARY: 2,
+            ICU.UCAL_MARCH: 3,
+            ICU.UCAL_APRIL: 4,
+            ICU.UCAL_MAY: 5,
+            ICU.UCAL_JUNE: 6,
+            ICU.UCAL_JULY: 7,
+            ICU.UCAL_AUGUST: 8,
+            ICU.UCAL_SEPTEMBER: 9,
+            ICU.UCAL_OCTOBER: 10,
+            ICU.UCAL_NOVEMBER: 11,
+            ICU.UCAL_DECEMBER: 12,
+            ICU.UCAL_UNDECIMBER: 13,
+        }[month]
+
+
+    @classmethod
+    def _adjustToICULeapMonth(cls, rscale, month, isleapmonth):
+        """
+        For the Hebrew calendar, ICU uses a count of 13 months rather than 12 months
+        plus an "isleapmonth" indicator. So when converting to/from ICU we need to make
+        that adjustment as we always use 12 months + isleapmonth. This method converts
+        from our internal representation to what ICU uses.
+
+        @param rscale: calendar scale to convert to
+        @type rscale: L{str}
+        @param month: month number (12 month cycle)
+        @type month: L{int}
+        @param isleapmonth: is leap month indicator
+        @type isleapmonth: L{bool} of L{None}
+
+        @return: a tuple of the ICU-mapped month number and isleapmonth indicator
+        @rtype: L{tuple} of (L{int}, L{bool}
+        """
+
+        if rscale.lower() == cls.RSCALE_HEBREW:
+            if month == 5 and isleapmonth:
+                month = 6
+                isleapmonth = None
+            elif month >= 6:
+                month += 1
+        return (month, isleapmonth,)
+
+
+    @classmethod
+    def _adjustFromICULeapMonth(cls, rscale, month, isleapmonth):
+        """
+        For the Hebrew calendar, ISU uses a count of 13 months rather than 12 months
+        plus an "isleapmonth" indicator. So when converting to/from ICU we need to make
+        that adjustment as we always use 12 months + isleapmonth. This method converts
+        to our internal representation from what ICU uses.
+
+        @param rscale: calendar scale to convert from
+        @type rscale: L{str}
+        @param month: month number (13 month cycle)
+        @type month: L{int}
+        @param isleapmonth: is leap month indicator
+        @type isleapmonth: L{bool} of L{None}
+
+        @return: a tuple of the month number and isleapmonth indicator
+        @rtype: L{tuple} of (L{int}, L{bool}
+        """
+
+        if rscale.lower() == cls.RSCALE_HEBREW:
+            isleapmonth = False
+            if month == 6:
+                isleapmonth = True
+            elif month >= 6:
+                month -= 1
+        return (month, isleapmonth,)
+
+
+    @classmethod
+    def _transferHHMMSS(cls, from_dt, to_dt):
+        """
+        Transfer the time and timezone components from one L{ICUDateTime} to another.
+
+        @param from_dt: object to copy from
+        @type from_dt: L{ICUDateTime}
+        @param to_dt: object to copy to
+        @type to_dt: L{ICUDateTime}
+        """
+        if not from_dt.isDateOnly():
+            to_dt.setDateOnly(False)
+            to_dt.setHHMMSS(from_dt.getHours(), from_dt.getMinutes(), from_dt.getSeconds())
+            to_dt.setTimezoneID(from_dt.getTimezoneID())
+            to_dt.setTimezoneUTC(from_dt.getTimezoneUTC())
+
+
+    def convertTo(self, rscale):
+        """
+        Convert this L{ICUDateTime} into another one in the specified calendar scale.
+
+        @param rscale: calendar scale to convert to
+        @type rscale: L{str}
+
+        @return: the converted date
+        @rtype: L{ICUDateTime}
+        """
+        error = ffi.new("UErrorCode *", 0)
+        converted = self._newUcal(rscale)
+        millis = ICU.ucal_getMillis(self.ucal, error)
+        ICU.ucal_setMillis(converted, millis, error)
+        dt = ICUDateTime(rscale, converted)
+        self._transferHHMMSS(self, dt)
+
+        # For some reason this is needed to properly setup all the fields. Without this, I have
+        # noticed that ucal_getLimit does not return the correct day of month limit for a Chinese
+        # calendar.
+        dt.getDateComponents()
+
+        return dt
+
+
+    def getDateComponents(self):
+        """
+        Get the year, month, day, isleapmonth components in our internal format from
+        this ICU date.
+
+        @return: the date components
+        @rtype: L{tuple} of (L{int}, L{int}, L{int}, L{bool})
+        """
+        year = self.getYear()
+        month = self.getMonth()
+        day = self.getDay()
+        isleapmonth = self.getLeapMonth()
+
+        month, isleapmonth = self._adjustFromICULeapMonth(self.rscale, month, isleapmonth)
+
+        return (year, month, day, isleapmonth,)
+
+
+    def getPosixTime(self):
+        """
+        Return an integer representing a standard offset in seconds from a specific
+        epoch. This is used for sorting similar object.
+        """
+
+        # Use the ICU "millis" for this.
+        error = ffi.new("UErrorCode *", 0)
+        return ICU.ucal_getMillis(self.ucal, error)
+
+
+    def isDateOnly(self):
+        return self.mDateOnly
+
+
+    def setDateOnly(self, date_only):
+        self.mDateOnly = date_only
+
+
+    def setYYMMDD(self, year, month, day, isleapmonth=False):
+        self.setYear(year)
+        self.setMonth(month, isleapmonth)
+        self.setDay(day)
+
+        self.testInvalid(year, month, day, isleapmonth)
+
+
+    def getYear(self):
+        error = ffi.new("UErrorCode *", 0)
+        return ICU.ucal_get(self.ucal, ICU.UCAL_EXTENDED_YEAR, error)
+
+
+    def setYear(self, year):
+        _ignore_old_year, old_month, old_day, old_isleapmonth = self.getDateComponents()
+        ICU.ucal_set(self.ucal, ICU.UCAL_EXTENDED_YEAR, year)
+        self.testInvalid(year, old_month, old_day, old_isleapmonth)
+
+
+    def offsetYear(self, diff_year):
+        """
+        Offset the ICU date year component by the specified amount.
+
+        @param diff_year: amount to offset
+        @type diff_year: L{int}
+        """
+        error = ffi.new("UErrorCode *", 0)
+        ICU.ucal_add(self.ucal, ICU.UCAL_EXTENDED_YEAR, diff_year, error)
+
+
+    def getMonth(self):
+        error = ffi.new("UErrorCode *", 0)
+        return self._icuToNumericMonth(ICU.ucal_get(self.ucal, ICU.UCAL_MONTH, error))
+
+
+    def setMonth(self, month, isleapmonth=False):
+        old_year, _ignore_old_month, old_day, _ignore_old_isleapmonth = self.getDateComponents()
+        ICU.ucal_set(self.ucal, ICU.UCAL_MONTH, self._numericMonthToICU(month))
+        ICU.ucal_set(self.ucal, ICU.UCAL_IS_LEAP_MONTH, isleapmonth)
+        self.testInvalid(old_year, month, old_day, isleapmonth)
+
+
+    def offsetMonth(self, diff_month):
+        """
+        Offset the ICU date month component by the specified amount.
+
+        @param diff_year: amount to offset
+        @type diff_year: L{int}
+        """
+        error = ffi.new("UErrorCode *", 0)
+        ICU.ucal_add(self.ucal, ICU.UCAL_MONTH, diff_month, error)
+
+
+    def getLeapMonth(self):
+        error = ffi.new("UErrorCode *", 0)
+        return ICU.ucal_get(self.ucal, ICU.UCAL_IS_LEAP_MONTH, error) != 0
+
+
+    def getDay(self):
+        error = ffi.new("UErrorCode *", 0)
+        return ICU.ucal_get(self.ucal, ICU.UCAL_DAY_OF_MONTH, error)
+
+
+    def setDay(self, day):
+        old_year, old_month, _ignore_old_day, old_isleapmonth = self.getDateComponents()
+        ICU.ucal_set(self.ucal, ICU.UCAL_DAY_OF_MONTH, day)
+        self.testInvalid(old_year, old_month, day, old_isleapmonth)
+
+
+    def offsetDay(self, diff_day):
+        """
+        Offset the ICU date month component by the specified amount.
+
+        @param diff_year: amount to offset
+        @type diff_year: L{int}
+        """
+        error = ffi.new("UErrorCode *", 0)
+        ICU.ucal_add(self.ucal, ICU.UCAL_DAY_OF_MONTH, diff_day, error)
+
+
+    def setYearDay(self, day, allow_invalid=False):
+
+        # Find the limit for the current year
+        error = ffi.new("UErrorCode *", 0)
+        limit = ICU.ucal_getLimit(self.ucal, ICU.UCAL_DAY_OF_YEAR, ICU.UCAL_ACTUAL_MAXIMUM, error)
+
+        if day > 0:
+            ICU.ucal_set(self.ucal, ICU.UCAL_DAY_OF_YEAR, min(day, limit))
+            if day > limit and allow_invalid:
+                self.setInvalid(self.getYear(), 1, day)
+            else:
+                self.clearInvalid()
+        elif day < 0:
+            offset = limit + day + 1
+            ICU.ucal_set(self.ucal, ICU.UCAL_DAY_OF_YEAR, max(offset, 1))
+            if offset <= 0 and allow_invalid:
+                self.setInvalid(self.getYear(), 1, day)
+            else:
+                self.clearInvalid()
+
+
+    def getYearDay(self):
+        error = ffi.new("UErrorCode *", 0)
+        return ICU.ucal_get(self.ucal, ICU.UCAL_DAY_OF_YEAR, error)
+
+
+    def setMonthDay(self, day, allow_invalid=False):
+
+        # Find the limit for the current year
+        error = ffi.new("UErrorCode *", 0)
+        limit = ICU.ucal_getLimit(self.ucal, ICU.UCAL_DAY_OF_MONTH, ICU.UCAL_ACTUAL_MAXIMUM, error)
+
+        if day > 0:
+            ICU.ucal_set(self.ucal, ICU.UCAL_DAY_OF_MONTH, min(day, limit))
+            if day > limit and allow_invalid:
+                y, m, _ignore_d, l = self.getDateComponents()
+                self.setInvalid(y, m, day, l)
+            else:
+                self.clearInvalid()
+
+        elif day < 0:
+            offset = limit + day + 1
+            ICU.ucal_set(self.ucal, ICU.UCAL_DAY_OF_MONTH, max(offset, 1))
+            if offset <= 0 and allow_invalid:
+                y, m, _ignore_d, l = self.getDateComponents()
+                self.setInvalid(y, m, day, l)
+            else:
+                self.clearInvalid()
+
+
+    def isMonthDay(self, day):
+        if day > 0:
+            return self.getDay() == day
+        elif day < 0:
+            error = ffi.new("UErrorCode *", 0)
+            limit = ICU.ucal_getLimit(self.ucal, ICU.UCAL_DAY_OF_MONTH, ICU.UCAL_ACTUAL_MAXIMUM, error)
+            return self.getDay() - 1 - limit == day
+        else:
+            return False
+
+
+    def setWeekNo(self, weekno):
+        """
+        Set the current date to one with the same day of the week in the current year with the
+        specified week number. Note this might cause the year to shift backwards or forwards
+        if the date is at the boundary between two years.
+
+        @param weekno: the week number to set (currently must be positive)
+        @type weekno: C{int}
+        """
+
+        # Only supported for Gregorian calendars
+        if self.rscale.lower() != self.RSCALE_GREGORIAN:
+            raise ValueError("Week numbers only supported for Gregorian calendars")
+        dt = self.toDateTime()
+        dt.setWeekNo(weekno)
+        self.setYYMMDD(dt.getYear(), dt.getMonth(), dt.getDay())
+
+
+    def getWeekNo(self):
+        """
+        Return the ISO week number for the current date.
+        """
+
+        # Only supported for Gregorian calendars
+        if self.rscale.lower() != self.RSCALE_GREGORIAN:
+            raise ValueError("Week numbers only supported for Gregorian calendars")
+        dt = self.toDateTime()
+        return dt.getWeekNo()
+
+
+    def isWeekNo(self, weekno):
+        # This is the iso 8601 week number definition
+
+        if weekno > 0:
+            return self.getWeekNo() == weekno
+        else:
+            # This needs to calculate the negative offset from the last week in
+            # the current year
+            return False
+
+
+    def setDayOfWeekInYear(self, offset, day):
+        # Set to first day in year
+        self.setYYMMDD(self.getYear(), 1, 1, False)
+
+        # Determine first weekday in year
+        first_day = self.getDayOfWeek()
+
+        if offset > 0:
+            cycle = (offset - 1) * 7 + day
+            cycle -= first_day
+            if first_day > day:
+                cycle += 7
+            self.offsetDay(cycle)
+        elif offset < 0:
+            # Find the limit for the current year
+            error = ffi.new("UErrorCode *", 0)
+            limit = ICU.ucal_getLimit(self.ucal, ICU.UCAL_DAY_OF_YEAR, ICU.UCAL_ACTUAL_MAXIMUM, error)
+
+            first_day += limit - 1
+            first_day %= 7
+
+            cycle = (-offset - 1) * 7 - day
+            cycle += first_day
+            if day > first_day:
+                cycle += 7
+            self.offsetDay(limit - cycle - 1)
+
+        self.clearInvalid()
+
+
+    def setDayOfWeekInMonth(self, offset, day, allow_invalid=False):
+        # Set to first day in month
+        y, m, d, l = self.getDateComponents()
+        self.setYYMMDD(y, m, 1, l)
+
+        # Determine first weekday in month
+        first_day = self.getDayOfWeek()
+
+        if offset > 0:
+            cycle = (offset - 1) * 7 + day
+            cycle -= first_day
+            if first_day > day:
+                cycle += 7
+            mday = cycle + 1
+            self.offsetDay(cycle)
+        elif offset < 0:
+            # Find the limit for the current year
+            error = ffi.new("UErrorCode *", 0)
+            days_in_month = ICU.ucal_getLimit(self.ucal, ICU.UCAL_DAY_OF_MONTH, ICU.UCAL_ACTUAL_MAXIMUM, error)
+
+            first_day += days_in_month - 1
+            first_day %= 7
+
+            cycle = (-offset - 1) * 7 - day
+            cycle += first_day
+            if day > first_day:
+                cycle += 7
+            mday = days_in_month - cycle
+            self.offsetDay(days_in_month - cycle - 1)
+
+        if self.getDay() != mday and allow_invalid:
+            self.setInvalid(y, m, d, l)
+        else:
+            self.clearInvalid()
+
+
+    def isDayOfWeekInMonth(self, offset, day):
+        # First of the actual day must match
+        if self.getDayOfWeek() != day:
+            return False
+
+        # If there is no count the we match any of this day in the month
+        if offset == 0:
+            return True
+
+        # Create temp date-time with the appropriate parameters and then
+        # compare
+        temp = self.duplicate()
+        temp.setDayOfWeekInMonth(offset, day)
+
+        # Now compare dates
+        return self.getDateComponents() == temp.getDateComponents()
+
+
+    def getDayOfWeek(self):
+        error = ffi.new("UErrorCode *", 0)
+        return ICU.ucal_get(self.ucal, ICU.UCAL_DAY_OF_WEEK, error) - 1
+
+
+    def setHHMMSS(self, hours, minutes, seconds):
+        if (self.mHours != hours) or (self.mMinutes != minutes) or (self.mSeconds != seconds):
+            self.mHours = hours
+            self.mMinutes = minutes
+            self.mSeconds = seconds
+
+
+    def getHours(self):
+        return self.mHours
+
+
+    def setHours(self, hours):
+        if self.mHours != hours:
+            self.mHours = hours
+
+
+    def offsetHours(self, diff_hour):
+        self.mHours += diff_hour
+        self.normalise()
+
+
+    def getMinutes(self):
+        return self.mMinutes
+
+
+    def setMinutes(self, minutes):
+        if self.mMinutes != minutes:
+            self.mMinutes = minutes
+
+
+    def offsetMinutes(self, diff_minutes):
+        self.mMinutes += diff_minutes
+        self.normalise()
+
+
+    def getSeconds(self):
+        return self.mSeconds
+
+
+    def setSeconds(self, seconds):
+        if self.mSeconds != seconds:
+            self.mSeconds = seconds
+
+
+    def offsetSeconds(self, diff_seconds):
+        self.mSeconds += diff_seconds
+        self.normalise()
+
+
+    def getTimezoneUTC(self):
+        return self.mTZUTC
+
+
+    def setTimezoneUTC(self, utc):
+        if self.mTZUTC != utc:
+            self.mTZUTC = utc
+
+
+    def getTimezoneID(self):
+        return self.mTZID
+
+
+    def setTimezoneID(self, tzid):
+        self.mTZUTC = False
+        self.mTZID = tzid
+
+
+    # When doing recurrence iteration we sometimes need to preserve an invalid value for
+    # either day or month (though month is never invalid for Gregorian calendars it can
+    # be for non-Gregorian). For this class we simply set the stored attributes to their
+    # invalid values.
+    def setInvalid(self, year, month, day, isleapmonth=False):
+        self.mInvalid = (year, month, day, isleapmonth,)
+
+
+    def testInvalid(self, year, month, day, isleapmonth=False):
+        """
+        If the requested set of YYMMDDLL does not match the current set of YYMMDDLL then the requested
+        set was invalid.
+        """
+        components = self.getDateComponents()
+        if components != (year, month, day, isleapmonth,):
+            self.setInvalid(year, month, day, isleapmonth)
+        else:
+            self.clearInvalid()
+
+
+    def clearInvalid(self):
+        self.mInvalid = None
+
+
+    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.
+
+        return self.mInvalid is not None
+
+
+    def invalidSkip(self, skip):
+        """
+        If this is an invalid value skip backward or forward or not at all.
+
+        @param skip: the skip mode (yes, backward, forward)
+        @type skip: L{int}
+        """
+
+        if self.mInvalid:
+            if skip == definitions.eRecurrence_SKIP_YES:
+                # Leave it as invalid
+                pass
+            else:
+                # Need to determine which component (day or month/leap) is invalid,
+                # and react accordingly
+                _ignore_y, m, d, l = self.getDateComponents()
+                if (m, l) != (self.mInvalid[1], self.mInvalid[3]):
+                    # Month/leap is invalid
+                    if skip == definitions.eRecurrence_SKIP_BACKWARD:
+                        # Defaults to skip backward
+                        pass
+                    elif skip == definitions.eRecurrence_SKIP_FORWARD:
+                        self.offsetDay(1)
+
+                elif d != self.mInvalid[2]:
+                    if skip == definitions.eRecurrence_SKIP_BACKWARD:
+                        if self.mInvalid[2] < 1:
+                            self.offsetDay(-1)
+                    elif skip == definitions.eRecurrence_SKIP_FORWARD:
+                        if self.mInvalid[2] > 0:
+                            self.offsetDay(1)
+
+                self.clearInvalid()
+
+
+    def normalise(self):
+        # Normalise seconds
+        normalised_secs = self.mSeconds % 60
+        adjustment_mins = self.mSeconds / 60
+        if normalised_secs < 0:
+            normalised_secs += 60
+            adjustment_mins -= 1
+        self.mSeconds = normalised_secs
+        self.mMinutes += adjustment_mins
+
+        # Normalise minutes
+        normalised_mins = self.mMinutes % 60
+        adjustment_hours = self.mMinutes / 60
+        if normalised_mins < 0:
+            normalised_mins += 60
+            adjustment_hours -= 1
+        self.mMinutes = normalised_mins
+        self.mHours += adjustment_hours
+
+        # Normalise hours
+        normalised_hours = self.mHours % 24
+        adjustment_days = self.mHours / 24
+        if normalised_hours < 0:
+            normalised_hours += 24
+            adjustment_days -= 1
+        self.mHours = normalised_hours
+
+        self.offsetDay(adjustment_days)
+
+        # Wipe the time if date only
+        if self.mDateOnly:
+            self.mSeconds = self.mMinutes = self.mHours = 0
+
+
+    def getText(self):
+        """
+        Generate an ISO-8601 string representation of this ICU date. Use a code
+        prefix for the calendar scale.
+
+        @return: the ISO-8601 text
+        @rtype L{str}
+        """
+        calcode = self.RSCALE_CALCODE.get(self.rscale.lower(), "{}:".format(self.rscale))
+        if calcode:
+            calcode = "{{{}}}".format(calcode)
+        year, month, day, isleapmonth = self.getDateComponents()
+        date = "{}{:04d}{:02d}{}{:02d}".format(calcode, year, month, "L" if isleapmonth else "", day)
+        if not self.isDateOnly():
+            date += "T{:02d}{:02d}{:02d}{}".format(self.mHours, self.mMinutes, self.mSeconds, "Z" if self.mTZUTC else "")
+        return date
+
+
+if __name__ == '__main__':
+    newyear = ICUDateTime.fromDateComponents("chinese", 4651, 1, 1, False)
+    print("From: {} to {}".format(
+        newyear.getText(),
+        newyear.convertTo("gregorian").getText(),
+    ))
+
+    for i in range(0):
+        newyear.offsetDay(1)
+        print("From: {} to {}".format(
+            newyear.getText(),
+            newyear.convertTo("gregorian").getText(),
+        ))
+
+    offset = 1
+    greg = ICUDateTime.fromDateComponents("gregorian", 2014, 1, 31, False)
+    greg.offsetMonth(offset)
+    print(greg.getText())
+
+    greg = DateTime(2014, 1, 31)
+    greg.offsetMonth(offset)
+    print(greg.getText())

Added: PyCalendar/trunk/src/pycalendar/icalendar/recuriter.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/icalendar/recuriter.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/icalendar/recuriter.py	2014-12-01 15:15:13 UTC (rev 14191)
@@ -0,0 +1,151 @@
+##
+#    Copyright (c) 2014 Cyrus Daboo. All rights reserved.
+#
+#    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.
+##
+
+from pycalendar.icalendar import definitions
+from pycalendar.icalendar.icudatetime import ICUDateTime
+import collections
+
+class RecurrenceIterator(collections.Iterator):
+    """
+    An iterator that iterates a simple recurrence pattern.
+    """
+
+    def __init__(self, start, freq, interval, rscale=None, skip=definitions.eRecurrence_SKIP_YES, allow_invalid=False):
+        """
+        @param start: the start date-time
+        @type start: L{DateTime} or L{ICUDateTime}
+        @param freq: the frequency of iteration
+        @type freq: L{int}
+        @param interval: the interval for each iteration
+        @type interval: L{int}
+        @param rscale: calendar scale to apply recurrence pattern to
+        @type rscale: L{str}
+        @param skip: skipping behavior for invalid dates
+        @type skip: L{int}
+        @param allow_invalid: whether or not invalid values are allowed
+        @type allow_invalid: L{InvalidDateTime}
+        """
+        self.start = start
+        self.freq = freq
+        self.interval = interval
+        self.rscale = rscale
+        self.skip = skip
+        self.allow_invalid = allow_invalid
+
+        self.step = 0
+
+        # If an RSCALE is set, the C{self.start} value is a normal L{DateTime} object but we want
+        # to have the recurrence apply to the non-Gregorian calendar. So convert the C{self.start}
+        # value into the corresponding L{ICUDateTime} object.
+        if self.rscale:
+            self.start = ICUDateTime.fromDateTime(self.start, self.rscale)
+
+
+    def __iter__(self):
+        return self
+
+
+    def next(self):
+        """
+        Iterate one step of the recurrence. Always return an L{DateTime} for an rscale based
+        recurrence.
+
+        @return: the resulting date-time - this object is not re-used by the iterator so can be used
+            directly by the caller without any need to copy it
+        @rtype L{DateTime}
+        """
+
+        dt = self.nextraw()
+
+        # Always return the L{DateTime} equivalent when using C{self.rscale}
+        return dt.toDateTime() if self.rscale else dt
+
+
+    def nextraw(self):
+        """
+        Iterate one step of the recurrence using the native date-time calendar scale, and return
+        the native value.
+
+        @return: the resulting date-time - this object is not re-used by the iterator so can be used
+            directly by the caller without any need to copy it
+        @rtype L{DateTime} or L{ICUDateTime}
+        """
+
+        dt = self.start.duplicate()
+
+        # Add appropriate interval
+        if self.freq == definitions.eRecurrence_SECONDLY:
+            dt.offsetSeconds(self.step)
+        elif self.freq == definitions.eRecurrence_MINUTELY:
+            dt.offsetMinutes(self.step)
+        elif self.freq == definitions.eRecurrence_HOURLY:
+            dt.offsetHours(self.step)
+        elif self.freq == definitions.eRecurrence_DAILY:
+            dt.offsetDay(self.step)
+        elif self.freq == definitions.eRecurrence_WEEKLY:
+            dt.offsetDay(7 * self.step)
+        elif self.freq == definitions.eRecurrence_MONTHLY:
+            dt.offsetMonth(self.step)
+
+            # Check whether the day matches the start - if not we stepped
+            # to an invalid date so apply skip behavior
+            if dt.getDay() != self.start.getDay():
+                if self.allow_invalid:
+                    dt.setInvalid(dt.getYear(), dt.getMonth(), self.start.getDay(), dt.getLeapMonth())
+                elif self.skip == definitions.eRecurrence_SKIP_YES:
+                    # Iterate until we have a valid month
+                    while dt.getDay() != self.start.getDay():
+                        self.step += self.interval
+                        dt = self.start.duplicate()
+                        dt.offsetMonth(self.step)
+                elif self.skip == definitions.eRecurrence_SKIP_BACKWARD:
+                    # Both ICU and PyCalendar skip back by default
+                    pass
+                elif self.skip == definitions.eRecurrence_SKIP_FORWARD:
+                    # Go one day forward
+                    dt.offsetDay(1)
+
+        elif self.freq == definitions.eRecurrence_YEARLY:
+            dt.offsetYear(self.step)
+
+            # Check whether the month/day matches the start - if not we stepped
+            # to an invalid date so apply skip behavior
+            if dt.getDay() != self.start.getDay() or dt.getMonth() != self.start.getMonth() or dt.getLeapMonth() != self.start.getLeapMonth():
+                if self.allow_invalid:
+                    dt.setInvalid(dt.getYear(), self.start.getMonth(), self.start.getDay(), self.start.getLeapMonth())
+                elif self.skip == definitions.eRecurrence_SKIP_YES:
+                    # Iterate until we have a valid date-time
+                    while dt.getDay() != self.start.getDay() or dt.getMonth() != self.start.getMonth() or dt.getLeapMonth() != self.start.getLeapMonth():
+                        self.step += self.interval
+                        dt = self.start.duplicate()
+                        dt.offsetYear(self.step)
+                elif self.skip == definitions.eRecurrence_SKIP_BACKWARD:
+                    # Both ICU and PyCalendar skip back by default
+                    pass
+                elif self.skip == definitions.eRecurrence_SKIP_FORWARD:
+                    # Go one day forward
+                    dt.offsetDay(1)
+
+        self.step += self.interval
+
+        return dt
+
+
+if __name__ == '__main__':
+    icudt = ICUDateTime.fromDateComponents("gregorian", 2014, 1, 31)
+    iter = RecurrenceIterator(icudt, definitions.eRecurrence_MONTHLY, 1, definitions.eRecurrence_SKIP_BACKWARD)
+    for i in range(12):
+        print(iter.next().getText())

Modified: PyCalendar/trunk/src/pycalendar/icalendar/recurrence.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/icalendar/recurrence.py	2014-12-01 15:13:17 UTC (rev 14190)
+++ PyCalendar/trunk/src/pycalendar/icalendar/recurrence.py	2014-12-01 15:15:13 UTC (rev 14191)
@@ -21,6 +21,7 @@
 from pycalendar.valueutils import ValueMixin
 import cStringIO as StringIO
 import xml.etree.cElementTree as XML
+from pycalendar.icalendar.recuriter import RecurrenceIterator
 
 def WeekDayNumCompare_compare(w1, w2):
 
@@ -55,6 +56,8 @@
         definitions.cICalValue_RECUR_YEARLY   : definitions.eRecurrence_YEARLY,
     }
 
+    cFreqInverseMap = dict([(v, k) for k, v in cFreqMap.items()])
+
     cFreqToXMLMap = {
         definitions.eRecurrence_SECONDLY: xmldefinitions.recur_freq_secondly,
         definitions.eRecurrence_MINUTELY: xmldefinitions.recur_freq_minutely,
@@ -80,6 +83,8 @@
         definitions.cICalValue_RECUR_BYMONTH    : definitions.eRecurrence_BYMONTH,
         definitions.cICalValue_RECUR_BYSETPOS   : definitions.eRecurrence_BYSETPOS,
         definitions.cICalValue_RECUR_WKST       : definitions.eRecurrence_WKST,
+        definitions.cICalValue_RECUR_RSCALE     : definitions.eRecurrence_RSCALE,
+        definitions.cICalValue_RECUR_SKIP       : definitions.eRecurrence_SKIP,
     }
 
     cWeekdayMap = {
@@ -94,6 +99,20 @@
 
     cWeekdayRecurMap = dict([(v, k) for k, v in cWeekdayMap.items()])
 
+    cSkipMap = {
+        definitions.cICalValue_RECUR_SKIP_YES       : definitions.eRecurrence_SKIP_YES,
+        definitions.cICalValue_RECUR_SKIP_BACKWARD  : definitions.eRecurrence_SKIP_BACKWARD,
+        definitions.cICalValue_RECUR_SKIP_FORWARD   : definitions.eRecurrence_SKIP_FORWARD,
+    }
+
+    cSkipInverseMap = dict([(v, k) for k, v in cSkipMap.items()])
+
+    cSkipToXMLMap = {
+        definitions.eRecurrence_SKIP_YES: xmldefinitions.recur_skip_yes,
+        definitions.eRecurrence_SKIP_BACKWARD: xmldefinitions.recur_skip_backward,
+        definitions.eRecurrence_SKIP_FORWARD: xmldefinitions.recur_skip_forward,
+    }
+
     cUnknownIndex = -1
 
     def __init__(self):
@@ -103,6 +122,7 @@
     def duplicate(self):
         other = Recurrence()
 
+        other.mRscale = self.mRscale
         other.mFreq = self.mFreq
 
         other.mUseCount = self.mUseCount
@@ -112,6 +132,9 @@
             other.mUntil = self.mUntil.duplicate()
 
         other.mInterval = self.mInterval
+
+        other.mSkip = self.mSkip
+
         if self.mBySeconds is not None:
             other.mBySeconds = self.mBySeconds[:]
         if self.mByMinutes is not None:
@@ -145,6 +168,8 @@
 
 
     def init_Recurrence(self):
+
+        self.mRscale = None
         self.mFreq = definitions.eRecurrence_YEARLY
 
         self.mUseCount = False
@@ -154,6 +179,9 @@
         self.mUntil = None
 
         self.mInterval = 1
+
+        self.mSkip = None
+
         self.mBySeconds = None
         self.mByMinutes = None
         self.mByHours = None
@@ -174,12 +202,14 @@
 
     def __hash__(self):
         return hash((
+            self.mRscale,
             self.mFreq,
             self.mUseCount,
             self.mCount,
             self.mUseUntil,
             self.mUntil,
             self.mInterval,
+            self.mSkip,
             tuple(self.mBySeconds) if self.mBySeconds else None,
             tuple(self.mByMinutes) if self.mByMinutes else None,
             tuple(self.mByHours) if self.mByHours else None,
@@ -205,10 +235,12 @@
 
     def equals(self, comp):
         return (
-            (self.mFreq == comp.mFreq)
+            (self.mRscale == comp.mRscale)
+            and (self.mFreq == comp.mFreq)
             and (self.mUseCount == comp.mUseCount) and (self.mCount == comp.mCount)
             and (self.mUseUntil == comp.mUseUntil) and (self.mUntil == comp.mUntil)
             and (self.mInterval == comp.mInterval)
+            and (self.mSkip == comp.mSkip)
             and self.equalsNum(self.mBySeconds, comp.mBySeconds)
             and self.equalsNum(self.mByMinutes, comp.mByMinutes)
             and self.equalsNum(self.mByHours, comp.mByHours)
@@ -274,6 +306,14 @@
             setattr(self, attr, value)
 
 
+    def getRscale(self):
+        return self.mRscale
+
+
+    def setRscale(self, rscale):
+        self._setAndclearIfChanged("mRscale", rscale)
+
+
     def getFreq(self):
         return self.mFreq
 
@@ -322,6 +362,24 @@
         self._setAndclearIfChanged("mInterval", interval)
 
 
+    def getSkip(self):
+        return self.mSkip
+
+
+    def effectiveSkip(self):
+        """
+        The default skip value depends on whether RSCALE is used or not
+        """
+        if self.mSkip is None:
+            return definitions.eRecurrence_SKIP_YES if self.mRscale is None else definitions.eRecurrence_SKIP_BACKWARD
+        else:
+            return self.mSkip
+
+
+    def setSkip(self, skip):
+        self._setAndclearIfChanged("mSkip", skip)
+
+
     def getByMonth(self):
         return self.mByMonth
 
@@ -473,7 +531,7 @@
                 if self.mByMonth is not None:
                     raise ValueError("Recurrence: Only one BYMONTH allowed")
                 self.mByMonth = []
-                self.parseList(tvalue, self.mByMonth, 1, 12, errmsg="Recurrence: Invalid BYMONTH value")
+                self.parseMonthNumList(tvalue, self.mByMonth, 1, 12, errmsg="Recurrence: Invalid BYMONTH value")
 
             elif index == definitions.eRecurrence_BYSETPOS:
                 if self.mBySetPos is not None:
@@ -487,7 +545,21 @@
                     raise ValueError("Recurrence: Invalid WKST value")
                 self.mWeekstart = index
 
+            elif index == definitions.eRecurrence_RSCALE:
+                self.mRscale = tvalue.upper()
 
+            elif index == definitions.eRecurrence_SKIP:
+                # Get the SKIP value
+                index = Recurrence.cSkipMap.get(tvalue, Recurrence.cUnknownIndex)
+                if index == Recurrence.cUnknownIndex:
+                    raise ValueError("Recurrence: Invalid SKIP value")
+                self.mSkip = index
+
+        # Final validity checks
+        if self.mRscale is None and self.mSkip is not None:
+            raise ValueError("Recurrence: SKIP only allowed with RSCALE")
+
+
     def parseList(self, txt, list, min=None, max=None, allowNegative=False, errmsg=""):
 
         if "," in txt:
@@ -507,6 +579,33 @@
             list.append(value)
 
 
+    def parseMonthNumList(self, txt, list, min=None, max=None, allowNegative=False, errmsg=""):
+        """
+        Month numbers can include "L" leap month suffix.
+        """
+
+        if "," in txt:
+            tokens = txt.split(",")
+        else:
+            tokens = (txt,)
+
+        for token in tokens:
+            if token.endswith("L"):
+                suffix = True
+                token = token[:-1]
+            else:
+                suffix = False
+            value = int(token)
+            if not allowNegative and value < 0:
+                raise ValueError(errmsg)
+            avalue = abs(value)
+            if min is not None and avalue < min:
+                raise ValueError(errmsg)
+            if max is not None and avalue > max:
+                raise ValueError(errmsg)
+            list.append((value, suffix,))
+
+
     def parseListDW(self, txt, list, errmsg=""):
 
         if "," in txt:
@@ -542,30 +641,16 @@
 
     def generate(self, os):
         try:
+            if self.mRscale:
+                os.write(definitions.cICalValue_RECUR_RSCALE)
+                os.write("=")
+                os.write(self.mRscale.upper())
+                os.write(";")
+
             os.write(definitions.cICalValue_RECUR_FREQ)
             os.write("=")
+            os.write(self.cFreqInverseMap[self.mFreq])
 
-            if self.mFreq == definitions.eRecurrence_SECONDLY:
-                os.write(definitions.cICalValue_RECUR_SECONDLY)
-
-            elif self.mFreq == definitions.eRecurrence_MINUTELY:
-                os.write(definitions.cICalValue_RECUR_MINUTELY)
-
-            elif self.mFreq == definitions.eRecurrence_HOURLY:
-                os.write(definitions.cICalValue_RECUR_HOURLY)
-
-            elif self.mFreq == definitions.eRecurrence_DAILY:
-                os.write(definitions.cICalValue_RECUR_DAILY)
-
-            elif self.mFreq == definitions.eRecurrence_WEEKLY:
-                os.write(definitions.cICalValue_RECUR_WEEKLY)
-
-            elif self.mFreq == definitions.eRecurrence_MONTHLY:
-                os.write(definitions.cICalValue_RECUR_MONTHLY)
-
-            elif self.mFreq == definitions.eRecurrence_YEARLY:
-                os.write(definitions.cICalValue_RECUR_YEARLY)
-
             if self.mUseCount:
                 os.write(";")
                 os.write(definitions.cICalValue_RECUR_COUNT)
@@ -583,6 +668,12 @@
                 os.write("=")
                 os.write(str(self.mInterval))
 
+            if self.mSkip is not None:
+                os.write(";")
+                os.write(definitions.cICalValue_RECUR_SKIP)
+                os.write("=")
+                os.write(self.cSkipInverseMap[self.mSkip])
+
             self.generateList(os, definitions.cICalValue_RECUR_BYSECOND, self.mBySeconds)
             self.generateList(os, definitions.cICalValue_RECUR_BYMINUTE, self.mByMinutes)
             self.generateList(os, definitions.cICalValue_RECUR_BYHOUR, self.mByHours)
@@ -624,7 +715,7 @@
             self.generateList(os, definitions.cICalValue_RECUR_BYMONTHDAY, self.mByMonthDay)
             self.generateList(os, definitions.cICalValue_RECUR_BYYEARDAY, self.mByYearDay)
             self.generateList(os, definitions.cICalValue_RECUR_BYWEEKNO, self.mByWeekNo)
-            self.generateList(os, definitions.cICalValue_RECUR_BYMONTH, self.mByMonth)
+            self.generateMonthNumList(os, definitions.cICalValue_RECUR_BYMONTH, self.mByMonth)
             self.generateList(os, definitions.cICalValue_RECUR_BYSETPOS, self.mBySetPos)
 
             # MO is the default so we do not need it
@@ -672,10 +763,31 @@
                 os.write(str(e))
 
 
+    def generateMonthNumList(self, os, title, items):
+        """
+        Month numbers can include "L" leap month suffix.
+        """
+
+        if (items is not None) and (len(items) != 0):
+            os.write(";")
+            os.write(title)
+            os.write("=")
+            comma = False
+            for item in items:
+                if comma:
+                    os.write(",")
+                comma = True
+                os.write(str(item[0]) + ("L" if item[1] else ""))
+
+
     def writeXML(self, node, namespace):
 
         recur = XML.SubElement(node, xmlutils.makeTag(namespace, xmldefinitions.value_recur))
 
+        if self.mRscale:
+            freq = XML.SubElement(recur, xmlutils.makeTag(namespace, xmldefinitions.recur_rscale))
+            freq.text = self.mRscale
+
         freq = XML.SubElement(recur, xmlutils.makeTag(namespace, xmldefinitions.recur_freq))
         freq.text = self.cFreqToXMLMap[self.mFreq]
 
@@ -690,6 +802,10 @@
             interval = XML.SubElement(recur, xmlutils.makeTag(namespace, xmldefinitions.recur_interval))
             interval.text = str(self.mInterval)
 
+        if self.mSkip is not None:
+            skip = XML.SubElement(recur, xmlutils.makeTag(namespace, xmldefinitions.recur_skip))
+            skip.text = self.cSkipToXMLMap[self.mSkip]
+
         self.writeXMLList(recur, namespace, xmldefinitions.recur_bysecond, self.mBySeconds)
         self.writeXMLList(recur, namespace, xmldefinitions.recur_byminute, self.mByMinutes)
         self.writeXMLList(recur, namespace, xmldefinitions.recur_byhour, self.mByHours)
@@ -706,7 +822,7 @@
         self.writeXMLList(recur, namespace, xmldefinitions.recur_bymonthday, self.mByMonthDay)
         self.writeXMLList(recur, namespace, xmldefinitions.recur_byyearday, self.mByYearDay)
         self.writeXMLList(recur, namespace, xmldefinitions.recur_byweekno, self.mByWeekNo)
-        self.writeXMLList(recur, namespace, xmldefinitions.recur_bymonth, self.mByMonth)
+        self.writeXMLMonthNumList(recur, namespace, xmldefinitions.recur_bymonth, self.mByMonth)
         self.writeXMLList(recur, namespace, xmldefinitions.recur_bysetpos, self.mBySetPos)
 
         # MO is the default so we do not need it
@@ -722,6 +838,17 @@
                 child.text = str(item)
 
 
+    def writeXMLMonthNumList(self, node, namespace, name, items):
+        """
+        Month numbers can include "L" leap month suffix.
+        """
+
+        if items is not None and len(items) != 0:
+            for item in items:
+                child = XML.SubElement(node, xmlutils.makeTag(namespace, name))
+                child.text = str(item[0]) + ("L" if item[1] else "")
+
+
     def parseJSON(self, jobject):
         """
         jCal splits the value into components. We need to convert that back to the
@@ -750,6 +877,9 @@
         """
         jdict = {}
 
+        if self.mRscale:
+            jdict[xmldefinitions.recur_rscale] = self.mRscale
+
         jdict[xmldefinitions.recur_freq] = self.cFreqToXMLMap[self.mFreq]
 
         if self.mUseCount:
@@ -760,6 +890,9 @@
         if self.mInterval > 1:
             jdict[xmldefinitions.recur_interval] = self.mInterval
 
+        if self.mSkip is not None:
+            jdict[xmldefinitions.recur_skip] = self.cSkipToXMLMap[self.mSkip]
+
         if self.mBySeconds:
             jdict[xmldefinitions.recur_bysecond] = self.mBySeconds
         if self.mByMinutes:
@@ -784,7 +917,7 @@
         if self.mByWeekNo:
             jdict[xmldefinitions.recur_byweekno] = self.mByWeekNo
         if self.mByMonth:
-            jdict[xmldefinitions.recur_bymonth] = self.mByMonth
+            jdict[xmldefinitions.recur_bymonth] = [(str(item[0]) + "L") if item[1] else item[0] for item in self.mByMonth]
         if self.mBySetPos:
             jdict[xmldefinitions.recur_bysetpos] = self.mBySetPos
 
@@ -925,9 +1058,7 @@
         return limited
 
 
-    def simpleExpand(self, start, range, items, float_offset):
-        start_iter = start.duplicate()
-        ctr = 0
+    def simpleExpand(self, start, range, results, float_offset):
 
         if self.mUseUntil:
             float_until = self.mUntil.duplicate()
@@ -935,35 +1066,30 @@
                 float_until.setTimezoneID(0)
                 float_until.offsetSeconds(float_offset)
 
+        riter = RecurrenceIterator(start, self.mFreq, self.mInterval, self.mRscale, self.effectiveSkip())
         while True:
+            start_iter = riter.next()
+
             # Exit if after period we want
             if range.isDateAfterPeriod(start_iter):
                 return False
+            elif self.mUseUntil:
+                # Exit if next item is after until (it is OK if it is the same as
+                # UNTIL as UNTIL is inclusive)
+                if start_iter > float_until:
+                    return True
 
             # Add current one to list
-            items.append(start_iter.duplicate())
+            results.append(start_iter)
 
-            # Get next item
-            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:
-                # Bump counter and exit if over
-                ctr += 1
-                if ctr >= self.mCount:
+                # Exit if max count reached
+                if len(results) >= self.mCount:
                     return True
-            elif self.mUseUntil:
-                # Exit if next item is after until (its OK if its the same as
-                # UNTIL as UNTIL is inclusive)
-                if start_iter > float_until:
-                    return True
 
 
-    def complexExpand(self, start, range, items, float_offset):
-        start_iter = start.duplicate()
-        ctr = 0
+    def complexExpand(self, start, range, results, float_offset):
 
         if self.mUseUntil:
             float_until = self.mUntil.duplicate()
@@ -971,16 +1097,15 @@
                 float_until.setTimezoneID(None)
                 float_until.offsetSeconds(float_offset)
 
-        # Always add the initial instance DTSTART
-        if self.mUseCount:
-            # Bump counter and exit if over
-            ctr += 1
-            if ctr >= self.mCount:
-                return True
+        # Allow invalid values during the complex iteration as those may end up being coerced to a valid value
+        # when a BYxxx rule expands
+        riter = RecurrenceIterator(start, self.mFreq, self.mInterval, self.mRscale, self.effectiveSkip(), allow_invalid=True)
+        while True:
+            # Keep the iterated date-time value in its native calendar scale
+            start_iter = riter.nextraw()
 
-        # Need to re-initialise start based on BYxxx rules
-        while True:
-            # Behaviour is based on frequency
+            # Each recurrence cycle may generate multiple items based on the frequency and other rule parts,
+            # but we need to limit based on until and range and count
             set_items = []
 
             if self.mFreq == definitions.eRecurrence_SECONDLY:
@@ -1005,12 +1130,23 @@
                 self.generateYearlySet(start_iter, set_items)
 
             # Ignore if it is invalid
+            def _invalidMap(dt):
+                dt.invalidSkip(self.effectiveSkip())
+                return dt
+            set_items = map(lambda x: _invalidMap(x), set_items)
             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())
 
+            if (self.mBySetPos is not None) and (len(self.mBySetPos) != 0):
+                set_items[:] = self.bySetPosLimit(set_items)
+
+            # Remaining behavior requires L{DateTime} objects
+            if self.mRscale:
+                set_items[:] = map(lambda date: date.toDateTime(), set_items)
+
             # Process each one in the generated set
             for iter in set_items:
 
@@ -1027,34 +1163,22 @@
                     return False
 
                 # Exit if beyond the UNTIL limit
-                if self.mUseUntil:
+                elif self.mUseUntil:
                     # Exit if next item is after until (its OK if its the same
                     # as UNTIL as UNTIL is inclusive)
                     if iter > float_until:
                         return True
 
-                # Special for start instance
-                if (ctr == 1) and (start == iter):
-                    continue
-
                 # Add current one to list
-                items.append(iter)
+                results.append(iter)
 
                 # Check limits
                 if self.mUseCount:
-                    # Bump counter and exit if over
-                    ctr += 1
-                    if ctr >= self.mCount:
+                    # Exit if max count reached
+                    if len(results) >= self.mCount:
                         return True
 
-            # Exit if after period we want
-            if range.isDateAfterPeriod(start_iter):
-                return False
 
-            # Get next item
-            start_iter.recur(self.mFreq, self.mInterval, allow_invalid=True)
-
-
     def clear(self):
         self.mCached = False
         self.mFullyCached = False
@@ -1065,8 +1189,7 @@
     # IMPORTANT ExcludeFutureRecurrence assumes mCacheStart is setup with the
     # owning VEVENT's DTSTART
     # Currently this method is only called when a recurrence is being removed
-    # so
-    # the recurrence data should be cached
+    # so the recurrence data should be cached
 
     # Exclude dates on or after the chosen one
     def excludeFutureRecurrence(self, exclude):
@@ -1098,7 +1221,7 @@
         # All possible BYxxx are valid, though some combinations are not
 
         # Start with initial date-time
-        items.append(start.duplicate())
+        items.append(start)
 
         if (self.mByMonth is not None) and (len(self.mByMonth) != 0):
             items[:] = self.byMonthExpand(items)
@@ -1138,15 +1261,12 @@
         if (self.mBySeconds is not None) and (len(self.mBySeconds) != 0):
             items[:] = self.bySecondExpand(items)
 
-        if (self.mBySetPos is not None) and (len(self.mBySetPos) != 0):
-            items[:] = self.bySetPosLimit(items)
 
-
     def generateMonthlySet(self, start, items):
         # Cannot have BYYEARDAY and BYWEEKNO
 
         # Start with initial date-time
-        items.append(start.duplicate())
+        items.append(start)
 
         if (self.mByMonth is not None) and (len(self.mByMonth) != 0):
             # BYMONTH limits the range of possible values
@@ -1183,15 +1303,12 @@
         if ((self.mBySeconds is not None) and (len(self.mBySeconds) != 0)):
             items[:] = self.bySecondExpand(items)
 
-        if ((self.mBySetPos is not None) and (len(self.mBySetPos) != 0)):
-            items[:] = self.bySetPosLimit(items)
 
-
     def generateWeeklySet(self, start, items):
         # Cannot have BYYEARDAY and BYMONTHDAY
 
         # Start with initial date-time
-        items.append(start.duplicate())
+        items.append(start)
 
         if (self.mByMonth is not None) and (len(self.mByMonth) != 0):
             # BYMONTH limits the range of possible values
@@ -1220,15 +1337,12 @@
         if (self.mBySeconds is not None) and (len(self.mBySeconds) != 0):
             items[:] = self.bySecondExpand(items)
 
-        if (self.mBySetPos is not None) and (len(self.mBySetPos) != 0):
-            items[:] = self.bySetPosLimit(items)
 
-
     def generateDailySet(self, start, items):
         # Cannot have BYYEARDAY
 
         # Start with initial date-time
-        items.append(start.duplicate())
+        items.append(start)
 
         if (self.mByMonth is not None) and (len(self.mByMonth) != 0):
             # BYMONTH limits the range of possible values
@@ -1262,15 +1376,12 @@
         if (self.mBySeconds is not None) and (len(self.mBySeconds) != 0):
             items[:] = self.bySecondExpand(items)
 
-        if (self.mBySetPos is not None) and (len(self.mBySetPos) != 0):
-            items[:] = self.bySetPosLimit(items)
 
-
     def generateHourlySet(self, start, items):
         # Cannot have BYYEARDAY
 
         # Start with initial date-time
-        items.append(start.duplicate())
+        items.append(start)
 
         if (self.mByMonth is not None) and (len(self.mByMonth) != 0):
             # BYMONTH limits the range of possible values
@@ -1306,15 +1417,12 @@
         if (self.mBySeconds is not None) and (len(self.mBySeconds) != 0):
             items[:] = self.bySecondExpand(items)
 
-        if (self.mBySetPos is not None) and (len(self.mBySetPos) != 0):
-            items[:] = self.bySetPosLimit(items)
 
-
     def generateMinutelySet(self, start, items):
         # Cannot have BYYEARDAY
 
         # Start with initial date-time
-        items.append(start.duplicate())
+        items.append(start)
 
         if (self.mByMonth is not None) and (len(self.mByMonth) != 0):
             # BYMONTH limits the range of possible values
@@ -1352,15 +1460,12 @@
         if (self.mBySeconds is not None) and (len(self.mBySeconds) != 0):
             items[:] = self.bySecondExpand(items)
 
-        if (self.mBySetPos is not None) and (len(self.mBySetPos) != 0):
-            items[:] = self.bySetPosLimit(items)
 
-
     def generateSecondlySet(self, start, items):
         # Cannot have BYYEARDAY
 
         # Start with initial date-time
-        items.append(start.duplicate())
+        items.append(start)
 
         if (self.mByMonth is not None) and (len(self.mByMonth) != 0):
             # BYMONTH limits the range of possible values
@@ -1400,10 +1505,7 @@
             if (len(items) == 0):
                 return
 
-        if (self.mBySetPos is not None) and (len(self.mBySetPos) != 0):
-            items[:] = self.bySetPosLimit(items)
 
-
     def byMonthExpand(self, dates):
         # Loop over all input items
         output = []
@@ -1412,7 +1514,7 @@
             # insert into output
             for iter2 in self.mByMonth:
                 temp = iter1.duplicate()
-                temp.setMonth(iter2)
+                temp.setMonth(*iter2)
                 output.append(temp)
 
         return output
@@ -1581,125 +1683,40 @@
 
 
     def byMonthLimit(self, dates):
-        # Loop over all input items
-        output = []
-        for iter1 in dates:
-            # Loop over each BYMONTH and indicate keep if input month matches
-            keep = False
-            for iter2 in self.mByMonth:
-                keep = (iter1.getMonth() == iter2)
-                if keep:
-                    break
+        # Keep each date that matches a BYMONTH
+        return filter(lambda date: date.getMonth() in self.mByMonth, dates)
 
-            if keep:
-                output.append(iter1)
 
-        return output
-
-
     def byWeekNoLimit(self, dates):
-        # Loop over all input items
-        output = []
-        for iter1 in dates:
-            # Loop over each BYWEEKNO and indicate keep if input month matches
-            keep = False
-            for iter2 in self.mByWeekNo:
-                keep = iter1.isWeekNo(iter2)
-                if keep:
-                    break
+        # Keep each date that matches a BYWEEKNO
+        return filter(lambda date: any([date.isWeekNo(iter) for iter in self.mByWeekNo]), dates)
 
-            if keep:
-                output.append(iter1)
 
-        return output
-
-
     def byMonthDayLimit(self, dates):
-        # Loop over all input items
-        output = []
-        for iter1 in dates:
-            # Loop over each BYMONTHDAY and indicate keep if input month
-            # matches
-            keep = False
-            for iter2 in self.mByMonthDay:
-                keep = iter1.isMonthDay(iter2)
-                if keep:
-                    break
+        # Keep each date that matches a BYMONTHDAY
+        return filter(lambda date: any([date.isMonthDay(iter) for iter in self.mByMonthDay]), dates)
 
-            if keep:
-                output.append(iter1)
 
-        return output
-
-
     def byDayLimit(self, dates):
-        # Loop over all input items
-        output = []
-        for iter1 in dates:
-            # Loop over each BYDAY and indicate keep if input month matches
-            keep = False
-            for iter2 in self.mByDay:
-                keep = iter1.isDayOfWeekInMonth(iter2[0], iter2[1])
-                if keep:
-                    break
+        # Keep each date that matches a BYDAY
+        return filter(lambda date: any([date.isDayOfWeekInMonth(iter[0], iter[1]) for iter in self.mByDay]), dates)
 
-            if keep:
-                output.append(iter1)
 
-        return output
-
-
     def byHourLimit(self, dates):
-        # Loop over all input items
-        output = []
-        for iter1 in dates:
-            # Loop over each BYHOUR and indicate keep if input hour matches
-            keep = False
-            for iter2 in self.mByHours:
-                keep = (iter1.getHours() == iter2)
-                if keep:
-                    break
+        # Keep each date that matches a BYHOUR
+        return filter(lambda date: date.getHours() in self.mByHours, dates)
 
-            if keep:
-                output.append(iter1)
 
-        return output
-
-
     def byMinuteLimit(self, dates):
-        # Loop over all input items
-        output = []
-        for iter1 in dates:
-            # Loop over each BYMINUTE and indicate keep if input minute matches
-            keep = False
-            for iter2 in self.mByMinutes:
-                keep = (iter1.getMinutes() == iter2)
-                if keep:
-                    break
+        # Keep each date that matches a BYMINUTE
+        return filter(lambda date: date.getMinutes() in self.mByMinutes, dates)
 
-            if keep:
-                output.append(iter1)
 
-        return output
-
-
     def bySecondLimit(self, dates):
-        # Loop over all input items
-        output = []
-        for iter1 in dates:
-            # Loop over each BYSECOND and indicate keep if input second matches
-            keep = False
-            for iter2 in self.mBySeconds:
-                keep = (iter1.getSeconds() == iter2)
-                if keep:
-                    break
+        # Keep each date that matches a BYSECOND
+        return filter(lambda date: date.getSeconds() in self.mBySeconds, dates)
 
-            if keep:
-                output.append(iter1)
 
-        return output
-
-
     def bySetPosLimit(self, dates):
         # The input dates MUST be sorted in order for this to work properly
         # dates.sort(cmp=DateTime.sort)

Added: PyCalendar/trunk/src/pycalendar/icalendar/tests/test_icudatetime.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/icalendar/tests/test_icudatetime.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/icalendar/tests/test_icudatetime.py	2014-12-01 15:15:13 UTC (rev 14191)
@@ -0,0 +1,225 @@
+# coding: utf-8
+##
+#    Copyright (c) 2007-2013 Cyrus Daboo. All rights reserved.
+#
+#    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 unittest
+from pycalendar.datetime import DateTime
+from pycalendar.icalendar.icudatetime import ICUDateTime
+
+class TestICUDateTime(unittest.TestCase):
+    """
+    Test L{ICUDateTime}
+    """
+
+    def testRoundtripDateText(self):
+
+        data_date = (
+            ("gregorian", 2011, 1, 2, False, "20110102",),
+            ("gregorian", 1, 1, 2, False, "00010102",),
+            ("chinese", 4651, 1, 1, False, "{C}46510101",),
+            ("chinese", 4651, 9, 1, True, "{C}465109L01",),
+        )
+
+        for rscale, y, m, d, l, result in data_date:
+            dt = ICUDateTime.fromDateComponents(rscale, y, m, d, l)
+            self.assertEqual(dt.getText(), result, "Failed on: %s" % (result,))
+
+
+    def testRoundtripDateTime(self):
+
+        data = (
+            ("20110102", "gregorian", "20110102"),
+            ("20110102T010203Z", "gregorian", "20110102T010203Z"),
+            ("00010102", "gregorian", "00010102"),
+            ("20140102", "chinese", "{C}46501202"),
+            ("20140102T010203Z", "chinese", "{C}46501202T010203Z"),
+            ("20141025", "chinese", "{C}465109L02"),
+            ("20141025T010203", "chinese", "{C}465109L02T010203"),
+        )
+
+        for item, rscale, uitem in data:
+            dt = DateTime.parseText(item, False)
+            udt = ICUDateTime.fromDateTime(dt, rscale)
+            self.assertEqual(udt.getText(), uitem, "Failed on icu: %s" % (uitem,))
+            result = udt.toDateTime()
+            self.assertEqual(result.getText(), item, "Failed on dt: %s" % (item,))
+
+
+    def testSetYear(self):
+
+        data_date = (
+            ("gregorian", 2012, 1, 2, False, 2013, "20130102", False,),
+            ("gregorian", 2012, 2, 29, False, 2013, "20130301", True,),
+            ("gregorian", 2012, 2, 29, False, 2016, "20160229", False,),
+        )
+
+        for rscale, y, m, d, l, year, result, result_invalid in data_date:
+            dt = ICUDateTime.fromDateComponents(rscale, y, m, d, l)
+            dt.setYear(year)
+            self.assertEqual(dt.getText(), result, "Failed on: {} {} {}".format(year, result, result_invalid,))
+            self.assertEqual(dt.invalid(), result_invalid, "Failed invalid on: {} {} {}".format(year, result, result_invalid,))
+
+
+    def testOffsetYear(self):
+
+        data_date = (
+            ("gregorian", 2012, 1, 2, False, 1, "20130102", False,),
+            ("gregorian", 2012, 2, 29, False, 1, "20130228", False,),
+            ("gregorian", 2012, 2, 29, False, 4, "20160229", False,),
+        )
+
+        for rscale, y, m, d, l, year, result, result_invalid in data_date:
+            dt = ICUDateTime.fromDateComponents(rscale, y, m, d, l)
+            dt.offsetYear(year)
+            self.assertEqual(dt.getText(), result, "Failed on: {} {} {}".format(year, result, result_invalid,))
+            self.assertEqual(dt.invalid(), result_invalid, "Failed invalid on: {} {} {}".format(year, result, result_invalid,))
+
+
+    def testSetMonth(self):
+
+        data_date = (
+            ("gregorian", 2012, 1, 2, False, 2, "20120202", False,),
+            ("gregorian", 2012, 1, 29, False, 2, "20120229", False,),
+            ("gregorian", 2012, 1, 31, False, 2, "20120302", True,),
+            ("gregorian", 2012, 2, 29, False, 3, "20120329", False,),
+        )
+
+        for rscale, y, m, d, l, month, result, result_invalid in data_date:
+            dt = ICUDateTime.fromDateComponents(rscale, y, m, d, l)
+            dt.setMonth(month)
+            self.assertEqual(dt.getText(), result, "Failed on: {} {} {}".format(month, result, result_invalid,))
+            self.assertEqual(dt.invalid(), result_invalid, "Failed invalid on: {} {} {}".format(month, result, result_invalid,))
+
+
+    def testOffsetMonth(self):
+
+        data_date = (
+            ("gregorian", 2012, 1, 2, False, 1, "20120202", False,),
+            ("gregorian", 2012, 1, 29, False, 1, "20120229", False,),
+            ("gregorian", 2012, 1, 31, False, 1, "20120229", False,),
+            ("gregorian", 2012, 2, 29, False, 1, "20120329", False,),
+        )
+
+        for rscale, y, m, d, l, month, result, result_invalid in data_date:
+            dt = ICUDateTime.fromDateComponents(rscale, y, m, d, l)
+            dt.offsetMonth(month)
+            self.assertEqual(dt.getText(), result, "Failed on: {} {} {}".format(month, result, result_invalid,))
+            self.assertEqual(dt.invalid(), result_invalid, "Failed invalid on: {} {} {}".format(month, result, result_invalid,))
+
+
+    def testSetDay(self):
+
+        data_date = (
+            ("gregorian", 2012, 2, 1, False, 2, "20120202", False,),
+            ("gregorian", 2012, 2, 1, False, 29, "20120229", False,),
+            ("gregorian", 2012, 2, 1, False, 31, "20120302", True,),
+        )
+
+        for rscale, y, m, d, l, day, result, result_invalid in data_date:
+            dt = ICUDateTime.fromDateComponents(rscale, y, m, d, l)
+            dt.setDay(day)
+            self.assertEqual(dt.getText(), result, "Failed on: {} {} {}".format(day, result, result_invalid,))
+            self.assertEqual(dt.invalid(), result_invalid, "Failed invalid on: {} {} {}".format(day, result, result_invalid,))
+
+
+    def testOffsetDay(self):
+
+        data_date = (
+            ("gregorian", 2012, 2, 1, False, 1, "20120202", False,),
+            ("gregorian", 2012, 2, 1, False, 28, "20120229", False,),
+            ("gregorian", 2012, 2, 1, False, 30, "20120302", False,),
+        )
+
+        for rscale, y, m, d, l, day, result, result_invalid in data_date:
+            dt = ICUDateTime.fromDateComponents(rscale, y, m, d, l)
+            dt.offsetDay(day)
+            self.assertEqual(dt.getText(), result, "Failed on: {} {} {}".format(day, result, result_invalid,))
+            self.assertEqual(dt.invalid(), result_invalid, "Failed invalid on: {} {} {}".format(day, result, result_invalid,))
+
+
+    def testYearDay(self):
+
+        data_date = (
+            ("gregorian", 2012, 1, 2, False, 1, False, "20120101", False,),
+            ("gregorian", 2012, 1, 2, False, 60, False, "20120229", False,),
+            ("gregorian", 2012, 1, 2, False, 366, False, "20121231", False,),
+            ("gregorian", 2012, 1, 2, False, 367, False, "20121231", False,),
+            ("gregorian", 2012, 1, 2, False, 367, True, "20121231", True,),
+            ("gregorian", 2012, 1, 2, False, -1, False, "20121231", False,),
+            ("gregorian", 2012, 1, 2, False, -307, False, "20120229", False,),
+            ("gregorian", 2012, 1, 2, False, -366, False, "20120101", False,),
+            ("gregorian", 2012, 1, 2, False, -367, False, "20120101", False,),
+            ("gregorian", 2012, 1, 2, False, -367, True, "20120101", True,),
+        )
+
+        for rscale, y, m, d, l, yearday, allow_invalid, result, result_invalid in data_date:
+            dt = ICUDateTime.fromDateComponents(rscale, y, m, d, l)
+            dt.setYearDay(yearday, allow_invalid=allow_invalid)
+            self.assertEqual(dt.getText(), result, "Failed on: {} {} {}".format(yearday, result, result_invalid,))
+            self.assertEqual(dt.invalid(), result_invalid, "Failed invalid on: {} {} {}".format(yearday, result, result_invalid,))
+
+
+    def testMonthDay(self):
+
+        data_date = (
+            ("gregorian", 2012, 1, 2, False, 1, False, "20120101", False,),
+            ("gregorian", 2012, 1, 31, False, 1, False, "20120101", False,),
+            ("gregorian", 2012, 2, 1, False, 31, False, "20120229", False,),
+            ("gregorian", 2012, 2, 1, False, 31, True, "20120229", True,),
+            ("gregorian", 2012, 1, 2, False, -1, False, "20120131", False,),
+            ("gregorian", 2012, 2, 29, False, -29, False, "20120201", False,),
+            ("gregorian", 2012, 2, 29, False, -30, False, "20120201", False,),
+            ("gregorian", 2012, 2, 29, False, -30, True, "20120201", True,),
+        )
+
+        for rscale, y, m, d, l, monthday, allow_invalid, result, result_invalid in data_date:
+            dt = ICUDateTime.fromDateComponents(rscale, y, m, d, l)
+            dt.setMonthDay(monthday, allow_invalid=allow_invalid)
+            self.assertEqual(dt.getText(), result, "Failed on: {} {} {}".format(monthday, result, result_invalid,))
+            self.assertEqual(dt.invalid(), result_invalid, "Failed invalid on: {} {} {}".format(monthday, result, result_invalid,))
+
+
+    def testWeekNum(self):
+
+        data_date = (
+            ("gregorian", 2012, 1, 3, False, 1, 2, "20120110",),
+            ("gregorian", 2014, 1, 3, False, 1, 2, "20140110",),
+            ("gregorian", 2012, 1, 1, False, 52, 2, "20120115",),
+        )
+
+        for rscale, y, m, d, l, start_weekno, weekno, result in data_date:
+            dt = ICUDateTime.fromDateComponents(rscale, y, m, d, l)
+            self.assertEqual(dt.getWeekNo(), start_weekno)
+            dt.setWeekNo(weekno)
+            self.assertTrue(dt.isWeekNo(weekno))
+            self.assertEqual(dt.getText(), result, "Failed on: {} {} vs {}".format(weekno, result, dt.getText(),))
+
+
+    def testDayOfWeekInYear(self):
+
+        data_date = (
+            ("gregorian", 2012, 1, 3, False, 1, DateTime.SUNDAY, "20120101",),
+            ("gregorian", 2012, 1, 3, False, 1, DateTime.MONDAY, "20120102",),
+            ("gregorian", 2012, 1, 3, False, 2, DateTime.SUNDAY, "20120108",),
+            ("gregorian", 2012, 1, 3, False, 10, DateTime.SUNDAY, "20120304",),
+            ("gregorian", 2012, 1, 3, False, -1, DateTime.SUNDAY, "20121230",),
+            ("gregorian", 2012, 1, 3, False, -45, DateTime.SUNDAY, "20120226",),
+        )
+
+        for rscale, y, m, d, l, offset, day, result in data_date:
+            dt = ICUDateTime.fromDateComponents(rscale, y, m, d, l)
+            dt.setDayOfWeekInYear(offset, day)
+            self.assertEqual(dt.getText(), result, "Failed on: {} vs {}".format(result, dt.getText(),))

Added: PyCalendar/trunk/src/pycalendar/icalendar/tests/test_recuriter.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/icalendar/tests/test_recuriter.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/icalendar/tests/test_recuriter.py	2014-12-01 15:15:13 UTC (rev 14191)
@@ -0,0 +1,559 @@
+# coding: utf-8
+##
+#    Copyright (c) 2007-2013 Cyrus Daboo. All rights reserved.
+#
+#    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.
+##
+
+from pycalendar.datetime import DateTime
+from pycalendar.icalendar import definitions
+from pycalendar.icalendar.icudatetime import ICUDateTime
+from pycalendar.icalendar.recuriter import RecurrenceIterator
+import unittest
+
+class MonthlySkips(object):
+
+    def testMonthlySkipYes(self):
+
+        riter = RecurrenceIterator(self.dt, definitions.eRecurrence_MONTHLY, 1, rscale=self.rscale, skip=definitions.eRecurrence_SKIP_YES)
+        results = [riter.next().getText() for _ in range(12)]
+        self.assertEqual(
+            results,
+            [
+                "20140131",
+                "20140331",
+                "20140531",
+                "20140731",
+                "20140831",
+                "20141031",
+                "20141231",
+                "20150131",
+                "20150331",
+                "20150531",
+                "20150731",
+                "20150831",
+            ]
+        )
+
+
+    def testMonthlySkipForward(self):
+
+        riter = RecurrenceIterator(self.dt, definitions.eRecurrence_MONTHLY, 1, rscale=self.rscale, skip=definitions.eRecurrence_SKIP_FORWARD)
+        results = [riter.next().getText() for _ in range(12)]
+        self.assertEqual(
+            results,
+            [
+                "20140131",
+                "20140301",
+                "20140331",
+                "20140501",
+                "20140531",
+                "20140701",
+                "20140731",
+                "20140831",
+                "20141001",
+                "20141031",
+                "20141201",
+                "20141231",
+            ]
+        )
+
+
+    def testMonthlySkipBackward(self):
+
+        riter = RecurrenceIterator(self.dt, definitions.eRecurrence_MONTHLY, 1, rscale=self.rscale, skip=definitions.eRecurrence_SKIP_BACKWARD)
+        results = [riter.next().getText() for _ in range(12)]
+        self.assertEqual(
+            results,
+            [
+                "20140131",
+                "20140228",
+                "20140331",
+                "20140430",
+                "20140531",
+                "20140630",
+                "20140731",
+                "20140831",
+                "20140930",
+                "20141031",
+                "20141130",
+                "20141231",
+            ]
+        )
+
+
+    def testMonthlySkipBackwardLeap(self):
+
+        riter = RecurrenceIterator(self.dtleap, definitions.eRecurrence_MONTHLY, 1, rscale=self.rscale, skip=definitions.eRecurrence_SKIP_BACKWARD)
+        results = [riter.next().getText() for _ in range(12)]
+        self.assertEqual(
+            results,
+            [
+                "20160131",
+                "20160229",
+                "20160331",
+                "20160430",
+                "20160531",
+                "20160630",
+                "20160731",
+                "20160831",
+                "20160930",
+                "20161031",
+                "20161130",
+                "20161231",
+            ]
+        )
+
+
+
+class TestMonthlyDateTime(unittest.TestCase, MonthlySkips):
+
+    def setUp(self):
+        self.dt = DateTime(2014, 1, 31)
+        self.dtleap = DateTime(2016, 1, 31)
+        self.rscale = None
+
+
+
+class TestMonthlyGregorianICU(unittest.TestCase, MonthlySkips):
+
+    def setUp(self):
+        self.dt = ICUDateTime.fromDateComponents("gregorian", 2014, 1, 31)
+        self.dtleap = ICUDateTime.fromDateComponents("gregorian", 2016, 1, 31)
+        self.rscale = None
+
+
+
+class YearlySkipsOnLeapDay(object):
+
+    def testYearlySkipYes(self):
+
+        riter = RecurrenceIterator(self.dt, definitions.eRecurrence_YEARLY, 1, rscale=self.rscale, skip=definitions.eRecurrence_SKIP_YES)
+        results = [riter.next().getText() for _ in range(5)]
+        self.assertEqual(
+            results,
+            [
+                "20160229",
+                "20200229",
+                "20240229",
+                "20280229",
+                "20320229",
+            ]
+        )
+
+
+    def testYearlySkipForward(self):
+
+        riter = RecurrenceIterator(self.dt, definitions.eRecurrence_YEARLY, 1, rscale=self.rscale, skip=definitions.eRecurrence_SKIP_FORWARD)
+        results = [riter.next().getText() for _ in range(5)]
+        self.assertEqual(
+            results,
+            [
+                "20160229",
+                "20170301",
+                "20180301",
+                "20190301",
+                "20200229",
+            ]
+        )
+
+
+    def testYearlySkipBackward(self):
+
+        riter = RecurrenceIterator(self.dt, definitions.eRecurrence_YEARLY, 1, rscale=self.rscale, skip=definitions.eRecurrence_SKIP_BACKWARD)
+        results = [riter.next().getText() for _ in range(5)]
+        self.assertEqual(
+            results,
+            [
+                "20160229",
+                "20170228",
+                "20180228",
+                "20190228",
+                "20200229",
+            ]
+        )
+
+
+
+class TestYearlyDateTime(unittest.TestCase, YearlySkipsOnLeapDay):
+
+    def setUp(self):
+        self.dt = DateTime(2016, 2, 29)
+        self.rscale = None
+
+
+
+class TestYearlyGregorianICU(unittest.TestCase, YearlySkipsOnLeapDay):
+
+    def setUp(self):
+        self.dt = ICUDateTime.fromDateComponents("gregorian", 2016, 2, 29)
+        self.rscale = None
+
+
+
+class TestMonthlyChineseICU(unittest.TestCase):
+
+    def testMonthlyStartInLeapYearSkipYes(self):
+        dt = ICUDateTime.fromDateComponents("chinese", 4650, 12, 30)
+
+        riter = RecurrenceIterator(dt, definitions.eRecurrence_MONTHLY, 1, rscale=None, skip=definitions.eRecurrence_SKIP_YES)
+        results = []
+        while True:
+            result = riter.next()
+            if result.getYear() >= 4655:
+                break
+            results.append(result.getText())
+        self.assertEqual(
+            results,
+            [
+                "{C}46501230",
+                "{C}46510230",
+                "{C}46510430",
+                "{C}46510630",
+                "{C}46510830",
+                "{C}46510930",
+                "{C}46511030",
+                "{C}46511230",
+                "{C}46520230",
+                "{C}46520530",
+                "{C}46520730",
+                "{C}46520830",
+                "{C}46520930",
+                "{C}46521130",
+                "{C}46530130",
+                "{C}46530330",
+                "{C}46530630",
+                "{C}46530830",
+                "{C}46530930",
+                "{C}46531130",
+                "{C}46531230",
+                "{C}46540230",
+                "{C}46540430",
+                "{C}465406L30",
+                "{C}46540830",
+                "{C}46541030",
+                "{C}46541130",
+                "{C}46541230",
+            ]
+        )
+
+
+    def testMonthlyStartInLeapYearSkipForward(self):
+        dt = ICUDateTime.fromDateComponents("chinese", 4650, 12, 30)
+
+        riter = RecurrenceIterator(dt, definitions.eRecurrence_MONTHLY, 1, rscale=None, skip=definitions.eRecurrence_SKIP_FORWARD)
+        results = []
+        while True:
+            result = riter.next()
+            if result.getYear() >= 4655:
+                break
+            results.append(result.getText())
+        self.assertEqual(
+            results,
+            [
+                "{C}46501230",
+                "{C}46510201",
+                "{C}46510230",
+                "{C}46510401",
+                "{C}46510430",
+                "{C}46510601",
+                "{C}46510630",
+                "{C}46510801",
+                "{C}46510830",
+                "{C}46510930",
+                "{C}46511001",
+                "{C}46511030",
+                "{C}46511201",
+                "{C}46511230",
+                "{C}46520201",
+                "{C}46520230",
+                "{C}46520401",
+                "{C}46520501",
+                "{C}46520530",
+                "{C}46520701",
+                "{C}46520730",
+                "{C}46520830",
+                "{C}46520930",
+                "{C}46521101",
+                "{C}46521130",
+                "{C}46530101",
+                "{C}46530130",
+                "{C}46530301",
+                "{C}46530330",
+                "{C}46530501",
+                "{C}46530601",
+                "{C}46530630",
+                "{C}46530801",
+                "{C}46530830",
+                "{C}46530930",
+                "{C}46531101",
+                "{C}46531130",
+                "{C}46531230",
+                "{C}46540201",
+                "{C}46540230",
+                "{C}46540401",
+                "{C}46540430",
+                "{C}46540601",
+                "{C}465406L01",
+                "{C}465406L30",
+                "{C}46540801",
+                "{C}46540830",
+                "{C}46541001",
+                "{C}46541030",
+                "{C}46541130",
+                "{C}46541230",
+            ]
+        )
+
+
+    def testMonthlyStartInLeapYearSkipBackward(self):
+        dt = ICUDateTime.fromDateComponents("chinese", 4650, 12, 30)
+
+        riter = RecurrenceIterator(dt, definitions.eRecurrence_MONTHLY, 1, rscale=None, skip=definitions.eRecurrence_SKIP_BACKWARD)
+        results = []
+        while True:
+            result = riter.next()
+            if result.getYear() >= 4655:
+                break
+            results.append(result.getText())
+        self.assertEqual(
+            results,
+            [
+                "{C}46501230",
+                "{C}46510129",
+                "{C}46510230",
+                "{C}46510329",
+                "{C}46510430",
+                "{C}46510529",
+                "{C}46510630",
+                "{C}46510729",
+                "{C}46510830",
+                "{C}46510930",
+                "{C}465109L29",
+                "{C}46511030",
+                "{C}46511129",
+                "{C}46511230",
+                "{C}46520129",
+                "{C}46520230",
+                "{C}46520329",
+                "{C}46520429",
+                "{C}46520530",
+                "{C}46520629",
+                "{C}46520730",
+                "{C}46520830",
+                "{C}46520930",
+                "{C}46521029",
+                "{C}46521130",
+                "{C}46521229",
+                "{C}46530130",
+                "{C}46530229",
+                "{C}46530330",
+                "{C}46530429",
+                "{C}46530529",
+                "{C}46530630",
+                "{C}46530729",
+                "{C}46530830",
+                "{C}46530930",
+                "{C}46531029",
+                "{C}46531130",
+                "{C}46531230",
+                "{C}46540129",
+                "{C}46540230",
+                "{C}46540329",
+                "{C}46540430",
+                "{C}46540529",
+                "{C}46540629",
+                "{C}465406L30",
+                "{C}46540729",
+                "{C}46540830",
+                "{C}46540929",
+                "{C}46541030",
+                "{C}46541130",
+                "{C}46541230",
+            ]
+        )
+
+
+    def testMonthlyRscaleStartInLeapYearSkipYes(self):
+        dt = ICUDateTime.fromDateComponents("chinese", 4650, 12, 30).toDateTime()
+
+        riter = RecurrenceIterator(dt, definitions.eRecurrence_MONTHLY, 1, rscale="chinese", skip=definitions.eRecurrence_SKIP_YES)
+        results = []
+        while True:
+            result = riter.next()
+            if result.getYear() >= 2018:
+                break
+            results.append(result.getText())
+        self.assertEqual(
+            results,
+            [
+                "20140130",
+                "20140330",
+                "20140528",
+                "20140726",
+                "20140923",
+                "20141023",
+                "20141221",
+                "20150218",
+                "20150418",
+                "20150715",
+                "20150912",
+                "20151012",
+                "20151111",
+                "20160109",
+                "20160308",
+                "20160506",
+                "20160802",
+                "20160930",
+                "20161030",
+                "20161228",
+                "20170127",
+                "20170327",
+                "20170525",
+                "20170821",
+                "20171019",
+                "20171217",
+            ]
+        )
+
+
+    def testMonthlyRscaleStartInLeapYearSkipForward(self):
+        dt = ICUDateTime.fromDateComponents("chinese", 4650, 12, 30).toDateTime()
+
+        riter = RecurrenceIterator(dt, definitions.eRecurrence_MONTHLY, 1, rscale="chinese", skip=definitions.eRecurrence_SKIP_FORWARD)
+        results = []
+        while True:
+            result = riter.next()
+            if result.getYear() >= 2018:
+                break
+            results.append(result.getText())
+        self.assertEqual(
+            results,
+            [
+                "20140130",
+                "20140301",
+                "20140330",
+                "20140429",
+                "20140528",
+                "20140627",
+                "20140726",
+                "20140825",
+                "20140923",
+                "20141023",
+                "20141122",
+                "20141221",
+                "20150120",
+                "20150218",
+                "20150320",
+                "20150418",
+                "20150518",
+                "20150616",
+                "20150715",
+                "20150814",
+                "20150912",
+                "20151012",
+                "20151111",
+                "20151211",
+                "20160109",
+                "20160208",
+                "20160308",
+                "20160407",
+                "20160506",
+                "20160605",
+                "20160704",
+                "20160802",
+                "20160901",
+                "20160930",
+                "20161030",
+                "20161129",
+                "20161228",
+                "20170127",
+                "20170226",
+                "20170327",
+                "20170426",
+                "20170525",
+                "20170624",
+                "20170723",
+                "20170821",
+                "20170920",
+                "20171019",
+                "20171118",
+                "20171217",
+            ]
+        )
+
+
+    def testMonthlyRscaleStartInLeapYearSkipBackward(self):
+        dt = ICUDateTime.fromDateComponents("chinese", 4650, 12, 30).toDateTime()
+
+        riter = RecurrenceIterator(dt, definitions.eRecurrence_MONTHLY, 1, rscale="chinese", skip=definitions.eRecurrence_SKIP_BACKWARD)
+        results = []
+        while True:
+            result = riter.next()
+            if result.getYear() >= 2018:
+                break
+            results.append(result.getText())
+        self.assertEqual(
+            results,
+            [
+                "20140130",
+                "20140228",
+                "20140330",
+                "20140428",
+                "20140528",
+                "20140626",
+                "20140726",
+                "20140824",
+                "20140923",
+                "20141023",
+                "20141121",
+                "20141221",
+                "20150119",
+                "20150218",
+                "20150319",
+                "20150418",
+                "20150517",
+                "20150615",
+                "20150715",
+                "20150813",
+                "20150912",
+                "20151012",
+                "20151111",
+                "20151210",
+                "20160109",
+                "20160207",
+                "20160308",
+                "20160406",
+                "20160506",
+                "20160604",
+                "20160703",
+                "20160802",
+                "20160831",
+                "20160930",
+                "20161030",
+                "20161128",
+                "20161228",
+                "20170127",
+                "20170225",
+                "20170327",
+                "20170425",
+                "20170525",
+                "20170623",
+                "20170722",
+                "20170821",
+                "20170919",
+                "20171019",
+                "20171117",
+                "20171217",
+            ]
+        )

Modified: PyCalendar/trunk/src/pycalendar/icalendar/tests/test_recurrence.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/icalendar/tests/test_recurrence.py	2014-12-01 15:13:17 UTC (rev 14190)
+++ PyCalendar/trunk/src/pycalendar/icalendar/tests/test_recurrence.py	2014-12-01 15:15:13 UTC (rev 14191)
@@ -51,6 +51,14 @@
         "FREQ=YEARLY;BYDAY=MO;BYWEEKNO=20",
         "FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3",
         "FREQ=DAILY;BYMINUTE=0,20,40;BYHOUR=9,10,11,12,13,14,15,16",
+
+        # RSCALE
+        "RSCALE=CHINESE;FREQ=DAILY",
+        "RSCALE=GREGORIAN;FREQ=YEARLY;COUNT=400;SKIP=YES",
+        "RSCALE=GREGORIAN;FREQ=YEARLY;COUNT=400;SKIP=BACKWARD",
+        "RSCALE=GREGORIAN;FREQ=YEARLY;COUNT=400;SKIP=FORWARD",
+        "RSCALE=CHINESE;FREQ=YEARLY;BYMONTH=5,6,6L,7",
+
     )
 
 
@@ -59,7 +67,7 @@
         for item in TestRecurrence.items:
             recur = Recurrence()
             recur.parse(item)
-            self.assertEqual(recur.getText(), item, "Failed to parse and re-generate '%s'" % (item,))
+            self.assertEqual(recur.getText(), item, "Failed to parse and re-generate '%s' '%s'" % (item, recur.getText()))
 
 
     def testParseInvalid(self):
@@ -78,6 +86,8 @@
             "FREQ=MONTHLY;BYDAY=+1,3MO",
             "FREQ=MONTHLY;BYHOUR=A",
             "FREQ=MONTHLY;BYHOUR=54",
+            "FREQ=MONTHLY;SKIP=YES",
+            "RSCALE=CHINESE;FREQ=MONTHLY;SKIP=NO",
         )
 
         for item in items:
@@ -118,153 +128,618 @@
 
     def testByWeekNoExpand(self):
 
+        rule = "FREQ=YEARLY;BYWEEKNO=1,2"
+        for rrule in (
+            rule,
+            "RSCALE=GREGORIAN;{};SKIP=YES".format(rule)
+        ):
+            recur = Recurrence()
+            recur.parse(rrule)
+            start = DateTime(2013, 1, 1, 0, 0, 0)
+            end = DateTime(2017, 1, 1, 0, 0, 0)
+            items = []
+            range = Period(start, end)
+            recur.expand(start, range, items)
+            self.assertEqual(
+                items,
+                [
+                    DateTime(2013, 1, 1, 0, 0, 0),
+                    DateTime(2013, 1, 8, 0, 0, 0),
+                    DateTime(2014, 1, 1, 0, 0, 0),
+                    DateTime(2014, 1, 8, 0, 0, 0),
+                    DateTime(2015, 1, 1, 0, 0, 0),
+                    DateTime(2015, 1, 8, 0, 0, 0),
+                    DateTime(2016, 1, 8, 0, 0, 0),
+                    DateTime(2016, 1, 15, 0, 0, 0),
+                ],
+                msg="Failed: {}".format(rrule),
+            )
+
+
+    def testMonthlyInvalidStart(self):
+
+        rule = "FREQ=MONTHLY"
+        for rrule in (
+            rule,
+            "RSCALE=GREGORIAN;{};SKIP=YES".format(rule)
+        ):
+            recur = Recurrence()
+            recur.parse(rrule)
+            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),
+                ],
+                msg="Failed: {}".format(rrule),
+            )
+
+
+    def testWeeklyTwice(self):
+
+        rule = "FREQ=WEEKLY"
+        for rrule in (
+            rule,
+            "RSCALE=GREGORIAN;{};SKIP=YES".format(rule)
+        ):
+            recur = Recurrence()
+            recur.parse(rrule)
+            start = DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(utc=True))
+            end = DateTime(2014, 2, 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, 1, 8, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
+                    DateTime(2014, 1, 15, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
+                    DateTime(2014, 1, 22, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
+                    DateTime(2014, 1, 29, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
+
+                ],
+                msg="Failed: {}".format(rrule),
+            )
+
+            start = DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(utc=True))
+            end = DateTime(2014, 3, 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, 1, 8, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
+                    DateTime(2014, 1, 15, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
+                    DateTime(2014, 1, 22, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
+                    DateTime(2014, 1, 29, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
+                    DateTime(2014, 2, 5, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
+                    DateTime(2014, 2, 12, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
+                    DateTime(2014, 2, 19, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
+                    DateTime(2014, 2, 26, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
+                ],
+                msg="Failed: {}".format(rrule),
+            )
+
+
+    def testMonthlyInUTC(self):
+
+        rule = "FREQ=MONTHLY"
+        for rrule in (
+            rule,
+            "RSCALE=GREGORIAN;{};SKIP=YES".format(rule)
+        ):
+            recur = Recurrence()
+            recur.parse(rrule)
+            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),
+                ],
+                msg="Failed: {}".format(rrule),
+            )
+
+
+    def testMonthlyStart31st(self):
+
+        rule = "FREQ=MONTHLY"
+        for rrule in (
+            rule,
+            "RSCALE=GREGORIAN;{};SKIP=YES".format(rule)
+        ):
+            recur = Recurrence()
+            recur.parse(rrule)
+            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),
+                ],
+                msg="Failed: {}".format(rrule),
+            )
+
+
+    def testMonthlyByMonthDay31(self):
+
+        rule = "FREQ=MONTHLY;BYMONTHDAY=31"
+        for rrule in (
+            rule,
+            "RSCALE=GREGORIAN;{};SKIP=YES".format(rule)
+        ):
+            recur = Recurrence()
+            recur.parse(rrule)
+            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),
+                ],
+                msg="Failed: {}".format(rrule),
+            )
+
+
+    def testMonthlyByMonthDayMinus31(self):
+
+        rule = "FREQ=MONTHLY;BYMONTHDAY=-31"
+        for rrule in (
+            rule,
+            "RSCALE=GREGORIAN;{};SKIP=YES".format(rule)
+        ):
+            recur = Recurrence()
+            recur.parse(rrule)
+            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),
+                ],
+                msg="Failed: {}".format(rrule),
+            )
+
+
+    def testMonthlyByLastFridayExpand(self):
+
+        rule = "FREQ=MONTHLY;BYDAY=-1FR"
+        for rrule in (
+            rule,
+            "RSCALE=GREGORIAN;{};SKIP=YES".format(rule)
+        ):
+            recur = Recurrence()
+            recur.parse(rrule)
+            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),
+                ],
+                msg="Failed: {}".format(rrule),
+            )
+
+
+    def testMonthlyByFifthFridayExpand(self):
+
+        rule = "FREQ=MONTHLY;BYDAY=5FR"
+        for rrule in (
+            rule,
+            "RSCALE=GREGORIAN;{};SKIP=YES".format(rule)
+        ):
+            recur = Recurrence()
+            recur.parse(rrule)
+            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),
+                ],
+                msg="Failed: {}".format(rrule),
+            )
+
+
+    def testYearlyLeapDay(self):
+
+        rule = "FREQ=YEARLY"
+        for rrule in (
+            rule,
+            "RSCALE=GREGORIAN;{};SKIP=YES".format(rule)
+        ):
+            recur = Recurrence()
+            recur.parse(rrule)
+            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),
+                ],
+                msg="Failed: {}".format(rrule),
+            )
+
+
+    def testYearlyYearDay(self):
+
+        rule = "FREQ=YEARLY;BYYEARDAY=366"
+        for rrule in (
+            rule,
+            "RSCALE=GREGORIAN;{};SKIP=YES".format(rule)
+        ):
+            recur = Recurrence()
+            recur.parse(rrule)
+            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),
+                ],
+                msg="Failed: {}".format(rrule),
+            )
+
+
+    def testClearOnChange(self):
+
         recur = Recurrence()
-        recur.parse("FREQ=YEARLY;BYWEEKNO=1,2")
+        recur.parse("FREQ=DAILY")
+
         start = DateTime(2013, 1, 1, 0, 0, 0)
         end = DateTime(2017, 1, 1, 0, 0, 0)
+        range = Period(start, end)
         items = []
-        range = Period(start, end)
         recur.expand(start, range, items)
-        self.assertEqual(
-            items,
-            [
-                DateTime(2013, 1, 1, 0, 0, 0),
-                DateTime(2013, 1, 8, 0, 0, 0),
-                DateTime(2014, 1, 1, 0, 0, 0),
-                DateTime(2014, 1, 8, 0, 0, 0),
-                DateTime(2015, 1, 1, 0, 0, 0),
-                DateTime(2015, 1, 8, 0, 0, 0),
-                DateTime(2016, 1, 8, 0, 0, 0),
-                DateTime(2016, 1, 15, 0, 0, 0),
-            ],
-        )
+        self.assertTrue(recur.mCached)
+        self.assertTrue(len(items) > 100)
 
+        recur.setUseCount(True)
+        recur.setCount(10)
+        self.assertFalse(recur.mCached)
+        items = []
+        recur.expand(start, range, items)
+        self.assertEqual(len(items), 10)
 
-    def testMonthlyInvalidStart(self):
 
+
+class TestRecurrenceRscale(unittest.TestCase):
+
+    def testMonthlyRscaleStartInLeapYearSkipYes(self):
+
         recur = Recurrence()
-        recur.parse("FREQ=MONTHLY")
-        start = DateTime(2014, 1, 40, 12, 0, 0)
-        end = DateTime(2015, 1, 1, 0, 0, 0)
+        recur.parse("RSCALE=CHINESE;FREQ=MONTHLY;SKIP=YES")
+        start = DateTime(2014, 1, 30) # {C}46501230
+        end = DateTime(2018, 1, 1)
         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),
+                DateTime(2014, 1, 30),
+                DateTime(2014, 3, 30),
+                DateTime(2014, 5, 28),
+                DateTime(2014, 7, 26),
+                DateTime(2014, 9, 23),
+                DateTime(2014, 10, 23),
+                DateTime(2014, 12, 21),
+                DateTime(2015, 2, 18),
+                DateTime(2015, 4, 18),
+                DateTime(2015, 7, 15),
+                DateTime(2015, 9, 12),
+                DateTime(2015, 10, 12),
+                DateTime(2015, 11, 11),
+                DateTime(2016, 1, 9),
+                DateTime(2016, 3, 8),
+                DateTime(2016, 5, 6),
+                DateTime(2016, 8, 2),
+                DateTime(2016, 9, 30),
+                DateTime(2016, 10, 30),
+                DateTime(2016, 12, 28),
+                DateTime(2017, 1, 27),
+                DateTime(2017, 3, 27),
+                DateTime(2017, 5, 25),
+                DateTime(2017, 8, 21),
+                DateTime(2017, 10, 19),
+                DateTime(2017, 12, 17),
             ],
         )
 
 
-    def testWeeklyTwice(self):
+    def testMonthlyRscaleStartInLeapYearSkipForward(self):
 
         recur = Recurrence()
-        recur.parse("FREQ=WEEKLY")
-        start = DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(utc=True))
-        end = DateTime(2014, 2, 1, 0, 0, 0, tzid=Timezone(utc=True))
+        recur.parse("RSCALE=CHINESE;FREQ=MONTHLY;SKIP=FORWARD")
+        start = DateTime(2014, 1, 30) # {C}46501230
+        end = DateTime(2018, 1, 1)
         items = []
         range = Period(start, end)
-        recur.expand(DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), range, items)
+        recur.expand(start, range, items)
         self.assertEqual(
             items,
             [
-                DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
-                DateTime(2014, 1, 8, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
-                DateTime(2014, 1, 15, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
-                DateTime(2014, 1, 22, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
-                DateTime(2014, 1, 29, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
-
+                DateTime(2014, 1, 30),
+                DateTime(2014, 3, 1),
+                DateTime(2014, 3, 30),
+                DateTime(2014, 4, 29),
+                DateTime(2014, 5, 28),
+                DateTime(2014, 6, 27),
+                DateTime(2014, 7, 26),
+                DateTime(2014, 8, 25),
+                DateTime(2014, 9, 23),
+                DateTime(2014, 10, 23),
+                DateTime(2014, 11, 22),
+                DateTime(2014, 12, 21),
+                DateTime(2015, 1, 20),
+                DateTime(2015, 2, 18),
+                DateTime(2015, 3, 20),
+                DateTime(2015, 4, 18),
+                DateTime(2015, 5, 18),
+                DateTime(2015, 6, 16),
+                DateTime(2015, 7, 15),
+                DateTime(2015, 8, 14),
+                DateTime(2015, 9, 12),
+                DateTime(2015, 10, 12),
+                DateTime(2015, 11, 11),
+                DateTime(2015, 12, 11),
+                DateTime(2016, 1, 9),
+                DateTime(2016, 2, 8),
+                DateTime(2016, 3, 8),
+                DateTime(2016, 4, 7),
+                DateTime(2016, 5, 6),
+                DateTime(2016, 6, 5),
+                DateTime(2016, 7, 4),
+                DateTime(2016, 8, 2),
+                DateTime(2016, 9, 1),
+                DateTime(2016, 9, 30),
+                DateTime(2016, 10, 30),
+                DateTime(2016, 11, 29),
+                DateTime(2016, 12, 28),
+                DateTime(2017, 1, 27),
+                DateTime(2017, 2, 26),
+                DateTime(2017, 3, 27),
+                DateTime(2017, 4, 26),
+                DateTime(2017, 5, 25),
+                DateTime(2017, 6, 24),
+                DateTime(2017, 7, 23),
+                DateTime(2017, 8, 21),
+                DateTime(2017, 9, 20),
+                DateTime(2017, 10, 19),
+                DateTime(2017, 11, 18),
+                DateTime(2017, 12, 17),
             ],
         )
 
-        start = DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(utc=True))
-        end = DateTime(2014, 3, 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, 1, 8, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
-                DateTime(2014, 1, 15, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
-                DateTime(2014, 1, 22, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
-                DateTime(2014, 1, 29, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
-                DateTime(2014, 2, 5, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
-                DateTime(2014, 2, 12, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
-                DateTime(2014, 2, 19, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
-                DateTime(2014, 2, 26, 12, 0, 0, tzid=Timezone(tzid="America/New_York")),
-            ],
-        )
 
+    def testMonthlyRscaleStartInLeapYearSkipBackwardDefault(self):
 
-    def testMonthlyInUTC(self):
+        for rrule in (
+            "RSCALE=CHINESE;FREQ=MONTHLY;SKIP=BACKWARD",
+            "RSCALE=CHINESE;FREQ=MONTHLY"
+        ):
+            recur = Recurrence()
+            recur.parse(rrule)
+            start = DateTime(2014, 1, 30) # {C}46501230
+            end = DateTime(2018, 1, 1)
+            items = []
+            range = Period(start, end)
+            recur.expand(start, range, items)
+            self.assertEqual(
+                items,
+                [
+                    DateTime(2014, 1, 30),
+                    DateTime(2014, 2, 28),
+                    DateTime(2014, 3, 30),
+                    DateTime(2014, 4, 28),
+                    DateTime(2014, 5, 28),
+                    DateTime(2014, 6, 26),
+                    DateTime(2014, 7, 26),
+                    DateTime(2014, 8, 24),
+                    DateTime(2014, 9, 23),
+                    DateTime(2014, 10, 23),
+                    DateTime(2014, 11, 21),
+                    DateTime(2014, 12, 21),
+                    DateTime(2015, 1, 19),
+                    DateTime(2015, 2, 18),
+                    DateTime(2015, 3, 19),
+                    DateTime(2015, 4, 18),
+                    DateTime(2015, 5, 17),
+                    DateTime(2015, 6, 15),
+                    DateTime(2015, 7, 15),
+                    DateTime(2015, 8, 13),
+                    DateTime(2015, 9, 12),
+                    DateTime(2015, 10, 12),
+                    DateTime(2015, 11, 11),
+                    DateTime(2015, 12, 10),
+                    DateTime(2016, 1, 9),
+                    DateTime(2016, 2, 7),
+                    DateTime(2016, 3, 8),
+                    DateTime(2016, 4, 6),
+                    DateTime(2016, 5, 6),
+                    DateTime(2016, 6, 4),
+                    DateTime(2016, 7, 3),
+                    DateTime(2016, 8, 2),
+                    DateTime(2016, 8, 31),
+                    DateTime(2016, 9, 30),
+                    DateTime(2016, 10, 30),
+                    DateTime(2016, 11, 28),
+                    DateTime(2016, 12, 28),
+                    DateTime(2017, 1, 27),
+                    DateTime(2017, 2, 25),
+                    DateTime(2017, 3, 27),
+                    DateTime(2017, 4, 25),
+                    DateTime(2017, 5, 25),
+                    DateTime(2017, 6, 23),
+                    DateTime(2017, 7, 22),
+                    DateTime(2017, 8, 21),
+                    DateTime(2017, 9, 19),
+                    DateTime(2017, 10, 19),
+                    DateTime(2017, 11, 17),
+                    DateTime(2017, 12, 17),
+                ],
+            )
 
+
+    def testYearlyLeapDaySkipYes(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))
+        recur.parse("RSCALE=GREGORIAN;FREQ=YEARLY;SKIP=YES;COUNT=5")
+        start = DateTime(2016, 2, 29)
+        end = DateTime(2100, 1, 1)
         items = []
         range = Period(start, end)
-        recur.expand(DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), range, items)
+        recur.expand(start, 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),
-            ],
+                DateTime(2016, 2, 29),
+                DateTime(2020, 2, 29),
+                DateTime(2024, 2, 29),
+                DateTime(2028, 2, 29),
+                DateTime(2032, 2, 29),
+            ]
         )
 
 
-    def testMonthlyStart31st(self):
+    def testYearlyLeapDaySkipForward(self):
 
         recur = Recurrence()
-        recur.parse("FREQ=MONTHLY")
-        start = DateTime(2014, 1, 31, 12, 0, 0)
-        end = DateTime(2015, 1, 1, 0, 0, 0)
+        recur.parse("RSCALE=GREGORIAN;FREQ=YEARLY;SKIP=FORWARD;COUNT=5")
+        start = DateTime(2016, 2, 29)
+        end = DateTime(2100, 1, 1)
         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),
-            ],
+                DateTime(2016, 2, 29),
+                DateTime(2017, 3, 1),
+                DateTime(2018, 3, 1),
+                DateTime(2019, 3, 1),
+                DateTime(2020, 2, 29),
+            ]
         )
 
 
-    def testMonthlyByMonthDay31(self):
+    def testYearlyLeapDaySkipBackwardDefault(self):
 
+        for rrule in (
+            "RSCALE=GREGORIAN;FREQ=YEARLY;SKIP=BACKWARD;COUNT=5",
+            "RSCALE=GREGORIAN;FREQ=YEARLY;COUNT=5",
+        ):
+            recur = Recurrence()
+            recur.parse(rrule)
+            start = DateTime(2016, 2, 29)
+            end = DateTime(2100, 1, 1)
+            items = []
+            range = Period(start, end)
+            recur.expand(start, range, items)
+            self.assertEqual(
+                items,
+                [
+                    DateTime(2016, 2, 29),
+                    DateTime(2017, 2, 28),
+                    DateTime(2018, 2, 28),
+                    DateTime(2019, 2, 28),
+                    DateTime(2020, 2, 29),
+                ]
+            )
+
+
+    def testChineseMonthlyByMonthDay30SkipYes(self):
+
+        rrule = "RSCALE=CHINESE;FREQ=MONTHLY;BYMONTHDAY=30;SKIP=YES"
         recur = Recurrence()
-        recur.parse("FREQ=MONTHLY;BYMONTHDAY=31")
-        start = DateTime(2014, 1, 31, 12, 0, 0)
+        recur.parse(rrule)
+        start = DateTime(2014, 1, 30, 12, 0, 0)
         end = DateTime(2015, 1, 1, 0, 0, 0)
         items = []
         range = Period(start, end)
@@ -272,22 +747,24 @@
         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),
+                DateTime(2014, 1, 30, 12, 0, 0),
+                DateTime(2014, 3, 30, 12, 0, 0),
+                DateTime(2014, 5, 28, 12, 0, 0),
+                DateTime(2014, 7, 26, 12, 0, 0),
+                DateTime(2014, 9, 23, 12, 0, 0),
+                DateTime(2014, 10, 23, 12, 0, 0),
+                DateTime(2014, 12, 21, 12, 0, 0),
             ],
+            msg="Failed: {} {}".format(rrule, items,),
         )
 
 
-    def testMonthlyByMonthDayMinus31(self):
+    def testChineseMonthlyByMonthDay30SkipBackward(self):
 
+        rrule = "RSCALE=CHINESE;FREQ=MONTHLY;BYMONTHDAY=30;SKIP=BACKWARD"
         recur = Recurrence()
-        recur.parse("FREQ=MONTHLY;BYMONTHDAY=-31")
-        start = DateTime(2014, 1, 1, 12, 0, 0)
+        recur.parse(rrule)
+        start = DateTime(2014, 1, 30, 12, 0, 0)
         end = DateTime(2015, 1, 1, 0, 0, 0)
         items = []
         range = Period(start, end)
@@ -295,22 +772,29 @@
         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),
+                DateTime(2014, 1, 30, 12, 0, 0),
+                DateTime(2014, 2, 28, 12, 0, 0),
+                DateTime(2014, 3, 30, 12, 0, 0),
+                DateTime(2014, 4, 28, 12, 0, 0),
+                DateTime(2014, 5, 28, 12, 0, 0),
+                DateTime(2014, 6, 26, 12, 0, 0),
+                DateTime(2014, 7, 26, 12, 0, 0),
+                DateTime(2014, 8, 24, 12, 0, 0),
+                DateTime(2014, 9, 23, 12, 0, 0),
+                DateTime(2014, 10, 23, 12, 0, 0),
+                DateTime(2014, 11, 21, 12, 0, 0),
+                DateTime(2014, 12, 21, 12, 0, 0),
             ],
+            msg="Failed: {} {}".format(rrule, items,),
         )
 
 
-    def testMonthlyByLastFridayExpand(self):
+    def testChineseMonthlyByMonthDay30SkipForward(self):
 
+        rrule = "RSCALE=CHINESE;FREQ=MONTHLY;BYMONTHDAY=30;SKIP=FORWARD"
         recur = Recurrence()
-        recur.parse("FREQ=MONTHLY;BYDAY=-1FR")
-        start = DateTime(2014, 1, 31, 12, 0, 0)
+        recur.parse(rrule)
+        start = DateTime(2014, 1, 30, 12, 0, 0)
         end = DateTime(2015, 1, 1, 0, 0, 0)
         items = []
         range = Period(start, end)
@@ -318,27 +802,29 @@
         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, 1, 30, 12, 0, 0),
+                DateTime(2014, 3, 1, 12, 0, 0),
+                DateTime(2014, 3, 30, 12, 0, 0),
+                DateTime(2014, 4, 29, 12, 0, 0),
+                DateTime(2014, 5, 28, 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),
+                DateTime(2014, 7, 26, 12, 0, 0),
+                DateTime(2014, 8, 25, 12, 0, 0),
+                DateTime(2014, 9, 23, 12, 0, 0),
+                DateTime(2014, 10, 23, 12, 0, 0),
+                DateTime(2014, 11, 22, 12, 0, 0),
+                DateTime(2014, 12, 21, 12, 0, 0),
             ],
+            msg="Failed: {} {}".format(rrule, items,),
         )
 
 
-    def testMonthlyByFifthFridayExpand(self):
+    def testChineseMonthlyByMonthDayMinus30SkipYes(self):
 
+        rrule = "RSCALE=CHINESE;FREQ=MONTHLY;BYMONTHDAY=-30;SKIP=YES"
         recur = Recurrence()
-        recur.parse("FREQ=MONTHLY;BYDAY=5FR")
-        start = DateTime(2014, 1, 31, 12, 0, 0)
+        recur.parse(rrule)
+        start = DateTime(2014, 1, 30, 12, 0, 0)
         end = DateTime(2015, 1, 1, 0, 0, 0)
         items = []
         range = Period(start, end)
@@ -346,66 +832,72 @@
         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),
+                DateTime(2014, 3, 1, 12, 0, 0),
+                DateTime(2014, 4, 29, 12, 0, 0),
+                DateTime(2014, 6, 27, 12, 0, 0),
+                DateTime(2014, 8, 25, 12, 0, 0),
+                DateTime(2014, 9, 24, 12, 0, 0),
+                DateTime(2014, 11, 22, 12, 0, 0),
             ],
+            msg="Failed: {} {}".format(rrule, items,),
         )
 
 
-    def testYearlyLeapDay(self):
+    def testChineseMonthlyByMonthDayMinus30SkipBackward(self):
 
+        rrule = "RSCALE=CHINESE;FREQ=MONTHLY;BYMONTHDAY=-30;SKIP=BACKWARD"
         recur = Recurrence()
-        recur.parse("FREQ=YEARLY")
-        start = DateTime(2012, 2, 29, 12, 0, 0)
-        end = DateTime(2020, 1, 1, 0, 0, 0)
+        recur.parse(rrule)
+        start = DateTime(2014, 1, 30, 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(2012, 2, 29, 12, 0, 0),
-                DateTime(2016, 2, 29, 12, 0, 0),
+                DateTime(2014, 1, 30, 12, 0, 0),
+                DateTime(2014, 3, 1, 12, 0, 0),
+                DateTime(2014, 3, 30, 12, 0, 0),
+                DateTime(2014, 4, 29, 12, 0, 0),
+                DateTime(2014, 5, 28, 12, 0, 0),
+                DateTime(2014, 6, 27, 12, 0, 0),
+                DateTime(2014, 7, 26, 12, 0, 0),
+                DateTime(2014, 8, 25, 12, 0, 0),
+                DateTime(2014, 9, 24, 12, 0, 0),
+                DateTime(2014, 10, 23, 12, 0, 0),
+                DateTime(2014, 11, 22, 12, 0, 0),
+                DateTime(2014, 12, 21, 12, 0, 0),
             ],
+            msg="Failed: {} {}".format(rrule, items,),
         )
 
 
-    def testYearlyYearDay(self):
+    def testChineseMonthlyByMonthDayMinus30SkipForward(self):
 
+        rrule = "RSCALE=CHINESE;FREQ=MONTHLY;BYMONTHDAY=-30;SKIP=FORWARD"
         recur = Recurrence()
-        recur.parse("FREQ=YEARLY;BYYEARDAY=366")
-        start = DateTime(2012, 12, 31, 12, 0, 0)
-        end = DateTime(2020, 1, 1, 0, 0, 0)
+        recur.parse(rrule)
+        start = DateTime(2014, 1, 30, 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(2012, 12, 31, 12, 0, 0),
-                DateTime(2016, 12, 31, 12, 0, 0),
+                DateTime(2014, 1, 31, 12, 0, 0),
+                DateTime(2014, 3, 1, 12, 0, 0),
+                DateTime(2014, 3, 31, 12, 0, 0),
+                DateTime(2014, 4, 29, 12, 0, 0),
+                DateTime(2014, 5, 29, 12, 0, 0),
+                DateTime(2014, 6, 27, 12, 0, 0),
+                DateTime(2014, 7, 27, 12, 0, 0),
+                DateTime(2014, 8, 25, 12, 0, 0),
+                DateTime(2014, 9, 24, 12, 0, 0),
+                DateTime(2014, 10, 24, 12, 0, 0),
+                DateTime(2014, 11, 22, 12, 0, 0),
+                DateTime(2014, 12, 22, 12, 0, 0),
             ],
+            msg="Failed: {} {}".format(rrule, items,),
         )
-
-
-    def testClearOnChange(self):
-
-        recur = Recurrence()
-        recur.parse("FREQ=DAILY")
-
-        start = DateTime(2013, 1, 1, 0, 0, 0)
-        end = DateTime(2017, 1, 1, 0, 0, 0)
-        range = Period(start, end)
-        items = []
-        recur.expand(start, range, items)
-        self.assertTrue(recur.mCached)
-        self.assertTrue(len(items) > 100)
-
-        recur.setUseCount(True)
-        recur.setCount(10)
-        self.assertFalse(recur.mCached)
-        items = []
-        recur.expand(start, range, items)
-        self.assertEqual(len(items), 10)

Modified: PyCalendar/trunk/src/pycalendar/icalendar/xmldefinitions.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/icalendar/xmldefinitions.py	2014-12-01 15:13:17 UTC (rev 14190)
+++ PyCalendar/trunk/src/pycalendar/icalendar/xmldefinitions.py	2014-12-01 15:15:13 UTC (rev 14191)
@@ -22,6 +22,8 @@
 
 value_recur = "recur"
 
+recur_rscale = "rscale"
+
 recur_freq = "freq"
 recur_freq_secondly = "SECONDLY"
 recur_freq_minutely = "MINUTELY"
@@ -35,6 +37,11 @@
 recur_until = "until"
 recur_interval = "interval"
 
+recur_skip = "skip"
+recur_skip_yes = "yes"
+recur_skip_backward = "backward"
+recur_skip_forward = "forward"
+
 recur_bysecond = "bysecond"
 recur_byminute = "byminute"
 recur_byhour = "byhour"
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20141201/422f5cfb/attachment-0001.html>


More information about the calendarserver-changes mailing list