[CalendarServer-changes] [10554] PyCalendar/trunk

source_changes at macosforge.org source_changes at macosforge.org
Sat Jan 26 10:24:59 PST 2013


Revision: 10554
          http://trac.calendarserver.org//changeset/10554
Author:   cdaboo at apple.com
Date:     2013-01-26 10:24:59 -0800 (Sat, 26 Jan 2013)
Log Message:
-----------
Merge /branches/server to trunk.

Modified Paths:
--------------
    PyCalendar/trunk/setup.py
    PyCalendar/trunk/src/pycalendar/__init__.py
    PyCalendar/trunk/src/pycalendar/attribute.py
    PyCalendar/trunk/src/pycalendar/caladdressvalue.py
    PyCalendar/trunk/src/pycalendar/calendar.py
    PyCalendar/trunk/src/pycalendar/component.py
    PyCalendar/trunk/src/pycalendar/componentbase.py
    PyCalendar/trunk/src/pycalendar/componentexpanded.py
    PyCalendar/trunk/src/pycalendar/componentrecur.py
    PyCalendar/trunk/src/pycalendar/datetime.py
    PyCalendar/trunk/src/pycalendar/datetimevalue.py
    PyCalendar/trunk/src/pycalendar/definitions.py
    PyCalendar/trunk/src/pycalendar/duration.py
    PyCalendar/trunk/src/pycalendar/durationvalue.py
    PyCalendar/trunk/src/pycalendar/freebusy.py
    PyCalendar/trunk/src/pycalendar/integervalue.py
    PyCalendar/trunk/src/pycalendar/itipdefinitions.py
    PyCalendar/trunk/src/pycalendar/locale.py
    PyCalendar/trunk/src/pycalendar/manager.py
    PyCalendar/trunk/src/pycalendar/multivalue.py
    PyCalendar/trunk/src/pycalendar/outputfilter.py
    PyCalendar/trunk/src/pycalendar/period.py
    PyCalendar/trunk/src/pycalendar/periodvalue.py
    PyCalendar/trunk/src/pycalendar/plaintextvalue.py
    PyCalendar/trunk/src/pycalendar/property.py
    PyCalendar/trunk/src/pycalendar/recurrence.py
    PyCalendar/trunk/src/pycalendar/recurrenceset.py
    PyCalendar/trunk/src/pycalendar/recurrencevalue.py
    PyCalendar/trunk/src/pycalendar/stringutils.py
    PyCalendar/trunk/src/pycalendar/tests/__init__.py
    PyCalendar/trunk/src/pycalendar/textvalue.py
    PyCalendar/trunk/src/pycalendar/timezone.py
    PyCalendar/trunk/src/pycalendar/urivalue.py
    PyCalendar/trunk/src/pycalendar/utcoffsetvalue.py
    PyCalendar/trunk/src/pycalendar/utils.py
    PyCalendar/trunk/src/pycalendar/valarm.py
    PyCalendar/trunk/src/pycalendar/value.py
    PyCalendar/trunk/src/pycalendar/vevent.py
    PyCalendar/trunk/src/pycalendar/vfreebusy.py
    PyCalendar/trunk/src/pycalendar/vjournal.py
    PyCalendar/trunk/src/pycalendar/vtimezone.py
    PyCalendar/trunk/src/pycalendar/vtimezonedaylight.py
    PyCalendar/trunk/src/pycalendar/vtimezoneelement.py
    PyCalendar/trunk/src/pycalendar/vtimezonestandard.py
    PyCalendar/trunk/src/pycalendar/vtodo.py

Added Paths:
-----------
    PyCalendar/trunk/src/pycalendar/adr.py
    PyCalendar/trunk/src/pycalendar/adrvalue.py
    PyCalendar/trunk/src/pycalendar/available.py
    PyCalendar/trunk/src/pycalendar/binaryvalue.py
    PyCalendar/trunk/src/pycalendar/exceptions.py
    PyCalendar/trunk/src/pycalendar/icalendar/
    PyCalendar/trunk/src/pycalendar/icalendar/__init__.py
    PyCalendar/trunk/src/pycalendar/icalendar/tests/
    PyCalendar/trunk/src/pycalendar/icalendar/tests/__init__.py
    PyCalendar/trunk/src/pycalendar/icalendar/tests/test_validation.py
    PyCalendar/trunk/src/pycalendar/icalendar/validation.py
    PyCalendar/trunk/src/pycalendar/n.py
    PyCalendar/trunk/src/pycalendar/nvalue.py
    PyCalendar/trunk/src/pycalendar/orgvalue.py
    PyCalendar/trunk/src/pycalendar/parser.py
    PyCalendar/trunk/src/pycalendar/requeststatusvalue.py
    PyCalendar/trunk/src/pycalendar/tests/test_adr.py
    PyCalendar/trunk/src/pycalendar/tests/test_adrvalue.py
    PyCalendar/trunk/src/pycalendar/tests/test_calendar.py
    PyCalendar/trunk/src/pycalendar/tests/test_componentrecur.py
    PyCalendar/trunk/src/pycalendar/tests/test_datetime.py
    PyCalendar/trunk/src/pycalendar/tests/test_duration.py
    PyCalendar/trunk/src/pycalendar/tests/test_i18n.py
    PyCalendar/trunk/src/pycalendar/tests/test_multivalue.py
    PyCalendar/trunk/src/pycalendar/tests/test_n.py
    PyCalendar/trunk/src/pycalendar/tests/test_nvalue.py
    PyCalendar/trunk/src/pycalendar/tests/test_orgvalue.py
    PyCalendar/trunk/src/pycalendar/tests/test_property.py
    PyCalendar/trunk/src/pycalendar/tests/test_recurrence.py
    PyCalendar/trunk/src/pycalendar/tests/test_requeststatus.py
    PyCalendar/trunk/src/pycalendar/tests/test_timezone.py
    PyCalendar/trunk/src/pycalendar/tests/test_urivalue.py
    PyCalendar/trunk/src/pycalendar/tests/test_utils.py
    PyCalendar/trunk/src/pycalendar/tests/test_validation.py
    PyCalendar/trunk/src/pycalendar/tests/test_xml.py
    PyCalendar/trunk/src/pycalendar/timezonedb.py
    PyCalendar/trunk/src/pycalendar/unknownvalue.py
    PyCalendar/trunk/src/pycalendar/validation.py
    PyCalendar/trunk/src/pycalendar/valueutils.py
    PyCalendar/trunk/src/pycalendar/vavailability.py
    PyCalendar/trunk/src/pycalendar/vcard/
    PyCalendar/trunk/src/pycalendar/vcard/__init__.py
    PyCalendar/trunk/src/pycalendar/vcard/card.py
    PyCalendar/trunk/src/pycalendar/vcard/definitions.py
    PyCalendar/trunk/src/pycalendar/vcard/property.py
    PyCalendar/trunk/src/pycalendar/vcard/tests/
    PyCalendar/trunk/src/pycalendar/vcard/tests/__init__.py
    PyCalendar/trunk/src/pycalendar/vcard/tests/test_card.py
    PyCalendar/trunk/src/pycalendar/vcard/tests/test_property.py
    PyCalendar/trunk/src/pycalendar/vcard/tests/test_validation.py
    PyCalendar/trunk/src/pycalendar/vcard/validation.py
    PyCalendar/trunk/src/pycalendar/vunknown.py
    PyCalendar/trunk/src/pycalendar/xmldefs.py
    PyCalendar/trunk/src/zonal/
    PyCalendar/trunk/src/zonal/__init__.py
    PyCalendar/trunk/src/zonal/rule.py
    PyCalendar/trunk/src/zonal/tests/
    PyCalendar/trunk/src/zonal/tests/__init__.py
    PyCalendar/trunk/src/zonal/tests/test_rule.py
    PyCalendar/trunk/src/zonal/tests/test_zone.py
    PyCalendar/trunk/src/zonal/tzconvert.py
    PyCalendar/trunk/src/zonal/tzdump.py
    PyCalendar/trunk/src/zonal/tzverify.py
    PyCalendar/trunk/src/zonal/utils.py
    PyCalendar/trunk/src/zonal/zone.py

Removed Paths:
-------------
    PyCalendar/trunk/src/pycalendar/componentdb.py
    PyCalendar/trunk/src/pycalendar/dummyvalue.py
    PyCalendar/trunk/src/pycalendar/icalendar/__init__.py
    PyCalendar/trunk/src/pycalendar/icalendar/tests/
    PyCalendar/trunk/src/pycalendar/icalendar/tests/__init__.py
    PyCalendar/trunk/src/pycalendar/icalendar/tests/test_validation.py
    PyCalendar/trunk/src/pycalendar/icalendar/validation.py
    PyCalendar/trunk/src/pycalendar/tests/calendar.py
    PyCalendar/trunk/src/pycalendar/tests/duration.py
    PyCalendar/trunk/src/pycalendar/vcard/__init__.py
    PyCalendar/trunk/src/pycalendar/vcard/card.py
    PyCalendar/trunk/src/pycalendar/vcard/definitions.py
    PyCalendar/trunk/src/pycalendar/vcard/property.py
    PyCalendar/trunk/src/pycalendar/vcard/tests/
    PyCalendar/trunk/src/pycalendar/vcard/tests/__init__.py
    PyCalendar/trunk/src/pycalendar/vcard/tests/test_card.py
    PyCalendar/trunk/src/pycalendar/vcard/tests/test_property.py
    PyCalendar/trunk/src/pycalendar/vcard/tests/test_validation.py
    PyCalendar/trunk/src/pycalendar/vcard/validation.py
    PyCalendar/trunk/src/zonal/__init__.py
    PyCalendar/trunk/src/zonal/rule.py
    PyCalendar/trunk/src/zonal/tests/
    PyCalendar/trunk/src/zonal/tests/__init__.py
    PyCalendar/trunk/src/zonal/tests/test_rule.py
    PyCalendar/trunk/src/zonal/tests/test_zone.py
    PyCalendar/trunk/src/zonal/tzconvert.py
    PyCalendar/trunk/src/zonal/tzdump.py
    PyCalendar/trunk/src/zonal/tzverify.py
    PyCalendar/trunk/src/zonal/utils.py
    PyCalendar/trunk/src/zonal/zone.py

Property Changed:
----------------
    PyCalendar/trunk/


Property changes on: PyCalendar/trunk
___________________________________________________________________
Added: svn:ignore
   + stuff
.settings

Added: svn:mergeinfo
   + /PyCalendar/branches/duplicate-items:10464-10465
/PyCalendar/branches/revised-api-126:10457-10463
/PyCalendar/branches/server:10466-10553

Modified: PyCalendar/trunk/setup.py
===================================================================
--- PyCalendar/trunk/setup.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/setup.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,5 +1,5 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
+#    Copyright (c) 2007-2011 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.
@@ -18,10 +18,14 @@
 
 setup (
     name = "pycalendar",
-    version = "1.0",
-    description = "iCalendar Library",
+    version = "2.0",
+    description = "iCalendar/vCard Library",
+    license = "Apache 2.0",
+    platforms = ["any"],
     package_dir={'': 'src'},
     packages = [
         'pycalendar',
+        'pycalendar.icalendar',
+        'pycalendar.vcard',
     ]
 )

Modified: PyCalendar/trunk/src/pycalendar/__init__.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/__init__.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/__init__.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -16,17 +16,20 @@
 
 __all__ = [
     "attribute",
+    "available",
+    "binaryvalue",
     "caladdressvalue",
     "calendar",
     "datetime",
     "datetimevalue",
     "definitions",
-    "dummyvalue",
     "duration",
     "durationvalue",
+    "exceptions",
     "freebusy",
     "integervalue",
     "locale",
+    "manager",
     "multivalue",
     "period",
     "periodvalue",
@@ -34,21 +37,39 @@
     "property",
     "recurrence",
     "recurrencevalue",
+    "requeststatusvalue",
     "textvalue",
     "timezone",
+    "timezonedb",
+    "unknownvalue",
     "urivalue",
     "utcoffsetvalue",
-    "utils",
+    "valarm",
     "value",
     "vevent",
     "vfreebusy",
-    "stringutils"
+    "vjournal",
+    "vtimezone",
+    "vtimezonedaylight",
+    "vtimezonestandard",
+    "vtodo",
+    "vunknown",
 ]
 
 # Import these to register the values
-import caladdressvalue, datetimevalue, dummyvalue, durationvalue, \
-        integervalue, multivalue, periodvalue, recurrencevalue, \
-        textvalue, urivalue, utcoffsetvalue
-    
+import binaryvalue
+import caladdressvalue
+import datetimevalue
+import durationvalue
+import integervalue
+import multivalue
+import periodvalue
+import recurrencevalue
+import requeststatusvalue
+import textvalue
+import unknownvalue
+import urivalue
+import utcoffsetvalue
+
 # Import these to force static initialisation
 import property

Copied: PyCalendar/trunk/src/pycalendar/adr.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/adr.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/adr.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/adr.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,127 @@
+##
+#    Copyright (c) 2007-2012 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.
+##
+
+# vCard ADR value
+
+from pycalendar import utils
+from pycalendar.valueutils import ValueMixin
+
+class Adr(ValueMixin):
+    """
+    mValue is a tuple of seven str or tuples of str
+    """
+
+    (
+        POBOX,
+        EXTENDED,
+        STREET,
+        LOCALITY,
+        REGION,
+        POSTALCODE,
+        COUNTRY,
+        MAXITEMS
+    ) = range(8)
+
+    def __init__(self, pobox="", extended="", street="", locality="", region="", postalcode="", country=""):
+        self.mValue = (pobox, extended, street, locality, region, postalcode, country)
+
+
+    def duplicate(self):
+        return Adr(*self.mValue)
+
+
+    def __hash__(self):
+        return hash(self.mValue)
+
+
+    def __repr__(self):
+        return "ADR %s" % (self.getText(),)
+
+
+    def __eq__(self, comp):
+        return self.mValue == comp.mValue
+
+
+    def getPobox(self):
+        return self.mValue[Adr.POBOX]
+
+
+    def setPobox(self, value):
+        self.mValue[Adr.POBOX] = value
+
+
+    def getExtended(self):
+        return self.mValue[Adr.EXTENDED]
+
+
+    def setExtended(self, value):
+        self.mValue[Adr.EXTENDED] = value
+
+
+    def getStreet(self):
+        return self.mValue[Adr.STREET]
+
+
+    def setStreet(self, value):
+        self.mValue[Adr.STREET] = value
+
+
+    def getLocality(self):
+        return self.mValue[Adr.LOCALITY]
+
+
+    def setLocality(self, value):
+        self.mValue[Adr.LOCALITY] = value
+
+
+    def getRegion(self):
+        return self.mValue[Adr.REGION]
+
+
+    def setRegion(self, value):
+        self.mValue[Adr.REGION] = value
+
+
+    def getPostalCode(self):
+        return self.mValue[Adr.POSTALCODE]
+
+
+    def setPostalCode(self, value):
+        self.mValue[Adr.POSTALCODE] = value
+
+
+    def getCountry(self):
+        return self.mValue[Adr.COUNTRY]
+
+
+    def setCountry(self, value):
+        self.mValue[Adr.COUNTRY] = value
+
+
+    def parse(self, data):
+        self.mValue = utils.parseDoubleNestedList(data, Adr.MAXITEMS)
+
+
+    def generate(self, os):
+        utils.generateDoubleNestedList(os, self.mValue)
+
+
+    def getValue(self):
+        return self.mValue
+
+
+    def setValue(self, value):
+        self.mValue = value

Copied: PyCalendar/trunk/src/pycalendar/adrvalue.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/adrvalue.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/adrvalue.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/adrvalue.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,51 @@
+##
+#    Copyright (c) 2007-2012 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.
+##
+
+# vCard ADR value
+
+from pycalendar.adr import Adr
+from pycalendar.value import PyCalendarValue
+
+class AdrValue(PyCalendarValue):
+
+    def __init__(self, value=None):
+        self.mValue = value if value is not None else Adr()
+
+
+    def duplicate(self):
+        return AdrValue(self.mValue.duplicate())
+
+
+    def getType(self):
+        return PyCalendarValue.VALUETYPE_ADR
+
+
+    def parse(self, data):
+        self.mValue.parse(data)
+
+
+    def generate(self, os):
+        self.mValue.generate(os)
+
+
+    def getValue(self):
+        return self.mValue
+
+
+    def setValue(self, value):
+        self.mValue = value
+
+PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_ADR, AdrValue, None)

Modified: PyCalendar/trunk/src/pycalendar/attribute.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/attribute.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/attribute.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -18,83 +18,111 @@
 ICalendar attribute.
 
 The attribute can consist of one or more values, all string.
-We optimise for the usual case of a single value by having a single string
-attribute in the class, and then an array for multi-values, which is None
-unless there is more than one value.
 """
 
+from pycalendar import xmldefs
+from pycalendar.utils import encodeParameterValue
+import xml.etree.cElementTree as XML
+
 class PyCalendarAttribute(object):
 
-    def __init__( self, name = None, value = None, copyit = None ):
-        
-        if name and value:
-            self.mName = name
-            self.mValue = value
-            self.mValues = None
-        elif copyit:
-            self.mName = copyit.mName
-            self.mValue = copyit.mValue
-            if copyit.mValues is not None:
-                self.mValues = [i for i in copyit.mValues]
-            else:
-                self.mValues = None
+    def __init__(self, name, value=None):
+        self.mName = name
+        if value is None:
+            self.mValues = []
+        elif isinstance(value, basestring):
+            self.mValues = [value]
         else:
-            self.mName = ''
-            self.mValue = ''
-            self.mValues = None
+            self.mValues = value
 
-    def getName( self ):
+
+    def duplicate(self):
+        other = PyCalendarAttribute(self.mName)
+        other.mValues = self.mValues[:]
+        return other
+
+
+    def __hash__(self):
+        return hash((self.mName.upper(), tuple(self.mValues)))
+
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+
+    def __eq__(self, other):
+        if not isinstance(other, PyCalendarAttribute):
+            return False
+        return self.mName.upper() == other.mName.upper() and self.mValues == other.mValues
+
+
+    def getName(self):
         return self.mName
 
-    def setName( self, name ):
+
+    def setName(self, name):
         self.mName = name
 
-    def getFirstValue( self ):
-        if self.mValues is not None:
-            return self.mValues[0]
-        else:
-            return self.mValue
 
-    def getValues( self ):
+    def getFirstValue(self):
+        return self.mValues[0]
+
+
+    def getValues(self):
         return self.mValues
 
-    def setValues( self, values ):
+
+    def setValues(self, values):
         self.mValues = values
 
-    def addValue( self, value ):
-        # See if switch from single to multi-value is needed
-        if self.mValues is None:
-            self.mValues = []
-            self.mValues.append( self.mValue )
-            self.mValue = None
-        self.mValues.append( value )
 
-    def generate( self, os ):
+    def addValue(self, value):
+        self.mValues.append(value)
+
+
+    def removeValue(self, value):
+        self.mValues.remove(value)
+        return len(self.mValues)
+
+
+    def generate(self, os):
         try:
-            os.write( self.mName )
-            os.write( "=" )
+            os.write(self.mName)
 
-            if self.mValues is None:
-                # Write with quotation if required
-                self.generateValue( os, self.mValue )
-            else:
+            # To support vCard 2.1 syntax we allow parameters without values
+            if self.mValues:
+                os.write("=")
+
                 first = True
                 for s in self.mValues:
                     if first:
                         first = False
                     else:
-                        os.write( "," )
+                        os.write(",")
 
                     # Write with quotation if required
-                    self.generateValue( os, s )
+                    self.generateValue(os, s)
 
         except:
             # We ignore errors
             pass
-    
-    def generateValue( self, os, str ):
+
+
+    def generateValue(self, os, str):
+
+        # ^-escaping
+        str = encodeParameterValue(str)
+
         # Look for quoting
-        if str.find( ":" ) != -1 or str.find( ";" ) != -1 or str.find( "," ) != -1:
-            os.write( "\"%s\"" % (str,) )
+        if str.find(":") != -1 or str.find(";") != -1 or str.find(",") != -1:
+            os.write("\"%s\"" % (str,))
         else:
-            os.write( str )
+            os.write(str)
+
+
+    def writeXML(self, node, namespace):
+        param = XML.SubElement(node, xmldefs.makeTag(namespace, self.getName()))
+        for value in self.getValues():
+            # TODO: need to figure out proper value types
+            text = XML.SubElement(param, xmldefs.makeTag(namespace, xmldefs.value_text))
+            text.text = value

Copied: PyCalendar/trunk/src/pycalendar/available.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/available.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/available.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/available.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,90 @@
+##
+#    Copyright (c) 2011-2012 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 import definitions
+from pycalendar.componentrecur import PyCalendarComponentRecur
+from pycalendar.icalendar.validation import ICALENDAR_VALUE_CHECKS
+
+class PyCalendarAvailable(PyCalendarComponentRecur):
+
+    propertyCardinality_1 = (
+        definitions.cICalProperty_DTSTAMP,
+        definitions.cICalProperty_DTSTART,
+        definitions.cICalProperty_UID,
+    )
+
+    propertyCardinality_0_1 = (
+        definitions.cICalProperty_CREATED,
+        definitions.cICalProperty_DESCRIPTION,
+        definitions.cICalProperty_GEO,
+        definitions.cICalProperty_LAST_MODIFIED,
+        definitions.cICalProperty_LOCATION,
+        definitions.cICalProperty_RECURRENCE_ID,
+        definitions.cICalProperty_RRULE,
+        definitions.cICalProperty_SEQUENCE,
+        definitions.cICalProperty_SUMMARY,
+        definitions.cICalProperty_DTEND,
+        definitions.cICalProperty_DURATION,
+    )
+
+    propertyValueChecks = ICALENDAR_VALUE_CHECKS
+
+    def __init__(self, parent=None):
+        super(PyCalendarAvailable, self).__init__(parent=parent)
+
+
+    def duplicate(self, parent=None):
+        return super(PyCalendarAvailable, self).duplicate(parent=parent)
+
+
+    def getType(self):
+        return definitions.cICalComponent_AVAILABLE
+
+
+    def validate(self, doFix=False):
+        """
+        Validate the data in this component and optionally fix any problems, else raise. If
+        loggedProblems is not None it must be a C{list} and problem descriptions are appended
+        to that.
+        """
+
+        fixed, unfixed = super(PyCalendarAvailable, self).validate(doFix)
+
+        # Extra constraint: only one of DTEND or DURATION
+        if self.hasProperty(definitions.cICalProperty_DTEND) and self.hasProperty(definitions.cICalProperty_DURATION):
+            # Fix by removing the DTEND
+            logProblem = "[%s] Properties must not both be present: %s, %s" % (
+                self.getType(),
+                definitions.cICalProperty_DTEND,
+                definitions.cICalProperty_DURATION,
+            )
+            if doFix:
+                self.removeProperties(definitions.cICalProperty_DTEND)
+                fixed.append(logProblem)
+            else:
+                unfixed.append(logProblem)
+
+        return fixed, unfixed
+
+
+    def sortedPropertyKeyOrder(self):
+        return (
+            definitions.cICalProperty_UID,
+            definitions.cICalProperty_RECURRENCE_ID,
+            definitions.cICalProperty_DTSTART,
+            definitions.cICalProperty_DURATION,
+            definitions.cICalProperty_DTEND,
+        )

Copied: PyCalendar/trunk/src/pycalendar/binaryvalue.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/binaryvalue.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/binaryvalue.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/binaryvalue.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,28 @@
+##
+#    Copyright (c) 2011-2012 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.
+##
+
+# iCalendar Binary value
+
+from pycalendar import xmldefs
+from pycalendar.plaintextvalue import PyCalendarPlainTextValue
+from pycalendar.value import PyCalendarValue
+
+class PyCalendarBinaryValue(PyCalendarPlainTextValue):
+
+    def getType(self):
+        return PyCalendarValue.VALUETYPE_BINARY
+
+PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_BINARY, PyCalendarBinaryValue, xmldefs.value_binary)

Modified: PyCalendar/trunk/src/pycalendar/caladdressvalue.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/caladdressvalue.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/caladdressvalue.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -16,12 +16,13 @@
 
 # iCalendar UTC Offset value
 
-from plaintextvalue import PyCalendarPlainTextValue
-from value import PyCalendarValue
+from pycalendar import xmldefs
+from pycalendar.plaintextvalue import PyCalendarPlainTextValue
+from pycalendar.value import PyCalendarValue
 
-class PyCalendarCalAddressValue( PyCalendarPlainTextValue ):
+class PyCalendarCalAddressValue(PyCalendarPlainTextValue):
 
-    def getType( self ):
+    def getType(self):
         return PyCalendarValue.VALUETYPE_CALADDRESS
 
-PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_CALADDRESS, PyCalendarCalAddressValue)
+PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_CALADDRESS, PyCalendarCalAddressValue, xmldefs.value_cal_address)

Modified: PyCalendar/trunk/src/pycalendar/calendar.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/calendar.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/calendar.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,25 +14,33 @@
 #    limitations under the License.
 ##
 
-from component import PyCalendarComponent
-from componentbase import PyCalendarComponentBase
-from componentdb import PyCalendarComponentDB
-from componentexpanded import PyCalendarComponentExpanded
-from componentrecur import PyCalendarComponentRecur
-from datetime import PyCalendarDateTime
-from freebusy import PyCalendarFreeBusy
-from period import PyCalendarPeriod
-from property import PyCalendarProperty
-from utils import readFoldedLine
-from valarm import PyCalendarVAlarm
-from vevent import PyCalendarVEvent
-from vfreebusy import PyCalendarVFreeBusy
-from vjournal import PyCalendarVJournal
-from vtimezone import PyCalendarVTimezone
-from vtimezonedaylight import PyCalendarVTimezoneDaylight
-from vtimezonestandard import PyCalendarVTimezoneStandard
-from vtodo import PyCalendarVToDo
-import definitions
+from cStringIO import StringIO
+from pycalendar import definitions, xmldefs
+from pycalendar.available import PyCalendarAvailable
+from pycalendar.componentbase import PyCalendarComponentBase
+from pycalendar.componentexpanded import PyCalendarComponentExpanded
+from pycalendar.componentrecur import PyCalendarComponentRecur
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.exceptions import PyCalendarInvalidData, \
+    PyCalendarValidationError
+from pycalendar.freebusy import PyCalendarFreeBusy
+from pycalendar.icalendar.validation import ICALENDAR_VALUE_CHECKS
+from pycalendar.parser import ParserContext
+from pycalendar.period import PyCalendarPeriod
+from pycalendar.property import PyCalendarProperty
+from pycalendar.utils import readFoldedLine
+from pycalendar.valarm import PyCalendarVAlarm
+from pycalendar.vavailability import PyCalendarVAvailability
+from pycalendar.vevent import PyCalendarVEvent
+from pycalendar.vfreebusy import PyCalendarVFreeBusy
+from pycalendar.vjournal import PyCalendarVJournal
+from pycalendar.vtimezone import PyCalendarVTimezone
+from pycalendar.vtimezonedaylight import PyCalendarVTimezoneDaylight
+from pycalendar.vtimezonestandard import PyCalendarVTimezoneStandard
+from pycalendar.vtodo import PyCalendarVToDo
+from pycalendar.vunknown import PyCalendarUnknownComponent
+import collections
+import xml.etree.cElementTree as XML
 
 class PyCalendar(PyCalendarComponentBase):
 
@@ -43,133 +51,95 @@
     FIND_EXACT = 0
     FIND_MASTER = 1
 
-    VEVENT = 0
-    VTODO = 1
-    VJOURNAL = 2
-    VFREEBUSY = 3
-    VTIMEZONE = 4
-    MAXV = 5
-
-    # static attributes
-    sICalendars = {}
-    sICalendarRefCtr = 1
-    sICalendar = None
-
     sProdID = "-//mulberrymail.com//Mulberry v4.0//EN"
     sDomain = "mulberrymail.com"
 
     @staticmethod
-    def getICalendar(ref):
-        if PyCalendar.sICalendars.has_key(ref):
-            return PyCalendar.sICalendars[ref]
-        else:
-            return None
-
-    @staticmethod
-    def loadStatics():
-        PyCalendar.initComponents()
-
-    @staticmethod
     def setPRODID(prodid):
         PyCalendar.sProdID = prodid
 
+
     @staticmethod
     def setDomain(domain):
         PyCalendar.sDomain = domain
 
-    def __init__(self):
-        super(PyCalendar, self).__init__()
+    propertyCardinality_1 = (
+        definitions.cICalProperty_PRODID,
+        definitions.cICalProperty_VERSION,
+    )
 
-        self.mICalendarRef = ++PyCalendar.sICalendarRefCtr
-        PyCalendar.sICalendars[self.mICalendarRef] = self
-    
-        self.mReadOnly = False
-        self.mDirty = False
-        self.mTotalReplace = False
+    propertyCardinality_0_1 = (
+        definitions.cICalProperty_CALSCALE,
+        definitions.cICalProperty_METHOD,
+    )
 
+    propertyValueChecks = ICALENDAR_VALUE_CHECKS
+
+    def __init__(self, parent=None, add_defaults=True):
+        super(PyCalendar, self).__init__(None)
+
         self.mName = ""
         self.mDescription = ""
-    
-        self.addDefaultProperties()
-    
-        # Special init for static item
-        if self is PyCalendar.sICalendar:
-            self.initDefaultTimezones()
+        self.mMasterComponentsByTypeAndUID = collections.defaultdict(lambda: collections.defaultdict(list))
+        self.mOverriddenComponentsByUID = collections.defaultdict(list)
 
-        self.mV = []
-        for _ignore_i in range(PyCalendar.MAXV):
-            self.mV.append(PyCalendarComponentDB())
+        if add_defaults:
+            self.addDefaultProperties()
 
-        # Special init for static item
-        if self is PyCalendar.sICalendar:
-            self.initDefaultTimezones()
-    
-    def close(self):
-        # Clean up the map items
-        for v in self.mV:
-            v.close()
 
-    def getRef(self):
-        return self.mICalendarRef
+    def duplicate(self):
+        other = super(PyCalendar, self).duplicate()
+        other.mName = self.mName
+        other.mDescription = self.mDescription
+        return other
 
-    def remove(self):
-        # Broadcast closing before removing components
-        # Broadcast_Message(eBroadcast_Closed, this)
-    
-        # Clean up the map items
-        for v in self.mV:
-            v.removeAllComponents()
-    
-        del PyCalendar.sICalendars[self.mICalendarRef]
 
+    def getType(self):
+        return definitions.cICalComponent_VCALENDAR
+
+
     def getName(self):
         return self.mName
-    
+
+
     def setName(self, name):
         self.mName = name
 
+
     def editName(self, name):
         if self.mName != name:
             # Updated cached value
             self.mName = name
-    
+
             # Remove existing items
             self.removeProperties(definitions.cICalProperty_XWRCALNAME)
-            
+
             # Now create properties
-            if name.length():
+            if len(name):
                 self.ddProperty(PyCalendarProperty(definitions.cICalProperty_XWRCALNAME, name))
-            
-            # Mark as dirty
-            self.setDirty()
-            
-            # Broadcast change
-            #Broadcast_Message(eBroadcast_Edit, this)
 
+
     def getDescription(self):
         return self.mDescription
-    
+
+
     def setDescription(self, description):
         self.mDescription = description
 
+
     def editDescription(self, description):
         if self.mDescription != description:
             # Updated cached value
             self.mDescription = description
-    
+
             # Remove existing items
             self.removeProperties(definitions.cICalProperty_XWRCALDESC)
-            
+
             # Now create properties
-            if description.length():
+            if len(description):
                 self.addProperty(PyCalendarProperty(definitions.cICalProperty_XWRCALDESC, description))
-            
-            # Mark as dirty
-            self.setDirty()
-            
-            # Broadcast change
-            #Broadcast_Message(eBroadcast_Edit, this)
 
+
     def getMethod(self):
         result = ""
         if self.hasProperty(definitions.cICalProperty_METHOD):
@@ -179,397 +149,359 @@
 
     def finalise(self):
         # Get calendar name if present
-    
+
         # Get X-WR-CALNAME
         temps = self.loadValueString(definitions.cICalProperty_XWRCALNAME)
         if temps is not None:
             self.mName = temps
-    
+
         # Get X-WR-CALDESC
         temps = self.loadValueString(definitions.cICalProperty_XWRCALDESC)
         if temps is not None:
             self.mDescription = temps
 
+
+    def validate(self, doFix=False, doRaise=False):
+        """
+        Validate the data in this component and optionally fix any problems. Return
+        a tuple containing two lists: the first describes problems that were fixed, the
+        second problems that were not fixed. Caller can then decide what to do with unfixed
+        issues.
+        """
+
+        # Optional raise behavior
+        fixed, unfixed = super(PyCalendar, self).validate(doFix)
+        if doRaise and unfixed:
+            raise PyCalendarValidationError(";".join(unfixed))
+        return fixed, unfixed
+
+
+    def sortedComponentNames(self):
+        return (
+            definitions.cICalComponent_VTIMEZONE,
+            definitions.cICalComponent_VEVENT,
+            definitions.cICalComponent_VTODO,
+            definitions.cICalComponent_VJOURNAL,
+            definitions.cICalComponent_VFREEBUSY,
+            definitions.cICalComponent_VAVAILABILITY,
+        )
+
+
+    def sortedPropertyKeyOrder(self):
+        return (
+            definitions.cICalProperty_VERSION,
+            definitions.cICalProperty_CALSCALE,
+            definitions.cICalProperty_METHOD,
+            definitions.cICalProperty_PRODID,
+        )
+
+
+    @staticmethod
+    def parseText(data):
+
+        cal = PyCalendar(add_defaults=False)
+        if cal.parse(StringIO(data)):
+            return cal
+        else:
+            return None
+
+
     def parse(self, ins):
 
         result = False
-    
+
+        self.setProperties({})
+
         LOOK_FOR_VCALENDAR = 0
         GET_PROPERTY_OR_COMPONENT = 1
-        GET_COMPONENT_PROPERTY = 2
-        GET_SUB_COMPONENT_PROPERTY = 3
-    
+
         state = LOOK_FOR_VCALENDAR
-    
+
         # Get lines looking for start of calendar
-        lines = ["", ""]
-        comp = None
+        lines = [None, None]
+        comp = self
         compend = None
-        prevcomp = None
-        prevcompend = None
-        compmap = None
-    
+        componentstack = []
+
         while readFoldedLine(ins, lines):
-            
+
             line = lines[0]
+
             if state == LOOK_FOR_VCALENDAR:
 
                 # Look for start
-                if line == definitions.cICalComponent_BEGINVCALENDAR:
+                if line == self.getBeginDelimiter():
                     # Next state
                     state = GET_PROPERTY_OR_COMPONENT
-    
+
                     # Indicate success at this point
                     result = True
 
+                # Handle blank line
+                elif len(line) == 0:
+                    # Raise if requested, otherwise just ignore
+                    if ParserContext.BLANK_LINES_IN_DATA == ParserContext.PARSER_RAISE:
+                        raise PyCalendarInvalidData("iCalendar data has blank lines")
+
+                # Unrecognized data
+                else:
+                    raise PyCalendarInvalidData("iCalendar data not recognized", line)
+
             elif state == GET_PROPERTY_OR_COMPONENT:
 
                 # Parse property or look for start of component
-                if PyCalendar.sComponents.has_key(line):
+                if line.startswith("BEGIN:"):
 
+                    # Push previous details to stack
+                    componentstack.append((comp, compend,))
+
                     # Start a new component
-                    comp = PyCalendar.makeComponent(PyCalendar.sComponents[line].mID, self.getRef())
+                    comp = PyCalendar.makeComponent(line[6:], comp)
                     compend = comp.getEndDelimiter()
-    
-                    # Set the marker for the end of this component and the map to store it in
-                    compmap = self.getComponents(PyCalendar.sComponents[line].mType)
-    
-                    # Change state
-                    state = GET_COMPONENT_PROPERTY
 
-                elif line == definitions.cICalComponent_ENDVCALENDAR:
+                # Look for end of object
+                elif line == self.getEndDelimiter():
 
                     # Finalise the current calendar
                     self.finalise()
-    
+
                     # Change state
                     state = LOOK_FOR_VCALENDAR
 
-                else:
-
-                    # Parse attribute/value for top-level calendar item
-                    prop = PyCalendarProperty()
-                    if prop.parse(line):
-
-                        # Check for valid property
-                        if not self.validProperty(prop):
-                            return False
-                        elif not self.ignoreProperty(prop):
-                            self.addProperty(prop)
-
-            elif state in (GET_COMPONENT_PROPERTY, GET_SUB_COMPONENT_PROPERTY):
-
                 # Look for end of current component
-                if line == compend:
+                elif line == compend:
 
                     # Finalise the component (this caches data from the properties)
                     comp.finalise()
-    
-                    # Check whether this is embedded
-                    if prevcomp is not None:
 
-                        # Embed component in parent and reset to use parent
-                        prevcomp.addComponent(comp)
-                        comp = prevcomp
-                        compend = prevcompend
-                        prevcomp = None
-                        prevcompend = None
-    
-                        # Reset state
-                        state = GET_COMPONENT_PROPERTY
+                    # Embed component in parent and reset to use parent
+                    componentstack[-1][0].addComponent(comp)
+                    comp, compend = componentstack.pop()
 
-                    else:
+                # Blank line
+                elif len(line) == 0:
+                    # Raise if requested, otherwise just ignore
+                    if ParserContext.BLANK_LINES_IN_DATA == ParserContext.PARSER_RAISE:
+                        raise PyCalendarInvalidData("iCalendar data has blank lines")
 
-                        # Check for valid component
-                        compmap.addComponent(comp)
-                        comp = None
-                        compend = None
-                        compmap = None
-    
-                        # Reset state
-                        state = GET_PROPERTY_OR_COMPONENT
-
+                # Must be a property
                 else:
 
-                    # Look for start of embedded component (can only do once)
-                    if (state != GET_SUB_COMPONENT_PROPERTY) and PyCalendar.sEmbeddedComponents.has_key(line):
+                    # Parse attribute/value for top-level calendar item
+                    prop = PyCalendarProperty()
+                    if prop.parse(line):
 
-                        # Start a new component (saving off the current one)
-                        prevcomp = comp
-                        prevcompend = compend
-                        comp = PyCalendar.makeComponent(PyCalendar.sEmbeddedComponents[line].mID, self.getRef())
-                        compend = comp.getEndDelimiter()
-    
-                        # Reset state
-                        state = GET_SUB_COMPONENT_PROPERTY
+                        # Check for valid property
+                        if comp is self:
+                            if not comp.validProperty(prop):
+                                raise PyCalendarInvalidData("Invalid property", str(prop))
+                            elif not comp.ignoreProperty(prop):
+                                comp.addProperty(prop)
+                        else:
+                            comp.addProperty(prop)
 
-                    else:
+        # Check for truncated data
+        if state != LOOK_FOR_VCALENDAR:
+            raise PyCalendarInvalidData("iCalendar data not complete")
 
-                        # Parse attribute/value and store in component
-                        prop = PyCalendarProperty()
-                        if prop.parse(line):
-                            comp.addProperty(prop)
-    
         # We need to store all timezones in the static object so they can be accessed by any date object
-        if self is not PyCalendar.sICalendar:
-            PyCalendar.sICalendar.mergeTimezones(self)
-    
+        from timezonedb import PyCalendarTimezoneDatabase
+        PyCalendarTimezoneDatabase.mergeTimezones(self, self.getComponents(definitions.cICalComponent_VTIMEZONE))
+
+        # Validate some things
+        if result and not self.hasProperty("VERSION"):
+            raise PyCalendarInvalidData("iCalendar missing VERSION")
+
         return result
-    
-    def parseComponent(self, ins, rurl, etag):
-        
+
+
+    def parseComponent(self, ins):
+
         result = None
-    
+
         LOOK_FOR_VCALENDAR = 0
         GET_PROPERTY_OR_COMPONENT = 1
-        GET_COMPONENT_PROPERTY = 2
-        GET_SUB_COMPONENT_PROPERTY = 3
         GOT_VCALENDAR = 4
-    
+
         state = LOOK_FOR_VCALENDAR
-    
+
         # Get lines looking for start of calendar
-        lines = ["", ""]
-        comp = None
-        prevcomp = None
-        compmap = None
+        lines = [None, None]
+        comp = self
+        compend = None
+        componentstack = []
         got_timezone = False
-    
+
         while readFoldedLine(ins, lines):
 
             if state == LOOK_FOR_VCALENDAR:
 
                 # Look for start
-                if lines[0] == definitions.cICalComponent_BEGINVCALENDAR:
+                if lines[0] == self.getBeginDelimiter():
                     # Next state
                     state = GET_PROPERTY_OR_COMPONENT
 
+                # Handle blank line
+                elif len(lines[0]) == 0:
+                    # Raise if requested, otherwise just ignore
+                    if ParserContext.BLANK_LINES_IN_DATA == ParserContext.PARSER_RAISE:
+                        raise PyCalendarInvalidData("iCalendar data has blank lines")
+
+                # Unrecognized data
+                else:
+                    raise PyCalendarInvalidData("iCalendar data not recognized", lines[0])
+
             elif state == GET_PROPERTY_OR_COMPONENT:
 
                 # Parse property or look for start of component
-                if PyCalendar.sComponents.has_key(lines[0]):
+                if lines[0].startswith("BEGIN:"):
 
+                    # Push previous details to stack
+                    componentstack.append((comp, compend,))
+
                     # Start a new component
-                    comp = PyCalendar.makeComponent(PyCalendar.sComponents[lines[0]].mID, self.getRef())
-                    
+                    comp = PyCalendar.makeComponent(lines[0][6:], comp)
+                    compend = comp.getEndDelimiter()
+
                     # Cache as result - but only the first one, we ignore the rest
                     if result is None:
                         result = comp
-    
-                    # Set the marker for the end of this component and the map to store it in
-                    compmap = self.getComponents(PyCalendar.sComponents[lines[0]].mType)
-    
+
                     # Look for timezone component to trigger timezone merge only if one is present
-                    if PyCalendar.sComponents[lines[0]].mType == PyCalendarComponent.eVTIMEZONE:
+                    if comp.getType() == definitions.cICalComponent_VTIMEZONE:
                         got_timezone = True
-    
-                    # Change state
-                    state = GET_COMPONENT_PROPERTY
 
-                elif lines[0] == definitions.cICalComponent_ENDVCALENDAR:
+                elif lines[0] == self.getEndDelimiter():
 
                     # Change state
                     state = GOT_VCALENDAR
 
-                else:
-
-                    # Ignore top-level items
-                    pass
-
-            elif state in (GET_COMPONENT_PROPERTY, GET_SUB_COMPONENT_PROPERTY):
                 # Look for end of current component
-                if lines[0] == comp.getEndDelimiter():
+                elif lines[0] == compend:
 
                     # Finalise the component (this caches data from the properties)
                     comp.finalise()
-                    comp.setRURL(rurl)
-                    comp.setETag(etag)
-    
-                    # Check whether this is embedded
-                    if prevcomp is not None:
 
-                        # Embed component in parent and reset to use parent
-                        if not prevcomp.addComponent(comp):
-                            comp = None
-                        comp = prevcomp
-                        prevcomp = None
-    
-                        # Reset state
-                        state = GET_COMPONENT_PROPERTY
+                    # Embed component in parent and reset to use parent
+                    componentstack[-1][0].addComponent(comp)
+                    comp, compend = componentstack.pop()
 
-                    else:
+                # Blank line
+                elif len(lines[0]) == 0:
+                    # Raise if requested, otherwise just ignore
+                    if ParserContext.BLANK_LINES_IN_DATA == ParserContext.PARSER_RAISE:
+                        raise PyCalendarInvalidData("iCalendar data has blank lines")
 
-                        # Check for valid component
-                        if not compmap.addComponent(comp):
+                # Ignore top-level items
+                elif comp is self:
+                    pass
 
-                            if result == comp:
-                                result = None
-                            comp = None
-
-                        comp = None
-                        compmap = None
-    
-                        # Reset state
-                        state = GET_PROPERTY_OR_COMPONENT
-
+                # Must be a property
                 else:
 
-                    # Look for start of embedded component (can only do once)
-                    if (state != GET_SUB_COMPONENT_PROPERTY) and PyCalendar.sEmbeddedComponents.has_key(lines[0]):
+                    # Parse attribute/value for top-level calendar item
+                    prop = PyCalendarProperty()
+                    if prop.parse(lines[0]):
 
-                        # Start a new component (saving off the current one)
-                        prevcomp = comp
-                        comp = PyCalendar.makeComponent(PyCalendar.sEmbeddedComponents[lines[0]].mID, self.getRef())
-    
-                        # Reset state
-                        state = GET_SUB_COMPONENT_PROPERTY
-
-                    else:
-
-                        # Parse attribute/value and store in component
-                        prop = PyCalendarProperty
-                        if prop.parse(lines[0]):
+                        # Check for valid property
+                        if comp is not self:
                             comp.addProperty(prop)
-            
+
             # Exit if we have one - ignore all the rest
             if state == GOT_VCALENDAR:
                 break
-    
+
         # We need to store all timezones in the static object so they can be accessed by any date object
         # Only do this if we read in a timezone
-        if got_timezone and (self is not PyCalendar.sICalendar):
-            PyCalendar.sICalendar.mergeTimezones(self)
-    
+        if got_timezone:
+            from timezonedb import PyCalendarTimezoneDatabase
+            PyCalendarTimezoneDatabase.mergeTimezones(self, self.getComponents(definitions.cICalComponent_VTIMEZONE))
+
         return result
-        
-    def generate(self, os, for_cache = False):
-        # Make sure all required timezones are in this object
-        self.includeTimezones()
-        
-        # Write header
-        os.write(definitions.cICalComponent_BEGINVCALENDAR)
-        os.write("\n")
-        
-        # Write properties (we always handle PRODID & VERSION)
-        self.writeProperties(os)
-        
-        # Write out each type of component (not VALARMS which are embedded in others)
-        # Do VTIMEZONES at the start
-        self.generateDB(os, self.mV[PyCalendar.VTIMEZONE], for_cache)
-        self.generateDB(os, self.mV[PyCalendar.VEVENT], for_cache)
-        self.generateDB(os, self.mV[PyCalendar.VTODO], for_cache)
-        self.generateDB(os, self.mV[PyCalendar.VJOURNAL], for_cache)
-        self.generateDB(os, self.mV[PyCalendar.VFREEBUSY], for_cache)
-        
-        # Write trailer
-        os.write(definitions.cICalComponent_ENDVCALENDAR)
-        os.write("\n")
-        
-    def generateOne(self, os, comp):
-        # Write header
-        os.write(definitions.cICalComponent_BEGINVCALENDAR)
-        os.write("\n")
-   
-        # Write properties (we always handle PRODID & VERSION)
-        self.writeProperties(os)
-    
-        # Make sure each timezone is written out
-        tzids = set()
-        comp.getTimezones(tzids)
-        for tzid in tzids:
 
-            tz = self.getTimezone(tzid)
-            if tz == None:
 
-                # Find it in the static object
-                tz = PyCalendar.sICalendar.getTimezone(tzid)
-            
-            if tz is not None:
-                tz.generate(os, False)
-    
-        # Check for recurring component and potentially write out all instances
-        if isinstance(comp, PyCalendarComponentRecur):
+    def addComponent(self, component):
+        """
+        Override to track components by UID.
+        """
+        super(PyCalendar, self).addComponent(component)
 
-            # Write this one out first
-            comp.generate(os, False)
-            
-            # Get list of all instances
-            instances = self.getRecurrenceInstances(comp.getType(), comp.getUID())
-    
-            # Write each instance out
-            for r in instances:
-                # Write the component
-                r.generate(os. False)
+        if isinstance(component, PyCalendarComponentRecur):
+            uid = component.getUID()
+            rid = component.getRecurrenceID()
+            if rid:
+                self.mOverriddenComponentsByUID[uid].append(component)
+            else:
+                self.mMasterComponentsByTypeAndUID[component.getType()][uid] = component
 
-        else:
-            # Write the component
-            comp.generate(os, False)
-    
-        # Write trailer
-        os.write(definitions.cICalComponent_ENDVCALENDAR)
-        os.write("\n")
 
-    # Get components
-    def getVEventsDB(self):
-        return self.mV[PyCalendar.VEVENT]
+    def removeComponent(self, component):
+        """
+        Override to track components by UID.
+        """
+        super(PyCalendar, self).removeComponent(component)
 
-    def getVToDosDB(self):
-        return self.mV[PyCalendar.VTODO]
+        if isinstance(component, PyCalendarComponentRecur):
+            uid = component.getUID()
+            rid = component.getRecurrenceID()
+            if rid:
+                self.mOverriddenComponentsByUID[uid].remove(component)
+            else:
+                del self.mMasterComponentsByTypeAndUID[component.getType()][uid]
 
-    def getVJournalsDB(self):
-        return self.mV[PyCalendar.VJOURNAL]
 
-    def getVFreeBusyDB(self):
-        return self.mV[PyCalendar.VFREEBUSY]
+    def getText(self, includeTimezones=False):
+        s = StringIO()
+        self.generate(s, includeTimezones=includeTimezones)
+        return s.getvalue()
 
-    def getVTimezoneDB(self):
-        return self.mV[PyCalendar.VTIMEZONE]
 
-    def getAllDBs(self, list):
-        
-        list.extend(self.mV)
+    def generate(self, os, includeTimezones=False):
+        # Make sure all required timezones are in this object
+        if includeTimezones:
+            self.includeTimezones()
+        super(PyCalendar, self).generate(os)
 
-    # Disconnected support
-    def getETag(self):
-        return self.mETag
 
-    def setETag(self, etag):
-        self.mETag = etag
+    def getTextXML(self, includeTimezones=False):
+        node = self.writeXML(includeTimezones)
+        return xmldefs.toString(node)
 
-#    def getRecording(self):
-#        return self.mRecordDB
-#
-#    def clearRecording(self):
-#        self.mRecordDB.clear()
-#
-#    def needsSync(self):
-#        return not self.mRecordDB.empty()
 
-    #void    ParseCache(istream& is)
-    #void    GenerateCache(ostream& os) const
+    def writeXML(self, includeTimezones=False):
+        # Make sure all required timezones are in this object
+        if includeTimezones:
+            self.includeTimezones()
 
+        # Root node structure
+        root = XML.Element(xmldefs.makeTag(xmldefs.iCalendar20_namespace, xmldefs.icalendar))
+        super(PyCalendar, self).writeXML(root, xmldefs.iCalendar20_namespace)
+        return root
+
+
     # Get expanded components
-    def getVEvents(self, period, list, all_day_at_top = True):
+    def getVEvents(self, period, list, all_day_at_top=True):
         # Look at each VEvent
-        for vevent in self.mV[PyCalendar.VEVENT]:
+        for vevent in self.getComponents(definitions.cICalComponent_VEVENT):
             vevent.expandPeriod(period, list)
-        
+
         if (all_day_at_top):
             list.sort(PyCalendarComponentExpanded.sort_by_dtstart_allday)
         else:
             list.sort(PyCalendarComponentExpanded.sort_by_dtstart)
-        
+
+
     def getVToDos(self, only_due, all_dates, upto_due_date, list):
         # Get current date-time less one day to test for completed events during the last day
         minusoneday = PyCalendarDateTime()
         minusoneday.setNowUTC()
         minusoneday.offsetDay(-1)
-    
+
         today = PyCalendarDateTime()
         today.setToday()
-    
+
         # Look at each VToDo
-        for vtodo in self.mV[PyCalendar.VTODO]:
+        for vtodo in self.getComponents(definitions.cICalComponent_VTODO):
 
             # Filter out done (that were complted more than a day ago) or cancelled to dos if required
             if only_due:
@@ -578,31 +510,35 @@
                 elif ((vtodo.getStatus() == definitions.eStatus_VToDo_Completed) and
                             (not vtodo.hasCompleted() or (vtodo.getCompleted() < minusoneday))):
                     continue
-    
+
             # Filter out those with end after chosen date if required
             if not all_dates:
                 if vtodo.hasEnd() and (vtodo.getEnd() > upto_due_date):
                     continue
                 elif not vtodo.hasEnd() and (today > upto_due_date):
                     continue
-    
+
             # TODO: fix this
             #list.append(PyCalendarComponentExpandedShared(PyCalendarComponentExpanded(vtodo, None)))
-        
+
+
     def getRecurrenceInstancesItems(self, type, uid, items):
         # Get instances from list
-        self.getComponents(type).getRecurrenceInstancesItems(uid, items)
+        items.extend(self.mOverriddenComponentsByUID.get(uid, ()))
 
+
     def getRecurrenceInstancesIds(self, type, uid, ids):
         # Get instances from list
-        self.getComponents(type).getRecurrenceInstancesIds(uid, ids)
+        ids.extend([comp.getRecurrenceID() for comp in self.mOverriddenComponentsByUID.get(uid, ())])
 
+
     # Freebusy generation
     def getVFreeBusyList(self, period, list):
         # Look at each VFreeBusy
-        for vfreebusy in self.mV[PyCalendar.VFREEBUSY]:
+        for vfreebusy in self.getComponents(definitions.cICalComponent_VFREEBUSY):
             vfreebusy.expandPeriod(period, list)
-        
+
+
     def getVFreeBusyFB(self, period, fb):
         # First create expanded set
         # TODO: fix this
@@ -610,7 +546,7 @@
         self.getVEvents(period, list)
         if len(list) == 0:
             return
-        
+
         # Get start/end list for each non-all-day expanded components
         dtstart = []
         dtend = []
@@ -619,19 +555,19 @@
             # Ignore if all-day
             if dt.getInstanceStart().isDateOnly():
                 continue
-            
+
             # Ignore if transparent to free-busy
             transp = ""
             if dt.getOwner().getProperty(definitions.cICalProperty_TRANSP, transp) and (transp == definitions.cICalProperty_TRANSPARENT):
                 continue
-            
+
             # Add start/end to list
             dtstart.append(dt.getInstanceStart())
             dtend.append(dt.getInstanceEnd())
-        
+
         # No longer need the expanded items
         list.clear()
-    
+
         # Create non-overlapping periods as properties in the freebusy component
         temp = PyCalendarPeriod(dtstart.front(), dtend.front())
         dtstart_iter = dtstart.iter()
@@ -645,37 +581,38 @@
 
                 # Current period is complete
                 fb.addProperty(PyCalendarProperty(definitions.cICalProperty_FREEBUSY, temp))
-                
+
                 # Reset period to new range
                 temp = PyCalendarPeriod(dtstart_iter, dtend_iter)
-            
+
             # They overlap - check for extended end
             if dtend_iter > temp.getEnd():
 
                 # Extend the end
                 temp = PyCalendarPeriod(temp.getStart(), dtend_iter)
-        
+
         # Add remaining period as property
         fb.addProperty(PyCalendarProperty(definitions.cICalProperty_FREEBUSY, temp))
 
+
     def getFreeBusy(self, period, fb):
         # First create expanded set
 
         list = []
         self.getVEvents(period, list)
-        
+
         # Get start/end list for each non-all-day expanded components
         for comp in list:
 
             # Ignore if all-day
             if comp.getInstanceStart().isDateOnly():
                 continue
-            
+
             # Ignore if transparent to free-busy
             transp = ""
             if comp.getOwner().getProperty(definitions.cICalProperty_TRANSP, transp) and (transp == definitions.cICalProperty_TRANSPARENT):
                 continue
-            
+
             # Add free busy item to list
             status = comp.getMaster().getStatus()
             if status in (definitions.eStatus_VEvent_None, definitions.eStatus_VEvent_Confirmed):
@@ -690,278 +627,44 @@
         # Now get the VFREEBUSY info
         list2 = []
         self.getVFreeBusy(period, list2)
-        
+
         # Get start/end list for each free-busy
         for comp in list2:
 
             # Expand component and add free busy info to list
             comp.expandPeriod(period, fb)
-            
+
         # Add remaining period as property
         PyCalendarFreeBusy.resolveOverlaps(fb)
-    
-    # Timezone lookups
-    def mergeTimezones(self, cal):
-        # Merge each timezone from other calendar
-        for tz in cal.mV[PyCalendar.VTIMEZONE]:
 
-            # See whether matching item is already installed
-            if not self.mV[PyCalendar.VTIMEZONE].has_key(tz.getMapKey()):
 
-                # Item does not already exist - so copy and add it
-                copy = PyCalendarVTimezone(copyit=tz)
-                self.mV[PyCalendar.VTIMEZONE].addComponent(copy)
-
-            else:
-                # Merge similar items
-                self.mV[PyCalendar.VTIMEZONE][tz.getMapKey()].mergeTimezone(tz)
-        
-    def getTimezoneOffsetSeconds(self, timezone, dt):
+    def getTimezoneOffsetSeconds(self, tzid, dt):
         # Find timezone that matches the name (which is the same as the map key)
-        if self.mV[PyCalendar.VTIMEZONE].has_key(timezone):
-            return self.mV[PyCalendar.VTIMEZONE][timezone].getTimezoneOffsetSeconds(dt)
-        else:
-            return 0
+        timezone = self.getTimezone(tzid)
+        return timezone.getTimezoneOffsetSeconds(dt) if timezone else 0
 
-    def getTimezoneDescriptor(self, timezone, dt):
+
+    def getTimezoneDescriptor(self, tzid, dt):
         # Find timezone that matches the name (which is the same as the map key)
-        if self.mV[PyCalendar.VTIMEZONE].has_key(timezone):
-            return self.mV[PyCalendar.VTIMEZONE][timezone].getTimezoneDescriptor(dt)
-        else:
-            return ""
+        timezone = self.getTimezone(tzid)
+        return timezone.getTimezoneDescriptor(dt) if timezone else ""
 
-    def getTimezones(self, tzids):
-        # Get all timezones in a list for sorting
-        sorted = {}
-        for tz in self.mV[PyCalendar.VTIMEZONE]:
-            sorted.setdefault(tz.getSortKey(), []).append(tz)
-    
-        # Now add to list in sorted order
-        for tzs in sorted.itervalues():
-            for tz in tzs:
-                tzids.append(tz.getID())
-        
+
     def getTimezone(self, tzid):
         # Find timezone that matches the name (which is the same as the map key)
-        if self.mV[PyCalendar.VTIMEZONE].has_key(tzid):
-            return self.mV[PyCalendar.VTIMEZONE][tzid]
+        for timezone in self.getComponents(definitions.cICalComponent_VTIMEZONE):
+            if timezone.getID() == tzid:
+                return timezone
         else:
             return None
 
-    # Add/remove components
-    def changedComponent(self, comp):
-        #Calendar has changed
-        self.setDirty()
-        
-        # Record change
-        #PyCalendarComponentRecord.recordAction(self.mRecordDB, comp, PyCalendarComponentRecord.eChanged)
-        
-        # Broadcast change
-        #CComponentAction action(CComponentAction::eChanged, *this, *comp)
-        #Broadcast_Message(eBroadcast_ChangedComponent, &action)
-        
 
-    def addNewVEvent(self, vevent, moved = False):
-        # Do not init props if moving
-        if not moved:
-
-            # Make sure UID is set and unique
-            uid = ""
-            vevent.setUID(uid)
-            
-            # Init DTSTAMP to now
-            vevent.initDTSTAMP()
-    
-        self.mV[PyCalendar.VEVENT].addComponent(vevent)
-    
-        # Calendar has changed
-        self.setDirty()
-        
-        #Record change
-        #PyICalendarComponentRecord.recordAction(self.mRecordDB, vevent, PyCalendarComponentRecord.eAdded)
-        
-        # Broadcast change
-        #action = CComponentAction(CComponentAction.eAdded, self, vevent)
-        #Broadcast_Message(eBroadcast_AddedComponent, &action)
-
-    def removeVEvent(self, vevent, delete_it = True):
-        # Record change  before delete occurs
-        #PyCalendarComponentRecord.recordAction(self.mRecordDB, vevent, PyCalendarComponentRecord.eRemoved)
-    
-        # Remove from the map (do not delete here - wait until after broadcast)
-        self.mVEvent.removeComponent(vevent, False)
-        
-        # Calendar has changed
-        self.setDirty()
-        
-        # Broadcast change
-        #action = CComponentAction(CComponentAction.eRemoved, self, vevent)
-        #Broadcast_Message(eBroadcast_RemovedComponent, &action)
-        
-        # Delete it here after all changes
-        if delete_it:
-            vevent = None
-        
-    def removeRecurringVEvent(self, vevent, recur):
-        # Determine how to delete
-        if recur == PyCalendar.REMOVE_ALL:
-            # Remove the top-level master event
-            self.removeVEvent(vevent.getTrueMaster())
-
-        elif recur == PyCalendar.REMOVE_ONLY_THIS:
-            # Simply exclude this instance from the top-level master vevent -
-            # this works even if this instance is the top-level (first) one
-            master = vevent.getTrueMaster()
-
-            # NB the vevent will be deleted as part of this so cache the instance start before
-            exclude = PyCalendarDateTime(copyit=vevent.getInstanceStart())
-
-            # The start instance is the RECURRENCE-ID to exclude
-            master.excludeRecurrence(exclude)
-
-            # Tell it it has changed (i.e. bump sequence)
-            master.changed()
-
-        elif recur == PyCalendar.REMOVE_THIS_AND_FUTURE:
-            # Simply exclude this instance from the master vevent
-            master = vevent.getTrueMaster()
-
-            # NB the vevent will be deleted as part of this so cache the instance start before
-            exclude = PyCalendarDateTime(copyit=vevent.getInstanceStart())
-
-            # The DTSTART specifies the recurrence that we exclude
-            master.excludeFutureRecurrence(exclude)
-
-            # Tell it it has changed (i.e. bump sequence)
-            master.changed()
-        
-        # Calendar has changed
-        self.setDirty()
-        
-        # Broadcast change
-        #Broadcast_Message(eBroadcast_Edit, this)
-
-
-    def addNewVToDo(self, vtodo, moved = False):
-        # Do not init props if moving
-        if not moved:
-
-            # Make sure UID is set and unique
-            uid = ""
-            vtodo.setUID(uid)
-            
-            # Init DTSTAMP to now
-            vtodo.initDTSTAMP()
-        
-        self.mVToDo.addComponent(vtodo)
-        
-        # Calendar has changed
-        self.setDirty()
-        
-        # Record change
-        #PyCalendarComponentRecord.recordAction(self.mRecordDB, vtodo, PyCalendarComponentRecord.eAdded)
-        
-        # Broadcast change
-        #action = CComponentAction(CComponentAction.eAdded, self, vtodo)
-        #Broadcast_Message(eBroadcast_AddedComponent, &action)
-        
-    def removeVToDo(self, vtodo, delete_it = True):
-        # Record change  before delete occurs
-        #PyCalendarComponentRecord.recordAction(self.mRecordDB, vtodo, PyCalendarComponentRecord.eRemoved)
-    
-        # Remove from the map (do not delete here - wait until after broadcast)
-        self.mVToDo.removeComponent(vtodo, False)
-        
-        # Calendar has changed
-        self.setDirty()
-        
-        # Broadcast change
-        #action = CComponentAction(CComponentAction.eRemoved, self, vtodo)
-        #Broadcast_Message(eBroadcast_RemovedComponent, &action)
-        
-        # Delete it here after all changes
-        if delete_it:
-            vtodo = None
-
-    def findComponent(self, orig, find = FIND_EXACT):
-        # Based on original component type. If we have a component of one type with the same UID
-        # as a component of another type something is really f*cked up!
-        index = self.getComponentIndex(orig.getType())
-        if index != -1:
-            return self.findComponentDB(self.mV[index], orig, find)
-        else:
-            return None
-
-    def addComponent(self, comp):
-        # Based on original component type. If we have a component of one type with the same UID
-        # as a component of another type something is really f*cked up!
-        index = self.getComponentIndex(comp.getType())
-        if index != -1:
-            self.addComponentDB(self.mV[index], comp)
-        else:
-            comp = None
-
-    def getComponentByKey(self, mapkey):
-        
-        for compdb in self.mV:
-            result = self.getComponentByKeyDB(compdb, mapkey)
-            if result is not None:
-                return result
-        else:
-            return None
-
-    def removeComponentByKey(self, mapkey):
-
-        for compdb in self.mV:
-            if self.removeComponentByKeyDB(compdb, mapkey):
-                return
-        
-
-    def isReadOnly(self):
-        return self.mReadOnly
-
-    def setReadOnly(self, ro = True):
-        self.mReadOnly = ro
-
-    # Change state
-    def isDirty(self):
-        return self.mDirty
-
-    def setDirty(self, dirty = True):
-        self.mDirty = dirty
-
-    def isTotalReplace(self):
-        return self.mTotalReplace
-
-    def setTotalReplace(self, replace = True):
-        self.mTotalReplace = replace
-
-    def getComponents(self, type):
-        return self.mV[self.getComponentIndex(type)]
-
-    def getComponentIndex(self, type):
-        if type == PyCalendarComponent.eVEVENT:
-            return PyCalendar.VEVENT
-        elif type == PyCalendarComponent.eVTODO:
-            return PyCalendar.VTODO
-        elif type == PyCalendarComponent.eVJOURNAL:
-            return PyCalendar.VJOURNAL
-        elif type == PyCalendarComponent.eVFREEBUSY:
-            return PyCalendar.VFREEBUSY
-        elif type == PyCalendarComponent.eVTIMEZONE:
-            return PyCalendar.VTIMEZONE
-        else:
-            return -1
-
     def addDefaultProperties(self):
         self.addProperty(PyCalendarProperty(definitions.cICalProperty_PRODID, PyCalendar.sProdID))
         self.addProperty(PyCalendarProperty(definitions.cICalProperty_VERSION, "2.0"))
         self.addProperty(PyCalendarProperty(definitions.cICalProperty_CALSCALE, "GREGORIAN"))
-        
-    def generateDB(self, os, components, for_cache):
-        for comp in components:
-            comp.generate(os, for_cache)
 
+
     def validProperty(self, prop):
         if prop.getName() == definitions.cICalProperty_VERSION:
 
@@ -976,124 +679,49 @@
                 return False
 
         return True
-        
+
+
     def ignoreProperty(self, prop):
-        return prop.getName() in (definitions.cICalProperty_VERSION, definitions.cICalProperty_CALSCALE, definitions.cICalProperty_PRODID)
+        return False #prop.getName() in (definitions.cICalProperty_VERSION, definitions.cICalProperty_CALSCALE, definitions.cICalProperty_PRODID)
 
+
     def includeTimezones(self):
         # Get timezone names from each component
         tzids = set()
-        self.includeTimezonesDB(self.mV[PyCalendar.VEVENT], tzids)
-        self.includeTimezonesDB(self.mV[PyCalendar.VTODO], tzids)
-        self.includeTimezonesDB(self.mV[PyCalendar.VJOURNAL], tzids)
-        self.includeTimezonesDB(self.mV[PyCalendar.VFREEBUSY], tzids)
-    
+        for component in self.mComponents:
+            if component.getType() != definitions.cICalComponent_VTIMEZONE:
+                component.getTimezones(tzids)
+
         # Make sure each timezone is in current calendar
+        from timezonedb import PyCalendarTimezoneDatabase
         for tzid in tzids:
-
             tz = self.getTimezone(tzid)
             if tz is None:
-
                 # Find it in the static object
-                tz = PyCalendar.sICalendar.getTimezone(tzid)
+                tz = PyCalendarTimezoneDatabase.getTimezone(tzid)
                 if tz is not None:
+                    dup = tz.duplicate()
+                    self.addComponent(dup)
 
-                    dup = PyCalendarVTimezone(copyit=tz)
-                    self.mV[PyCalendar.VTIMEZONE].addComponent(dup)
-        
-    def includeTimezonesDB(self, components, tzids):
-        for comp in components:
-            comp.getTimezones(tzids)
 
-    def findComponentDB(self, db, orig, find = FIND_EXACT):
-        if find == PyCalendar.FIND_EXACT:
-            key = orig.getMapKey()
-        else:
-            key = orig.getMasterKey()
-
-        if db.has_key(key):
-            return db[key]
-        else:
-            return None
-        
-    def addComponentDB(self, db, comp):
-        # Just add it without doing anything as this is a copy being made during sync'ing
-        if not db.addComponent(comp):
-            comp = None
-
-    def getComponentByKeyDB(self, db, mapkey):
-
-        if db.has_key(mapkey):
-            return db[mapkey]
-        else:
-            return None
-
-    def removeComponentByKeyDB(self, db, mapkey):
-        if db.has_key(mapkey):
-            db.removeComponent(mapkey, True)
-            return True
-        else:
-            return False
-
-    class SComponentRegister(object):
-
-
-        def __init__(self, id, type):
-            self.mID = id
-            self.mType = type
-
-    sComponents = {}
-    sEmbeddedComponents = {}
-
     @staticmethod
-    def initComponents():
+    def makeComponent(compname, parent):
 
-        if len(PyCalendar.sComponents) == 0:
-            PyCalendar.sComponents[PyCalendarVEvent.getVBegin()] = PyCalendar.SComponentRegister(0, PyCalendarComponent.eVEVENT)
-            PyCalendar.sComponents[PyCalendarVToDo.getVBegin()] = PyCalendar.SComponentRegister(1, PyCalendarComponent.eVTODO)
-            PyCalendar.sComponents[PyCalendarVJournal.getVBegin()] = PyCalendar.SComponentRegister(2, PyCalendarComponent.eVJOURNAL)
-            PyCalendar.sComponents[PyCalendarVFreeBusy.getVBegin()] = PyCalendar.SComponentRegister(3, PyCalendarComponent.eVFREEBUSY)
-            PyCalendar.sComponents[PyCalendarVTimezone.getVBegin()] = PyCalendar.SComponentRegister(4, PyCalendarComponent.eVTIMEZONE)
-    
-        if len(PyCalendar.sEmbeddedComponents) == 0:
-            PyCalendar.sEmbeddedComponents[PyCalendarVAlarm.getVBegin()] = PyCalendar.SComponentRegister(5, PyCalendarComponent.eVALARM)
-            PyCalendar.sEmbeddedComponents[PyCalendarVTimezoneStandard.getVBegin()] = PyCalendar.SComponentRegister(6, PyCalendarComponent.eVTIMEZONE)
-            PyCalendar.sEmbeddedComponents[PyCalendarVTimezoneDaylight.getVBegin()] = PyCalendar.SComponentRegister(7, PyCalendarComponent.eVTIMEZONE)
+        mapper = {
+            definitions.cICalComponent_VEVENT: PyCalendarVEvent,
+            definitions.cICalComponent_VTODO: PyCalendarVToDo,
+            definitions.cICalComponent_VJOURNAL: PyCalendarVJournal,
+            definitions.cICalComponent_VFREEBUSY: PyCalendarVFreeBusy,
+            definitions.cICalComponent_VTIMEZONE: PyCalendarVTimezone,
+            definitions.cICalComponent_VAVAILABILITY: PyCalendarVAvailability,
+            definitions.cICalComponent_VALARM: PyCalendarVAlarm,
+            definitions.cICalComponent_AVAILABLE: PyCalendarAvailable,
+            definitions.cICalComponent_STANDARD: PyCalendarVTimezoneStandard,
+            definitions.cICalComponent_DAYLIGHT: PyCalendarVTimezoneDaylight,
+        }
 
-    @staticmethod
-    def makeComponent(id, calendar):
-
-        if id == 0:
-            return PyCalendarVEvent(calendar=calendar)
-
-        elif id == 1:
-            return PyCalendarVToDo(calendar=calendar)
-
-        elif id == 2:
-            return PyCalendarVJournal(calendar=calendar)
-
-        elif id == 3:
-            return PyCalendarVFreeBusy(calendar=calendar)
-
-        elif id == 4:
-            return PyCalendarVTimezone(calendar=calendar)
-
-        elif id == 5:
-            return PyCalendarVAlarm(calendar=calendar)
-
-        elif id == 6:
-            return PyCalendarVTimezoneStandard(calendar=calendar)
-
-        elif id == 7:
-            return PyCalendarVTimezoneDaylight(calendar=calendar)
-
-        else:
-            return None
-
-    @staticmethod
-    def initDefaultTimezones():
-        # Add default timezones to this (static) calendar
-        pass
-
-PyCalendar.loadStatics()
-PyCalendar.sICalendar = PyCalendar()
+        try:
+            cls = mapper[compname]
+            return cls(parent=parent)
+        except KeyError:
+            return PyCalendarUnknownComponent(parent=parent, comptype=compname)

Modified: PyCalendar/trunk/src/pycalendar/component.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/component.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/component.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,134 +14,70 @@
 #    limitations under the License.
 ##
 
-from componentbase import PyCalendarComponentBase
-from datetime import PyCalendarDateTime
-from property import PyCalendarProperty
+from pycalendar import definitions
+from pycalendar import stringutils
+from pycalendar.componentbase import PyCalendarComponentBase
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.property import PyCalendarProperty
 import os
-import definitions
-import stringutils
 import time
+import uuid
 
 class PyCalendarComponent(PyCalendarComponentBase):
 
     uid_ctr = 1
 
-    eVCALENDAR = -1
-    eVEVENT = 0
-    eVTODO = 1
-    eVJOURNAL = 2
-    eVFREEBUSY = 3
-    eVTIMEZONE = 4
-    eVALARM = 5
+    def __init__(self, parent=None):
 
-    # Pseudo components
-    eVTIMEZONESTANDARD = 6
-    eVTIMEZONEDAYLIGHT = 7
+        super(PyCalendarComponent, self).__init__(parent)
+        self.mUID = ""
+        self.mSeq = 0
+        self.mOriginalSeq = 0
+        self.mChanged = False
 
-    def __init__(self, calendar=None, copyit=None):
-        
-        if calendar:
-            super(PyCalendarComponent, self).__init__()
-            self.mCalendarRef = calendar
-            self.mUID = ""
-            self.mSeq = 0
-            self.mOriginalSeq = 0
-            self.mEmbedder = None
-            self.mEmbedded = None
-            self.mETag = None
-            self.mRURL = None
-            self.mChanged = False
-        elif copyit:
-            super(PyCalendarComponent, self).__init__(copyit=copyit)
-            self.mCalendarRef = copyit.mCalendarRef
-            self.mUID = copyit.mUID
-            self.mSeq = copyit.mSeq
-            self.mOriginalSeq = copyit.mOriginalSeq
-    
-            self.mEmbedder = None
-            self.mEmbedded = None
-            if copyit.mEmbedded != None:
-                # Do deep copy of element list
-                self.mEmbedded = []
-                for iter in copyit.mEmbedded:
-                    self.mEmbedded.append(iter.clone_it())
-                    self.mEmbedded[-1].setEmbedder(self)
 
-            self.mETag = copyit.mETag
-            self.mRURL = copyit.mRURL
-            self.mChanged = copyit.mChanged
+    def duplicate(self, parent=None, **args):
 
-    def close(self):
+        other = super(PyCalendarComponent, self).duplicate(parent=parent, **args)
+        other.mUID = self.mUID
+        other.mSeq = self.mSeq
+        other.mOriginalSeq = self.mOriginalSeq
 
-        self.mEmbedder = None
+        other.mChanged = self.mChanged
 
-        # Also close sub-components if present
-        if (self.mEmbedded != None):
-            for iter in self.mEmbedded:
-                iter.close()
-            self.mEmbedded = None
+        return other
 
-    def clone_it(self):
-        raise NotImplemented
 
-    def getType(self):
-        raise NotImplemented
+    def __repr__(self):
+        return "%s: UID: %s" % (self.getType(), self.getMapKey(),)
 
-    def getBeginDelimiter(self):
-        raise NotImplemented
 
-    def getEndDelimiter(self):
-        raise NotImplemented
-
     def getMimeComponentName(self):
-        raise NotImplemented
+        raise NotImplementedError
 
-    def addComponent(self, comp): #@UnusedVariable
-        # Sub-classes decide what can be embedded
-        return False
 
-    def removeComponent(self, comp):
-        if self.mEmbedded != None:
-            try:
-                self.mEmbedded.remove(comp)
-            except ValueError:
-                pass
+    def getMapKey(self):
+        if hasattr(self, "mMapKey"):
+            return self.mMapKey
+        elif self.mUID:
+            return self.mUID
+        else:
+            self.mMapKey = str(uuid.uuid4())
+            return self.mMapKey
 
-    def hasEmbeddedComponent(self, type):
-        if self.mEmbedded != None:
-            for iter in self.mEmbedded:
-                if iter.getType() == type:
-                    return True
-        return False
 
-    def getFirstEmbeddedComponent(self, type):
-        if self.mEmbedded != None:
-            for iter in self.mEmbedded:
-                if iter.getType() == type:
-                    return iter
-        return None
+    def getSortKey(self):
+        return self.getMapKey()
 
-    def setEmbedder(self, embedder):
-        self.mEmbedder = embedder
 
-    def getEmbedder(self):
-        return self.mEmbedder
-
-    def setCalendar(self, ref):
-        self.mCalendarRef = ref
-
-    def getCalendar(self):
-        return self.mCalendarRef
-
-    def getMapKey(self):
-        return self.mUID
-
     def getMasterKey(self):
         return self.mUID
 
+
     def getUID(self):
         return self.mUID
 
+
     def setUID(self, uid):
         if uid:
             self.mUID = uid
@@ -181,9 +117,11 @@
         prop = PyCalendarProperty(definitions.cICalProperty_UID, self.mUID)
         self.addProperty(prop)
 
+
     def getSeq(self):
         return self.mSeq
 
+
     def setSeq(self, seq):
         self.mSeq = seq
 
@@ -192,52 +130,19 @@
         prop = PyCalendarProperty(definitions.cICalProperty_SEQUENCE, self.mSeq)
         self.addProperty(prop)
 
+
     def getOriginalSeq(self):
         return self.mOriginalSeq
 
-    def getRURL(self):
-        return self.mRURL
 
-    def setRURL(self, rurl):
-        self.mRURL = rurl
-
-    def generateRURL(self):
-        # Format is:
-        #
-        # <<hash code>> *("-0"> .ics
-        if (self.mRURL is None) or (len(self.mRURL) == 0):
-            # Generate hash code
-            hash = ""
-            hash += self.getMapKey()
-            hash += ":"
-            hash += str(self.getSeq())
-            hash += ":"
-
-            dt = self.loadValueDateTime(definitions.cICalProperty_DTSTAMP)
-            if dt is not None:
-                hash += dt.getText()
-
-            self.mRURL = stringutils.md5digest(hash)
-        else:
-            # Strip off .ics
-            if self.mRURL.endswith(".ics"):
-                self.mRURL = self.mRURL[:-4]
-
-        # Add trailer
-        self.mRURL += "-0.ics"
-
-    def getETag(self):
-        return self.mETag
-
-    def setETag(self, etag):
-        self.mETag = etag
-
     def getChanged(self):
         return self.mChanged
 
+
     def setChanged(self, changed):
         self.mChanged = changed
 
+
     def initDTSTAMP(self):
         self.removeProperties(definitions.cICalProperty_DTSTAMP)
 
@@ -245,6 +150,7 @@
                                   PyCalendarDateTime.getNowUTC())
         self.addProperty(prop)
 
+
     def updateLastModified(self):
         self.removeProperties(definitions.cICalProperty_LAST_MODIFIED)
 
@@ -252,73 +158,16 @@
                                   PyCalendarDateTime.getNowUTC())
         self.addProperty(prop)
 
-    def added(self):
-        # Also add sub-components if present
-        if self.mEmbedded != None:
-            for iter in self.mEmbedded:
-                iter.added()
 
-        self.mChanged = True
-
-    def removed(self):
-        # Also remove sub-components if present
-        if self.mEmbedded != None:
-            for iter in self.mEmbedded:
-                iter.removed()
-
-        self.mChanged = True
-
-    def duplicated(self):
-        # Remove SEQ, UID, DTSTAMP
-        # These will be re-created when it is added to the calendar
-        self.removeProperties(definitions.cICalProperty_UID)
-        self.removeProperties(definitions.cICalProperty_SEQUENCE)
-        self.removeProperties(definitions.cICalProperty_DTSTAMP)
-
-        # Remove the cached values as well
-        self.mUID = ""
-        self.mSeq = 0
-        self.mOriginalSeq = 0
-
-        # Also duplicate sub-components if present
-        if self.mEmbedded != None:
-            for iter in self.mEmbedded:
-                iter.duplicated()
-
-        # Reset CalDAV items
-        self.mETag = None
-        self.mRURL = None
-        self.mChanged = True
-
-    def changed(self):
-        # Bump the sequence
-        self.setSeq(self.getSeq() + 1)
-
-        # Update last-modified
-        self.updateLastModified()
-
-        # Also change sub-components
-        if self.mEmbedded != None:
-            for iter in self.mEmbedded:
-                iter.changed()
-
-        self.mChanged = True
-
-        # Mark calendar as dirty
-        from calendar import PyCalendar
-        cal = PyCalendar.getICalendar(self.getCalendar())
-        if cal != None:
-            cal.changedComponent(self)
-
     def finalise(self):
         # Get UID
         temps = self.loadValueString(definitions.cICalProperty_UID)
-        if temps != None:
+        if temps is not None:
             self.mUID = temps
 
         # Get SEQ
         temp = self.loadValueInteger(definitions.cICalProperty_SEQUENCE)
-        if temp != None:
+        if temp is not None:
             self.mSeq = temp
 
         # Cache the original sequence when the component is read in.
@@ -326,70 +175,11 @@
         # same calendar
         self.mOriginalSeq = self.mSeq
 
-        # Get CalDAV info if present
-        temps = self.loadPrivateValue(definitions.cICalProperty_X_PRIVATE_RURL)
-        if temps != None:
-            self.mRURL = temps
-        temps = self.loadPrivateValue(definitions.cICalProperty_X_PRIVATE_ETAG)
-        if temps != None:
-            self.mETag = temps
 
     def canGenerateInstance(self):
         return True
 
-    def generate(self, os, for_cache):
-        # Header
-        os.write(self.getBeginDelimiter())
-        os.write("\n")
 
-        # Write each property
-        self.writeProperties(os)
-
-        # Do private properties if caching
-        if for_cache:
-            if self.mRURL:
-                self.writePrivateProperty(os,
-                        definitions.cICalProperty_X_PRIVATE_RURL,
-                        self.mRURL)
-            if self.mETag:
-                self.writePrivateProperty(os,
-                        definitions.cICalProperty_X_PRIVATE_ETAG,
-                        self.mETag)
-
-        # Write each embedded component
-        if self.mEmbedded != None:
-            for iter in self.mEmbedded:
-                iter.generate(os, for_cache)
-
-        # Footer
-        os.write(self.getEndDelimiter())
-        os.write("\n")
-
-    def generateFiltered(self, os, filter):
-            # Header
-            os.write(self.getBeginDelimiter())
-            os.write("\n")
-
-            # Write each property
-            self.writePropertiesFiltered(os, filter)
-
-            # Write each embedded component
-            if self.mEmbedded != None:
-                # Shortcut for alll sub-components
-                if filter.isAllSubComponents():
-                    for iter in self.mEmbedded:
-                        iter.generate(os, False)
-                elif filter.hasSubComponentFilters():
-                    for iter in self.mEmbedded:
-                        subcomp = iter
-                        subfilter = filter.getSubComponentFilter(self.getType())
-                        if subfilter != None:
-                            subcomp.generateFiltered(os, subfilter)
-
-            # Footer
-            os.write(self.getEndDelimiter())
-            os.write("\n")
-
     def getTimezones(self, tzids):
         # Look for all date-time properties
         for props in self.mProperties.itervalues():

Modified: PyCalendar/trunk/src/pycalendar/componentbase.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/componentbase.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/componentbase.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,123 +14,488 @@
 #    limitations under the License.
 ##
 
-from datetimevalue import PyCalendarDateTimeValue
-from periodvalue import PyCalendarPeriodValue
-from property import PyCalendarProperty
-from value import PyCalendarValue
+from cStringIO import StringIO
+from pycalendar import xmldefs
+from pycalendar.datetimevalue import PyCalendarDateTimeValue
+from pycalendar.periodvalue import PyCalendarPeriodValue
+from pycalendar.property import PyCalendarProperty
+from pycalendar.value import PyCalendarValue
+import xml.etree.cElementTree as XML
 
 class PyCalendarComponentBase(object):
 
-    def __init__(self, copyit=None):
-        if copyit:
-            self.mProperties = {}
-            for propname, props in copyit.mProperties.iteritems():
-                for prop in props:
-                    self.mProperties.setdefault(propname, []).append(PyCalendarProperty(prop))
+    # These are class attributes for sets of properties for testing cardinality constraints. The sets
+    # must contain property names.
+    propertyCardinality_1 = ()           # Must be present
+    propertyCardinality_1_Fix_Empty = () # Must be present but can be fixed by adding an empty value
+    propertyCardinality_0_1 = ()         # 0 or 1 only
+    propertyCardinality_1_More = ()      # 1 or more
+
+    propertyValueChecks = None  # Either iCalendar or vCard validation
+
+    sortSubComponents = True
+
+    def __init__(self, parent=None):
+        self.mParentComponent = parent
+        self.mComponents = []
+        self.mProperties = {}
+
+        # This is the set of checks we do by default for components
+        self.cardinalityChecks = (
+            self.check_cardinality_1,
+            self.check_cardinality_1_Fix_Empty,
+            self.check_cardinality_0_1,
+            self.check_cardinality_1_More,
+        )
+
+
+    def duplicate(self, **args):
+        other = self.__class__(**args)
+
+        for component in self.mComponents:
+            other.addComponent(component.duplicate(parent=other))
+
+        other.mProperties = {}
+        for propname, props in self.mProperties.iteritems():
+            other.mProperties[propname] = [i.duplicate() for i in props]
+        return other
+
+
+    def __str__(self):
+        return self.getText()
+
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+
+    def __eq__(self, other):
+        if not isinstance(other, PyCalendarComponentBase):
+            return False
+        return self.getType() == other.getType() and self.compareProperties(other) and self.compareComponents(other)
+
+
+    def getType(self):
+        raise NotImplementedError
+
+
+    def getBeginDelimiter(self):
+        return "BEGIN:" + self.getType()
+
+
+    def getEndDelimiter(self):
+        return "END:" + self.getType()
+
+
+    def getSortKey(self):
+        return ""
+
+
+    def getParentComponent(self):
+        return self.mParentComponent
+
+
+    def setParentComponent(self, parent):
+        self.mParentComponent = parent
+
+
+    def compareComponents(self, other):
+        mine = set(self.mComponents)
+        theirs = set(other.mComponents)
+
+        for item in mine:
+            for another in theirs:
+                if item == another:
+                    theirs.remove(another)
+                    break
+            else:
+                return False
+        return len(theirs) == 0
+
+
+    def getComponents(self, compname=None):
+        compname = compname.upper() if compname else None
+        return [component for component in self.mComponents if compname is None or component.getType().upper() == compname]
+
+
+    def getComponentByKey(self, key):
+        for component in self.mComponents:
+            if component.getMapKey() == key:
+                return component
         else:
-            self.mProperties = {}
+            return None
 
-    def getProperties(self):
-        return self.mProperties
 
+    def removeComponentByKey(self, key):
+
+        for component in self.mComponents:
+            if component.getMapKey() == key:
+                self.removeComponent(component)
+                return
+
+
+    def addComponent(self, component):
+        self.mComponents.append(component)
+
+
+    def hasComponent(self, compname):
+        return self.countComponents(compname) != 0
+
+
+    def countComponents(self, compname):
+        return len(self.getComponents(compname))
+
+
+    def removeComponent(self, component):
+        self.mComponents.remove(component)
+
+
+    def removeAllComponent(self, compname=None):
+        if compname:
+            compname = compname.upper()
+            for component in tuple(self.mComponents):
+                if component.getType().upper() == compname:
+                    self.removeComponent(component)
+        else:
+            self.mComponents = []
+
+
+    def sortedComponentNames(self):
+        return ()
+
+
+    def compareProperties(self, other):
+        mine = set()
+        for props in self.mProperties.values():
+            mine.update(props)
+        theirs = set()
+        for props in other.mProperties.values():
+            theirs.update(props)
+        return mine == theirs
+
+
+    def getProperties(self, propname=None):
+        return self.mProperties.get(propname.upper(), []) if propname else self.mProperties
+
+
     def setProperties(self, props):
         self.mProperties = props
 
+
     def addProperty(self, prop):
-        self.mProperties.setdefault(prop.getName(), []).append(prop)
+        self.mProperties.setdefault(prop.getName().upper(), []).append(prop)
 
-    def hasProperty(self, prop):
-        return self.mProperties.has_key(prop)
 
-    def countProperty(self, prop):
-        return len(self.mProperties.get(prop, []))
+    def hasProperty(self, propname):
+        return propname.upper() in self.mProperties
 
-    def findFirstProperty(self, prop):
-        return self.mProperties.get(prop, [None])[0]
 
-    def removeProperties(self, prop):
-        if self.mProperties.has_key(prop):
-            del self.mProperties[prop]
+    def countProperty(self, propname):
+        return len(self.mProperties.get(propname.upper(), []))
 
-    def getPropertyInteger(self, prop, type = None):
+
+    def findFirstProperty(self, propname):
+        return self.mProperties.get(propname.upper(), [None])[0]
+
+
+    def removeProperty(self, prop):
+        if prop.getName().upper() in self.mProperties:
+            self.mProperties[prop.getName().upper()].remove(prop)
+            if len(self.mProperties[prop.getName().upper()]) == 0:
+                del self.mProperties[prop.getName().upper()]
+
+
+    def removeProperties(self, propname):
+        if propname.upper() in self.mProperties:
+            del self.mProperties[propname.upper()]
+
+
+    def getPropertyInteger(self, prop, type=None):
         return self.loadValueInteger(prop, type)
 
+
     def getPropertyString(self, prop):
         return self.loadValueString(prop)
 
+
     def getProperty(self, prop, value):
         return self.loadValue(prop, value)
 
+
     def finalise(self):
         raise NotImplemented
 
-    def generate(self, os, for_cache):
-        raise NotImplemented
 
+    def validate(self, doFix=False):
+        """
+        Validate the data in this component and optionally fix any problems. Return
+        a tuple containing two lists: the first describes problems that were fixed, the
+        second problems that were not fixed. Caller can then decide what to do with unfixed
+        issues.
+        """
+
+        fixed = []
+        unfixed = []
+
+        # Cardinality tests
+        for check in self.cardinalityChecks:
+            check(fixed, unfixed, doFix)
+
+        # Value constraints - these tests come from class specific attributes
+        if self.propertyValueChecks is not None:
+            for properties in self.mProperties.values():
+                for property in properties:
+                    propname = property.getName().upper()
+                    if propname in self.propertyValueChecks:
+                        if not self.propertyValueChecks[propname](property):
+                            # Cannot fix a bad property value
+                            logProblem = "[%s] Property value incorrect: %s" % (self.getType(), propname,)
+                            unfixed.append(logProblem)
+
+        # Validate all subcomponents
+        for component in self.mComponents:
+            morefixed, moreunfixed = component.validate(doFix)
+            fixed.extend(morefixed)
+            unfixed.extend(moreunfixed)
+
+        return fixed, unfixed
+
+
+    def check_cardinality_1(self, fixed, unfixed, doFix):
+        for propname in self.propertyCardinality_1:
+            if self.countProperty(propname) != 1: # Cannot fix a missing required property
+                logProblem = "[%s] Missing or too many required property: %s" % (self.getType(), propname)
+                unfixed.append(logProblem)
+
+
+    def check_cardinality_1_Fix_Empty(self, fixed, unfixed, doFix):
+        for propname in self.propertyCardinality_1_Fix_Empty:
+            if self.countProperty(propname) > 1: # Cannot fix too many required property
+                logProblem = "[%s] Too many required property: %s" % (self.getType(), propname)
+                unfixed.append(logProblem)
+            elif self.countProperty(propname) == 0: # Possibly fix by adding empty property
+                logProblem = "[%s] Missing required property: %s" % (self.getType(), propname)
+                if doFix:
+                    self.addProperty(PyCalendarProperty(propname, ""))
+                    fixed.append(logProblem)
+                else:
+                    unfixed.append(logProblem)
+
+
+    def check_cardinality_0_1(self, fixed, unfixed, doFix):
+        for propname in self.propertyCardinality_0_1:
+            if self.countProperty(propname) > 1: # Cannot be fixed - no idea which one to delete
+                logProblem = "[%s] Too many properties present: %s" % (self.getType(), propname)
+                unfixed.append(logProblem)
+
+
+    def check_cardinality_1_More(self, fixed, unfixed, doFix):
+        for propname in self.propertyCardinality_1_More:
+            if not self.countProperty(propname) > 0: # Cannot fix a missing required property
+                logProblem = "[%s] Missing required property: %s" % (self.getType(), propname)
+                unfixed.append(logProblem)
+
+
+    def getText(self):
+        s = StringIO()
+        self.generate(s)
+        return s.getvalue()
+
+
+    def generate(self, os):
+        # Header
+        os.write(self.getBeginDelimiter())
+        os.write("\r\n")
+
+        # Write each property
+        self.writeProperties(os)
+
+        # Write each embedded component based on specific order
+        self.writeComponents(os)
+
+        # Footer
+        os.write(self.getEndDelimiter())
+        os.write("\r\n")
+
+
     def generateFiltered(self, os, filter):
-        raise NotImplemented
+        # Header
+        os.write(self.getBeginDelimiter())
+        os.write("\r\n")
 
+        # Write each property
+        self.writePropertiesFiltered(os, filter)
+
+        # Write each embedded component based on specific order
+        self.writeComponentsFiltered(os, filter)
+
+        # Footer
+        os.write(self.getEndDelimiter())
+        os.write("\r\n")
+
+
+    def writeXML(self, node, namespace):
+
+        # Component element
+        comp = XML.SubElement(node, xmldefs.makeTag(namespace, self.getType()))
+
+        # Each property
+        self.writePropertiesXML(comp, namespace)
+
+        # Each component
+        self.writeComponentsXML(comp, namespace)
+
+
+    def writeXMLFiltered(self, node, namespace, filter):
+        # Component element
+        comp = XML.SubElement(node, xmldefs.makeTag(namespace, self.getType()))
+
+        # Each property
+        self.writePropertiesFilteredXML(comp, namespace, filter)
+
+        # Each component
+        self.writeComponentsFilteredXML(comp, namespace, filter)
+
+
+    def sortedComponents(self):
+
+        components = self.mComponents[:]
+        sortedcomponents = []
+
+        # Write each component based on specific order
+        orderedNames = self.sortedComponentNames()
+        for name in orderedNames:
+
+            # Group by name then sort by map key (UID/R-ID)
+            namedcomponents = []
+            for component in tuple(components):
+                if component.getType().upper() == name:
+                    namedcomponents.append(component)
+                    components.remove(component)
+            for component in sorted(namedcomponents, key=lambda x: x.getSortKey()):
+                sortedcomponents.append(component)
+
+        # Write out the remainder sorted by name, sortKey
+        if self.sortSubComponents:
+            remainder = sorted(components, key=lambda x: (x.getType().upper(), x.getSortKey(),))
+        else:
+            remainder = components
+        for component in remainder:
+            sortedcomponents.append(component)
+
+        return sortedcomponents
+
+
+    def writeComponents(self, os):
+
+        # Write out the remainder
+        for component in self.sortedComponents():
+            component.generate(os)
+
+
+    def writeComponentsFiltered(self, os, filter):
+        # Shortcut for all sub-components
+        if filter.isAllSubComponents():
+            self.writeComponents(os)
+        elif filter.hasSubComponentFilters():
+            for subcomp in self.sortedcomponents():
+                subfilter = filter.getSubComponentFilter(subcomp.getType())
+                if subfilter is not None:
+                    subcomp.generateFiltered(os, subfilter)
+
+
+    def writeComponentsXML(self, node, namespace):
+
+        if self.mComponents:
+            comps = XML.SubElement(node, xmldefs.makeTag(namespace, xmldefs.components))
+
+            # Write out the remainder
+            for component in self.sortedComponents():
+                component.writeXML(comps, namespace)
+
+
+    def writeComponentsFilteredXML(self, node, namespace, filter):
+
+        if self.mComponents:
+            comps = XML.SubElement(node, xmldefs.makeTag(namespace, xmldefs.components))
+
+            # Shortcut for all sub-components
+            if filter.isAllSubComponents():
+                self.writeXML(comps, namespace)
+            elif filter.hasSubComponentFilters():
+                for subcomp in self.sortedcomponents():
+                    subfilter = filter.getSubComponentFilter(subcomp.getType())
+                    if subfilter is not None:
+                        subcomp.writeFilteredXML(comps, namespace, subfilter)
+
+
     def loadValue(self, value_name):
-        if self.getProperties().has_key(value_name):
-            return self.getProperties()[value_name][0]
+        if self.hasProperty(value_name):
+            return self.findFirstProperty(value_name)
 
         return None
 
+
     def loadValueInteger(self, value_name, type=None):
         if type:
-            if self.getProperties().has_key(value_name):
+            if self.hasProperty(value_name):
                 if type == PyCalendarValue.VALUETYPE_INTEGER:
-                    ivalue = self.getProperties()[value_name][0].getIntegerValue()
-                    if ivalue != None:
+                    ivalue = self.findFirstProperty(value_name).getIntegerValue()
+                    if ivalue is not None:
                         return ivalue.getValue()
                 elif type == PyCalendarValue.VALUETYPE_UTC_OFFSET:
-                    uvalue = self.getProperties()[value_name][0].getUTCOffsetValue()
-                    if (uvalue != None):
+                    uvalue = self.findFirstProperty(value_name).getUTCOffsetValue()
+                    if (uvalue is not None):
                         return uvalue.getValue()
-    
+
             return None
         else:
             return self.loadValueInteger(value_name, PyCalendarValue.VALUETYPE_INTEGER)
 
+
     def loadValueString(self, value_name):
-        if self.getProperties().has_key(value_name):
-            tvalue = self.getProperties()[value_name][0].getTextValue()
-            if (tvalue != None):
+        if self.hasProperty(value_name):
+            tvalue = self.findFirstProperty(value_name).getTextValue()
+            if (tvalue is not None):
                 return tvalue.getValue()
 
         return None
 
+
     def loadValueDateTime(self, value_name):
-        if self.getProperties().has_key(value_name):
-            dtvalue = self.getProperties()[value_name][0].getDateTimeValue()
-            if dtvalue != None:
+        if self.hasProperty(value_name):
+            dtvalue = self.findFirstProperty(value_name).getDateTimeValue()
+            if dtvalue is not None:
                 return dtvalue.getValue()
 
         return None
 
+
     def loadValueDuration(self, value_name):
-        if self.getProperties().has_key(value_name):
-            dvalue = self.getProperties()[value_name][0].getDurationValue()
-            if (dvalue != None):
+        if self.hasProperty(value_name):
+            dvalue = self.findFirstProperty(value_name).getDurationValue()
+            if (dvalue is not None):
                 return dvalue.getValue()
 
         return None
 
+
     def loadValuePeriod(self, value_name):
-        if self.getProperties().has_key(value_name):
-            pvalue = self.getProperties()[value_name][0].getPeriodValue()
-            if (pvalue != None):
+        if self.hasProperty(value_name):
+            pvalue = self.findFirstProperty(value_name).getPeriodValue()
+            if (pvalue is not None):
                 return pvalue.getValue()
 
         return None
 
+
     def loadValueRRULE(self, value_name, value, add):
         # Get RRULEs
-        if self.getProperties().has_key(value_name):
+        if self.hasProperty(value_name):
             items = self.getProperties()[value_name]
             for iter in items:
                 rvalue = iter.getRecurrenceValue()
-                if (rvalue != None):
+                if (rvalue is not None):
                     if add:
                         value.addRule(rvalue.getValue())
                     else:
@@ -139,13 +504,13 @@
         else:
             return False
 
+
     def loadValueRDATE(self, value_name, value, add):
         # Get RDATEs
-        if self.getProperties().has_key(value_name):
-            items = self.getProperties()[value_name]
-            for iter in items:
+        if self.hasProperty(value_name):
+            for iter in self.getProperties(value_name):
                 mvalue = iter.getMultiValue()
-                if (mvalue != None):
+                if (mvalue is not None):
                     for obj in mvalue.getValues():
                         # cast to date-time
                         if isinstance(obj, PyCalendarDateTimeValue):
@@ -155,53 +520,101 @@
                                 value.subtractDT(obj.getValue())
                         elif isinstance(obj, PyCalendarPeriodValue):
                             if add:
-                                value.addDT(obj.getValue().getStart())
+                                value.addPeriod(obj.getValue().getStart())
                             else:
-                                value.subtractDT(obj.getValue().getStart())
+                                value.subtractPeriod(obj.getValue().getStart())
 
             return True
         else:
             return False
 
+
+    def sortedPropertyKeys(self):
+        keys = self.mProperties.keys()
+        keys.sort()
+
+        results = []
+        for skey in self.sortedPropertyKeyOrder():
+            if skey in keys:
+                results.append(skey)
+                keys.remove(skey)
+        results.extend(keys)
+        return results
+
+
+    def sortedPropertyKeyOrder(self):
+        return ()
+
+
     def writeProperties(self, os):
         # Sort properties by name
-        keys = self.mProperties.keys()
-        keys.sort()
+        keys = self.sortedPropertyKeys()
         for key in keys:
             props = self.mProperties[key]
             for prop in props:
                 prop.generate(os)
 
+
     def writePropertiesFiltered(self, os, filter):
 
         # Sort properties by name
-        keys = self.mProperties.keys()
-        keys.sort()
+        keys = self.sortedPropertyKeys()
 
         # Shortcut for all properties
         if filter.isAllProperties():
             for key in keys:
-                props = self.mProperties[key]
-                for prop in props:
+                for prop in self.getProperties(key):
                     prop.generate(os)
         elif filter.hasPropertyFilters():
             for key in keys:
-                props = self.mProperties[key]
-                for prop in props:
+                for prop in self.getProperties(key):
                     prop.generateFiltered(os, filter)
 
+
+    def writePropertiesXML(self, node, namespace):
+
+        properties = XML.SubElement(node, xmldefs.makeTag(namespace, xmldefs.properties))
+
+        # Sort properties by name
+        keys = self.sortedPropertyKeys()
+        for key in keys:
+            props = self.mProperties[key]
+            for prop in props:
+                prop.writeXML(properties, namespace)
+
+
+    def writePropertiesFilteredXML(self, node, namespace, filter):
+
+        props = XML.SubElement(node, xmldefs.makeTag(namespace, xmldefs.properties))
+
+        # Sort properties by name
+        keys = self.sortedPropertyKeys()
+
+        # Shortcut for all properties
+        if filter.isAllProperties():
+            for key in keys:
+                for prop in self.getProperties(key):
+                    prop.writeXML(props, namespace)
+        elif filter.hasPropertyFilters():
+            for key in keys:
+                for prop in self.getProperties(key):
+                    prop.writeFilteredXML(props, namespace, filter)
+
+
     def loadPrivateValue(self, value_name):
         # Read it in from properties list and then delete the property from the
         # main list
         result = self.loadValueString(value_name)
-        if (result != None):
+        if (result is not None):
             self.removeProperties(value_name)
         return result
 
+
     def writePrivateProperty(self, os, key, value):
         prop = PyCalendarProperty(name=key, value=value)
         prop.generate(os)
 
+
     def editProperty(self, propname, propvalue):
 
         # Remove existing items

Deleted: PyCalendar/trunk/src/pycalendar/componentdb.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/componentdb.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/componentdb.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,178 +0,0 @@
-##
-#    Copyright (c) 2007 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 componentrecur import PyCalendarComponentRecur
-from datetime import PyCalendarDateTime
-
-class PyCalendarComponentDB(object):
-
-    def __init__(self, copyit=None):
-        self.mItems = {}
-        if copyit:
-            self.mRecurMap = None
-            if (copyit.mRecurMap != None):
-                self.mRecurMap = {}
-                for key, value in copyit.mRecurMap.items():
-                    list = []
-                    for item in value:
-                        list.append(PyCalendarDateTime(copyit=item))
-                    self.mRecurMap[key] = list
-        else:
-            self.mRecurMap = None
-
-    def __iter__(self):
-        return self.mItems.itervalues()
-
-    def __getitem__(self, i):
-        return self.mItems[i]
-
-    def size(self):
-        return len(self.mItems)
-
-    def has_key(self, k):
-        return self.mItems.has_key(k)
-
-    def close(self):
-        for value in self.mItems.values():
-            # Tell component it is removed and delete it
-            value.close()
-
-        self.mItems.clear()
-
-    def addComponent(self, comp):
-        # Must have valid UID
-        if (len(comp.getMapKey()) == 0):
-            return False
-
-        # Add the component to the calendar
-        bresult = True
-
-        # See if duplicate
-        if self.mItems.has_key(comp.getMapKey()):
-            result = self.mItems.get(comp.getMapKey())
-
-            # Replace existing if sequence is higher
-            if comp.getSeq() > result.getSeq():
-                self.mItems[comp.getMapKey()] = comp
-                bresult = True
-            else:
-                bresult = False
-        else:
-            self.mItems[comp.getMapKey()] = comp
-            bresult = True
-
-        # Now look for a recurrence component if it was added
-        if bresult and isinstance(comp, PyCalendarComponentRecur):
-
-            recur = comp
-
-            # Add each overridden instance to the override map
-            if recur.isRecurrenceInstance():
-                # Look for existiung UID->RID map entry
-                if self.mRecurMap == None:
-                    self.mRecurMap = {}
-                found = self.mRecurMap.get(recur.getUID(), None)
-                if found != None:
-                    # Add to existing list
-                    found.append(recur.getRecurrenceID())
-                else:
-                    # Create new entry for this UID
-                    temp = []
-                    temp.append(recur.getRecurrenceID())
-                    self.mRecurMap[recur.getUID()] = temp
-
-                # Now try and find the master component if it currently exists
-                found2 = self.mItems.get(recur.getUID(), None)
-                if found2 != None:
-                    # Tell the instance who its master is
-                    recur.setMaster(found2)
-
-            # Make sure the master is sync'd with any instances that may have
-            # been
-            # added before it, those added after
-            # will be sync'd when they are added
-            elif self.mRecurMap != None:
-                # See if master has an entry in the UID->RID map
-                found = self.mRecurMap.get(comp.getUID(), None)
-                if found != None:
-                    # Make sure each instance knows about its master
-                    for iter in found:
-                        # Get the instance
-                        instance = self.getRecurrenceInstance(comp.getUID(), iter)
-                        if instance != None:
-                            # Tell the instance who its master is
-                            instance.setMaster(recur)
-
-        # Tell component it has now been added
-        if bresult:
-            comp.added()
-
-        return bresult
-
-    def removeComponent(self, comp):
-        # Tell component it is removed
-        comp.removed()
-
-        # Only if present
-        if self.mItems.has_key(comp.getMapKey()):
-            del self.mItems[comp.getMapKey()]
-
-    def removeAllComponents(self):
-        for comp in self.mItems.values():
-            # Tell component it is removed and delete it
-            comp.removed()
-            comp.close()
-
-        self.clear()
-
-    def changedComponent(self, comp):
-        # Tell component it is changed
-        comp.changed()
-
-    def getRecurrenceInstancesIds(self, uid, ids):
-        if self.mRecurMap == None:
-            return
-
-        # Look for matching UID in recurrence instance map
-        found = self.mRecurMap.get(uid, None)
-        if found != None:
-            # Return the recurrence ids
-            ids.extend(found)
-            return
-
-        return
-
-    def getRecurrenceInstancesItems(self, uid, items):
-        if self.mRecurMap == None:
-            return
-
-        # Look for matching UID in recurrence instance map
-        found = self.mRecurMap.get(uid, None)
-        if found != None:
-            # Return all the recurrence ids
-            for iter in found:
-                # Look it up
-                recur = self.getRecurrenceInstance(uid, iter)
-                if recur != None:
-                    items.append(recur)
-
-    def getRecurrenceInstance(self, uid, rid):
-        found = self.mItems.get(PyCalendarComponentRecur.mapKey(uid, rid.getText()), None)
-        if found != None:
-            # Tell the instance who its master is
-            return found
-
-        return None

Modified: PyCalendar/trunk/src/pycalendar/componentexpanded.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/componentexpanded.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/componentexpanded.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,7 +14,7 @@
 #    limitations under the License.
 ##
 
-from datetime import PyCalendarDateTime
+from pycalendar.datetime import PyCalendarDateTime
 
 class PyCalendarComponentExpanded(object):
 
@@ -37,6 +37,7 @@
         else:
             return e1.mInstanceStart < e2.mInstanceStart
 
+
     @staticmethod
     def sort_by_dtstart(e1, e2):
         if e1.mInstanceStart == e2.mInstanceStart:
@@ -48,41 +49,56 @@
         else:
             return e1.mInstanceStart < e2.mInstanceStart
 
-    def __init__(self, owner=None, rid=None, copyit=None):
 
-        if owner is not None:
-            self.mOwner = owner
-            self.initFromOwner(rid)
-        elif copyit is not None:
-            self._copy_PyCalendarComponentExpanded(copyit)
+    def __init__(self, owner, rid):
 
+        self.mOwner = owner
+        self.initFromOwner(rid)
+
+
+    def duplicate(self):
+        other = PyCalendarComponentExpanded(self.mOwner, None)
+        other.mInstanceStart = self.mInstanceStart.duplicate()
+        other.mInstanceEnd = self.mInstanceEnd.duplicate()
+        other.mRecurring = self.mRecurring
+        return other
+
+
     def close(self):
         # Clean-up
         self.mOwner = None
 
+
     def getOwner(self):
         return self.mOwner
 
+
     def getMaster(self):
         return self.mOwner
 
+
     def getTrueMaster(self):
         return self.mOwner.getMaster()
 
+
     def getInstanceStart(self):
         return self.mInstanceStart
 
+
     def getInstanceEnd(self):
         return self.mInstanceEnd
 
+
     def recurring(self):
         return self.mRecurring
 
+
     def isNow(self):
         # Check instance start/end against current date-time
         now = PyCalendarDateTime.getNowUTC()
         return self.mInstanceStart <= now and self.mInstanceEnd > now
 
+
     def initFromOwner(self, rid):
         # There are four possibilities here:
         #
@@ -116,7 +132,7 @@
             if self.mOwner.hasEnd():
                 self.mInstanceEnd = self.mInstanceStart + (self.mOwner.getEnd() - self.mOwner.getStart())
             else:
-                self.mInstanceEnd = PyCalendarDateTime(copyit=self.mInstanceStart)
+                self.mInstanceEnd = self.mInstanceStart.duplicate()
 
             self.mRecurring = True
 
@@ -140,9 +156,3 @@
             self.mInstanceEnd = self.mInstanceStart + (self.mOwner.getEnd() - self.mOwner.getStart())
 
             self.mRecurring = True
-
-    def _copy_ICalendarComponentExpanded(self, copy):
-        self.mOwner = copy.self.mOwner
-        self.mInstanceStart = PyCalendarDateTime(copyit=copy.mInstanceStart)
-        self.mInstanceEnd = PyCalendarDateTime(copyit=copy.mInstanceEnd)
-        self.mRecurring = copy.mRecurring

Modified: PyCalendar/trunk/src/pycalendar/componentrecur.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/componentrecur.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/componentrecur.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,23 +14,33 @@
 #    limitations under the License.
 ##
 
-from component import PyCalendarComponent
-from componentexpanded import PyCalendarComponentExpanded
-from datetime import PyCalendarDateTime
-from property import PyCalendarProperty
-from recurrenceset import PyCalendarRecurrenceSet
-from utils import set_difference
-import definitions
+from pycalendar import definitions
+from pycalendar.component import PyCalendarComponent
+from pycalendar.componentexpanded import PyCalendarComponentExpanded
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.property import PyCalendarProperty
+from pycalendar.recurrenceset import PyCalendarRecurrenceSet
+from pycalendar.timezone import PyCalendarTimezone
+from pycalendar.utils import set_difference
+import uuid
 
 class PyCalendarComponentRecur(PyCalendarComponent):
 
+    propertyCardinality_STATUS_Fix = (
+        definitions.cICalProperty_STATUS,
+    )
+
     @staticmethod
-    def mapKey(uid, rid = None):
-        result = "u:" + uid
-        if rid is not None:
-            result += rid
-        return result
+    def mapKey(uid, rid=None):
+        if uid:
+            result = "u:" + uid
+            if rid is not None:
+                result += rid
+            return result
+        else:
+            return None
 
+
     @staticmethod
     def sort_by_dtstart_allday(e1, e2):
 
@@ -50,6 +60,7 @@
         else:
             return e1.self.mStart < e2.self.mStart
 
+
     @staticmethod
     def sort_by_dtstart(e1, e2):
         if e1.self.mStart == e2.self.mStart:
@@ -61,75 +72,90 @@
         else:
             return e1.self.mStart < e2.self.mStart
 
-    def __init__(self, calendar=None, copyit=None):
-        if calendar is not None:
-            super(PyCalendarComponentRecur, self).__init__(calendar=calendar)
-            self.mMaster = self
-            self.mMapKey = None
-            self.mSummary = None
-            self.mStamp = PyCalendarDateTime()
-            self.mHasStamp = False
-            self.mStart = PyCalendarDateTime()
-            self.mHasStart = False
-            self.mEnd = PyCalendarDateTime()
-            self.mHasEnd = False
-            self.mDuration = False
-            self.mHasRecurrenceID = False
-            self.mAdjustFuture = False
-            self.mAdjustPrior = False
-            self.mRecurrenceID = None
-            self.mRecurrences = None
-        elif copyit is not None:
-            super(PyCalendarComponentRecur, self).__init__(copyit)
-    
-            # Special determination of master
-            if copyit.recurring():
-                self.mMaster = copyit.mMaster
-            else:
-                self.mMaster = self
-    
-            self.mMapKey = copyit.mMapKey
-    
-            self.mSummary = copyit.mSummary
-    
-            if (copyit.mStamp != None):
-                self.mStamp = PyCalendarDateTime(copyit=copyit.mStamp)
-            self.mHasStamp = copyit.mHasStamp
-    
-            self.mStart = PyCalendarDateTime(copyit=copyit.mStart)
-            self.mHasStart = copyit.mHasStart
-            self.mEnd = PyCalendarDateTime(copyit=copyit.mEnd)
-            self.mHasEnd = copyit.mHasEnd
-            self.mDuration = copyit.mDuration
-    
-            self.mHasRecurrenceID = copyit.mHasRecurrenceID
-            self.mAdjustFuture = copyit.mAdjustFuture
-            self.mAdjustPrior = copyit.mAdjustPrior
-            if copyit.mRecurrenceID != None:
-                self.mRecurrenceID = PyCalendarDateTime(copyit=copyit.mRecurrenceID)
-    
-            if copyit.mRecurrences != None:
-                self.mRecurrences = PyCalendarRecurrenceSet(copyit=copyit.mRecurrences)
 
+    def __init__(self, parent=None):
+        super(PyCalendarComponentRecur, self).__init__(parent=parent)
+        self.mMaster = self
+        self.mMapKey = None
+        self.mSummary = None
+        self.mStamp = PyCalendarDateTime()
+        self.mHasStamp = False
+        self.mStart = PyCalendarDateTime()
+        self.mHasStart = False
+        self.mEnd = PyCalendarDateTime()
+        self.mHasEnd = False
+        self.mDuration = False
+        self.mHasRecurrenceID = False
+        self.mAdjustFuture = False
+        self.mAdjustPrior = False
+        self.mRecurrenceID = None
+        self.mRecurrences = None
+
+        # This is a special check we do only for STATUS due to a calendarserver bug
+        self.cardinalityChecks += (
+            self.check_cardinality_STATUS_Fix,
+        )
+
+
+    def duplicate(self, parent=None):
+        other = super(PyCalendarComponentRecur, self).duplicate(parent=parent)
+
+        # Special determination of master
+        other.mMaster = self.mMaster if self.recurring() else self
+
+        other.mMapKey = self.mMapKey
+
+        other.mSummary = self.mSummary
+
+        if (self.mStamp is not None):
+            other.mStamp = self.mStamp.duplicate()
+        other.mHasStamp = self.mHasStamp
+
+        other.mStart = self.mStart.duplicate()
+        other.mHasStart = self.mHasStart
+        other.mEnd = self.mEnd.duplicate()
+        other.mHasEnd = self.mHasEnd
+        other.mDuration = self.mDuration
+
+        other.mHasRecurrenceID = self.mHasRecurrenceID
+        other.mAdjustFuture = self.mAdjustFuture
+        other.mAdjustPrior = self.mAdjustPrior
+        if self.mRecurrenceID is not None:
+            other.mRecurrenceID = self.mRecurrenceID.duplicate()
+
+        other._resetRecurrenceSet()
+
+        return other
+
+
     def canGenerateInstance(self):
         return not self.mHasRecurrenceID
 
+
     def recurring(self):
-        return (self.mMaster != None) and (self.mMaster != self)
+        return (self.mMaster is not None) and (self.mMaster is not self)
 
+
     def setMaster(self, master):
         self.mMaster = master
         self.initFromMaster()
 
+
     def getMaster(self):
         return self.mMaster
 
+
     def getMapKey(self):
+
+        if self.mMapKey is None:
+            self.mMapKey = str(uuid.uuid4())
         return self.mMapKey
 
+
     def getMasterKey(self):
         return PyCalendarComponentRecur.mapKey(self.mUID)
 
+
     def initDTSTAMP(self):
         # Save new one
         super(PyCalendarComponentRecur, self).initDTSTAMP()
@@ -139,46 +165,60 @@
         self.mHasStamp = temp is not None
         if self.mHasStamp:
             self.mStamp = temp
-    
+
+
     def getStamp(self):
         return self.mStamp
 
+
     def hasStamp(self):
         return self.mHasStamp
 
+
     def getStart(self):
         return self.mStart
 
+
     def hasStart(self):
         return self.mHasStart
 
+
     def getEnd(self):
         return self.mEnd
 
+
     def hasEnd(self):
         return self.mHasEnd
 
+
     def useDuration(self):
         return self.mDuration
 
+
     def isRecurrenceInstance(self):
         return self.mHasRecurrenceID
 
+
     def isAdjustFuture(self):
         return self.mAdjustFuture
 
+
     def isAdjustPrior(self):
         return self.mAdjustPrior
 
+
     def getRecurrenceID(self):
         return self.mRecurrenceID
 
+
     def isRecurring(self):
-        return (self.mRecurrences != None) and self.mRecurrences.hasRecurrence()
+        return (self.mRecurrences is not None) and self.mRecurrences.hasRecurrence()
 
+
     def getRecurrenceSet(self):
         return self.mRecurrences
 
+
     def setUID(self, uid):
         super(PyCalendarComponentRecur, self).setUID(uid)
 
@@ -188,12 +228,15 @@
         else:
             self.mMapKey = self.mapKey(self.mUID)
 
+
     def getSummary(self):
         return self.mSummary
 
+
     def setSummary(self, summary):
         self.mSummary = summary
 
+
     def getDescription(self):
         # Get DESCRIPTION
         txt = self.loadValueString(definitions.cICalProperty_DESCRIPTION)
@@ -202,6 +245,7 @@
         else:
             return ""
 
+
     def getLocation(self):
         # Get LOCATION
         txt = self.loadValueString(definitions.cICalProperty_LOCATION)
@@ -210,6 +254,7 @@
         else:
             return ""
 
+
     def finalise(self):
         super(PyCalendarComponentRecur, self).finalise()
 
@@ -237,16 +282,13 @@
             else:
                 # If no end or duration then use the start
                 self.mHasEnd = False
-                self.mEnd = PyCalendarDateTime(copyit=self.mStart)
+                self.mEnd = self.mStart.duplicate()
                 self.mDuration = False
         else:
             self.mHasEnd = True
             self.mEnd = temp
             self.mDuration = False
 
-        # Make sure start/end values are sensible
-        self.FixStartEnd()
-
         # Get SUMMARY
         temp = self.loadValueString(definitions.cICalProperty_SUMMARY)
         if temp is not None:
@@ -263,7 +305,7 @@
 
             # Also get the RANGE attribute
             attrs = self.findFirstProperty(definitions.cICalProperty_RECURRENCE_ID).getAttributes()
-            if attrs.has_key(definitions.cICalAttribute_RANGE):
+            if definitions.cICalAttribute_RANGE in attrs:
                 self.mAdjustFuture = (attrs[definitions.cICalAttribute_RANGE][0].getFirstValue() == definitions.cICalAttribute_RANGE_THISANDFUTURE)
                 self.mAdjustPrior = (attrs[definitions.cICalAttribute_RANGE][0].getFirstValue() == definitions.cICalAttribute_RANGE_THISANDPRIOR)
             else:
@@ -272,35 +314,94 @@
         else:
             self.mMapKey = self.mapKey(self.mUID)
 
+        self._resetRecurrenceSet()
+
+
+    def validate(self, doFix=False):
+        """
+        Validate the data in this component and optionally fix any problems. Return
+        a tuple containing two lists: the first describes problems that were fixed, the
+        second problems that were not fixed. Caller can then decide what to do with unfixed
+        issues.
+        """
+
+        # Do normal checks
+        fixed, unfixed = super(PyCalendarComponentRecur, self).validate(doFix)
+
+        # Check that any UNTIL value matches that for DTSTART
+        if self.mHasStart and self.mRecurrences:
+            dtutc = self.mStart.duplicateAsUTC()
+            for rrule in self.mRecurrences.getRules():
+                if rrule.getUseUntil():
+                    if rrule.getUntil().isDateOnly() ^ self.mStart.isDateOnly():
+                        logProblem = "[%s] Value types must match: %s, %s" % (
+                            self.getType(),
+                            definitions.cICalProperty_DTSTART,
+                            definitions.cICalValue_RECUR_UNTIL,
+                        )
+                        if doFix:
+                            rrule.getUntil().setDateOnly(self.mStart.isDateOnly())
+                            if not self.mStart.isDateOnly():
+                                rrule.getUntil().setHHMMSS(dtutc.getHours(), dtutc.getMinutes(), dtutc.getSeconds())
+                                rrule.getUntil().setTimezone(PyCalendarTimezone(utc=True))
+                            self.mRecurrences.changed()
+                            fixed.append(logProblem)
+                        else:
+                            unfixed.append(logProblem)
+
+        return fixed, unfixed
+
+
+    def check_cardinality_STATUS_Fix(self, fixed, unfixed, doFix):
+        """
+        Special for bug with STATUS where STATUS:CANCELLED is added alongside
+        another STATUS. In this case we want STATUS:CANCELLED to win.
+        """
+        for propname in self.propertyCardinality_STATUS_Fix:
+            if self.countProperty(propname) > 1:
+                logProblem = "[%s] Too many properties: %s" % (self.getType(), propname)
+                if doFix:
+                    # Check that one of them is STATUS:CANCELLED
+                    for prop in self.getProperties(propname):
+                        if prop.getTextValue().getValue().upper() == definitions.cICalProperty_STATUS_CANCELLED:
+                            self.removeProperties(propname)
+                            self.addProperty(PyCalendarProperty(propname, definitions.cICalProperty_STATUS_CANCELLED))
+                            fixed.append(logProblem)
+                            break
+                    else:
+                        unfixed.append(logProblem)
+                else:
+                    unfixed.append(logProblem)
+
+
+    def _resetRecurrenceSet(self):
         # May need to create items
-        if ((self.countProperty(definitions.cICalProperty_RRULE) != 0)
-                or (self.countProperty(definitions.cICalProperty_RDATE) != 0)
-                or (self.countProperty(definitions.cICalProperty_EXRULE) != 0)
-                or (self.countProperty(definitions.cICalProperty_EXDATE) != 0)):
-            if self.mRecurrences is None:
-                self.mRecurrences = PyCalendarRecurrenceSet()
+        self.mRecurrences = None
+        if ((self.countProperty(definitions.cICalProperty_RRULE) != 0) or
+            (self.countProperty(definitions.cICalProperty_RDATE) != 0) or
+            (self.countProperty(definitions.cICalProperty_EXRULE) != 0) or
+            (self.countProperty(definitions.cICalProperty_EXDATE) != 0)):
 
+            self.mRecurrences = PyCalendarRecurrenceSet()
+
             # Get RRULEs
-            self.loadValueRRULE(definitions.cICalProperty_RRULE,
-                    self.mRecurrences, True)
+            self.loadValueRRULE(definitions.cICalProperty_RRULE, self.mRecurrences, True)
 
             # Get RDATEs
-            self.loadValueRDATE(definitions.cICalProperty_RDATE,
-                    self.mRecurrences, True)
+            self.loadValueRDATE(definitions.cICalProperty_RDATE, self.mRecurrences, True)
 
             # Get EXRULEs
-            self.loadValueRRULE(definitions.cICalProperty_EXRULE,
-                    self.mRecurrences, False)
+            self.loadValueRRULE(definitions.cICalProperty_EXRULE, self.mRecurrences, False)
 
             # Get EXDATEs
-            self.loadValueRDATE(definitions.cICalProperty_EXDATE,
-                    self.mRecurrences, False)
+            self.loadValueRDATE(definitions.cICalProperty_EXDATE, self.mRecurrences, False)
 
+
     def FixStartEnd(self):
         # End is always greater than start if start exists
         if self.mHasStart and self.mEnd <= self.mStart:
             # Use the start
-            self.mEnd = PyCalendarDateTime(copyit=self.mStart)
+            self.mEnd = self.mStart.duplicate()
             self.mDuration = False
 
             # Adjust to approriate non-inclusive end point
@@ -314,7 +415,8 @@
                 self.mEnd.offsetDay(1)
                 self.mEnd.setHHMMSS(0, 0, 0)
 
-    def expandPeriod(self, period, list):
+
+    def expandPeriod(self, period, results):
         # Check for recurrence and True master
         if ((self.mRecurrences is not None) and self.mRecurrences.hasRecurrence()
                 and not self.isRecurrenceInstance()):
@@ -323,12 +425,11 @@
             self.mRecurrences.expand(self.mStart, period, items)
 
             # Look for overridden recurrence items
-            from pycalendar.calendar import PyCalendar
-            cal = PyCalendar.getICalendar(self.getCalendar())
+            cal = self.mParentComponent
             if cal is not None:
                 # Remove recurrence instances from the list of items
                 recurs = []
-                cal.getRecurrenceInstancesIds(PyCalendarComponent.eVEVENT, self.getUID(), recurs)
+                cal.getRecurrenceInstancesIds(definitions.cICalComponent_VEVENT, self.getUID(), recurs)
                 recurs.sort()
                 if len(recurs) != 0:
                     temp = []
@@ -337,7 +438,7 @@
 
                     # Now get actual instances
                     instances = []
-                    cal.getRecurrenceInstancesItems(PyCalendarComponent.eVEVENT, self.getUID(), instances)
+                    cal.getRecurrenceInstancesItems(definitions.cICalComponent_VEVENT, self.getUID(), instances)
 
                     # Get list of each ones with RANGE
                     prior = []
@@ -352,7 +453,7 @@
                     if len(prior) + len(future) == 0:
                         # Add each expanded item
                         for iter in items:
-                            list.append(self.createExpanded(self, iter))
+                            results.append(self.createExpanded(self, iter))
                     else:
                         # Sort each list first
                         prior.sort(self.sort_by_dtstart)
@@ -381,22 +482,23 @@
 
                             if slave is None:
                                 slave = self
-                            list.append(self.createExpanded(slave, iter1))
+                            results.append(self.createExpanded(slave, iter1))
                 else:
                     # Add each expanded item
                     for iter in items:
-                        list.append(self.createExpanded(self, iter))
+                        results.append(self.createExpanded(self, iter))
 
         elif self.withinPeriod(period):
             if self.isRecurrenceInstance():
                 rid = self.mRecurrenceID
             else:
                 rid = None
-            list.append(PyCalendarComponentExpanded(self, rid))
+            results.append(PyCalendarComponentExpanded(self, rid))
 
+
     def withinPeriod(self, period):
         # Check for recurrence
-        if ((self.mRecurrences != None) and self.mRecurrences.hasRecurrence()):
+        if ((self.mRecurrences is not None) and self.mRecurrences.hasRecurrence()):
             items = []
             self.mRecurrences.expand(self.mStart, period, items)
             return len(items) != 0
@@ -408,11 +510,13 @@
             else:
                 return True
 
+
     def changedRecurrence(self):
         # Clear cached values
         if self.mRecurrences is not None:
             self.mRecurrences.changed()
 
+
     # Editing
     def editSummary(self, summary):
         # Updated cached value
@@ -421,12 +525,14 @@
         # Remove existing items
         self.editProperty(definitions.cICalProperty_SUMMARY, summary)
 
+
     def editDetails(self, description, location):
 
         # Edit existing items
         self.editProperty(definitions.cICalProperty_DESCRIPTION, description)
         self.editProperty(definitions.cICalProperty_LOCATION, location)
 
+
     def editTiming(self):
         # Updated cached values
         self.mHasStart = False
@@ -441,6 +547,7 @@
         self.removeProperties(definitions.cICalProperty_DURATION)
         self.removeProperties(definitions.cICalProperty_DUE)
 
+
     def editTimingDue(self, due):
         # Updated cached values
         self.mHasStart = False
@@ -459,6 +566,7 @@
         prop = PyCalendarProperty(definitions.cICalProperty_DUE, due)
         self.addProperty(prop)
 
+
     def editTimingStartEnd(self, start, end):
         # Updated cached values
         self.mHasStart = self.mHasEnd = True
@@ -477,12 +585,13 @@
         self.addProperty(prop)
 
         # If its an all day event and the end one day after the start, ignore it
-        temp = PyCalendarDateTime(copyit=start)
+        temp = start.duplicate()
         temp.offsetDay(1)
         if not start.isDateOnly() or end != temp:
             prop = PyCalendarProperty(definitions.cICalProperty_DTEND, end)
             self.addProperty(prop)
 
+
     def editTimingStartDuration(self, start, duration):
         # Updated cached values
         self.mHasStart = True
@@ -507,6 +616,7 @@
             prop = PyCalendarProperty(definitions.cICalProperty_DURATION, duration)
             self.addProperty(prop)
 
+
     def editRecurrenceSet(self, recurs):
         # Must have items
         if self.mRecurrences is None:
@@ -535,6 +645,7 @@
             prop = PyCalendarProperty(definitions.cICalProperty_EXDATE, iter)
             self.addProperty(prop)
 
+
     def excludeRecurrence(self, start):
         # Must have items
         if self.mRecurrences is None:
@@ -547,6 +658,7 @@
         prop = PyCalendarProperty(definitions.cICalProperty_EXDATE, start)
         self.addProperty(prop)
 
+
     def excludeFutureRecurrence(self, start):
         # Must have items
         if self.mRecurrences is None:
@@ -567,6 +679,7 @@
             prop = PyCalendarProperty(definitions.cICalProperty_RDATE, iter)
             self.addProperty(prop)
 
+
     def initFromMaster(self):
         # Only if not master
         if self.recurring():
@@ -596,5 +709,6 @@
                 temp = self.loadValueDuration(definitions.cICalProperty_DURATION)
                 self.mEnd = self.mStart + temp
 
+
     def createExpanded(self, master, recurid):
         return PyCalendarComponentExpanded(master, recurid)

Modified: PyCalendar/trunk/src/pycalendar/datetime.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/datetime.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/datetime.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,17 +14,19 @@
 #    limitations under the License.
 ##
 
+from pycalendar import definitions
+from pycalendar import locale
+from pycalendar import utils
+from pycalendar import xmldefs
+from pycalendar.duration import PyCalendarDuration
+from pycalendar.timezone import PyCalendarTimezone
+from pycalendar.valueutils import ValueMixin
 import cStringIO as StringIO
 import time
+import xml.etree.cElementTree as XML
 
-from duration import PyCalendarDuration
-from timezone import PyCalendarTimezone
-import definitions
-import locale
-import utils
+class PyCalendarDateTime(ValueMixin):
 
-class PyCalendarDateTime(object):
-
     SUNDAY = 0
     MONDAY = 1
     TUESDAY = 2
@@ -32,7 +34,7 @@
     THURSDAY = 4
     FRIDAY = 5
     SATURDAY = 6
-    
+
     FULLDATE = 0
     ABBREVDATE = 1
     NUMERICDATE = 2
@@ -45,8 +47,9 @@
 
         return e1.compareDateTime(e2)
 
-    def __init__( self, year = None, month = None, day = None, hours = None, minutes = None, seconds = None, tzid = None, packed = None, copyit = None ):
-        
+
+    def __init__(self, year=None, month=None, day=None, hours=None, minutes=None, seconds=None, tzid=None, utcoffset=None):
+
         self.mYear = 1970
         self.mMonth = 1
         self.mDay = 1
@@ -57,8 +60,9 @@
 
         self.mDateOnly = False
 
-        self.mTZUTC = True
+        self.mTZUTC = False
         self.mTZID = None
+        self.mTZOffset = None
 
         self.mPosixTimeCached = False
         self.mPosixTime = 0
@@ -76,100 +80,118 @@
             if tzid:
                 self.mTZUTC = tzid.getUTC()
                 self.mTZID = tzid.getTimezoneID()
-        elif (packed is not None):
-            # Unpack a packed date
-            self.mYear = utils.unpackDateYear( packed )
-            self.mMonth = utils.unpackDateMonth( packed )
-            self.mDay = utils.unpackDateDay( packed )
-            if self.mDay < 0:
-                self.mDay = -self.mDay
-            self.mDateOnly = True
-    
-            if tzid:
-                self.mTZUTC = tzid.getUTC()
-                self.mTZID = tzid.getTimezoneID()
-    
-            self.normalise()
-        elif (copyit is not None):
-            self.mYear = copyit.mYear
-            self.mMonth = copyit.mMonth
-            self.mDay = copyit.mDay
-    
-            self.mHours = copyit.mHours
-            self.mMinutes = copyit.mMinutes
-            self.mSeconds = copyit.mSeconds
-    
-            self.mDateOnly = copyit.mDateOnly
-    
-            self.mTZUTC = copyit.mTZUTC
-            self.mTZID = copyit.mTZID
-    
-            self.mPosixTimeCached = copyit.mPosixTimeCached
-            self.mPosixTime = copyit.mPosixTime
-    
+
+
+    def duplicate(self):
+        other = PyCalendarDateTime(self.mYear, self.mMonth, self.mDay, self.mHours, self.mMinutes, self.mSeconds)
+
+        other.mDateOnly = self.mDateOnly
+
+        other.mTZUTC = self.mTZUTC
+        other.mTZID = self.mTZID
+        other.mTZOffset = self.mTZOffset
+
+        other.mPosixTimeCached = self.mPosixTimeCached
+        other.mPosixTime = self.mPosixTime
+
+        return other
+
+
+    def duplicateAsUTC(self):
+        other = self.duplicate()
+        other.adjustToUTC()
+        return other
+
+
     def __repr__(self):
-        return self.getText()
+        return "PyCalendarDateTime: %s" % (self.getText(),)
 
+
     def __hash__(self):
-        return self.getPosixTime()
-    
+        return hash(self.getPosixTime())
+
+
     # Operators
-    def __add__( self, duration ):
+    def __add__(self, duration):
         # Add duration seconds to temp object and normalise it
-        result = PyCalendarDateTime(copyit=self)
+        result = self.duplicate()
         result.mSeconds += duration.getTotalSeconds()
-        result.changed()
         result.normalise()
         return result
- 
-    def __sub__( self, date ):
-        # Look for floating
-        if self.floating() or date.floating():
-            # Adjust the floating ones to fixed
-            copy1 = PyCalendarDateTime(copyit=self)
-            copy2 = PyCalendarDateTime(copyit=date)
 
-            if copy1.floating() and copy2.floating():
-                # Set both to UTC and do comparison
-                copy1.setTimezoneUTC( True )
-                copy2.setTimezoneUTC( True )
-                return copy1 - copy2
-            elif copy1.floating():
-                # Set to be the same
-                copy1.setTimezoneUTC( copy2.getTimezoneUTC() )
-                copy1.setTimezoneID( copy2.getTimezoneID() )
-                return copy1 - copy2
+
+    def __sub__(self, dateorduration):
+
+        if isinstance(dateorduration, PyCalendarDateTime):
+
+            date = dateorduration
+
+            # Look for floating
+            if self.floating() or date.floating():
+                # Adjust the floating ones to fixed
+                copy1 = self.duplicate()
+                copy2 = date.duplicate()
+
+                if copy1.floating() and copy2.floating():
+                    # Set both to UTC and do comparison
+                    copy1.setTimezoneUTC(True)
+                    copy2.setTimezoneUTC(True)
+                    return copy1 - copy2
+                elif copy1.floating():
+                    # Set to be the same
+                    copy1.setTimezoneID(copy2.getTimezoneID())
+                    copy1.setTimezoneUTC(copy2.getTimezoneUTC())
+                    return copy1 - copy2
+                else:
+                    # Set to be the same
+                    copy2.setTimezoneID(copy1.getTimezoneID())
+                    copy2.setTimezoneUTC(copy1.getTimezoneUTC())
+                    return copy1 - copy2
+
             else:
-                # Set to be the same
-                copy2.setTimezoneID( copy1.getTimezoneID() )
-                copy2.setTimezoneUTC( copy1.getTimezoneUTC() )
-                return copy1 - copy2
- 
-        else:
-            # Do diff of date-time in seconds
-            diff = self.getPosixTime() - date.getPosixTime()
-            return PyCalendarDuration(duration=diff)
+                # Do diff of date-time in seconds
+                diff = self.getPosixTime() - date.getPosixTime()
+                return PyCalendarDuration(duration=diff)
 
+        elif isinstance(dateorduration, PyCalendarDuration):
+
+            duration = dateorduration
+            result = self.duplicate()
+            result.mSeconds -= duration.getTotalSeconds()
+            result.normalise()
+            return result
+
+        raise ValueError
+
+
     # Comparators
-    def __eq__( self, comp ):
-        return self.compareDateTime( comp ) == 0
+    def __eq__(self, comp):
+        return self.compareDateTime(comp) == 0
 
-    def __ne__( self, comp ):
-        return self.compareDateTime( comp ) != 0
 
-    def __ge__( self, comp ):
-        return self.compareDateTime( comp ) >= 0
+    def __ne__(self, comp):
+        return self.compareDateTime(comp) != 0
 
-    def __le__( self, comp ):
-        return self.compareDateTime( comp ) <= 0
 
-    def __gt__( self, comp ):
-        return self.compareDateTime( comp ) > 0
+    def __ge__(self, comp):
+        return self.compareDateTime(comp) >= 0
 
-    def __lt__( self, comp ):
-        return self.compareDateTime( comp ) < 0
 
-    def compareDateTime( self, comp ):
+    def __le__(self, comp):
+        return self.compareDateTime(comp) <= 0
+
+
+    def __gt__(self, comp):
+        return self.compareDateTime(comp) > 0
+
+
+    def __lt__(self, comp):
+        return self.compareDateTime(comp) < 0
+
+
+    def compareDateTime(self, comp):
+        if comp is None:
+            return 1
         # If either are date only, then just do date compare
         if self.mDateOnly or comp.mDateOnly:
             if self.mYear == comp.mYear:
@@ -191,10 +213,10 @@
                     return -1
                 else:
                     return 1
- 
+
         # If they have the same timezone do simple compare - no posix calc
         # needed
-        elif ( PyCalendarTimezone.same( self.mTZUTC, self.mTZID, comp.mTZUTC, comp.mTZID ) ):
+        elif (PyCalendarTimezone.same(self.mTZUTC, self.mTZID, comp.mTZUTC, comp.mTZID)):
             if self.mYear == comp.mYear:
                 if self.mMonth == comp.mMonth:
                     if self.mDay == comp.mDay:
@@ -243,17 +265,19 @@
                 else:
                     return 1
 
-    def compareDate( self, comp ):
-        return ( self.mYear == comp.mYear ) and ( self.mMonth == comp.mMonth ) and ( self.mDay == comp.mDay )
- 
-    def getPosixTime( self ):
+
+    def compareDate(self, comp):
+        return (self.mYear == comp.mYear) and (self.mMonth == comp.mMonth) and (self.mDay == comp.mDay)
+
+
+    def getPosixTime(self):
         # Look for cached value (or floating time which has to be calculated
         # each time)
-        if ( not self.mPosixTimeCached ) or self.floating():
+        if (not self.mPosixTimeCached) or self.floating():
             result = 0L
 
             # Add hour/mins/secs
-            result = ( self.mHours * 60L + self.mMinutes ) * 60L + self.mSeconds
+            result = (self.mHours * 60L + self.mMinutes) * 60L + self.mSeconds
 
             # Number of days since 1970
             result += self.daysSince1970() * 24L * 60L * 60L
@@ -261,56 +285,68 @@
             # Adjust for timezone offset
             result -= self.timeZoneSecondsOffset()
 
-            # Now indcate cache state
+            # Now indicate cache state
             self.mPosixTimeCached = True
             self.mPosixTime = result
- 
+
         return self.mPosixTime
 
-    def isDateOnly( self ):
+
+    def isDateOnly(self):
         return self.mDateOnly
 
-    def setDateOnly( self, date_only ):
+
+    def setDateOnly(self, date_only):
         self.mDateOnly = date_only
         self.changed()
 
-    def getYear( self ):
+
+    def getYear(self):
         return self.mYear
 
-    def setYear( self, year ):
+
+    def setYear(self, year):
         if self.mYear != year:
             self.mYear = year
             self.changed()
 
-    def offsetYear( self, diff_year ):
+
+    def offsetYear(self, diff_year):
         self.mYear += diff_year
         self.normalise()
 
-    def getMonth( self ):
+
+    def getMonth(self):
         return self.mMonth
 
-    def setMonth( self, month ):
+
+    def setMonth(self, month):
         if self.mMonth != month:
             self.mMonth = month
             self.changed()
 
-    def offsetMonth( self, diff_month ):
+
+    def offsetMonth(self, diff_month):
         self.mMonth += diff_month
         self.normalise()
 
-    def getDay( self ):
+
+    def getDay(self):
         return self.mDay
 
-    def setDay( self, day ):
+
+    def setDay(self, day):
         if self.mDay != day:
             self.mDay = day
             self.changed()
 
-    def offsetDay( self, diff_day ):
+
+    def offsetDay(self, diff_day):
         self.mDay += diff_day
         self.normalise()
 
-    def setYearDay( self, day ):
+
+    def setYearDay(self, day):
         # 1 .. 366 offset from start, or
         # -1 .. -366 offset from end
 
@@ -336,10 +372,12 @@
             # Normalise to get proper year/month/day values
             self.normalise()
 
-    def getYearDay( self ):
-        return self.mDay + utils.daysUptoMonth( self.mMonth, self.mYear )
 
-    def setMonthDay( self, day ):
+    def getYearDay(self):
+        return self.mDay + utils.daysUptoMonth(self.mMonth, self.mYear)
+
+
+    def setMonthDay(self, day):
         # 1 .. 31 offset from start, or
         # -1 .. -31 offset from end
 
@@ -363,15 +401,17 @@
             # Normalise to get proper year/month/day values
             self.normalise()
 
-    def isMonthDay( self, day ):
+
+    def isMonthDay(self, day):
         if day > 0:
             return self.mDay == day
         elif day < 0:
-            return self.mDay - 1 - utils.daysInMonth( self.mMonth, self.mYear ) == day
+            return self.mDay - 1 - utils.daysInMonth(self.mMonth, self.mYear) == day
         else:
             return False
 
-    def setWeekNo( self, weekno ):
+
+    def setWeekNo(self, weekno):
         # This is the iso 8601 week number definition
 
         # What day does the current year start on
@@ -379,13 +419,14 @@
         first_day = temp.getDayOfWeek()
 
         # Calculate and set yearday for start of week
-        if ( first_day == PyCalendarDateTime.SUNDAY ) or ( first_day == PyCalendarDateTime.MONDAY ) or \
-            ( first_day == PyCalendarDateTime.TUESDAY ) or ( first_day == PyCalendarDateTime.WEDNESDAY ) or ( first_day == PyCalendarDateTime.THURSDAY ):
-            self.setYearDay( ( weekno - 1 ) * 7 - first_day )
-        elif ( first_day == PyCalendarDateTime.FRIDAY ) or ( first_day == PyCalendarDateTime.SATURDAY ):
-            self.setYearDay( ( weekno - 1 ) * 7 - first_day + 7 )
+        if (first_day == PyCalendarDateTime.SUNDAY) or (first_day == PyCalendarDateTime.MONDAY) or \
+            (first_day == PyCalendarDateTime.TUESDAY) or (first_day == PyCalendarDateTime.WEDNESDAY) or (first_day == PyCalendarDateTime.THURSDAY):
+            self.setYearDay((weekno - 1) * 7 - first_day)
+        elif (first_day == PyCalendarDateTime.FRIDAY) or (first_day == PyCalendarDateTime.SATURDAY):
+            self.setYearDay((weekno - 1) * 7 - first_day + 7)
 
-    def getWeekNo( self ):
+
+    def getWeekNo(self):
         # This is the iso 8601 week number definition
 
         # What day does the current year start on
@@ -395,13 +436,14 @@
         # Get days upto the current one
         yearday = self.getYearDay()
 
-        if ( first_day == PyCalendarDateTime.SUNDAY ) or ( first_day == PyCalendarDateTime.MONDAY ) or \
-            ( first_day == PyCalendarDateTime.TUESDAY ) or ( first_day == PyCalendarDateTime.WEDNESDAY ) or ( first_day == PyCalendarDateTime.THURSDAY ):
-            return ( yearday + first_day ) / 7 + 1
-        elif ( first_day == PyCalendarDateTime.FRIDAY ) or ( first_day == PyCalendarDateTime.SATURDAY ):
-            return ( yearday + first_day - 7 ) / 7 + 1
+        if (first_day == PyCalendarDateTime.SUNDAY) or (first_day == PyCalendarDateTime.MONDAY) or \
+            (first_day == PyCalendarDateTime.TUESDAY) or (first_day == PyCalendarDateTime.WEDNESDAY) or (first_day == PyCalendarDateTime.THURSDAY):
+            return (yearday + first_day) / 7 + 1
+        elif (first_day == PyCalendarDateTime.FRIDAY) or (first_day == PyCalendarDateTime.SATURDAY):
+            return (yearday + first_day - 7) / 7 + 1
 
-    def isWeekNo( self, weekno ):
+
+    def isWeekNo(self, weekno):
         # This is the iso 8601 week number definition
 
         if weekno > 0:
@@ -411,7 +453,8 @@
             # the current year
             return False
 
-    def setDayOfWeekInYear( self, offset, day ):
+
+    def setDayOfWeekInYear(self, offset, day):
         # Set to first day in year
         self.mMonth = 1
         self.mDay = 1
@@ -420,24 +463,25 @@
         first_day = self.getDayOfWeek()
 
         if offset > 0:
-            cycle = ( offset - 1 ) * 7 + day
+            cycle = (offset - 1) * 7 + day
             cycle -= first_day
             if first_day > day:
                 cycle += 7
             self.mDay = cycle + 1
         elif offset < 0:
-            first_day += 365 + [1, 0][not utils.isLeapYear( self.mYear )] - 1
+            first_day += 365 + [1, 0][not utils.isLeapYear(self.mYear)] - 1
             first_day %= 7
 
-            cycle = ( -offset - 1 ) * 7 - day
+            cycle = (-offset - 1) * 7 - day
             cycle += first_day
             if day > first_day:
                 cycle += 7
-            self.mDay = 365 + [1, 0][not utils.isLeapYear( self.mYear )] - cycle
+            self.mDay = 365 + [1, 0][not utils.isLeapYear(self.mYear)] - cycle
 
         self.normalise()
 
-    def setDayOfWeekInMonth( self, offset, day ):
+
+    def setDayOfWeekInMonth(self, offset, day):
         # Set to first day in month
         self.mDay = 1
 
@@ -445,17 +489,17 @@
         first_day = self.getDayOfWeek()
 
         if offset > 0:
-            cycle = ( offset - 1 ) * 7 + day
+            cycle = (offset - 1) * 7 + day
             cycle -= first_day
             if first_day > day:
                 cycle += 7
             self.mDay = cycle + 1
         elif offset < 0:
-            days_in_month = utils.daysInMonth( self.mMonth, self.mYear )
+            days_in_month = utils.daysInMonth(self.mMonth, self.mYear)
             first_day += days_in_month - 1
             first_day %= 7
 
-            cycle = ( -offset - 1 ) * 7 - day
+            cycle = (-offset - 1) * 7 - day
             cycle += first_day
             if day > first_day:
                 cycle += 7
@@ -463,7 +507,8 @@
 
         self.normalise()
 
-    def setNextDayOfWeek( self, start, day ):
+
+    def setNextDayOfWeek(self, start, day):
         # Set to first day in month
         self.mDay = start
 
@@ -474,10 +519,11 @@
             self.mDay += 7
 
         self.mDay += day - first_day
-            
+
         self.normalise()
 
-    def isDayOfWeekInMonth( self, offset, day ):
+
+    def isDayOfWeekInMonth(self, offset, day):
         # First of the actual day must match
         if self.getDayOfWeek() != day:
             return False
@@ -488,13 +534,14 @@
 
         # Create temp date-time with the appropriate parameters and then
         # compare
-        temp = PyCalendarDateTime(copyit=self)
-        temp.setDayOfWeekInMonth( offset, day )
+        temp = self.duplicate()
+        temp.setDayOfWeekInMonth(offset, day)
 
         # Now compare dates
-        return self.compareDate( temp )
+        return self.compareDate(temp)
 
-    def getDayOfWeek( self ):
+
+    def getDayOfWeek(self):
         # Count days since 01-Jan-1970 which was a Thursday
         result = PyCalendarDateTime.THURSDAY + self.daysSince1970()
         result %= 7
@@ -503,155 +550,196 @@
 
         return result
 
-    def getMonthText( self, short_txt ):
+
+    def getMonthText(self, short_txt):
         # Make sure range is valid
-        if ( self.mMonth < 1 ) or ( self.mMonth > 12 ):
+        if (self.mMonth < 1) or (self.mMonth > 12):
             return ""
         else:
-            return locale.getMonth( self.mMonth, 
-                    [locale.SHORT, locale.LONG][not short_txt] )
+            return locale.getMonth(self.mMonth,
+                    [locale.SHORT, locale.LONG][not short_txt])
 
-    def getDayOfWeekText( self, day ):
-        return locale.getDay( day, locale.SHORT )
 
-    def setHHMMSS( self, hours, minutes, seconds ):
-        if ( self.mHours != hours ) or ( self.mMinutes != minutes ) or ( self.mSeconds != seconds ):
+    def getDayOfWeekText(self, day):
+        return locale.getDay(day, locale.SHORT)
+
+
+    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
             self.changed()
 
-    def getHours( self ):
+
+    def getHours(self):
         return self.mHours
 
-    def setHours( self, hours ):
+
+    def setHours(self, hours):
         if self.mHours != hours:
             self.mHours = hours
             self.changed()
 
-    def offsetHours( self, diff_hour ):
+
+    def offsetHours(self, diff_hour):
         self.mHours += diff_hour
         self.normalise()
 
-    def getMinutes( self ):
+
+    def getMinutes(self):
         return self.mMinutes
 
-    def setMinutes( self, minutes ):
+
+    def setMinutes(self, minutes):
         if self.mMinutes != minutes:
             self.mMinutes = minutes
             self.changed()
 
-    def offsetMinutes( self, diff_minutes ):
+
+    def offsetMinutes(self, diff_minutes):
         self.mMinutes += diff_minutes
         self.normalise()
 
-    def getSeconds( self ):
+
+    def getSeconds(self):
         return self.mSeconds
 
-    def setSeconds( self, seconds ):
+
+    def setSeconds(self, seconds):
         if self.mSeconds != seconds:
             self.mSeconds = seconds
             self.changed()
 
-    def offsetSeconds( self, diff_seconds ):
+
+    def offsetSeconds(self, diff_seconds):
         self.mSeconds += diff_seconds
         self.normalise()
 
-    def getTimezoneUTC( self ):
+
+    def getTimezoneUTC(self):
         return self.mTZUTC
 
-    def setTimezoneUTC( self, utc ):
-        self.mTZUTC = utc
 
-    def getTimezoneID( self ):
+    def setTimezoneUTC(self, utc):
+        if self.mTZUTC != utc:
+            self.mTZUTC = utc
+            self.changed()
+
+
+    def getTimezoneID(self):
         return self.mTZID
 
-    def setTimezoneID( self, tzid ):
+
+    def setTimezoneID(self, tzid):
         self.mTZUTC = False
         self.mTZID = tzid
         self.changed()
 
-    def utc( self ):
+
+    def utc(self):
         return self.mTZUTC
 
-    def local( self ):
-        return ( not self.mTZUTC ) and self.mTZID
 
-    def floating( self ):
-        return ( not self.mTZUTC ) and not self.mTZID
+    def local(self):
+        return (not self.mTZUTC) and self.mTZID
 
-    #public ICalendarTimezone getTimezone() {
-    #    return mTimezone
-    #}
 
-    def setTimezone( self, tzid ):
+    def floating(self):
+        return (not self.mTZUTC) and not self.mTZID
+
+
+    def getTimezone(self):
+        return PyCalendarTimezone(utc=self.mTZUTC, tzid=self.mTZID)
+
+
+    def setTimezone(self, tzid):
         self.mTZUTC = tzid.getUTC()
         self.mTZID = tzid.getTimezoneID()
         self.changed()
 
-    def adjustTimezone( self, tzid ):
+
+    def adjustTimezone(self, tzid):
         # Only if different
         s1 = tzid.getTimezoneID()
-        if ( tzid.getUTC() != self.mTZUTC ) or ( s1 != self.mTZID ):
+        if (tzid.getUTC() != self.mTZUTC) or (s1 != self.mTZID):
             offset_from = self.timeZoneSecondsOffset()
-            self.setTimezone( tzid )
+            self.setTimezone(tzid)
             offset_to = self.timeZoneSecondsOffset()
-            self.offsetSeconds( offset_to - offset_from )
+            self.offsetSeconds(offset_to - offset_from)
+        return self
 
-    def adjustToUTC( self ):
-        if not self.mTZUTC:
+
+    def adjustToUTC(self):
+        if self.local() and not self.mDateOnly:
+            # Cache and restore and adjust the posix value to avoid a recalc since it won't change during this adjust
+            tempPosix = self.mPosixTime if self.mPosixTimeCached else None
+
             utc = PyCalendarTimezone(utc=True)
 
             offset_from = self.timeZoneSecondsOffset()
-            self.setTimezone( utc )
-            offset_to = self.timeZoneSecondsOffset()
+            self.setTimezone(utc)
 
-            self.offsetSeconds( offset_to - offset_from )
+            self.offsetSeconds(-offset_from)
 
-    #def getAdjustedTime( self, tzid = PyCalendarManager.sPyCalendarManager.getDefaultTimezone() ):
-    def getAdjustedTime( self, tzid = None ):
+            if tempPosix is not None:
+                self.mPosixTimeCached = True
+                self.mPosixTime = tempPosix
+
+            self.mTZOffset = 0
+
+        return self
+
+
+    def getAdjustedTime(self, tzid=None):
         # Copy this and adjust to input timezone
-        adjusted = PyCalendarDateTime(copyit=self)
-        adjusted.adjustTimezone( tzid )
+        adjusted = self.duplicate()
+        adjusted.adjustTimezone(tzid)
         return adjusted
 
-    def setToday( self ):
+
+    def setToday(self):
         tz = PyCalendarTimezone(utc=self.mTZUTC, tzid=self.mTZID)
-        self.copy_ICalendarDateTime( self.getToday( tz ) )
+        self.copy_ICalendarDateTime(self.getToday(tz))
 
+
     @staticmethod
-    def getToday( tzid ):
+    def getToday(tzid=None):
         # Get from posix time
         now = time.time()
-        now_tm = time.localtime( now )
-    
+        now_tm = time.localtime(now)
+
         temp = PyCalendarDateTime(year=now_tm.tm_year, month=now_tm.tm_mon, day=now_tm.tm_mday, tzid=tzid)
         return temp
-    
-    def setNow( self ):
+
+
+    def setNow(self):
         tz = PyCalendarTimezone(utc=self.mTZUTC, tzid=self.mTZID)
-        self.copy_ICalendarDateTime( self.getNow( tz ) )
+        self.copy_ICalendarDateTime(self.getNow(tz))
 
+
     @staticmethod
-    def getNow( tzid ):
+    def getNow(tzid):
         utc = PyCalendarDateTime.getNowUTC()
-        utc.adjustTimezone( [tzid, PyCalendarTimezone()][tzid == 0] )
+        utc.adjustTimezone(tzid if tzid is not None else PyCalendarTimezone())
         return utc
-    
-    def setNowUTC( self ):
-        self.copy_PyCalendarDateTime( self.getNowUTC() )
 
+
+    def setNowUTC(self):
+        self.copy_PyCalendarDateTime(self.getNowUTC())
+
+
     @staticmethod
-    def getNowUTC( ):
+    def getNowUTC():
         # Get from posix time
         now = time.time()
-        now_tm = time.gmtime( now )
+        now_tm = time.gmtime(now)
         tzid = PyCalendarTimezone(utc=True)
-    
-        temp = PyCalendarDateTime(year=now_tm.tm_year, month=now_tm.tm_mon, day=now_tm.tm_mday, hours=now_tm.tm_hour, minutes=now_tm.tm_min, seconds=now_tm.tm_sec, tzid=tzid )
-        return temp
 
-    def recur( self, freq, interval ):
+        return PyCalendarDateTime(year=now_tm.tm_year, month=now_tm.tm_mon, day=now_tm.tm_mday, hours=now_tm.tm_hour, minutes=now_tm.tm_min, seconds=now_tm.tm_sec, tzid=tzid)
+
+
+    def recur(self, freq, interval):
         # Add appropriate interval
         if freq == definitions.eRecurrence_SECONDLY:
             self.mSeconds += interval
@@ -671,7 +759,7 @@
             # the same day number as the current one.
             self.mMonth += interval
             self.normalise()
-            while self.mDay > utils.daysInMonth( self.mMonth, self.mYear ):
+            while self.mDay > utils.daysInMonth(self.mMonth, self.mYear):
                 self.mMonth += interval
                 self.normalise()
         elif freq == definitions.eRecurrence_YEARLY:
@@ -680,53 +768,55 @@
         # Normalise to standard date-time ranges
         self.normalise()
 
-    def getLocaleDate( self, locale ):
 
+    def getLocaleDate(self, locale):
+
         buf = StringIO.StringIO()
 
         if locale == PyCalendarDateTime.FULLDATE:
-            buf.write( locale.getDay( self.getDayOfWeek(), locale.LONG ) )
-            buf.write( ", " )
-            buf.write( locale.getMonth( self.mMonth, locale.LONG ) )
-            buf.write( " " )
-            buf.write( str( self.mDay ) )
-            buf.write( ", " )
-            buf.write( str( self.mYear ) )
+            buf.write(locale.getDay(self.getDayOfWeek(), locale.LONG))
+            buf.write(", ")
+            buf.write(locale.getMonth(self.mMonth, locale.LONG))
+            buf.write(" ")
+            buf.write(str(self.mDay))
+            buf.write(", ")
+            buf.write(str(self.mYear))
         elif locale == PyCalendarDateTime.ABBREVDATE:
-            buf.write( locale.getDay( self.getDayOfWeek(), locale.SHORT ) )
-            buf.write( ", " )
-            buf.write( locale.getMonth( self.mMonth, locale.SHORT ) )
-            buf.write( " " )
-            buf.write( str( self.mDay ) )
-            buf.write( ", " )
-            buf.write( str( self.mYear ) )
+            buf.write(locale.getDay(self.getDayOfWeek(), locale.SHORT))
+            buf.write(", ")
+            buf.write(locale.getMonth(self.mMonth, locale.SHORT))
+            buf.write(" ")
+            buf.write(str(self.mDay))
+            buf.write(", ")
+            buf.write(str(self.mYear))
         elif locale == PyCalendarDateTime.NUMERICDATE:
-            buf.write( str( self.mMonth ) )
-            buf.write( "/" )
-            buf.write( str( self.mDay ) )
-            buf.write( "/" )
-            buf.write( str( self.mYear ) )
+            buf.write(str(self.mMonth))
+            buf.write("/")
+            buf.write(str(self.mDay))
+            buf.write("/")
+            buf.write(str(self.mYear))
         elif locale == PyCalendarDateTime.FULLDATENOYEAR:
-            buf.write( locale.getDay( self.getDayOfWeek(), locale.LONG ) )
-            buf.write( ", " )
-            buf.write( locale.getMonth( self.mMonth, locale.LONG ) )
-            buf.write( " " )
-            buf.write( str( self.mDay ) )
+            buf.write(locale.getDay(self.getDayOfWeek(), locale.LONG))
+            buf.write(", ")
+            buf.write(locale.getMonth(self.mMonth, locale.LONG))
+            buf.write(" ")
+            buf.write(str(self.mDay))
         elif locale == PyCalendarDateTime.ABBREVDATENOYEAR:
-            buf.write( locale.getDay( self. getDayOfWeek(), locale.SHORT ) )
-            buf.write( ", " )
-            buf.write( locale.getMonth( self.mMonth, locale.SHORT ) )
-            buf.write( " " )
-            buf.write( str( self.mDay ) )
+            buf.write(locale.getDay(self. getDayOfWeek(), locale.SHORT))
+            buf.write(", ")
+            buf.write(locale.getMonth(self.mMonth, locale.SHORT))
+            buf.write(" ")
+            buf.write(str(self.mDay))
         elif locale == PyCalendarDateTime.NUMERICDATENOYEAR:
-            buf.write( str( self.mMonth ) )
-            buf.write( "/" )
-            buf.write( str( self.mDay ) )
+            buf.write(str(self.mMonth))
+            buf.write("/")
+            buf.write(str(self.mDay))
 
         return buf.getvalue()
 
-    def getTime( self, with_seconds, am_pm, tzid ):
 
+    def getTime(self, with_seconds, am_pm, tzid):
+
         buf = StringIO.StringIO()
         adjusted_hour = self.mHours
 
@@ -739,111 +829,235 @@
             if adjusted_hour == 0:
                 adjusted_hour = 12
 
-            buf.write( str( adjusted_hour ) )
-            buf.write( ":" )
+            buf.write(str(adjusted_hour))
+            buf.write(":")
             if self.mMinutes < 10:
-                buf.write( "0" )
-            buf.write( str( self.mMinutes ) )
+                buf.write("0")
+            buf.write(str(self.mMinutes))
             if with_seconds:
-                buf.write( ":" )
+                buf.write(":")
                 if self.mSeconds < 10:
-                    buf.write( "0" )
-                buf.write( str( self.mSeconds ) )
+                    buf.write("0")
+                buf.write(str(self.mSeconds))
 
-            buf.write( [" AM", " PM"][not am] )
+            buf.write([" AM", " PM"][not am])
         else:
             if self.mHours < 10:
-                buf.write( "0" )
-            buf.write( str( self.mHours ) )
-            buf.write( ":" )
+                buf.write("0")
+            buf.write(str(self.mHours))
+            buf.write(":")
             if self.mMinutes < 10:
-                buf.write( "0" )
-            buf.write( str( self.mMinutes ) )
+                buf.write("0")
+            buf.write(str(self.mMinutes))
             if with_seconds:
-                buf.write( ":" )
+                buf.write(":")
                 if self.mSeconds < 10:
-                    buf.write( "0" )
-                buf.write( str( self.mSeconds ) )
+                    buf.write("0")
+                buf.write(str(self.mSeconds))
 
         if tzid:
             desc = self.timeZoneDescriptor()
-            if len( desc ) > 0:
-                buf.write( " " )
-                buf.write( desc )
+            if len(desc) > 0:
+                buf.write(" ")
+                buf.write(desc)
 
         return buf.getvalue()
 
-    def getLocaleDateTime( self, locale, with_seconds, am_pm, tzid ):
-        return self.getLocaleDate( locale ) + " " + self.getTime( with_seconds, am_pm, tzid )
 
-    def getText( self ):
-    
+    def getLocaleDateTime(self, locale, with_seconds, am_pm, tzid):
+        return self.getLocaleDate(locale) + " " + self.getTime(with_seconds, am_pm, tzid)
+
+
+    def getText(self):
+
         if self.mDateOnly:
-            buf = "%4d%02d%02d" % ( self.mYear, self.mMonth, self.mDay )
+            return "%04d%02d%02d" % (self.mYear, self.mMonth, self.mDay)
         else:
             if self.mTZUTC:
-                buf = "%4d%02d%02dT%02d%02d%02dZ" % ( self.mYear, self.mMonth, self.mDay, self.mHours, self.mMinutes, self.mSeconds )
+                return "%04d%02d%02dT%02d%02d%02dZ" % (self.mYear, self.mMonth, self.mDay, self.mHours, self.mMinutes, self.mSeconds)
+            elif isinstance(self.mTZID, int):
+                sign = "-" if self.mTZID < 0 else "+"
+                hours = abs(self.mTZID) / 3600
+                minutes = divmod(abs(self.mTZID) / 60, 60)[1]
+                return "%04d%02d%02dT%02d%02d%02d%s%02d%02d" % (self.mYear, self.mMonth, self.mDay, self.mHours, self.mMinutes, self.mSeconds, sign, hours, minutes)
             else:
-                buf = "%4d%02d%02dT%02d%02d%02d" % ( self.mYear, self.mMonth, self.mDay, self.mHours, self.mMinutes, self.mSeconds )
-        return buf
+                return "%04d%02d%02dT%02d%02d%02d" % (self.mYear, self.mMonth, self.mDay, self.mHours, self.mMinutes, self.mSeconds)
 
-    def parse( self, data ):
 
-        # parse format YYYYMMDD[THHMMSS[Z]]
+    def getXMLText(self):
 
-        # Size must be at least 8
-        dlen = len(data)
-        if dlen < 8:
-            return
+        if self.mDateOnly:
+            return "%04d-%02d-%02d" % (self.mYear, self.mMonth, self.mDay)
+        else:
+            if self.mTZUTC:
+                return "%04d-%02d-%02dT%02d:%02d:%02dZ" % (self.mYear, self.mMonth, self.mDay, self.mHours, self.mMinutes, self.mSeconds)
+            elif isinstance(self.mTZID, int):
+                sign = "-" if self.mTZID < 0 else "+"
+                hours = abs(self.mTZID) / 3600
+                minutes = divmod(abs(self.mTZID) / 60, 60)[1]
+                return "%04d-%02d-%02dT%02d:%02d:%02d%s%02d:%02d" % (self.mYear, self.mMonth, self.mDay, self.mHours, self.mMinutes, self.mSeconds, sign, hours, minutes)
+            else:
+                return "%04d-%02d-%02dT%02d:%02d:%02d" % (self.mYear, self.mMonth, self.mDay, self.mHours, self.mMinutes, self.mSeconds)
 
+
+    @classmethod
+    def parseText(cls, data, fullISO=False):
+        dt = cls()
+        dt.parse(data, fullISO)
+        return dt
+
+
+    def parseDate(self, data, fullISO):
         # Get year
-        buf = data[0:4]
-        self.mYear = int( buf )
+        self.mYear = int(data[0:4])
+        index = 4
+        if fullISO and data[index] == "-":
+            index += 1
 
         # Get month
-        buf = data[4:6]
-        self.mMonth = int( buf )
+        self.mMonth = int(data[index:index + 2])
+        index += 2
+        if fullISO and data[index] == "-":
+            index += 1
 
         # Get day
-        buf = data[6:8]
-        self.mDay = int( buf )
+        self.mDay = int(data[index:index + 2])
+        index += 2
 
-        # Now look for more
-        if ( dlen >= 15 ) and ( data[8] == 'T' ):
-            # Get hours
-            buf = data[9:11]
-            self.mHours = int( buf )
+        if ' ' in data[:index]:
+            raise ValueError
+        if self.mYear < 0 or self.mMonth < 0 or self.mDay < 0:
+            raise ValueError
+        return index
 
-            # Get minutes
-            buf = data[11:13]
-            self.mMinutes = int( buf )
 
-            # Get seconds
-            buf = data[13:15]
-            self.mSeconds = int( buf )
+    def parseTime(self, data, index, fullISO):
+        # Get hours
+        self.mHours = int(data[index:index + 2])
+        index += 2
+        if fullISO and data[index] == ":":
+            index += 1
 
-            self.mDateOnly = False
+        # Get minutes
+        self.mMinutes = int(data[index:index + 2])
+        index += 2
+        if fullISO and data[index] == ":":
+            index += 1
 
-            if dlen > 15:
-                self.mTZUTC = ( data[15] == 'Z' )
+        # Get seconds
+        self.mSeconds = int(data[index:index + 2])
+        index += 2
+
+        return index
+
+
+    def parseFractionalAndUTCOffset(self, data, index, dlen):
+        # Optional fraction
+        if data[index] == ",":
+            index += 1
+            if not data[index].isdigit():
+                raise ValueError
+            while index < dlen and data[index].isdigit():
+                index += 1
+
+        # Optional timezone descriptor
+        if index < dlen:
+            if data[index] == "Z":
+                index += 1
+                self.mTZUTC = True
             else:
-                self.mTZUTC = False
-        else:
-            self.mDateOnly = True
+                if data[index] == "+":
+                    sign = 1
+                elif data[index] == "-":
+                    sign = -1
+                else:
+                    raise ValueError
+                index += 1
 
+                # Get hours
+                hours_offset = int(data[index:index + 2])
+                index += 2
+                if data[index] == ":":
+                    index += 1
+
+                # Get minutes
+                minutes_offset = int(data[index:index + 2])
+                index += 2
+
+                self.mTZID = sign * (hours_offset * 60 + minutes_offset) * 60
+
+        if index < dlen:
+            raise ValueError
+        return index
+
+
+    def parse(self, data, fullISO=False):
+
+        # iCalendar:
+        #   parse format YYYYMMDD[THHMMSS[Z]]
+        # vCard (fullISO)
+        #   parse format YYYY[-]MM[-]DD[THH[:]MM[:]SS[(Z/(+/-)HHMM]]
+
+        try:
+            # Parse out the date
+            index = self.parseDate(data, fullISO)
+
+            # Now look for more
+            dlen = len(data)
+            if index < dlen:
+
+                if data[index] != 'T':
+                    raise ValueError
+                index += 1
+
+                # Parse out the time
+                index = self.parseTime(data, index, fullISO)
+                self.mDateOnly = False
+
+                if index < dlen:
+                    if fullISO:
+                        index = self.parseFractionalAndUTCOffset(data, index, dlen)
+                    else:
+                        if index < dlen:
+                            if data[index] != 'Z':
+                                raise ValueError
+                            index += 1
+                            self.mTZUTC = True
+                            if index < dlen:
+                                raise ValueError
+                else:
+                    self.mTZUTC = False
+            else:
+                self.mDateOnly = True
+        except IndexError:
+            raise ValueError
+
         # Always uncache posix time
         self.changed()
 
-    def generate( self, os ):
+
+    def generate(self, os):
         try:
-            os.write( self.getText() )
+            os.write(self.getText())
         except:
             pass
 
-    def generateRFC2822( self, os ):
+
+    def generateRFC2822(self, os):
         pass
 
-    def normalise( self ):
+
+    def writeXML(self, node, namespace):
+        value = XML.SubElement(
+            node,
+            xmldefs.makeTag(
+                namespace,
+                xmldefs.value_date if self.isDateOnly() else xmldefs.value_date_time
+        ))
+        value.text = self.getXMLText()
+
+
+    def normalise(self):
         # Normalise seconds
         normalised_secs = self.mSeconds % 60
         adjustment_mins = self.mSeconds / 60
@@ -878,9 +1092,9 @@
         # Adjust the month first, since the day adjustment is month dependent
 
         # Normalise month
-        normalised_month = ( ( self.mMonth - 1 ) % 12 ) + 1
-        adjustment_year = ( self.mMonth - 1 ) / 12
-        if ( normalised_month - 1 ) < 0:
+        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
@@ -888,8 +1102,8 @@
 
         # Now do days
         if self.mDay > 0:
-            while self.mDay > utils.daysInMonth( self.mMonth, self.mYear ):
-                self.mDay -= utils.daysInMonth( self.mMonth, self.mYear )
+            while self.mDay > utils.daysInMonth(self.mMonth, self.mYear):
+                self.mDay -= utils.daysInMonth(self.mMonth, self.mYear)
                 self.mMonth += 1
                 if self.mMonth > 12:
                     self.mMonth = 1
@@ -900,34 +1114,41 @@
                 if self.mMonth < 1:
                     self.mMonth = 12
                     self.mYear -= 1
-                self.mDay += utils.daysInMonth( self.mMonth, self.mYear )
+                self.mDay += utils.daysInMonth(self.mMonth, self.mYear)
 
         # Always invalidate posix time cache
         self.changed()
 
-    def timeZoneSecondsOffset( self ):
+
+    def timeZoneSecondsOffset(self):
         if self.mTZUTC:
             return 0
-        tz = PyCalendarTimezone(utc=self.mTZUTC, tzid=self.mTZID)
-        return tz.timeZoneSecondsOffset( self )
+        if self.mTZOffset is None:
+            tz = PyCalendarTimezone(utc=self.mTZUTC, tzid=self.mTZID)
+            self.mTZOffset = tz.timeZoneSecondsOffset(self)
+        return self.mTZOffset
 
-    def timeZoneDescriptor( self ):
+
+    def timeZoneDescriptor(self):
         tz = PyCalendarTimezone(utc=self.mTZUTC, tzid=self.mTZID)
-        return tz.timeZoneDescriptor( self )
+        return tz.timeZoneDescriptor(self)
 
-    def changed( self ):
+
+    def changed(self):
         self.mPosixTimeCached = False
+        self.mTZOffset = None
 
-    def daysSince1970( self ):
-        # Add days betweenn 1970 and current year (ignoring leap days)
-        result = ( self.mYear - 1970 ) * 365
 
+    def daysSince1970(self):
+        # Add days between 1970 and current year (ignoring leap days)
+        result = (self.mYear - 1970) * 365
+
         # Add leap days between years
-        result += utils.leapDaysSince1970( self.mYear - 1970 )
+        result += utils.leapDaysSince1970(self.mYear - 1970)
 
         # Add days in current year up to current month (includes leap day for
         # current year as needed)
-        result += utils.daysUptoMonth( self.mMonth, self.mYear )
+        result += utils.daysUptoMonth(self.mMonth, self.mYear)
 
         # Add days in month
         result += self.mDay - 1

Modified: PyCalendar/trunk/src/pycalendar/datetimevalue.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/datetimevalue.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/datetimevalue.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,33 +14,42 @@
 #    limitations under the License.
 ##
 
-from datetime import PyCalendarDateTime
-from value import PyCalendarValue
+from pycalendar import xmldefs
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.value import PyCalendarValue
 
 class PyCalendarDateTimeValue(PyCalendarValue):
 
-    def __init__(self, value = None, copyit = None):
-        if value:
-            self.mValue = value
-        elif copyit:
-            self.mValue = PyCalendarDateTime(copyit=copyit.mValue)
-        else:
-            self.mValue = PyCalendarDateTime()
+    def __init__(self, value=None):
+        self.mValue = value if value is not None else PyCalendarDateTime()
 
+
+    def duplicate(self):
+        return PyCalendarDateTimeValue(self.mValue.duplicate())
+
+
     def getType(self):
         return  (PyCalendarValue.VALUETYPE_DATETIME, PyCalendarValue.VALUETYPE_DATE)[self.mValue.isDateOnly()]
 
-    def parse(self, data):
-        self.mValue.parse(data)
 
+    def parse(self, data, fullISO=False):
+        self.mValue.parse(data, fullISO)
+
+
     def generate(self, os):
         self.mValue.generate(os)
 
+
+    def writeXML(self, node, namespace):
+        self.mValue.writeXML(node, namespace)
+
+
     def getValue(self):
         return self.mValue
 
+
     def setValue(self, value):
         self.mValue = value
 
-PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_DATE, PyCalendarDateTimeValue)
-PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_DATETIME, PyCalendarDateTimeValue)
+PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_DATE, PyCalendarDateTimeValue, xmldefs.value_date)
+PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_DATETIME, PyCalendarDateTimeValue, xmldefs.value_date_time)

Modified: PyCalendar/trunk/src/pycalendar/definitions.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/definitions.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/definitions.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,32 +14,21 @@
 #    limitations under the License.
 ##
 
-#     2445 Component Header/Footer
+#     5545 Components
 
-cICalComponent_BEGINVCALENDAR = "BEGIN:VCALENDAR"
-cICalComponent_ENDVCALENDAR = "END:VCALENDAR"
-cICalComponent_BEGINVEVENT = "BEGIN:VEVENT"
-cICalComponent_ENDVEVENT = "END:VEVENT"
-cICalComponent_BEGINVTODO = "BEGIN:VTODO"
-cICalComponent_ENDVTODO = "END:VTODO"
-cICalComponent_BEGINVJOURNAL = "BEGIN:VJOURNAL"
-cICalComponent_ENDVJOURNAL = "END:VJOURNAL"
-cICalComponent_BEGINVFREEBUSY = "BEGIN:VFREEBUSY"
-cICalComponent_ENDVFREEBUSY = "END:VFREEBUSY"
-cICalComponent_BEGINVTIMEZONE = "BEGIN:VTIMEZONE"
-cICalComponent_ENDVTIMEZONE = "END:VTIMEZONE"
-cICalComponent_BEGINVALARM = "BEGIN:VALARM"
-cICalComponent_ENDVALARM = "END:VALARM"
+cICalComponent_VCALENDAR = "VCALENDAR"
+cICalComponent_VEVENT = "VEVENT"
+cICalComponent_VTODO = "VTODO"
+cICalComponent_VJOURNAL = "VJOURNAL"
+cICalComponent_VFREEBUSY = "VFREEBUSY"
+cICalComponent_VTIMEZONE = "VTIMEZONE"
+cICalComponent_VALARM = "VALARM"
+cICalComponent_STANDARD = "STANDARD"
+cICalComponent_DAYLIGHT = "DAYLIGHT"
 
-#     Pseudo components
-cICalComponent_BEGINSTANDARD = "BEGIN:STANDARD"
-cICalComponent_ENDSTANDARD = "END:STANDARD"
-cICalComponent_BEGINDAYLIGHT = "BEGIN:DAYLIGHT"
-cICalComponent_ENDDAYLIGHT = "END:DAYLIGHT"
+#     5545 Calendar Property Attributes
 
-#     2445 Calendar Property Atrributes
-
-#     2445 Section 4.2
+#     5545 Section 3.2
 cICalAttribute_ALTREP = "ALTREP"
 cICalAttribute_CN = "CN"
 cICalAttribute_CUTYPE = "CUTYPE"
@@ -63,13 +52,13 @@
 cICalAttribute_TZID = "TZID"
 cICalAttribute_VALUE = "VALUE"
 
-#     2445 Section 4.2.9
+#     5545 Section 3.2.9
 cICalAttribute_FBTYPE_FREE = "FREE"
 cICalAttribute_FBTYPE_BUSY = "BUSY"
 cICalAttribute_FBTYPE_BUSYUNAVAILABLE = "BUSY-UNAVAILABLE"
 cICalAttribute_FBTYPE_BUSYTENTATIVE = "BUSY-TENTATIVE"
 
-#     2445 Section 4.2.12
+#     5545 Section 3.2.12
 ePartStat_NeedsAction = 0
 ePartStat_Accepted = 1
 ePartStat_Declined = 2
@@ -86,26 +75,26 @@
 cICalAttribute_PARTSTAT_COMPLETED = "COMPLETE"
 cICalAttribute_PARTSTAT_INPROCESS = "IN-PROCESS"
 
-#     2445 Section 4.2.13
+#     5545 Section 3.2.13
 cICalAttribute_RANGE_THISANDFUTURE = "THISANDFUTURE"
-cICalAttribute_RANGE_THISANDPRIOR = "THISANDPRIOR"
+cICalAttribute_RANGE_THISANDPRIOR = "THISANDPRIOR"      # 2445 only
 
-#     2445 Section 4.2.14
+#     5545 Section 3.2.14
 cICalAttribute_RELATED_START = "START"
 cICalAttribute_RELATED_END = "END"
 
-#     2445 Section 4.2.16
+#     5545 Section 3.2.16
 ePartRole_Chair = 0
 ePartRole_Required = 1
 ePartRole_Optional = 2
 ePartRole_Non = 3
 
-cICalAttribute_ROLE_CHAIR    = "CHAIR"
+cICalAttribute_ROLE_CHAIR = "CHAIR"
 cICalAttribute_ROLE_REQ_PART = "REQ-PARTICIPANT"
 cICalAttribute_ROLE_OPT_PART = "OPT-PARTICIPANT"
 cICalAttribute_ROLE_NON_PART = "NON-PARTICIPANT"
 
-#     2445 Section 4.2.3
+#     5545 Section 3.2.3
 eCutype_Individual = 0
 eCutype_Group = 1
 eCutype_Resource = 2
@@ -113,14 +102,14 @@
 eCutype_Unknown = 4
 
 cICalAttribute_CUTYPE_INDIVIDUAL = "INDIVIDUAL"
-cICalAttribute_CUTYPE_GROUP      = "GROUP"
-cICalAttribute_CUTYPE_RESOURCE   = "RESOURCE"
-cICalAttribute_CUTYPE_ROOM       = "ROOM"
-cICalAttribute_CUTYPE_UNKNOWN    = "UNKNOWN"
+cICalAttribute_CUTYPE_GROUP = "GROUP"
+cICalAttribute_CUTYPE_RESOURCE = "RESOURCE"
+cICalAttribute_CUTYPE_ROOM = "ROOM"
+cICalAttribute_CUTYPE_UNKNOWN = "UNKNOWN"
 
-#     2445 Value types
+#     5545 Value types
 
-#     2445 Section 4.3
+#     5545 Section 3.3
 cICalValue_BINARY = "BINARY"
 cICalValue_BOOLEAN = "BOOLEAN"
 cICalValue_CAL_ADDRESS = "CAL-ADDRESS"
@@ -136,9 +125,9 @@
 cICalValue_URI = "URI"
 cICalValue_UTC_OFFSET = "UTC-OFFSET"
 
-#     2445 Calendar Properties
+#     5545 Calendar Properties
 
-#     2445 Section  4.7
+#     5545 Section  3.7
 
 cICalProperty_CALSCALE = "CALSCALE"
 cICalProperty_METHOD = "METHOD"
@@ -148,10 +137,11 @@
 #     Apple Extensions
 cICalProperty_XWRCALNAME = "X-WR-CALNAME"
 cICalProperty_XWRCALDESC = "X-WR-CALDESC"
+cICalProperty_XWRALARMUID = "X-WR-ALARMUID"
 
-#     2445 Componenty Property names
+#     5545 Component Property names
 
-#     2445 Section 4.8.1
+#     5545 Section 3.8.1
 cICalProperty_ATTACH = "ATTACH"
 cICalProperty_CATEGORIES = "CATEGORIES"
 cICalProperty_CLASS = "CLASS"
@@ -165,7 +155,7 @@
 cICalProperty_STATUS = "STATUS"
 cICalProperty_SUMMARY = "SUMMARY"
 
-#     2445 Section 4.8.2
+#     5545 Section 3.8.2
 cICalProperty_COMPLETED = "COMPLETED"
 cICalProperty_DTEND = "DTEND"
 cICalProperty_DUE = "DUE"
@@ -176,14 +166,14 @@
 cICalProperty_OPAQUE = "OPAQUE"
 cICalProperty_TRANSPARENT = "TRANSPARENT"
 
-#     2445 Section 4.8.3
+#     5545 Section 3.8.3
 cICalProperty_TZID = "TZID"
 cICalProperty_TZNAME = "TZNAME"
 cICalProperty_TZOFFSETFROM = "TZOFFSETFROM"
 cICalProperty_TZOFFSETTO = "TZOFFSETTO"
 cICalProperty_TZURL = "TZURL"
 
-#     2445 Section 4.8.4
+#     5545 Section 3.8.4
 cICalProperty_ATTENDEE = "ATTENDEE"
 cICalProperty_CONTACT = "CONTACT"
 cICalProperty_ORGANIZER = "ORGANIZER"
@@ -192,30 +182,30 @@
 cICalProperty_URL = "URL"
 cICalProperty_UID = "UID"
 
-#     2445 Section 4.8.5
+#     5545 Section 3.8.5
 cICalProperty_EXDATE = "EXDATE"
-cICalProperty_EXRULE = "EXRULE"
+cICalProperty_EXRULE = "EXRULE"     # 2445 only
 cICalProperty_RDATE = "RDATE"
 cICalProperty_RRULE = "RRULE"
 
-#     2445 Section 4.8.6
+#     5545 Section 3.8.6
 cICalProperty_ACTION = "ACTION"
 cICalProperty_REPEAT = "REPEAT"
 cICalProperty_TRIGGER = "TRIGGER"
 
-#     2445 Section 4.8.7
+#     5545 Section 3.8.7
 cICalProperty_CREATED = "CREATED"
 cICalProperty_DTSTAMP = "DTSTAMP"
 cICalProperty_LAST_MODIFIED = "LAST-MODIFIED"
 cICalProperty_SEQUENCE = "SEQUENCE"
 
-#     2445 Section 4.8.8.2
+#     5545 Section 3.8.8.3
 cICalProperty_REQUEST_STATUS = "REQUEST-STATUS"
 
 #     Enums
 #     Use ascending order for sensible sorting
 
-#     2445 Section 4.3.10
+#     5545 Section 3.3.10
 
 eRecurrence_SECONDLY = 0
 eRecurrence_MINUTELY = 1
@@ -225,7 +215,22 @@
 eRecurrence_MONTHLY = 5
 eRecurrence_YEARLY = 6
 
-cICalValue_RECUR_FREQ = "FREQ="
+eRecurrence_FREQ = 0
+eRecurrence_UNTIL = 1
+eRecurrence_COUNT = 2
+eRecurrence_INTERVAL = 3
+eRecurrence_BYSECOND = 4
+eRecurrence_BYMINUTE = 5
+eRecurrence_BYHOUR = 6
+eRecurrence_BYDAY = 7
+eRecurrence_BYMONTHDAY = 8
+eRecurrence_BYYEARDAY = 9
+eRecurrence_BYWEEKNO = 10
+eRecurrence_BYMONTH = 11
+eRecurrence_BYSETPOS = 12
+eRecurrence_WKST = 13
+
+cICalValue_RECUR_FREQ = "FREQ"
 cICalValue_RECUR_FREQ_LEN = 5
 
 cICalValue_RECUR_SECONDLY = "SECONDLY"
@@ -236,20 +241,20 @@
 cICalValue_RECUR_MONTHLY = "MONTHLY"
 cICalValue_RECUR_YEARLY = "YEARLY"
 
-cICalValue_RECUR_UNTIL = "UNTIL="
-cICalValue_RECUR_COUNT = "COUNT="
+cICalValue_RECUR_UNTIL = "UNTIL"
+cICalValue_RECUR_COUNT = "COUNT"
 
-cICalValue_RECUR_INTERVAL = "INTERVAL="
-cICalValue_RECUR_BYSECOND = "BYSECOND="
-cICalValue_RECUR_BYMINUTE = "BYMINUTE="
-cICalValue_RECUR_BYHOUR = "BYHOUR="
-cICalValue_RECUR_BYDAY = "BYDAY="
-cICalValue_RECUR_BYMONTHDAY = "BYMONTHDAY="
-cICalValue_RECUR_BYYEARDAY = "BYYEARDAY="
-cICalValue_RECUR_BYWEEKNO = "BYWEEKNO="
-cICalValue_RECUR_BYMONTH = "BYMONTH="
-cICalValue_RECUR_BYSETPOS = "BYSETPOS="
-cICalValue_RECUR_WKST = "WKST="
+cICalValue_RECUR_INTERVAL = "INTERVAL"
+cICalValue_RECUR_BYSECOND = "BYSECOND"
+cICalValue_RECUR_BYMINUTE = "BYMINUTE"
+cICalValue_RECUR_BYHOUR = "BYHOUR"
+cICalValue_RECUR_BYDAY = "BYDAY"
+cICalValue_RECUR_BYMONTHDAY = "BYMONTHDAY"
+cICalValue_RECUR_BYYEARDAY = "BYYEARDAY"
+cICalValue_RECUR_BYWEEKNO = "BYWEEKNO"
+cICalValue_RECUR_BYMONTH = "BYMONTH"
+cICalValue_RECUR_BYSETPOS = "BYSETPOS"
+cICalValue_RECUR_WKST = "WKST"
 
 eRecurrence_WEEKDAY_SU = 0
 eRecurrence_WEEKDAY_MO = 1
@@ -267,7 +272,7 @@
 cICalValue_RECUR_WEEKDAY_FR = "FR"
 cICalValue_RECUR_WEEKDAY_SA = "SA"
 
-#     2445 Section 4.8.1.11
+#     5545 Section 3.8.1.11
 eStatus_VEvent_None = 0
 eStatus_VEvent_Confirmed = 1
 eStatus_VEvent_Tentative = 2
@@ -293,7 +298,7 @@
 cICalProperty_STATUS_DRAFT = "DRAFT"
 cICalProperty_STATUS_FINAL = "FINAL"
 
-#     2445 Section 4.8.6.1
+#     5545 Section 3.8.6.1
 eAction_VAlarm_Audio = 0
 eAction_VAlarm_Display = 1
 eAction_VAlarm_Email = 2
@@ -305,11 +310,28 @@
 cICalProperty_ACTION_EMAIL = "EMAIL"
 cICalProperty_ACTION_PROCEDURE = "PROCEDURE"
 
+#     Extensions: draft-daboo-calendar-availability-02
 
+#     Section 3.1
+cICalComponent_VAVAILABILITY = "VAVAILABILITY"
+cICalComponent_AVAILABLE = "AVAILABLE"
+
+#     Section 3.2
+cICalProperty_BUSYTYPE = "BUSYTYPE"
+
+#     Extensions: draft-daboo-valarm-extensions-03
+
+#     Section 5
+eAction_VAlarm_URI = 5
+cICalProperty_ACTION_URI = "URI"
+
+#     Section 7.1
+cICalProperty_ACKNOWLEDGED = "ACKNOWLEDGED"
+
+eAction_VAlarm_None = 6
+cICalProperty_ACTION_NONE = "NONE"
+
 #     Mulberry extensions
-cICalProperty_X_PRIVATE_RURL = "X-MULBERRY-PRIVATE-RURL"
-cICalProperty_X_PRIVATE_ETAG = "X-MULBERRY-PRIVATE-ETAG"
-
 cICalProperty_ACTION_X_SPEAKTEXT = "X-MULBERRY-SPEAK-TEXT"
 cICalProperty_ALARM_X_LASTTRIGGER = "X-MULBERRY-LAST-TRIGGER"
 

Deleted: PyCalendar/trunk/src/pycalendar/dummyvalue.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/dummyvalue.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/dummyvalue.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,50 +0,0 @@
-##
-#    Copyright (c) 2007 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.
-##
-
-# iCalendar UTC Offset value
-
-from value import PyCalendarValue
-
-class PyCalendarDummyValue( PyCalendarValue ):
-
-    def __init__( self, type=None, copyit=None ):
-        if type is not None:
-            self.mType = type
-            self.mValue = ''
-        else:
-            self.mType = copyit.mType
-            self.mValue = copyit.mValue
-
-    def getType( self ):
-        return self.mType
-
-    def parse( self, data ):
-        self.mValue = data
-        
-    # os - StringIO object
-    def generate( self, os ):
-        try:
-            os.write( self.mValue )
-        except:
-            pass
-
-    def getValue( self ):
-        return self.mValue
-
-    def setValue( self, value ):
-        self.mValue = value
-
-PyCalendarValue.registerType("DUMMY", PyCalendarDummyValue)
\ No newline at end of file

Modified: PyCalendar/trunk/src/pycalendar/duration.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/duration.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/duration.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,11 +14,13 @@
 #    limitations under the License.
 ##
 
-import re
+from pycalendar.parser import ParserContext
+from pycalendar.stringutils import strtoul
+from pycalendar.valueutils import ValueMixin
 
-class PyCalendarDuration(object):
+class PyCalendarDuration(ValueMixin):
 
-    def __init__(self, duration=None, copyit=None):
+    def __init__(self, duration=None, weeks=0, days=0, hours=0, minutes=0, seconds=0):
         self.mForward = True
 
         self.mWeeks = 0
@@ -27,23 +29,47 @@
         self.mHours = 0
         self.mMinutes = 0
         self.mSeconds = 0
-        
-        if (duration is not None):
-            self.setDuration(duration)
-        elif (copyit is not None):
-            self.mForward = copyit.mForward
-    
-            self.mWeeks = copyit.mWeeks
-            self.mDays = copyit.mDays
-    
-            self.mHours = copyit.mHours
-            self.mMinutes = copyit.mMinutes
-            self.mSeconds = copyit.mSeconds
 
+        if duration is None:
+            duration = (((weeks * 7 + days) * 24 + hours) * 60 + minutes) * 60 + seconds
+        self.setDuration(duration)
+
+
+    def duplicate(self):
+        other = PyCalendarDuration(None)
+        other.mForward = self.mForward
+
+        other.mWeeks = self.mWeeks
+        other.mDays = self.mDays
+
+        other.mHours = self.mHours
+        other.mMinutes = self.mMinutes
+        other.mSeconds = self.mSeconds
+
+        return other
+
+
+    def __hash__(self):
+        return hash(self.getTotalSeconds())
+
+
+    def __eq__(self, comp):
+        return self.getTotalSeconds() == comp.getTotalSeconds()
+
+
+    def __gt__(self, comp):
+        return self.getTotalSeconds() > comp.getTotalSeconds()
+
+
+    def __lt__(self, comp):
+        return self.getTotalSeconds() < comp.getTotalSeconds()
+
+
     def getTotalSeconds(self):
         return [1, -1][not self.mForward] \
                 * (self.mSeconds + (self.mMinutes + (self.mHours + (self.mDays + (self.mWeeks * 7)) * 24) * 60) * 60)
 
+
     def setDuration(self, seconds):
         self.mForward = seconds >= 0
 
@@ -76,135 +102,143 @@
 
             self.mWeeks = 0
 
+
     def getForward(self):
         return self.mForward
 
+
     def getWeeks(self):
         return self.mWeeks
 
+
     def getDays(self):
         return self.mDays
 
+
     def getHours(self):
         return self.mHours
 
+
     def getMinutes(self):
         return self.mMinutes
 
+
     def getSeconds(self):
         return self.mSeconds
 
+
+    @classmethod
+    def parseText(cls, data):
+        dur = cls()
+        dur.parse(data)
+        return dur
+
+
     def parse(self, data):
         # parse format ([+]/-) "P" (dur-date / dur-time / dur-week)
 
-        regex = re.compile(r'([+/-]?)(P)([0-9]*)([DW]?)([T]?)([0-9]*)([HMS]?)([0-9]*)([MS]?)([0-9]*)([S]?)')
-        st = regex.search(data).groups()
-        st = [x for x in st if x != '']
-        st.reverse()
-        if len(st) == 0:
-            return
-        token = st.pop()
+        try:
+            offset = 0
+            maxoffset = len(data)
 
-        # Look for +/-
-        self.mForward = True
-        if token == "-":
-            self.mForward = False
-            if len(st) == 0:
-                return
-            token = st.pop()
-        elif token == "+":
-            if len(st) == 0:
-                return
-            token = st.pop()
+            # Look for +/-
+            self.mForward = True
+            if data[offset] in ('-', '+'):
+                self.mForward = data[offset] == '+'
+                offset += 1
 
-        # Must have a 'P'
-        if token != "P":
-            return
-        
-        # Look for time
-        if len(st) == 0:
-            return
-        token = st.pop()
-        if token != "T":
-            # Must have a number
-            num = int(token)
+            # Must have a 'P'
+            if data[offset] != "P":
+                raise ValueError
+            offset += 1
 
-            # Now look at character
-            if len(st) == 0:
-                return
-            token = st.pop()
-            if token == "W":
-                # Have a number of weeks
-                self.mWeeks = num
+            # Look for time
+            if data[offset] != "T":
+                # Must have a number
+                num, offset = strtoul(data, offset)
 
-                # There cannot bew anything else after this so just exit
-                return
-            elif token == "D":
-                # Have a number of days
-                self.mDays = num
+                # Now look at character
+                if data[offset] == "W":
+                    # Have a number of weeks
+                    self.mWeeks = num
+                    offset += 1
 
-                # Look for more data - exit if none
-                if len(st) == 0:
+                    # There cannot be anything else after this so just exit
+                    if offset != maxoffset:
+                        if ParserContext.INVALID_DURATION_VALUE != ParserContext.PARSER_RAISE:
+                            return
+                        raise ValueError
                     return
-                token = st.pop()
+                elif data[offset] == "D":
+                    # Have a number of days
+                    self.mDays = num
+                    offset += 1
 
-                # Look for time - exit if none
-                if token != "T":
+                    # Look for more data - exit if none
+                    if offset == maxoffset:
+                        return
+
+                    # Look for time - exit if none
+                    if data[offset] != "T":
+                        raise ValueError
+                else:
+                    # Error in format
+                    raise ValueError
+
+            # Have time
+            offset += 1
+
+            # Strictly speaking T must always be followed by time values, but some clients
+            # send T with no additional text
+            if offset == maxoffset:
+                if ParserContext.INVALID_DURATION_VALUE == ParserContext.PARSER_RAISE:
+                    raise ValueError
+                else:
                     return
-            else:
-                # Error in format
-                return
+            num, offset = strtoul(data, offset)
 
-        # Have time
-        if len(st) == 0:
-            return
-        token = st.pop()
-        num = int(token)
+            # Look for hour
+            if data[offset] == "H":
+                # Get hours
+                self.mHours = num
+                offset += 1
 
-        # Look for hour
-        if len(st) == 0:
-            return
-        token = st.pop()
-        if token == "H":
-            # Get hours
-            self.mHours = num
+                # Look for more data - exit if none
+                if offset == maxoffset:
+                    return
 
-            # Look for more data - exit if none
-            if len(st) == 0:
-                return
+                # Parse the next number
+                num, offset = strtoul(data, offset)
 
-            # Parse the next number
-            token = st.pop()
-            num = int(token)
+            # Look for minute
+            if data[offset] == "M":
+                # Get hours
+                self.mMinutes = num
+                offset += 1
 
-            # Parse the next char
-            if len(st) == 0:
-                return
-            token = st.pop()
+                # Look for more data - exit if none
+                if offset == maxoffset:
+                    return
 
-        # Look for minute
-        if token == "M":
-            # Get hours
-            self.mMinutes = num
+                # Parse the next number
+                num, offset = strtoul(data, offset)
 
-            # Look for more data - exit if none
-            if len(st) == 0:
-                return
+            # Look for seconds
+            if data[offset] == "S":
+                # Get hours
+                self.mSeconds = num
+                offset += 1
 
-            # Parse the next number
-            token = st.pop()
-            num = int(token)
+                # No more data - exit
+                if offset == maxoffset:
+                    return
 
-            # Parse the next char
-            if len(st) == 0:
-                return
-            token = st.pop()
+            raise ValueError
 
-        # Look for seconds
-        if token == "S":
-            # Get hours
-            self.mSeconds = num
+        except IndexError:
+            raise ValueError
 
+
     def generate(self, os):
         try:
             if not self.mForward and (self.mWeeks or self.mDays or self.mHours or self.mMinutes or self.mSeconds):
@@ -222,13 +256,17 @@
 
                     if self.mHours != 0:
                         os.write("%dH" % (self.mHours,))
-    
+
                     if (self.mMinutes != 0) or ((self.mHours != 0) and (self.mSeconds != 0)):
                         os.write("%dM" % (self.mMinutes,))
-    
+
                     if self.mSeconds != 0:
                         os.write("%dS" % (self.mSeconds,))
                 elif self.mDays == 0:
                     os.write("T0S")
         except:
             pass
+
+
+    def writeXML(self, node, namespace):
+        node.text = self.getText()

Modified: PyCalendar/trunk/src/pycalendar/durationvalue.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/durationvalue.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/durationvalue.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,32 +14,42 @@
 #    limitations under the License.
 ##
 
-from duration import PyCalendarDuration
-from value import PyCalendarValue
+from pycalendar import xmldefs
+from pycalendar.duration import PyCalendarDuration
+from pycalendar.value import PyCalendarValue
 
 class PyCalendarDurationValue(PyCalendarValue):
 
-    def __init__(self, value = None, copyit = None):
-        if value:
-            self.mValue = value
-        elif copyit:
-            self.mValue = PyCalendarDuration(duration=copyit.mValue)
-        else:
-            self.mValue = PyCalendarDuration()
+    def __init__(self, value=None):
+        self.mValue = value if value is not None else PyCalendarDuration()
 
+
+    def duplicate(self):
+        return PyCalendarDurationValue(self.mValue.duplicate())
+
+
     def getType(self):
         return PyCalendarValue.VALUETYPE_DURATION
 
+
     def parse(self, data):
         self.mValue.parse(data)
 
+
     def generate(self, os):
         self.mValue.generate(os)
 
+
+    def writeXML(self, node, namespace):
+        value = self.getXMLNode(node, namespace)
+        value.text = self.mValue.writeXML()
+
+
     def getValue(self):
         return self.mValue
 
-    def setValue(self,  value):
+
+    def setValue(self, value):
         self.mValue = value
 
-PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_DURATION, PyCalendarDurationValue)
+PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_DURATION, PyCalendarDurationValue, xmldefs.value_duration)

Copied: PyCalendar/trunk/src/pycalendar/exceptions.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/exceptions.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/exceptions.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/exceptions.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,44 @@
+##
+#    Copyright (c) 2011-2012 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.
+##
+
+class PyCalendarError(Exception):
+
+    def __init__(self, reason, data=""):
+        self.mReason = reason
+        self.mData = data
+
+
+
+class PyCalendarInvalidData(PyCalendarError):
+    pass
+
+
+
+class PyCalendarInvalidProperty(PyCalendarError):
+    pass
+
+
+
+class PyCalendarValidationError(PyCalendarError):
+    pass
+
+
+
+class PyCalendarNoTimezoneInDatabase(Exception):
+
+    def __init__(self, dbpath, tzid):
+        self.mTZDBpath = dbpath
+        self.mTZID = tzid

Modified: PyCalendar/trunk/src/pycalendar/freebusy.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/freebusy.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/freebusy.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,8 +14,6 @@
 #    limitations under the License.
 ##
 
-from period import PyCalendarPeriod
-
 class PyCalendarFreeBusy(object):
 
     FREE = 0
@@ -23,34 +21,36 @@
     BUSYUNAVAILABLE = 2
     BUSY = 3
 
-    def __init__( self, type = None, period = None, copyit = None ):
-        
-        if type and period:
-            self.mType = type
-            self.mPeriod = PyCalendarPeriod( copyit=period )
-        elif type:
-            self.mType = type
-        elif copyit:
-            self.mType = copyit.mType
-            self.mPeriod = PyCalendarPeriod( copyit=copyit.mPeriod )
-        else:
-            self.mType = PyCalendarFreeBusy.FREE
+    def __init__(self, type=None, period=None):
 
-    def setType( self, type ):
+        self.mType = type if type else PyCalendarFreeBusy.FREE
+        self.mPeriod = period.duplicate() if period is not None else None
+
+
+    def duplicate(self):
+        return PyCalendarFreeBusy(self.mType, self.mPeriod)
+
+
+    def setType(self, type):
         self.mType = type
 
-    def getType( self ):
+
+    def getType(self):
         return self.mType
 
-    def setPeriod( self, period ):
-        self.mPeriod = PyCalendarPeriod( copyit=period )
 
-    def getPeriod( self ):
+    def setPeriod(self, period):
+        self.mPeriod = period.duplicate()
+
+
+    def getPeriod(self):
         return self.mPeriod
 
-    def isPeriodOverlap( self, period ):
-        return self.mPeriod.isPeriodOverlap( period )
 
-    def resolveOverlaps( self, fb ):
+    def isPeriodOverlap(self, period):
+        return self.mPeriod.isPeriodOverlap(period)
+
+
+    def resolveOverlaps(self, fb):
         # TODO:
         pass

Deleted: PyCalendar/trunk/src/pycalendar/icalendar/__init__.py
===================================================================
--- PyCalendar/branches/server/src/pycalendar/icalendar/__init__.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/icalendar/__init__.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,15 +0,0 @@
-#
-#    Copyright (c) 2007-2012 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.
-##

Copied: PyCalendar/trunk/src/pycalendar/icalendar/__init__.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/icalendar/__init__.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/icalendar/__init__.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/icalendar/__init__.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,15 @@
+#
+#    Copyright (c) 2007-2012 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.
+##

Deleted: PyCalendar/trunk/src/pycalendar/icalendar/tests/__init__.py
===================================================================
--- PyCalendar/branches/server/src/pycalendar/icalendar/tests/__init__.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/icalendar/tests/__init__.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,15 +0,0 @@
-#
-#    Copyright (c) 2007-2012 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.
-##

Copied: PyCalendar/trunk/src/pycalendar/icalendar/tests/__init__.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/icalendar/tests/__init__.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/icalendar/tests/__init__.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/icalendar/tests/__init__.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,15 @@
+#
+#    Copyright (c) 2007-2012 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.
+##

Deleted: PyCalendar/trunk/src/pycalendar/icalendar/tests/test_validation.py
===================================================================
--- PyCalendar/branches/server/src/pycalendar/icalendar/tests/test_validation.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/icalendar/tests/test_validation.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,3189 +0,0 @@
-##
-#    Copyright (c) 2011-2012 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.calendar import PyCalendar
-from pycalendar.exceptions import PyCalendarValidationError
-import unittest
-
-class TestValidation(unittest.TestCase):
-
-    def test_basic(self):
-
-        data = (
-            (
-                "No problems",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set(),
-            ),
-            (
-                "No PRODID",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VCALENDAR] Missing or too many required property: PRODID",
-                )),
-            ),
-        )
-
-        for title, item, test_fixed, test_unfixed in data:
-            cal = PyCalendar.parseText(item)
-            fixed, unfixed = cal.validate(doFix=False)
-            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
-
-
-    def test_mode_no_fix_no_raise(self):
-
-        data = (
-            (
-                "OK",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set(),
-            ),
-            (
-                "Unfixable only",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VCALENDAR] Missing or too many required property: PRODID",
-                )),
-            ),
-            (
-                "Fixable only",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VEVENT] Properties must not both be present: DTEND, DURATION",
-                )),
-            ),
-            (
-                "Fixable and unfixable",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VCALENDAR] Missing or too many required property: PRODID",
-                    "[VEVENT] Properties must not both be present: DTEND, DURATION",
-                )),
-            ),
-        )
-
-        for title, test_old, test_new, test_fixed, test_unfixed in data:
-            cal = PyCalendar.parseText(test_old)
-            fixed, unfixed = cal.validate(doFix=False, doRaise=False)
-            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
-
-
-    def test_mode_fix_no_raise(self):
-
-        data = (
-            (
-                "OK",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set(),
-            ),
-            (
-                "Unfixable only",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VCALENDAR] Missing or too many required property: PRODID",
-                )),
-            ),
-            (
-                "Fixable only",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set((
-                    "[VEVENT] Properties must not both be present: DTEND, DURATION",
-                )),
-                set(),
-            ),
-            (
-                "Fixable and unfixable",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set((
-                    "[VEVENT] Properties must not both be present: DTEND, DURATION",
-                )),
-                set((
-                    "[VCALENDAR] Missing or too many required property: PRODID",
-                )),
-            ),
-        )
-
-        for title, test_old, test_new, test_fixed, test_unfixed in data:
-            cal = PyCalendar.parseText(test_old)
-            fixed, unfixed = cal.validate(doFix=True, doRaise=False)
-            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
-
-
-    def test_mode_no_fix_raise(self):
-
-        data = (
-            (
-                "OK",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set(),
-                False,
-            ),
-            (
-                "Unfixable only",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VCALENDAR] Missing or too many required property: PRODID",
-                )),
-                True,
-            ),
-            (
-                "Fixable only",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VEVENT] Properties must not both be present: DTEND, DURATION",
-                )),
-                True,
-            ),
-            (
-                "Fixable and unfixable",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VCALENDAR] Missing or too many required property: PRODID",
-                    "[VEVENT] Properties must not both be present: DTEND, DURATION",
-                )),
-                True,
-            ),
-        )
-
-        for title, test_old, test_new, test_fixed, test_unfixed, test_raises in data:
-            cal = PyCalendar.parseText(test_old)
-            if test_raises:
-                self.assertRaises(PyCalendarValidationError, cal.validate, doFix=False, doRaise=True)
-            else:
-                try:
-                    fixed, unfixed = cal.validate(doFix=False, doRaise=False)
-                except:
-                    self.fail(msg="Failed test: %s" % (title,))
-                self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
-                self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
-                self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
-
-
-    def test_mode_fix_raise(self):
-
-        data = (
-            (
-                "OK",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set(),
-                False,
-            ),
-            (
-                "Unfixable only",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VCALENDAR] Missing or too many required property: PRODID",
-                )),
-                True,
-            ),
-            (
-                "Fixable only",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set((
-                    "[VEVENT] Properties must not both be present: DTEND, DURATION",
-                )),
-                set(),
-                False,
-            ),
-            (
-                "Fixable and unfixable",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set((
-                    "[VEVENT] Properties must not both be present: DTEND, DURATION",
-                )),
-                set((
-                    "[VCALENDAR] Missing or too many required property: PRODID",
-                )),
-                True,
-            ),
-        )
-
-        for title, test_old, test_new, test_fixed, test_unfixed, test_raises in data:
-            cal = PyCalendar.parseText(test_old)
-            if test_raises:
-                self.assertRaises(PyCalendarValidationError, cal.validate, doFix=False, doRaise=True)
-            else:
-                try:
-                    fixed, unfixed = cal.validate(doFix=True, doRaise=False)
-                except:
-                    self.fail(msg="Failed test: %s" % (title,))
-                self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
-                self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
-                self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
-
-
-    def test_vevent(self):
-        data = (
-            (
-                "No problem",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set(),
-            ),
-            (
-                "Missing required",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VEVENT] Missing or too many required property: DTSTAMP",
-                )),
-            ),
-            (
-                "Too many",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3253
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-SUMMARY:New Year's Eve
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3253
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-SUMMARY:New Year's Eve
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VEVENT] Missing or too many required property: UID",
-                    "[VEVENT] Too many properties present: SUMMARY",
-                )),
-            ),
-            (
-                "PROP value",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VEVENT] Property value incorrect: DTSTAMP",
-                )),
-            ),
-            (
-                "No DTSTART without METHOD",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-METHOD:CANCEL
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTAMP:20020101T000000Z
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-METHOD:CANCEL
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTAMP:20020101T000000Z
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set(),
-            ),
-            (
-                "Combo fix",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set((
-                    "[VEVENT] Properties must not both be present: DTEND, DURATION",
-                )),
-                set(),
-            ),
-            (
-                "Mismatch UNTIL as DATE-TIME",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231T120000;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set((
-                    "[VEVENT] Value types must match: DTSTART, UNTIL",
-                )),
-                set(),
-            ),
-            (
-                "Mismatch UNTIL as DATE",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART:20020101T121212Z
-DURATION:P1D
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART:20020101T121212Z
-DURATION:P1D
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231T121212Z;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set((
-                    "[VEVENT] Value types must match: DTSTART, UNTIL",
-                )),
-                set(),
-            ),
-        )
-
-        for title, test_old, test_new, test_fixed, test_unfixed in data:
-            cal = PyCalendar.parseText(test_old)
-            fixed, unfixed = cal.validate(doFix=True)
-            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
-
-
-    def test_vfreebusy(self):
-        data = (
-            (
-                "No problem",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VFREEBUSY
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-END:VFREEBUSY
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VFREEBUSY
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-END:VFREEBUSY
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set(),
-            ),
-            (
-                "Missing required",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VFREEBUSY
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-END:VFREEBUSY
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VFREEBUSY
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-END:VFREEBUSY
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VFREEBUSY] Missing or too many required property: DTSTAMP",
-                )),
-            ),
-        )
-
-        for title, test_old, test_new, test_fixed, test_unfixed in data:
-            cal = PyCalendar.parseText(test_old)
-            fixed, unfixed = cal.validate(doFix=True)
-            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
-
-
-    def test_vjournal(self):
-        data = (
-            (
-                "No problem",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VJOURNAL
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTSTAMP:20020101T000000Z
-END:VJOURNAL
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VJOURNAL
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTSTAMP:20020101T000000Z
-END:VJOURNAL
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set(),
-            ),
-            (
-                "Missing required",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VJOURNAL
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-END:VJOURNAL
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VJOURNAL
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-END:VJOURNAL
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VJOURNAL] Missing or too many required property: DTSTAMP",
-                )),
-            ),
-        )
-
-        for title, test_old, test_new, test_fixed, test_unfixed in data:
-            cal = PyCalendar.parseText(test_old)
-            fixed, unfixed = cal.validate(doFix=True)
-            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
-
-
-    def test_vtimezone(self):
-        data = (
-            (
-                "No problem",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTIMEZONE
-TZID:America/New_York
-LAST-MODIFIED:20050809T050000Z
-BEGIN:DAYLIGHT
-DTSTART:19670430T020000
-RRULE:FREQ=YEARLY;UNTIL=19730429T070000Z;BYDAY=-1SU;BYMONTH=4
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:19671029T020000
-RRULE:FREQ=YEARLY;UNTIL=20061029T060000Z;BYDAY=-1SU;BYMONTH=10
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-END:VTIMEZONE
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTIMEZONE
-TZID:America/New_York
-LAST-MODIFIED:20050809T050000Z
-BEGIN:DAYLIGHT
-DTSTART:19670430T020000
-RRULE:FREQ=YEARLY;UNTIL=19730429T070000Z;BYDAY=-1SU;BYMONTH=4
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:19671029T020000
-RRULE:FREQ=YEARLY;UNTIL=20061029T060000Z;BYDAY=-1SU;BYMONTH=10
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-END:VTIMEZONE
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set(),
-            ),
-            (
-                "Missing required",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTIMEZONE
-LAST-MODIFIED:20050809T050000Z
-BEGIN:DAYLIGHT
-DTSTART:19670430T020000
-RRULE:FREQ=YEARLY;UNTIL=19730429T070000Z;BYDAY=-1SU;BYMONTH=4
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:19671029T020000
-RRULE:FREQ=YEARLY;UNTIL=20061029T060000Z;BYDAY=-1SU;BYMONTH=10
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-END:VTIMEZONE
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTIMEZONE
-LAST-MODIFIED:20050809T050000Z
-BEGIN:DAYLIGHT
-DTSTART:19670430T020000
-RRULE:FREQ=YEARLY;UNTIL=19730429T070000Z;BYDAY=-1SU;BYMONTH=4
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:19671029T020000
-RRULE:FREQ=YEARLY;UNTIL=20061029T060000Z;BYDAY=-1SU;BYMONTH=10
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-END:VTIMEZONE
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VTIMEZONE] Missing or too many required property: TZID",
-                )),
-            ),
-            (
-                "Missing components",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTIMEZONE
-TZID:America/New_York
-LAST-MODIFIED:20050809T050000Z
-END:VTIMEZONE
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTIMEZONE
-TZID:America/New_York
-LAST-MODIFIED:20050809T050000Z
-END:VTIMEZONE
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VTIMEZONE] At least one component must be present: STANDARD or DAYLIGHT",
-                )),
-            ),
-        )
-
-        for title, test_old, test_new, test_fixed, test_unfixed in data:
-            cal = PyCalendar.parseText(test_old)
-            fixed, unfixed = cal.validate(doFix=True)
-            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
-
-
-    def test_vtodo(self):
-        data = (
-            (
-                "No problem",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTODO
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DUE;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VTODO
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTODO
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DUE;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VTODO
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set(),
-            ),
-            (
-                "Missing required",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTODO
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DUE;VALUE=DATE:20020102
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VTODO
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTODO
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DUE;VALUE=DATE:20020102
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VTODO
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VTODO] Missing or too many required property: DTSTAMP",
-                )),
-            ),
-            (
-                "Too many",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTODO
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3253
-DTSTART;VALUE=DATE:20020101
-DUE;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-SUMMARY:New Year's Eve
-END:VTODO
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTODO
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3253
-DTSTART;VALUE=DATE:20020101
-DUE;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-SUMMARY:New Year's Eve
-END:VTODO
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VTODO] Missing or too many required property: UID",
-                    "[VTODO] Too many properties present: SUMMARY",
-                )),
-            ),
-            (
-                "PROP value",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTODO
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DUE;VALUE=DATE:20020102
-DTSTAMP:20020101T000000
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VTODO
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTODO
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DUE;VALUE=DATE:20020102
-DTSTAMP:20020101T000000
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VTODO
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VTODO] Property value incorrect: DTSTAMP",
-                )),
-            ),
-            (
-                "DURATION without DTSTART",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTODO
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DURATION:P1D
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VTODO
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTODO
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DURATION:P1D
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VTODO
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VTODO] Property must be present: DTSTART with DURATION",
-                )),
-            ),
-            (
-                "Combo fix",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTODO
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-DUE;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VTODO
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTODO
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DUE;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VTODO
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set((
-                    "[VTODO] Properties must not both be present: DUE, DURATION",
-                )),
-                set(),
-            ),
-        )
-
-        for title, test_old, test_new, test_fixed, test_unfixed in data:
-            cal = PyCalendar.parseText(test_old)
-            fixed, unfixed = cal.validate(doFix=True)
-            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
-
-
-    def test_vavailability(self):
-        data = (
-            (
-                "No problem",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VAVAILABILITY
-UID:20111005T133225Z-00001 at example.com
-DTSTAMP:20111005T133225Z
-ORGANIZER:mailto:bernard at example.com
-BEGIN:AVAILABLE
-UID:20111005T133225Z-00001-A at example.com
-DTSTART;TZID=America/Montreal:20111002T090000
-DTEND;TZID=America/Montreal:20111002T170000
-DTSTAMP:20111005T133225Z
-RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
-SUMMARY:Monday to Friday from 9:00 to 17:00
-END:AVAILABLE
-END:VAVAILABILITY
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VAVAILABILITY
-UID:20111005T133225Z-00001 at example.com
-DTSTAMP:20111005T133225Z
-ORGANIZER:mailto:bernard at example.com
-BEGIN:AVAILABLE
-UID:20111005T133225Z-00001-A at example.com
-DTSTART;TZID=America/Montreal:20111002T090000
-DTEND;TZID=America/Montreal:20111002T170000
-DTSTAMP:20111005T133225Z
-RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
-SUMMARY:Monday to Friday from 9:00 to 17:00
-END:AVAILABLE
-END:VAVAILABILITY
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set(),
-            ),
-            (
-                "Missing required",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VAVAILABILITY
-UID:20111005T133225Z-00001 at example.com
-ORGANIZER:mailto:bernard at example.com
-BEGIN:AVAILABLE
-UID:20111005T133225Z-00001-A at example.com
-DTSTART;TZID=America/Montreal:20111002T090000
-DTEND;TZID=America/Montreal:20111002T170000
-DTSTAMP:20111005T133225Z
-RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
-SUMMARY:Monday to Friday from 9:00 to 17:00
-END:AVAILABLE
-END:VAVAILABILITY
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VAVAILABILITY
-UID:20111005T133225Z-00001 at example.com
-ORGANIZER:mailto:bernard at example.com
-BEGIN:AVAILABLE
-UID:20111005T133225Z-00001-A at example.com
-DTSTART;TZID=America/Montreal:20111002T090000
-DTEND;TZID=America/Montreal:20111002T170000
-DTSTAMP:20111005T133225Z
-RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
-SUMMARY:Monday to Friday from 9:00 to 17:00
-END:AVAILABLE
-END:VAVAILABILITY
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VAVAILABILITY] Missing or too many required property: DTSTAMP",
-                )),
-            ),
-            (
-                "Too many",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VAVAILABILITY
-UID:20111005T133225Z-00001 at example.com
-UID:20111005T133225Z-00001 at example.com
-DTSTAMP:20111005T133225Z
-ORGANIZER:mailto:bernard at example.com
-ORGANIZER:mailto:bernard at example.com
-BEGIN:AVAILABLE
-UID:20111005T133225Z-00001-A at example.com
-DTSTART;TZID=America/Montreal:20111002T090000
-DTEND;TZID=America/Montreal:20111002T170000
-DTSTAMP:20111005T133225Z
-RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
-SUMMARY:Monday to Friday from 9:00 to 17:00
-END:AVAILABLE
-END:VAVAILABILITY
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VAVAILABILITY
-UID:20111005T133225Z-00001 at example.com
-UID:20111005T133225Z-00001 at example.com
-DTSTAMP:20111005T133225Z
-ORGANIZER:mailto:bernard at example.com
-ORGANIZER:mailto:bernard at example.com
-BEGIN:AVAILABLE
-UID:20111005T133225Z-00001-A at example.com
-DTSTART;TZID=America/Montreal:20111002T090000
-DTEND;TZID=America/Montreal:20111002T170000
-DTSTAMP:20111005T133225Z
-RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
-SUMMARY:Monday to Friday from 9:00 to 17:00
-END:AVAILABLE
-END:VAVAILABILITY
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VAVAILABILITY] Missing or too many required property: UID",
-                    "[VAVAILABILITY] Too many properties present: ORGANIZER",
-                )),
-            ),
-            (
-                "Combo fix",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VAVAILABILITY
-UID:20111005T133225Z-00001 at example.com
-DTSTART;TZID=America/Montreal:20111002T090000
-DURATION:P1D
-DTEND;TZID=America/Montreal:20111002T170000
-DTSTAMP:20111005T133225Z
-ORGANIZER:mailto:bernard at example.com
-BEGIN:AVAILABLE
-UID:20111005T133225Z-00001-A at example.com
-DTSTART;TZID=America/Montreal:20111002T090000
-DTEND;TZID=America/Montreal:20111002T170000
-DTSTAMP:20111005T133225Z
-RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
-SUMMARY:Monday to Friday from 9:00 to 17:00
-END:AVAILABLE
-END:VAVAILABILITY
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VAVAILABILITY
-UID:20111005T133225Z-00001 at example.com
-DTSTART;TZID=America/Montreal:20111002T090000
-DURATION:P1D
-DTSTAMP:20111005T133225Z
-ORGANIZER:mailto:bernard at example.com
-BEGIN:AVAILABLE
-UID:20111005T133225Z-00001-A at example.com
-DTSTART;TZID=America/Montreal:20111002T090000
-DTEND;TZID=America/Montreal:20111002T170000
-DTSTAMP:20111005T133225Z
-RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
-SUMMARY:Monday to Friday from 9:00 to 17:00
-END:AVAILABLE
-END:VAVAILABILITY
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set((
-                    "[VAVAILABILITY] Properties must not both be present: DTEND, DURATION",
-                )),
-                set(),
-            ),
-        )
-
-        for title, test_old, test_new, test_fixed, test_unfixed in data:
-            cal = PyCalendar.parseText(test_old)
-            fixed, unfixed = cal.validate(doFix=True)
-            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
-
-
-    def test_available(self):
-        data = (
-            (
-                "No problem",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VAVAILABILITY
-UID:20111005T133225Z-00001 at example.com
-DTSTAMP:20111005T133225Z
-ORGANIZER:mailto:bernard at example.com
-BEGIN:AVAILABLE
-UID:20111005T133225Z-00001-A at example.com
-DTSTART;TZID=America/Montreal:20111002T090000
-DTEND;TZID=America/Montreal:20111002T170000
-DTSTAMP:20111005T133225Z
-RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
-SUMMARY:Monday to Friday from 9:00 to 17:00
-END:AVAILABLE
-END:VAVAILABILITY
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VAVAILABILITY
-UID:20111005T133225Z-00001 at example.com
-DTSTAMP:20111005T133225Z
-ORGANIZER:mailto:bernard at example.com
-BEGIN:AVAILABLE
-UID:20111005T133225Z-00001-A at example.com
-DTSTART;TZID=America/Montreal:20111002T090000
-DTEND;TZID=America/Montreal:20111002T170000
-DTSTAMP:20111005T133225Z
-RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
-SUMMARY:Monday to Friday from 9:00 to 17:00
-END:AVAILABLE
-END:VAVAILABILITY
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set(),
-            ),
-            (
-                "Missing required",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VAVAILABILITY
-UID:20111005T133225Z-00001 at example.com
-DTSTAMP:20111005T133225Z
-ORGANIZER:mailto:bernard at example.com
-BEGIN:AVAILABLE
-UID:20111005T133225Z-00001-A at example.com
-DTSTART;TZID=America/Montreal:20111002T090000
-DTEND;TZID=America/Montreal:20111002T170000
-RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
-SUMMARY:Monday to Friday from 9:00 to 17:00
-END:AVAILABLE
-END:VAVAILABILITY
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VAVAILABILITY
-UID:20111005T133225Z-00001 at example.com
-DTSTAMP:20111005T133225Z
-ORGANIZER:mailto:bernard at example.com
-BEGIN:AVAILABLE
-UID:20111005T133225Z-00001-A at example.com
-DTSTART;TZID=America/Montreal:20111002T090000
-DTEND;TZID=America/Montreal:20111002T170000
-RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
-SUMMARY:Monday to Friday from 9:00 to 17:00
-END:AVAILABLE
-END:VAVAILABILITY
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[AVAILABLE] Missing or too many required property: DTSTAMP",
-                )),
-            ),
-            (
-                "Too many",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VAVAILABILITY
-UID:20111005T133225Z-00001 at example.com
-DTSTAMP:20111005T133225Z
-ORGANIZER:mailto:bernard at example.com
-BEGIN:AVAILABLE
-UID:20111005T133225Z-00001-A at example.com
-UID:20111005T133225Z-00001-A at example.com
-DTSTART;TZID=America/Montreal:20111002T090000
-DTEND;TZID=America/Montreal:20111002T170000
-DTSTAMP:20111005T133225Z
-RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
-SUMMARY:Monday to Friday from 9:00 to 17:00
-SUMMARY:Monday to Friday from 9 am to 5 pm
-END:AVAILABLE
-END:VAVAILABILITY
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VAVAILABILITY
-UID:20111005T133225Z-00001 at example.com
-DTSTAMP:20111005T133225Z
-ORGANIZER:mailto:bernard at example.com
-BEGIN:AVAILABLE
-UID:20111005T133225Z-00001-A at example.com
-UID:20111005T133225Z-00001-A at example.com
-DTSTART;TZID=America/Montreal:20111002T090000
-DTEND;TZID=America/Montreal:20111002T170000
-DTSTAMP:20111005T133225Z
-RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
-SUMMARY:Monday to Friday from 9:00 to 17:00
-SUMMARY:Monday to Friday from 9 am to 5 pm
-END:AVAILABLE
-END:VAVAILABILITY
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[AVAILABLE] Missing or too many required property: UID",
-                    "[AVAILABLE] Too many properties present: SUMMARY",
-                )),
-            ),
-            (
-                "Combo fix",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VAVAILABILITY
-UID:20111005T133225Z-00001 at example.com
-DTSTAMP:20111005T133225Z
-ORGANIZER:mailto:bernard at example.com
-BEGIN:AVAILABLE
-UID:20111005T133225Z-00001-A at example.com
-DTSTART;TZID=America/Montreal:20111002T090000
-DURATION:P1D
-DTEND;TZID=America/Montreal:20111002T170000
-DTSTAMP:20111005T133225Z
-RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
-SUMMARY:Monday to Friday from 9:00 to 17:00
-END:AVAILABLE
-END:VAVAILABILITY
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VAVAILABILITY
-UID:20111005T133225Z-00001 at example.com
-DTSTAMP:20111005T133225Z
-ORGANIZER:mailto:bernard at example.com
-BEGIN:AVAILABLE
-UID:20111005T133225Z-00001-A at example.com
-DTSTART;TZID=America/Montreal:20111002T090000
-DURATION:P1D
-DTSTAMP:20111005T133225Z
-RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
-SUMMARY:Monday to Friday from 9:00 to 17:00
-END:AVAILABLE
-END:VAVAILABILITY
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set((
-                    "[AVAILABLE] Properties must not both be present: DTEND, DURATION",
-                )),
-                set(),
-            ),
-        )
-
-        for title, test_old, test_new, test_fixed, test_unfixed in data:
-            cal = PyCalendar.parseText(test_old)
-            fixed, unfixed = cal.validate(doFix=True)
-            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
-
-
-    def test_valarm(self):
-        data = (
-            (
-                "No problem",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Reminder
-TRIGGER:-PT15M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Reminder
-TRIGGER:-PT15M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set(),
-            ),
-            (
-                "Missing required",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Reminder
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Reminder
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VALARM] Missing or too many required property: TRIGGER",
-                )),
-            ),
-            (
-                "Missing required with fix",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:DISPLAY
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set((
-                    "[VALARM] Missing required property: DESCRIPTION",
-                )),
-                set((
-                    "[VALARM] Missing or too many required property: TRIGGER",
-                )),
-            ),
-            (
-                "Too many",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:AUDIO
-ATTACH:http://example.com/audio/boink
-ATTACH:http://example.com/audio/quack
-TRIGGER:-PT15M
-TRIGGER:-PT30M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:AUDIO
-ATTACH:http://example.com/audio/boink
-ATTACH:http://example.com/audio/quack
-TRIGGER:-PT15M
-TRIGGER:-PT30M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VALARM] Missing or too many required property: TRIGGER",
-                    "[VALARM] Too many properties present: ATTACH",
-                )),
-            ),
-            (
-                "Too few",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:EMAIL
-DESCRIPTION:Reminder
-SUMMARY:Reminder
-TRIGGER:-PT15M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:EMAIL
-DESCRIPTION:Reminder
-SUMMARY:Reminder
-TRIGGER:-PT15M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VALARM] Missing required property: ATTENDEE",
-                )),
-            ),
-            (
-                "PROP value",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Reminder
-DURATION:-P1D
-REPEAT:-1
-TRIGGER:-PT15M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Reminder
-DURATION:-P1D
-REPEAT:-1
-TRIGGER:-PT15M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VALARM] Property value incorrect: REPEAT",
-                )),
-            ),
-            (
-                "DUARTION and REPEAT together",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-METHOD:CANCEL
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Reminder
-REPEAT:2
-TRIGGER:-PT15M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-METHOD:CANCEL
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Reminder
-REPEAT:2
-TRIGGER:-PT15M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VALARM] Properties must be present together: DURATION, REPEAT",
-                )),
-            ),
-            (
-                "Combo fix",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Reminder
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Reminder
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set((
-                    "[VEVENT] Properties must not both be present: DTEND, DURATION",
-                )),
-                set((
-                    "[VALARM] Missing or too many required property: TRIGGER",
-                )),
-            ),
-            (
-                "ACKNOWLEDGED OK",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACKNOWLEDGED:20020101T000000Z
-ACTION:DISPLAY
-DESCRIPTION:Reminder
-TRIGGER:-PT15M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACKNOWLEDGED:20020101T000000Z
-ACTION:DISPLAY
-DESCRIPTION:Reminder
-TRIGGER:-PT15M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set(),
-            ),
-            (
-                "ACKNOWLEDGED Bad value",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACKNOWLEDGED:20020101T000000
-ACTION:DISPLAY
-DESCRIPTION:Reminder
-TRIGGER:-PT15M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACKNOWLEDGED:20020101T000000
-ACTION:DISPLAY
-DESCRIPTION:Reminder
-TRIGGER:-PT15M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VALARM] Property value incorrect: ACKNOWLEDGED",
-                )),
-            ),
-            (
-                "ACKNOWLEDGED too many",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACKNOWLEDGED:20020101T000000Z
-ACKNOWLEDGED:20020102T000000Z
-ACTION:DISPLAY
-DESCRIPTION:Reminder
-TRIGGER:-PT15M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACKNOWLEDGED:20020101T000000Z
-ACKNOWLEDGED:20020102T000000Z
-ACTION:DISPLAY
-DESCRIPTION:Reminder
-TRIGGER:-PT15M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VALARM] Too many properties present: ACKNOWLEDGED",
-                )),
-            ),
-            (
-                "No problem ACTION=URI",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:URI
-TRIGGER:-PT15M
-URL:http://test.example.com
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:URI
-TRIGGER:-PT15M
-URL:http://test.example.com
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set(),
-            ),
-            (
-                "ACTION=URI missing URL",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:URI
-TRIGGER:-PT15M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:URI
-TRIGGER:-PT15M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VALARM] Missing or too many required property: URL",
-                )),
-            ),
-            (
-                "No problem ACTION=NONE",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:NONE
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:NONE
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set(),
-            ),
-            (
-                "No problem ACTION=NONE with TRIGGER",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:NONE
-TRIGGER;VALUE=DATE-TIME:19760401T005545Z
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-BEGIN:VALARM
-ACTION:NONE
-TRIGGER;VALUE=DATE-TIME:19760401T005545Z
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set(),
-            ),
-        )
-
-        for title, test_old, test_new, test_fixed, test_unfixed in data:
-            cal = PyCalendar.parseText(test_old)
-            fixed, unfixed = cal.validate(doFix=True)
-            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
-
-
-    def test_xcomponents(self):
-        data = (
-            (
-                "No problem #1",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-BEGIN:X-COMPONENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-SUMMARY:New Year's Day
-END:X-COMPONENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-BEGIN:X-COMPONENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-SUMMARY:New Year's Day
-END:X-COMPONENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set(),
-            ),
-            (
-                "No problem #2",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-BEGIN:X-COMPONENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTEND;VALUE=DATE:20020102
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-SUMMARY:New Year's Day
-END:X-COMPONENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-BEGIN:X-COMPONENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTEND;VALUE=DATE:20020102
-DTSTART;VALUE=DATE:20020101
-DURATION:P1D
-SUMMARY:New Year's Day
-END:X-COMPONENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set(),
-            ),
-            (
-                "Prop value",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-BEGIN:X-COMPONENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000
-DTSTART;VALUE=DATE:20020101
-SUMMARY:New Year's Day
-END:X-COMPONENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-END:VEVENT
-BEGIN:X-COMPONENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000
-DTSTART;VALUE=DATE:20020101
-SUMMARY:New Year's Day
-END:X-COMPONENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[X-COMPONENT] Property value incorrect: DTSTAMP",
-                )),
-            ),
-        )
-
-        for title, test_old, test_new, test_fixed, test_unfixed in data:
-            cal = PyCalendar.parseText(test_old)
-            fixed, unfixed = cal.validate(doFix=True)
-            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
-
-
-    def test_STATUS_fix(self):
-        """
-        Test calendarserver issue where multiple STATUS properties can be present in components.
-        Want to make sure we report those as an error or fix them.
-        """
-        data = (
-            (
-                "1.1 Two STATUS in VEVENT - fix",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-STATUS:CONFIRMED
-STATUS:CANCELLED
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-STATUS:CANCELLED
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                True,
-                set((
-                    "[VEVENT] Too many properties: STATUS",
-                )),
-                set(),
-            ),
-            (
-                "1.2 Two STATUS in VEVENT - no fix",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-STATUS:CONFIRMED
-STATUS:CANCELLED
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-STATUS:CONFIRMED
-STATUS:CANCELLED
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                False,
-                set(),
-                set((
-                    "[VEVENT] Too many properties: STATUS",
-                )),
-            ),
-            (
-                "1.3 Two STATUS in VEVENT, none CANCELLED - fix",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-STATUS:CONFIRMED
-STATUS:TENTATIVE
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-STATUS:CONFIRMED
-STATUS:TENTATIVE
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                True,
-                set(),
-                set((
-                    "[VEVENT] Too many properties: STATUS",
-                )),
-            ),
-            (
-                "1.4 Two STATUS in VEVENT, none CANCELLED - no fix",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-STATUS:CONFIRMED
-STATUS:TENTATIVE
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VEVENT
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-STATUS:CONFIRMED
-STATUS:TENTATIVE
-SUMMARY:New Year's Day
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                False,
-                set(),
-                set((
-                    "[VEVENT] Too many properties: STATUS",
-                )),
-            ),
-            (
-                "2.1 Two STATUS in VTODO - fix",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTODO
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DUE;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-STATUS:NEEDS-ACTION
-STATUS:CANCELLED
-SUMMARY:New Year's Day
-END:VTODO
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTODO
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DUE;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-STATUS:CANCELLED
-SUMMARY:New Year's Day
-END:VTODO
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                True,
-                set((
-                    "[VTODO] Too many properties: STATUS",
-                )),
-                set(),
-            ),
-            (
-                "2.2 Two STATUS in VTODO - no fix",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTODO
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DUE;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-STATUS:NEEDS-ACTION
-STATUS:CANCELLED
-SUMMARY:New Year's Day
-END:VTODO
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTODO
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DUE;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-STATUS:NEEDS-ACTION
-STATUS:CANCELLED
-SUMMARY:New Year's Day
-END:VTODO
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                False,
-                set(),
-                set((
-                    "[VTODO] Too many properties: STATUS",
-                )),
-            ),
-            (
-                "2.3 Two STATUS in VTODO, none CANCELLED - fix",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTODO
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DUE;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-STATUS:NEEDS-ACTION
-STATUS:COMPLETED
-SUMMARY:New Year's Day
-END:VTODO
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTODO
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DUE;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-STATUS:NEEDS-ACTION
-STATUS:COMPLETED
-SUMMARY:New Year's Day
-END:VTODO
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                True,
-                set(),
-                set((
-                    "[VTODO] Too many properties: STATUS",
-                )),
-            ),
-            (
-                "2.4 Two STATUS in VTODO, none CANCELLED - no fix",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTODO
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DUE;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-STATUS:NEEDS-ACTION
-STATUS:COMPLETED
-SUMMARY:New Year's Day
-END:VTODO
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VTODO
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DUE;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-STATUS:NEEDS-ACTION
-STATUS:COMPLETED
-SUMMARY:New Year's Day
-END:VTODO
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                False,
-                set(),
-                set((
-                    "[VTODO] Too many properties: STATUS",
-                )),
-            ),
-            (
-                "3.1 Two STATUS in VJOURNAL - fix",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VJOURNAL
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTSTAMP:20020101T000000Z
-STATUS:DRAFT
-STATUS:CANCELLED
-END:VJOURNAL
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VJOURNAL
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTSTAMP:20020101T000000Z
-STATUS:CANCELLED
-END:VJOURNAL
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                True,
-                set((
-                    "[VJOURNAL] Too many properties: STATUS",
-                )),
-                set(),
-            ),
-            (
-                "3.2 Two STATUS in VJOURNAL - no fix",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VJOURNAL
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTSTAMP:20020101T000000Z
-STATUS:DRAFT
-STATUS:CANCELLED
-END:VJOURNAL
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VJOURNAL
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTSTAMP:20020101T000000Z
-STATUS:DRAFT
-STATUS:CANCELLED
-END:VJOURNAL
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                False,
-                set(),
-                set((
-                    "[VJOURNAL] Too many properties: STATUS",
-                )),
-            ),
-            (
-                "3.3 Two STATUS in VJOURNAL, none CANCELLED - fix",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VJOURNAL
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTSTAMP:20020101T000000Z
-STATUS:DRAFT
-STATUS:FINAL
-END:VJOURNAL
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VJOURNAL
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTSTAMP:20020101T000000Z
-STATUS:DRAFT
-STATUS:FINAL
-END:VJOURNAL
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                True,
-                set(),
-                set((
-                    "[VJOURNAL] Too many properties: STATUS",
-                )),
-            ),
-            (
-                "3.4 Two STATUS in VJOURNAL, none CANCELLED - no fix",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VJOURNAL
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTSTAMP:20020101T000000Z
-STATUS:DRAFT
-STATUS:FINAL
-END:VJOURNAL
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-BEGIN:VJOURNAL
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-DTSTART;VALUE=DATE:20020101
-DTSTAMP:20020101T000000Z
-STATUS:DRAFT
-STATUS:FINAL
-END:VJOURNAL
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-                False,
-                set(),
-                set((
-                    "[VJOURNAL] Too many properties: STATUS",
-                )),
-            ),
-        )
-
-        for title, test_old, test_new, test_doFix, test_fixed, test_unfixed in data:
-            cal = PyCalendar.parseText(test_old)
-            fixed, unfixed = cal.validate(doFix=test_doFix)
-            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))

Copied: PyCalendar/trunk/src/pycalendar/icalendar/tests/test_validation.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/icalendar/tests/test_validation.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/icalendar/tests/test_validation.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/icalendar/tests/test_validation.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,3189 @@
+##
+#    Copyright (c) 2011-2012 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.calendar import PyCalendar
+from pycalendar.exceptions import PyCalendarValidationError
+import unittest
+
+class TestValidation(unittest.TestCase):
+
+    def test_basic(self):
+
+        data = (
+            (
+                "No problems",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set(),
+            ),
+            (
+                "No PRODID",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VCALENDAR] Missing or too many required property: PRODID",
+                )),
+            ),
+        )
+
+        for title, item, test_fixed, test_unfixed in data:
+            cal = PyCalendar.parseText(item)
+            fixed, unfixed = cal.validate(doFix=False)
+            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
+
+
+    def test_mode_no_fix_no_raise(self):
+
+        data = (
+            (
+                "OK",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set(),
+            ),
+            (
+                "Unfixable only",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VCALENDAR] Missing or too many required property: PRODID",
+                )),
+            ),
+            (
+                "Fixable only",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VEVENT] Properties must not both be present: DTEND, DURATION",
+                )),
+            ),
+            (
+                "Fixable and unfixable",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VCALENDAR] Missing or too many required property: PRODID",
+                    "[VEVENT] Properties must not both be present: DTEND, DURATION",
+                )),
+            ),
+        )
+
+        for title, test_old, test_new, test_fixed, test_unfixed in data:
+            cal = PyCalendar.parseText(test_old)
+            fixed, unfixed = cal.validate(doFix=False, doRaise=False)
+            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
+
+
+    def test_mode_fix_no_raise(self):
+
+        data = (
+            (
+                "OK",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set(),
+            ),
+            (
+                "Unfixable only",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VCALENDAR] Missing or too many required property: PRODID",
+                )),
+            ),
+            (
+                "Fixable only",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set((
+                    "[VEVENT] Properties must not both be present: DTEND, DURATION",
+                )),
+                set(),
+            ),
+            (
+                "Fixable and unfixable",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set((
+                    "[VEVENT] Properties must not both be present: DTEND, DURATION",
+                )),
+                set((
+                    "[VCALENDAR] Missing or too many required property: PRODID",
+                )),
+            ),
+        )
+
+        for title, test_old, test_new, test_fixed, test_unfixed in data:
+            cal = PyCalendar.parseText(test_old)
+            fixed, unfixed = cal.validate(doFix=True, doRaise=False)
+            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
+
+
+    def test_mode_no_fix_raise(self):
+
+        data = (
+            (
+                "OK",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set(),
+                False,
+            ),
+            (
+                "Unfixable only",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VCALENDAR] Missing or too many required property: PRODID",
+                )),
+                True,
+            ),
+            (
+                "Fixable only",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VEVENT] Properties must not both be present: DTEND, DURATION",
+                )),
+                True,
+            ),
+            (
+                "Fixable and unfixable",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VCALENDAR] Missing or too many required property: PRODID",
+                    "[VEVENT] Properties must not both be present: DTEND, DURATION",
+                )),
+                True,
+            ),
+        )
+
+        for title, test_old, test_new, test_fixed, test_unfixed, test_raises in data:
+            cal = PyCalendar.parseText(test_old)
+            if test_raises:
+                self.assertRaises(PyCalendarValidationError, cal.validate, doFix=False, doRaise=True)
+            else:
+                try:
+                    fixed, unfixed = cal.validate(doFix=False, doRaise=False)
+                except:
+                    self.fail(msg="Failed test: %s" % (title,))
+                self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
+                self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
+                self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
+
+
+    def test_mode_fix_raise(self):
+
+        data = (
+            (
+                "OK",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set(),
+                False,
+            ),
+            (
+                "Unfixable only",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VCALENDAR] Missing or too many required property: PRODID",
+                )),
+                True,
+            ),
+            (
+                "Fixable only",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set((
+                    "[VEVENT] Properties must not both be present: DTEND, DURATION",
+                )),
+                set(),
+                False,
+            ),
+            (
+                "Fixable and unfixable",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set((
+                    "[VEVENT] Properties must not both be present: DTEND, DURATION",
+                )),
+                set((
+                    "[VCALENDAR] Missing or too many required property: PRODID",
+                )),
+                True,
+            ),
+        )
+
+        for title, test_old, test_new, test_fixed, test_unfixed, test_raises in data:
+            cal = PyCalendar.parseText(test_old)
+            if test_raises:
+                self.assertRaises(PyCalendarValidationError, cal.validate, doFix=False, doRaise=True)
+            else:
+                try:
+                    fixed, unfixed = cal.validate(doFix=True, doRaise=False)
+                except:
+                    self.fail(msg="Failed test: %s" % (title,))
+                self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
+                self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
+                self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
+
+
+    def test_vevent(self):
+        data = (
+            (
+                "No problem",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set(),
+            ),
+            (
+                "Missing required",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VEVENT] Missing or too many required property: DTSTAMP",
+                )),
+            ),
+            (
+                "Too many",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3253
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+SUMMARY:New Year's Eve
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3253
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+SUMMARY:New Year's Eve
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VEVENT] Missing or too many required property: UID",
+                    "[VEVENT] Too many properties present: SUMMARY",
+                )),
+            ),
+            (
+                "PROP value",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VEVENT] Property value incorrect: DTSTAMP",
+                )),
+            ),
+            (
+                "No DTSTART without METHOD",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:CANCEL
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20020101T000000Z
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:CANCEL
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTAMP:20020101T000000Z
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set(),
+            ),
+            (
+                "Combo fix",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set((
+                    "[VEVENT] Properties must not both be present: DTEND, DURATION",
+                )),
+                set(),
+            ),
+            (
+                "Mismatch UNTIL as DATE-TIME",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231T120000;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set((
+                    "[VEVENT] Value types must match: DTSTART, UNTIL",
+                )),
+                set(),
+            ),
+            (
+                "Mismatch UNTIL as DATE",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART:20020101T121212Z
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART:20020101T121212Z
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231T121212Z;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set((
+                    "[VEVENT] Value types must match: DTSTART, UNTIL",
+                )),
+                set(),
+            ),
+        )
+
+        for title, test_old, test_new, test_fixed, test_unfixed in data:
+            cal = PyCalendar.parseText(test_old)
+            fixed, unfixed = cal.validate(doFix=True)
+            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
+
+
+    def test_vfreebusy(self):
+        data = (
+            (
+                "No problem",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VFREEBUSY
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+END:VFREEBUSY
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VFREEBUSY
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+END:VFREEBUSY
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set(),
+            ),
+            (
+                "Missing required",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VFREEBUSY
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+END:VFREEBUSY
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VFREEBUSY
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+END:VFREEBUSY
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VFREEBUSY] Missing or too many required property: DTSTAMP",
+                )),
+            ),
+        )
+
+        for title, test_old, test_new, test_fixed, test_unfixed in data:
+            cal = PyCalendar.parseText(test_old)
+            fixed, unfixed = cal.validate(doFix=True)
+            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
+
+
+    def test_vjournal(self):
+        data = (
+            (
+                "No problem",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VJOURNAL
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTSTAMP:20020101T000000Z
+END:VJOURNAL
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VJOURNAL
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTSTAMP:20020101T000000Z
+END:VJOURNAL
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set(),
+            ),
+            (
+                "Missing required",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VJOURNAL
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+END:VJOURNAL
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VJOURNAL
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+END:VJOURNAL
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VJOURNAL] Missing or too many required property: DTSTAMP",
+                )),
+            ),
+        )
+
+        for title, test_old, test_new, test_fixed, test_unfixed in data:
+            cal = PyCalendar.parseText(test_old)
+            fixed, unfixed = cal.validate(doFix=True)
+            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
+
+
+    def test_vtimezone(self):
+        data = (
+            (
+                "No problem",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTIMEZONE
+TZID:America/New_York
+LAST-MODIFIED:20050809T050000Z
+BEGIN:DAYLIGHT
+DTSTART:19670430T020000
+RRULE:FREQ=YEARLY;UNTIL=19730429T070000Z;BYDAY=-1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;UNTIL=20061029T060000Z;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTIMEZONE
+TZID:America/New_York
+LAST-MODIFIED:20050809T050000Z
+BEGIN:DAYLIGHT
+DTSTART:19670430T020000
+RRULE:FREQ=YEARLY;UNTIL=19730429T070000Z;BYDAY=-1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;UNTIL=20061029T060000Z;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set(),
+            ),
+            (
+                "Missing required",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTIMEZONE
+LAST-MODIFIED:20050809T050000Z
+BEGIN:DAYLIGHT
+DTSTART:19670430T020000
+RRULE:FREQ=YEARLY;UNTIL=19730429T070000Z;BYDAY=-1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;UNTIL=20061029T060000Z;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTIMEZONE
+LAST-MODIFIED:20050809T050000Z
+BEGIN:DAYLIGHT
+DTSTART:19670430T020000
+RRULE:FREQ=YEARLY;UNTIL=19730429T070000Z;BYDAY=-1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;UNTIL=20061029T060000Z;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VTIMEZONE] Missing or too many required property: TZID",
+                )),
+            ),
+            (
+                "Missing components",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTIMEZONE
+TZID:America/New_York
+LAST-MODIFIED:20050809T050000Z
+END:VTIMEZONE
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTIMEZONE
+TZID:America/New_York
+LAST-MODIFIED:20050809T050000Z
+END:VTIMEZONE
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VTIMEZONE] At least one component must be present: STANDARD or DAYLIGHT",
+                )),
+            ),
+        )
+
+        for title, test_old, test_new, test_fixed, test_unfixed in data:
+            cal = PyCalendar.parseText(test_old)
+            fixed, unfixed = cal.validate(doFix=True)
+            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
+
+
+    def test_vtodo(self):
+        data = (
+            (
+                "No problem",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTODO
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DUE;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VTODO
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTODO
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DUE;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VTODO
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set(),
+            ),
+            (
+                "Missing required",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTODO
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DUE;VALUE=DATE:20020102
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VTODO
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTODO
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DUE;VALUE=DATE:20020102
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VTODO
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VTODO] Missing or too many required property: DTSTAMP",
+                )),
+            ),
+            (
+                "Too many",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTODO
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3253
+DTSTART;VALUE=DATE:20020101
+DUE;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+SUMMARY:New Year's Eve
+END:VTODO
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTODO
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3253
+DTSTART;VALUE=DATE:20020101
+DUE;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+SUMMARY:New Year's Eve
+END:VTODO
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VTODO] Missing or too many required property: UID",
+                    "[VTODO] Too many properties present: SUMMARY",
+                )),
+            ),
+            (
+                "PROP value",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTODO
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DUE;VALUE=DATE:20020102
+DTSTAMP:20020101T000000
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VTODO
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTODO
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DUE;VALUE=DATE:20020102
+DTSTAMP:20020101T000000
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VTODO
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VTODO] Property value incorrect: DTSTAMP",
+                )),
+            ),
+            (
+                "DURATION without DTSTART",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTODO
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VTODO
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTODO
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VTODO
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VTODO] Property must be present: DTSTART with DURATION",
+                )),
+            ),
+            (
+                "Combo fix",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTODO
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+DUE;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VTODO
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTODO
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DUE;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VTODO
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set((
+                    "[VTODO] Properties must not both be present: DUE, DURATION",
+                )),
+                set(),
+            ),
+        )
+
+        for title, test_old, test_new, test_fixed, test_unfixed in data:
+            cal = PyCalendar.parseText(test_old)
+            fixed, unfixed = cal.validate(doFix=True)
+            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
+
+
+    def test_vavailability(self):
+        data = (
+            (
+                "No problem",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VAVAILABILITY
+UID:20111005T133225Z-00001 at example.com
+DTSTAMP:20111005T133225Z
+ORGANIZER:mailto:bernard at example.com
+BEGIN:AVAILABLE
+UID:20111005T133225Z-00001-A at example.com
+DTSTART;TZID=America/Montreal:20111002T090000
+DTEND;TZID=America/Montreal:20111002T170000
+DTSTAMP:20111005T133225Z
+RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
+SUMMARY:Monday to Friday from 9:00 to 17:00
+END:AVAILABLE
+END:VAVAILABILITY
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VAVAILABILITY
+UID:20111005T133225Z-00001 at example.com
+DTSTAMP:20111005T133225Z
+ORGANIZER:mailto:bernard at example.com
+BEGIN:AVAILABLE
+UID:20111005T133225Z-00001-A at example.com
+DTSTART;TZID=America/Montreal:20111002T090000
+DTEND;TZID=America/Montreal:20111002T170000
+DTSTAMP:20111005T133225Z
+RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
+SUMMARY:Monday to Friday from 9:00 to 17:00
+END:AVAILABLE
+END:VAVAILABILITY
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set(),
+            ),
+            (
+                "Missing required",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VAVAILABILITY
+UID:20111005T133225Z-00001 at example.com
+ORGANIZER:mailto:bernard at example.com
+BEGIN:AVAILABLE
+UID:20111005T133225Z-00001-A at example.com
+DTSTART;TZID=America/Montreal:20111002T090000
+DTEND;TZID=America/Montreal:20111002T170000
+DTSTAMP:20111005T133225Z
+RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
+SUMMARY:Monday to Friday from 9:00 to 17:00
+END:AVAILABLE
+END:VAVAILABILITY
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VAVAILABILITY
+UID:20111005T133225Z-00001 at example.com
+ORGANIZER:mailto:bernard at example.com
+BEGIN:AVAILABLE
+UID:20111005T133225Z-00001-A at example.com
+DTSTART;TZID=America/Montreal:20111002T090000
+DTEND;TZID=America/Montreal:20111002T170000
+DTSTAMP:20111005T133225Z
+RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
+SUMMARY:Monday to Friday from 9:00 to 17:00
+END:AVAILABLE
+END:VAVAILABILITY
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VAVAILABILITY] Missing or too many required property: DTSTAMP",
+                )),
+            ),
+            (
+                "Too many",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VAVAILABILITY
+UID:20111005T133225Z-00001 at example.com
+UID:20111005T133225Z-00001 at example.com
+DTSTAMP:20111005T133225Z
+ORGANIZER:mailto:bernard at example.com
+ORGANIZER:mailto:bernard at example.com
+BEGIN:AVAILABLE
+UID:20111005T133225Z-00001-A at example.com
+DTSTART;TZID=America/Montreal:20111002T090000
+DTEND;TZID=America/Montreal:20111002T170000
+DTSTAMP:20111005T133225Z
+RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
+SUMMARY:Monday to Friday from 9:00 to 17:00
+END:AVAILABLE
+END:VAVAILABILITY
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VAVAILABILITY
+UID:20111005T133225Z-00001 at example.com
+UID:20111005T133225Z-00001 at example.com
+DTSTAMP:20111005T133225Z
+ORGANIZER:mailto:bernard at example.com
+ORGANIZER:mailto:bernard at example.com
+BEGIN:AVAILABLE
+UID:20111005T133225Z-00001-A at example.com
+DTSTART;TZID=America/Montreal:20111002T090000
+DTEND;TZID=America/Montreal:20111002T170000
+DTSTAMP:20111005T133225Z
+RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
+SUMMARY:Monday to Friday from 9:00 to 17:00
+END:AVAILABLE
+END:VAVAILABILITY
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VAVAILABILITY] Missing or too many required property: UID",
+                    "[VAVAILABILITY] Too many properties present: ORGANIZER",
+                )),
+            ),
+            (
+                "Combo fix",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VAVAILABILITY
+UID:20111005T133225Z-00001 at example.com
+DTSTART;TZID=America/Montreal:20111002T090000
+DURATION:P1D
+DTEND;TZID=America/Montreal:20111002T170000
+DTSTAMP:20111005T133225Z
+ORGANIZER:mailto:bernard at example.com
+BEGIN:AVAILABLE
+UID:20111005T133225Z-00001-A at example.com
+DTSTART;TZID=America/Montreal:20111002T090000
+DTEND;TZID=America/Montreal:20111002T170000
+DTSTAMP:20111005T133225Z
+RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
+SUMMARY:Monday to Friday from 9:00 to 17:00
+END:AVAILABLE
+END:VAVAILABILITY
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VAVAILABILITY
+UID:20111005T133225Z-00001 at example.com
+DTSTART;TZID=America/Montreal:20111002T090000
+DURATION:P1D
+DTSTAMP:20111005T133225Z
+ORGANIZER:mailto:bernard at example.com
+BEGIN:AVAILABLE
+UID:20111005T133225Z-00001-A at example.com
+DTSTART;TZID=America/Montreal:20111002T090000
+DTEND;TZID=America/Montreal:20111002T170000
+DTSTAMP:20111005T133225Z
+RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
+SUMMARY:Monday to Friday from 9:00 to 17:00
+END:AVAILABLE
+END:VAVAILABILITY
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set((
+                    "[VAVAILABILITY] Properties must not both be present: DTEND, DURATION",
+                )),
+                set(),
+            ),
+        )
+
+        for title, test_old, test_new, test_fixed, test_unfixed in data:
+            cal = PyCalendar.parseText(test_old)
+            fixed, unfixed = cal.validate(doFix=True)
+            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
+
+
+    def test_available(self):
+        data = (
+            (
+                "No problem",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VAVAILABILITY
+UID:20111005T133225Z-00001 at example.com
+DTSTAMP:20111005T133225Z
+ORGANIZER:mailto:bernard at example.com
+BEGIN:AVAILABLE
+UID:20111005T133225Z-00001-A at example.com
+DTSTART;TZID=America/Montreal:20111002T090000
+DTEND;TZID=America/Montreal:20111002T170000
+DTSTAMP:20111005T133225Z
+RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
+SUMMARY:Monday to Friday from 9:00 to 17:00
+END:AVAILABLE
+END:VAVAILABILITY
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VAVAILABILITY
+UID:20111005T133225Z-00001 at example.com
+DTSTAMP:20111005T133225Z
+ORGANIZER:mailto:bernard at example.com
+BEGIN:AVAILABLE
+UID:20111005T133225Z-00001-A at example.com
+DTSTART;TZID=America/Montreal:20111002T090000
+DTEND;TZID=America/Montreal:20111002T170000
+DTSTAMP:20111005T133225Z
+RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
+SUMMARY:Monday to Friday from 9:00 to 17:00
+END:AVAILABLE
+END:VAVAILABILITY
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set(),
+            ),
+            (
+                "Missing required",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VAVAILABILITY
+UID:20111005T133225Z-00001 at example.com
+DTSTAMP:20111005T133225Z
+ORGANIZER:mailto:bernard at example.com
+BEGIN:AVAILABLE
+UID:20111005T133225Z-00001-A at example.com
+DTSTART;TZID=America/Montreal:20111002T090000
+DTEND;TZID=America/Montreal:20111002T170000
+RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
+SUMMARY:Monday to Friday from 9:00 to 17:00
+END:AVAILABLE
+END:VAVAILABILITY
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VAVAILABILITY
+UID:20111005T133225Z-00001 at example.com
+DTSTAMP:20111005T133225Z
+ORGANIZER:mailto:bernard at example.com
+BEGIN:AVAILABLE
+UID:20111005T133225Z-00001-A at example.com
+DTSTART;TZID=America/Montreal:20111002T090000
+DTEND;TZID=America/Montreal:20111002T170000
+RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
+SUMMARY:Monday to Friday from 9:00 to 17:00
+END:AVAILABLE
+END:VAVAILABILITY
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[AVAILABLE] Missing or too many required property: DTSTAMP",
+                )),
+            ),
+            (
+                "Too many",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VAVAILABILITY
+UID:20111005T133225Z-00001 at example.com
+DTSTAMP:20111005T133225Z
+ORGANIZER:mailto:bernard at example.com
+BEGIN:AVAILABLE
+UID:20111005T133225Z-00001-A at example.com
+UID:20111005T133225Z-00001-A at example.com
+DTSTART;TZID=America/Montreal:20111002T090000
+DTEND;TZID=America/Montreal:20111002T170000
+DTSTAMP:20111005T133225Z
+RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
+SUMMARY:Monday to Friday from 9:00 to 17:00
+SUMMARY:Monday to Friday from 9 am to 5 pm
+END:AVAILABLE
+END:VAVAILABILITY
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VAVAILABILITY
+UID:20111005T133225Z-00001 at example.com
+DTSTAMP:20111005T133225Z
+ORGANIZER:mailto:bernard at example.com
+BEGIN:AVAILABLE
+UID:20111005T133225Z-00001-A at example.com
+UID:20111005T133225Z-00001-A at example.com
+DTSTART;TZID=America/Montreal:20111002T090000
+DTEND;TZID=America/Montreal:20111002T170000
+DTSTAMP:20111005T133225Z
+RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
+SUMMARY:Monday to Friday from 9:00 to 17:00
+SUMMARY:Monday to Friday from 9 am to 5 pm
+END:AVAILABLE
+END:VAVAILABILITY
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[AVAILABLE] Missing or too many required property: UID",
+                    "[AVAILABLE] Too many properties present: SUMMARY",
+                )),
+            ),
+            (
+                "Combo fix",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VAVAILABILITY
+UID:20111005T133225Z-00001 at example.com
+DTSTAMP:20111005T133225Z
+ORGANIZER:mailto:bernard at example.com
+BEGIN:AVAILABLE
+UID:20111005T133225Z-00001-A at example.com
+DTSTART;TZID=America/Montreal:20111002T090000
+DURATION:P1D
+DTEND;TZID=America/Montreal:20111002T170000
+DTSTAMP:20111005T133225Z
+RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
+SUMMARY:Monday to Friday from 9:00 to 17:00
+END:AVAILABLE
+END:VAVAILABILITY
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VAVAILABILITY
+UID:20111005T133225Z-00001 at example.com
+DTSTAMP:20111005T133225Z
+ORGANIZER:mailto:bernard at example.com
+BEGIN:AVAILABLE
+UID:20111005T133225Z-00001-A at example.com
+DTSTART;TZID=America/Montreal:20111002T090000
+DURATION:P1D
+DTSTAMP:20111005T133225Z
+RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
+SUMMARY:Monday to Friday from 9:00 to 17:00
+END:AVAILABLE
+END:VAVAILABILITY
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set((
+                    "[AVAILABLE] Properties must not both be present: DTEND, DURATION",
+                )),
+                set(),
+            ),
+        )
+
+        for title, test_old, test_new, test_fixed, test_unfixed in data:
+            cal = PyCalendar.parseText(test_old)
+            fixed, unfixed = cal.validate(doFix=True)
+            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
+
+
+    def test_valarm(self):
+        data = (
+            (
+                "No problem",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Reminder
+TRIGGER:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Reminder
+TRIGGER:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set(),
+            ),
+            (
+                "Missing required",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Reminder
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Reminder
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VALARM] Missing or too many required property: TRIGGER",
+                )),
+            ),
+            (
+                "Missing required with fix",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:DISPLAY
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set((
+                    "[VALARM] Missing required property: DESCRIPTION",
+                )),
+                set((
+                    "[VALARM] Missing or too many required property: TRIGGER",
+                )),
+            ),
+            (
+                "Too many",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:AUDIO
+ATTACH:http://example.com/audio/boink
+ATTACH:http://example.com/audio/quack
+TRIGGER:-PT15M
+TRIGGER:-PT30M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:AUDIO
+ATTACH:http://example.com/audio/boink
+ATTACH:http://example.com/audio/quack
+TRIGGER:-PT15M
+TRIGGER:-PT30M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VALARM] Missing or too many required property: TRIGGER",
+                    "[VALARM] Too many properties present: ATTACH",
+                )),
+            ),
+            (
+                "Too few",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:EMAIL
+DESCRIPTION:Reminder
+SUMMARY:Reminder
+TRIGGER:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:EMAIL
+DESCRIPTION:Reminder
+SUMMARY:Reminder
+TRIGGER:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VALARM] Missing required property: ATTENDEE",
+                )),
+            ),
+            (
+                "PROP value",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Reminder
+DURATION:-P1D
+REPEAT:-1
+TRIGGER:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Reminder
+DURATION:-P1D
+REPEAT:-1
+TRIGGER:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VALARM] Property value incorrect: REPEAT",
+                )),
+            ),
+            (
+                "DUARTION and REPEAT together",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:CANCEL
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Reminder
+REPEAT:2
+TRIGGER:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:CANCEL
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Reminder
+REPEAT:2
+TRIGGER:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VALARM] Properties must be present together: DURATION, REPEAT",
+                )),
+            ),
+            (
+                "Combo fix",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Reminder
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Reminder
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set((
+                    "[VEVENT] Properties must not both be present: DTEND, DURATION",
+                )),
+                set((
+                    "[VALARM] Missing or too many required property: TRIGGER",
+                )),
+            ),
+            (
+                "ACKNOWLEDGED OK",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACKNOWLEDGED:20020101T000000Z
+ACTION:DISPLAY
+DESCRIPTION:Reminder
+TRIGGER:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACKNOWLEDGED:20020101T000000Z
+ACTION:DISPLAY
+DESCRIPTION:Reminder
+TRIGGER:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set(),
+            ),
+            (
+                "ACKNOWLEDGED Bad value",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACKNOWLEDGED:20020101T000000
+ACTION:DISPLAY
+DESCRIPTION:Reminder
+TRIGGER:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACKNOWLEDGED:20020101T000000
+ACTION:DISPLAY
+DESCRIPTION:Reminder
+TRIGGER:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VALARM] Property value incorrect: ACKNOWLEDGED",
+                )),
+            ),
+            (
+                "ACKNOWLEDGED too many",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACKNOWLEDGED:20020101T000000Z
+ACKNOWLEDGED:20020102T000000Z
+ACTION:DISPLAY
+DESCRIPTION:Reminder
+TRIGGER:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACKNOWLEDGED:20020101T000000Z
+ACKNOWLEDGED:20020102T000000Z
+ACTION:DISPLAY
+DESCRIPTION:Reminder
+TRIGGER:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VALARM] Too many properties present: ACKNOWLEDGED",
+                )),
+            ),
+            (
+                "No problem ACTION=URI",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:URI
+TRIGGER:-PT15M
+URL:http://test.example.com
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:URI
+TRIGGER:-PT15M
+URL:http://test.example.com
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set(),
+            ),
+            (
+                "ACTION=URI missing URL",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:URI
+TRIGGER:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:URI
+TRIGGER:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VALARM] Missing or too many required property: URL",
+                )),
+            ),
+            (
+                "No problem ACTION=NONE",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:NONE
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:NONE
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set(),
+            ),
+            (
+                "No problem ACTION=NONE with TRIGGER",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:NONE
+TRIGGER;VALUE=DATE-TIME:19760401T005545Z
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:NONE
+TRIGGER;VALUE=DATE-TIME:19760401T005545Z
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set(),
+            ),
+        )
+
+        for title, test_old, test_new, test_fixed, test_unfixed in data:
+            cal = PyCalendar.parseText(test_old)
+            fixed, unfixed = cal.validate(doFix=True)
+            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
+
+
+    def test_xcomponents(self):
+        data = (
+            (
+                "No problem #1",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+BEGIN:X-COMPONENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+SUMMARY:New Year's Day
+END:X-COMPONENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+BEGIN:X-COMPONENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+SUMMARY:New Year's Day
+END:X-COMPONENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set(),
+            ),
+            (
+                "No problem #2",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+BEGIN:X-COMPONENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTEND;VALUE=DATE:20020102
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+SUMMARY:New Year's Day
+END:X-COMPONENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+BEGIN:X-COMPONENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTEND;VALUE=DATE:20020102
+DTSTART;VALUE=DATE:20020101
+DURATION:P1D
+SUMMARY:New Year's Day
+END:X-COMPONENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set(),
+            ),
+            (
+                "Prop value",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+BEGIN:X-COMPONENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000
+DTSTART;VALUE=DATE:20020101
+SUMMARY:New Year's Day
+END:X-COMPONENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+BEGIN:X-COMPONENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000
+DTSTART;VALUE=DATE:20020101
+SUMMARY:New Year's Day
+END:X-COMPONENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[X-COMPONENT] Property value incorrect: DTSTAMP",
+                )),
+            ),
+        )
+
+        for title, test_old, test_new, test_fixed, test_unfixed in data:
+            cal = PyCalendar.parseText(test_old)
+            fixed, unfixed = cal.validate(doFix=True)
+            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
+
+
+    def test_STATUS_fix(self):
+        """
+        Test calendarserver issue where multiple STATUS properties can be present in components.
+        Want to make sure we report those as an error or fix them.
+        """
+        data = (
+            (
+                "1.1 Two STATUS in VEVENT - fix",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+STATUS:CONFIRMED
+STATUS:CANCELLED
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+STATUS:CANCELLED
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                True,
+                set((
+                    "[VEVENT] Too many properties: STATUS",
+                )),
+                set(),
+            ),
+            (
+                "1.2 Two STATUS in VEVENT - no fix",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+STATUS:CONFIRMED
+STATUS:CANCELLED
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+STATUS:CONFIRMED
+STATUS:CANCELLED
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                False,
+                set(),
+                set((
+                    "[VEVENT] Too many properties: STATUS",
+                )),
+            ),
+            (
+                "1.3 Two STATUS in VEVENT, none CANCELLED - fix",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+STATUS:CONFIRMED
+STATUS:TENTATIVE
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+STATUS:CONFIRMED
+STATUS:TENTATIVE
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                True,
+                set(),
+                set((
+                    "[VEVENT] Too many properties: STATUS",
+                )),
+            ),
+            (
+                "1.4 Two STATUS in VEVENT, none CANCELLED - no fix",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+STATUS:CONFIRMED
+STATUS:TENTATIVE
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+STATUS:CONFIRMED
+STATUS:TENTATIVE
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                False,
+                set(),
+                set((
+                    "[VEVENT] Too many properties: STATUS",
+                )),
+            ),
+            (
+                "2.1 Two STATUS in VTODO - fix",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTODO
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DUE;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+STATUS:NEEDS-ACTION
+STATUS:CANCELLED
+SUMMARY:New Year's Day
+END:VTODO
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTODO
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DUE;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+STATUS:CANCELLED
+SUMMARY:New Year's Day
+END:VTODO
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                True,
+                set((
+                    "[VTODO] Too many properties: STATUS",
+                )),
+                set(),
+            ),
+            (
+                "2.2 Two STATUS in VTODO - no fix",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTODO
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DUE;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+STATUS:NEEDS-ACTION
+STATUS:CANCELLED
+SUMMARY:New Year's Day
+END:VTODO
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTODO
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DUE;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+STATUS:NEEDS-ACTION
+STATUS:CANCELLED
+SUMMARY:New Year's Day
+END:VTODO
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                False,
+                set(),
+                set((
+                    "[VTODO] Too many properties: STATUS",
+                )),
+            ),
+            (
+                "2.3 Two STATUS in VTODO, none CANCELLED - fix",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTODO
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DUE;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+STATUS:NEEDS-ACTION
+STATUS:COMPLETED
+SUMMARY:New Year's Day
+END:VTODO
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTODO
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DUE;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+STATUS:NEEDS-ACTION
+STATUS:COMPLETED
+SUMMARY:New Year's Day
+END:VTODO
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                True,
+                set(),
+                set((
+                    "[VTODO] Too many properties: STATUS",
+                )),
+            ),
+            (
+                "2.4 Two STATUS in VTODO, none CANCELLED - no fix",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTODO
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DUE;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+STATUS:NEEDS-ACTION
+STATUS:COMPLETED
+SUMMARY:New Year's Day
+END:VTODO
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTODO
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DUE;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+STATUS:NEEDS-ACTION
+STATUS:COMPLETED
+SUMMARY:New Year's Day
+END:VTODO
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                False,
+                set(),
+                set((
+                    "[VTODO] Too many properties: STATUS",
+                )),
+            ),
+            (
+                "3.1 Two STATUS in VJOURNAL - fix",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VJOURNAL
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTSTAMP:20020101T000000Z
+STATUS:DRAFT
+STATUS:CANCELLED
+END:VJOURNAL
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VJOURNAL
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTSTAMP:20020101T000000Z
+STATUS:CANCELLED
+END:VJOURNAL
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                True,
+                set((
+                    "[VJOURNAL] Too many properties: STATUS",
+                )),
+                set(),
+            ),
+            (
+                "3.2 Two STATUS in VJOURNAL - no fix",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VJOURNAL
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTSTAMP:20020101T000000Z
+STATUS:DRAFT
+STATUS:CANCELLED
+END:VJOURNAL
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VJOURNAL
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTSTAMP:20020101T000000Z
+STATUS:DRAFT
+STATUS:CANCELLED
+END:VJOURNAL
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                False,
+                set(),
+                set((
+                    "[VJOURNAL] Too many properties: STATUS",
+                )),
+            ),
+            (
+                "3.3 Two STATUS in VJOURNAL, none CANCELLED - fix",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VJOURNAL
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTSTAMP:20020101T000000Z
+STATUS:DRAFT
+STATUS:FINAL
+END:VJOURNAL
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VJOURNAL
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTSTAMP:20020101T000000Z
+STATUS:DRAFT
+STATUS:FINAL
+END:VJOURNAL
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                True,
+                set(),
+                set((
+                    "[VJOURNAL] Too many properties: STATUS",
+                )),
+            ),
+            (
+                "3.4 Two STATUS in VJOURNAL, none CANCELLED - no fix",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VJOURNAL
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTSTAMP:20020101T000000Z
+STATUS:DRAFT
+STATUS:FINAL
+END:VJOURNAL
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VJOURNAL
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTSTAMP:20020101T000000Z
+STATUS:DRAFT
+STATUS:FINAL
+END:VJOURNAL
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                False,
+                set(),
+                set((
+                    "[VJOURNAL] Too many properties: STATUS",
+                )),
+            ),
+        )
+
+        for title, test_old, test_new, test_doFix, test_fixed, test_unfixed in data:
+            cal = PyCalendar.parseText(test_old)
+            fixed, unfixed = cal.validate(doFix=test_doFix)
+            self.assertEqual(str(cal), test_new, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))

Deleted: PyCalendar/trunk/src/pycalendar/icalendar/validation.py
===================================================================
--- PyCalendar/branches/server/src/pycalendar/icalendar/validation.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/icalendar/validation.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,37 +0,0 @@
-##
-#    Copyright (c) 2011-2012 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 import definitions
-from pycalendar.validation import partial, PropertyValueChecks
-
-ICALENDAR_VALUE_CHECKS = {
-    definitions.cICalProperty_CALSCALE: partial(PropertyValueChecks.stringValue, "GREGORIAN"),
-    definitions.cICalProperty_VERSION: partial(PropertyValueChecks.stringValue, "2.0"),
-
-    definitions.cICalProperty_PERCENT_COMPLETE: partial(PropertyValueChecks.numericRange, 0, 100),
-    definitions.cICalProperty_PRIORITY: partial(PropertyValueChecks.numericRange, 0, 9),
-
-    definitions.cICalProperty_COMPLETED: PropertyValueChecks.alwaysUTC,
-
-    definitions.cICalProperty_REPEAT: PropertyValueChecks.positiveIntegerOrZero,
-
-    definitions.cICalProperty_CREATED: PropertyValueChecks.alwaysUTC,
-    definitions.cICalProperty_DTSTAMP: PropertyValueChecks.alwaysUTC,
-    definitions.cICalProperty_LAST_MODIFIED: PropertyValueChecks.alwaysUTC,
-    definitions.cICalProperty_SEQUENCE: PropertyValueChecks.positiveIntegerOrZero,
-
-    definitions.cICalProperty_ACKNOWLEDGED: PropertyValueChecks.alwaysUTC,
-}

Copied: PyCalendar/trunk/src/pycalendar/icalendar/validation.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/icalendar/validation.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/icalendar/validation.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/icalendar/validation.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,37 @@
+##
+#    Copyright (c) 2011-2012 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 import definitions
+from pycalendar.validation import partial, PropertyValueChecks
+
+ICALENDAR_VALUE_CHECKS = {
+    definitions.cICalProperty_CALSCALE: partial(PropertyValueChecks.stringValue, "GREGORIAN"),
+    definitions.cICalProperty_VERSION: partial(PropertyValueChecks.stringValue, "2.0"),
+
+    definitions.cICalProperty_PERCENT_COMPLETE: partial(PropertyValueChecks.numericRange, 0, 100),
+    definitions.cICalProperty_PRIORITY: partial(PropertyValueChecks.numericRange, 0, 9),
+
+    definitions.cICalProperty_COMPLETED: PropertyValueChecks.alwaysUTC,
+
+    definitions.cICalProperty_REPEAT: PropertyValueChecks.positiveIntegerOrZero,
+
+    definitions.cICalProperty_CREATED: PropertyValueChecks.alwaysUTC,
+    definitions.cICalProperty_DTSTAMP: PropertyValueChecks.alwaysUTC,
+    definitions.cICalProperty_LAST_MODIFIED: PropertyValueChecks.alwaysUTC,
+    definitions.cICalProperty_SEQUENCE: PropertyValueChecks.positiveIntegerOrZero,
+
+    definitions.cICalProperty_ACKNOWLEDGED: PropertyValueChecks.alwaysUTC,
+}

Modified: PyCalendar/trunk/src/pycalendar/integervalue.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/integervalue.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/integervalue.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -16,35 +16,45 @@
 
 # iCalendar UTC Offset value
 
-from value import PyCalendarValue
+from pycalendar import xmldefs
+from pycalendar.value import PyCalendarValue
 
-class PyCalendarIntegerValue( PyCalendarValue ):
+class PyCalendarIntegerValue(PyCalendarValue):
 
-    def __init__(self, value = None, copyit = None):
-        if value:
-            self.mValue = value
-        elif copyit:
-            self.mValue = copyit.mValue
-        else:
-            self.mValue = 0
+    def __init__(self, value=None):
+        self.mValue = value if value is not None else 0
 
+
+    def duplicate(self):
+        return PyCalendarIntegerValue(self.mValue)
+
+
     def getType(self):
         return PyCalendarValue.VALUETYPE_INTEGER
 
-    def parse( self, data ):
+
+    def parse(self, data):
         self.mValue = int(data)
-        
+
+
     # os - StringIO object
-    def generate( self, os ):
+    def generate(self, os):
         try:
-            os.write( str(self.mValue) )
+            os.write(str(self.mValue))
         except:
             pass
 
+
+    def writeXML(self, node, namespace):
+        value = self.getXMLNode(node, namespace)
+        value.text = str(self.mValue)
+
+
     def getValue(self):
         return self.mValue
 
-    def setValue( self, value ):
+
+    def setValue(self, value):
         self.mValue = value
 
-PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_INTEGER, PyCalendarIntegerValue)
+PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_INTEGER, PyCalendarIntegerValue, xmldefs.value_integer)

Modified: PyCalendar/trunk/src/pycalendar/itipdefinitions.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/itipdefinitions.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/itipdefinitions.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -39,3 +39,4 @@
 cICalMIMEComponent_VTODO = "vtodo"
 cICalMIMEComponent_VJOURNAL = "vjournal"
 cICalMIMEComponent_VFREEBUSY = "vfreebusy"
+cICalMIMEComponent_VAVAILABILITY = "vavailability"

Modified: PyCalendar/trunk/src/pycalendar/locale.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/locale.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/locale.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -19,20 +19,20 @@
 SHORT = 1
 ABBREVIATED = 2
 
-cLongDays = [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ]
+cLongDays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
 
-cShortDays = [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ]
+cShortDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
 
-cAbbrevDays = [ "S", "M", "T", "W", "T", "F", "S" ]
+cAbbrevDays = ["S", "M", "T", "W", "T", "F", "S"]
 
-cLongMonths = [ "", "January", "February", "March", "April", "May", "June",
-                "July", "August", "September", "October", "November", "December" ]
+cLongMonths = ["", "January", "February", "March", "April", "May", "June",
+               "July", "August", "September", "October", "November", "December"]
 
-cShortMonths = [ "", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
-                 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]
+cShortMonths = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+                "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
 
-cAbbrevMonths = [ "", "J", "F", "M", "A", "M", "J",
-                  "J", "A", "S", "O", "N", "D" ]
+cAbbrevMonths = ["", "J", "F", "M", "A", "M", "J",
+                 "J", "A", "S", "O", "N", "D"]
 
 s24HourTime = False
 sDDMMDate = False
@@ -41,21 +41,24 @@
 def getDay(day, strl):
     return {LONG: cLongDays[day], SHORT: cShortDays[day], ABBREVIATED: cAbbrevDays[day]}[strl]
 
+
+
 #     1..12 - January - December
 def getMonth(month, strl):
     return {LONG: cLongMonths[month], SHORT: cShortMonths[month], ABBREVIATED: cAbbrevMonths[month]}[strl]
 
+
+
 #     Use 24 hour time display
 def use24HourTime():
 
     # TODO: get 24 hour option from system prefs
+    return s24HourTime
 
-    return s24HourTime;
 
+
 #     Use DD/MM date display
 def useDDMMDate():
 
     # TODO: get 24 hour option from system prefs
-
-    return sDDMMDate;
-
+    return sDDMMDate

Modified: PyCalendar/trunk/src/pycalendar/manager.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/manager.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/manager.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,22 +14,16 @@
 #    limitations under the License.
 ##
 
-from calendar import PyCalendar
-from property import PyCalendarProperty
-from timezone import PyCalendarTimezone
+from pycalendar.timezone import PyCalendarTimezone
 
 class PyCalendarManager(object):
 
     sICalendarManager = None
 
-    @staticmethod
-    def loadStatics():
-        PyCalendar.loadStatics()
-        PyCalendarProperty.loadStatics()
-
     def __init__(self):
-        self.mDefaultTimezone = PyCalendarTimezone()
+        PyCalendarTimezone.sDefaultTimezone = PyCalendarTimezone()
 
+
     def initManager(self):
         # TODO: - read in timezones from vtimezones.ics file
 
@@ -37,6 +31,7 @@
         # hard-coded to my personal prefs!
         self.setDefaultTimezone(PyCalendarTimezone(utc=False, tzid="US/Eastern"))
 
+
     def setDefaultTimezoneID(self, tzid):
         # Check for UTC
         if tzid == "UTC":
@@ -46,16 +41,19 @@
             temp = PyCalendarTimezone(utc=False, tzid=tzid)
             self.setDefaultTimezone(temp)
 
+
     def setDefaultTimezone(self, tzid):
-        self.mDefaultTimezone = tzid
+        PyCalendarTimezone.sDefaultTimezone = tzid
 
+
     def getDefaultTimezoneID(self):
-        if self.mDefaultTimezone.getUTC():
+        if PyCalendarTimezone.sDefaultTimezone.getUTC():
             return "UTC"
         else:
-            return self.mDefaultTimezone.getTimezoneID()
+            return PyCalendarTimezone.sDefaultTimezone.getTimezoneID()
 
+
     def getDefaultTimezone(self):
-        return self.mDefaultTimezone
+        return PyCalendarTimezone.sDefaultTimezone
 
 PyCalendarManager.sICalendarManager = PyCalendarManager()

Modified: PyCalendar/trunk/src/pycalendar/multivalue.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/multivalue.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/multivalue.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,54 +14,83 @@
 #    limitations under the License.
 ##
 
-from value import PyCalendarValue
+from pycalendar.value import PyCalendarValue
 
-class PyCalendarMultiValue( PyCalendarValue ):
+class PyCalendarMultiValue(PyCalendarValue):
 
-    def __init__( self, type = None, copyit = None ):
+    def __init__(self, type):
 
-        if type:
-            self.mType = type
-            self.mValues = []
-        elif copyit:
-            self.mType = copyit.mType
-            self.mValues = [i.copy() for i in copyit.mValues]
+        self.mType = type
+        self.mValues = []
 
-    def getType( self ):
+
+    def __hash__(self):
+        return hash(tuple(self.mValues))
+
+
+    def duplicate(self):
+        other = PyCalendarMultiValue(self.mType)
+        other.mValues = [i.duplicate() for i in self.mValues]
+        return other
+
+
+    def getType(self):
         return self.mType
 
+
     def getRealType(self):
         return PyCalendarValue.VALUETYPE_MULTIVALUE
 
-    def getValues( self ):
+
+    def getValue(self):
         return self.mValues
 
-    def addValue( self, value ):
-        self.mValues.append( value )
 
-    def parse( self, data ):
+    def getValues(self):
+        return self.mValues
+
+
+    def addValue(self, value):
+        self.mValues.append(value)
+
+
+    def setValue(self, value):
+        newValues = []
+        for v in value:
+            pyCalendarValue = PyCalendarValue.createFromType(self.mType)
+            pyCalendarValue.setValue(v)
+            newValues.append(pyCalendarValue)
+        self.mValues = newValues
+
+
+    def parse(self, data):
         # Tokenize on comma
         if "," in data:
-            tokens = data.split( "," )
+            tokens = data.split(",")
         else:
             tokens = (data,)
         for token in tokens:
             # Create single value, and parse data
-            value = PyCalendarValue.createFromType( self.mType )
-            value.parse( token )
-            self.mValues.append( value )
+            value = PyCalendarValue.createFromType(self.mType)
+            value.parse(token)
+            self.mValues.append(value)
 
-    def generate( self, os ):
+
+    def generate(self, os):
         try:
             first = True
             for iter in self.mValues:
                 if first:
                     first = False
                 else:
-                    os.write( "," )
-                iter.generate( os )
+                    os.write(",")
+                iter.generate(os)
         except:
             pass
 
-PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_MULTIVALUE, PyCalendarMultiValue)
-            
\ No newline at end of file
+
+    def writeXML(self, node, namespace):
+        for iter in self.mValues:
+            iter.writeXML(node, namespace)
+
+PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_MULTIVALUE, PyCalendarMultiValue, None)

Copied: PyCalendar/trunk/src/pycalendar/n.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/n.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/n.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/n.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,124 @@
+##
+#    Copyright (c) 2007-2012 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.
+##
+
+# vCard ADR value
+
+from pycalendar import utils
+from pycalendar.valueutils import ValueMixin
+
+class N(ValueMixin):
+    """
+    mValue is a tuple of seven str or tuples of str
+    """
+
+    (
+        LAST,
+        FIRST,
+        MIDDLE,
+        PREFIX,
+        SUFFIX,
+        MAXITEMS
+    ) = range(6)
+
+    def __init__(self, last="", first="", middle="", prefix="", suffix=""):
+        self.mValue = (last, first, middle, prefix, suffix)
+
+
+    def duplicate(self):
+        return N(*self.mValue)
+
+
+    def __hash__(self):
+        return hash(self.mValue)
+
+
+    def __repr__(self):
+        return "N %s" % (self.getText(),)
+
+
+    def __eq__(self, comp):
+        return self.mValue == comp.mValue
+
+
+    def getFirst(self):
+        return self.mValue[N.FIRST]
+
+
+    def setFirst(self, value):
+        self.mValue[N.FIRST] = value
+
+
+    def getLast(self):
+        return self.mValue[N.LAST]
+
+
+    def setLast(self, value):
+        self.mValue[N.LAST] = value
+
+
+    def getMiddle(self):
+        return self.mValue[N.MIDDLE]
+
+
+    def setMiddle(self, value):
+        self.mValue[N.MIDDLE] = value
+
+
+    def getPrefix(self):
+        return self.mValue[N.PREFIX]
+
+
+    def setPrefix(self, value):
+        self.mValue[N.PREFIX] = value
+
+
+    def getSuffix(self):
+        return self.mValue[N.SUFFIX]
+
+
+    def setSuffix(self, value):
+        self.mValue[N.SUFFIX] = value
+
+
+    def getFullName(self):
+
+
+        def _stringOrList(item):
+            return item if isinstance(item, basestring) else " ".join(item)
+
+        results = []
+        for i in (N.PREFIX, N.FIRST, N.MIDDLE, N.LAST, N.SUFFIX):
+            result = _stringOrList(self.mValue[i])
+            if result:
+                results.append(result)
+
+        return " ".join(results)
+
+
+    def parse(self, data):
+        self.mValue = utils.parseDoubleNestedList(data, N.MAXITEMS)
+
+
+    def generate(self, os):
+        utils.generateDoubleNestedList(os, self.mValue)
+
+
+    def getValue(self):
+        return self.mValue
+
+
+    def setValue(self, value):
+        self.mValue = value

Copied: PyCalendar/trunk/src/pycalendar/nvalue.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/nvalue.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/nvalue.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/nvalue.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,51 @@
+##
+#    Copyright (c) 2007-2012 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.
+##
+
+# vCard ADR value
+
+from pycalendar.n import N
+from pycalendar.value import PyCalendarValue
+
+class NValue(PyCalendarValue):
+
+    def __init__(self, value=None):
+        self.mValue = value if value is not None else N()
+
+
+    def duplicate(self):
+        return NValue(self.mValue.duplicate())
+
+
+    def getType(self):
+        return PyCalendarValue.VALUETYPE_N
+
+
+    def parse(self, data):
+        self.mValue.parse(data)
+
+
+    def generate(self, os):
+        self.mValue.generate(os)
+
+
+    def getValue(self):
+        return self.mValue
+
+
+    def setValue(self, value):
+        self.mValue = value
+
+PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_N, NValue, None)

Copied: PyCalendar/trunk/src/pycalendar/orgvalue.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/orgvalue.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/orgvalue.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/orgvalue.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,54 @@
+##
+#    Copyright (c) 2007-2012 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.
+##
+
+# vCard ORG value
+
+from pycalendar import utils
+from pycalendar.value import PyCalendarValue
+
+class OrgValue(PyCalendarValue):
+    """
+    mValue is a str or tuple of str
+    """
+
+    def __init__(self, value=None):
+        self.mValue = value
+
+
+    def duplicate(self):
+        return OrgValue(self.mValue)
+
+
+    def getType(self):
+        return PyCalendarValue.VALUETYPE_ORG
+
+
+    def parse(self, data):
+        self.mValue = utils.parseTextList(data, ';')
+
+
+    def generate(self, os):
+        utils.generateTextList(os, self.mValue, ';')
+
+
+    def getValue(self):
+        return self.mValue
+
+
+    def setValue(self, value):
+        self.mValue = value
+
+PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_ORG, OrgValue, None)

Modified: PyCalendar/trunk/src/pycalendar/outputfilter.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/outputfilter.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/outputfilter.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -24,65 +24,78 @@
         self.mAllProperties = False
         self.mProperties = None
 
+
     def getType(self):
         return self.mType
 
+
     # Test to see if component type can be written out
-    def testComponent(self, type):
-        return self.mType == type
+    def testComponent(self, oftype):
+        return self.mType == oftype
 
+
     def isAllSubComponents(self):
         return self.mAllSubComponents
 
+
     def setAllSubComponents(self):
         self.mAllSubComponents = True
         self.mSubComponents = None
 
+
     def addSubComponent(self, comp):
         if self.mSubComponents == None:
             self.mSubComponents = {}
 
         self.mSubComponents[comp.getType()] = comp
 
+
     # Test to see if sub-component type can be written out
-    def testSubComponent(self, type):
+    def testSubComponent(self, oftype):
         return self.mAllSubComponents or (self.mSubComponents is not None) \
-                and self.mSubComponents.has_key(type)
+                and oftype in self.mSubComponents
 
+
     def hasSubComponentFilters(self):
         return self.mSubComponents is not None
 
+
     def getSubComponentFilter(self, type):
         if self.mSubComponents is not None:
             return self.mSubComponents.get(type, None)
         else:
             return None
 
+
     def isAllProperties(self):
         return self.mAllProperties
 
+
     def setAllProperties(self):
         self.mAllProperties = True
         self.mProperties = None
 
+
     def addProperty(self, name, no_value):
         if self.mProperties is None:
             self.mProperties = {}
 
         self.mProperties[name] = no_value
 
+
     def hasPropertyFilters(self):
         return self.mProperties is not None
 
+
     # Test to see if property can be written out and also return whether
     # the property value is used
     def testPropertyValue(self, name):
 
         if self.mAllProperties:
             return True, False
-        
+
         if self.mProperties is None:
             return False, False
-        
+
         result = self.mProperties.get(name, None)
         return result is not None, result

Copied: PyCalendar/trunk/src/pycalendar/parser.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/parser.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/parser.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/parser.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,56 @@
+##
+#    Copyright (c) 2011-2012 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.
+##
+
+class ParserContext(object):
+    """
+    Ultimately want to have these states as per-object so we can pass a context down
+    through the entire parse call chain so that we can use different error handling for
+    different situations. For now though it is a module static.
+    """
+
+    (
+        PARSER_ALLOW, # Pass the "suspect" data through to the object model
+        PARSER_IGNORE, # Ignore the "suspect" data
+        PARSER_FIX, # Fix (or if not possible ignore) the "suspect" data
+        PARSER_RAISE, # Raise an exception
+    ) = range(4)
+
+    # Some clients escape ":" - fix
+    INVALID_COLON_ESCAPE_SEQUENCE = PARSER_FIX
+
+    # Other escape sequences - raise
+    INVALID_ESCAPE_SEQUENCES = PARSER_RAISE
+
+    # Some client generate empty lines in the body of the data
+    BLANK_LINES_IN_DATA = PARSER_FIX
+
+    # Some clients still generate vCard 2 parameter syntax
+    VCARD_2_NO_PARAMETER_VALUES = PARSER_ALLOW
+
+    # Use this to fix v2 BASE64 to v3 ENCODING=b - only PARSER_FIX or PARSER_ALLOW
+    VCARD_2_BASE64 = PARSER_FIX
+
+    # Allow slightly invalid DURATION values
+    INVALID_DURATION_VALUE = PARSER_FIX
+
+    # Truncate over long ADR and N values
+    INVALID_ADR_N_VALUES = PARSER_FIX
+
+    # REQUEST-STATUS values with \; as the first separator
+    INVALID_REQUEST_STATUS_VALUE = PARSER_FIX
+
+    # Remove \-escaping in URI values when parsing - only PARSER_FIX or PARSER_ALLOW
+    BACKSLASH_IN_URI_VALUE = PARSER_FIX

Modified: PyCalendar/trunk/src/pycalendar/period.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/period.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/period.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,102 +14,150 @@
 #    limitations under the License.
 ##
 
-import cStringIO as StringIO
+from pycalendar import xmldefs
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.duration import PyCalendarDuration
+from pycalendar.valueutils import ValueMixin
+import xml.etree.cElementTree as XML
 
-from datetime import PyCalendarDateTime
-from duration import PyCalendarDuration
+class PyCalendarPeriod(ValueMixin):
 
-class PyCalendarPeriod(object):
+    def __init__(self, start=None, end=None, duration=None):
 
-    def __init__( self, start = None, end = None, duration = None, copyit = None ):
-        
-        if (start is not None) and (end is not None):
-            self.mStart = start
+        self.mStart = start if start is not None else PyCalendarDateTime()
+
+        if end is not None:
             self.mEnd = end
             self.mDuration = self.mEnd - self.mStart
             self.mUseDuration = False
-        elif (start is not None) and (duration is not None):
-            self.mStart = start
+        elif duration is not None:
             self.mDuration = duration
             self.mEnd = self.mStart + self.mDuration
             self.mUseDuration = True
-        elif (copyit is not None):
-            self.mStart = PyCalendarDateTime(copyit=copyit.mStart)
-            self.mEnd = PyCalendarDateTime(copyit=copyit.mEnd)
-            self.mDuration = PyCalendarDuration(copyit=copyit.mDuration)
-            self.mUseDuration = copyit.mUseDuration
         else:
-            self.mStart = PyCalendarDateTime()
-            self.mEnd = PyCalendarDateTime()
+            self.mEnd = self.mStart.duplicate()
             self.mDuration = PyCalendarDuration()
             self.mUseDuration = False
 
+
+    def duplicate(self):
+        other = PyCalendarPeriod(start=self.mStart.duplicate(), end=self.mEnd.duplicate())
+        other.mUseDuration = self.mUseDuration
+        return other
+
+
+    def __hash__(self):
+        return hash((self.mStart, self.mEnd,))
+
+
     def __repr__(self):
-        os = StringIO.StringIO()
-        self.generate(os)
-        return os.getvalue()
+        return "PyCalendarPeriod %s" % (self.getText(),)
 
-    def __eq__( self, comp ):
+
+    def __str__(self):
+        return self.getText()
+
+
+    def __eq__(self, comp):
         return self.mStart == comp.mStart and self.mEnd == comp.mEnd
 
-    def __gt__( self, comp ):
+
+    def __gt__(self, comp):
         return self.mStart > comp
 
-    def __lt__( self, comp ):
-        return self.mStart <  comp.mStart  \
-                or ( self.mStart == comp.mStart ) and self.mEnd < comp.mEnd
 
-    def parse( self, data ):
-        splits = data.split( '/', 1 )
+    def __lt__(self, comp):
+        return self.mStart < comp.mStart  \
+                or (self.mStart == comp.mStart) and self.mEnd < comp.mEnd
+
+
+    def parse(self, data):
+        splits = data.split('/', 1)
         if len(splits) == 2:
             start = splits[0]
             end = splits[1]
 
-            self.mStart.parse( start )
+            self.mStart.parse(start)
             if end[0] == 'P':
-                self.mDuration.parse( end )
+                self.mDuration.parse(end)
                 self.mUseDuration = True
-                self.mEnd = self.mStart.add( self.mDuration )
+                self.mEnd = self.mStart + self.mDuration
             else:
-                self.mEnd.parse( end )
+                self.mEnd.parse(end)
                 self.mUseDuration = False
                 self.mDuration = self.mEnd - self.mStart
+        else:
+            raise ValueError
 
-    def generate( self, os ):
+
+    def generate(self, os):
         try:
-            self.mStart.generate( os )
-            os.write( "/" )
+            self.mStart.generate(os)
+            os.write("/")
             if self.mUseDuration:
-                self.mDuration.generate( os )
+                self.mDuration.generate(os)
             else:
-                self.mEnd.generate( os )
+                self.mEnd.generate(os)
         except:
             pass
 
-    def getStart( self ):
+
+    def writeXML(self, node, namespace):
+        start = XML.SubElement(node, xmldefs.makeTag(namespace, xmldefs.period_start))
+        start.text = self.mStart.getXMLText()
+
+        if self.mUseDuration:
+            duration = XML.SubElement(node, xmldefs.makeTag(namespace, xmldefs.period_duration))
+            duration.text = self.mDuration.getText()
+        else:
+            end = XML.SubElement(node, xmldefs.makeTag(namespace, xmldefs.period_end))
+            end.text = self.mEnd.getXMLText()
+
+
+    def getStart(self):
         return self.mStart
 
-    def getEnd( self ):
+
+    def getEnd(self):
         return self.mEnd
 
-    def getDuration( self ):
+
+    def getDuration(self):
         return self.mDuration
 
-    def isDateWithinPeriod( self, dt ):
+
+    def getUseDuration(self):
+        return self.mUseDuration
+
+
+    def setUseDuration(self, use):
+        self.mUseDuration = use
+
+
+    def isDateWithinPeriod(self, dt):
         # Inclusive start, exclusive end
         return dt >= self.mStart and dt < self.mEnd
 
-    def isDateBeforePeriod( self, dt ):
+
+    def isDateBeforePeriod(self, dt):
         # Inclusive start
         return dt < self.mStart
 
-    def isDateAfterPeriod( self, dt ):
+
+    def isDateAfterPeriod(self, dt):
         # Exclusive end
         return dt >= self.mEnd
 
-    def isPeriodOverlap( self, p ):
+
+    def isPeriodOverlap(self, p):
         # Inclusive start, exclusive end
-        return not ( self.mStart >= p.mEnd or self.mEnd <= p.mStart )
+        return not (self.mStart >= p.mEnd or self.mEnd <= p.mStart)
 
-    def describeDuration( self ):
+
+    def adjustToUTC(self):
+        self.mStart.adjustToUTC()
+        self.mEnd.adjustToUTC()
+
+
+    def describeDuration(self):
         return ""

Modified: PyCalendar/trunk/src/pycalendar/periodvalue.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/periodvalue.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/periodvalue.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,32 +14,42 @@
 #    limitations under the License.
 ##
 
-from period import PyCalendarPeriod
-from value import PyCalendarValue
+from pycalendar import xmldefs
+from pycalendar.period import PyCalendarPeriod
+from pycalendar.value import PyCalendarValue
 
-class PyCalendarPeriodValue( PyCalendarValue ):
+class PyCalendarPeriodValue(PyCalendarValue):
 
-    def __init__( self, value = None, copyit = None ):
-        if value:
-            self.mValue = value
-        elif copyit:
-            self.mValue = PyCalendarPeriod( copyit=copyit.mValue )
-        else:
-            self.mValue = PyCalendarPeriod()
+    def __init__(self, value=None):
+        self.mValue = value if value is not None else PyCalendarPeriod()
 
-    def getType( self ):
+
+    def duplicate(self):
+        return PyCalendarPeriodValue(self.mValue.duplicate())
+
+
+    def getType(self):
         return PyCalendarValue.VALUETYPE_PERIOD
 
-    def parse( self, data ):
-        self.mValue.parse( data )
 
-    def generate( self, os ):
-        self.mValue.generate( os )
+    def parse(self, data):
+        self.mValue.parse(data)
 
-    def getValue( self ):
+
+    def generate(self, os):
+        self.mValue.generate(os)
+
+
+    def writeXML(self, node, namespace):
+        value = self.getXMLNode(node, namespace)
+        value.text = self.mValue.writeXML()
+
+
+    def getValue(self):
         return self.mValue
 
-    def setValue( self, value ):
+
+    def setValue(self, value):
         self.mValue = value
 
-PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_PERIOD, PyCalendarPeriodValue)
+PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_PERIOD, PyCalendarPeriodValue, xmldefs.value_period)

Modified: PyCalendar/trunk/src/pycalendar/plaintextvalue.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/plaintextvalue.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/plaintextvalue.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -16,31 +16,40 @@
 
 # iCalendar UTC Offset value
 
-from value import PyCalendarValue
+from pycalendar.value import PyCalendarValue
 
-class PyCalendarPlainTextValue( PyCalendarValue ):
+class PyCalendarPlainTextValue(PyCalendarValue):
 
-    def __init__(self, copyit=None):
-        
-        if copyit:
-            self.mValue = copyit.mValue
-        else:
-            self.mValue = ''
+    def __init__(self, value=''):
+        self.mValue = value
 
-    def parse( self, data ):
+
+    def duplicate(self):
+        return self.__class__(self.mValue)
+
+
+    def parse(self, data):
         # No decoding required
         self.mValue = data
-        
+
+
     # os - StringIO object
-    def generate( self, os ):
+    def generate(self, os):
         try:
             # No encoding required
-            os.write( self.mValue )
+            os.write(self.mValue)
         except:
             pass
 
+
+    def writeXML(self, node, namespace):
+        value = self.getXMLNode(node, namespace)
+        value.text = self.mValue
+
+
     def getValue(self):
         return self.mValue
 
-    def setValue( self, value ):
+
+    def setValue(self, value):
         self.mValue = value

Modified: PyCalendar/trunk/src/pycalendar/property.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/property.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/property.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,119 +14,296 @@
 #    limitations under the License.
 ##
 
+from pycalendar import definitions, xmldefs
+from pycalendar import stringutils
+from pycalendar.attribute import PyCalendarAttribute
+from pycalendar.binaryvalue import PyCalendarBinaryValue
+from pycalendar.caladdressvalue import PyCalendarCalAddressValue
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.datetimevalue import PyCalendarDateTimeValue
+from pycalendar.duration import PyCalendarDuration
+from pycalendar.durationvalue import PyCalendarDurationValue
+from pycalendar.exceptions import PyCalendarInvalidProperty
+from pycalendar.integervalue import PyCalendarIntegerValue
+from pycalendar.multivalue import PyCalendarMultiValue
+from pycalendar.period import PyCalendarPeriod
+from pycalendar.periodvalue import PyCalendarPeriodValue
+from pycalendar.plaintextvalue import PyCalendarPlainTextValue
+from pycalendar.recurrence import PyCalendarRecurrence
+from pycalendar.recurrencevalue import PyCalendarRecurrenceValue
+from pycalendar.requeststatusvalue import PyCalendarRequestStatusValue
+from pycalendar.unknownvalue import PyCalendarUnknownValue
+from pycalendar.urivalue import PyCalendarURIValue
+from pycalendar.utcoffsetvalue import PyCalendarUTCOffsetValue
+from pycalendar.utils import decodeParameterValue
+from pycalendar.value import PyCalendarValue
 import cStringIO as StringIO
+import xml.etree.cElementTree as XML
 
-from attribute import PyCalendarAttribute
-from caladdressvalue import PyCalendarCalAddressValue
-from datetime import PyCalendarDateTime
-from datetimevalue import PyCalendarDateTimeValue
-from duration import PyCalendarDuration
-from durationvalue import PyCalendarDurationValue
-from integervalue import PyCalendarIntegerValue
-from multivalue import PyCalendarMultiValue
-from period import PyCalendarPeriod
-from periodvalue import PyCalendarPeriodValue
-from plaintextvalue import PyCalendarPlainTextValue
-from recurrence import PyCalendarRecurrence
-from recurrencevalue import PyCalendarRecurrenceValue
-from urivalue import PyCalendarURIValue
-from utcoffsetvalue import PyCalendarUTCOffsetValue
-from value import PyCalendarValue
-import definitions
-import stringutils
-
 class PyCalendarProperty(object):
 
-#    protected string mName
-#
-#    protected MultiMap mAttributes
-#
-#    protected ICalendarValue mValue
-#
+    sDefaultValueTypeMap = {
 
-    sDefaultValueTypeMap = None
-    sValueTypeMap = None
-    sTypeValueMap = None
-    sMultiValues = None
+        # 5545 Section 3.7
+        definitions.cICalProperty_CALSCALE         : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.cICalProperty_METHOD           : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.cICalProperty_PRODID           : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.cICalProperty_VERSION          : PyCalendarValue.VALUETYPE_TEXT,
 
+        # 5545 Section 3.8.1
+        definitions.cICalProperty_ATTACH           : PyCalendarValue.VALUETYPE_URI,
+        definitions.cICalProperty_CATEGORIES       : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.cICalProperty_CLASS            : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.cICalProperty_COMMENT          : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.cICalProperty_DESCRIPTION      : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.cICalProperty_GEO              : PyCalendarValue.VALUETYPE_GEO,
+        definitions.cICalProperty_LOCATION         : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.cICalProperty_PERCENT_COMPLETE : PyCalendarValue.VALUETYPE_INTEGER,
+        definitions.cICalProperty_PRIORITY         : PyCalendarValue.VALUETYPE_INTEGER,
+        definitions.cICalProperty_RESOURCES        : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.cICalProperty_STATUS           : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.cICalProperty_SUMMARY          : PyCalendarValue.VALUETYPE_TEXT,
+
+        # 5545 Section 3.8.2
+        definitions.cICalProperty_COMPLETED : PyCalendarValue.VALUETYPE_DATETIME,
+        definitions.cICalProperty_DTEND     : PyCalendarValue.VALUETYPE_DATETIME,
+        definitions.cICalProperty_DUE       : PyCalendarValue.VALUETYPE_DATETIME,
+        definitions.cICalProperty_DTSTART   : PyCalendarValue.VALUETYPE_DATETIME,
+        definitions.cICalProperty_DURATION  : PyCalendarValue.VALUETYPE_DURATION,
+        definitions.cICalProperty_FREEBUSY  : PyCalendarValue.VALUETYPE_PERIOD,
+        definitions.cICalProperty_TRANSP    : PyCalendarValue.VALUETYPE_TEXT,
+
+        # 5545 Section 3.8.3
+        definitions.cICalProperty_TZID         : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.cICalProperty_TZNAME       : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.cICalProperty_TZOFFSETFROM : PyCalendarValue.VALUETYPE_UTC_OFFSET,
+        definitions.cICalProperty_TZOFFSETTO   : PyCalendarValue.VALUETYPE_UTC_OFFSET,
+        definitions.cICalProperty_TZURL        : PyCalendarValue.VALUETYPE_URI,
+
+        # 5545 Section 3.8.4
+        definitions.cICalProperty_ATTENDEE      : PyCalendarValue.VALUETYPE_CALADDRESS,
+        definitions.cICalProperty_CONTACT       : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.cICalProperty_ORGANIZER     : PyCalendarValue.VALUETYPE_CALADDRESS,
+        definitions.cICalProperty_RECURRENCE_ID : PyCalendarValue.VALUETYPE_DATETIME,
+        definitions.cICalProperty_RELATED_TO    : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.cICalProperty_URL           : PyCalendarValue.VALUETYPE_URI,
+        definitions.cICalProperty_UID           : PyCalendarValue.VALUETYPE_TEXT,
+
+        # 5545 Section 3.8.5
+        definitions.cICalProperty_EXDATE : PyCalendarValue.VALUETYPE_DATETIME,
+        definitions.cICalProperty_EXRULE : PyCalendarValue.VALUETYPE_RECUR, # 2445 only
+        definitions.cICalProperty_RDATE  : PyCalendarValue.VALUETYPE_DATETIME,
+        definitions.cICalProperty_RRULE  : PyCalendarValue.VALUETYPE_RECUR,
+
+        # 5545 Section 3.8.6
+        definitions.cICalProperty_ACTION  : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.cICalProperty_REPEAT  : PyCalendarValue.VALUETYPE_INTEGER,
+        definitions.cICalProperty_TRIGGER : PyCalendarValue.VALUETYPE_DURATION,
+
+        # 5545 Section 3.8.7
+        definitions.cICalProperty_CREATED       : PyCalendarValue.VALUETYPE_DATETIME,
+        definitions.cICalProperty_DTSTAMP       : PyCalendarValue.VALUETYPE_DATETIME,
+        definitions.cICalProperty_LAST_MODIFIED : PyCalendarValue.VALUETYPE_DATETIME,
+        definitions.cICalProperty_SEQUENCE      : PyCalendarValue.VALUETYPE_INTEGER,
+
+        # 5545 Section 3.8.8
+        definitions.cICalProperty_REQUEST_STATUS : PyCalendarValue.VALUETYPE_REQUEST_STATUS,
+
+        # Extensions: draft-daboo-valarm-extensions-03
+        definitions.cICalProperty_ACKNOWLEDGED   : PyCalendarValue.VALUETYPE_DATETIME,
+
+        # Apple Extensions
+        definitions.cICalProperty_XWRCALNAME  : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.cICalProperty_XWRCALDESC  : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.cICalProperty_XWRALARMUID : PyCalendarValue.VALUETYPE_TEXT,
+
+        # Mulberry extensions
+        definitions.cICalProperty_ACTION_X_SPEAKTEXT  : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.cICalProperty_ALARM_X_LASTTRIGGER : PyCalendarValue.VALUETYPE_DATETIME,
+        definitions.cICalProperty_ALARM_X_ALARMSTATUS : PyCalendarValue.VALUETYPE_TEXT,
+    }
+
+    sValueTypeMap = {
+
+        # 5545 Section 3.3
+        definitions.cICalValue_BINARY      : PyCalendarValue.VALUETYPE_BINARY,
+        definitions.cICalValue_BOOLEAN     : PyCalendarValue.VALUETYPE_BOOLEAN,
+        definitions.cICalValue_CAL_ADDRESS : PyCalendarValue.VALUETYPE_CALADDRESS,
+        definitions.cICalValue_DATE        : PyCalendarValue.VALUETYPE_DATE,
+        definitions.cICalValue_DATE_TIME   : PyCalendarValue.VALUETYPE_DATETIME,
+        definitions.cICalValue_DURATION    : PyCalendarValue.VALUETYPE_DURATION,
+        definitions.cICalValue_FLOAT       : PyCalendarValue.VALUETYPE_FLOAT,
+        definitions.cICalValue_INTEGER     : PyCalendarValue.VALUETYPE_INTEGER,
+        definitions.cICalValue_PERIOD      : PyCalendarValue.VALUETYPE_PERIOD,
+        definitions.cICalValue_RECUR       : PyCalendarValue.VALUETYPE_RECUR,
+        definitions.cICalValue_TEXT        : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.cICalValue_TIME        : PyCalendarValue.VALUETYPE_TIME,
+        definitions.cICalValue_URI         : PyCalendarValue.VALUETYPE_URI,
+        definitions.cICalValue_UTC_OFFSET  : PyCalendarValue.VALUETYPE_UTC_OFFSET,
+    }
+
+    sTypeValueMap = {
+
+        # 5545 Section 3.3
+        PyCalendarValue.VALUETYPE_BINARY         : definitions.cICalValue_BINARY,
+        PyCalendarValue.VALUETYPE_BOOLEAN        : definitions.cICalValue_BOOLEAN,
+        PyCalendarValue.VALUETYPE_CALADDRESS     : definitions.cICalValue_CAL_ADDRESS,
+        PyCalendarValue.VALUETYPE_DATE           : definitions.cICalValue_DATE,
+        PyCalendarValue.VALUETYPE_DATETIME       : definitions.cICalValue_DATE_TIME,
+        PyCalendarValue.VALUETYPE_DURATION       : definitions.cICalValue_DURATION,
+        PyCalendarValue.VALUETYPE_FLOAT          : definitions.cICalValue_FLOAT,
+        PyCalendarValue.VALUETYPE_GEO            : definitions.cICalValue_FLOAT,
+        PyCalendarValue.VALUETYPE_INTEGER        : definitions.cICalValue_INTEGER,
+        PyCalendarValue.VALUETYPE_PERIOD         : definitions.cICalValue_PERIOD,
+        PyCalendarValue.VALUETYPE_RECUR          : definitions.cICalValue_RECUR,
+        PyCalendarValue.VALUETYPE_TEXT           : definitions.cICalValue_TEXT,
+        PyCalendarValue.VALUETYPE_REQUEST_STATUS : definitions.cICalValue_TEXT,
+        PyCalendarValue.VALUETYPE_TIME           : definitions.cICalValue_TIME,
+        PyCalendarValue.VALUETYPE_URI            : definitions.cICalValue_URI,
+        PyCalendarValue.VALUETYPE_UTC_OFFSET     : definitions.cICalValue_UTC_OFFSET,
+    }
+
+    sMultiValues = set((
+        definitions.cICalProperty_CATEGORIES,
+        definitions.cICalProperty_RESOURCES,
+        definitions.cICalProperty_FREEBUSY,
+        definitions.cICalProperty_EXDATE,
+        definitions.cICalProperty_RDATE,
+    ))
+
     @staticmethod
-    def loadStatics():
-        PyCalendarProperty._init_map()
+    def registerDefaultValue(propname, valuetype):
+        if propname not in PyCalendarProperty.sDefaultValueTypeMap:
+            PyCalendarProperty.sDefaultValueTypeMap[propname] = valuetype
 
-    def __init__(self, arg1 = None, arg2 = None, arg3 = None):
+
+    def __init__(self, name=None, value=None, valuetype=None):
         self._init_PyCalendarProperty()
+        self.mName = name if name is not None else ""
 
-        arg1str = isinstance(arg1, str)
-        arg2str = isinstance(arg2, str)
-        if arg1str:
-            if isinstance(arg2, int) and (arg3 is None):
-                self.mName = arg1
-                self.init_attr_value_int(arg2)
-            elif arg2str and (arg3 is None):
-                self.mName = arg1
-                self._init_attr_value_text(arg2, PyCalendarValue.VALUETYPE_TEXT)
-            elif arg2str and isinstance(arg3, int):
-                self.mName = arg1
-                self._init_attr_value_text(arg2, arg3)
-            elif isinstance(arg2, PyCalendarDateTime) and (arg3 is None):
-                self.mName = arg1
-                self._init_attr_value_datetime(arg2)
-            elif isinstance(arg2, list) and (arg3 is None):
-                self.mName = arg1
-                self._init_attr_value_datetimelist(arg2)
-            elif isinstance(arg2, PyCalendarDuration) and (arg3 is None):
-                self.mName = arg1
-                self._init_attr_value_duration(arg2)
-            elif isinstance(arg2, PyCalendarPeriod) and (arg3 is None):
-                self.mName = arg1
-                self._init_attr_value_period(arg2)
-            elif isinstance(arg2, PyCalendarRecurrence) and (arg3 is None):
-                self.mName = arg1
-                self._init_attr_value_recur(arg2)
-            elif isinstance(arg2, PyCalendarUTCOffsetValue) and (arg3 is None):
-                self.mName = arg1
-                self._init_attr_value_utcoffset(arg2)
-        elif isinstance(arg1, PyCalendarProperty) and (arg2 is None) and (arg3 is None):
-            self._copy_PyCalendarProperty(arg1)
+        # The None check speeds up .duplicate()
+        if value is None:
+            pass
 
+        # Order these based on most likely occurrence to speed up this method
+        elif isinstance(value, str):
+            self._init_attr_value_text(value, valuetype if valuetype else PyCalendarProperty.sDefaultValueTypeMap.get(self.mName.upper(), PyCalendarValue.VALUETYPE_UNKNOWN))
+
+        elif isinstance(value, PyCalendarDateTime):
+            self._init_attr_value_datetime(value)
+
+        elif isinstance(value, PyCalendarDuration):
+            self._init_attr_value_duration(value)
+
+        elif isinstance(value, PyCalendarRecurrence):
+            self._init_attr_value_recur(value)
+
+        elif isinstance(value, PyCalendarPeriod):
+            self._init_attr_value_period(value)
+
+        elif isinstance(value, int):
+            self._init_attr_value_int(value)
+
+        elif isinstance(value, list):
+            if name.upper() == definitions.cICalProperty_REQUEST_STATUS:
+                self._init_attr_value_requeststatus(value)
+            else:
+                period_list = False
+                if len(value) != 0:
+                    period_list = isinstance(value[0], PyCalendarPeriod)
+                if period_list:
+                    self._init_attr_value_periodlist(value)
+                else:
+                    self._init_attr_value_datetimelist(value)
+
+        elif isinstance(value, PyCalendarUTCOffsetValue):
+            self._init_attr_value_utcoffset(value)
+
+
+    def duplicate(self):
+        other = PyCalendarProperty(self.mName)
+        for attrname, attrs in self.mAttributes.items():
+            other.mAttributes[attrname] = [i.duplicate() for i in attrs]
+        other.mValue = self.mValue.duplicate()
+
+        return other
+
+
+    def __hash__(self):
+        return hash((
+            self.mName,
+            tuple([tuple(self.mAttributes[attrname]) for attrname in sorted(self.mAttributes.keys())]),
+            self.mValue,
+        ))
+
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+
+    def __eq__(self, other):
+        if not isinstance(other, PyCalendarProperty):
+            return False
+        return self.mName == other.mName and self.mValue == other.mValue and self.mAttributes == other.mAttributes
+
+
     def __repr__(self):
-        os = StringIO.StringIO()
-        self.generate(os)
-        return os.getvalue()
+        return "PyCalendarProperty: %s" % (self.getText(),)
 
+
     def __str__(self):
-        os = StringIO.StringIO()
-        self.generate(os)
-        return os.getvalue()
+        return self.getText()
 
+
     def getName(self):
         return self.mName
 
+
     def setName(self, name):
         self.mName = name
 
+
     def getAttributes(self):
         return self.mAttributes
 
+
     def setAttributes(self, attributes):
-        self.mAttributes = attributes
+        self.mAttributes = dict([(k.upper(), v) for k, v in attributes.iteritems()])
 
+
     def hasAttribute(self, attr):
-        return self.mAttributes.has_key(attr)
+        return attr.upper() in self.mAttributes
 
+
     def getAttributeValue(self, attr):
-        return self.mAttributes[attr][0].getFirstValue()
+        return self.mAttributes[attr.upper()][0].getFirstValue()
 
+
     def addAttribute(self, attr):
-        self.mAttributes.setdefault(attr.getName(), []).append(attr)
+        self.mAttributes.setdefault(attr.getName().upper(), []).append(attr)
 
+
+    def replaceAttribute(self, attr):
+        self.mAttributes[attr.getName().upper()] = [attr]
+
+
     def removeAttributes(self, attr):
-        if self.mAttributes.has_key(attr):
-            del self.mAttributes[attr]
+        if attr.upper() in self.mAttributes:
+            del self.mAttributes[attr.upper()]
 
+
     def getValue(self):
         return self.mValue
 
+
+    def getBinaryValue(self):
+
+        if isinstance(self.mValue, PyCalendarBinaryValue):
+            return self.mValue
+        else:
+            return None
+
+
     def getCalAddressValue(self):
 
         if isinstance(self.mValue, PyCalendarCalAddressValue):
@@ -134,6 +311,7 @@
         else:
             return None
 
+
     def getDateTimeValue(self):
 
         if isinstance(self.mValue, PyCalendarDateTimeValue):
@@ -141,6 +319,7 @@
         else:
             return None
 
+
     def getDurationValue(self):
 
         if isinstance(self.mValue, PyCalendarDurationValue):
@@ -148,6 +327,7 @@
         else:
             return None
 
+
     def getIntegerValue(self):
 
         if isinstance(self.mValue, PyCalendarIntegerValue):
@@ -155,6 +335,7 @@
         else:
             return None
 
+
     def getMultiValue(self):
 
         if isinstance(self.mValue, PyCalendarMultiValue):
@@ -162,6 +343,7 @@
         else:
             return None
 
+
     def getPeriodValue(self):
 
         if isinstance(self.mValue, PyCalendarPeriodValue):
@@ -169,6 +351,7 @@
         else:
             return None
 
+
     def getRecurrenceValue(self):
 
         if isinstance(self.mValue, PyCalendarRecurrenceValue):
@@ -176,6 +359,7 @@
         else:
             return None
 
+
     def getTextValue(self):
 
         if isinstance(self.mValue, PyCalendarPlainTextValue):
@@ -183,6 +367,7 @@
         else:
             return None
 
+
     def getURIValue(self):
 
         if isinstance(self.mValue, PyCalendarURIValue):
@@ -190,6 +375,7 @@
         else:
             return None
 
+
     def getUTCOffsetValue(self):
 
         if isinstance(self.mValue, PyCalendarUTCOffsetValue):
@@ -197,54 +383,71 @@
         else:
             return None
 
+
     def parse(self, data):
         # Look for attribute or value delimiter
         prop_name, txt = stringutils.strduptokenstr(data, ";:")
         if not prop_name:
-            return False
+            raise PyCalendarInvalidProperty("Invalid property", data)
 
         # We have the name
         self.mName = prop_name
-        
+
         # TODO: Try to use static string for the name
 
         # Now loop getting data
-        while txt:
-            if txt[0] == ';':
-                # Parse attribute
+        try:
+            while txt:
+                if txt[0] == ';':
+                    # Parse attribute
 
-                # Move past delimiter
-                txt = txt[1:]
+                    # Move past delimiter
+                    txt = txt[1:]
 
-                # Get quoted string or token
-                attribute_name, txt = stringutils.strduptokenstr(txt, "=")
-                if attribute_name is None:
-                    return False
-                txt = txt[1:]
-                attribute_value, txt = stringutils.strduptokenstr(txt, ":;,")
-                if attribute_value is None:
-                    return False
+                    # Get quoted string or token
+                    attribute_name, txt = stringutils.strduptokenstr(txt, "=")
+                    if attribute_name is None:
+                        raise PyCalendarInvalidProperty("Invalid property", data)
+                    txt = txt[1:]
+                    attribute_value, txt = stringutils.strduptokenstr(txt, ":;,")
+                    if attribute_value is None:
+                        raise PyCalendarInvalidProperty("Invalid property", data)
 
-                # Now add attribute value
-                attrvalue = PyCalendarAttribute(name = attribute_name, value=attribute_value)
-                self.mAttributes.setdefault(attribute_name, []).append(attrvalue)
+                    # Now add attribute value (decode ^-escpaing)
+                    attrvalue = PyCalendarAttribute(name=attribute_name, value=decodeParameterValue(attribute_value))
+                    self.mAttributes.setdefault(attribute_name.upper(), []).append(attrvalue)
 
-                # Look for additional values
-                while txt[0] == ',':
+                    # Look for additional values
+                    while txt[0] == ',':
+                        txt = txt[1:]
+                        attribute_value2, txt = stringutils.strduptokenstr(txt, ":;,")
+                        if attribute_value2 is None:
+                            raise PyCalendarInvalidProperty("Invalid property", data)
+                        attrvalue.addValue(decodeParameterValue(attribute_value2))
+                elif txt[0] == ':':
                     txt = txt[1:]
-                    attribute_value2, txt = stringutils.strduptokenstr(txt, ":;,")
-                    if attribute_value2 is None:
-                        return False
-                    attrvalue.addValue(attribute_value2)
-            elif txt[0] == ':':
-                txt = txt[1:]
-                self.createValue(txt)
-                txt = None
+                    self.createValue(txt)
+                    txt = None
+                else:
+                    # We should never get here but if we do we need to terminate the loop
+                    raise PyCalendarInvalidProperty("Invalid property", data)
 
+        except IndexError:
+            raise PyCalendarInvalidProperty("Invalid property", data)
+
         # We must have a value of some kind
-        return self.mValue is not None
+        if self.mValue is None:
+            raise PyCalendarInvalidProperty("Invalid property", data)
 
+        return True
 
+
+    def getText(self):
+        os = StringIO.StringIO()
+        self.generate(os)
+        return os.getvalue()
+
+
     def generate(self, os):
 
         # Write it out always with value
@@ -252,12 +455,13 @@
 
 
     def generateFiltered(self, os, filter):
-        
+
         # Check for property in filter and whether value is written out
-        test, novalue = filter.testPropertyValue(self.mName)
+        test, novalue = filter.testPropertyValue(self.mName.upper())
         if test:
             self.generateValue(os, novalue)
 
+
     # Write out the actual property, possibly skipping the value
     def generateValue(self, os, novalue):
 
@@ -268,8 +472,8 @@
         sout.write(self.mName)
 
         # Write all attributes
-        for attrs in self.mAttributes.values():
-            for attr in attrs:
+        for key in sorted(self.mAttributes.keys()):
+            for attr in self.mAttributes[key]:
                 sout.write(";")
                 attr.generate(sout)
 
@@ -301,169 +505,77 @@
                     while (temp[offset] > 0x7F) and ((ord(temp[offset]) & 0xC0) == 0x80):
                         # Step back until we have a valid char
                         offset -= 1
-                    
+
                     line = temp[start:offset]
                     os.write(line)
-                    os.write("\n ")
+                    os.write("\r\n ")
                     written += offset - start
                     start = offset
-    
-        os.write("\n")
-    
-    def _init_PyCalendarProperty(self):
-        self.mName = ""
-        self.mAttributes = {}
-        self.mValue = None
 
-    def _copy_PyCalendarProperty(self, copyit):
-        self.mName = copyit.mName
-        self.mAttributes = {}
-        for attr in copyit.mAttributes.values():
-            self.mAttributes.setdefault(attr.getName(), []).append(PyCalendarAttribute(copyit=attr))
-        self.mValue = copyit.mValue.copy()
+        os.write("\r\n")
 
-    @staticmethod
-    def _init_map():
-        # Only if empty
-        if PyCalendarProperty.sDefaultValueTypeMap is None:
-            PyCalendarProperty.sDefaultValueTypeMap = {}
 
-            # 2445 ?4.8.1
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_ATTACH] = PyCalendarValue.VALUETYPE_BINARY
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_CATEGORIES] = PyCalendarValue.VALUETYPE_TEXT
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_CLASS] = PyCalendarValue.VALUETYPE_TEXT
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_COMMENT] = PyCalendarValue.VALUETYPE_TEXT
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_DESCRIPTION] = PyCalendarValue.VALUETYPE_TEXT
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_GEO] = PyCalendarValue.VALUETYPE_GEO
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_LOCATION] = PyCalendarValue.VALUETYPE_TEXT
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_PERCENT_COMPLETE] = PyCalendarValue.VALUETYPE_INTEGER
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_PRIORITY] = PyCalendarValue.VALUETYPE_INTEGER
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_RESOURCES] = PyCalendarValue.VALUETYPE_TEXT
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_STATUS] = PyCalendarValue.VALUETYPE_TEXT
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_SUMMARY] = PyCalendarValue.VALUETYPE_TEXT
+    def writeXML(self, node, namespace):
 
-            # 2445 ?4.8.2
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_COMPLETED] = PyCalendarValue.VALUETYPE_DATETIME
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_DTEND] = PyCalendarValue.VALUETYPE_DATETIME
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_DUE] = PyCalendarValue.VALUETYPE_DATETIME
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_DTSTART] = PyCalendarValue.VALUETYPE_DATETIME
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_DURATION] = PyCalendarValue.VALUETYPE_DURATION
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_FREEBUSY] = PyCalendarValue.VALUETYPE_PERIOD
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_TRANSP] = PyCalendarValue.VALUETYPE_TEXT
+        # Write it out always with value
+        self.generateValueXML(node, namespace, False)
 
-            # 2445 ?4.8.3
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_TZID] = PyCalendarValue.VALUETYPE_TEXT
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_TZNAME] = PyCalendarValue.VALUETYPE_TEXT
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_TZOFFSETFROM] = PyCalendarValue.VALUETYPE_UTC_OFFSET
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_TZOFFSETTO] = PyCalendarValue.VALUETYPE_UTC_OFFSET
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_TZURL] = PyCalendarValue.VALUETYPE_URI
 
-            # 2445 ?4.8.4
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_ATTENDEE] = PyCalendarValue.VALUETYPE_CALADDRESS
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_CONTACT] = PyCalendarValue.VALUETYPE_TEXT
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_ORGANIZER] = PyCalendarValue.VALUETYPE_CALADDRESS
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_RECURRENCE_ID] = PyCalendarValue.VALUETYPE_DATETIME
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_RELATED_TO] = PyCalendarValue.VALUETYPE_TEXT
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_URL] = PyCalendarValue.VALUETYPE_URI
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_UID] = PyCalendarValue.VALUETYPE_TEXT
+    def generateFilteredXML(self, node, namespace, filter):
 
-            # 2445 ?4.8.5
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_EXDATE] = PyCalendarValue.VALUETYPE_DATETIME
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_EXRULE] = PyCalendarValue.VALUETYPE_RECUR
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_RDATE] = PyCalendarValue.VALUETYPE_DATETIME
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_RRULE] = PyCalendarValue.VALUETYPE_RECUR
+        # Check for property in filter and whether value is written out
+        test, novalue = filter.testPropertyValue(self.mName.upper())
+        if test:
+            self.generateValueXML(node, namespace, novalue)
 
-            # 2445 ?4.8.6
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_ACTION] = PyCalendarValue.VALUETYPE_TEXT
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_REPEAT] = PyCalendarValue.VALUETYPE_INTEGER
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_TRIGGER] = PyCalendarValue.VALUETYPE_DURATION
 
-            # 2445 ?4.8.7
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_CREATED] = PyCalendarValue.VALUETYPE_DATETIME
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_DTSTAMP] = PyCalendarValue.VALUETYPE_DATETIME
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_LAST_MODIFIED] = PyCalendarValue.VALUETYPE_DATETIME
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_SEQUENCE] = PyCalendarValue.VALUETYPE_INTEGER
+    # Write out the actual property, possibly skipping the value
+    def generateValueXML(self, node, namespace, novalue):
 
-            # Apple Extensions
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_XWRCALNAME] = PyCalendarValue.VALUETYPE_TEXT
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_XWRCALDESC] = PyCalendarValue.VALUETYPE_TEXT
+        prop = XML.SubElement(node, xmldefs.makeTag(namespace, self.getName()))
 
-            # Mulberry extensions
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_X_PRIVATE_RURL] = PyCalendarValue.VALUETYPE_TEXT
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_X_PRIVATE_ETAG] = PyCalendarValue.VALUETYPE_TEXT
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_ACTION_X_SPEAKTEXT] = PyCalendarValue.VALUETYPE_TEXT
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_ALARM_X_LASTTRIGGER] = PyCalendarValue.VALUETYPE_DATETIME
-            PyCalendarProperty.sDefaultValueTypeMap[definitions.cICalProperty_ALARM_X_ALARMSTATUS] = PyCalendarValue.VALUETYPE_TEXT
+        # Write all attributes
+        if len(self.mAttributes):
+            params = XML.SubElement(prop, xmldefs.makeTag(namespace, xmldefs.parameters))
+            for key in sorted(self.mAttributes.keys()):
+                for attr in self.mAttributes[key]:
+                    if attr.getName() != "VALUE":
+                        attr.writeXML(params, namespace)
 
-        # Only if empty
-        if PyCalendarProperty.sValueTypeMap is None:
-            PyCalendarProperty.sValueTypeMap = {}
+        # Write value
+        if self.mValue and not novalue:
+            self.mValue.writeXML(prop, namespace)
 
-            # 2445 ?4.3
-            PyCalendarProperty.sValueTypeMap[definitions.cICalValue_BINARY] = PyCalendarValue.VALUETYPE_BINARY
-            PyCalendarProperty.sValueTypeMap[definitions.cICalValue_BOOLEAN] = PyCalendarValue.VALUETYPE_BOOLEAN
-            PyCalendarProperty.sValueTypeMap[definitions.cICalValue_CAL_ADDRESS] = PyCalendarValue.VALUETYPE_CALADDRESS
-            PyCalendarProperty.sValueTypeMap[definitions.cICalValue_DATE] = PyCalendarValue.VALUETYPE_DATE
-            PyCalendarProperty.sValueTypeMap[definitions.cICalValue_DATE_TIME] = PyCalendarValue.VALUETYPE_DATETIME
-            PyCalendarProperty.sValueTypeMap[definitions.cICalValue_DURATION] = PyCalendarValue.VALUETYPE_DURATION
-            PyCalendarProperty.sValueTypeMap[definitions.cICalValue_FLOAT] = PyCalendarValue.VALUETYPE_FLOAT
-            PyCalendarProperty.sValueTypeMap[definitions.cICalValue_INTEGER] = PyCalendarValue.VALUETYPE_INTEGER
-            PyCalendarProperty.sValueTypeMap[definitions.cICalValue_PERIOD] = PyCalendarValue.VALUETYPE_PERIOD
-            PyCalendarProperty.sValueTypeMap[definitions.cICalValue_RECUR] = PyCalendarValue.VALUETYPE_RECUR
-            PyCalendarProperty.sValueTypeMap[definitions.cICalValue_TEXT] = PyCalendarValue.VALUETYPE_TEXT
-            PyCalendarProperty.sValueTypeMap[definitions.cICalValue_TIME] = PyCalendarValue.VALUETYPE_TIME
-            PyCalendarProperty.sValueTypeMap[definitions.cICalValue_URI] = PyCalendarValue.VALUETYPE_URI
-            PyCalendarProperty.sValueTypeMap[definitions.cICalValue_UTC_OFFSET] = PyCalendarValue.VALUETYPE_UTC_OFFSET
 
-        if PyCalendarProperty.sTypeValueMap is None:
-            PyCalendarProperty.sTypeValueMap = {}
+    def _init_PyCalendarProperty(self):
+        self.mName = ""
+        self.mAttributes = {}
+        self.mValue = None
 
-            # 2445 ?4.3
-            PyCalendarProperty.sTypeValueMap[PyCalendarValue.VALUETYPE_BINARY] = definitions.cICalValue_BINARY
-            PyCalendarProperty.sTypeValueMap[PyCalendarValue.VALUETYPE_BOOLEAN] = definitions.cICalValue_BOOLEAN
-            PyCalendarProperty.sTypeValueMap[PyCalendarValue.VALUETYPE_CALADDRESS] = definitions.cICalValue_CAL_ADDRESS
-            PyCalendarProperty.sTypeValueMap[PyCalendarValue.VALUETYPE_DATE] = definitions.cICalValue_DATE
-            PyCalendarProperty.sTypeValueMap[PyCalendarValue.VALUETYPE_DATETIME] = definitions.cICalValue_DATE_TIME
-            PyCalendarProperty.sTypeValueMap[PyCalendarValue.VALUETYPE_DURATION] = definitions.cICalValue_DURATION
-            PyCalendarProperty.sTypeValueMap[PyCalendarValue.VALUETYPE_FLOAT] = definitions.cICalValue_FLOAT
-            PyCalendarProperty.sTypeValueMap[PyCalendarValue.VALUETYPE_GEO] = definitions.cICalValue_FLOAT
-            PyCalendarProperty.sTypeValueMap[PyCalendarValue.VALUETYPE_INTEGER] = definitions.cICalValue_INTEGER
-            PyCalendarProperty.sTypeValueMap[PyCalendarValue.VALUETYPE_PERIOD] = definitions.cICalValue_PERIOD
-            PyCalendarProperty.sTypeValueMap[PyCalendarValue.VALUETYPE_RECUR] = definitions.cICalValue_RECUR
-            PyCalendarProperty.sTypeValueMap[PyCalendarValue.VALUETYPE_TEXT] = definitions.cICalValue_TEXT
-            PyCalendarProperty.sTypeValueMap[PyCalendarValue.VALUETYPE_TIME] = definitions.cICalValue_TIME
-            PyCalendarProperty.sTypeValueMap[PyCalendarValue.VALUETYPE_URI] = definitions.cICalValue_URI
-            PyCalendarProperty.sTypeValueMap[PyCalendarValue.VALUETYPE_UTC_OFFSET] = definitions.cICalValue_UTC_OFFSET
 
-        if PyCalendarProperty.sMultiValues is None:
-            PyCalendarProperty.sMultiValues = set()
-
-            PyCalendarProperty.sMultiValues.add(definitions.cICalProperty_CATEGORIES)
-            PyCalendarProperty.sMultiValues.add(definitions.cICalProperty_RESOURCES)
-            PyCalendarProperty.sMultiValues.add(definitions.cICalProperty_FREEBUSY)
-            PyCalendarProperty.sMultiValues.add(definitions.cICalProperty_EXDATE)
-            PyCalendarProperty.sMultiValues.add(definitions.cICalProperty_RDATE)
-
     def createValue(self, data):
         # Tidy first
         self.mValue = None
 
         # Get value type from property name
-        type = PyCalendarProperty.sDefaultValueTypeMap.get(self.mName, PyCalendarValue.VALUETYPE_TEXT)
+        type = PyCalendarProperty.sDefaultValueTypeMap.get(self.mName.upper(), PyCalendarValue.VALUETYPE_UNKNOWN)
 
         # Check whether custom value is set
-        if self.mAttributes.has_key(definitions.cICalAttribute_VALUE):
+        if definitions.cICalAttribute_VALUE in self.mAttributes:
             type = PyCalendarProperty.sValueTypeMap.get(self.getAttributeValue(definitions.cICalAttribute_VALUE), type)
 
         # Check for multivalued
-        if self.mName in PyCalendarProperty.sMultiValues:
+        if self.mName.upper() in PyCalendarProperty.sMultiValues:
             self.mValue = PyCalendarMultiValue(type)
         else:
             # Create the type
             self.mValue = PyCalendarValue.createFromType(type)
 
         # Now parse the data
-        self.mValue.parse(data)
+        try:
+            self.mValue.parse(data)
+        except ValueError:
+            raise PyCalendarInvalidProperty("Invalid property value", data)
 
         # Special post-create for some types
         if type in (PyCalendarValue.VALUETYPE_TIME, PyCalendarValue.VALUETYPE_DATETIME):
@@ -471,7 +583,7 @@
             tzid = None
             if (self.hasAttribute(definitions.cICalAttribute_TZID)):
                 tzid = self.getAttributeValue(definitions.cICalAttribute_TZID)
-                
+
                 if isinstance(self.mValue, PyCalendarDateTimeValue):
                     self.mValue.getValue().setTimezoneID(tzid)
                 elif isinstance(self.mValue, PyCalendarMultiValue):
@@ -479,23 +591,60 @@
                         if isinstance(item, PyCalendarDateTimeValue):
                             item.getValue().setTimezoneID(tzid)
 
+
+    def setValue(self, value):
+        # Tidy first
+        self.mValue = None
+
+        # Get value type from property name
+        type = PyCalendarProperty.sDefaultValueTypeMap.get(self.mName.upper(), PyCalendarValue.VALUETYPE_TEXT)
+
+        # Check whether custom value is set
+        if definitions.cICalAttribute_VALUE in self.mAttributes:
+            type = PyCalendarProperty.sValueTypeMap.get(self.getAttributeValue(definitions.cICalAttribute_VALUE), type)
+
+        # Check for multivalued
+        if self.mName.upper() in PyCalendarProperty.sMultiValues:
+            self.mValue = PyCalendarMultiValue(type)
+        else:
+            # Create the type
+            self.mValue = PyCalendarValue.createFromType(type)
+
+        self.mValue.setValue(value)
+
+        # Special post-create for some types
+        if type in (PyCalendarValue.VALUETYPE_TIME, PyCalendarValue.VALUETYPE_DATETIME):
+            # Look for TZID attribute
+            tzid = None
+            if (self.hasAttribute(definitions.cICalAttribute_TZID)):
+                tzid = self.getAttributeValue(definitions.cICalAttribute_TZID)
+
+                if isinstance(self.mValue, PyCalendarDateTimeValue):
+                    self.mValue.getValue().setTimezoneID(tzid)
+                elif isinstance(self.mValue, PyCalendarMultiValue):
+                    for item in self.mValue.getValues():
+                        if isinstance(item, PyCalendarDateTimeValue):
+                            item.getValue().setTimezoneID(tzid)
+
+
     def setupValueAttribute(self):
-        if self.mAttributes.has_key(definitions.cICalAttribute_VALUE):
+        if definitions.cICalAttribute_VALUE in self.mAttributes:
             del self.mAttributes[definitions.cICalAttribute_VALUE]
 
         # Only if we have a value right now
         if self.mValue is None:
             return
 
-        # See if current type is default for this property
-        found = self.sDefaultValueTypeMap.get(self.mName, None)
-        if found is not None:
-            default_type = found
-            if default_type != self.mValue.getType():
-                found2 = self.sTypeValueMap.get(self.mValue.getType(), None)
-                if found2 is not None:
-                    self.mAttributes.setdefault(definitions.cICalAttribute_VALUE, []).append(PyCalendarAttribute(name=definitions.cICalAttribute_VALUE, value=found2))
+        # See if current type is default for this property. If there is no mapping available,
+        # then always add VALUE if it is not TEXT.
+        default_type = self.sDefaultValueTypeMap.get(self.mName.upper())
+        actual_type = self.mValue.getType()
+        if default_type is None or default_type != actual_type:
+            actual_value = self.sTypeValueMap.get(actual_type)
+            if actual_value is not None and (default_type is not None or actual_type != PyCalendarValue.VALUETYPE_TEXT):
+                self.mAttributes.setdefault(definitions.cICalAttribute_VALUE, []).append(PyCalendarAttribute(name=definitions.cICalAttribute_VALUE, value=actual_value))
 
+
     # Creation
     def _init_attr_value_int(self, ival):
         # Value
@@ -508,12 +657,21 @@
     def _init_attr_value_text(self, txt, value_type):
         # Value
         self.mValue = PyCalendarValue.createFromType(value_type)
-        if isinstance(self.mValue, PyCalendarPlainTextValue):
+        if isinstance(self.mValue, PyCalendarPlainTextValue) or isinstance(self.mValue, PyCalendarUnknownValue):
             self.mValue.setValue(txt)
 
         # Attributes
         self.setupValueAttribute()
 
+
+    def _init_attr_value_requeststatus(self, reqstatus):
+        # Value
+        self.mValue = PyCalendarRequestStatusValue(reqstatus)
+
+        # Attributes
+        self.setupValueAttribute()
+
+
     def _init_attr_value_datetime(self, dt):
         # Value
         self.mValue = PyCalendarDateTimeValue(value=dt)
@@ -523,11 +681,12 @@
 
         # Look for timezone
         if not dt.isDateOnly() and dt.local():
-            if self.mAttributes.has_key(definitions.cICalAttribute_TZID):
+            if definitions.cICalAttribute_TZID in self.mAttributes:
                 del self.mAttributes[definitions.cICalAttribute_TZID]
             self.mAttributes.setdefault(definitions.cICalAttribute_TZID, []).append(
                     PyCalendarAttribute(name=definitions.cICalAttribute_TZID, value=dt.getTimezoneID()))
-    
+
+
     def _init_attr_value_datetimelist(self, dtl):
         # Value
         date_only = (len(dtl) > 0) and dtl[0].isDateOnly()
@@ -545,12 +704,23 @@
         # Look for timezone
         if ((len(dtl) > 0)
                 and not dtl[0].isDateOnly()
-                and not dtl[0].floating()):
-            if self.mAttributes.has_key(definitions.cICalAttribute_TZID):
+                and dtl[0].local()):
+            if definitions.cICalAttribute_TZID in self.mAttributes:
                 del self.mAttributes[definitions.cICalAttribute_TZID]
             self.mAttributes.setdefault(definitions.cICalAttribute_TZID, []).append(
                     PyCalendarAttribute(name=definitions.cICalAttribute_TZID, value=dtl[0].getTimezoneID()))
 
+
+    def _init_attr_value_periodlist(self, periodlist):
+        # Value
+        self.mValue = PyCalendarMultiValue(PyCalendarValue.VALUETYPE_PERIOD)
+        for period in periodlist:
+            self.mValue.addValue(PyCalendarPeriodValue(period))
+
+        # Attributes
+        self.setupValueAttribute()
+
+
     def _init_attr_value_duration(self, du):
         # Value
         self.mValue = PyCalendarDurationValue(value=du)
@@ -574,6 +744,7 @@
         # Attributes
         self.setupValueAttribute()
 
+
     def _init_attr_value_utcoffset(self, utcoffset):
         # Value
         self.mValue = PyCalendarUTCOffsetValue()
@@ -581,19 +752,3 @@
 
         # Attributes
         self.setupValueAttribute()
-
-PyCalendarProperty.loadStatics()
-
-if __name__ == '__main__':
-    prop = PyCalendarProperty()
-    prop.parse("DTSTART;TZID=\"US/Eastern\":20060226T120000")
-    io = StringIO.StringIO()
-    prop.generate(io)
-    print io.getvalue()
-    
-    prop = PyCalendarProperty(definitions.cICalProperty_DTSTAMP,
-                              PyCalendarDateTime.getNowUTC())
-    prop.generate(io)
-    print io.getvalue()
-
-    
\ No newline at end of file

Modified: PyCalendar/trunk/src/pycalendar/recurrence.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/recurrence.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/recurrence.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,13 +14,14 @@
 #    limitations under the License.
 ##
 
+from pycalendar import definitions
+from pycalendar import xmldefs
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.period import PyCalendarPeriod
+from pycalendar.valueutils import ValueMixin
 import cStringIO as StringIO
+import xml.etree.cElementTree as XML
 
-from datetime import PyCalendarDateTime
-from period import PyCalendarPeriod
-import definitions
-import stringutils
-
 def WeekDayNumCompare_compare(w1, w2):
 
     if w1[0] < w2[0]:
@@ -33,58 +34,116 @@
         return 1
     else:
         return 0
-        
+
+
+
 def WeekDayNumSort_less_than(w1, w2):
 
     return (w1[0] < w2[0]) or (w1[0] == w2[0]) and (w1[1] < w2[1])
 
-class PyCalendarRecurrence(object):
 
-    cFreqMap = [
-                definitions.cICalValue_RECUR_SECONDLY,
-                definitions.cICalValue_RECUR_MINUTELY,
-                definitions.cICalValue_RECUR_HOURLY,
-                definitions.cICalValue_RECUR_DAILY,
-                definitions.cICalValue_RECUR_WEEKLY,
-                definitions.cICalValue_RECUR_MONTHLY,
-                definitions.cICalValue_RECUR_YEARLY,
-                0]
-    
-    cRecurMap = [
-                definitions.cICalValue_RECUR_UNTIL,
-                definitions.cICalValue_RECUR_COUNT,
-                definitions.cICalValue_RECUR_INTERVAL,
-                definitions.cICalValue_RECUR_BYSECOND,
-                definitions.cICalValue_RECUR_BYMINUTE,
-                definitions.cICalValue_RECUR_BYHOUR,
-                definitions.cICalValue_RECUR_BYDAY,
-                definitions.cICalValue_RECUR_BYMONTHDAY,
-                definitions.cICalValue_RECUR_BYYEARDAY,
-                definitions.cICalValue_RECUR_BYWEEKNO,
-                definitions.cICalValue_RECUR_BYMONTH,
-                definitions.cICalValue_RECUR_BYSETPOS,
-                definitions.cICalValue_RECUR_WKST,
-                0]
-    
-    cWeekdayMap = [
-                definitions.cICalValue_RECUR_WEEKDAY_SU,
-                definitions.cICalValue_RECUR_WEEKDAY_MO,
-                definitions.cICalValue_RECUR_WEEKDAY_TU,
-                definitions.cICalValue_RECUR_WEEKDAY_WE,
-                definitions.cICalValue_RECUR_WEEKDAY_TH,
-                definitions.cICalValue_RECUR_WEEKDAY_FR,
-                definitions.cICalValue_RECUR_WEEKDAY_SA,
-                0]
-                
+
+class PyCalendarRecurrence(ValueMixin):
+
+    cFreqMap = {
+        definitions.cICalValue_RECUR_SECONDLY : definitions.eRecurrence_SECONDLY,
+        definitions.cICalValue_RECUR_MINUTELY : definitions.eRecurrence_MINUTELY,
+        definitions.cICalValue_RECUR_HOURLY   : definitions.eRecurrence_HOURLY,
+        definitions.cICalValue_RECUR_DAILY    : definitions.eRecurrence_DAILY,
+        definitions.cICalValue_RECUR_WEEKLY   : definitions.eRecurrence_WEEKLY,
+        definitions.cICalValue_RECUR_MONTHLY  : definitions.eRecurrence_MONTHLY,
+        definitions.cICalValue_RECUR_YEARLY   : definitions.eRecurrence_YEARLY,
+    }
+
+    cFreqToXMLMap = {
+        definitions.eRecurrence_SECONDLY: xmldefs.recur_freq_secondly,
+        definitions.eRecurrence_MINUTELY: xmldefs.recur_freq_minutely,
+        definitions.eRecurrence_HOURLY: xmldefs.recur_freq_hourly,
+        definitions.eRecurrence_DAILY: xmldefs.recur_freq_daily,
+        definitions.eRecurrence_WEEKLY: xmldefs.recur_freq_weekly,
+        definitions.eRecurrence_MONTHLY: xmldefs.recur_freq_monthly,
+        definitions.eRecurrence_YEARLY: xmldefs.recur_freq_yearly,
+    }
+
+    cRecurMap = {
+        definitions.cICalValue_RECUR_FREQ       : definitions.eRecurrence_FREQ,
+        definitions.cICalValue_RECUR_UNTIL      : definitions.eRecurrence_UNTIL,
+        definitions.cICalValue_RECUR_COUNT      : definitions.eRecurrence_COUNT,
+        definitions.cICalValue_RECUR_INTERVAL   : definitions.eRecurrence_INTERVAL,
+        definitions.cICalValue_RECUR_BYSECOND   : definitions.eRecurrence_BYSECOND,
+        definitions.cICalValue_RECUR_BYMINUTE   : definitions.eRecurrence_BYMINUTE,
+        definitions.cICalValue_RECUR_BYHOUR     : definitions.eRecurrence_BYHOUR,
+        definitions.cICalValue_RECUR_BYDAY      : definitions.eRecurrence_BYDAY,
+        definitions.cICalValue_RECUR_BYMONTHDAY : definitions.eRecurrence_BYMONTHDAY,
+        definitions.cICalValue_RECUR_BYYEARDAY  : definitions.eRecurrence_BYYEARDAY,
+        definitions.cICalValue_RECUR_BYWEEKNO   : definitions.eRecurrence_BYWEEKNO,
+        definitions.cICalValue_RECUR_BYMONTH    : definitions.eRecurrence_BYMONTH,
+        definitions.cICalValue_RECUR_BYSETPOS   : definitions.eRecurrence_BYSETPOS,
+        definitions.cICalValue_RECUR_WKST       : definitions.eRecurrence_WKST,
+    }
+
+    cWeekdayMap = {
+        definitions.cICalValue_RECUR_WEEKDAY_SU : definitions.eRecurrence_WEEKDAY_SU,
+        definitions.cICalValue_RECUR_WEEKDAY_MO : definitions.eRecurrence_WEEKDAY_MO,
+        definitions.cICalValue_RECUR_WEEKDAY_TU : definitions.eRecurrence_WEEKDAY_TU,
+        definitions.cICalValue_RECUR_WEEKDAY_WE : definitions.eRecurrence_WEEKDAY_WE,
+        definitions.cICalValue_RECUR_WEEKDAY_TH : definitions.eRecurrence_WEEKDAY_TH,
+        definitions.cICalValue_RECUR_WEEKDAY_FR : definitions.eRecurrence_WEEKDAY_FR,
+        definitions.cICalValue_RECUR_WEEKDAY_SA : definitions.eRecurrence_WEEKDAY_SA,
+    }
+
+    cWeekdayRecurMap = dict([(v, k) for k, v in cWeekdayMap.items()])
+
     cUnknownIndex = -1
 
-    def __init__(self, arg = None):
-        
-        if isinstance(arg, PyCalendarRecurrence):
-            self.copy_PyCalendarRecurrence(arg)
-        else:
-            self.init_PyCalendarRecurrence()
+    def __init__(self):
+        self.init_PyCalendarRecurrence()
 
+
+    def duplicate(self):
+        other = PyCalendarRecurrence()
+
+        other.mFreq = self.mFreq
+
+        other.mUseCount = self.mUseCount
+        other.mCount = self.mCount
+        other.mUseUntil = self.mUseUntil
+        if other.mUseUntil:
+            other.mUntil = self.mUntil.duplicate()
+
+        other.mInterval = self.mInterval
+        if self.mBySeconds is not None:
+            other.mBySeconds = self.mBySeconds[:]
+        if self.mByMinutes is not None:
+            other.mByMinutes = self.mByMinutes[:]
+        if self.mByHours is not None:
+            other.mByHours = self.mByHours[:]
+        if self.mByDay is not None:
+            other.mByDay = self.mByDay[:]
+        if self.mByMonthDay is not None:
+            other.mByMonthDay = self.mByMonthDay[:]
+        if self.mByYearDay is not None:
+            other.mByYearDay = self.mByYearDay[:]
+        if self.mByWeekNo is not None:
+            other.mByWeekNo = self.mByWeekNo[:]
+        if self.mByMonth is not None:
+            other.mByMonth = self.mByMonth[:]
+        if self.mBySetPos is not None:
+            other.mBySetPos = self.mBySetPos[:]
+        other.mWeekstart = self.mWeekstart
+
+        other.mCached = self.mCached
+        if self.mCacheStart:
+            other.mCacheStart = self.mCacheStart.duplicate()
+        if self.mCacheUpto:
+            other.mCacheUpto = self.mCacheUpto.duplicate()
+        other.mFullyCached = self.mFullyCached
+        if self.mRecurrences:
+            other.mRecurrences = self.mRecurrences[:]
+
+        return other
+
+
     def init_PyCalendarRecurrence(self):
         self.mFreq = definitions.eRecurrence_YEARLY
 
@@ -92,18 +151,18 @@
         self.mCount = 0
 
         self.mUseUntil = False
-        self.mUntil = 0
+        self.mUntil = None
 
         self.mInterval = 1
-        self.mBySeconds = 0
-        self.mByMinutes = 0
-        self.mByHours = 0
-        self.mByDay = 0
-        self.mByMonthDay = 0
-        self.mByYearDay = 0
-        self.mByWeekNo = 0
-        self.mByMonth = 0
-        self.mBySetPos = 0
+        self.mBySeconds = None
+        self.mByMinutes = None
+        self.mByHours = None
+        self.mByDay = None
+        self.mByMonthDay = None
+        self.mByYearDay = None
+        self.mByWeekNo = None
+        self.mByMonth = None
+        self.mBySetPos = None
         self.mWeekstart = definitions.eRecurrence_WEEKDAY_MO
 
         self.mCached = False
@@ -112,55 +171,41 @@
         self.mFullyCached = False
         self.mRecurrences = None
 
-    def copy_PyCalendarRecurrence(self, copy):
-        self.init_PyCalendarRecurrence()
 
-        self.mFreq = copy.mFreq
+    def __hash__(self):
+        return hash((
+            self.mFreq,
+            self.mUseCount,
+            self.mCount,
+            self.mUseUntil,
+            self.mUntil,
+            self.mInterval,
+            tuple(self.mBySeconds) if self.mBySeconds else None,
+            tuple(self.mByMinutes) if self.mByMinutes else None,
+            tuple(self.mByHours) if self.mByHours else None,
+            tuple(self.mByDay) if self.mByDay else None,
+            tuple(self.mByMonthDay) if self.mByMonthDay else None,
+            tuple(self.mByYearDay) if self.mByYearDay else None,
+            tuple(self.mByWeekNo) if self.mByWeekNo else None,
+            tuple(self.mByMonth) if self.mByMonth else None,
+            tuple(self.mBySetPos) if self.mBySetPos else None,
+            self.mWeekstart,
+        ))
 
-        self.mUseCount = copy.mUseCount
-        self.mCount = copy.mCount
-        self.mUseUntil = copy.mUseUntil
-        if self.mUseUntil:
-            self.mUntil = PyCalendarDateTime(copyit=copy.mUntil)
 
-        self.mInterval = copy.mInterval
-        if copy.mBySeconds != 0:
-            self.mBySeconds = copy.mBySeconds[:]
-        if copy.mByMinutes != 0:
-            self.mByMinutes = copy.mByMinutes[:]
-        if copy.mByHours != 0:
-            self.mByHours = copy.mByHours[:]
-        if copy.mByDay != 0:
-            self.mByDay = copy.mByDay[:]
-        if copy.mByMonthDay != 0:
-            self.mByMonthDay = copy.mByMonthDay[:]
-        if copy.mByYearDay != 0:
-            self.mByYearDay = copy.mByYearDay[:]
-        if copy.mByWeekNo != 0:
-            self.mByWeekNo = copy.mByWeekNo[:]
-        if copy.mByMonth != 0:
-            self.mByMonth = copy.mByMonth[:]
-        if copy.mBySetPos != 0:
-            self.mBySetPos = copy.mBySetPos[:]
-        self.mWeekstart = copy.mWeekstart
+    def __ne__(self, other):
+        return not self.__eq__(other)
 
-        self.mCached = copy.mCached
-        if copy.mCacheStart:
-            self.mCacheStart = PyCalendarDateTime(copyit=copy.mCacheStart)
-        else:
-            self.mCacheStart = None
-        if copy.mCacheUpto:
-            self.mCacheUpto = PyCalendarDateTime(copyit=copy.mCacheUpto)
-        else:
-            self.mCacheUpto = None
-        self.mFullyCached = copy.mFullyCached
-        if copy.mRecurrences:
-            self.mRecurrences = copy.mRecurrences[:]
-        else:
-            self.mRecurrences = None
 
+    def __eq__(self, other):
+        if not isinstance(other, PyCalendarRecurrence):
+            return False
+        return self.equals(other)
+
+
     def equals(self, comp):
-        return (self.mFreq == comp.mFreq) and (self.mCount == comp.mCount) \
+        return (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.equalsNum(self.mBySeconds, comp.mBySeconds) \
@@ -174,8 +219,13 @@
                 and self.equalsNum(self.mBySetPos, comp.mBySetPos) \
                 and (self.mWeekstart == comp.mWeekstart)
 
+
     def equalsNum(self, items1, items2):
         # Check sizes first
+        if items1 is None:
+            items1 = []
+        if items2 is None:
+            items2 = []
         if len(items1) != len(items2):
             return False
         elif len(items1) == 0:
@@ -192,8 +242,13 @@
                 return False
         return True
 
+
     def equalsDayNum(self, items1, items2):
         # Check sizes first
+        if items1 is None:
+            items1 = []
+        if items2 is None:
+            items2 = []
         if len(items1) != len(items2):
             return False
         elif len(items1) == 0:
@@ -210,57 +265,83 @@
                 return False
         return True
 
+
     def getFreq(self):
         return self.mFreq
 
+
     def setFreq(self, freq):
         self.mFreq = freq
 
+
     def getUseUntil(self):
         return self.mUseUntil
 
+
     def setUseUntil(self, use_until):
         self.mUseUntil = use_until
 
+
     def getUntil(self):
         return self.mUntil
 
+
     def setUntil(self, until):
         self.mUntil = until
 
+
     def getUseCount(self):
         return self.mUseCount
 
+
     def setUseCount(self, use_count):
         self.mUseCount = use_count
 
+
     def getCount(self):
         return self.mCount
 
+
     def setCount(self, count):
         self.mCount = count
 
+
     def getInterval(self):
         return self.mInterval
 
+
     def setInterval(self, interval):
         self.mInterval = interval
 
+
     def getByMonth(self):
         return self.mByMonth
 
+
     def setByMonth(self, by):
         self.mByMonth = by[:]
 
+
     def getByMonthDay(self):
         return self.mByMonthDay
 
+
     def setByMonthDay(self, by):
         self.mByMonthDay = by[:]
 
+
+    def getByYearDay(self):
+        return self.mByYearDay
+
+
+    def setByYearDay(self, by):
+        self.mByYearDay = by[:]
+
+
     def getByDay(self):
         return self.mByDay
 
+
     def setByDay(self, by):
         self.mByDay = by[:]
 
@@ -268,9 +349,11 @@
     def getBySetPos(self):
         return self.mBySetPos
 
+
     def setBySetPos(self, by):
         self.mBySetPos = by
 
+
     def parse(self, data):
         self.init_PyCalendarRecurrence()
 
@@ -278,133 +361,146 @@
         tokens = data.split(";")
         tokens.reverse()
 
-        # Look for FREQ= with delimiter
-           
         if len(tokens) == 0:
-            return
-        token = tokens.pop()
+            raise ValueError("PyCalendarRecurrence: Invalid recurrence rule value")
 
-        # Make sure it is the token we expect
-        if not token.startswith(definitions.cICalValue_RECUR_FREQ):
-            return
-        q = token[definitions.cICalValue_RECUR_FREQ_LEN:]
-
-        # Get the FREQ value
-        index = stringutils.strindexfind(q, PyCalendarRecurrence.cFreqMap, PyCalendarRecurrence.cUnknownIndex)
-        if index == PyCalendarRecurrence.cUnknownIndex:
-            return
-        self.mFreq = index
-    
-
         while len(tokens) != 0:
             # Get next token
             token = tokens.pop()
+            try:
+                tname, tvalue = token.split("=")
+            except ValueError:
+                raise ValueError("PyCalendarRecurrence: Invalid token '%s'" % (token,))
 
             # Determine token type
-            index = stringutils.strnindexfind(token, PyCalendarRecurrence.cRecurMap, PyCalendarRecurrence.cUnknownIndex)
+            index = PyCalendarRecurrence.cRecurMap.get(tname, PyCalendarRecurrence.cUnknownIndex)
             if index == PyCalendarRecurrence.cUnknownIndex:
-                return
+                raise ValueError("PyCalendarRecurrence: Invalid token '%s'" % (tname,))
 
             # Parse remainder based on index
-            q = token[token.find('=') + 1:]
+            if index == definitions.eRecurrence_FREQ:
+                # Get the FREQ value
+                index = PyCalendarRecurrence.cFreqMap.get(tvalue, PyCalendarRecurrence.cUnknownIndex)
+                if index == PyCalendarRecurrence.cUnknownIndex:
+                    raise ValueError("PyCalendarRecurrence: Invalid FREQ value")
+                self.mFreq = index
 
-            if index == 0: # UNTIL
+            elif index == definitions.eRecurrence_UNTIL:
                 if self.mUseCount:
-                    return
+                    raise ValueError("PyCalendarRecurrence: Can't have both UNTIL and COUNT")
                 self.mUseUntil = True
-                if self.mUntil == 0:
+                if self.mUntil is None:
                     self.mUntil = PyCalendarDateTime()
-                self.mUntil.parse(q)
+                try:
+                    self.mUntil.parse(tvalue)
+                except ValueError:
+                    raise ValueError("PyCalendarRecurrence: Invalid UNTIL value")
 
-            elif index == 1: # COUNT
+            elif index == definitions.eRecurrence_COUNT:
                 if self.mUseUntil:
-                    return
+                    raise ValueError("PyCalendarRecurrence: Can't have both UNTIL and COUNT")
                 self.mUseCount = True
-                self.mCount = int(q)
+                try:
+                    self.mCount = int(tvalue)
+                except ValueError:
+                    raise ValueError("PyCalendarRecurrence: Invalid COUNT value")
 
                 # Must not be less than one
                 if self.mCount < 1:
-                    self.mCount = 1
+                    raise ValueError("PyCalendarRecurrence: Invalid COUNT value")
 
-            elif index == 2: # INTERVAL
-                self.mInterval = int(q)
+            elif index == definitions.eRecurrence_INTERVAL:
+                try:
+                    self.mInterval = int(tvalue)
+                except ValueError:
+                    raise ValueError("PyCalendarRecurrence: Invalid INTERVAL value")
 
                 # Must NOT be less than one
                 if self.mInterval < 1:
-                    self.mInterval = 1
- 
-            elif index == 3: # BYSECOND
-                if self.mBySeconds != 0:
-                    return
+                    raise ValueError("PyCalendarRecurrence: Invalid INTERVAL value")
+
+            elif index == definitions.eRecurrence_BYSECOND:
+                if self.mBySeconds is not None:
+                    raise ValueError("PyCalendarRecurrence: Only one BYSECOND allowed")
                 self.mBySeconds = []
-                self.parseList(q, self.mBySeconds)
+                self.parseList(tvalue, self.mBySeconds, 0, 60, errmsg="PyCalendarRecurrence: Invalid BYSECOND value")
 
-            elif index == 4: # BYMINUTE
-                if self.mByMinutes != 0:
-                    return
+            elif index == definitions.eRecurrence_BYMINUTE:
+                if self.mByMinutes is not None:
+                    raise ValueError("PyCalendarRecurrence: Only one BYMINUTE allowed")
                 self.mByMinutes = []
-                self.parseList(q, self.mByMinutes)
+                self.parseList(tvalue, self.mByMinutes, 0, 59, errmsg="PyCalendarRecurrence: Invalid BYMINUTE value")
 
-            elif index == 5: # BYHOUR
-                if self.mByHours != 0:
-                    return
+            elif index == definitions.eRecurrence_BYHOUR:
+                if self.mByHours is not None:
+                    raise ValueError("PyCalendarRecurrence: Only one BYHOUR allowed")
                 self.mByHours = []
-                self.parseList(q, self.mByHours)
+                self.parseList(tvalue, self.mByHours, 0, 23, errmsg="PyCalendarRecurrence: Invalid BYHOUR value")
 
-            elif index == 6: # BYDAY
-                if self.mByDay != 0:
-                    return
+            elif index == definitions.eRecurrence_BYDAY:
+                if self.mByDay is not None:
+                    raise ValueError("PyCalendarRecurrence: Only one BYDAY allowed")
                 self.mByDay = []
-                self.parseListDW(q, self.mByDay)
+                self.parseListDW(tvalue, self.mByDay, errmsg="PyCalendarRecurrence: Invalid BYDAY value")
 
-            elif index == 7: # BYMONTHDAY
-                if self.mByMonthDay != 0:
-                    return
+            elif index == definitions.eRecurrence_BYMONTHDAY:
+                if self.mByMonthDay is not None:
+                    raise ValueError("PyCalendarRecurrence: Only one BYMONTHDAY allowed")
                 self.mByMonthDay = []
-                self.parseList(q, self.mByMonthDay)
+                self.parseList(tvalue, self.mByMonthDay, 1, 31, True, errmsg="PyCalendarRecurrence: Invalid BYMONTHDAY value")
 
-            elif index == 8: # BYYEARDAY
-                if self.mByYearDay != 0:
-                    return
+            elif index == definitions.eRecurrence_BYYEARDAY:
+                if self.mByYearDay is not None:
+                    raise ValueError("PyCalendarRecurrence: Only one BYYEARDAY allowed")
                 self.mByYearDay = []
-                self.parseList(q, self.mByYearDay)
+                self.parseList(tvalue, self.mByYearDay, 1, 366, True, errmsg="PyCalendarRecurrence: Invalid BYYEARDAY value")
 
-            elif index == 9: # BYWEEKNO
-                if self.mByWeekNo != 0:
-                    return
+            elif index == definitions.eRecurrence_BYWEEKNO:
+                if self.mByWeekNo is not None:
+                    raise ValueError("PyCalendarRecurrence: Only one BYWEEKNO allowed")
                 self.mByWeekNo = []
-                self.parseList(q, self.mByWeekNo)
+                self.parseList(tvalue, self.mByWeekNo, 1, 53, True, errmsg="PyCalendarRecurrence: Invalid BYWEEKNO value")
 
-            elif index == 10: # BYMONTH
-                if self.mByMonth != 0:
-                    return
+            elif index == definitions.eRecurrence_BYMONTH:
+                if self.mByMonth is not None:
+                    raise ValueError("PyCalendarRecurrence: Only one BYMONTH allowed")
                 self.mByMonth = []
-                self.parseList(q, self.mByMonth)
+                self.parseList(tvalue, self.mByMonth, 1, 12, errmsg="PyCalendarRecurrence: Invalid BYMONTH value")
 
-            elif index == 11: # BYSETPOS
-                if self.mBySetPos != 0:
-                    return
+            elif index == definitions.eRecurrence_BYSETPOS:
+                if self.mBySetPos is not None:
+                    raise ValueError("PyCalendarRecurrence: Only one BYSETPOS allowed")
                 self.mBySetPos = []
-                self.parseList(q, self.mBySetPos)
+                self.parseList(tvalue, self.mBySetPos, allowNegative=True, errmsg="PyCalendarRecurrence: Invalid BYSETPOS value")
 
-            elif index == 12: # WKST
-                index = stringutils.strindexfind(q, PyCalendarRecurrence.cWeekdayMap, PyCalendarRecurrence.cUnknownIndex)
+            elif index == definitions.eRecurrence_WKST:
+                index = PyCalendarRecurrence.cWeekdayMap.get(tvalue, PyCalendarRecurrence.cUnknownIndex)
                 if (index == PyCalendarRecurrence.cUnknownIndex):
-                    return
-                self.mWeekstart = index        
+                    raise ValueError("PyCalendarRecurrence: Invalid WKST value")
+                self.mWeekstart = index
 
-    def parseList(self, txt, list):
-        
+
+    def parseList(self, txt, list, min=None, max=None, allowNegative=False, errmsg=""):
+
         if "," in txt:
             tokens = txt.split(",")
         else:
             tokens = (txt,)
 
         for token in tokens:
-            list.append(int(token))
+            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)
 
-    def parseListDW(self, txt, list):
 
+    def parseListDW(self, txt, list, errmsg=""):
+
         if "," in txt:
             tokens = txt.split(",")
         else:
@@ -417,67 +513,76 @@
                 offset = 0
                 while (offset < len(token)) and token[offset] in "+-1234567890":
                     offset += 1
-            
+
                 num = int(token[0:offset])
                 token = token[offset:]
-        
 
+                anum = abs(num)
+                if anum < 1:
+                    raise ValueError(errmsg)
+                if anum > 53:
+                    raise ValueError(errmsg)
+
             # Get day
-            index = stringutils.strnindexfind(token, PyCalendarRecurrence.cWeekdayMap, PyCalendarRecurrence.cUnknownIndex)
+            index = PyCalendarRecurrence.cWeekdayMap.get(token, PyCalendarRecurrence.cUnknownIndex)
             if (index == PyCalendarRecurrence.cUnknownIndex):
-                return
+                raise ValueError(errmsg)
             wday = index
 
             list.append((num, wday))
-    
+
+
     def generate(self, os):
         try:
             os.write(definitions.cICalValue_RECUR_FREQ)
+            os.write("=")
 
-            if self.mFreq ==  definitions.eRecurrence_SECONDLY:
+            if self.mFreq == definitions.eRecurrence_SECONDLY:
                 os.write(definitions.cICalValue_RECUR_SECONDLY)
 
-            elif self.mFreq ==  definitions.eRecurrence_MINUTELY:
+            elif self.mFreq == definitions.eRecurrence_MINUTELY:
                 os.write(definitions.cICalValue_RECUR_MINUTELY)
 
-            elif self.mFreq ==  definitions.eRecurrence_HOURLY:
+            elif self.mFreq == definitions.eRecurrence_HOURLY:
                 os.write(definitions.cICalValue_RECUR_HOURLY)
 
-            elif self.mFreq ==  definitions.eRecurrence_DAILY:
+            elif self.mFreq == definitions.eRecurrence_DAILY:
                 os.write(definitions.cICalValue_RECUR_DAILY)
 
-            elif self.mFreq ==  definitions.eRecurrence_WEEKLY:
+            elif self.mFreq == definitions.eRecurrence_WEEKLY:
                 os.write(definitions.cICalValue_RECUR_WEEKLY)
 
-            elif self.mFreq ==  definitions.eRecurrence_MONTHLY:
+            elif self.mFreq == definitions.eRecurrence_MONTHLY:
                 os.write(definitions.cICalValue_RECUR_MONTHLY)
 
-            elif self.mFreq ==  definitions.eRecurrence_YEARLY:
+            elif self.mFreq == definitions.eRecurrence_YEARLY:
                 os.write(definitions.cICalValue_RECUR_YEARLY)
 
             if self.mUseCount:
                 os.write(";")
                 os.write(definitions.cICalValue_RECUR_COUNT)
+                os.write("=")
                 os.write(str(self.mCount))
-            elif (self.mUseUntil):
+            elif self.mUseUntil:
                 os.write(";")
                 os.write(definitions.cICalValue_RECUR_UNTIL)
+                os.write("=")
                 self.mUntil.generate(os)
-        
 
             if self.mInterval > 1:
                 os.write(";")
                 os.write(definitions.cICalValue_RECUR_INTERVAL)
+                os.write("=")
                 os.write(str(self.mInterval))
-        
 
             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)
 
-            if (self.mByDay != 0) and (len(self.mByDay) != 0):
+            if (self.mByDay is not None) and (len(self.mByDay) != 0):
                 os.write(";")
                 os.write(definitions.cICalValue_RECUR_BYDAY)
+                os.write("=")
                 comma = False
                 for iter in self.mByDay:
                     if comma:
@@ -493,19 +598,19 @@
                     elif iter[1] == definitions.eRecurrence_WEEKDAY_MO:
                         os.write(definitions.cICalValue_RECUR_WEEKDAY_MO)
 
-                    elif iter[1] ==  definitions.eRecurrence_WEEKDAY_TU:
+                    elif iter[1] == definitions.eRecurrence_WEEKDAY_TU:
                         os.write(definitions.cICalValue_RECUR_WEEKDAY_TU)
 
-                    elif iter[1] ==  definitions.eRecurrence_WEEKDAY_WE:
+                    elif iter[1] == definitions.eRecurrence_WEEKDAY_WE:
                         os.write(definitions.cICalValue_RECUR_WEEKDAY_WE)
 
-                    elif iter[1] ==  definitions.eRecurrence_WEEKDAY_TH:
+                    elif iter[1] == definitions.eRecurrence_WEEKDAY_TH:
                         os.write(definitions.cICalValue_RECUR_WEEKDAY_TH)
 
-                    elif iter[1] ==  definitions.eRecurrence_WEEKDAY_FR:
+                    elif iter[1] == definitions.eRecurrence_WEEKDAY_FR:
                         os.write(definitions.cICalValue_RECUR_WEEKDAY_FR)
 
-                    elif iter[1] ==  definitions.eRecurrence_WEEKDAY_SA:
+                    elif iter[1] == definitions.eRecurrence_WEEKDAY_SA:
                         os.write(definitions.cICalValue_RECUR_WEEKDAY_SA)
 
             self.generateList(os, definitions.cICalValue_RECUR_BYMONTHDAY, self.mByMonthDay)
@@ -518,8 +623,9 @@
             if self.mWeekstart != definitions.eRecurrence_WEEKDAY_MO:
                 os.write(";")
                 os.write(definitions.cICalValue_RECUR_WKST)
+                os.write("=")
 
-                if self.mWeekstart ==  definitions.eRecurrence_WEEKDAY_SU:
+                if self.mWeekstart == definitions.eRecurrence_WEEKDAY_SU:
                     os.write(definitions.cICalValue_RECUR_WEEKDAY_SU)
 
                 elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_MO:
@@ -543,28 +649,81 @@
         except:
             pass
 
-    def generateList(self, os, title, list):
 
-        if (list != 0) and (len(list) != 0):
+    def generateList(self, os, title, items):
+
+        if (items is not None) and (len(items) != 0):
             os.write(";")
             os.write(title)
+            os.write("=")
             comma = False
-            for e in list:
+            for e in items:
                 if comma:
                     os.write(",")
                 comma = True
                 os.write(str(e))
 
+
+    def writeXML(self, node, namespace):
+
+        recur = XML.SubElement(node, xmldefs.makeTag(namespace, xmldefs.value_recur))
+
+        freq = XML.SubElement(recur, xmldefs.makeTag(namespace, xmldefs.recur_freq))
+        freq.text = self.cFreqToXMLMap[self.mFreq]
+
+        if self.mUseCount:
+            count = XML.SubElement(recur, xmldefs.makeTag(namespace, xmldefs.recur_count))
+            count.text = str(self.mCount)
+        elif self.mUseUntil:
+            until = XML.SubElement(recur, xmldefs.makeTag(namespace, xmldefs.recur_until))
+            self.mUntil.writeXML(until, namespace)
+
+        if self.mInterval > 1:
+            interval = XML.SubElement(recur, xmldefs.makeTag(namespace, xmldefs.recur_interval))
+            interval.text = str(self.mInterval)
+
+        self.writeXMLList(recur, namespace, xmldefs.recur_bysecond, self.mBySeconds)
+        self.writeXMLList(recur, namespace, xmldefs.recur_byminute, self.mByMinutes)
+        self.writeXMLList(recur, namespace, xmldefs.recur_byhour, self.mByHours)
+
+        if self.mByDay is not None and len(self.mByDay) != 0:
+            for iter in self.mByDay:
+                byday = XML.SubElement(recur, xmldefs.makeTag(namespace, xmldefs.recur_byday))
+                data = ""
+                if iter[0] != 0:
+                    data = str(iter[0])
+                data += self.cWeekdayRecurMap.get(iter[1], "")
+                byday.text = data
+
+        self.writeXMLList(recur, namespace, xmldefs.recur_bymonthday, self.mByMonthDay)
+        self.writeXMLList(recur, namespace, xmldefs.recur_byyearday, self.mByYearDay)
+        self.writeXMLList(recur, namespace, xmldefs.recur_byweekno, self.mByWeekNo)
+        self.writeXMLList(recur, namespace, xmldefs.recur_bymonth, self.mByMonth)
+        self.writeXMLList(recur, namespace, xmldefs.recur_bysetpos, self.mBySetPos)
+
+        # MO is the default so we do not need it
+        if self.mWeekstart != definitions.eRecurrence_WEEKDAY_MO:
+            wkst = XML.SubElement(recur, xmldefs.makeTag(namespace, xmldefs.recur_wkst))
+            wkst.text = self.cWeekdayRecurMap.get(self.mWeekstart, definitions.cICalValue_RECUR_WEEKDAY_MO)
+
+
+    def writeXMLList(self, node, namespace, name, items):
+        if items is not None and len(items) != 0:
+            for item in items:
+                child = XML.SubElement(node, xmldefs.makeTag(namespace, name))
+                child.text = str(item)
+
+
     def hasBy(self):
-        return (self.mBySeconds != 0) and (len(self.mBySeconds) != 0) \
-                or (self.mByMinutes != 0) and (len(self.mByMinutes) != 0) \
-                or (self.mByHours != 0) and (len(self.mByHours) != 0) \
-                or (self.mByDay != 0) and (len(self.mByDay) != 0) \
-                or (self.mByMonthDay != 0) and (len(self.mByMonthDay) != 0) \
-                or (self.mByYearDay != 0) and (len(self.mByYearDay) != 0) \
-                or (self.mByWeekNo != 0) and (len(self.mByWeekNo) != 0) \
-                or (self.mByMonth != 0) and (len(self.mByMonth) != 0) \
-                or (self.mBySetPos != 0) and (len(self.mBySetPos) != 0)
+        return (self.mBySeconds is not None) and (len(self.mBySeconds) != 0) \
+                or (self.mByMinutes is not None) and (len(self.mByMinutes) != 0) \
+                or (self.mByHours is not None) and (len(self.mByHours) != 0) \
+                or (self.mByDay is not None) and (len(self.mByDay) != 0) \
+                or (self.mByMonthDay is not None) and (len(self.mByMonthDay) != 0) \
+                or (self.mByYearDay is not None) and (len(self.mByYearDay) != 0) \
+                or (self.mByWeekNo is not None) and (len(self.mByWeekNo) != 0) \
+                or (self.mByMonth is not None) and (len(self.mByMonth) != 0) \
+                or (self.mBySetPos is not None) and (len(self.mBySetPos) != 0)
 
 
     def isSimpleRule(self):
@@ -581,21 +740,21 @@
         # no others
 
         # First checks the ones we do not handle at all
-        if ((self.mBySeconds != 0) and (len(self.mBySeconds) != 0) \
-                or (self.mByMinutes != 0) and (len(self.mByMinutes) != 0) \
-                or (self.mByHours != 0) and (len(self.mByHours) != 0) \
-                or (self.mByYearDay != 0) and (len(self.mByYearDay) != 0) \
-                or (self.mByWeekNo != 0) and (len(self.mByWeekNo) != 0)):
+        if ((self.mBySeconds is not None) and (len(self.mBySeconds) != 0) \
+                or (self.mByMinutes is not None) and (len(self.mByMinutes) != 0) \
+                or (self.mByHours is not None) and (len(self.mByHours) != 0) \
+                or (self.mByYearDay is not None) and (len(self.mByYearDay) != 0) \
+                or (self.mByWeekNo is not None) and (len(self.mByWeekNo) != 0)):
             return False
 
         # Check BYMONTHDAY numbers (we can handle -7...-1, 1..31)
-        if self.mByMonthDay != 0:
+        if self.mByMonthDay is not None:
             for iter in self.mByMonthDay:
                 if (iter < -7) or (iter > 31) or (iter == 0):
                     return False
 
         # Check BYDAY numbers
-        if self.mByDay != 0:
+        if self.mByDay is not None:
             number = 0
             first = True
             for iter in self.mByDay:
@@ -608,19 +767,17 @@
                     # Check number range
                     if (number > 4) or (number < -2):
                         return False
-            
 
                 # If current differs from last, then we have an error
                 elif number != iter[0]:
                     return False
 
         # Check BYSETPOS numbers
-        if self.mBySetPos != 0:
+        if self.mBySetPos is not None:
             if len(self.mBySetPos) > 1:
                 return False
             if (len(self.mBySetPos) == 1) and (self.mBySetPos[0] != -1) and (self.mBySetPos[0] != 1):
                 return False
-    
 
         # If we get here it must be OK
         return True
@@ -634,7 +791,7 @@
             result = sout.getvalue()
         except:
             result = ""
-    
+
         return result
 
 
@@ -649,12 +806,12 @@
             self.mCached = False
             self.mFullyCached = False
             self.mRecurrences = []
-    
-        # Is the current cache complete or does it extaned past the requested
+
+        # Is the current cache complete or does it extend past the requested
         # range end
         if not self.mCached or not self.mFullyCached \
                 and (self.mCacheUpto is None or self.mCacheUpto < range.getEnd()):
-            cache_range = PyCalendarPeriod(copyit=range)
+            cache_range = range.duplicate()
 
             # If partially cached just cache from previous cache end up to new
             # end
@@ -671,18 +828,23 @@
             self.mCached = True
             self.mCacheStart = start
             self.mCacheUpto = range.getEnd()
-    
+
         # Just return the cached items in the requested range
+        limited = not self.mFullyCached
         for iter in self.mRecurrences:
             if range.isDateWithinPeriod(iter):
                 items.append(iter)
-    
+            else:
+                limited = True
+        return limited
+
+
     def simpleExpand(self, start, range, items, float_offset):
-        start_iter = PyCalendarDateTime(copyit=start)
+        start_iter = start.duplicate()
         ctr = 0
 
         if self.mUseUntil:
-            float_until = PyCalendarDateTime(copyit=self.mUntil)
+            float_until = self.mUntil.duplicate()
             if start.floating():
                 float_until.setTimezoneID(0)
                 float_until.offsetSeconds(float_offset)
@@ -693,7 +855,7 @@
                 return False
 
             # Add current one to list
-            items.append(PyCalendarDateTime(copyit=start_iter))
+            items.append(start_iter.duplicate())
 
             # Get next item
             start_iter.recur(self.mFreq, self.mInterval)
@@ -710,18 +872,19 @@
                 if start_iter > float_until:
                     return True
 
+
     def complexExpand(self, start, range, items, float_offset):
-        start_iter = PyCalendarDateTime(copyit=start)
+        start_iter = start.duplicate()
         ctr = 0
 
         if self.mUseUntil:
-            float_until = PyCalendarDateTime(copyit=self.mUntil)
+            float_until = self.mUntil.duplicate()
             if start.floating():
                 float_until.setTimezoneID(None)
                 float_until.offsetSeconds(float_offset)
 
         # Always add the initial instance DTSTART
-        items.append(PyCalendarDateTime(copyit=start))
+        items.append(start.duplicate())
         if self.mUseCount:
             # Bump counter and exit if over
             ctr += 1
@@ -752,10 +915,11 @@
                 self.generateMonthlySet(start_iter, set_items)
 
             elif self.mFreq == definitions.eRecurrence_YEARLY:
-                self.generateYearlySet(start_iter, set_items)        
+                self.generateYearlySet(start_iter, set_items)
 
             # Always sort the set as BYxxx rules may not be sorted
-            set_items.sort(cmp=PyCalendarDateTime.sort)
+            #set_items.sort(cmp=PyCalendarDateTime.sort)
+            set_items.sort(key=lambda x: x.getPosixTime())
 
             # Process each one in the generated set
             for iter in set_items:
@@ -800,6 +964,7 @@
             # Get next item
             start_iter.recur(self.mFreq, self.mInterval)
 
+
     def clear(self):
         self.mCached = False
         self.mFullyCached = False
@@ -834,330 +999,321 @@
             # The last one is just less than the exclude date
             self.mUseCount = True
             self.mCount = len(items)
-    
+
         # Now clear out the cached set after making changes
         self.clear()
 
+
     def generateYearlySet(self, start, items):
         # All possible BYxxx are valid, though some combinations are not
 
         # Start with initial date-time
-        items.append(PyCalendarDateTime(copyit=start))
+        items.append(start.duplicate())
 
-        if (self.mByMonth != 0) and (len(self.mByMonth) != 0):
+        if (self.mByMonth is not None) and (len(self.mByMonth) != 0):
             items[:] = self.byMonthExpand(items)
-    
 
-        if (self.mByWeekNo != 0) and (len(self.mByWeekNo) != 0):
+        if (self.mByWeekNo is not None) and (len(self.mByWeekNo) != 0):
             items[:] = self.byWeekNoExpand(items)
-    
 
-        if (self.mByYearDay != 0) and (len(self.mByYearDay) != 0):
+        if (self.mByYearDay is not None) and (len(self.mByYearDay) != 0):
             items[:] = self.byYearDayExpand(items)
-    
 
-        if (self.mByMonthDay != 0) and (len(self.mByMonthDay) != 0):
+        if (self.mByMonthDay is not None) and (len(self.mByMonthDay) != 0):
             items[:] = self.byMonthDayExpand(items)
-    
 
-        if (self.mByDay != 0) and (len(self.mByDay) != 0):
+        if (self.mByDay is not None) and (len(self.mByDay) != 0):
             # BYDAY is complicated:
             # if BYDAY is included with BYYEARDAY or BYMONTHDAY then it
             # contracts the recurrence set
             # else it expands it, but the expansion depends on the frequency
             # and other BYxxx periodicities
 
-            if ((self.mByYearDay != 0) and (len(self.mByYearDay) != 0)) \
-                    or ((self.mByMonthDay != 0) and (len(self.mByMonthDay) != 0)):
+            if ((self.mByYearDay is not None) and (len(self.mByYearDay) != 0)) \
+                    or ((self.mByMonthDay is not None) and (len(self.mByMonthDay) != 0)):
                 items[:] = self.byDayLimit(items)
-            elif (self.mByWeekNo != 0) and (len(self.mByWeekNo) != 0):
+            elif (self.mByWeekNo is not None) and (len(self.mByWeekNo) != 0):
                 items[:] = self.byDayExpandWeekly(items)
-            elif (self.mByMonth != 0) and (len(self.mByMonth) != 0):
+            elif (self.mByMonth is not None) and (len(self.mByMonth) != 0):
                 items[:] = self.byDayExpandMonthly(items)
             else:
                 items[:] = self.byDayExpandYearly(items)
 
-        if (self.mByHours != 0) and (len(self.mByHours) != 0):
+        if (self.mByHours is not None) and (len(self.mByHours) != 0):
             items[:] = self.byHourExpand(items)
-    
-        if (self.mByMinutes != 0) and (len(self.mByMinutes) != 0):
+
+        if (self.mByMinutes is not None) and (len(self.mByMinutes) != 0):
             items[:] = self.byMinuteExpand(items)
-    
-        if (self.mBySeconds != 0) and (len(self.mBySeconds) != 0):
+
+        if (self.mBySeconds is not None) and (len(self.mBySeconds) != 0):
             items[:] = self.bySecondExpand(items)
-    
-        if (self.mBySetPos != 0) and (len(self.mBySetPos) != 0):
+
+        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(PyCalendarDateTime(copyit=start))
+        items.append(start.duplicate())
 
-        if (self.mByMonth != 0) and (len(self.mByMonth) != 0):
+        if (self.mByMonth is not None) and (len(self.mByMonth) != 0):
             # BYMONTH limits the range of possible values
             items[:] = self.byMonthLimit(items)
             if (len(items) == 0):
                 return
-    
+
         # No BYWEEKNO
 
         # No BYYEARDAY
 
-        if (self.mByMonthDay != 0) and (len(self.mByMonthDay) != 0):
+        if (self.mByMonthDay is not None) and (len(self.mByMonthDay) != 0):
             items[:] = self.byMonthDayExpand(items)
-    
-        if (self.mByDay != 0) and (len(self.mByDay) != 0):
+
+        if (self.mByDay is not None) and (len(self.mByDay) != 0):
             # BYDAY is complicated:
             # if BYDAY is included with BYYEARDAY or BYMONTHDAY then it
             # contracts the recurrence set
             # else it expands it, but the expansion depends on the frequency
             # and other BYxxx periodicities
 
-            if ((self.mByYearDay != 0) and (len(self.mByYearDay) != 0)) \
-                    or ((self.mByMonthDay != 0) and (len(self.mByMonthDay) != 0)):
+            if ((self.mByYearDay is not None) and (len(self.mByYearDay) != 0)) \
+                    or ((self.mByMonthDay is not None) and (len(self.mByMonthDay) != 0)):
                 items[:] = self.byDayLimit(items)
             else:
                 items[:] = self.byDayExpandMonthly(items)
-    
-        if ((self.mByHours != 0) and (len(self.mByHours) != 0)):
+
+        if ((self.mByHours is not None) and (len(self.mByHours) != 0)):
             items[:] = self.byHourExpand(items)
-    
-        if ((self.mByMinutes != 0) and (len(self.mByMinutes) != 0)):
+
+        if ((self.mByMinutes is not None) and (len(self.mByMinutes) != 0)):
             items[:] = self.byMinuteExpand(items)
-    
-        if ((self.mBySeconds != 0) and (len(self.mBySeconds) != 0)):
+
+        if ((self.mBySeconds is not None) and (len(self.mBySeconds) != 0)):
             items[:] = self.bySecondExpand(items)
-    
-        if ((self.mBySetPos != 0) and (len(self.mBySetPos) != 0)):
+
+        if ((self.mBySetPos is not None) and (len(self.mBySetPos) != 0)):
             items[:] = self.bySetPosLimit(items)
-    
-    def generateWeeklySet(self, start,  items):
+
+
+    def generateWeeklySet(self, start, items):
         # Cannot have BYYEARDAY and BYMONTHDAY
 
         # Start with initial date-time
-        items.append(PyCalendarDateTime(copyit=start))
+        items.append(start.duplicate())
 
-        if (self.mByMonth != 0) and (len(self.mByMonth) != 0):
+        if (self.mByMonth is not None) and (len(self.mByMonth) != 0):
             # BYMONTH limits the range of possible values
             items[:] = self.byMonthLimit(items)
             if (len(items) == 0):
                 return
-    
-        if (self.mByWeekNo != 0) and (len(self.mByWeekNo) != 0):
+
+        if (self.mByWeekNo is not None) and (len(self.mByWeekNo) != 0):
             items[:] = self.byWeekNoLimit(items)
             if (len(items) == 0):
                 return
-    
+
         # No BYYEARDAY
 
         # No BYMONTHDAY
 
-        if (self.mByDay != 0) and (len(self.mByDay) != 0):
+        if (self.mByDay is not None) and (len(self.mByDay) != 0):
             items[:] = self.byDayExpandWeekly(items)
-    
-        if (self.mByHours != 0) and (len(self.mByHours) != 0):
+
+        if (self.mByHours is not None) and (len(self.mByHours) != 0):
             items[:] = self.byHourExpand(items)
-    
-        if (self.mByMinutes != 0) and (len(self.mByMinutes) != 0):
+
+        if (self.mByMinutes is not None) and (len(self.mByMinutes) != 0):
             items[:] = self.byMinuteExpand(items)
-    
-        if (self.mBySeconds != 0) and (len(self.mBySeconds) != 0):
+
+        if (self.mBySeconds is not None) and (len(self.mBySeconds) != 0):
             items[:] = self.bySecondExpand(items)
-    
-        if (self.mBySetPos != 0) and (len(self.mBySetPos) != 0):
+
+        if (self.mBySetPos is not None) and (len(self.mBySetPos) != 0):
             items[:] = self.bySetPosLimit(items)
-    
-    def generateDailySet(self, start,  items):
+
+
+    def generateDailySet(self, start, items):
         # Cannot have BYYEARDAY
 
         # Start with initial date-time
-        items.append(PyCalendarDateTime(copyit=start))
+        items.append(start.duplicate())
 
-        if (self.mByMonth != 0) and (len(self.mByMonth) != 0):
+        if (self.mByMonth is not None) and (len(self.mByMonth) != 0):
             # BYMONTH limits the range of possible values
             items[:] = self.byMonthLimit(items)
             if (len(items) == 0):
                 return
-    
 
-        if (self.mByWeekNo != 0) and (len(self.mByWeekNo) != 0):
+        if (self.mByWeekNo is not None) and (len(self.mByWeekNo) != 0):
             items[:] = self.byWeekNoLimit(items)
             if (len(items) == 0):
                 return
-    
 
         # No BYYEARDAY
 
-        if (self.mByMonthDay != 0) and (len(self.mByMonthDay) != 0):
+        if (self.mByMonthDay is not None) and (len(self.mByMonthDay) != 0):
             items[:] = self.byMonthDayLimit(items)
             if (len(items) == 0):
                 return
-    
 
-        if (self.mByDay != 0) and (len(self.mByDay) != 0):
+        if (self.mByDay is not None) and (len(self.mByDay) != 0):
             items[:] = self.byDayLimit(items)
             if (len(items) == 0):
                 return
-    
 
-        if (self.mByHours != 0) and (len(self.mByHours) != 0):
+        if (self.mByHours is not None) and (len(self.mByHours) != 0):
             items[:] = self.byHourExpand(items)
-    
 
-        if (self.mByMinutes != 0) and (len(self.mByMinutes) != 0):
+        if (self.mByMinutes is not None) and (len(self.mByMinutes) != 0):
             items[:] = self.byMinuteExpand(items)
-    
 
-        if (self.mBySeconds != 0) and (len(self.mBySeconds) != 0):
+        if (self.mBySeconds is not None) and (len(self.mBySeconds) != 0):
             items[:] = self.bySecondExpand(items)
-    
 
-        if (self.mBySetPos != 0) and (len(self.mBySetPos) != 0):
+        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(PyCalendarDateTime(copyit=start))
+        items.append(start.duplicate())
 
-        if (self.mByMonth != 0) and (len(self.mByMonth) != 0):
+        if (self.mByMonth is not None) and (len(self.mByMonth) != 0):
             # BYMONTH limits the range of possible values
             items[:] = self.byMonthLimit(items)
             if (len(items) == 0):
                 return
-    
 
-        if (self.mByWeekNo != 0) and (len(self.mByWeekNo) != 0):
+        if (self.mByWeekNo is not None) and (len(self.mByWeekNo) != 0):
             items[:] = self.byWeekNoLimit(items)
             if (len(items) == 0):
                 return
-    
 
         # No BYYEARDAY
 
-        if (self.mByMonthDay != 0) and (len(self.mByMonthDay) != 0):
+        if (self.mByMonthDay is not None) and (len(self.mByMonthDay) != 0):
             items[:] = self.byMonthDayLimit(items)
             if (len(items) == 0):
                 return
-    
 
-        if (self.mByDay != 0) and (len(self.mByDay) != 0):
+        if (self.mByDay is not None) and (len(self.mByDay) != 0):
             items[:] = self.byDayLimit(items)
             if (len(items) == 0):
                 return
-    
 
-        if (self.mByHours != 0) and (len(self.mByHours) != 0):
+        if (self.mByHours is not None) and (len(self.mByHours) != 0):
             items[:] = self.byHourLimit(items)
             if (len(items) == 0):
                 return
-    
-        if (self.mByMinutes != 0) and (len(self.mByMinutes) != 0):
+
+        if (self.mByMinutes is not None) and (len(self.mByMinutes) != 0):
             items[:] = self.byMinuteExpand(items)
-    
 
-        if (self.mBySeconds != 0) and (len(self.mBySeconds) != 0):
+        if (self.mBySeconds is not None) and (len(self.mBySeconds) != 0):
             items[:] = self.bySecondExpand(items)
-    
 
-        if (self.mBySetPos != 0) and (len(self.mBySetPos) != 0):
+        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(PyCalendarDateTime(copyit=start))
+        items.append(start.duplicate())
 
-        if (self.mByMonth != 0) and (len(self.mByMonth) != 0):
+        if (self.mByMonth is not None) and (len(self.mByMonth) != 0):
             # BYMONTH limits the range of possible values
             items[:] = self.byMonthLimit(items)
             if (len(items) == 0):
                 return
-    
-        if (self.mByWeekNo != 0) and (len(self.mByWeekNo) != 0):
+
+        if (self.mByWeekNo is not None) and (len(self.mByWeekNo) != 0):
             items[:] = self.byWeekNoLimit(items)
             if (len(items) == 0):
                 return
-    
+
         # No BYYEARDAY
 
-        if (self.mByMonthDay != 0) and (len(self.mByMonthDay) != 0):
+        if (self.mByMonthDay is not None) and (len(self.mByMonthDay) != 0):
             items[:] = self.byMonthDayLimit(items)
             if (len(items) == 0):
                 return
-    
-        if (self.mByDay != 0) and (len(self.mByDay) != 0):
+
+        if (self.mByDay is not None) and (len(self.mByDay) != 0):
             items[:] = self.byDayLimit(items)
             if (len(items) == 0):
                 return
-    
-        if (self.mByHours != 0) and (len(self.mByHours) != 0):
+
+        if (self.mByHours is not None) and (len(self.mByHours) != 0):
             items[:] = self.byHourLimit(items)
             if (len(items) == 0):
                 return
-    
-        if (self.mByMinutes != 0) and (len(self.mByMinutes) != 0):
+
+        if (self.mByMinutes is not None) and (len(self.mByMinutes) != 0):
             items[:] = self.byMinuteLimit(items)
             if (len(items) == 0):
                 return
-    
-        if (self.mBySeconds != 0) and (len(self.mBySeconds) != 0):
+
+        if (self.mBySeconds is not None) and (len(self.mBySeconds) != 0):
             items[:] = self.bySecondExpand(items)
-    
-        if (self.mBySetPos != 0) and (len(self.mBySetPos) != 0):
+
+        if (self.mBySetPos is not None) and (len(self.mBySetPos) != 0):
             items[:] = self.bySetPosLimit(items)
-    
-    def generateSecondlySet(self, start,  items):
+
+
+    def generateSecondlySet(self, start, items):
         # Cannot have BYYEARDAY
 
         # Start with initial date-time
-        items.append(PyCalendarDateTime(copyit=start))
+        items.append(start.duplicate())
 
-        if (self.mByMonth != 0) and (len(self.mByMonth) != 0):
+        if (self.mByMonth is not None) and (len(self.mByMonth) != 0):
             # BYMONTH limits the range of possible values
             items[:] = self.byMonthLimit(items)
             if (len(items) == 0):
                 return
-    
-        if (self.mByWeekNo != 0) and (len(self.mByWeekNo) != 0):
+
+        if (self.mByWeekNo is not None) and (len(self.mByWeekNo) != 0):
             items[:] = self.byWeekNoLimit(items)
             if (len(items) == 0):
                 return
-    
+
         # No BYYEARDAY
 
-        if (self.mByMonthDay != 0) and (len(self.mByMonthDay) != 0):
+        if (self.mByMonthDay is not None) and (len(self.mByMonthDay) != 0):
             items[:] = self.byMonthDayLimit(items)
             if (len(items) == 0):
                 return
-    
-        if (self.mByDay != 0) and (len(self.mByDay) != 0):
+
+        if (self.mByDay is not None) and (len(self.mByDay) != 0):
             items[:] = self.byDayLimit(items)
             if (len(items) == 0):
                 return
-    
-        if (self.mByHours != 0) and (len(self.mByHours) != 0):
+
+        if (self.mByHours is not None) and (len(self.mByHours) != 0):
             items[:] = self.byHourLimit(items)
             if (len(items) == 0):
                 return
-    
-        if (self.mByMinutes != 0) and (len(self.mByMinutes) != 0):
+
+        if (self.mByMinutes is not None) and (len(self.mByMinutes) != 0):
             items[:] = self.byMinuteLimit(items)
             if (len(items) == 0):
                 return
-    
-        if (self.mBySeconds != 0) and (len(self.mBySeconds) != 0):
+
+        if (self.mBySeconds is not None) and (len(self.mBySeconds) != 0):
             items[:] = self.bySecondLimit(items)
             if (len(items) == 0):
                 return
-    
-        if (self.mBySetPos != 0) and (len(self.mBySetPos) != 0):
+
+        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 = []
@@ -1165,12 +1321,13 @@
             # Loop over each BYMONTH and generating a new date-time for it and
             # insert into output
             for iter2 in self.mByMonth:
-                temp = PyCalendarDateTime(copyit=iter1)
+                temp = iter1.duplicate()
                 temp.setMonth(iter2)
                 output.append(temp)
 
         return output
 
+
     def byWeekNoExpand(self, dates):
         # Loop over all input items
         output = []
@@ -1178,12 +1335,13 @@
             # Loop over each BYWEEKNO and generating a new date-time for it and
             # insert into output
             for iter2 in self.mByWeekNo:
-                temp = PyCalendarDateTime(copyit=iter1)
+                temp = iter1.duplicate()
                 temp.setWeekNo(iter2)
                 output.append(temp)
-                
+
         return output
 
+
     def byYearDayExpand(self, dates):
         # Loop over all input items
         output = []
@@ -1191,12 +1349,13 @@
             # Loop over each BYYEARDAY and generating a new date-time for it
             # and insert into output
             for iter2 in self.mByYearDay:
-                temp = PyCalendarDateTime(copyit=iter1)
+                temp = iter1.duplicate()
                 temp.setYearDay(iter2)
                 output.append(temp)
 
         return output
 
+
     def byMonthDayExpand(self, dates):
         # Loop over all input items
         output = []
@@ -1204,12 +1363,13 @@
             # Loop over each BYMONTHDAY and generating a new date-time for it
             # and insert into output
             for iter2 in self.mByMonthDay:
-                temp = PyCalendarDateTime(copyit=iter1)
+                temp = iter1.duplicate()
                 temp.setMonthDay(iter2)
                 output.append(temp)
 
         return output
 
+
     def byDayExpandYearly(self, dates):
         # Loop over all input items
         output = []
@@ -1219,19 +1379,20 @@
             for iter2 in self.mByDay:
                 # Numeric value means specific instance
                 if iter2[0] != 0:
-                    temp = PyCalendarDateTime(copyit=iter1)
+                    temp = iter1.duplicate()
                     temp.setDayOfWeekInYear(iter2[0], iter2[1])
                     output.append(temp)
                 else:
                     # Every matching day in the year
                     for  i in range(1, 54):
-                        temp = PyCalendarDateTime(copyit=iter1)
+                        temp = iter1.duplicate()
                         temp.setDayOfWeekInYear(i, iter2[1])
                         if temp.getYear() == (iter1).getYear():
                             output.append(temp)
 
         return output
 
+
     def byDayExpandMonthly(self, dates):
         # Loop over all input items
         output = []
@@ -1241,19 +1402,20 @@
             for iter2 in self.mByDay:
                 # Numeric value means specific instance
                 if iter2[0] != 0:
-                    temp = PyCalendarDateTime(copyit=iter1)
+                    temp = iter1.duplicate()
                     temp.setDayOfWeekInMonth(iter2[0], iter2[1])
                     output.append(temp)
                 else:
                     # Every matching day in the month
                     for i in range(1, 7):
-                        temp = PyCalendarDateTime(copyit=iter1)
+                        temp = iter1.duplicate()
                         temp.setDayOfWeekInMonth(i, iter2[1])
                         if temp.getMonth() == iter1.getMonth():
                             output.append(temp)
 
         return output
 
+
     def byDayExpandWeekly(self, dates):
         # Must take into account the WKST value
 
@@ -1265,7 +1427,7 @@
             for iter2 in self.mByDay:
                 # Numeric values are meaningless so ignore them
                 if iter2[0] == 0:
-                    temp = PyCalendarDateTime(copyit=iter1)
+                    temp = iter1.duplicate()
 
                     # Determine amount of offset to apply to temp to shift it
                     # to the start of the week (backwards)
@@ -1285,6 +1447,7 @@
 
         return output
 
+
     def byHourExpand(self, dates):
         # Loop over all input items
         output = []
@@ -1292,12 +1455,13 @@
             # Loop over each BYHOUR and generating a new date-time for it and
             # insert into output
             for iter2 in self.mByHours:
-                temp = PyCalendarDateTime(copyit=iter1)
+                temp = iter1.duplicate()
                 temp.setHours(iter2)
                 output.append(temp)
 
         return output
 
+
     def byMinuteExpand(self, dates):
         # Loop over all input items
         output = []
@@ -1305,12 +1469,13 @@
             # Loop over each BYMINUTE and generating a new date-time for it and
             # insert into output
             for iter2 in self.mByMinutes:
-                temp = PyCalendarDateTime(copyit=iter1)
+                temp = iter1.duplicate()
                 temp.setMinutes(iter2)
                 output.append(temp)
 
         return output
 
+
     def bySecondExpand(self, dates):
         # Loop over all input items
         output = []
@@ -1318,12 +1483,13 @@
             # Loop over each BYSECOND and generating a new date-time for it and
             # insert into output
             for iter2 in self.mBySeconds:
-                temp = PyCalendarDateTime(copyit=iter1)
+                temp = iter1.duplicate()
                 temp.setSeconds(iter2)
                 output.append(temp)
 
         return output
 
+
     def byMonthLimit(self, dates):
         # Loop over all input items
         output = []
@@ -1334,12 +1500,13 @@
                 keep = (iter1.getMonth() == iter2)
                 if keep:
                     break
-        
+
             if keep:
                 output.append(iter1)
 
         return output
 
+
     def byWeekNoLimit(self, dates):
         # Loop over all input items
         output = []
@@ -1350,12 +1517,13 @@
                 keep = iter1.isWeekNo(iter2)
                 if keep:
                     break
-        
+
             if keep:
                 output.append(iter1)
-    
+
         return output
 
+
     def byMonthDayLimit(self, dates):
         # Loop over all input items
         output = []
@@ -1367,12 +1535,13 @@
                 keep = iter1.isMonthDay(iter2)
                 if keep:
                     break
-        
+
             if keep:
                 output.append(iter1)
-    
+
         return output
 
+
     def byDayLimit(self, dates):
         # Loop over all input items
         output = []
@@ -1383,12 +1552,13 @@
                 keep = iter1.isDayOfWeekInMonth(iter2[0], iter2[1])
                 if keep:
                     break
-        
+
             if keep:
                 output.append(iter1)
 
         return output
 
+
     def byHourLimit(self, dates):
         # Loop over all input items
         output = []
@@ -1399,12 +1569,13 @@
                 keep = (iter1.getHours() == iter2)
                 if keep:
                     break
-        
+
             if keep:
                 output.append(iter1)
-    
+
         return output
 
+
     def byMinuteLimit(self, dates):
         # Loop over all input items
         output = []
@@ -1415,12 +1586,13 @@
                 keep = (iter1.getMinutes() == iter2)
                 if keep:
                     break
-        
+
             if keep:
                 output.append(iter1)
 
         return output
 
+
     def bySecondLimit(self, dates):
         # Loop over all input items
         output = []
@@ -1431,15 +1603,17 @@
                 keep = (iter1.getSeconds() == iter2)
                 if keep:
                     break
-        
+
             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=PyCalendarDateTime.sort)
+        #dates.sort(cmp=PyCalendarDateTime.sort)
+        dates.sort(key=lambda x: x.getPosixTime())
 
         # Loop over each BYSETPOS and extract the relevant component from the
         # input array and add to the output
@@ -1454,5 +1628,5 @@
                 # Negative values are offset from the end
                 if -iter <= input_size:
                     output.append(dates[input_size + iter])
- 
+
         return output

Modified: PyCalendar/trunk/src/pycalendar/recurrenceset.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/recurrenceset.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/recurrenceset.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,32 +14,36 @@
 #    limitations under the License.
 ##
 
-from datetime import PyCalendarDateTime
-from utils import set_difference
+from pycalendar.utils import set_difference
 
 class PyCalendarRecurrenceSet(object):
 
-    def __init__(self, copyit=None):
-        if copyit is None:
-            self.mRrules = []
-            self.mExrules = []
-            self.mRdates = []
-            self.mExdates = []
-            self.mRperiods = []
-            self.mExperiods = []
-        else:
-            self.mRrules = copyit.self.mRrules
-            self.mExrules = copyit.self.mExrules
-            self.mRdates = copyit.self.mRdates
-            self.mExdates = copyit.self.mExdates
-            self.mRperiods = copyit.self.mRperiods
-            self.mExperiods = copyit.self.mExperiods
+    def __init__(self):
+        self.mRrules = []
+        self.mExrules = []
+        self.mRdates = []
+        self.mExdates = []
+        self.mRperiods = []
+        self.mExperiods = []
 
+
+    def duplicate(self):
+        other = PyCalendarRecurrenceSet()
+        other.mRrules = [i.duplicate() for i in self.mRrules]
+        other.mExrules = [i.duplicate() for i in self.mExrules]
+        other.mRdates = [i.duplicate() for i in self.mRdates]
+        other.mExdates = [i.duplicate() for i in self.mExdates]
+        other.mRperiods = [i.duplicate() for i in self.mRperiods]
+        other.mExperiods = [i.duplicate() for i in self.mExperiods]
+        return other
+
+
     def hasRecurrence(self):
         return ((len(self.mRrules) != 0) or (len(self.mRdates) != 0) or (len(self.mRperiods) != 0)
                     or (len(self.mExrules) != 0) or (len(self.mExdates) != 0)
                     or (len(self.mExperiods) != 0))
 
+
     def equals(self, comp):
         # Look at RRULEs
         if not self.equalsRules(self.mRrules, comp.self.mRrules):
@@ -64,6 +68,7 @@
         # If we get here they match
         return True
 
+
     def equalsRules(self, rules1, rules2):
         # Check sizes first
         if len(rules1) != len(rules2):
@@ -92,6 +97,7 @@
 
         return True
 
+
     def equalsDates(self, dates1, dates2):
         # Check sizes first
         if len(dates1) != len(dates2):
@@ -103,11 +109,12 @@
         dt1 = dates1[:]
         dt2 = dates2[:]
 
-        dt1.sort(cmp=PyCalendarDateTime.sort)
-        dt2.sort(cmp=PyCalendarDateTime.sort)
+        dt1.sort(key=lambda x: x.getPosixTime())
+        dt2.sort(key=lambda x: x.getPosixTime())
 
         return dt1.equal(dt2)
 
+
     def equalsPeriods(self, periods1, periods2):
         # Check sizes first
         if len(periods1) != len(periods2):
@@ -124,65 +131,88 @@
 
         return p1.equal(p2)
 
+
     def addRule(self, rule):
         self.mRrules.append(rule)
 
+
     def subtractRule(self, rule):
         self.mExrules.append(rule)
 
+
     def addDT(self, dt):
         self.mRdates.append(dt)
 
+
     def subtractDT(self, dt):
         self.mExdates.append(dt)
 
+
     def addPeriod(self, p):
         self.mRperiods.append(p)
 
+
     def subtractPeriod(self, p):
         self.mExperiods.append(p)
 
+
     def getRules(self):
         return self.mRrules
 
+
     def getExrules(self):
         return self.mExrules
 
+
     def getDates(self):
         return self.mRdates
 
+
     def getExdates(self):
         return self.mExdates
 
+
     def getPeriods(self):
         return self.mRperiods
 
+
     def getExperiods(self):
         return self.mExperiods
 
+
     def expand(self, start, range, items, float_offset=0):
+        # Need to return whether the limit was applied or not
+        limited = False
+
         # Now create list of items to include
         include = []
 
         # Always include the initial DTSTART if within the range
         if range.isDateWithinPeriod(start):
             include.append(start)
+        else:
+            limited = True
 
         # RRULES
         for iter in self.mRrules:
-            iter.expand(start, range, include, float_offset=float_offset)
+            if iter.expand(start, range, include, float_offset=float_offset):
+                limited = True
 
         # RDATES
         for iter in self.mRdates:
             if range.isDateWithinPeriod(iter):
                 include.append(iter)
+            else:
+                limited = True
         for iter in self.mRperiods:
             if range.isPeriodOverlap(iter):
                 include.append(iter.getStart())
+            else:
+                limited = True
 
         # Make sure the list is unique
         include = [x for x in set(include)]
-        include.sort(cmp=PyCalendarDateTime.sort)
+        include.sort(key=lambda x: x.getPosixTime())
 
         # Now create list of items to exclude
         exclude = []
@@ -201,12 +231,14 @@
 
         # Make sure the list is unique
         exclude = [x for x in set(exclude)]
-        exclude.sort(cmp=PyCalendarDateTime.sort)
+        exclude.sort(key=lambda x: x.getPosixTime())
 
         # Add difference between to the two sets (include - exclude) to the
         # results
         items.extend(set_difference(include, exclude))
+        return limited
 
+
     def changed(self):
         # RRULES
         for iter in self.mRrules:
@@ -216,6 +248,7 @@
         for iter in self.mExrules:
             iter.clear()
 
+
     def excludeFutureRecurrence(self, exclude):
         # Adjust RRULES to end before start
         for iter in self.mRrules:
@@ -227,6 +260,7 @@
             if iter > exclude:
                 self.mRperiods.remove(iter)
 
+
     # UI operations
     def isSimpleUI(self):
         # Right now the Event dialog only handles a single RRULE (but we allow
@@ -242,6 +276,7 @@
         else:
             return True
 
+
     def isAdvancedUI(self):
         # Right now the Event dialog only handles a single RRULE
         if ((len(self.mRrules) > 1) or (len(self.mExrules) > 0)
@@ -254,12 +289,14 @@
         else:
             return True
 
+
     def getUIRecurrence(self):
         if len(self.mRrules) == 1:
             return self.mRrules[0]
         else:
             return None
 
+
     def getUIDescription(self):
         # Check for anything
         if not self.hasRecurrence():

Modified: PyCalendar/trunk/src/pycalendar/recurrencevalue.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/recurrencevalue.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/recurrencevalue.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,32 +14,41 @@
 #    limitations under the License.
 ##
 
-from recurrence import PyCalendarRecurrence
-from value import PyCalendarValue
+from pycalendar import xmldefs
+from pycalendar.recurrence import PyCalendarRecurrence
+from pycalendar.value import PyCalendarValue
 
-class PyCalendarRecurrenceValue( PyCalendarValue ):
+class PyCalendarRecurrenceValue(PyCalendarValue):
 
-    def __init__( self, value = None, copyit = None ):
-        if value:
-            self.mValue = value
-        elif copyit:
-            self.mValue = PyCalendarRecurrence( copyit.mValue )
-        else:
-            self.mValue = PyCalendarRecurrence()
+    def __init__(self, value=None):
+        self.mValue = value if value is not None else PyCalendarRecurrence()
 
-    def getType( self ):
+
+    def duplicate(self):
+        return PyCalendarRecurrenceValue(self.mValue.duplicate())
+
+
+    def getType(self):
         return PyCalendarValue.VALUETYPE_RECUR
 
-    def parse( self, data ):
-        self.mValue.parse( data )
 
-    def generate( self, os ):
-        self.mValue.generate( os )
+    def parse(self, data):
+        self.mValue.parse(data)
 
-    def getValue( self ):
+
+    def generate(self, os):
+        self.mValue.generate(os)
+
+
+    def writeXML(self, node, namespace):
+        self.mValue.writeXML(node, namespace)
+
+
+    def getValue(self):
         return self.mValue
 
-    def setValue( self, value ):
+
+    def setValue(self, value):
         self.mValue = value
 
-PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_RECUR, PyCalendarRecurrenceValue)
+PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_RECUR, PyCalendarRecurrenceValue, xmldefs.value_recur)

Copied: PyCalendar/trunk/src/pycalendar/requeststatusvalue.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/requeststatusvalue.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/requeststatusvalue.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/requeststatusvalue.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,109 @@
+##
+#    Copyright (c) 2011-2012 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.
+##
+
+# iCalendar REQUEST-STATUS value
+
+from pycalendar import utils, xmldefs
+from pycalendar.parser import ParserContext
+from pycalendar.value import PyCalendarValue
+import xml.etree.cElementTree as XML
+
+class PyCalendarRequestStatusValue(PyCalendarValue):
+    """
+    The value is a list of strings (either 2 or 3 items)
+    """
+
+    def __init__(self, value=None):
+        self.mValue = value if value is not None else ["2.0", "Success"]
+
+
+    def __hash__(self):
+        return hash(tuple(self.mValue))
+
+
+    def duplicate(self):
+        return PyCalendarRequestStatusValue(self.mValue[:])
+
+
+    def getType(self):
+        return PyCalendarValue.VALUETYPE_REQUEST_STATUS
+
+
+    def parse(self, data):
+
+        # Split fields based on ;
+        code, rest = data.split(";", 1)
+
+        if "\\" in code and ParserContext.INVALID_REQUEST_STATUS_VALUE in (ParserContext.PARSER_IGNORE, ParserContext.PARSER_FIX):
+            code = code.replace("\\", "")
+        elif ParserContext.INVALID_REQUEST_STATUS_VALUE == ParserContext.PARSER_RAISE:
+            raise ValueError
+
+        # The next two items are text with possible \; sequences so we have to punt on those
+        desc = ""
+        semicolon = rest.find(";")
+        while semicolon != -1:
+            if rest[semicolon - 1] == "\\":
+                desc += rest[:semicolon + 1]
+                rest = rest[semicolon + 1:]
+                semicolon = rest.find(";")
+            else:
+                desc += rest[:semicolon]
+                rest = rest[semicolon + 1:]
+                break
+
+        if semicolon == -1:
+            desc += rest
+            rest = ""
+
+        # Decoding required
+        self.mValue = [code, utils.decodeTextValue(desc), utils.decodeTextValue(rest) if rest else None]
+
+
+    # os - StringIO object
+    def generate(self, os):
+        try:
+            # Encoding required
+            utils.writeTextValue(os, self.mValue[0])
+            os.write(";")
+            utils.writeTextValue(os, self.mValue[1])
+            if len(self.mValue) == 3 and self.mValue[2]:
+                os.write(";")
+                utils.writeTextValue(os, self.mValue[2])
+        except:
+            pass
+
+
+    def writeXML(self, node, namespace):
+        code = XML.SubElement(node, xmldefs.makeTag(namespace, xmldefs.req_status_code))
+        code.text = self.mValue[0]
+
+        description = XML.SubElement(node, xmldefs.makeTag(namespace, xmldefs.req_status_description))
+        description.text = self.mValue[1]
+
+        if len(self.mValue) == 3 and self.mValue[2]:
+            data = XML.SubElement(node, xmldefs.makeTag(namespace, xmldefs.req_status_data))
+            data.text = self.mValue[1]
+
+
+    def getValue(self):
+        return self.mValue
+
+
+    def setValue(self, value):
+        self.mValue = value
+
+PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_REQUEST_STATUS, PyCalendarRequestStatusValue, None)

Modified: PyCalendar/trunk/src/pycalendar/stringutils.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/stringutils.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/stringutils.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,10 +14,10 @@
 #    limitations under the License.
 ##
 
-import md5
+from hashlib import md5
 
 def strduptokenstr(txt, tokens):
-    
+
     result = None
     start = 0
 
@@ -32,7 +32,7 @@
 
     # Handle quoted string
     if txt[start] == '\"':
-        
+
         maxlen = len(txt)
         # Punt leading quote
         start += 1
@@ -58,12 +58,34 @@
         for relend, s in enumerate(txt[start:]):
             if s in tokens:
                 if relend:
-                    result = txt[start:start+relend]
+                    result = txt[start:start + relend]
                 else:
                     result = ""
-                return result, txt[start+relend:]
+                return result, txt[start + relend:]
         return txt[start:], ""
 
+
+
+def strtoul(s, offset=0):
+
+    max = len(s)
+    startoffset = offset
+    while offset < max:
+        if s[offset] in "0123456789":
+            offset += 1
+            continue
+        elif offset == 0:
+            raise ValueError
+        else:
+            return int(s[startoffset:offset]), offset
+    else:
+        if offset == 0:
+            raise ValueError
+        else:
+            return int(s[startoffset:]), offset
+
+
+
 def strindexfind(s, ss, default_index):
     if s and ss:
         i = 0
@@ -75,6 +97,8 @@
 
     return default_index
 
+
+
 def strnindexfind(s, ss, default_index):
     if s and ss:
         i = 0
@@ -86,6 +110,8 @@
 
     return default_index
 
+
+
 def compareStringsSafe(s1, s2):
     if s1 is None and s2 is None:
         return True
@@ -94,5 +120,7 @@
     else:
         return s1 == s2
 
+
+
 def md5digest(txt):
     return md5.new(txt).hexdigest()

Modified: PyCalendar/trunk/src/pycalendar/tests/__init__.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/tests/__init__.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/tests/__init__.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,20 +1,15 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
 ##
-
-__all__ = [
-    "calendar",
-    "duration",
-]

Deleted: PyCalendar/trunk/src/pycalendar/tests/calendar.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/tests/calendar.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/tests/calendar.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,85 +0,0 @@
-##
-#    Copyright (c) 2007 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.calendar import PyCalendar
-
-import StringIO
-import unittest
-
-class TestCalendar(unittest.TestCase):
-    
-    def testRoundtrip(self):
-        
-        data = (
-"""BEGIN:VCALENDAR
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-VERSION:2.0
-BEGIN:VEVENT
-DTEND;VALUE=DATE:20020102
-DTSTAMP:20020101T000000Z
-DTSTART;VALUE=DATE:20020101
-RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
-SUMMARY:New Year's Day
-UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
-END:VEVENT
-END:VCALENDAR
-""",
-
-"""BEGIN:VCALENDAR
-CALSCALE:GREGORIAN
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-VERSION:2.0
-X-WR-CALNAME:PayDay
-BEGIN:VTIMEZONE
-LAST-MODIFIED:20040110T032845Z
-TZID:US/Eastern
-BEGIN:DAYLIGHT
-DTSTART:20000404T020000
-RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:20001026T020000
-RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-END:VTIMEZONE
-BEGIN:VEVENT
-DTSTAMP:20050211T173501Z
-DTSTART;VALUE=DATE:20040227
-RRULE:FREQ=MONTHLY;BYDAY=-1MO,-1TU,-1WE,-1TH,-1FR;BYSETPOS=-1
-SUMMARY:PAY DAY
-UID:DC3D0301C7790B38631F1FBB at ninevah.local
-END:VEVENT
-END:VCALENDAR
-""",
-)
-
-        def _doRoundtrip(caldata):
-            cal = PyCalendar()
-            cal.parse(StringIO.StringIO(caldata))
-            
-            s = StringIO.StringIO()
-            cal.generate(s)
-            
-            self.assertEqual(caldata, s.getvalue())
-
-        for item in data:
-            _doRoundtrip(item)

Deleted: PyCalendar/trunk/src/pycalendar/tests/duration.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/tests/duration.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/tests/duration.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,54 +0,0 @@
-##
-#    Copyright (c) 2007 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 StringIO import StringIO
-
-import unittest
-
-from pycalendar.duration import PyCalendarDuration
-
-class TestDuration(unittest.TestCase):
-    
-    def testGenerate(self):
-        
-        def _doTest(d, result):
-            os = StringIO()
-            d.generate(os)
-            self.assertEqual(os.getvalue(), result)
-
-        test_data = (
-            (0, "PT0S"),
-            (1, "PT1S"),
-            (60, "PT1M"),
-            (60+2, "PT1M2S"),
-            (1*60*60, "PT1H"),
-            (1*60*60 + 2*60, "PT1H2M"),
-            (1*60*60 + 1, "PT1H0M1S"),
-            (1*60*60 + 2*60 + 1, "PT1H2M1S"),
-            (24*60*60, "P1D"),
-            (24*60*60 + 3*60*60, "P1DT3H"),
-            (24*60*60 + 2*60, "P1DT2M"),
-            (24*60*60 + 3*60*60 + 2*60, "P1DT3H2M"),
-            (24*60*60 + 1, "P1DT1S"),
-            (24*60*60 + 2*60 + 1, "P1DT2M1S"),
-            (24*60*60 + 3*60*60 + 1, "P1DT3H0M1S"),
-            (24*60*60 + 3*60*60 + 2*60 + 1, "P1DT3H2M1S"),
-            (14*24*60*60, "P2W"),
-            (15*24*60*60, "P15D"),
-            (14*24*60*60 + 1, "P14DT1S"),
-        )
-        
-        for seconds, result in test_data:
-            _doTest(PyCalendarDuration(duration=seconds), result)

Copied: PyCalendar/trunk/src/pycalendar/tests/test_adr.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/tests/test_adr.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/tests/test_adr.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/tests/test_adr.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,50 @@
+##
+#    Copyright (c) 2011-2012 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.adr import Adr
+import unittest
+
+class TestAdrValue(unittest.TestCase):
+
+    def testInit(self):
+        data = (
+            (
+                ("pobox", "extended", "street", "locality", "region", "postalcode", "country"),
+                "pobox;extended;street;locality;region;postalcode;country",
+            ),
+            (
+                (("pobox",), ("extended",), ("street1", "street2",), "locality", "region", (), "country"),
+                "pobox;extended;street1,street2;locality;region;;country",
+            ),
+        )
+
+        for args, result in data:
+            a = Adr(*args)
+
+            self.assertEqual(
+                a.getValue(),
+                args,
+            )
+
+            self.assertEqual(
+                a.getText(),
+                result,
+            )
+
+            self.assertEqual(
+                a.duplicate().getText(),
+                result,
+            )

Copied: PyCalendar/trunk/src/pycalendar/tests/test_adrvalue.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/tests/test_adrvalue.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/tests/test_adrvalue.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/tests/test_adrvalue.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,62 @@
+##
+#    Copyright (c) 2011-2012 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.adrvalue import AdrValue
+from pycalendar.vcard.property import Property
+import unittest
+
+class TestAdrValue(unittest.TestCase):
+
+    def testParseValue(self):
+
+        items = (
+            ("", ";;;;;;"),
+            (";", ";;;;;;"),
+            (";;;;;;", ";;;;;;"),
+            (";;123 Main Street;Any Town;CA;91921-1234", ";;123 Main Street;Any Town;CA;91921-1234;"),
+            (";;;;;;USA", ";;;;;;USA"),
+            ("POB1", "POB1;;;;;;"),
+            (";EXT", ";EXT;;;;;"),
+            (";;123 Main Street,The Cards;Any Town;CA;91921-1234", ";;123 Main Street,The Cards;Any Town;CA;91921-1234;"),
+            (";;123 Main\, Street,The Cards;Any Town;CA;91921-1234", ";;123 Main\, Street,The Cards;Any Town;CA;91921-1234;"),
+            (";;123 Main\, Street,The\, Cards;Any Town;CA;91921-1234", ";;123 Main\, Street,The\, Cards;Any Town;CA;91921-1234;"),
+        )
+
+        for item, result in items:
+            req = AdrValue()
+            req.parse(item)
+            test = req.getText()
+            self.assertEqual(test, result, "Failed to parse and re-generate '%s'" % (item,))
+
+
+    def testParseProperty(self):
+
+        items = (
+            ("ADR:", "ADR:;;;;;;"),
+            ("ADR:;", "ADR:;;;;;;"),
+            ("ADR:;;;;;;", "ADR:;;;;;;"),
+            ("ADR:;;123 Main Street;Any Town;CA;91921-1234", "ADR:;;123 Main Street;Any Town;CA;91921-1234;"),
+            ("ADR:;;;;;;USA", "ADR:;;;;;;USA"),
+            ("ADR:POB1", "ADR:POB1;;;;;;"),
+            ("ADR:;EXT", "ADR:;EXT;;;;;"),
+            ("ADR;VALUE=TEXT:;;123 Main Street;Any Town;CA;91921-1234", "ADR:;;123 Main Street;Any Town;CA;91921-1234;"),
+        )
+
+        for item, result in items:
+            prop = Property()
+            prop.parse(item)
+            test = prop.getText()
+            self.assertEqual(test, result + "\r\n", "Failed to parse and re-generate '%s'" % (item,))

Copied: PyCalendar/trunk/src/pycalendar/tests/test_calendar.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/tests/test_calendar.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/tests/test_calendar.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/tests/test_calendar.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,854 @@
+##
+#    Copyright (c) 2007-2012 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.calendar import PyCalendar
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.exceptions import PyCalendarInvalidData
+from pycalendar.parser import ParserContext
+from pycalendar.period import PyCalendarPeriod
+from pycalendar.property import PyCalendarProperty
+import cStringIO as StringIO
+import difflib
+import unittest
+
+class TestCalendar(unittest.TestCase):
+
+    data = (
+"""BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+
+"""BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+X-WR-CALNAME:PayDay
+BEGIN:VTIMEZONE
+TZID:US/Eastern
+LAST-MODIFIED:20040110T032845Z
+BEGIN:DAYLIGHT
+DTSTART:20000404T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20001026T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+UID:DC3D0301C7790B38631F1FBB at ninevah.local
+DTSTART;VALUE=DATE:20040227
+DTSTAMP:20050211T173501Z
+RRULE:FREQ=MONTHLY;BYDAY=-1MO,-1TU,-1WE,-1TH,-1FR;BYSETPOS=-1
+SUMMARY:PAY DAY
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Alarm for Organizer!
+TRIGGER;RELATED=START:-PT15M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+
+"""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+EXDATE:20081114T000000Z
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+
+"""BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+ATTACH:http://example.com/test.jpg
+DTSTAMP:20020101T000000Z
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+
+"""BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+ATTACH;ENCODING=BASE64;VALUE=BINARY:dGVzdA==
+DTSTAMP:20020101T000000Z
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+
+"""BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+BEGIN:VTIMEZONE
+TZID:America/Montreal
+LAST-MODIFIED:20040110T032845Z
+BEGIN:DAYLIGHT
+DTSTART:20000404T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20001026T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VAVAILABILITY
+UID:20061005T133225Z-00001-availability at example.com
+DTSTART;TZID=America/Montreal:20060101T000000
+DTEND;TZID=America/Montreal:20060108T000000
+DTSTAMP:20061005T133225Z
+ORGANIZER:mailto:bernard at example.com
+BEGIN:AVAILABLE
+UID:20061005T133225Z-00001-A-availability at example.com
+DTSTART;TZID=America/Montreal:20060102T090000
+DTEND;TZID=America/Montreal:20060102T120000
+DTSTAMP:20061005T133225Z
+RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR
+SUMMARY:Monday\\, Wednesday and Friday from 9:00 to 12:00
+END:AVAILABLE
+BEGIN:AVAILABLE
+UID:20061005T133225Z-00001-A-availability at example.com
+RECURRENCE-ID;TZID=America/Montreal:20060106T090000
+DTSTART;TZID=America/Montreal:20060106T120000
+DTEND;TZID=America/Montreal:20060106T170000
+DTSTAMP:20061005T133225Z
+SUMMARY:Friday override from 12:00 to 17:00
+END:AVAILABLE
+END:VAVAILABILITY
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+
+"""BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+BEGIN:VTODO
+UID:event1 at ninevah.local
+CREATED:20060101T150000Z
+DTSTAMP:20051222T205953Z
+SUMMARY:event 1
+END:VTODO
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+
+"""BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+BEGIN:X-COMPONENT
+UID:1234
+END:X-COMPONENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+
+"""BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+BEGIN:VTIMEZONE
+TZID:US/Pacific
+BEGIN:DAYLIGHT
+DTSTART:20070311T020000
+RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
+TZNAME:PDT
+TZOFFSETFROM:-0800
+TZOFFSETTO:-0700
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20071104T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
+TZNAME:PST
+TZOFFSETFROM:-0700
+TZOFFSETTO:-0800
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+UID:uid4
+DTSTART;TZID=US/Pacific:20100207T170000
+DTEND;TZID=US/Pacific:20100207T173000
+CREATED:20100203T013849Z
+DTSTAMP:20100203T013909Z
+SEQUENCE:3
+SUMMARY:New Event
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:AUDIO
+ATTACH:Basso
+TRIGGER:-PT20M
+X-WR-ALARMUID:1377CCC7-F85C-4610-8583-9513D4B364E1
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+
+"""BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+ATTACH:http://example.com/test.jpg
+DTSTAMP:20020101T000000Z
+SUMMARY:New Year's Day
+X-APPLE-STRUCTURED-LOCATION:geo:123.123,123.123
+X-Test:Some\, text.
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+
+"""BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+ATTACH:http://example.com/test.jpg
+DTSTAMP:20020101T000000Z
+SUMMARY:New Year's Day
+X-APPLE-STRUCTURED-LOCATION;VALUE=URI:geo:123.123,123.123
+X-Test:Some\, text.
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+
+)
+    data2 = (
+                (
+"""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+EXDATE:20081114T000000Z
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+X-TEST:Testing
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+
+"""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+X-TEST:Testing
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+EXDATE:20081114T000000Z
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                ),
+)
+
+
+    def testRoundtrip(self):
+
+
+        def _doRoundtrip(caldata, resultdata=None):
+            test1 = resultdata if resultdata is not None else caldata
+
+            cal = PyCalendar()
+            cal.parse(StringIO.StringIO(caldata))
+
+            s = StringIO.StringIO()
+            cal.generate(s)
+            test2 = s.getvalue()
+
+            self.assertEqual(
+                test1,
+                test2,
+                "\n".join(difflib.unified_diff(str(test1).splitlines(), test2.splitlines()))
+            )
+
+        for item in self.data:
+            _doRoundtrip(item)
+
+        for item1, item2 in self.data2:
+            _doRoundtrip(item1, item2)
+
+
+    def testRoundtripDuplicate(self):
+
+
+        def _doDuplicateRoundtrip(caldata):
+            cal = PyCalendar()
+            cal.parse(StringIO.StringIO(caldata))
+            cal = cal.duplicate()
+
+            s = StringIO.StringIO()
+            cal.generate(s)
+            self.assertEqual(caldata, s.getvalue())
+
+        for item in self.data:
+            _doDuplicateRoundtrip(item)
+
+
+    def testEquality(self):
+
+
+        def _doEquality(caldata):
+            cal1 = PyCalendar()
+            cal1.parse(StringIO.StringIO(caldata))
+
+            cal2 = PyCalendar()
+            cal2.parse(StringIO.StringIO(caldata))
+
+            self.assertEqual(cal1, cal2, "%s\n\n%s" % (cal1, cal2,))
+
+
+        def _doNonEquality(caldata):
+            cal1 = PyCalendar()
+            cal1.parse(StringIO.StringIO(caldata))
+
+            cal2 = PyCalendar()
+            cal2.parse(StringIO.StringIO(caldata))
+            cal2.addProperty(PyCalendarProperty("X-FOO", "BAR"))
+
+            self.assertNotEqual(cal1, cal2)
+
+        for item in self.data:
+            _doEquality(item)
+            _doNonEquality(item)
+
+
+    def testParseComponent(self):
+
+        data1 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+        data2 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+BEGIN:VTIMEZONE
+TZID:America/Montreal
+LAST-MODIFIED:20040110T032845Z
+BEGIN:DAYLIGHT
+DTSTART:20000404T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20001026T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+        result = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VTIMEZONE
+TZID:America/Montreal
+LAST-MODIFIED:20040110T032845Z
+BEGIN:DAYLIGHT
+DTSTART:20000404T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20001026T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+        cal = PyCalendar()
+        cal.parse(StringIO.StringIO(data1))
+        cal.parseComponent(StringIO.StringIO(data2))
+        self.assertEqual(str(cal), result)
+
+
+    def testParseFail(self):
+
+        data = (
+"""BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+""".replace("\n", "\r\n"),
+
+"""BEGIN:VCARD
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+VERSION:2.0
+END:VCARD
+""".replace("\n", "\r\n"),
+
+"""BOGUS
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+
+"""BOGUS
+
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+
+"""BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+BOGUS
+""".replace("\n", "\r\n"),
+
+"""BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+
+BOGUS
+""".replace("\n", "\r\n"),
+
+        )
+
+        for item in data:
+            self.assertRaises(PyCalendarInvalidData, PyCalendar.parseText, item)
+
+
+    def testParseBlank(self):
+
+        data = (
+"""
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+
+"""
+
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+
+"""BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+
+
+""".replace("\n", "\r\n"),
+
+"""BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+
+"""BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+
+
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+        )
+
+        save = ParserContext.BLANK_LINES_IN_DATA
+        for item in data:
+            ParserContext.BLANK_LINES_IN_DATA = ParserContext.PARSER_RAISE
+            self.assertRaises(PyCalendarInvalidData, PyCalendar.parseText, item)
+
+            ParserContext.BLANK_LINES_IN_DATA = ParserContext.PARSER_IGNORE
+            lines = item.split("\r\n")
+            result = "\r\n".join([line for line in lines if line]) + "\r\n"
+            self.assertEqual(str(PyCalendar.parseText(item)), result)
+
+        ParserContext.BLANK_LINES_IN_DATA = save
+
+
+    def testGetVEvents(self):
+
+        data = (
+            (
+                "Non-recurring match",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20110601
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                (PyCalendarDateTime(2011, 6, 1),),
+            ),
+            (
+                "Non-recurring no-match",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20110501
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                (),
+            ),
+            (
+                "Recurring match",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20110601
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=DAILY;COUNT=2
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                (
+                    PyCalendarDateTime(2011, 6, 1),
+                    PyCalendarDateTime(2011, 6, 2),
+                ),
+            ),
+            (
+                "Recurring no match",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20110501
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=DAILY;COUNT=2
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                (),
+            ),
+            (
+                "Recurring with override match",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART:20110601T120000
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=DAILY;COUNT=2
+SUMMARY:New Year's Day
+END:VEVENT
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+RECURRENCE-ID;VALUE=DATE:20110602T120000
+DTSTART;VALUE=DATE:20110602T130000
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                (
+                    PyCalendarDateTime(2011, 6, 1, 12, 0, 0),
+                    PyCalendarDateTime(2011, 6, 2, 13, 0, 0),
+                ),
+            ),
+            (
+                "Recurring with override no match",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART:20110501T120000
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=DAILY;COUNT=2
+SUMMARY:New Year's Day
+END:VEVENT
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+RECURRENCE-ID;VALUE=DATE:20110502T120000
+DTSTART;VALUE=DATE:20110502T130000
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                (),
+            ),
+            (
+                "Recurring partial match",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20110531
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=DAILY;COUNT=2
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                (
+                    PyCalendarDateTime(2011, 6, 1),
+                ),
+            ),
+            (
+                "Recurring with override partial match",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART:20110531T120000
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=DAILY;COUNT=2
+SUMMARY:New Year's Day
+END:VEVENT
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+RECURRENCE-ID;VALUE=DATE:20110601T120000
+DTSTART;VALUE=DATE:20110601T130000
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                (
+                    PyCalendarDateTime(2011, 6, 1, 13, 0, 0),
+                ),
+            ),
+        )
+
+        for title, caldata, result in data:
+            calendar = PyCalendar.parseText(caldata)
+            instances = []
+            calendar.getVEvents(
+                PyCalendarPeriod(
+                    start=PyCalendarDateTime(2011, 6, 1),
+                    end=PyCalendarDateTime(2011, 7, 1),
+                ),
+                instances
+            )
+            instances = tuple([instance.getInstanceStart() for instance in instances])
+            self.assertEqual(instances, result, "Failed in %s: got %s, expected %s" % (title, instances, result))

Copied: PyCalendar/trunk/src/pycalendar/tests/test_componentrecur.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/tests/test_componentrecur.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/tests/test_componentrecur.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/tests/test_componentrecur.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,66 @@
+#    Copyright (c) 2007-2012 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.calendar import PyCalendar
+import cStringIO as StringIO
+import unittest
+
+class TestCalendar(unittest.TestCase):
+
+    def testDuplicateWithRecurrenceChange(self):
+
+        data = (
+"""BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+"""BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;COUNT=400
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+)
+
+        cal1 = PyCalendar()
+        cal1.parse(StringIO.StringIO(data[0]))
+        cal2 = cal1.duplicate()
+        vevent = cal2.getComponents()[0]
+        rrules = vevent.getRecurrenceSet()
+        for rrule in rrules.getRules():
+            rrule.setUseCount(True)
+            rrule.setCount(400)
+            rrules.changed()
+
+        self.assertEqual(data[0], str(cal1))
+        self.assertEqual(data[1], str(cal2))

Copied: PyCalendar/trunk/src/pycalendar/tests/test_datetime.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/tests/test_datetime.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/tests/test_datetime.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/tests/test_datetime.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,324 @@
+##
+#    Copyright (c) 2011-2012 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.calendar import PyCalendar
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.timezone import PyCalendarTimezone
+import unittest
+
+class TestDateTime(unittest.TestCase):
+
+    def testDuplicateASUTC(self):
+
+        items = (
+            (
+                PyCalendarDateTime(2011, 1, 1, 0, 0, 0, tzid=PyCalendarTimezone(utc=True)),
+                PyCalendarDateTime(2011, 1, 1, 0, 0, 0, tzid=PyCalendarTimezone(utc=True)),
+            ),
+            (
+                PyCalendarDateTime(2011, 1, 1, 0, 0, 0),
+                PyCalendarDateTime(2011, 1, 1, 0, 0, 0),
+            ),
+            (
+                PyCalendarDateTime(2011, 1, 1),
+                PyCalendarDateTime(2011, 1, 1),
+            )
+        )
+
+        for item, result in items:
+            dup = item.duplicateAsUTC()
+            self.assertEqual(str(dup), str(result), "Failed to convert '%s'" % (item,))
+
+
+    def testDuplicateInSet(self):
+
+        s = set(
+            (
+                PyCalendarDateTime(2011, 1, 1, 0, 0, 0, tzid=PyCalendarTimezone(utc=True)),
+                PyCalendarDateTime(2011, 1, 2, 0, 0, 0, tzid=PyCalendarTimezone(utc=True)),
+            )
+        )
+
+        self.assertTrue(PyCalendarDateTime(2011, 1, 1, 0, 0, 0, tzid=PyCalendarTimezone(utc=True)) in s)
+        self.assertFalse(PyCalendarDateTime(2011, 1, 3, 0, 0, 0, tzid=PyCalendarTimezone(utc=True)) in s)
+
+
+    def testRoundtrip(self):
+
+        data1 = (
+            "20110102",
+            "20110103T121212",
+            "20110103T121212Z",
+            "00010102",
+            "00010103T121212",
+            "00010103T121212Z",
+        )
+
+        data2 = (
+            ("20110102", "20110102"),
+            ("2011-01-02", "20110102"),
+            ("20110103T121212", "20110103T121212"),
+            ("2011-01-03T12:12:12", "20110103T121212"),
+            ("20110103T121212Z", "20110103T121212Z"),
+            ("2011-01-03T12:12:12Z", "20110103T121212Z"),
+            ("20110103T121212+0100", "20110103T121212+0100"),
+            ("2011-01-03T12:12:12-0500", "20110103T121212-0500"),
+            ("20110103T121212,123", "20110103T121212"),
+            ("2011-01-03T12:12:12,123", "20110103T121212"),
+            ("20110103T121212,123Z", "20110103T121212Z"),
+            ("2011-01-03T12:12:12,123Z", "20110103T121212Z"),
+            ("20110103T121212,123+0100", "20110103T121212+0100"),
+            ("2011-01-03T12:12:12,123-0500", "20110103T121212-0500"),
+        )
+
+        for item in data1:
+            dt = PyCalendarDateTime.parseText(item, False)
+            self.assertEqual(dt.getText(), item, "Failed on: %s" % (item,))
+
+        for item, result in data2:
+            dt = PyCalendarDateTime.parseText(item, True)
+            self.assertEqual(dt.getText(), result, "Failed on: %s" % (item,))
+
+
+    def testBadParse(self):
+
+        data1 = (
+            "2011",
+            "201101023",
+            "20110103t121212",
+            "20110103T1212",
+            "20110103T1212123",
+            "20110103T121212A",
+            "2011-01-03T121212Z",
+            "20110103T12:12:12Z",
+            "20110103T121212+0500",
+            "   10102",
+            "   10102T010101",
+            "2011 102",
+            "201101 3T121212",
+            "-1110102",
+            "2011-102",
+            "201101-3T121212",
+        )
+        data2 = (
+            "2011-01+02",
+            "20110103T12-12-12",
+            "2011-01-03T12:12:12,",
+            "2011-01-03T12:12:12,ABC",
+            "20110103T12:12:12-1",
+        )
+
+        for item in data1:
+            self.assertRaises(ValueError, PyCalendarDateTime.parseText, item, False)
+
+        for item in data2:
+            self.assertRaises(ValueError, PyCalendarDateTime.parseText, item, False)
+
+
+    def testCachePreserveOnAdjustment(self):
+
+        # UTC first
+        dt = PyCalendarDateTime(2012, 6, 7, 12, 0, 0, PyCalendarTimezone(tzid="utc"))
+        dt.getPosixTime()
+
+        # check existing cache is complete
+        self.assertTrue(dt.mPosixTimeCached)
+        self.assertNotEqual(dt.mPosixTime, 0)
+        self.assertEqual(dt.mTZOffset, None)
+
+        # duplicate preserves cache details
+        dt2 = dt.duplicate()
+        self.assertTrue(dt2.mPosixTimeCached)
+        self.assertEqual(dt2.mPosixTime, dt.mPosixTime)
+        self.assertEqual(dt2.mTZOffset, dt.mTZOffset)
+
+        # adjust preserves cache details
+        dt2.adjustToUTC()
+        self.assertTrue(dt2.mPosixTimeCached)
+        self.assertEqual(dt2.mPosixTime, dt.mPosixTime)
+        self.assertEqual(dt2.mTZOffset, dt.mTZOffset)
+
+        # Now timezone
+        tzdata = """BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//calendarserver.org//Zonal//EN
+VERSION:2.0
+BEGIN:VTIMEZONE
+TZID:America/Pittsburgh
+BEGIN:STANDARD
+DTSTART:18831118T120358
+RDATE:18831118T120358
+TZNAME:EST
+TZOFFSETFROM:-045602
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19180331T020000
+RRULE:FREQ=YEARLY;UNTIL=19190330T070000Z;BYDAY=-1SU;BYMONTH=3
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19181027T020000
+RRULE:FREQ=YEARLY;UNTIL=19191026T060000Z;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:STANDARD
+DTSTART:19200101T000000
+RDATE:19200101T000000
+RDATE:19420101T000000
+RDATE:19460101T000000
+RDATE:19670101T000000
+TZNAME:EST
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19200328T020000
+RDATE:19200328T020000
+RDATE:19740106T020000
+RDATE:19750223T020000
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19201031T020000
+RDATE:19201031T020000
+RDATE:19450930T020000
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19210424T020000
+RRULE:FREQ=YEARLY;UNTIL=19410427T070000Z;BYDAY=-1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19210925T020000
+RRULE:FREQ=YEARLY;UNTIL=19410928T060000Z;BYDAY=-1SU;BYMONTH=9
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19420209T020000
+RDATE:19420209T020000
+TZNAME:EWT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+DTSTART:19450814T190000
+RDATE:19450814T190000
+TZNAME:EPT
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+DTSTART:19460428T020000
+RRULE:FREQ=YEARLY;UNTIL=19660424T070000Z;BYDAY=-1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19460929T020000
+RRULE:FREQ=YEARLY;UNTIL=19540926T060000Z;BYDAY=-1SU;BYMONTH=9
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:STANDARD
+DTSTART:19551030T020000
+RRULE:FREQ=YEARLY;UNTIL=19661030T060000Z;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19670430T020000
+RRULE:FREQ=YEARLY;UNTIL=19730429T070000Z;BYDAY=-1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;UNTIL=20061029T060000Z;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19760425T020000
+RRULE:FREQ=YEARLY;UNTIL=19860427T070000Z;BYDAY=-1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+DTSTART:19870405T020000
+RRULE:FREQ=YEARLY;UNTIL=20060402T070000Z;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+DTSTART:20070311T020000
+RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20071104T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+        PyCalendar.parseText(tzdata)
+
+        dt = PyCalendarDateTime(2012, 6, 7, 12, 0, 0, PyCalendarTimezone(tzid="America/Pittsburgh"))
+        dt.getPosixTime()
+
+        # check existing cache is complete
+        self.assertTrue(dt.mPosixTimeCached)
+        self.assertNotEqual(dt.mPosixTime, 0)
+        self.assertEqual(dt.mTZOffset, -14400)
+
+        # duplicate preserves cache details
+        dt2 = dt.duplicate()
+        self.assertTrue(dt2.mPosixTimeCached)
+        self.assertEqual(dt2.mPosixTime, dt.mPosixTime)
+        self.assertEqual(dt2.mTZOffset, dt.mTZOffset)
+
+        # adjust preserves cache details
+        dt2.adjustToUTC()
+        self.assertTrue(dt2.mPosixTimeCached)
+        self.assertEqual(dt2.mPosixTime, dt.mPosixTime)
+        self.assertEqual(dt2.mTZOffset, 0)

Copied: PyCalendar/trunk/src/pycalendar/tests/test_duration.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/tests/test_duration.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/tests/test_duration.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/tests/test_duration.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,101 @@
+##
+#    Copyright (c) 2007-2012 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 cStringIO import StringIO
+from pycalendar.duration import PyCalendarDuration
+from pycalendar.parser import ParserContext
+import unittest
+
+class TestDuration(unittest.TestCase):
+
+    test_data = (
+        (0, "PT0S"),
+        (1, "PT1S"),
+        (1, "+PT1S"),
+        (-1, "-PT1S"),
+        (60, "PT1M"),
+        (60 + 2, "PT1M2S"),
+        (1 * 60 * 60, "PT1H"),
+        (1 * 60 * 60 + 2 * 60, "PT1H2M"),
+        (1 * 60 * 60 + 1, "PT1H0M1S"),
+        (1 * 60 * 60 + 2 * 60 + 1, "PT1H2M1S"),
+        (24 * 60 * 60, "P1D"),
+        (24 * 60 * 60 + 3 * 60 * 60, "P1DT3H"),
+        (24 * 60 * 60 + 2 * 60, "P1DT2M"),
+        (24 * 60 * 60 + 3 * 60 * 60 + 2 * 60, "P1DT3H2M"),
+        (24 * 60 * 60 + 1, "P1DT1S"),
+        (24 * 60 * 60 + 2 * 60 + 1, "P1DT2M1S"),
+        (24 * 60 * 60 + 3 * 60 * 60 + 1, "P1DT3H0M1S"),
+        (24 * 60 * 60 + 3 * 60 * 60 + 2 * 60 + 1, "P1DT3H2M1S"),
+        (14 * 24 * 60 * 60, "P2W"),
+        (15 * 24 * 60 * 60, "P15D"),
+        (14 * 24 * 60 * 60 + 1, "P14DT1S"),
+    )
+
+    def testGenerate(self):
+
+        def _doTest(d, result):
+
+            if result[0] == "+":
+                result = result[1:]
+            os = StringIO()
+            d.generate(os)
+            self.assertEqual(os.getvalue(), result)
+
+        for seconds, result in TestDuration.test_data:
+            _doTest(PyCalendarDuration(duration=seconds), result)
+
+
+    def testParse(self):
+
+        for seconds, result in TestDuration.test_data:
+            duration = PyCalendarDuration().parseText(result)
+            self.assertEqual(duration.getTotalSeconds(), seconds)
+
+
+    def testParseBad(self):
+
+        test_bad_data = (
+            "",
+            "ABC",
+            "P",
+            "PABC",
+            "P12",
+            "P12D1",
+            "P12DTAB",
+            "P12DT1HA",
+            "P12DT1MA",
+            "P12DT1SA",
+        )
+        for data in test_bad_data:
+            self.assertRaises(ValueError, PyCalendarDuration.parseText, data)
+
+
+    def testRelaxedBad(self):
+
+        test_relaxed_data = (
+            ("P12DT", 12 * 24 * 60 * 60, "P12D"),
+            ("-P1WT", -7 * 24 * 60 * 60, "-P1W"),
+            ("-P1W1D", -7 * 24 * 60 * 60, "-P1W"),
+        )
+        for text, seconds, result in test_relaxed_data:
+
+            ParserContext.INVALID_DURATION_VALUE = ParserContext.PARSER_FIX
+            self.assertEqual(PyCalendarDuration.parseText(text).getTotalSeconds(), seconds)
+            self.assertEqual(PyCalendarDuration.parseText(text).getText(), result)
+
+            ParserContext.INVALID_DURATION_VALUE = ParserContext.PARSER_RAISE
+            self.assertRaises(ValueError, PyCalendarDuration.parseText, text)

Copied: PyCalendar/trunk/src/pycalendar/tests/test_i18n.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/tests/test_i18n.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/tests/test_i18n.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/tests/test_i18n.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,74 @@
+# coding: utf-8
+##
+#    Copyright (c) 2007-2012 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.attribute import PyCalendarAttribute
+from pycalendar.calendar import PyCalendar
+import cStringIO as StringIO
+import unittest
+
+class TestCalendar(unittest.TestCase):
+
+    def testAddCN(self):
+
+        data = (
+"""BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+ORGANIZER:user01 at example.com
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+
+    "まだ",
+
+"""BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+ORGANIZER;CN=まだ:user01 at example.com
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+
+        )
+
+        cal1 = PyCalendar()
+        cal1.parse(StringIO.StringIO(data[0]))
+
+        vevent = cal1.getComponents("VEVENT")[0]
+        organizer = vevent.getProperties("ORGANIZER")[0]
+        organizer.addAttribute(PyCalendarAttribute("CN", data[1]))
+
+        cal2 = PyCalendar()
+        cal2.parse(StringIO.StringIO(data[2]))
+
+        self.assertEqual(str(cal1), str(cal2))

Copied: PyCalendar/trunk/src/pycalendar/tests/test_multivalue.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/tests/test_multivalue.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/tests/test_multivalue.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/tests/test_multivalue.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,45 @@
+##
+#    Copyright (c) 2011-2012 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.multivalue import PyCalendarMultiValue
+from pycalendar.value import PyCalendarValue
+
+class TestMultiValue(unittest.TestCase):
+
+    def testParseValue(self):
+
+        items = (
+            ("", "", 1),
+            ("Example", "Example", 1),
+            ("Example1,Example2", "Example1,Example2", 2),
+        )
+
+        for item, result, count in items:
+            req = PyCalendarMultiValue(PyCalendarValue.VALUETYPE_TEXT)
+            req.parse(item)
+            test = req.getText()
+            self.assertEqual(test, result, "Failed to parse and re-generate '%s'" % (item,))
+            self.assertEqual(len(req.mValues), count, "Failed to parse and re-generate '%s'" % (item,))
+
+
+    def testSetValue(self):
+
+        req = PyCalendarMultiValue(PyCalendarValue.VALUETYPE_TEXT)
+        req.parse("Example1, Example2")
+        req.setValue(("Example3", "Example4",))
+        test = req.getText()
+        self.assertEqual(test, "Example3,Example4")

Copied: PyCalendar/trunk/src/pycalendar/tests/test_n.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/tests/test_n.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/tests/test_n.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/tests/test_n.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,92 @@
+##
+#    Copyright (c) 2011-2012 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.n import N
+import unittest
+
+class TestAdrValue(unittest.TestCase):
+
+    def testInit(self):
+
+        data = (
+            (
+                ("last", "first", "middle", "prefix", "suffix"),
+                "last;first;middle;prefix;suffix",
+                "prefix first middle last suffix",
+            ),
+            (
+                ("last", ("first",), ("middle1", "middle2",), (), ("suffix",)),
+                "last;first;middle1,middle2;;suffix",
+                "first middle1 middle2 last suffix",
+            ),
+        )
+
+        for args, result, fullName in data:
+            n = N(*args)
+
+            self.assertEqual(
+                n.getValue(),
+                args,
+            )
+
+            self.assertEqual(
+                n.getText(),
+                result,
+            )
+
+            self.assertEqual(
+                n.getFullName(),
+                fullName,
+            )
+
+            self.assertEqual(
+                n.duplicate().getText(),
+                result,
+            )
+
+
+    def testInitWithKeywords(self):
+
+        data = (
+            (
+                {"first": "first", "last": "last", "middle": "middle", "prefix": "prefix", "suffix": "suffix"},
+                "last;first;middle;prefix;suffix",
+                "prefix first middle last suffix",
+            ),
+            (
+                {"first": ("first",), "last": "last", "middle": ("middle1", "middle2",), "prefix": (), "suffix": ("suffix",)},
+                "last;first;middle1,middle2;;suffix",
+                "first middle1 middle2 last suffix",
+            ),
+        )
+
+        for kwargs, result, fullName in data:
+            n = N(**kwargs)
+
+            self.assertEqual(
+                n.getText(),
+                result,
+            )
+
+            self.assertEqual(
+                n.getFullName(),
+                fullName,
+            )
+
+            self.assertEqual(
+                n.duplicate().getText(),
+                result,
+            )

Copied: PyCalendar/trunk/src/pycalendar/tests/test_nvalue.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/tests/test_nvalue.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/tests/test_nvalue.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/tests/test_nvalue.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,59 @@
+##
+#    Copyright (c) 2011-2012 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.nvalue import NValue
+from pycalendar.vcard.property import Property
+import unittest
+
+class TestNValue(unittest.TestCase):
+
+    def testParseValue(self):
+
+        items = (
+            ("", ";;;;"),
+            (";", ";;;;"),
+            (";;;;", ";;;;"),
+            ("Cyrus;Daboo;;Dr.", "Cyrus;Daboo;;Dr.;"),
+            (";;;;PhD.", ";;;;PhD."),
+            ("Cyrus", "Cyrus;;;;"),
+            (";Daboo", ";Daboo;;;"),
+        )
+
+        for item, result in items:
+            req = NValue()
+            req.parse(item)
+            test = req.getText()
+            self.assertEqual(test, result, "Failed to parse and re-generate '%s'" % (item,))
+
+
+    def testParseProperty(self):
+
+        items = (
+            ("N:", "N:;;;;"),
+            ("N:;", "N:;;;;"),
+            ("N:;;;;", "N:;;;;"),
+            ("N:Cyrus;Daboo;;Dr.", "N:Cyrus;Daboo;;Dr.;"),
+            ("N:;;;;PhD.", "N:;;;;PhD."),
+            ("N:Cyrus", "N:Cyrus;;;;"),
+            ("N:;Daboo", "N:;Daboo;;;"),
+            ("N;VALUE=TEXT:Cyrus;Daboo;;Dr.", "N:Cyrus;Daboo;;Dr.;"),
+        )
+
+        for item, result in items:
+            prop = Property()
+            prop.parse(item)
+            test = prop.getText()
+            self.assertEqual(test, result + "\r\n", "Failed to parse and re-generate '%s'" % (item,))

Copied: PyCalendar/trunk/src/pycalendar/tests/test_orgvalue.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/tests/test_orgvalue.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/tests/test_orgvalue.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/tests/test_orgvalue.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,53 @@
+##
+#    Copyright (c) 2011-2012 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.orgvalue import OrgValue
+from pycalendar.vcard.property import Property
+import unittest
+
+class TestNValue(unittest.TestCase):
+
+    def testParseValue(self):
+
+        items = (
+            ("", ""),
+            ("Example", "Example"),
+            ("Example\, Inc.", "Example\, Inc."),
+            ("Example\; Inc;Dept. of Silly Walks", "Example\; Inc;Dept. of Silly Walks"),
+        )
+
+        for item, result in items:
+            req = OrgValue()
+            req.parse(item)
+            test = req.getText()
+            self.assertEqual(test, result, "Failed to parse and re-generate '%s'" % (item,))
+
+
+    def testParseProperty(self):
+
+        items = (
+            ("ORG:", "ORG:"),
+            ("ORG:Example", "ORG:Example"),
+            ("ORG:Example\, Inc.", "ORG:Example\, Inc."),
+            ("ORG:Example\; Inc;Dept. of Silly Walks", "ORG:Example\; Inc;Dept. of Silly Walks"),
+            ("ORG;VALUE=TEXT:Example", "ORG:Example"),
+        )
+
+        for item, result in items:
+            prop = Property()
+            prop.parse(item)
+            test = prop.getText()
+            self.assertEqual(test, result + "\r\n", "Failed to parse and re-generate '%s'" % (item,))

Copied: PyCalendar/trunk/src/pycalendar/tests/test_property.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/tests/test_property.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/tests/test_property.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/tests/test_property.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,189 @@
+##
+#    Copyright (c) 2007-2012 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.attribute import PyCalendarAttribute
+from pycalendar.exceptions import PyCalendarInvalidProperty
+from pycalendar.parser import ParserContext
+from pycalendar.property import PyCalendarProperty
+from pycalendar.value import PyCalendarValue
+import unittest
+
+class TestProperty(unittest.TestCase):
+
+    test_data = (
+        # Different value types
+        "ATTACH;VALUE=BINARY:VGVzdA==",
+        "attach;VALUE=BINARY:VGVzdA==",
+        "ORGANIZER:mailto:jdoe at example.com",
+        "DTSTART;TZID=US/Eastern:20060226T120000",
+        "DTSTART;VALUE=DATE:20060226",
+        "DTSTART:20060226T130000Z",
+        "X-FOO:BAR",
+        "DURATION:PT10M",
+        "duraTION:PT10M",
+        "SEQUENCE:1",
+        "RDATE:20060226T120000Z,20060227T120000Z",
+        "FREEBUSY:20060226T120000Z/20060227T120000Z",
+        "SUMMARY:Some \\ntext",
+        "RRULE:FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=-1",
+        "REQUEST-STATUS:2.0;Success",
+        "URI:http://www.example.com",
+        "TZOFFSETFROM:-0500",
+        "X-Test:Some\, text.",
+        "X-Test:Some:, text.",
+        "X-APPLE-STRUCTURED-LOCATION;VALUE=URI:geo:123.123,123.123",
+        "X-CALENDARSERVER-PRIVATE-COMMENT:This\\ntest\\nis\\, here.\\n",
+
+        # Various parameters
+        "DTSTART;TZID=\"Somewhere, else\":20060226T120000",
+        "ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:jdoe at example.com",
+
+        # Parameter escaping
+        "ATTENDEE;CN=My ^'Test^' Name;ROLE=CHAIR:mailto:jdoe at example.com",
+    )
+
+
+    def testParseGenerate(self):
+
+        for data in TestProperty.test_data:
+            prop = PyCalendarProperty()
+            prop.parse(data)
+            propstr = str(prop)
+            self.assertEqual(propstr[:-2], data, "Failed parse/generate: %s to %s" % (data, propstr,))
+
+
+    def testEquality(self):
+
+        for data in TestProperty.test_data:
+            prop1 = PyCalendarProperty()
+            prop1.parse(data)
+            prop2 = PyCalendarProperty()
+            prop2.parse(data)
+            self.assertEqual(prop1, prop2, "Failed equality: %s" % (data,))
+
+
+    def testParseBad(self):
+
+        test_bad_data = (
+            "DTSTART;TZID=US/Eastern:abc",
+            "DTSTART;VALUE=DATE:20060226T",
+            "DTSTART:20060226T120000A",
+            "X-FOO;:BAR",
+            "DURATION:A",
+            "SEQUENCE:b",
+            "RDATE:20060226T120000Z;20060227T120000Z",
+            "FREEBUSY:20060226T120000Z/ABC",
+            "SUMMARY:Some \\qtext",
+            "RRULE:FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,VE;BYSETPOS=-1",
+            "REQUEST-STATUS:2.0,Success",
+            "TZOFFSETFROM:-050",
+            """ATTENDEE;CN="\\";CUTYPE=INDIVIDUAL;PARTSTAT=X-UNDELIVERABLE:invalid:nomai
+ l""",
+        )
+        save = ParserContext.INVALID_ESCAPE_SEQUENCES
+        for data in test_bad_data:
+            ParserContext.INVALID_ESCAPE_SEQUENCES = ParserContext.PARSER_RAISE
+            prop = PyCalendarProperty()
+            self.assertRaises(PyCalendarInvalidProperty, prop.parse, data)
+        ParserContext.INVALID_ESCAPE_SEQUENCES = save
+
+
+    def testHash(self):
+
+        hashes = []
+        for item in TestProperty.test_data:
+            prop = PyCalendarProperty()
+            prop.parse(item)
+            hashes.append(hash(prop))
+        hashes.sort()
+        for i in range(1, len(hashes)):
+            self.assertNotEqual(hashes[i - 1], hashes[i])
+
+
+    def testDefaultValueCreate(self):
+
+        test_data = (
+            ("ATTENDEE", "mailto:attendee at example.com", "ATTENDEE:mailto:attendee at example.com\r\n"),
+            ("attendee", "mailto:attendee at example.com", "attendee:mailto:attendee at example.com\r\n"),
+            ("ORGANIZER", "mailto:organizer at example.com", "ORGANIZER:mailto:organizer at example.com\r\n"),
+            ("ORGANizer", "mailto:organizer at example.com", "ORGANizer:mailto:organizer at example.com\r\n"),
+            ("URL", "http://example.com/tz1", "URL:http://example.com/tz1\r\n"),
+            ("TZURL", "http://example.com/tz2", "TZURL:http://example.com/tz2\r\n"),
+        )
+        for propname, propvalue, result in test_data:
+            prop = PyCalendarProperty(name=propname, value=propvalue)
+            self.assertEqual(str(prop), result)
+
+
+    def testGEOValueRoundtrip(self):
+
+        data = "GEO:123.456,789.101"
+        prop = PyCalendarProperty()
+        prop.parse(data)
+        self.assertEqual(str(prop), data + "\r\n")
+
+
+    def testUnknownValueRoundtrip(self):
+
+        data = "X-FOO:Text, not escaped"
+        prop = PyCalendarProperty()
+        prop.parse(data)
+        self.assertEqual(str(prop), data + "\r\n")
+
+        prop = PyCalendarProperty("X-FOO", "Text, not escaped")
+        self.assertEqual(str(prop), data + "\r\n")
+
+        data = "X-FOO:Text\\, escaped\\n"
+        prop = PyCalendarProperty()
+        prop.parse(data)
+        self.assertEqual(str(prop), data + "\r\n")
+
+        prop = PyCalendarProperty("X-FOO", "Text\\, escaped\\n")
+        self.assertEqual(str(prop), data + "\r\n")
+
+
+    def testNewRegistrationValueRoundtrip(self):
+
+        PyCalendarProperty.registerDefaultValue("X-SPECIAL-REGISTRATION", PyCalendarValue.VALUETYPE_TEXT)
+
+        data = "X-SPECIAL-REGISTRATION:Text\\, escaped\\n"
+        prop = PyCalendarProperty()
+        prop.parse(data)
+        self.assertEqual(str(prop), "X-SPECIAL-REGISTRATION:Text\\, escaped\\n\r\n")
+
+        prop = PyCalendarProperty("X-SPECIAL-REGISTRATION", "Text, escaped\n")
+        self.assertEqual(str(prop), "X-SPECIAL-REGISTRATION:Text\\, escaped\\n\r\n")
+
+
+    def testParameterEncodingDecoding(self):
+
+        prop = PyCalendarProperty("X-FOO", "Test")
+        prop.addAttribute(PyCalendarAttribute("X-BAR", "\"Check\""))
+        self.assertEqual(str(prop), "X-FOO;X-BAR=^'Check^':Test\r\n")
+
+        prop.addAttribute(PyCalendarAttribute("X-BAR2", "Check\nThis\tOut\n"))
+        self.assertEqual(str(prop), "X-FOO;X-BAR=^'Check^';X-BAR2=Check^nThis^tOut^n:Test\r\n")
+
+        data = "X-FOO;X-BAR=^'Check^':Test"
+        prop = PyCalendarProperty()
+        prop.parse(data)
+        self.assertEqual(prop.getAttributeValue("X-BAR"), "\"Check\"")
+
+        data = "X-FOO;X-BAR=^'Check^';X-BAR2=Check^nThis^tOut^n:Test"
+        prop = PyCalendarProperty()
+        prop.parse(data)
+        self.assertEqual(prop.getAttributeValue("X-BAR"), "\"Check\"")
+        self.assertEqual(prop.getAttributeValue("X-BAR2"), "Check\nThis\tOut\n")

Copied: PyCalendar/trunk/src/pycalendar/tests/test_recurrence.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/tests/test_recurrence.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/tests/test_recurrence.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/tests/test_recurrence.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,113 @@
+##
+#    Copyright (c) 2011-2012 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.recurrence import PyCalendarRecurrence
+import unittest
+
+class TestRecurrence(unittest.TestCase):
+
+    items = (
+        "FREQ=DAILY",
+        "FREQ=YEARLY;COUNT=400",
+        "FREQ=MONTHLY;UNTIL=20110102",
+        "FREQ=MONTHLY;UNTIL=20110102T090000",
+        "FREQ=MONTHLY;UNTIL=20110102T100000Z",
+        "FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=-1",
+
+        # These are from RFC5545 examples
+        "FREQ=YEARLY;INTERVAL=2;BYMINUTE=30;BYHOUR=8,9;BYDAY=SU;BYMONTH=1",
+        "FREQ=YEARLY;UNTIL=19730429T070000Z;BYDAY=-1SU;BYMONTH=4",
+        "FREQ=YEARLY;UNTIL=20060402T070000Z;BYDAY=1SU;BYMONTH=4",
+        "FREQ=YEARLY;BYDAY=2SU;BYMONTH=3",
+        "FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10",
+        "FREQ=DAILY;INTERVAL=2",
+        "FREQ=DAILY;COUNT=5;INTERVAL=10",
+        "FREQ=DAILY;UNTIL=20000131T140000Z;BYMONTH=1",
+        "FREQ=WEEKLY;INTERVAL=2;WKST=SU",
+        "FREQ=WEEKLY;UNTIL=19971007T000000Z;BYDAY=TU,TH;WKST=SU",
+        "FREQ=WEEKLY;COUNT=10;BYDAY=TU,TH;WKST=SU",
+        "FREQ=WEEKLY;UNTIL=19971224T000000Z;INTERVAL=2;BYDAY=MO,WE,FR;WKST=SU",
+        "FREQ=WEEKLY;COUNT=8;INTERVAL=2;BYDAY=TU,TH;WKST=SU",
+        "FREQ=MONTHLY;BYMONTHDAY=-3",
+        "FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1",
+        "FREQ=MONTHLY;COUNT=10;INTERVAL=18;BYMONTHDAY=10,11,12,13,14,15",
+        "FREQ=YEARLY;COUNT=10;INTERVAL=3;BYYEARDAY=1,100,200",
+        "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",
+    )
+
+
+    def testParse(self):
+
+        for item in TestRecurrence.items:
+            recur = PyCalendarRecurrence()
+            recur.parse(item)
+            self.assertEqual(recur.getText(), item, "Failed to parse and re-generate '%s'" % (item,))
+
+
+    def testParseInvalid(self):
+
+        items = (
+            "",
+            "FREQ=",
+            "FREQ=MICROSECONDLY",
+            "FREQ=YEARLY;COUNT=ABC",
+            "FREQ=YEARLY;COUNT=123;UNTIL=20110102",
+            "FREQ=MONTHLY;UNTIL=20110102T",
+            "FREQ=MONTHLY;UNTIL=20110102t090000",
+            "FREQ=MONTHLY;UNTIL=20110102T100000z",
+            "FREQ=MONTHLY;UNTIL=20110102TAABBCCz",
+            "FREQ=MONTHLY;BYDAY=A",
+            "FREQ=MONTHLY;BYDAY=+1,3MO",
+            "FREQ=MONTHLY;BYHOUR=A",
+            "FREQ=MONTHLY;BYHOUR=54",
+        )
+
+        for item in items:
+            self.assertRaises(ValueError, PyCalendarRecurrence().parse, item)
+
+
+    def testEquality(self):
+
+        recur1 = PyCalendarRecurrence()
+        recur1.parse("FREQ=YEARLY;COUNT=400")
+        recur2 = PyCalendarRecurrence()
+        recur2.parse("COUNT=400;FREQ=YEARLY")
+
+        self.assertEqual(recur1, recur2)
+
+
+    def testInequality(self):
+
+        recur1 = PyCalendarRecurrence()
+        recur1.parse("FREQ=YEARLY;COUNT=400")
+        recur2 = PyCalendarRecurrence()
+        recur2.parse("COUNT=400;FREQ=YEARLY;BYMONTH=1")
+
+        self.assertNotEqual(recur1, recur2)
+
+
+    def testHash(self):
+
+        hashes = []
+        for item in TestRecurrence.items:
+            recur = PyCalendarRecurrence()
+            recur.parse(item)
+            hashes.append(hash(recur))
+        hashes.sort()
+        for i in range(1, len(hashes)):
+            self.assertNotEqual(hashes[i - 1], hashes[i])

Copied: PyCalendar/trunk/src/pycalendar/tests/test_requeststatus.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/tests/test_requeststatus.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/tests/test_requeststatus.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/tests/test_requeststatus.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,75 @@
+##
+#    Copyright (c) 2011-2012 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.parser import ParserContext
+from pycalendar.property import PyCalendarProperty
+from pycalendar.requeststatusvalue import PyCalendarRequestStatusValue
+import unittest
+
+class TestRequestStatus(unittest.TestCase):
+
+    def testParseValue(self):
+
+        items = (
+            "2.0;Success",
+            "2.0;Success\;here",
+            "2.0;Success;Extra",
+            "2.0;Success\;here;Extra",
+            "2.0;Success;Extra\;here",
+            "2.0;Success\;here;Extra\;here too",
+        )
+
+        for item in items:
+            req = PyCalendarRequestStatusValue()
+            req.parse(item)
+            self.assertEqual(req.getText(), item, "Failed to parse and re-generate '%s'" % (item,))
+
+
+    def testBadValue(self):
+
+        bad_value = "2.0\;Success"
+        ok_value = "2.0;Success"
+
+        # Fix the value
+        oldContext = ParserContext.INVALID_REQUEST_STATUS_VALUE
+        ParserContext.INVALID_REQUEST_STATUS_VALUE = ParserContext.PARSER_FIX
+        req = PyCalendarRequestStatusValue()
+        req.parse(bad_value)
+        self.assertEqual(req.getText(), ok_value, "Failed to parse and re-generate '%s'" % (bad_value,))
+
+        # Raise the value
+        ParserContext.INVALID_REQUEST_STATUS_VALUE = ParserContext.PARSER_RAISE
+        req = PyCalendarRequestStatusValue()
+        self.assertRaises(ValueError, req.parse, bad_value)
+
+        ParserContext.INVALID_REQUEST_STATUS_VALUE = oldContext
+
+
+    def testParseProperty(self):
+
+        items = (
+            "REQUEST-STATUS:2.0;Success",
+            "REQUEST-STATUS:2.0;Success\;here",
+            "REQUEST-STATUS:2.0;Success;Extra",
+            "REQUEST-STATUS:2.0;Success\;here;Extra",
+            "REQUEST-STATUS:2.0;Success;Extra\;here",
+            "REQUEST-STATUS:2.0;Success\;here;Extra\;here too",
+        )
+
+        for item in items:
+            req = PyCalendarProperty()
+            req.parse(item)
+            self.assertEqual(req.getText(), item + "\r\n", "Failed to parse and re-generate '%s'" % (item,))

Copied: PyCalendar/trunk/src/pycalendar/tests/test_timezone.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/tests/test_timezone.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/tests/test_timezone.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/tests/test_timezone.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,235 @@
+##
+#    Copyright (c) 2007-2012 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.calendar import PyCalendar
+from pycalendar.datetime import PyCalendarDateTime
+import unittest
+
+class TestCalendar(unittest.TestCase):
+
+    def testOffsets(self):
+
+        data = (
+                    ("""BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//calendarserver.org//Zonal//EN
+VERSION:2.0
+BEGIN:VTIMEZONE
+TZID:America/New_York
+X-LIC-LOCATION:America/New_York
+BEGIN:STANDARD
+DTSTART:18831118T120358
+RDATE:18831118T120358
+TZNAME:EST
+TZOFFSETFROM:-045602
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19180331T020000
+RRULE:FREQ=YEARLY;UNTIL=19190330T070000Z;BYDAY=-1SU;BYMONTH=3
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19181027T020000
+RRULE:FREQ=YEARLY;UNTIL=19191026T060000Z;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:STANDARD
+DTSTART:19200101T000000
+RDATE:19200101T000000
+RDATE:19420101T000000
+RDATE:19460101T000000
+RDATE:19670101T000000
+TZNAME:EST
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19200328T020000
+RDATE:19200328T020000
+RDATE:19740106T020000
+RDATE:19750223T020000
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19201031T020000
+RDATE:19201031T020000
+RDATE:19450930T020000
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19210424T020000
+RRULE:FREQ=YEARLY;UNTIL=19410427T070000Z;BYDAY=-1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19210925T020000
+RRULE:FREQ=YEARLY;UNTIL=19410928T060000Z;BYDAY=-1SU;BYMONTH=9
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19420209T020000
+RDATE:19420209T020000
+TZNAME:EWT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+DTSTART:19450814T190000
+RDATE:19450814T190000
+TZNAME:EPT
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+DTSTART:19460428T020000
+RRULE:FREQ=YEARLY;UNTIL=19660424T070000Z;BYDAY=-1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19460929T020000
+RRULE:FREQ=YEARLY;UNTIL=19540926T060000Z;BYDAY=-1SU;BYMONTH=9
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:STANDARD
+DTSTART:19551030T020000
+RRULE:FREQ=YEARLY;UNTIL=19661030T060000Z;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19670430T020000
+RRULE:FREQ=YEARLY;UNTIL=19730429T070000Z;BYDAY=-1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;UNTIL=20061029T060000Z;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19760425T020000
+RRULE:FREQ=YEARLY;UNTIL=19860427T070000Z;BYDAY=-1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+DTSTART:19870405T020000
+RRULE:FREQ=YEARLY;UNTIL=20060402T070000Z;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+DTSTART:20070311T020000
+RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20071104T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+END:VCALENDAR
+""",
+                (
+                    (PyCalendarDateTime(1942, 2, 8), -5),
+                    (PyCalendarDateTime(1942, 2, 10), -4),
+                    (PyCalendarDateTime(2011, 1, 1), -5),
+                    (PyCalendarDateTime(2011, 4, 1), -4),
+                    (PyCalendarDateTime(2011, 10, 24), -4),
+                    (PyCalendarDateTime(2011, 11, 8), -5),
+                    (PyCalendarDateTime(2006, 1, 1), -5),
+                    (PyCalendarDateTime(2006, 4, 1), -5),
+                    (PyCalendarDateTime(2006, 5, 1), -4),
+                    (PyCalendarDateTime(2006, 10, 1), -4),
+                    (PyCalendarDateTime(2006, 10, 24), -4),
+                    (PyCalendarDateTime(2006, 11, 8), -5),
+                )
+            ),
+                    ("""BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//calendarserver.org//Zonal//EN
+VERSION:2.0
+BEGIN:VTIMEZONE
+TZID:Etc/GMT+8
+X-LIC-LOCATION:Etc/GMT+8
+BEGIN:STANDARD
+DTSTART:18000101T000000
+RDATE:18000101T000000
+TZNAME:GMT+8
+TZOFFSETFROM:-0800
+TZOFFSETTO:-0800
+END:STANDARD
+END:VTIMEZONE
+END:VCALENDAR
+""",
+                (
+                    (PyCalendarDateTime(1942, 2, 8), -8),
+                    (PyCalendarDateTime(1942, 2, 10), -8),
+                    (PyCalendarDateTime(2011, 1, 1), -8),
+                    (PyCalendarDateTime(2011, 4, 1), -8),
+                )
+            ),
+        )
+
+        for tzdata, offsets in data:
+
+            cal = PyCalendar.parseText(tzdata.replace("\n", "\r\n"))
+            tz = cal.getComponents()[0]
+
+            for dt, offset in offsets:
+                tzoffset = tz.getTimezoneOffsetSeconds(dt)
+                self.assertEqual(tzoffset, offset * 60 * 60, "Failed to match offset for %s at %s with caching" % (tz.getID(), dt,))
+            for dt, offset in reversed(offsets):
+                tzoffset = tz.getTimezoneOffsetSeconds(dt)
+                self.assertEqual(tzoffset, offset * 60 * 60, "Failed to match offset for %s at %s with caching, reversed" % (tz.getID(), dt,))
+
+            for dt, offset in offsets:
+                tz.mCachedExpandAllMax = None
+                tzoffset = tz.getTimezoneOffsetSeconds(dt)
+                self.assertEqual(tzoffset, offset * 60 * 60, "Failed to match offset for %s at %s without caching" % (tz.getID(), dt,))
+            for dt, offset in reversed(offsets):
+                tz.mCachedExpandAllMax = None
+                tzoffset = tz.getTimezoneOffsetSeconds(dt)
+                self.assertEqual(tzoffset, offset * 60 * 60, "Failed to match offset for %s at %s without caching, reversed" % (tz.getID(), dt,))

Copied: PyCalendar/trunk/src/pycalendar/tests/test_urivalue.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/tests/test_urivalue.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/tests/test_urivalue.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/tests/test_urivalue.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,87 @@
+##
+#    Copyright (c) 2012 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.parser import ParserContext
+from pycalendar.urivalue import PyCalendarURIValue
+from pycalendar.vcard.property import Property
+import unittest
+
+class TestNValue(unittest.TestCase):
+
+    def testParseValue(self):
+
+        # Restore BACKSLASH_IN_URI_VALUE after test
+        old_state = ParserContext.BACKSLASH_IN_URI_VALUE
+        self.addCleanup(setattr, ParserContext, "BACKSLASH_IN_URI_VALUE", old_state)
+
+        # Test with BACKSLASH_IN_URI_VALUE = PARSER_FIX
+        ParserContext.BACKSLASH_IN_URI_VALUE = ParserContext.PARSER_FIX
+        items = (
+            ("http://example.com", "http://example.com"),
+            ("http://example.com&abd\\,def", "http://example.com&abd,def"),
+        )
+
+        for item, result in items:
+            req = PyCalendarURIValue()
+            req.parse(item)
+            test = req.getText()
+            self.assertEqual(test, result, "Failed to parse and re-generate '%s'" % (item,))
+
+        # Test with BACKSLASH_IN_URI_VALUE = PARSER_ALLOW
+        ParserContext.BACKSLASH_IN_URI_VALUE = ParserContext.PARSER_ALLOW
+        items = (
+            ("http://example.com", "http://example.com"),
+            ("http://example.com&abd\\,def", "http://example.com&abd\\,def"),
+        )
+
+        for item, result in items:
+            req = PyCalendarURIValue()
+            req.parse(item)
+            test = req.getText()
+            self.assertEqual(test, result, "Failed to parse and re-generate '%s'" % (item,))
+
+
+    def testParseProperty(self):
+
+        # Restore BACKSLASH_IN_URI_VALUE after test
+        old_state = ParserContext.BACKSLASH_IN_URI_VALUE
+        self.addCleanup(setattr, ParserContext, "BACKSLASH_IN_URI_VALUE", old_state)
+
+        # Test with BACKSLASH_IN_URI_VALUE = PARSER_FIX
+        ParserContext.BACKSLASH_IN_URI_VALUE = ParserContext.PARSER_FIX
+        items = (
+            ("URL:http://example.com", "URL:http://example.com"),
+            ("URL:http://example.com&abd\\,def", "URL:http://example.com&abd,def"),
+        )
+
+        for item, result in items:
+            prop = Property()
+            prop.parse(item)
+            test = prop.getText()
+            self.assertEqual(test, result + "\r\n", "Failed to parse and re-generate '%s'" % (item,))
+
+        # Test with BACKSLASH_IN_URI_VALUE = PARSER_ALLOW
+        ParserContext.BACKSLASH_IN_URI_VALUE = ParserContext.PARSER_ALLOW
+        items = (
+            ("URL:http://example.com", "URL:http://example.com"),
+            ("URL:http://example.com&abd\\,def", "URL:http://example.com&abd\\,def"),
+        )
+
+        for item, result in items:
+            prop = Property()
+            prop.parse(item)
+            test = prop.getText()
+            self.assertEqual(test, result + "\r\n", "Failed to parse and re-generate '%s'" % (item,))

Copied: PyCalendar/trunk/src/pycalendar/tests/test_utils.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/tests/test_utils.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/tests/test_utils.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/tests/test_utils.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,56 @@
+##
+#    Copyright (c) 2012 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.utils import encodeParameterValue, decodeParameterValue
+
+class TestUtils(unittest.TestCase):
+
+    def test_encodeParameterValue(self):
+        """
+        Round trip encodeParameterValue and decodeParameterValue.
+        """
+
+        data = (
+            ("abc", "abc", None),
+            ("\"abc\"", "^'abc^'", None),
+            ("abc\ndef", "abc^ndef", None),
+            ("abc\rdef", "abc^ndef", "abc\ndef"),
+            ("abc\r\ndef", "abc^ndef", "abc\ndef"),
+            ("abc\n\tdef", "abc^n^tdef", None),
+            ("abc^2", "abc^^2", None),
+            ("^abc^", "^^abc^^", None),
+        )
+
+        for value, encoded, decoded in data:
+            if decoded is None:
+                decoded = value
+            self.assertEqual(encodeParameterValue(value), encoded)
+            self.assertEqual(decodeParameterValue(encoded), decoded)
+
+
+    def test_decodeParameterValue(self):
+        """
+        Special cases for decodeParameterValue.
+        """
+
+        data = (
+            ("^a^bc^", "^a^bc^"),
+            ("^^^abc", "^^abc"),
+        )
+
+        for value, decoded in data:
+            self.assertEqual(decodeParameterValue(value), decoded)

Copied: PyCalendar/trunk/src/pycalendar/tests/test_validation.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/tests/test_validation.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/tests/test_validation.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/tests/test_validation.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,92 @@
+##
+#    Copyright (c) 2011-2012 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.property import PyCalendarProperty
+from pycalendar.validation import partial, PropertyValueChecks
+import unittest
+
+class TestValidation(unittest.TestCase):
+
+    def test_partial(self):
+
+
+        def _test(a, b):
+            return (a, b)
+
+        self.assertEqual(partial(_test, "a", "b")(), ("a", "b",))
+        self.assertEqual(partial(_test, "a")("b"), ("a", "b",))
+        self.assertEqual(partial(_test)("a", "b"), ("a", "b",))
+
+
+    def test_stringValue(self):
+
+        props = (
+            ("SUMMARY:Test", "Test", True,),
+            ("SUMMARY:Test", "TEST", True,),
+            ("DTSTART:20110623T174806", "Test", False),
+        )
+
+        for prop, test, result in props:
+            property = PyCalendarProperty()
+            property.parse(prop)
+            self.assertEqual(PropertyValueChecks.stringValue(test, property), result)
+
+
+    def test_alwaysUTC(self):
+
+        props = (
+            ("SUMMARY:Test", False,),
+            ("DTSTART:20110623T174806", False),
+            ("DTSTART;VALUE=DATE:20110623", False),
+            ("DTSTART:20110623T174806Z", True),
+        )
+
+        for prop, result in props:
+            property = PyCalendarProperty()
+            property.parse(prop)
+            self.assertEqual(PropertyValueChecks.alwaysUTC(property), result)
+
+
+    def test_numericRange(self):
+
+        props = (
+            ("SUMMARY:Test", 0, 100, False,),
+            ("PERCENT-COMPLETE:0", 0, 100, True,),
+            ("PERCENT-COMPLETE:100", 0, 100, True,),
+            ("PERCENT-COMPLETE:50", 0, 100, True,),
+            ("PERCENT-COMPLETE:200", 0, 100, False,),
+            ("PERCENT-COMPLETE:-1", 0, 100, False,),
+        )
+
+        for prop, low, high, result in props:
+            property = PyCalendarProperty()
+            property.parse(prop)
+            self.assertEqual(PropertyValueChecks.numericRange(low, high, property), result)
+
+
+    def test_positiveIntegerOrZero(self):
+
+        props = (
+            ("SUMMARY:Test", False,),
+            ("REPEAT:0", True,),
+            ("REPEAT:100", True,),
+            ("REPEAT:-1", False,),
+        )
+
+        for prop, result in props:
+            property = PyCalendarProperty()
+            property.parse(prop)
+            self.assertEqual(PropertyValueChecks.positiveIntegerOrZero(property), result)

Copied: PyCalendar/trunk/src/pycalendar/tests/test_xml.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/tests/test_xml.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/tests/test_xml.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/tests/test_xml.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,104 @@
+##
+#    Copyright (c) 2007-2012 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.calendar import PyCalendar
+import cStringIO as StringIO
+import difflib
+import unittest
+
+class TestCalendar(unittest.TestCase):
+
+    data = (
+                (
+"""BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+EXDATE:20081114T000000Z
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+X-TEST:Testing
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+
+"""<?xml version="1.0" encoding="utf-8"?>
+<ns0:icalendar xmlns:ns0="urn:ietf:params:xml:ns:icalendar-2.0">
+  <ns0:vcalendar>
+    <ns0:properties>
+      <ns0:version>
+        <ns0:text>2.0</ns0:text>
+      </ns0:version>
+      <ns0:prodid>
+        <ns0:text>-//mulberrymail.com//Mulberry v4.0//EN</ns0:text>
+      </ns0:prodid>
+      <ns0:x-test>
+        <ns0:unknown>Testing</ns0:unknown>
+      </ns0:x-test>
+    </ns0:properties>
+    <ns0:components>
+      <ns0:vevent>
+        <ns0:properties>
+          <ns0:uid>
+            <ns0:text>12345-67890-3</ns0:text>
+          </ns0:uid>
+          <ns0:dtstart>
+            <ns0:date-time>2007-11-14T00:00:00Z</ns0:date-time>
+          </ns0:dtstart>
+          <ns0:attendee>
+            <ns0:cal-address>mailto:user2 at example.com</ns0:cal-address>
+          </ns0:attendee>
+          <ns0:exdate>
+            <ns0:date-time>2008-11-14T00:00:00Z</ns0:date-time>
+          </ns0:exdate>
+          <ns0:organizer>
+            <ns0:cal-address>mailto:user1 at example.com</ns0:cal-address>
+          </ns0:organizer>
+          <ns0:rrule>
+            <ns0:recur>
+              <ns0:freq>YEARLY</ns0:freq>
+            </ns0:recur>
+          </ns0:rrule>
+        </ns0:properties>
+      </ns0:vevent>
+    </ns0:components>
+  </ns0:vcalendar>
+</ns0:icalendar>
+""",
+                ),
+)
+
+    def testGenerateXML(self):
+
+        def _doRoundtrip(caldata, resultdata=None):
+            test1 = resultdata if resultdata is not None else caldata
+
+            cal = PyCalendar()
+            cal.parse(StringIO.StringIO(caldata))
+
+            test2 = cal.getTextXML()
+
+            self.assertEqual(
+                test1,
+                test2,
+                "\n".join(difflib.unified_diff(str(test1).splitlines(), test2.splitlines()))
+            )
+
+        for item1, item2 in self.data:
+            _doRoundtrip(item1, item2)

Modified: PyCalendar/trunk/src/pycalendar/textvalue.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/textvalue.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/textvalue.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -16,26 +16,27 @@
 
 # iCalendar UTC Offset value
 
-from plaintextvalue import PyCalendarPlainTextValue
-from value import PyCalendarValue
-import utils
+from pycalendar import utils, xmldefs
+from pycalendar.plaintextvalue import PyCalendarPlainTextValue
+from pycalendar.value import PyCalendarValue
 
-class PyCalendarTextValue( PyCalendarPlainTextValue ):
+class PyCalendarTextValue(PyCalendarPlainTextValue):
 
-    def getType( self ):
+    def getType(self):
         return PyCalendarValue.VALUETYPE_TEXT
 
-    def parse( self, data ):
+
+    def parse(self, data):
         # Decoding required
-        self.mValue = utils.decodeTextValue( data )
-        
+        self.mValue = utils.decodeTextValue(data)
+
+
     # os - StringIO object
-    def generate( self, os ):
+    def generate(self, os):
         try:
             # Encoding required
-            utils.writeTextValue( os, self.mValue )
+            utils.writeTextValue(os, self.mValue)
         except:
             pass
 
-PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_TEXT, PyCalendarTextValue)
-    
\ No newline at end of file
+PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_TEXT, PyCalendarTextValue, xmldefs.value_text)

Modified: PyCalendar/trunk/src/pycalendar/timezone.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/timezone.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/timezone.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,26 +14,42 @@
 #    limitations under the License.
 ##
 
-import stringutils
+from pycalendar import stringutils
+from pycalendar.timezonedb import PyCalendarTimezoneDatabase
 
 class PyCalendarTimezone(object):
-    
-    def __init__(self, utc=None, tzid=None, copyit=None):
-        
+    """
+    Wrapper around a timezone specification. There are three options:
+
+    UTC - when mUTC is True
+    TZID - when mUTC is False and tzid is a str
+    UTCOFFSET - when mUTC is False and tzid is an int
+    """
+
+    sDefaultTimezone = None
+
+    def __init__(self, utc=None, tzid=None):
+
         if utc is not None:
             self.mUTC = utc
             self.mTimezone = tzid
-        elif copyit is not None:
-            self._copy_PyCalendarTimezone(copyit)
+        elif tzid is not None:
+            self.mUTC = tzid.lower() == 'utc'
+            self.mTimezone = None if tzid.lower() == 'utc' else tzid
         else:
             self.mUTC = True
             self.mTimezone = None
-    
-            # Copy defauilt timezone if it exists
-            from manager import PyCalendarManager
-            if PyCalendarManager.sICalendarManager is not None:
-                self._copy_PyCalendarTimezone(PyCalendarManager.sICalendarManager.getDefaultTimezone())
 
+            # Copy default timezone if it exists
+            if PyCalendarTimezone.sDefaultTimezone is not None:
+                self.mUTC = PyCalendarTimezone.sDefaultTimezone.mUTC
+                self.mTimezone = PyCalendarTimezone.sDefaultTimezone.mTimezone
+
+
+    def duplicate(self):
+        return PyCalendarTimezone(self.mUTC, self.mTimezone)
+
+
     def equals(self, comp):
         # Always match if any one of them is 'floating'
         if self.floating() or comp.floating():
@@ -43,6 +59,7 @@
         else:
             return self.mUTC or stringutils.compareStringsSafe(self.mTimezone, comp.mTimezone)
 
+
     @staticmethod
     def same(utc1, tzid1, utc2, tzid2):
         # Always match if any one of them is 'floating'
@@ -53,51 +70,60 @@
         else:
             return utc1 or stringutils.compareStringsSafe(tzid1, tzid2)
 
+
     @staticmethod
     def is_float(utc, tzid):
         return not utc and not tzid
 
+
     def getUTC(self):
         return self.mUTC
 
+
     def setUTC(self, utc):
         self.mUTC = utc
 
+
     def getTimezoneID(self):
         return self.mTimezone
 
+
     def setTimezoneID(self, tzid):
         self.mTimezone = tzid
 
+
     def floating(self):
-        return not self.mUTC and not self.mTimezone
+        return not self.mUTC and self.mTimezone is None
 
+
     def hasTZID(self):
-        return not self.mUTC and self.mTimezone
+        return not self.mUTC and self.mTimezone is not None
 
+
     def timeZoneSecondsOffset(self, dt):
-        from manager import PyCalendarManager
-        from calendar import PyCalendar
         if self.mUTC:
             return 0
-        elif not self.mTimezone:
-            return PyCalendar.sICalendar.getTimezoneOffsetSeconds(PyCalendarManager.sICalendarManager.getDefaultTimezone().getTimezoneID(), dt)
+        elif self.mTimezone is None:
+            return PyCalendarTimezoneDatabase.getTimezoneOffsetSeconds(PyCalendarTimezone.sDefaultTimezone.getTimezoneID(), dt)
+        elif isinstance(self.mTimezone, int):
+            return self.mTimezone
+        else:
+            # Look up timezone and resolve date using default timezones
+            return PyCalendarTimezoneDatabase.getTimezoneOffsetSeconds(self.mTimezone, dt)
 
-        # Look up timezone and resolve date using default timezones
-        return PyCalendar.sICalendar.getTimezoneOffsetSeconds(self.mTimezone, dt)
 
     def timeZoneDescriptor(self, dt):
-        from manager import PyCalendarManager
-        from calendar import PyCalendar
         if self.mUTC:
             return "(UTC)"
-        elif not self.mTimezone:
-            return PyCalendar.sICalendar.getTimezoneDescriptor(PyCalendarManager.sICalendarManager.getDefaultTimezone().getTimezoneID(), dt)
+        elif self.mTimezone is None:
+            return PyCalendarTimezoneDatabase.getTimezoneDescriptor(PyCalendarTimezone.sDefaultTimezone.getTimezoneID(), dt)
+        elif isinstance(self.mTimezone, int):
+            sign = "-" if self.mTimezone < 0 else "+"
+            hours = abs(self.mTimezone) / 3600
+            minutes = divmod(abs(self.mTimezone) / 60, 60)[1]
+            return "%s%02d%02d" % (sign, hours, minutes,)
+        else:
+            # Look up timezone and resolve date using default timezones
+            return PyCalendarTimezoneDatabase.getTimezoneDescriptor(self.mTimezone, dt)
 
-        # Look up timezone and resolve date using default timezones
-        return PyCalendar.sICalendar.getTimezoneDescriptor(self.mTimezone, dt)
-
-    def _copy_PyCalendarTimezone(self, copy):
-        self.mUTC = copy.mUTC
-        self.mTimezone = copy.mTimezone
-    
\ No newline at end of file
+PyCalendarTimezone.sDefaultTimezone = PyCalendarTimezone()

Copied: PyCalendar/trunk/src/pycalendar/timezonedb.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/timezonedb.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/timezonedb.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/timezonedb.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,160 @@
+##
+#    Copyright (c) 2011-2012 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.exceptions import PyCalendarNoTimezoneInDatabase, \
+    PyCalendarInvalidData
+import os
+
+class PyCalendarTimezoneDatabase(object):
+    """
+    On demand timezone database cache. This scans a TZdb directory for .ics files matching a
+    TZID and caches the component data in a calendar from whence the actual component is returned.
+    """
+
+    sTimezoneDatabase = None
+
+
+    @staticmethod
+    def createTimezoneDatabase(dbpath):
+        PyCalendarTimezoneDatabase.sTimezoneDatabase = PyCalendarTimezoneDatabase()
+        PyCalendarTimezoneDatabase.sTimezoneDatabase.setPath(dbpath)
+
+
+    @staticmethod
+    def clearTimezoneDatabase():
+        if PyCalendarTimezoneDatabase.sTimezoneDatabase is not None:
+            PyCalendarTimezoneDatabase.sTimezoneDatabase.clear()
+
+
+    def __init__(self):
+        from pycalendar.calendar import PyCalendar
+        self.dbpath = None
+        self.calendar = PyCalendar()
+
+
+    def setPath(self, dbpath):
+        self.dbpath = dbpath
+
+
+    def clear(self):
+        from pycalendar.calendar import PyCalendar
+        self.calendar = PyCalendar()
+
+
+    @staticmethod
+    def getTimezoneDatabase():
+        if PyCalendarTimezoneDatabase.sTimezoneDatabase is None:
+            PyCalendarTimezoneDatabase.sTimezoneDatabase = PyCalendarTimezoneDatabase()
+        return PyCalendarTimezoneDatabase.sTimezoneDatabase
+
+
+    @staticmethod
+    def getTimezone(tzid):
+
+        # Check whether current cached
+        tzdb = PyCalendarTimezoneDatabase.getTimezoneDatabase()
+        tz = tzdb.calendar.getTimezone(tzid)
+        if tz is None:
+            try:
+                tzdb.cacheTimezone(tzid)
+            except PyCalendarNoTimezoneInDatabase:
+                pass
+            tz = tzdb.calendar.getTimezone(tzid)
+
+        return tz
+
+
+    @staticmethod
+    def getTimezoneInCalendar(tzid):
+        """
+        Return a VTIMEZONE inside a valid VCALENDAR
+        """
+
+        tz = PyCalendarTimezoneDatabase.getTimezone(tzid)
+        if tz is not None:
+            from pycalendar.calendar import PyCalendar
+            cal = PyCalendar()
+            cal.addComponent(tz.duplicate(cal))
+            return cal
+        else:
+            return None
+
+
+    @staticmethod
+    def getTimezoneOffsetSeconds(tzid, dt):
+        # Cache it first
+        tz = PyCalendarTimezoneDatabase.getTimezone(tzid)
+        if tz is not None:
+            return tz.getTimezoneOffsetSeconds(dt)
+        else:
+            return 0
+
+
+    @staticmethod
+    def getTimezoneDescriptor(tzid, dt):
+        # Cache it first
+        tz = PyCalendarTimezoneDatabase.getTimezone(tzid)
+        if tz is not None:
+            return tz.getTimezoneDescriptor(dt)
+        else:
+            return ""
+
+
+    def cacheTimezone(self, tzid):
+
+        if self.dbpath is None:
+            return
+
+        tzpath = os.path.join(self.dbpath, "%s.ics" % (tzid,))
+        tzpath = os.path.normpath(tzpath)
+        if tzpath.startswith(self.dbpath) and os.path.isfile(tzpath):
+            try:
+                self.calendar.parseComponent(open(tzpath))
+            except (IOError, PyCalendarInvalidData):
+                raise PyCalendarNoTimezoneInDatabase(self.dbpath, tzid)
+        else:
+            raise PyCalendarNoTimezoneInDatabase(self.dbpath, tzid)
+
+
+    def addTimezone(self, tz):
+        copy = tz.duplicate(self.calendar)
+        self.calendar.addComponent(copy)
+
+
+    @staticmethod
+    def mergeTimezones(cal, tzs):
+        """
+        Merge each timezone from other calendar.
+        """
+
+        tzdb = PyCalendarTimezoneDatabase.getTimezoneDatabase()
+
+        # Not if our own calendar
+        if cal is tzdb.calendar:
+            return
+
+        # Merge each timezone from other calendar
+        for tz in tzs:
+            tzdb.mergeTimezone(tz)
+
+
+    def mergeTimezone(self, tz):
+        """
+        If the supplied VTIMEZONE is not in our cache then store it in memory.
+        """
+
+        if self.getTimezone(tz.getID()) is None:
+            self.addTimezone(tz)

Copied: PyCalendar/trunk/src/pycalendar/unknownvalue.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/unknownvalue.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/unknownvalue.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/unknownvalue.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,28 @@
+##
+#    Copyright (c) 2007-2012 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.
+##
+
+# iCalendar Unknown value - one whose default type we don't know about
+
+from pycalendar import xmldefs
+from pycalendar.plaintextvalue import PyCalendarPlainTextValue
+from pycalendar.value import PyCalendarValue
+
+class PyCalendarUnknownValue(PyCalendarPlainTextValue):
+
+    def getType(self):
+        return PyCalendarUnknownValue.VALUETYPE_UNKNOWN
+
+PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_UNKNOWN, PyCalendarUnknownValue, xmldefs.value_unknown)

Modified: PyCalendar/trunk/src/pycalendar/urivalue.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/urivalue.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/urivalue.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,16 +14,26 @@
 #    limitations under the License.
 ##
 
-# iCalendar UTC Offset value
+# iCalendar URI value
 
-from plaintextvalue import PyCalendarPlainTextValue
-from value import PyCalendarValue
+from pycalendar import xmldefs, utils
+from pycalendar.plaintextvalue import PyCalendarPlainTextValue
+from pycalendar.value import PyCalendarValue
+from pycalendar.parser import ParserContext
 
-class PyCalendarURIValue( PyCalendarPlainTextValue ):
+class PyCalendarURIValue(PyCalendarPlainTextValue):
 
     def getType(self):
         return PyCalendarURIValue.VALUETYPE_URI
 
-PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_URI, PyCalendarURIValue)
 
-        
\ No newline at end of file
+    def parse(self, data):
+
+        if ParserContext.BACKSLASH_IN_URI_VALUE == ParserContext.PARSER_FIX:
+            # Decoding required
+            self.mValue = utils.decodeTextValue(data)
+        else:
+            # No decoding required
+            self.mValue = data
+
+PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_URI, PyCalendarURIValue, xmldefs.value_uri)

Modified: PyCalendar/trunk/src/pycalendar/utcoffsetvalue.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/utcoffsetvalue.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/utcoffsetvalue.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -16,77 +16,96 @@
 
 # iCalendar UTC Offset value
 
-from value import PyCalendarValue
+from cStringIO import StringIO
+from pycalendar import xmldefs
+from pycalendar.value import PyCalendarValue
 
-class PyCalendarUTCOffsetValue( PyCalendarValue ):
+class PyCalendarUTCOffsetValue(PyCalendarValue):
 
-    def __init__( self, copyit = None ):
-        
-        if copyit:
-            self.mValue = copyit.mValue
-        else:
-            self.mValue = 0
+    def __init__(self, value=0):
+        self.mValue = value
 
-    def getType( self ):
+
+    def duplicate(self):
+        return PyCalendarUTCOffsetValue(self.mValue)
+
+
+    def getType(self):
         return PyCalendarValue.VALUETYPE_UTC_OFFSET
 
-    def parse( self, data ):
+
+    def parse(self, data):
         # Must be of specific lengths
-        if ( len( data ) != 5 ) and ( len( data ) != 7 ):
+        datalen = len(data)
+        if datalen not in (5, 7):
             self.mValue = 0
-            return
+            raise ValueError
 
         # Get sign
-        plus = ( data[0] == '+' )
+        if data[0] not in ('+', '-'):
+            raise ValueError
+        plus = (data[0] == '+')
 
         # Get hours
-        hours = int( data[1:3] )
+        hours = int(data[1:3])
 
         # Get minutes
-        mins = int( data[3:5] )
+        mins = int(data[3:5])
 
         # Get seconds if present
         secs = 0
-        if ( len( data ) == 7 ):
-            secs = int( data[5:] )
+        if datalen == 7 :
+            secs = int(data[5:])
 
+        self.mValue = ((hours * 60) + mins) * 60 + secs
+        if not plus:
+            self.mValue = -self.mValue
 
-        self.mValue = ( ( hours * 60 ) + mins ) * 60 + secs
-        if ( not plus ):
-            self.mValue = -self.mValue
- 
+
     # os - StringIO object
-    def generate( self, os ):
+    def generate(self, os):
         try:
             abs_value = self.mValue
             if self.mValue < 0 :
-                os.write( "-" )
+                os.write("-")
                 abs_value = -self.mValue
             else:
-                os.write( "+" )
+                os.write("+")
 
             secs = abs_value % 60
-            mins = ( abs_value / 60 ) % 60
-            hours = abs_value / ( 60 * 60 )
+            mins = (abs_value / 60) % 60
+            hours = abs_value / (60 * 60)
 
-            if ( hours < 10 ):
-                os.write( "0" )
-            os.write( str( hours ) )
-            if ( mins < 10 ):
-                os.write( "0" )
-            os.write( str( mins ) )
-            if ( secs != 0 ):
-                if ( secs < 10 ):
-                    os.write( "0" )
-                os.write( str( secs ) )
+            if (hours < 10):
+                os.write("0")
+            os.write(str(hours))
+            if (mins < 10):
+                os.write("0")
+            os.write(str(mins))
+            if (secs != 0):
+                if (secs < 10):
+                    os.write("0")
+                os.write(str(secs))
         except:
             pass
 
-    def getValue( self ):
+
+    def writeXML(self, node, namespace):
+
+        os = StringIO.StringIO()
+        self.generate(os)
+        text = os.getvalue()
+        text = text[:-2] + ":" + text[-2:]
+
+        value = self.getXMLNode(node, namespace)
+        value.text = text
+
+
+    def getValue(self):
         return self.mValue
 
-    def setValue( self, value ):
+
+    def setValue(self, value):
         self.mValue = value
 
-PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_UTC_OFFSET, PyCalendarUTCOffsetValue)
-
+PyCalendarValue.registerType(PyCalendarValue.VALUETYPE_UTC_OFFSET, PyCalendarUTCOffsetValue, xmldefs.value_utc_offset)

Modified: PyCalendar/trunk/src/pycalendar/utils.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/utils.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/utils.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,19 +14,20 @@
 #    limitations under the License.
 ##
 
+from pycalendar.parser import ParserContext
 import cStringIO as StringIO
 
-#from PyCalendarDateTime import PyCalendarDateTime
+def readFoldedLine(ins, lines):
 
-def readFoldedLine( ins, lines ):
-
     # If line2 already has data, transfer that into line1
-    if lines[1]:
+    if lines[1] is not None:
         lines[0] = lines[1]
     else:
         # Fill first line
         try:
             myline = ins.readline()
+            if len(myline) == 0:
+                raise ValueError
             if myline[-1] == "\n":
                 if myline[-2] == "\r":
                     lines[0] = myline[:-2]
@@ -36,18 +37,19 @@
                 lines[0] = myline[:-1]
             else:
                 lines[0] = myline
+        except IndexError:
+            lines[0] = ""
         except:
-            lines[0] = ""
+            lines[0] = None
             return False
 
-        if not lines[0]:
-            return False
- 
     # Now loop looking ahead at the next line to see if it is folded
     while True:
         # Get next line
         try:
             myline = ins.readline()
+            if len(myline) == 0:
+                raise ValueError
             if myline[-1] == "\n":
                 if myline[-2] == "\r":
                     lines[1] = myline[:-2]
@@ -57,8 +59,10 @@
                 lines[1] = myline[:-1]
             else:
                 lines[1] = myline
+        except IndexError:
+            lines[1] = ""
         except:
-            lines[1] = ""
+            lines[1] = None
             return True
 
         if not lines[1]:
@@ -75,20 +79,31 @@
 
     return True
 
-def find_first_of( text, tokens, offset ):
+
+
+def find_first_of(text, tokens, offset):
     for ctr, c in enumerate(text[offset:]):
         if c in tokens:
             return offset + ctr
     return -1
-    
-def writeTextValue( os, value ):
+
+
+
+def escapeTextValue(value):
+    os = StringIO.StringIO()
+    writeTextValue(os, value)
+    return os.getvalue()
+
+
+
+def writeTextValue(os, value):
     try:
         start_pos = 0
-        end_pos = find_first_of( value, "\r\n;\\,", start_pos )
+        end_pos = find_first_of(value, "\r\n;\\,", start_pos)
         if end_pos != -1:
             while True:
                 # Write current segment
-                os.write( value[start_pos:end_pos] )
+                os.write(value[start_pos:end_pos])
 
                 # Write escape
                 os.write("\\")
@@ -113,10 +128,12 @@
                     break
         else:
             os.write(value)
-    
+
     except:
         pass
-    
+
+
+
 def decodeTextValue(value):
     os = StringIO.StringIO()
 
@@ -139,15 +156,30 @@
                 os.write('\r')
             elif c == 'n':
                 os.write('\n')
+            elif c == 'N':
+                os.write('\n')
             elif c == '':
                 os.write('')
             elif c == '\\':
                 os.write('\\')
             elif c == ',':
                 os.write(',')
+            elif c == ';':
+                os.write(';')
+            elif c == ':':
+                # ":" escape normally invalid
+                if ParserContext.INVALID_COLON_ESCAPE_SEQUENCE == ParserContext.PARSER_RAISE:
+                    raise ValueError
+                elif ParserContext.INVALID_COLON_ESCAPE_SEQUENCE == ParserContext.PARSER_FIX:
+                    os.write(':')
 
-            # Bump past escapee and look for next segment (not past the
-            # end)
+            # Other escaped chars normally not allowed
+            elif ParserContext.INVALID_ESCAPE_SEQUENCES == ParserContext.PARSER_RAISE:
+                raise ValueError
+            elif ParserContext.INVALID_ESCAPE_SEQUENCES == ParserContext.PARSER_FIX:
+                os.write(c)
+
+            # Bump past escapee and look for next segment (not past the end)
             start_pos = end_pos + 1
             if start_pos >= size_pos:
                 break
@@ -162,9 +194,167 @@
 
     return os.getvalue()
 
+
+
+def encodeParameterValue(value):
+
+    encoded = []
+    last = ''
+    for c in value:
+        if c == '\t':
+            encoded.append('^')
+            encoded.append('t')
+        elif c == '\r':
+            encoded.append('^')
+            encoded.append('n')
+        elif c == '\n':
+            if last != '\r':
+                encoded.append('^')
+                encoded.append('n')
+        elif c == '"':
+            encoded.append('^')
+            encoded.append('\'')
+        elif c == '^':
+            encoded.append('^')
+            encoded.append('^')
+        else:
+            encoded.append(c)
+        last = c
+
+    return "".join(encoded)
+
+
+
+def decodeParameterValue(value):
+
+    if value is None:
+        return None
+    decoded = []
+    last = ''
+    for c in value:
+        if last == '^':
+            if c == 't':
+                decoded.append('\t')
+            elif c == 'n':
+                decoded.append('\n')
+            elif c == '\'':
+                decoded.append('"')
+            elif c == '^':
+                decoded.append('^')
+                c = ''
+            else:
+                decoded.append('^')
+                decoded.append(c)
+        elif c != '^':
+            decoded.append(c)
+        last = c
+    if last == '^':
+        decoded.append('^')
+    return "".join(decoded)
+
+
+
+# vCard text list parsing/generation
+def parseTextList(data, sep=';'):
+    """
+    Each element of the list has to be separately un-escaped
+    """
+    results = []
+    item = []
+    pre_s = ''
+    for s in data:
+        if s == sep and pre_s != '\\':
+            results.append(decodeTextValue("".join(item)))
+            item = []
+        else:
+            item.append(s)
+        pre_s = s
+
+    results.append(decodeTextValue("".join(item)))
+
+    return tuple(results) if len(results) > 1 else (results[0] if len(results) else "")
+
+
+
+def generateTextList(os, data, sep=';'):
+    """
+    Each element of the list must be separately escaped
+    """
+    try:
+        if isinstance(data, basestring):
+            data = (data,)
+        results = [escapeTextValue(value) for value in data]
+        os.write(sep.join(results))
+    except:
+        pass
+
+
+
+# vCard double-nested list parsing/generation
+def parseDoubleNestedList(data, maxsize):
+    results = []
+    items = [""]
+    pre_s = ''
+    for s in data:
+        if s == ';' and pre_s != '\\':
+
+            if len(items) > 1:
+                results.append(tuple([decodeTextValue(item) for item in items]))
+            elif len(items) == 1:
+                results.append(decodeTextValue(items[0]))
+            else:
+                results.append("")
+
+            items = [""]
+        elif s == ',' and pre_s != '\\':
+            items.append("")
+        else:
+            items[-1] += s
+        pre_s = s
+
+    if len(items) > 1:
+        results.append(tuple([decodeTextValue(item) for item in items]))
+    elif len(items) == 1:
+        results.append(decodeTextValue(items[0]))
+    else:
+        results.append("")
+
+    for _ignore in range(maxsize - len(results)):
+        results.append("")
+
+    if len(results) > maxsize:
+        if ParserContext.INVALID_ADR_N_VALUES == ParserContext.PARSER_FIX:
+            results = results[:maxsize]
+        elif ParserContext.INVALID_ADR_N_VALUES == ParserContext.PARSER_RAISE:
+            raise ValueError
+
+    return tuple(results)
+
+
+
+def generateDoubleNestedList(os, data):
+    try:
+        def _writeElement(item):
+            if isinstance(item, basestring):
+                writeTextValue(os, item)
+            else:
+                if item:
+                    writeTextValue(os, item[0])
+                    for bit in item[1:]:
+                        os.write(",")
+                        writeTextValue(os, bit)
+
+        for item in data[:-1]:
+            _writeElement(item)
+            os.write(";")
+        _writeElement(data[-1])
+
+    except:
+        pass
+
 # Date/time calcs
-days_in_month      = ( 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 )
-days_in_month_leap = ( 0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 )
+days_in_month = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
+days_in_month_leap = (0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
 
 def daysInMonth(month, year):
     # NB month is 1..12 so use dummy value at start of array to avoid index
@@ -174,8 +364,8 @@
     else:
         return days_in_month[month]
 
-days_upto_month      = ( 0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 )
-days_upto_month_leap = ( 0, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335 )
+days_upto_month = (0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334)
+days_upto_month_leap = (0, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335)
 
 def daysUptoMonth(month, year):
     # NB month is 1..12 so use dummy value at start of array to avoid index
@@ -185,49 +375,67 @@
     else:
         return days_upto_month[month]
 
+cachedLeapYears = {}
 def isLeapYear(year):
-    if year <= 1752:
-        return (year % 4 == 0)
-    else:
-        return ((year % 4 == 0) and (year % 100 != 0)) or (year % 400 == 0)
 
+    try:
+        return cachedLeapYears[year]
+    except KeyError:
+        if year <= 1752:
+            result = (year % 4 == 0)
+        else:
+            result = ((year % 4 == 0) and (year % 100 != 0)) or (year % 400 == 0)
+        cachedLeapYears[year] = result
+        return result
+
+cachedLeapDaysSince1970 = {}
 def leapDaysSince1970(year_offset):
-    if year_offset > 2:
-        return (year_offset + 1) / 4
-    elif year_offset < -1:
-        # Python will round down negative numbers (i.e. -5/4 = -2, but we want -1), so
-        # what is (year_offset - 2) in C code is actually (year_offset - 2 + 3) in Python.
-        return (year_offset + 1) / 4
-    else:
-        return 0
 
-#    public static int getLocalTimezoneOffsetSeconds() {
-#        Calendar rightNow = Calendar.getInstance()
-#        int tzoffset = rightNow.get(Calendar.ZONE_OFFSET)
-#        int dstoffset = rightNow.get(Calendar.DST_OFFSET)
-#        return -(tzoffset + dstoffset) / 1000
-#    }
+    try:
+        return cachedLeapDaysSince1970[year_offset]
+    except KeyError:
+        if year_offset > 2:
+            result = (year_offset + 1) / 4
+        elif year_offset < -1:
+            # Python will round down negative numbers (i.e. -5/4 = -2, but we want -1), so
+            # what is (year_offset - 2) in C code is actually (year_offset - 2 + 3) in Python.
+            result = (year_offset + 1) / 4
+        else:
+            result = 0
+        cachedLeapDaysSince1970[year_offset] = result
+        return result
 
-    # Packed date
+
+
+# Packed date
 def packDate(year, month, day):
     return (year << 16) | (month << 8) | (day + 128)
 
+
+
 def unpackDate(data, unpacked):
     unpacked[0] = (data & 0xFFFF0000) >> 16
     unpacked[1] = (data & 0x0000FF00) >> 8
     unpacked[2] = (data & 0xFF) - 128
 
+
+
 def unpackDateYear(data):
     return (data & 0xFFFF0000) >> 16
 
+
+
 def unpackDateMonth(data):
     return (data & 0x0000FF00) >> 8
 
+
+
 def unpackDateDay(data):
     return (data & 0xFF) - 128
 
+
+
 # Display elements
-
 def getMonthTable(month, year, weekstart, table, today_index):
     from pycalendar.datetime import PyCalendarDateTime
 
@@ -250,7 +458,7 @@
     max_day = daysInMonth(month, year)
 
     # Fill up each row
-    for day in range( 1, max_day + 1):
+    for day in range(1, max_day + 1):
         # Insert new row if we are at the start of a row
         if (col == 0) or (day == 1):
             table.extend([0] * 7)
@@ -278,7 +486,7 @@
             # Check on today
             if (temp.getYear() == today.getYear()) and (temp.getMonth() == today.getMonth()) and (day == today.getDay()):
                 today_index = [row, col]
-                                                 
+
             day += 1
             col += 1
 
@@ -293,12 +501,14 @@
             # Check on today
             if (temp.getYear() == today.getYear()) and (temp.getMonth() == today.getMonth()) and (day == today.getDay()):
                 today_index = [0, back_col]
-                                                              
+
             back_col -= 1
             day -= 1
 
     return table, today_index
 
+
+
 def set_difference(v1, v2):
     if len(v1) == 0 or len(v2) == 0:
         return v1
@@ -306,4 +516,4 @@
     s1 = set(v1)
     s2 = set(v2)
     s3 = s1.difference(s2)
-    return list(s3)
\ No newline at end of file
+    return list(s3)

Modified: PyCalendar/trunk/src/pycalendar/valarm.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/valarm.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/valarm.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,32 +14,49 @@
 #    limitations under the License.
 ##
 
-from attribute import PyCalendarAttribute
-from component import PyCalendarComponent
-from datetime import PyCalendarDateTime
-from duration import PyCalendarDuration
-from property import PyCalendarProperty
-from value import PyCalendarValue
-import definitions
+from pycalendar import definitions
+from pycalendar.attribute import PyCalendarAttribute
+from pycalendar.component import PyCalendarComponent
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.duration import PyCalendarDuration
+from pycalendar.icalendar.validation import ICALENDAR_VALUE_CHECKS
+from pycalendar.property import PyCalendarProperty
+from pycalendar.value import PyCalendarValue
 
 class PyCalendarVAlarm(PyCalendarComponent):
 
-    sBeginDelimiter = definitions.cICalComponent_BEGINVALARM
+    sActionMap = {
+        definitions.cICalProperty_ACTION_AUDIO: definitions.eAction_VAlarm_Audio,
+        definitions.cICalProperty_ACTION_DISPLAY: definitions.eAction_VAlarm_Display,
+        definitions.cICalProperty_ACTION_EMAIL: definitions.eAction_VAlarm_Email,
+        definitions.cICalProperty_ACTION_PROCEDURE: definitions.eAction_VAlarm_Procedure,
+        definitions.cICalProperty_ACTION_URI: definitions.eAction_VAlarm_URI,
+        definitions.cICalProperty_ACTION_NONE: definitions.eAction_VAlarm_None,
+    }
 
-    sEndDelimiter = definitions.cICalComponent_ENDVALARM
+    sActionValueMap = {
+        definitions.eAction_VAlarm_Audio: definitions.cICalProperty_ACTION_AUDIO,
+        definitions.eAction_VAlarm_Display: definitions.cICalProperty_ACTION_DISPLAY,
+        definitions.eAction_VAlarm_Email: definitions.cICalProperty_ACTION_EMAIL,
+        definitions.eAction_VAlarm_Procedure: definitions.cICalProperty_ACTION_PROCEDURE,
+        definitions.eAction_VAlarm_URI: definitions.cICalProperty_ACTION_URI,
+        definitions.eAction_VAlarm_None: definitions.cICalProperty_ACTION_NONE,
+    }
 
     # Classes for each action encapsulating action-specific data
     class PyCalendarVAlarmAction(object):
 
-        def __init__(self, type=None, copyit=None):
-            if type is not None:
-                self.mType = type
-            elif copyit is not None:
-                self.mType = copyit.mType
+        propertyCardinality_1 = ()
+        propertyCardinality_1_Fix_Empty = ()
+        propertyCardinality_0_1 = ()
+        propertyCardinality_1_More = ()
 
-        def clone_it(self):
-            pass
+        def __init__(self, type):
+            self.mType = type
 
+        def duplicate(self):
+            return PyCalendarVAlarm.PyCalendarVAlarmAction(self.mType)
+
         def load(self, valarm):
             pass
 
@@ -52,21 +69,28 @@
         def getType(self):
             return self.mType
 
+
     class PyCalendarVAlarmAudio(PyCalendarVAlarmAction):
 
-        def __init__(self, speak=None, copyit=None):
-            if speak is not None:
-                super(PyCalendarVAlarm.PyCalendarVAlarmAudio, self).__init__(type=definitions.eAction_VAlarm_Audio)
-                self.mSpeakText = speak
-            elif copyit is not None:
-                super(PyCalendarVAlarm.PyCalendarVAlarmAudio, self).__init__(copyit=copyit)
-                self.mSpeakText = copyit.mSpeakText
-            else:
-                super(PyCalendarVAlarm.PyCalendarVAlarmAudio, self).__init__(type=definitions.eAction_VAlarm_Audio)
+        propertyCardinality_1 = (
+            definitions.cICalProperty_ACTION,
+            definitions.cICalProperty_TRIGGER,
+        )
 
-        def clone_it(self):
-            return PyCalendarVAlarm.PyCalendarVAlarmAudio(self)
+        propertyCardinality_0_1 = (
+            definitions.cICalProperty_DURATION,
+            definitions.cICalProperty_REPEAT,
+            definitions.cICalProperty_ATTACH,
+            definitions.cICalProperty_ACKNOWLEDGED,
+        )
 
+        def __init__(self, speak=None):
+            super(PyCalendarVAlarm.PyCalendarVAlarmAudio, self).__init__(type=definitions.eAction_VAlarm_Audio)
+            self.mSpeakText = speak
+
+        def duplicate(self):
+            return PyCalendarVAlarm.PyCalendarVAlarmAudio(self.mSpeakText)
+
         def load(self, valarm):
             # Get properties
             self.mSpeakText = valarm.loadValueString(definitions.cICalProperty_ACTION_X_SPEAKTEXT)
@@ -87,21 +111,31 @@
         def getSpeakText(self):
             return self.mSpeakText
 
+
     class PyCalendarVAlarmDisplay(PyCalendarVAlarmAction):
 
-        def __init__(self, description=None, copyit=None):
-            if description is not None:
-                super(PyCalendarVAlarm.PyCalendarVAlarmDisplay, self).__init__(type=definitions.eAction_VAlarm_Display)
-                self.mDescription = description
-            elif copyit is not None:
-                super(PyCalendarVAlarm.PyCalendarVAlarmDisplay, self).__init__(copyit=copyit)
-                self.mDescription = copyit.mDescription
-            else:
-                super(PyCalendarVAlarm.PyCalendarVAlarmDisplay, self).__init__(type=definitions.eAction_VAlarm_Display)
+        propertyCardinality_1 = (
+            definitions.cICalProperty_ACTION,
+            definitions.cICalProperty_TRIGGER,
+        )
 
-        def clone_it(self):
-            return PyCalendarVAlarm.PyCalendarVAlarmDisplay(self)
+        propertyCardinality_1_Fix_Empty = (
+            definitions.cICalProperty_DESCRIPTION,
+        )
 
+        propertyCardinality_0_1 = (
+            definitions.cICalProperty_DURATION,
+            definitions.cICalProperty_REPEAT,
+            definitions.cICalProperty_ACKNOWLEDGED,
+        )
+
+        def __init__(self, description=None):
+            super(PyCalendarVAlarm.PyCalendarVAlarmDisplay, self).__init__(type=definitions.eAction_VAlarm_Display)
+            self.mDescription = description
+
+        def duplicate(self):
+            return PyCalendarVAlarm.PyCalendarVAlarmDisplay(self.mDescription)
+
         def load(self, valarm):
             # Get properties
             self.mDescription = valarm.loadValueString(definitions.cICalProperty_DESCRIPTION)
@@ -119,25 +153,38 @@
         def getDescription(self):
             return self.mDescription
 
+
     class PyCalendarVAlarmEmail(PyCalendarVAlarmAction):
 
-        def __init__(self, description=None, summary=None, attendees=None, copyit=None):
-            if description is not None:
-                super(PyCalendarVAlarm.PyCalendarVAlarmEmail, self).__init__(type=definitions.eAction_VAlarm_Display)
-                self.mDescription = description
-                self.mSummary = summary
-                self.mAttendees = attendees
-            elif copyit is not None:
-                super(PyCalendarVAlarm.PyCalendarVAlarmEmail, self).__init__(copyit=copyit)
-                self.mDescription = self.mDescription
-                self.mSummary = self.mSummary
-                self.mAttendees = copyit.mAttendees[:]
-            else:
-                super(PyCalendarVAlarm.PyCalendarVAlarmEmail, self).__init__(type=definitions.eAction_VAlarm_Display)
+        propertyCardinality_1 = (
+            definitions.cICalProperty_ACTION,
+            definitions.cICalProperty_TRIGGER,
+        )
 
-        def clone_it(self):
-            return PyCalendarVAlarm.PyCalendarVAlarmEmail(self)
+        propertyCardinality_1_Fix_Empty = (
+            definitions.cICalProperty_DESCRIPTION,
+            definitions.cICalProperty_SUMMARY,
+        )
 
+        propertyCardinality_0_1 = (
+            definitions.cICalProperty_DURATION,
+            definitions.cICalProperty_REPEAT,
+            definitions.cICalProperty_ACKNOWLEDGED,
+        )
+
+        propertyCardinality_1_More = (
+            definitions.cICalProperty_ATTENDEE,
+        )
+
+        def __init__(self, description=None, summary=None, attendees=None):
+            super(PyCalendarVAlarm.PyCalendarVAlarmEmail, self).__init__(type=definitions.eAction_VAlarm_Email)
+            self.mDescription = description
+            self.mSummary = summary
+            self.mAttendees = attendees
+
+        def duplicate(self):
+            return PyCalendarVAlarm.PyCalendarVAlarmEmail(self.mDescription, self.mSummary, self.mAttendees)
+
         def load(self, valarm):
             # Get properties
             self.mDescription = valarm.loadValueString(definitions.cICalProperty_DESCRIPTION)
@@ -150,7 +197,7 @@
                 for iter in range:
                     # Get the attendee value
                     attendee = iter.getCalAddressValue()
-                    if attendee != None:
+                    if attendee is not None:
                         self.mAttendees.append(attendee.getValue())
 
         def add(self, valarm):
@@ -181,124 +228,180 @@
         def getAttendees(self):
             return self.mAttendees
 
+
     class PyCalendarVAlarmUnknown(PyCalendarVAlarmAction):
 
-        def __init__(self, copyit=None):
-            if copyit is not None:
-                super(PyCalendarVAlarm.PyCalendarVAlarmUnknown, self).__init__(copyit=copyit)
-            else:
-                super(PyCalendarVAlarm.PyCalendarVAlarmUnknown, self).__init__(type=definitions.eAction_VAlarm_Unknown)
+        propertyCardinality_1 = (
+            definitions.cICalProperty_ACTION,
+            definitions.cICalProperty_TRIGGER,
+        )
 
-        def clone_it(self):
-            return PyCalendarVAlarm.PyCalendarVAlarmUnknown(self)
+        propertyCardinality_0_1 = (
+            definitions.cICalProperty_DURATION,
+            definitions.cICalProperty_REPEAT,
+            definitions.cICalProperty_ACKNOWLEDGED,
+        )
 
+        def __init__(self):
+            super(PyCalendarVAlarm.PyCalendarVAlarmUnknown, self).__init__(type=definitions.eAction_VAlarm_Unknown)
+
+        def duplicate(self):
+            return PyCalendarVAlarm.PyCalendarVAlarmUnknown()
+
+
+    class PyCalendarVAlarmURI(PyCalendarVAlarmAction):
+
+        propertyCardinality_1 = (
+            definitions.cICalProperty_ACTION,
+            definitions.cICalProperty_TRIGGER,
+            definitions.cICalProperty_URL,
+        )
+
+        propertyCardinality_0_1 = (
+            definitions.cICalProperty_DURATION,
+            definitions.cICalProperty_REPEAT,
+            definitions.cICalProperty_ACKNOWLEDGED,
+        )
+
+        def __init__(self, uri=None):
+            super(PyCalendarVAlarm.PyCalendarVAlarmURI, self).__init__(type=definitions.eAction_VAlarm_URI)
+            self.mURI = uri
+
+        def duplicate(self):
+            return PyCalendarVAlarm.PyCalendarVAlarmURI(self.mURI)
+
         def load(self, valarm):
-            pass
+            # Get properties
+            self.mURI = valarm.loadValueString(definitions.cICalProperty_URL)
 
         def add(self, valarm):
-            pass
+            # Delete existing then add
+            self.remove(valarm)
 
+            prop = PyCalendarProperty(definitions.cICalProperty_URL, self.mURI)
+            valarm.addProperty(prop)
+
         def remove(self, valarm):
-            pass
+            valarm.removeProperties(definitions.cICalProperty_URL)
 
-    @staticmethod
-    def getVBegin():
-        return PyCalendarVAlarm.sBeginDelimiter
+        def getURI(self):
+            return self.mURI
 
-    @staticmethod
-    def getVEnd():
-        return PyCalendarVAlarm.sEndDelimiter
 
+    class PyCalendarVAlarmNone(PyCalendarVAlarmAction):
+
+        propertyCardinality_1 = (
+            definitions.cICalProperty_ACTION,
+        )
+
+        def __init__(self):
+            super(PyCalendarVAlarm.PyCalendarVAlarmNone, self).__init__(type=definitions.eAction_VAlarm_None)
+
+        def duplicate(self):
+            return PyCalendarVAlarm.PyCalendarVAlarmNone()
+
+
     def getMimeComponentName(self):
         # Cannot be sent as a separate MIME object
         return None
 
-    def __init__(self, calendar=None, copyit=None):
-        
-        if calendar is not None:
-            super(PyCalendarVAlarm, self).__init__(calendar=calendar)
-    
-            self.mAction = definitions.eAction_VAlarm_Display
-            self.mTriggerAbsolute = False
-            self.mTriggerOnStart = True
-            self.mTriggerOn = PyCalendarDateTime()
-    
-            # Set duration default to 1 hour
-            self.mTriggerBy = PyCalendarDuration()
-            self.mTriggerBy.setDuration(60 * 60)
-    
-            # Does not repeat by default
-            self.mRepeats = 0
-            self.mRepeatInterval = PyCalendarDuration()
-            self.mRepeatInterval.setDuration(5 * 60) # Five minutes
-    
-            # Status
-            self.mStatusInit = False
-            self.mAlarmStatus = definitions.eAlarm_Status_Pending
-            self.mLastTrigger = PyCalendarDateTime()
-            self.mNextTrigger = PyCalendarDateTime()
-            self.mDoneCount = 0
-    
-            # Create action data
-            self.mActionData = PyCalendarVAlarm.PyCalendarVAlarmDisplay("")
+    sActionToAlarmMap = {
+        definitions.eAction_VAlarm_Audio: PyCalendarVAlarmAudio,
+        definitions.eAction_VAlarm_Display: PyCalendarVAlarmDisplay,
+        definitions.eAction_VAlarm_Email: PyCalendarVAlarmEmail,
+        definitions.eAction_VAlarm_URI: PyCalendarVAlarmURI,
+        definitions.eAction_VAlarm_None: PyCalendarVAlarmNone,
+    }
 
-        elif copyit is not None:
-            super(PyCalendarVAlarm, self).__init__(copyit=copyit)
-    
-            self.mAction = copyit.mAction
-            self.mTriggerAbsolute = copyit.mTriggerAbsolute
-            self.mTriggerOn = PyCalendarDateTime(copyit.mTriggerOn)
-            self.mTriggerBy = PyCalendarDuration(copyit.mTriggerBy)
-            self.mTriggerOnStart = copyit.mTriggerOnStart
-    
-            self.mRepeats = copyit.mRepeats
-            self.mRepeatInterval = PyCalendarDuration(copyit.mRepeatInterval)
-    
-            self.mAlarmStatus = copyit.mAlarmStatus
-            if copyit.mLastTrigger is not None:
-                self.mLastTrigger = PyCalendarDateTime(copyit.mLastTrigger)
-            if copyit.mNextTrigger is not None:
-                self.mNextTrigger = PyCalendarDateTime(copyit.mNextTrigger)
-            self.mDoneCount = copyit.mDoneCount
-    
-            self.mActionData = copyit.mActionData.clone_it()
+    propertyValueChecks = ICALENDAR_VALUE_CHECKS
 
-    def clone_it(self):
-        return PyCalendarVAlarm(self)
+    def __init__(self, parent=None):
 
+        super(PyCalendarVAlarm, self).__init__(parent=parent)
+
+        self.mAction = definitions.eAction_VAlarm_Display
+        self.mTriggerAbsolute = False
+        self.mTriggerOnStart = True
+        self.mTriggerOn = PyCalendarDateTime()
+
+        # Set duration default to 1 hour
+        self.mTriggerBy = PyCalendarDuration()
+        self.mTriggerBy.setDuration(60 * 60)
+
+        # Does not repeat by default
+        self.mRepeats = 0
+        self.mRepeatInterval = PyCalendarDuration()
+        self.mRepeatInterval.setDuration(5 * 60) # Five minutes
+
+        # Status
+        self.mStatusInit = False
+        self.mAlarmStatus = definitions.eAlarm_Status_Pending
+        self.mLastTrigger = PyCalendarDateTime()
+        self.mNextTrigger = PyCalendarDateTime()
+        self.mDoneCount = 0
+
+        # Create action data
+        self.mActionData = PyCalendarVAlarm.PyCalendarVAlarmDisplay("")
+
+
+    def duplicate(self, parent=None):
+        other = super(PyCalendarVAlarm, self).duplicate(parent=parent)
+        other.mAction = self.mAction
+        other.mTriggerAbsolute = self.mTriggerAbsolute
+        other.mTriggerOn = self.mTriggerOn.duplicate()
+        other.mTriggerBy = self.mTriggerBy.duplicate()
+        other.mTriggerOnStart = self.mTriggerOnStart
+
+        other.mRepeats = self.mRepeats
+        other.mRepeatInterval = self.mRepeatInterval.duplicate()
+
+        other.mAlarmStatus = self.mAlarmStatus
+        if self.mLastTrigger is not None:
+            other.mLastTrigger = self.mLastTrigger.duplicate()
+        if self.mNextTrigger is not None:
+            other.mNextTrigger = self.mNextTrigger.duplicate()
+        other.mDoneCount = self.mDoneCount
+
+        other.mActionData = self.mActionData.duplicate()
+        return other
+
+
     def getType(self):
-        return PyCalendarComponent.eVALARM
+        return definitions.cICalComponent_VALARM
 
-    def getBeginDelimiter(self):
-        return PyCalendarVAlarm.sBeginDelimiter
 
-    def getEndDelimiter(self):
-        return PyCalendarVAlarm.sEndDelimiter
-
     def getAction(self):
         return self.mAction
 
+
     def getActionData(self):
         return self.mActionData
 
+
     def isTriggerAbsolute(self):
         return self.mTriggerAbsolute
 
+
     def getTriggerOn(self):
         return self.mTriggerOn
 
+
     def getTriggerDuration(self):
         return self.mTriggerBy
-  
+
+
     def isTriggerOnStart(self):
         return self.mTriggerOnStart
 
+
     def getRepeats(self):
         return self.mRepeats
 
+
     def getInterval(self):
         return self.mRepeatInterval
 
+
     def added(self):
         # Added to calendar so add to calendar notifier
         # calstore::CCalendarNotifier::sCalendarNotifier.AddAlarm(this)
@@ -306,6 +409,7 @@
         # Do inherited
         super(PyCalendarVAlarm, self).added()
 
+
     def removed(self):
         # Removed from calendar so add to calendar notifier
         # calstore::CCalendarNotifier::sCalendarNotifier.RemoveAlarm(this)
@@ -313,6 +417,7 @@
         # Do inherited
         super(PyCalendarVAlarm, self).removed()
 
+
     def changed(self):
         # Always force recalc of trigger status
         self.mStatusInit = False
@@ -324,6 +429,7 @@
         # do top-level component changes
         # super.changed()
 
+
     def finalise(self):
         # Do inherited
         super(PyCalendarVAlarm, self).finalise()
@@ -331,17 +437,7 @@
         # Get the ACTION
         temp = self.loadValueString(definitions.cICalProperty_ACTION)
         if temp is not None:
-            if temp == definitions.cICalProperty_ACTION_AUDIO:
-                self.mAction = definitions.eAction_VAlarm_Audio
-            elif temp == definitions.cICalProperty_ACTION_DISPLAY:
-                self.mAction = definitions.eAction_VAlarm_Display
-            elif temp == definitions.cICalProperty_ACTION_EMAIL:
-                self.mAction = definitions.eAction_VAlarm_Email
-            elif temp == definitions.cICalProperty_ACTION_PROCEDURE:
-                self.mAction = definitions.eAction_VAlarm_Procedure
-            else:
-                self.mAction = definitions.eAction_VAlarm_Unknown
-
+            self.mAction = PyCalendarVAlarm.sActionMap.get(temp, definitions.eAction_VAlarm_Unknown)
             self.loadAction()
 
         # Get the trigger
@@ -377,6 +473,9 @@
         if temp is not None:
             self.mRepeatInterval = temp
 
+        # Set a map key for sorting
+        self.mMapKey = "%s:%s" % (self.mAction, self.mTriggerOn if self.mTriggerAbsolute else self.mTriggerBy,)
+
         # Alarm status - private to Mulberry
         status = self.loadValueString(definitions.cICalProperty_ALARM_X_ALARMSTATUS)
         if status is not None:
@@ -394,6 +493,35 @@
         if temp is not None:
             self.mLastTrigger = temp
 
+
+    def validate(self, doFix=False):
+        """
+        Validate the data in this component and optionally fix any problems, else raise. If
+        loggedProblems is not None it must be a C{list} and problem descriptions are appended
+        to that.
+        """
+
+        # Validate using action specific constraints
+        self.propertyCardinality_1 = self.mActionData.propertyCardinality_1
+        self.propertyCardinality_1_Fix_Empty = self.mActionData.propertyCardinality_1_Fix_Empty
+        self.propertyCardinality_0_1 = self.mActionData.propertyCardinality_0_1
+        self.propertyCardinality_1_More = self.mActionData.propertyCardinality_1_More
+
+        fixed, unfixed = super(PyCalendarVAlarm, self).validate(doFix)
+
+        # Extra constraint: both DURATION and REPEAT must be present togethe
+        if self.hasProperty(definitions.cICalProperty_DURATION) ^ self.hasProperty(definitions.cICalProperty_REPEAT):
+            # Cannot fix this
+            logProblem = "[%s] Properties must be present together: %s, %s" % (
+                self.getType(),
+                definitions.cICalProperty_DURATION,
+                definitions.cICalProperty_REPEAT,
+            )
+            unfixed.append(logProblem)
+
+        return fixed, unfixed
+
+
     def editStatus(self, status):
         # Remove existing
         self.removeProperties(definitions.cICalProperty_ALARM_X_ALARMSTATUS)
@@ -411,6 +539,7 @@
             status_txt = definitions.cICalProperty_ALARM_X_ALARMSTATUS_DISABLED
         self.addProperty(PyCalendarProperty(definitions.cICalProperty_ALARM_X_ALARMSTATUS, status_txt))
 
+
     def editAction(self, action, data):
         # Remove existing
         self.removeProperties(definitions.cICalProperty_ACTION)
@@ -422,21 +551,14 @@
         self.mActionData = data
 
         # Add new properties to alarm
-        action_txt = ""
-        if self.mAction == definitions.eAction_VAlarm_Audio:
-            action_txt = definitions.cICalProperty_ACTION_AUDIO
-        elif self.mAction == definitions.eAction_VAlarm_Display:
-            action_txt = definitions.cICalProperty_ACTION_DISPLAY
-        elif self.mAction == definitions.eAction_VAlarm_Email:
-            action_txt = definitions.cICalProperty_ACTION_EMAIL
-        else:
-            action_txt = definitions.cICalProperty_ACTION_PROCEDURE
+        action_txt = PyCalendarVAlarm.sActionValueMap.get(self.mAction, definitions.cICalProperty_ACTION_PROCEDURE)
 
         prop = PyCalendarProperty(definitions.cICalProperty_ACTION, action_txt)
         self.addProperty(prop)
 
         self.mActionData.add(self)
 
+
     def editTriggerOn(self, dt):
         # Remove existing
         self.removeProperties(definitions.cICalProperty_TRIGGER)
@@ -449,6 +571,7 @@
         prop = PyCalendarProperty(definitions.cICalProperty_TRIGGER, dt)
         self.addProperty(prop)
 
+
     def editTriggerBy(self, duration, trigger_start):
         # Remove existing
         self.removeProperties(definitions.cICalProperty_TRIGGER)
@@ -466,6 +589,7 @@
         prop.addAttribute(attr)
         self.addProperty(prop)
 
+
     def editRepeats(self, repeat, interval):
         # Remove existing
         self.removeProperties(definitions.cICalProperty_REPEAT)
@@ -480,14 +604,17 @@
             self.addProperty(PyCalendarProperty(definitions.cICalProperty_REPEAT, repeat))
             self.addProperty(PyCalendarProperty(definitions.cICalProperty_DURATION, interval))
 
+
     def getAlarmStatus(self):
         return self.mAlarmStatus
 
+
     def getNextTrigger(self, dt):
         if not self.mStatusInit:
             self.initNextTrigger()
         dt.copy(self.mNextTrigger)
 
+
     def alarmTriggered(self, dt):
         # Remove existing
         self.removeProperties(definitions.cICalProperty_ALARM_X_LASTTRIGGER)
@@ -518,22 +645,14 @@
         # Now update dt to the next alarm time
         return self.mAlarmStatus == definitions.eAlarm_Status_Pending
 
+
     def loadAction(self):
         # Delete current one
         self.mActionData = None
-        if self.mAction == definitions.eAction_VAlarm_Audio:
-            self.mActionData = PyCalendarVAlarm.PyCalendarVAlarmAudio()
-            self.mActionData.load(self)
-        elif self.mAction == definitions.eAction_VAlarm_Display:
-            self.mActionData = PyCalendarVAlarm.PyCalendarVAlarmDisplay()
-            self.mActionData.load(self)
-        elif self.mAction == definitions.eAction_VAlarm_Email:
-            self.mActionData = PyCalendarVAlarm.PyCalendarVAlarmEmail()
-            self.mActionData.load(self)
-        else:
-            self.mActionData = PyCalendarVAlarm.PyCalendarVAlarmUnknown()
-            self.mActionData.load(self)
+        self.mActionData = PyCalendarVAlarm.sActionToAlarmMap.get(self.mAction, PyCalendarVAlarm.PyCalendarVAlarmUnknown)()
+        self.mActionData.load(self)
 
+
     def initNextTrigger(self):
         # Do not bother if its completed
         if self.mAlarmStatus == definitions.eAlarm_Status_Completed:
@@ -569,6 +688,7 @@
 
         self.mNextTrigger = trigger
 
+
     def getFirstTrigger(self, dt):
         # If absolute trigger, use that
         if self.isTriggerAbsolute():

Copied: PyCalendar/trunk/src/pycalendar/validation.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/validation.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/validation.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/validation.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,75 @@
+##
+#    Copyright (c) 2011-2012 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.plaintextvalue import PyCalendarPlainTextValue
+
+# Grabbed from http://docs.python.org/library/functools.html since we need to support Python 2.5
+def partial(func, *args, **keywords):
+    def newfunc(*fargs, **fkeywords):
+        newkeywords = keywords.copy()
+        newkeywords.update(fkeywords)
+        return func(*(args + fargs), **newkeywords)
+    newfunc.func = func
+    newfunc.args = args
+    newfunc.keywords = keywords
+    return newfunc
+
+
+
+class PropertyValueChecks(object):
+
+    @staticmethod
+    def stringValue(text, property):
+
+        value = property.getValue()
+        if value and isinstance(value, PyCalendarPlainTextValue):
+            value = value.getValue()
+            return value.lower() == text.lower()
+
+        return False
+
+
+    @staticmethod
+    def alwaysUTC(property):
+
+        value = property.getDateTimeValue()
+        if value:
+            value = value.getValue()
+            return value.utc()
+
+        return False
+
+
+    @staticmethod
+    def numericRange(low, high, property):
+
+        value = property.getIntegerValue()
+        if value:
+            value = value.getValue()
+            return value >= low and value <= high
+
+        return False
+
+
+    @staticmethod
+    def positiveIntegerOrZero(property):
+
+        value = property.getIntegerValue()
+        if value:
+            value = value.getValue()
+            return value >= 0
+
+        return False

Modified: PyCalendar/trunk/src/pycalendar/value.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/value.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/value.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -16,32 +16,62 @@
 
 # ICalendar Value class
 
-class PyCalendarValue(object):
+from pycalendar.valueutils import ValueMixin
+from pycalendar import xmldefs
+import xml.etree.cElementTree as XML
 
-    VALUETYPE_BINARY = 0
-    VALUETYPE_BOOLEAN = 1
-    VALUETYPE_CALADDRESS = 2
-    VALUETYPE_DATE = 3
-    VALUETYPE_DATETIME = 4
-    VALUETYPE_DURATION = 5
-    VALUETYPE_FLOAT = 6
-    VALUETYPE_GEO = 7
-    VALUETYPE_INTEGER = 8
-    VALUETYPE_PERIOD = 9
-    VALUETYPE_RECUR = 10
-    VALUETYPE_TEXT = 11
-    VALUETYPE_TIME = 12
-    VALUETYPE_URI = 13
-    VALUETYPE_UTC_OFFSET = 14
-    VALUETYPE_MULTIVALUE = 15
-    VALUETYPE_XNAME = 16
-    
+class PyCalendarValue(ValueMixin):
+
+    (
+        VALUETYPE_ADR,
+        VALUETYPE_BINARY,
+        VALUETYPE_BOOLEAN,
+        VALUETYPE_CALADDRESS,
+        VALUETYPE_DATE,
+        VALUETYPE_DATETIME,
+        VALUETYPE_DURATION,
+        VALUETYPE_FLOAT,
+        VALUETYPE_GEO,
+        VALUETYPE_INTEGER,
+        VALUETYPE_N,
+        VALUETYPE_ORG,
+        VALUETYPE_PERIOD,
+        VALUETYPE_RECUR,
+        VALUETYPE_REQUEST_STATUS,
+        VALUETYPE_TEXT,
+        VALUETYPE_TIME,
+        VALUETYPE_UNKNOWN,
+        VALUETYPE_URI,
+        VALUETYPE_UTC_OFFSET,
+        VALUETYPE_VCARD,
+        VALUETYPE_MULTIVALUE,
+        VALUETYPE_XNAME,
+    ) = range(23)
+
     _typeMap = {}
-    
+    _xmlMap = {}
+
+
+    def __hash__(self):
+        return hash((self.getType(), self.getValue()))
+
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+
+    def __eq__(self, other):
+        if not isinstance(other, PyCalendarValue):
+            return False
+        return self.getType() == other.getType() and self.getValue() == other.getValue()
+
+
     @classmethod
-    def registerType(clz, type, cls):
+    def registerType(clz, type, cls, xmlNode):
         clz._typeMap[type] = cls
-    
+        clz._xmlMap[type] = xmlNode
+
+
     @classmethod
     def createFromType(clz, type):
         # Create the type
@@ -49,21 +79,28 @@
         if created:
             return created()
         else:
-            return clz._typeMap.get("DUMMY")(type)
-    
-    def copy(self):
-        classType = PyCalendarValue._typeMap.get(self.getRealType(), None)
-        return classType(copyit=self)
+            return clz._typeMap.get(PyCalendarValue.VALUETYPE_UNKNOWN)(type)
 
+
     def getType(self):
-        raise NotImplemented
+        raise NotImplementedError
 
+
     def getRealType(self):
         return self.getType()
 
-    def parse(self, data):
-        raise NotImplemented
-    
-    def generate(self, os):
-        raise NotImplemented
-    
\ No newline at end of file
+
+    def getValue(self):
+        raise NotImplementedError
+
+
+    def setValue(self, value):
+        raise NotImplementedError
+
+
+    def writeXML(self, node, namespace):
+        raise NotImplementedError
+
+
+    def getXMLNode(self, node, namespace):
+        return XML.SubElement(node, xmldefs.makeTag(namespace, self._xmlMap[self.getType()]))

Copied: PyCalendar/trunk/src/pycalendar/valueutils.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/valueutils.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/valueutils.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/valueutils.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,49 @@
+##
+#    Copyright (c) 2007-2012 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.
+##
+
+# Helpers for value classes
+
+from cStringIO import StringIO
+
+class ValueMixin(object):
+
+    def __str__(self):
+        return self.getText()
+
+
+    @classmethod
+    def parseText(cls, data):
+        value = cls()
+        value.parse(data)
+        return value
+
+
+    def parse(self, data):
+        raise NotImplementedError
+
+
+    def generate(self, os):
+        raise NotImplementedError
+
+
+    def getText(self):
+        os = StringIO()
+        self.generate(os)
+        return os.getvalue()
+
+
+    def writeXML(self, node, namespace):
+        raise NotImplementedError

Copied: PyCalendar/trunk/src/pycalendar/vavailability.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/vavailability.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/vavailability.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/vavailability.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,107 @@
+##
+#    Copyright (c) 2011-2012 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 import definitions
+from pycalendar import itipdefinitions
+from pycalendar.component import PyCalendarComponent
+from pycalendar.icalendar.validation import ICALENDAR_VALUE_CHECKS
+
+class PyCalendarVAvailability(PyCalendarComponent):
+
+    propertyCardinality_1 = (
+        definitions.cICalProperty_DTSTAMP,
+        definitions.cICalProperty_UID,
+    )
+
+    propertyCardinality_0_1 = (
+        definitions.cICalProperty_BUSYTYPE,
+        definitions.cICalProperty_CLASS,
+        definitions.cICalProperty_CREATED,
+        definitions.cICalProperty_DESCRIPTION,
+        definitions.cICalProperty_DTSTART,
+        definitions.cICalProperty_LAST_MODIFIED,
+        definitions.cICalProperty_ORGANIZER,
+        definitions.cICalProperty_SEQUENCE,
+        definitions.cICalProperty_SUMMARY,
+        definitions.cICalProperty_URL,
+        definitions.cICalProperty_RECURRENCE_ID,
+        definitions.cICalProperty_DTEND,
+        definitions.cICalProperty_DURATION,
+    )
+
+    propertyValueChecks = ICALENDAR_VALUE_CHECKS
+
+    def __init__(self, parent=None):
+        super(PyCalendarVAvailability, self).__init__(parent=parent)
+
+
+    def duplicate(self, parent=None):
+        return super(PyCalendarVAvailability, self).duplicate(parent=parent)
+
+
+    def getType(self):
+        return definitions.cICalComponent_VAVAILABILITY
+
+
+    def getMimeComponentName(self):
+        return itipdefinitions.cICalMIMEComponent_VAVAILABILITY
+
+
+    def finalise(self):
+        super(PyCalendarVAvailability, self).finalise()
+
+
+    def validate(self, doFix=False):
+        """
+        Validate the data in this component and optionally fix any problems, else raise. If
+        loggedProblems is not None it must be a C{list} and problem descriptions are appended
+        to that.
+        """
+
+        fixed, unfixed = super(PyCalendarVAvailability, self).validate(doFix)
+
+        # Extra constraint: only one of DTEND or DURATION
+        if self.hasProperty(definitions.cICalProperty_DTEND) and self.hasProperty(definitions.cICalProperty_DURATION):
+            # Fix by removing the DTEND
+            logProblem = "[%s] Properties must not both be present: %s, %s" % (
+                self.getType(),
+                definitions.cICalProperty_DTEND,
+                definitions.cICalProperty_DURATION,
+            )
+            if doFix:
+                self.removeProperties(definitions.cICalProperty_DTEND)
+                fixed.append(logProblem)
+            else:
+                unfixed.append(logProblem)
+
+        return fixed, unfixed
+
+
+    def addComponent(self, comp):
+        # We can embed the available components only
+        if comp.getType() == definitions.cICalComponent_AVAILABLE:
+            super(PyCalendarVAvailability, self).addComponent(comp)
+        else:
+            raise ValueError
+
+
+    def sortedPropertyKeyOrder(self):
+        return (
+            definitions.cICalProperty_UID,
+            definitions.cICalProperty_DTSTART,
+            definitions.cICalProperty_DURATION,
+            definitions.cICalProperty_DTEND,
+        )

Deleted: PyCalendar/trunk/src/pycalendar/vcard/__init__.py
===================================================================
--- PyCalendar/branches/server/src/pycalendar/vcard/__init__.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/vcard/__init__.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,15 +0,0 @@
-#
-#    Copyright (c) 2007-2012 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.
-##

Copied: PyCalendar/trunk/src/pycalendar/vcard/__init__.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/vcard/__init__.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/vcard/__init__.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/vcard/__init__.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,15 @@
+#
+#    Copyright (c) 2007-2012 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.
+##

Deleted: PyCalendar/trunk/src/pycalendar/vcard/card.py
===================================================================
--- PyCalendar/branches/server/src/pycalendar/vcard/card.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/vcard/card.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,285 +0,0 @@
-##
-#    Copyright (c) 2007-2012 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 cStringIO import StringIO
-from pycalendar.componentbase import PyCalendarComponentBase
-from pycalendar.exceptions import PyCalendarInvalidData, \
-    PyCalendarValidationError
-from pycalendar.parser import ParserContext
-from pycalendar.utils import readFoldedLine
-from pycalendar.vcard import definitions
-from pycalendar.vcard.definitions import VCARD, Property_VERSION, \
-    Property_PRODID, Property_UID
-from pycalendar.vcard.property import Property
-from pycalendar.vcard.validation import VCARD_VALUE_CHECKS
-
-class Card(PyCalendarComponentBase):
-
-    sProdID = "-//mulberrymail.com//Mulberry v4.0//EN"
-    sDomain = "mulberrymail.com"
-
-    @staticmethod
-    def setPRODID(prodid):
-        Card.sProdID = prodid
-
-
-    @staticmethod
-    def setDomain(domain):
-        Card.sDomain = domain
-
-    propertyCardinality_1 = (
-        definitions.Property_VERSION,
-        definitions.Property_N,
-    )
-
-    propertyCardinality_0_1 = (
-        definitions.Property_BDAY,
-        definitions.Property_PRODID,
-        definitions.Property_REV,
-        definitions.Property_UID,
-    )
-
-    propertyCardinality_1_More = (
-        definitions.Property_FN,
-    )
-
-    propertyValueChecks = VCARD_VALUE_CHECKS
-
-    def __init__(self, add_defaults=True):
-        super(Card, self).__init__()
-
-        if add_defaults:
-            self.addDefaultProperties()
-
-
-    def duplicate(self):
-        return super(Card, self).duplicate()
-
-
-    def getType(self):
-        return VCARD
-
-
-    def finalise(self):
-        pass
-
-
-    def validate(self, doFix=False, doRaise=False):
-        """
-        Validate the data in this component and optionally fix any problems. Return
-        a tuple containing two lists: the first describes problems that were fixed, the
-        second problems that were not fixed. Caller can then decide what to do with unfixed
-        issues.
-        """
-
-        # Optional raise behavior
-        fixed, unfixed = super(Card, self).validate(doFix)
-        if doRaise and unfixed:
-            raise PyCalendarValidationError(";".join(unfixed))
-        return fixed, unfixed
-
-
-    def sortedPropertyKeyOrder(self):
-        return (
-            Property_VERSION,
-            Property_PRODID,
-            Property_UID,
-        )
-
-
-    @staticmethod
-    def parseMultiple(ins):
-
-        results = []
-
-        card = Card(add_defaults=False)
-
-        LOOK_FOR_VCARD = 0
-        GET_PROPERTY = 1
-
-        state = LOOK_FOR_VCARD
-
-        # Get lines looking for start of calendar
-        lines = [None, None]
-
-        while readFoldedLine(ins, lines):
-
-            line = lines[0]
-            if state == LOOK_FOR_VCARD:
-
-                # Look for start
-                if line == card.getBeginDelimiter():
-                    # Next state
-                    state = GET_PROPERTY
-
-                # Handle blank line
-                elif len(line) == 0:
-                    # Raise if requested, otherwise just ignore
-                    if ParserContext.BLANK_LINES_IN_DATA == ParserContext.PARSER_RAISE:
-                        raise PyCalendarInvalidData("vCard data has blank lines")
-
-                # Unrecognized data
-                else:
-                    raise PyCalendarInvalidData("vCard data not recognized", line)
-
-            elif state == GET_PROPERTY:
-
-                # Look for end of object
-                if line == card.getEndDelimiter():
-
-                    # Finalise the current calendar
-                    card.finalise()
-
-                    # Validate some things
-                    if not card.hasProperty("VERSION"):
-                        raise PyCalendarInvalidData("vCard missing VERSION", "")
-
-                    results.append(card)
-
-                    # Change state
-                    card = Card(add_defaults=False)
-                    state = LOOK_FOR_VCARD
-
-                # Blank line
-                elif len(line) == 0:
-                    # Raise if requested, otherwise just ignore
-                    if ParserContext.BLANK_LINES_IN_DATA == ParserContext.PARSER_RAISE:
-                        raise PyCalendarInvalidData("vCard data has blank lines")
-
-                # Must be a property
-                else:
-
-                    # Parse attribute/value for top-level calendar item
-                    prop = Property()
-                    if prop.parse(line):
-
-                        # Check for valid property
-                        if not card.validProperty(prop):
-                            raise PyCalendarInvalidData("Invalid property", str(prop))
-                        else:
-                            card.addProperty(prop)
-
-        # Check for truncated data
-        if state != LOOK_FOR_VCARD:
-            raise PyCalendarInvalidData("vCard data not complete")
-
-        return results
-
-
-    @staticmethod
-    def parseText(data):
-
-        cal = Card(add_defaults=False)
-        if cal.parse(StringIO(data)):
-            return cal
-        else:
-            return None
-
-
-    def parse(self, ins):
-
-        result = False
-
-        self.setProperties({})
-
-        LOOK_FOR_VCARD = 0
-        GET_PROPERTY = 1
-
-        state = LOOK_FOR_VCARD
-
-        # Get lines looking for start of calendar
-        lines = [None, None]
-
-        while readFoldedLine(ins, lines):
-
-            line = lines[0]
-            if state == LOOK_FOR_VCARD:
-
-                # Look for start
-                if line == self.getBeginDelimiter():
-                    # Next state
-                    state = GET_PROPERTY
-
-                    # Indicate success at this point
-                    result = True
-
-                # Handle blank line
-                elif len(line) == 0:
-                    # Raise if requested, otherwise just ignore
-                    if ParserContext.BLANK_LINES_IN_DATA == ParserContext.PARSER_RAISE:
-                        raise PyCalendarInvalidData("vCard data has blank lines")
-
-                # Unrecognized data
-                else:
-                    raise PyCalendarInvalidData("vCard data not recognized", line)
-
-            elif state == GET_PROPERTY:
-
-                # Look for end of object
-                if line == self.getEndDelimiter():
-
-                    # Finalise the current calendar
-                    self.finalise()
-
-                    # Change state
-                    state = LOOK_FOR_VCARD
-
-                # Blank line
-                elif len(line) == 0:
-                    # Raise if requested, otherwise just ignore
-                    if ParserContext.BLANK_LINES_IN_DATA == ParserContext.PARSER_RAISE:
-                        raise PyCalendarInvalidData("vCard data has blank lines")
-
-                # Must be a property
-                else:
-
-                    # Parse attribute/value for top-level calendar item
-                    prop = Property()
-                    try:
-                        if prop.parse(line):
-
-                            # Check for valid property
-                            if not self.validProperty(prop):
-                                raise PyCalendarInvalidData("Invalid property", str(prop))
-                            else:
-                                self.addProperty(prop)
-                    except IndexError:
-                        print line
-
-        # Check for truncated data
-        if state != LOOK_FOR_VCARD:
-            raise PyCalendarInvalidData("vCard data not complete", "")
-
-        # Validate some things
-        if result and not self.hasProperty("VERSION"):
-            raise PyCalendarInvalidData("vCard missing VERSION", "")
-
-        return result
-
-
-    def addDefaultProperties(self):
-        self.addProperty(Property(definitions.Property_PRODID, Card.sProdID))
-        self.addProperty(Property(definitions.Property_VERSION, "3.0"))
-
-
-    def validProperty(self, prop):
-        if prop.getName() == definitions.Property_VERSION:
-
-            tvalue = prop.getValue()
-            if ((tvalue is None) or (tvalue.getValue() != "3.0")):
-                return False
-
-        return True

Copied: PyCalendar/trunk/src/pycalendar/vcard/card.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/vcard/card.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/vcard/card.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/vcard/card.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,285 @@
+##
+#    Copyright (c) 2007-2012 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 cStringIO import StringIO
+from pycalendar.componentbase import PyCalendarComponentBase
+from pycalendar.exceptions import PyCalendarInvalidData, \
+    PyCalendarValidationError
+from pycalendar.parser import ParserContext
+from pycalendar.utils import readFoldedLine
+from pycalendar.vcard import definitions
+from pycalendar.vcard.definitions import VCARD, Property_VERSION, \
+    Property_PRODID, Property_UID
+from pycalendar.vcard.property import Property
+from pycalendar.vcard.validation import VCARD_VALUE_CHECKS
+
+class Card(PyCalendarComponentBase):
+
+    sProdID = "-//mulberrymail.com//Mulberry v4.0//EN"
+    sDomain = "mulberrymail.com"
+
+    @staticmethod
+    def setPRODID(prodid):
+        Card.sProdID = prodid
+
+
+    @staticmethod
+    def setDomain(domain):
+        Card.sDomain = domain
+
+    propertyCardinality_1 = (
+        definitions.Property_VERSION,
+        definitions.Property_N,
+    )
+
+    propertyCardinality_0_1 = (
+        definitions.Property_BDAY,
+        definitions.Property_PRODID,
+        definitions.Property_REV,
+        definitions.Property_UID,
+    )
+
+    propertyCardinality_1_More = (
+        definitions.Property_FN,
+    )
+
+    propertyValueChecks = VCARD_VALUE_CHECKS
+
+    def __init__(self, add_defaults=True):
+        super(Card, self).__init__()
+
+        if add_defaults:
+            self.addDefaultProperties()
+
+
+    def duplicate(self):
+        return super(Card, self).duplicate()
+
+
+    def getType(self):
+        return VCARD
+
+
+    def finalise(self):
+        pass
+
+
+    def validate(self, doFix=False, doRaise=False):
+        """
+        Validate the data in this component and optionally fix any problems. Return
+        a tuple containing two lists: the first describes problems that were fixed, the
+        second problems that were not fixed. Caller can then decide what to do with unfixed
+        issues.
+        """
+
+        # Optional raise behavior
+        fixed, unfixed = super(Card, self).validate(doFix)
+        if doRaise and unfixed:
+            raise PyCalendarValidationError(";".join(unfixed))
+        return fixed, unfixed
+
+
+    def sortedPropertyKeyOrder(self):
+        return (
+            Property_VERSION,
+            Property_PRODID,
+            Property_UID,
+        )
+
+
+    @staticmethod
+    def parseMultiple(ins):
+
+        results = []
+
+        card = Card(add_defaults=False)
+
+        LOOK_FOR_VCARD = 0
+        GET_PROPERTY = 1
+
+        state = LOOK_FOR_VCARD
+
+        # Get lines looking for start of calendar
+        lines = [None, None]
+
+        while readFoldedLine(ins, lines):
+
+            line = lines[0]
+            if state == LOOK_FOR_VCARD:
+
+                # Look for start
+                if line == card.getBeginDelimiter():
+                    # Next state
+                    state = GET_PROPERTY
+
+                # Handle blank line
+                elif len(line) == 0:
+                    # Raise if requested, otherwise just ignore
+                    if ParserContext.BLANK_LINES_IN_DATA == ParserContext.PARSER_RAISE:
+                        raise PyCalendarInvalidData("vCard data has blank lines")
+
+                # Unrecognized data
+                else:
+                    raise PyCalendarInvalidData("vCard data not recognized", line)
+
+            elif state == GET_PROPERTY:
+
+                # Look for end of object
+                if line == card.getEndDelimiter():
+
+                    # Finalise the current calendar
+                    card.finalise()
+
+                    # Validate some things
+                    if not card.hasProperty("VERSION"):
+                        raise PyCalendarInvalidData("vCard missing VERSION", "")
+
+                    results.append(card)
+
+                    # Change state
+                    card = Card(add_defaults=False)
+                    state = LOOK_FOR_VCARD
+
+                # Blank line
+                elif len(line) == 0:
+                    # Raise if requested, otherwise just ignore
+                    if ParserContext.BLANK_LINES_IN_DATA == ParserContext.PARSER_RAISE:
+                        raise PyCalendarInvalidData("vCard data has blank lines")
+
+                # Must be a property
+                else:
+
+                    # Parse attribute/value for top-level calendar item
+                    prop = Property()
+                    if prop.parse(line):
+
+                        # Check for valid property
+                        if not card.validProperty(prop):
+                            raise PyCalendarInvalidData("Invalid property", str(prop))
+                        else:
+                            card.addProperty(prop)
+
+        # Check for truncated data
+        if state != LOOK_FOR_VCARD:
+            raise PyCalendarInvalidData("vCard data not complete")
+
+        return results
+
+
+    @staticmethod
+    def parseText(data):
+
+        cal = Card(add_defaults=False)
+        if cal.parse(StringIO(data)):
+            return cal
+        else:
+            return None
+
+
+    def parse(self, ins):
+
+        result = False
+
+        self.setProperties({})
+
+        LOOK_FOR_VCARD = 0
+        GET_PROPERTY = 1
+
+        state = LOOK_FOR_VCARD
+
+        # Get lines looking for start of calendar
+        lines = [None, None]
+
+        while readFoldedLine(ins, lines):
+
+            line = lines[0]
+            if state == LOOK_FOR_VCARD:
+
+                # Look for start
+                if line == self.getBeginDelimiter():
+                    # Next state
+                    state = GET_PROPERTY
+
+                    # Indicate success at this point
+                    result = True
+
+                # Handle blank line
+                elif len(line) == 0:
+                    # Raise if requested, otherwise just ignore
+                    if ParserContext.BLANK_LINES_IN_DATA == ParserContext.PARSER_RAISE:
+                        raise PyCalendarInvalidData("vCard data has blank lines")
+
+                # Unrecognized data
+                else:
+                    raise PyCalendarInvalidData("vCard data not recognized", line)
+
+            elif state == GET_PROPERTY:
+
+                # Look for end of object
+                if line == self.getEndDelimiter():
+
+                    # Finalise the current calendar
+                    self.finalise()
+
+                    # Change state
+                    state = LOOK_FOR_VCARD
+
+                # Blank line
+                elif len(line) == 0:
+                    # Raise if requested, otherwise just ignore
+                    if ParserContext.BLANK_LINES_IN_DATA == ParserContext.PARSER_RAISE:
+                        raise PyCalendarInvalidData("vCard data has blank lines")
+
+                # Must be a property
+                else:
+
+                    # Parse attribute/value for top-level calendar item
+                    prop = Property()
+                    try:
+                        if prop.parse(line):
+
+                            # Check for valid property
+                            if not self.validProperty(prop):
+                                raise PyCalendarInvalidData("Invalid property", str(prop))
+                            else:
+                                self.addProperty(prop)
+                    except IndexError:
+                        print line
+
+        # Check for truncated data
+        if state != LOOK_FOR_VCARD:
+            raise PyCalendarInvalidData("vCard data not complete", "")
+
+        # Validate some things
+        if result and not self.hasProperty("VERSION"):
+            raise PyCalendarInvalidData("vCard missing VERSION", "")
+
+        return result
+
+
+    def addDefaultProperties(self):
+        self.addProperty(Property(definitions.Property_PRODID, Card.sProdID))
+        self.addProperty(Property(definitions.Property_VERSION, "3.0"))
+
+
+    def validProperty(self, prop):
+        if prop.getName() == definitions.Property_VERSION:
+
+            tvalue = prop.getValue()
+            if ((tvalue is None) or (tvalue.getValue() != "3.0")):
+                return False
+
+        return True

Deleted: PyCalendar/trunk/src/pycalendar/vcard/definitions.py
===================================================================
--- PyCalendar/branches/server/src/pycalendar/vcard/definitions.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/vcard/definitions.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,88 +0,0 @@
-##
-#    Copyright (c) 2007-2012 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.
-##
-
-#     2426 Component
-
-VCARD = "VCARD"
-
-#     2425 Properties
-Property_SOURCE = "SOURCE"
-Property_NAME = "NAME"
-Property_PROFILE = "PROFILE"
-
-#     2426 vCard Properties
-
-#     2426 Section 3.1
-Property_FN = "FN"
-Property_N = "N"
-Property_NICKNAME = "NICKNAME"
-Property_PHOTO = "PHOTO"
-Property_BDAY = "BDAY"
-
-#     2426 Section 3.2
-Property_ADR = "ADR"
-Property_LABEL = "LABEL"
-
-#     2426 Section 3.3
-Property_TEL = "TEL"
-Property_EMAIL = "EMAIL"
-Property_MAILER = "MAILER"
-
-#     2426 Section 3.4
-Property_TZ = "TZ"
-Property_GEO = "GEO"
-
-#     2426 Section 3.5
-Property_TITLE = "TITLE"
-Property_ROLE = "ROLE"
-Property_LOGO = "LOGO"
-Property_AGENT = "AGENT"
-Property_ORG = "ORG"
-
-#     2426 Section 3.6
-Property_CATEGORIES = "CATEGORIES"
-Property_NOTE = "NOTE"
-Property_PRODID = "PRODID"
-Property_REV = "REV"
-Property_SORT_STRING = "SORT-STRING"
-Property_SOUND = "SOUND"
-Property_UID = "UID"
-Property_URL = "URL"
-Property_VERSION = "VERSION"
-
-#     2426 Section 3.7
-Property_CLASS = "CLASS"
-Property_KEY = "KEY"
-
-#     2426 Value Types
-Value_BINARY = "BINARY"
-Value_BOOLEAN = "BOOLEAN"
-Value_DATE = "DATE"
-Value_DATE_TIME = "DATE-TIME"
-Value_FLOAT = "FLOAT"
-Value_INTEGER = "INTEGER"
-Value_TEXT = "TEXT"
-Value_TIME = "TIME"
-Value_URI = "URI"
-Value_UTCOFFSET = "UTCOFFSET"
-Value_VCARD = "VCARD"
-
-Parameter_ENCODING = "ENCODING"
-Parameter_LANGUAGE = "LANGUAGE"
-Parameter_TYPE = "TYPE"
-Parameter_VALUE = "VALUE"
-
-Parameter_Value_ENCODING_B = "B"

Copied: PyCalendar/trunk/src/pycalendar/vcard/definitions.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/vcard/definitions.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/vcard/definitions.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/vcard/definitions.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,88 @@
+##
+#    Copyright (c) 2007-2012 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.
+##
+
+#     2426 Component
+
+VCARD = "VCARD"
+
+#     2425 Properties
+Property_SOURCE = "SOURCE"
+Property_NAME = "NAME"
+Property_PROFILE = "PROFILE"
+
+#     2426 vCard Properties
+
+#     2426 Section 3.1
+Property_FN = "FN"
+Property_N = "N"
+Property_NICKNAME = "NICKNAME"
+Property_PHOTO = "PHOTO"
+Property_BDAY = "BDAY"
+
+#     2426 Section 3.2
+Property_ADR = "ADR"
+Property_LABEL = "LABEL"
+
+#     2426 Section 3.3
+Property_TEL = "TEL"
+Property_EMAIL = "EMAIL"
+Property_MAILER = "MAILER"
+
+#     2426 Section 3.4
+Property_TZ = "TZ"
+Property_GEO = "GEO"
+
+#     2426 Section 3.5
+Property_TITLE = "TITLE"
+Property_ROLE = "ROLE"
+Property_LOGO = "LOGO"
+Property_AGENT = "AGENT"
+Property_ORG = "ORG"
+
+#     2426 Section 3.6
+Property_CATEGORIES = "CATEGORIES"
+Property_NOTE = "NOTE"
+Property_PRODID = "PRODID"
+Property_REV = "REV"
+Property_SORT_STRING = "SORT-STRING"
+Property_SOUND = "SOUND"
+Property_UID = "UID"
+Property_URL = "URL"
+Property_VERSION = "VERSION"
+
+#     2426 Section 3.7
+Property_CLASS = "CLASS"
+Property_KEY = "KEY"
+
+#     2426 Value Types
+Value_BINARY = "BINARY"
+Value_BOOLEAN = "BOOLEAN"
+Value_DATE = "DATE"
+Value_DATE_TIME = "DATE-TIME"
+Value_FLOAT = "FLOAT"
+Value_INTEGER = "INTEGER"
+Value_TEXT = "TEXT"
+Value_TIME = "TIME"
+Value_URI = "URI"
+Value_UTCOFFSET = "UTCOFFSET"
+Value_VCARD = "VCARD"
+
+Parameter_ENCODING = "ENCODING"
+Parameter_LANGUAGE = "LANGUAGE"
+Parameter_TYPE = "TYPE"
+Parameter_VALUE = "VALUE"
+
+Parameter_Value_ENCODING_B = "B"

Deleted: PyCalendar/trunk/src/pycalendar/vcard/property.py
===================================================================
--- PyCalendar/branches/server/src/pycalendar/vcard/property.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/vcard/property.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,573 +0,0 @@
-##
-#    Copyright (c) 2007-2012 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 import stringutils
-from pycalendar.adr import Adr
-from pycalendar.adrvalue import AdrValue
-from pycalendar.attribute import PyCalendarAttribute
-from pycalendar.datetime import PyCalendarDateTime
-from pycalendar.datetimevalue import PyCalendarDateTimeValue
-from pycalendar.exceptions import PyCalendarInvalidProperty
-from pycalendar.integervalue import PyCalendarIntegerValue
-from pycalendar.multivalue import PyCalendarMultiValue
-from pycalendar.n import N
-from pycalendar.nvalue import NValue
-from pycalendar.orgvalue import OrgValue
-from pycalendar.parser import ParserContext
-from pycalendar.plaintextvalue import PyCalendarPlainTextValue
-from pycalendar.unknownvalue import PyCalendarUnknownValue
-from pycalendar.utcoffsetvalue import PyCalendarUTCOffsetValue
-from pycalendar.utils import decodeParameterValue
-from pycalendar.value import PyCalendarValue
-from pycalendar.vcard import definitions
-import cStringIO as StringIO
-
-handleOptions = ("allow", "ignore", "fix", "raise")
-missingParameterValues = "fix"
-
-class Property(object):
-
-    sDefaultValueTypeMap = {
-
-        #     2425 Properties
-        definitions.Property_SOURCE  : PyCalendarValue.VALUETYPE_URI,
-        definitions.Property_NAME    : PyCalendarValue.VALUETYPE_TEXT,
-        definitions.Property_PROFILE : PyCalendarValue.VALUETYPE_TEXT,
-
-        #     2426 vCard Properties
-
-        #     2426 Section 3.1
-        definitions.Property_FN       : PyCalendarValue.VALUETYPE_TEXT,
-        definitions.Property_N        : PyCalendarValue.VALUETYPE_N,
-        definitions.Property_NICKNAME : PyCalendarValue.VALUETYPE_TEXT,
-        definitions.Property_PHOTO    : PyCalendarValue.VALUETYPE_BINARY,
-        definitions.Property_BDAY     : PyCalendarValue.VALUETYPE_DATE,
-
-        #     2426 Section 3.2
-        definitions.Property_ADR   : PyCalendarValue.VALUETYPE_ADR,
-        definitions.Property_LABEL : PyCalendarValue.VALUETYPE_TEXT,
-
-        #     2426 Section 3.3
-        definitions.Property_TEL    : PyCalendarValue.VALUETYPE_TEXT,
-        definitions.Property_EMAIL  : PyCalendarValue.VALUETYPE_TEXT,
-        definitions.Property_MAILER : PyCalendarValue.VALUETYPE_TEXT,
-
-        #     2426 Section 3.4
-        definitions.Property_TZ  : PyCalendarValue.VALUETYPE_UTC_OFFSET,
-        definitions.Property_GEO : PyCalendarValue.VALUETYPE_GEO,
-
-        #     2426 Section 3.5
-        definitions.Property_TITLE : PyCalendarValue.VALUETYPE_TEXT,
-        definitions.Property_ROLE  : PyCalendarValue.VALUETYPE_TEXT,
-        definitions.Property_LOGO  : PyCalendarValue.VALUETYPE_BINARY,
-        definitions.Property_AGENT : PyCalendarValue.VALUETYPE_VCARD,
-        definitions.Property_ORG   : PyCalendarValue.VALUETYPE_ORG,
-
-        #     2426 Section 3.6
-        definitions.Property_CATEGORIES  : PyCalendarValue.VALUETYPE_TEXT,
-        definitions.Property_NOTE        : PyCalendarValue.VALUETYPE_TEXT,
-        definitions.Property_PRODID      : PyCalendarValue.VALUETYPE_TEXT,
-        definitions.Property_REV         : PyCalendarValue.VALUETYPE_DATETIME,
-        definitions.Property_SORT_STRING : PyCalendarValue.VALUETYPE_TEXT,
-        definitions.Property_SOUND       : PyCalendarValue.VALUETYPE_BINARY,
-        definitions.Property_UID         : PyCalendarValue.VALUETYPE_TEXT,
-        definitions.Property_URL         : PyCalendarValue.VALUETYPE_URI,
-        definitions.Property_VERSION     : PyCalendarValue.VALUETYPE_TEXT,
-
-        #     2426 Section 3.7
-        definitions.Property_CLASS       : PyCalendarValue.VALUETYPE_TEXT,
-        definitions.Property_KEY         : PyCalendarValue.VALUETYPE_BINARY,
-    }
-
-    sValueTypeMap = {
-        definitions.Value_BINARY      : PyCalendarValue.VALUETYPE_BINARY,
-        definitions.Value_BOOLEAN     : PyCalendarValue.VALUETYPE_BOOLEAN,
-        definitions.Value_DATE        : PyCalendarValue.VALUETYPE_DATE,
-        definitions.Value_DATE_TIME   : PyCalendarValue.VALUETYPE_DATETIME,
-        definitions.Value_FLOAT       : PyCalendarValue.VALUETYPE_FLOAT,
-        definitions.Value_INTEGER     : PyCalendarValue.VALUETYPE_INTEGER,
-        definitions.Value_TEXT        : PyCalendarValue.VALUETYPE_TEXT,
-        definitions.Value_TIME        : PyCalendarValue.VALUETYPE_TIME,
-        definitions.Value_URI         : PyCalendarValue.VALUETYPE_URI,
-        definitions.Value_UTCOFFSET   : PyCalendarValue.VALUETYPE_UTC_OFFSET,
-        definitions.Value_VCARD       : PyCalendarValue.VALUETYPE_VCARD,
-    }
-
-    sTypeValueMap = {
-        PyCalendarValue.VALUETYPE_ADR        : definitions.Value_TEXT,
-        PyCalendarValue.VALUETYPE_BINARY     : definitions.Value_BINARY,
-        PyCalendarValue.VALUETYPE_BOOLEAN    : definitions.Value_BOOLEAN,
-        PyCalendarValue.VALUETYPE_DATE       : definitions.Value_DATE,
-        PyCalendarValue.VALUETYPE_DATETIME   : definitions.Value_DATE_TIME,
-        PyCalendarValue.VALUETYPE_FLOAT      : definitions.Value_FLOAT,
-        PyCalendarValue.VALUETYPE_GEO        : definitions.Value_FLOAT,
-        PyCalendarValue.VALUETYPE_INTEGER    : definitions.Value_INTEGER,
-        PyCalendarValue.VALUETYPE_N          : definitions.Value_TEXT,
-        PyCalendarValue.VALUETYPE_ORG        : definitions.Value_TEXT,
-        PyCalendarValue.VALUETYPE_TEXT       : definitions.Value_TEXT,
-        PyCalendarValue.VALUETYPE_TIME       : definitions.Value_TIME,
-        PyCalendarValue.VALUETYPE_URI        : definitions.Value_URI,
-        PyCalendarValue.VALUETYPE_UTC_OFFSET : definitions.Value_UTCOFFSET,
-        PyCalendarValue.VALUETYPE_VCARD      : definitions.Value_VCARD,
-    }
-
-    sMultiValues = set((
-        definitions.Property_NICKNAME,
-        definitions.Property_CATEGORIES,
-    ))
-
-    sTextVariants = set((
-        definitions.Property_ADR,
-        definitions.Property_N,
-        definitions.Property_ORG,
-    ))
-
-    def __init__(self, group=None, name=None, value=None, valuetype=None):
-        self._init_PyCalendarProperty()
-
-        self.mGroup = group
-
-        self.mName = name if name is not None else ""
-
-        if isinstance(value, int):
-            self._init_attr_value_int(value)
-
-        elif isinstance(value, str):
-            self._init_attr_value_text(value, valuetype if valuetype else Property.sDefaultValueTypeMap.get(self.mName.upper(), PyCalendarValue.VALUETYPE_TEXT))
-
-        elif isinstance(value, PyCalendarDateTime):
-            self._init_attr_value_datetime(value)
-
-        elif isinstance(value, Adr):
-            self._init_attr_value_adr(value)
-
-        elif isinstance(value, N):
-            self._init_attr_value_n(value)
-
-        elif isinstance(value, list) or isinstance(value, tuple):
-            if name.upper() == definitions.Property_ORG:
-                self._init_attr_value_org(value)
-            elif name.upper() == definitions.Property_GEO:
-                self._init_attr_value_geo(value)
-            else:
-                # Assume everything else is a text list
-                self._init_attr_value_text_list(value)
-
-        elif isinstance(value, PyCalendarUTCOffsetValue):
-            self._init_attr_value_utcoffset(value)
-
-
-    def duplicate(self):
-        other = Property(self.mGroup, self.mName)
-        for attrname, attrs in self.mAttributes.items():
-            other.mAttributes[attrname] = [i.duplicate() for i in attrs]
-        other.mValue = self.mValue.duplicate()
-
-        return other
-
-
-    def __hash__(self):
-        return hash((
-            self.mName,
-            tuple([tuple(self.mAttributes[attrname]) for attrname in sorted(self.mAttributes.keys())]),
-            self.mValue,
-        ))
-
-
-    def __ne__(self, other):
-        return not self.__eq__(other)
-
-
-    def __eq__(self, other):
-        if not isinstance(other, Property):
-            return False
-        return (
-            self.mGroup == self.mGroup and
-            self.mName == other.mName and
-            self.mValue == other.mValue and
-            self.mAttributes == other.mAttributes
-        )
-
-
-    def __repr__(self):
-        return "vCard Property: %s" % (self.getText(),)
-
-
-    def __str__(self):
-        return self.getText()
-
-
-    def getGroup(self):
-        return self.mGroup
-
-
-    def setGroup(self, group):
-        self.mGroup = group
-
-
-    def getName(self):
-        return self.mName
-
-
-    def setName(self, name):
-        self.mName = name
-
-
-    def getAttributes(self):
-        return self.mAttributes
-
-
-    def setAttributes(self, attributes):
-        self.mAttributes = dict([(k.upper(), v) for k, v in attributes.iteritems()])
-
-
-    def hasAttribute(self, attr):
-        return attr.upper() in self.mAttributes
-
-
-    def getAttributeValue(self, attr):
-        return self.mAttributes[attr.upper()][0].getFirstValue()
-
-
-    def addAttribute(self, attr):
-        self.mAttributes.setdefault(attr.getName().upper(), []).append(attr)
-
-
-    def replaceAttribute(self, attr):
-        self.mAttributes[attr.getName().upper()] = [attr]
-
-
-    def removeAttributes(self, attr):
-        if attr.upper() in self.mAttributes:
-            del self.mAttributes[attr.upper()]
-
-
-    def getValue(self):
-        return self.mValue
-
-
-    def parse(self, data):
-        # Look for attribute or value delimiter
-        prop_name, txt = stringutils.strduptokenstr(data, ";:")
-        if not prop_name:
-            raise PyCalendarInvalidProperty("Invalid property", data)
-
-        # Check for group prefix
-        splits = prop_name.split(".", 1)
-        if len(splits) == 2:
-            # We have both group and name
-            self.mGroup = splits[0]
-            self.mName = splits[1]
-        else:
-            # We have the name
-            self.mName = prop_name
-
-        # Now loop getting data
-        try:
-            stripValueSpaces = False    # Fix for AB.app base PHOTO properties that use two spaces at start of line
-            while txt:
-                if txt[0] == ';':
-                    # Parse attribute
-
-                    # Move past delimiter
-                    txt = txt[1:]
-
-                    # Get quoted string or token - in iCalendar we only look for "=" here
-                    # but for "broken" vCard BASE64 property we need to also terminate on
-                    # ":;"
-                    attribute_name, txt = stringutils.strduptokenstr(txt, "=:;")
-                    if attribute_name is None:
-                        raise PyCalendarInvalidProperty("Invalid property", data)
-
-                    if txt[0] != "=":
-                        # Deal with parameters without values
-                        if ParserContext.VCARD_2_NO_PARAMETER_VALUES == ParserContext.PARSER_RAISE:
-                            raise PyCalendarInvalidProperty("Invalid property parameter", data)
-                        elif ParserContext.VCARD_2_NO_PARAMETER_VALUES == ParserContext.PARSER_ALLOW:
-                            attribute_value = None
-                        else: # PARSER_IGNORE and PARSER_FIX
-                            attribute_name = None
-
-                        if attribute_name.upper() == "BASE64" and ParserContext.VCARD_2_BASE64 == ParserContext.PARSER_FIX:
-                            attribute_name = definitions.Parameter_ENCODING
-                            attribute_value = definitions.Parameter_Value_ENCODING_B
-                            stripValueSpaces = True
-                    else:
-                        txt = txt[1:]
-                        attribute_value, txt = stringutils.strduptokenstr(txt, ":;,")
-                        if attribute_value is None:
-                            raise PyCalendarInvalidProperty("Invalid property", data)
-
-                    # Now add attribute value (decode ^-escaping)
-                    if attribute_name is not None:
-                        attrvalue = PyCalendarAttribute(name=attribute_name, value=decodeParameterValue(attribute_value))
-                        self.mAttributes.setdefault(attribute_name.upper(), []).append(attrvalue)
-
-                    # Look for additional values
-                    while txt[0] == ',':
-                        txt = txt[1:]
-                        attribute_value2, txt = stringutils.strduptokenstr(txt, ":;,")
-                        if attribute_value2 is None:
-                            raise PyCalendarInvalidProperty("Invalid property", data)
-                        attrvalue.addValue(decodeParameterValue(attribute_value2))
-                elif txt[0] == ':':
-                    txt = txt[1:]
-                    if stripValueSpaces:
-                        txt = txt.replace(" ", "")
-                    self.createValue(txt)
-                    txt = None
-
-        except IndexError:
-            raise PyCalendarInvalidProperty("Invalid property", data)
-
-        # We must have a value of some kind
-        if self.mValue is None:
-            raise PyCalendarInvalidProperty("Invalid property", data)
-
-        return True
-
-
-    def getText(self):
-        os = StringIO.StringIO()
-        self.generate(os)
-        return os.getvalue()
-
-
-    def generate(self, os):
-
-        # Write it out always with value
-        self.generateValue(os, False)
-
-
-    def generateFiltered(self, os, filter):
-
-        # Check for property in filter and whether value is written out
-        test, novalue = filter.testPropertyValue(self.mName.upper())
-        if test:
-            self.generateValue(os, novalue)
-
-
-    # Write out the actual property, possibly skipping the value
-    def generateValue(self, os, novalue):
-
-        self.setupValueAttribute()
-
-        # Must write to temp buffer and then wrap
-        sout = StringIO.StringIO()
-        if self.mGroup:
-            sout.write(self.mGroup + ".")
-        sout.write(self.mName)
-
-        # Write all attributes
-        for key in sorted(self.mAttributes.keys()):
-            for attr in self.mAttributes[key]:
-                sout.write(";")
-                attr.generate(sout)
-
-        # Write value
-        sout.write(":")
-        if self.mName.upper() == "PHOTO" and self.mValue.getType() == PyCalendarValue.VALUETYPE_BINARY:
-            # Handle AB.app PHOTO values
-            sout.write("\r\n")
-
-            value = self.mValue.getText()
-            value_len = len(value)
-            offset = 0
-            while(value_len > 72):
-                sout.write(" ")
-                sout.write(value[offset:offset + 72])
-                sout.write("\r\n")
-                value_len -= 72
-                offset += 72
-            sout.write(" ")
-            sout.write(value[offset:])
-            os.write(sout.getvalue())
-        else:
-            if self.mValue and not novalue:
-                self.mValue.generate(sout)
-
-            # Get string text
-            temp = sout.getvalue()
-            sout.close()
-
-            # Look for line length exceed
-            if len(temp) < 75:
-                os.write(temp)
-            else:
-                # Look for valid utf8 range and write that out
-                start = 0
-                written = 0
-                lineWrap = 74
-                while written < len(temp):
-                    # Start 74 chars on from where we are
-                    offset = start + lineWrap
-                    if offset >= len(temp):
-                        line = temp[start:]
-                        os.write(line)
-                        written = len(temp)
-                    else:
-                        # Check whether next char is valid utf8 lead byte
-                        while (temp[offset] > 0x7F) and ((ord(temp[offset]) & 0xC0) == 0x80):
-                            # Step back until we have a valid char
-                            offset -= 1
-
-                        line = temp[start:offset]
-                        os.write(line)
-                        os.write("\r\n ")
-                        lineWrap = 73   # We are now adding a space at the start
-                        written += offset - start
-                        start = offset
-
-        os.write("\r\n")
-
-
-    def _init_PyCalendarProperty(self):
-        self.mGroup = None
-        self.mName = ""
-        self.mAttributes = {}
-        self.mValue = None
-
-
-    def createValue(self, data):
-        # Tidy first
-        self.mValue = None
-
-        # Get value type from property name
-        valueType = Property.sDefaultValueTypeMap.get(self.mName.upper(), PyCalendarValue.VALUETYPE_TEXT)
-
-        # Check whether custom value is set
-        if definitions.Parameter_VALUE in self.mAttributes:
-            attr = self.getAttributeValue(definitions.Parameter_VALUE)
-            if attr != definitions.Value_TEXT or self.mName.upper() not in Property.sTextVariants:
-                valueType = Property.sValueTypeMap.get(attr, valueType)
-
-        # Check for multivalued
-        if self.mName.upper() in Property.sMultiValues:
-            self.mValue = PyCalendarMultiValue(valueType)
-        else:
-            # Create the type
-            self.mValue = PyCalendarValue.createFromType(valueType)
-
-        # Now parse the data
-        try:
-            if valueType in (PyCalendarValue.VALUETYPE_DATE, PyCalendarValue.VALUETYPE_DATETIME):
-                # vCard supports a slightly different, expanded form, of date
-                self.mValue.parse(data, fullISO=True)
-            else:
-                self.mValue.parse(data)
-        except ValueError:
-            raise PyCalendarInvalidProperty("Invalid property value", data)
-
-
-    def setValue(self, value):
-        # Tidy first
-        self.mValue = None
-
-        # Get value type from property name
-        valueType = Property.sDefaultValueTypeMap.get(self.mName.upper(), PyCalendarUnknownValue)
-
-        # Check whether custom value is set
-        if definitions.Parameter_VALUE in self.mAttributes:
-            attr = self.getAttributeValue(definitions.Parameter_VALUE)
-            if attr != definitions.Value_TEXT or self.mName.upper() not in Property.sTextVariants:
-                valueType = Property.sValueTypeMap.get(attr, valueType)
-
-        # Check for multivalued
-        if self.mName.upper() in Property.sMultiValues:
-            self.mValue = PyCalendarMultiValue(valueType)
-        else:
-            # Create the type
-            self.mValue = PyCalendarValue.createFromType(valueType)
-
-        self.mValue.setValue(value)
-
-
-    def setupValueAttribute(self):
-        if definitions.Parameter_VALUE in self.mAttributes:
-            del self.mAttributes[definitions.Parameter_VALUE]
-
-        # Only if we have a value right now
-        if self.mValue is None:
-            return
-
-        # See if current type is default for this property. If there is no mapping available,
-        # then always add VALUE if it is not TEXT.
-        default_type = Property.sDefaultValueTypeMap.get(self.mName.upper())
-        actual_type = self.mValue.getType()
-        if default_type is None or default_type != actual_type:
-            actual_value = self.sTypeValueMap.get(actual_type)
-            if actual_value is not None and (default_type is not None or actual_type != PyCalendarValue.VALUETYPE_TEXT):
-                self.mAttributes.setdefault(definitions.Parameter_VALUE, []).append(PyCalendarAttribute(name=definitions.Parameter_VALUE, value=actual_value))
-
-
-    # Creation
-    def _init_attr_value_int(self, ival):
-        # Value
-        self.mValue = PyCalendarIntegerValue(value=ival)
-
-        # Attributes
-        self.setupValueAttribute()
-
-
-    def _init_attr_value_text(self, txt, value_type):
-        # Value
-        self.mValue = PyCalendarValue.createFromType(value_type)
-        if isinstance(self.mValue, PyCalendarPlainTextValue) or isinstance(self.mValue, PyCalendarUnknownValue):
-            self.mValue.setValue(txt)
-
-        # Attributes
-        self.setupValueAttribute()
-
-
-    def _init_attr_value_adr(self, reqstatus):
-        # Value
-        self.mValue = AdrValue(reqstatus)
-
-        # Attributes
-        self.setupValueAttribute()
-
-
-    def _init_attr_value_n(self, reqstatus):
-        # Value
-        self.mValue = NValue(reqstatus)
-
-        # Attributes
-        self.setupValueAttribute()
-
-
-    def _init_attr_value_org(self, reqstatus):
-        # Value
-        self.mValue = OrgValue(reqstatus)
-
-        # Attributes
-        self.setupValueAttribute()
-
-
-    def _init_attr_value_datetime(self, dt):
-        # Value
-        self.mValue = PyCalendarDateTimeValue(value=dt)
-
-        # Attributes
-        self.setupValueAttribute()
-
-
-    def _init_attr_value_utcoffset(self, utcoffset):
-        # Value
-        self.mValue = PyCalendarUTCOffsetValue()
-        self.mValue.setValue(utcoffset.getValue())
-
-        # Attributes
-        self.setupValueAttribute()

Copied: PyCalendar/trunk/src/pycalendar/vcard/property.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/vcard/property.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/vcard/property.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/vcard/property.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,573 @@
+##
+#    Copyright (c) 2007-2012 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 import stringutils
+from pycalendar.adr import Adr
+from pycalendar.adrvalue import AdrValue
+from pycalendar.attribute import PyCalendarAttribute
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.datetimevalue import PyCalendarDateTimeValue
+from pycalendar.exceptions import PyCalendarInvalidProperty
+from pycalendar.integervalue import PyCalendarIntegerValue
+from pycalendar.multivalue import PyCalendarMultiValue
+from pycalendar.n import N
+from pycalendar.nvalue import NValue
+from pycalendar.orgvalue import OrgValue
+from pycalendar.parser import ParserContext
+from pycalendar.plaintextvalue import PyCalendarPlainTextValue
+from pycalendar.unknownvalue import PyCalendarUnknownValue
+from pycalendar.utcoffsetvalue import PyCalendarUTCOffsetValue
+from pycalendar.utils import decodeParameterValue
+from pycalendar.value import PyCalendarValue
+from pycalendar.vcard import definitions
+import cStringIO as StringIO
+
+handleOptions = ("allow", "ignore", "fix", "raise")
+missingParameterValues = "fix"
+
+class Property(object):
+
+    sDefaultValueTypeMap = {
+
+        #     2425 Properties
+        definitions.Property_SOURCE  : PyCalendarValue.VALUETYPE_URI,
+        definitions.Property_NAME    : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.Property_PROFILE : PyCalendarValue.VALUETYPE_TEXT,
+
+        #     2426 vCard Properties
+
+        #     2426 Section 3.1
+        definitions.Property_FN       : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.Property_N        : PyCalendarValue.VALUETYPE_N,
+        definitions.Property_NICKNAME : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.Property_PHOTO    : PyCalendarValue.VALUETYPE_BINARY,
+        definitions.Property_BDAY     : PyCalendarValue.VALUETYPE_DATE,
+
+        #     2426 Section 3.2
+        definitions.Property_ADR   : PyCalendarValue.VALUETYPE_ADR,
+        definitions.Property_LABEL : PyCalendarValue.VALUETYPE_TEXT,
+
+        #     2426 Section 3.3
+        definitions.Property_TEL    : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.Property_EMAIL  : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.Property_MAILER : PyCalendarValue.VALUETYPE_TEXT,
+
+        #     2426 Section 3.4
+        definitions.Property_TZ  : PyCalendarValue.VALUETYPE_UTC_OFFSET,
+        definitions.Property_GEO : PyCalendarValue.VALUETYPE_GEO,
+
+        #     2426 Section 3.5
+        definitions.Property_TITLE : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.Property_ROLE  : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.Property_LOGO  : PyCalendarValue.VALUETYPE_BINARY,
+        definitions.Property_AGENT : PyCalendarValue.VALUETYPE_VCARD,
+        definitions.Property_ORG   : PyCalendarValue.VALUETYPE_ORG,
+
+        #     2426 Section 3.6
+        definitions.Property_CATEGORIES  : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.Property_NOTE        : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.Property_PRODID      : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.Property_REV         : PyCalendarValue.VALUETYPE_DATETIME,
+        definitions.Property_SORT_STRING : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.Property_SOUND       : PyCalendarValue.VALUETYPE_BINARY,
+        definitions.Property_UID         : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.Property_URL         : PyCalendarValue.VALUETYPE_URI,
+        definitions.Property_VERSION     : PyCalendarValue.VALUETYPE_TEXT,
+
+        #     2426 Section 3.7
+        definitions.Property_CLASS       : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.Property_KEY         : PyCalendarValue.VALUETYPE_BINARY,
+    }
+
+    sValueTypeMap = {
+        definitions.Value_BINARY      : PyCalendarValue.VALUETYPE_BINARY,
+        definitions.Value_BOOLEAN     : PyCalendarValue.VALUETYPE_BOOLEAN,
+        definitions.Value_DATE        : PyCalendarValue.VALUETYPE_DATE,
+        definitions.Value_DATE_TIME   : PyCalendarValue.VALUETYPE_DATETIME,
+        definitions.Value_FLOAT       : PyCalendarValue.VALUETYPE_FLOAT,
+        definitions.Value_INTEGER     : PyCalendarValue.VALUETYPE_INTEGER,
+        definitions.Value_TEXT        : PyCalendarValue.VALUETYPE_TEXT,
+        definitions.Value_TIME        : PyCalendarValue.VALUETYPE_TIME,
+        definitions.Value_URI         : PyCalendarValue.VALUETYPE_URI,
+        definitions.Value_UTCOFFSET   : PyCalendarValue.VALUETYPE_UTC_OFFSET,
+        definitions.Value_VCARD       : PyCalendarValue.VALUETYPE_VCARD,
+    }
+
+    sTypeValueMap = {
+        PyCalendarValue.VALUETYPE_ADR        : definitions.Value_TEXT,
+        PyCalendarValue.VALUETYPE_BINARY     : definitions.Value_BINARY,
+        PyCalendarValue.VALUETYPE_BOOLEAN    : definitions.Value_BOOLEAN,
+        PyCalendarValue.VALUETYPE_DATE       : definitions.Value_DATE,
+        PyCalendarValue.VALUETYPE_DATETIME   : definitions.Value_DATE_TIME,
+        PyCalendarValue.VALUETYPE_FLOAT      : definitions.Value_FLOAT,
+        PyCalendarValue.VALUETYPE_GEO        : definitions.Value_FLOAT,
+        PyCalendarValue.VALUETYPE_INTEGER    : definitions.Value_INTEGER,
+        PyCalendarValue.VALUETYPE_N          : definitions.Value_TEXT,
+        PyCalendarValue.VALUETYPE_ORG        : definitions.Value_TEXT,
+        PyCalendarValue.VALUETYPE_TEXT       : definitions.Value_TEXT,
+        PyCalendarValue.VALUETYPE_TIME       : definitions.Value_TIME,
+        PyCalendarValue.VALUETYPE_URI        : definitions.Value_URI,
+        PyCalendarValue.VALUETYPE_UTC_OFFSET : definitions.Value_UTCOFFSET,
+        PyCalendarValue.VALUETYPE_VCARD      : definitions.Value_VCARD,
+    }
+
+    sMultiValues = set((
+        definitions.Property_NICKNAME,
+        definitions.Property_CATEGORIES,
+    ))
+
+    sTextVariants = set((
+        definitions.Property_ADR,
+        definitions.Property_N,
+        definitions.Property_ORG,
+    ))
+
+    def __init__(self, group=None, name=None, value=None, valuetype=None):
+        self._init_PyCalendarProperty()
+
+        self.mGroup = group
+
+        self.mName = name if name is not None else ""
+
+        if isinstance(value, int):
+            self._init_attr_value_int(value)
+
+        elif isinstance(value, str):
+            self._init_attr_value_text(value, valuetype if valuetype else Property.sDefaultValueTypeMap.get(self.mName.upper(), PyCalendarValue.VALUETYPE_TEXT))
+
+        elif isinstance(value, PyCalendarDateTime):
+            self._init_attr_value_datetime(value)
+
+        elif isinstance(value, Adr):
+            self._init_attr_value_adr(value)
+
+        elif isinstance(value, N):
+            self._init_attr_value_n(value)
+
+        elif isinstance(value, list) or isinstance(value, tuple):
+            if name.upper() == definitions.Property_ORG:
+                self._init_attr_value_org(value)
+            elif name.upper() == definitions.Property_GEO:
+                self._init_attr_value_geo(value)
+            else:
+                # Assume everything else is a text list
+                self._init_attr_value_text_list(value)
+
+        elif isinstance(value, PyCalendarUTCOffsetValue):
+            self._init_attr_value_utcoffset(value)
+
+
+    def duplicate(self):
+        other = Property(self.mGroup, self.mName)
+        for attrname, attrs in self.mAttributes.items():
+            other.mAttributes[attrname] = [i.duplicate() for i in attrs]
+        other.mValue = self.mValue.duplicate()
+
+        return other
+
+
+    def __hash__(self):
+        return hash((
+            self.mName,
+            tuple([tuple(self.mAttributes[attrname]) for attrname in sorted(self.mAttributes.keys())]),
+            self.mValue,
+        ))
+
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+
+    def __eq__(self, other):
+        if not isinstance(other, Property):
+            return False
+        return (
+            self.mGroup == self.mGroup and
+            self.mName == other.mName and
+            self.mValue == other.mValue and
+            self.mAttributes == other.mAttributes
+        )
+
+
+    def __repr__(self):
+        return "vCard Property: %s" % (self.getText(),)
+
+
+    def __str__(self):
+        return self.getText()
+
+
+    def getGroup(self):
+        return self.mGroup
+
+
+    def setGroup(self, group):
+        self.mGroup = group
+
+
+    def getName(self):
+        return self.mName
+
+
+    def setName(self, name):
+        self.mName = name
+
+
+    def getAttributes(self):
+        return self.mAttributes
+
+
+    def setAttributes(self, attributes):
+        self.mAttributes = dict([(k.upper(), v) for k, v in attributes.iteritems()])
+
+
+    def hasAttribute(self, attr):
+        return attr.upper() in self.mAttributes
+
+
+    def getAttributeValue(self, attr):
+        return self.mAttributes[attr.upper()][0].getFirstValue()
+
+
+    def addAttribute(self, attr):
+        self.mAttributes.setdefault(attr.getName().upper(), []).append(attr)
+
+
+    def replaceAttribute(self, attr):
+        self.mAttributes[attr.getName().upper()] = [attr]
+
+
+    def removeAttributes(self, attr):
+        if attr.upper() in self.mAttributes:
+            del self.mAttributes[attr.upper()]
+
+
+    def getValue(self):
+        return self.mValue
+
+
+    def parse(self, data):
+        # Look for attribute or value delimiter
+        prop_name, txt = stringutils.strduptokenstr(data, ";:")
+        if not prop_name:
+            raise PyCalendarInvalidProperty("Invalid property", data)
+
+        # Check for group prefix
+        splits = prop_name.split(".", 1)
+        if len(splits) == 2:
+            # We have both group and name
+            self.mGroup = splits[0]
+            self.mName = splits[1]
+        else:
+            # We have the name
+            self.mName = prop_name
+
+        # Now loop getting data
+        try:
+            stripValueSpaces = False    # Fix for AB.app base PHOTO properties that use two spaces at start of line
+            while txt:
+                if txt[0] == ';':
+                    # Parse attribute
+
+                    # Move past delimiter
+                    txt = txt[1:]
+
+                    # Get quoted string or token - in iCalendar we only look for "=" here
+                    # but for "broken" vCard BASE64 property we need to also terminate on
+                    # ":;"
+                    attribute_name, txt = stringutils.strduptokenstr(txt, "=:;")
+                    if attribute_name is None:
+                        raise PyCalendarInvalidProperty("Invalid property", data)
+
+                    if txt[0] != "=":
+                        # Deal with parameters without values
+                        if ParserContext.VCARD_2_NO_PARAMETER_VALUES == ParserContext.PARSER_RAISE:
+                            raise PyCalendarInvalidProperty("Invalid property parameter", data)
+                        elif ParserContext.VCARD_2_NO_PARAMETER_VALUES == ParserContext.PARSER_ALLOW:
+                            attribute_value = None
+                        else: # PARSER_IGNORE and PARSER_FIX
+                            attribute_name = None
+
+                        if attribute_name.upper() == "BASE64" and ParserContext.VCARD_2_BASE64 == ParserContext.PARSER_FIX:
+                            attribute_name = definitions.Parameter_ENCODING
+                            attribute_value = definitions.Parameter_Value_ENCODING_B
+                            stripValueSpaces = True
+                    else:
+                        txt = txt[1:]
+                        attribute_value, txt = stringutils.strduptokenstr(txt, ":;,")
+                        if attribute_value is None:
+                            raise PyCalendarInvalidProperty("Invalid property", data)
+
+                    # Now add attribute value (decode ^-escaping)
+                    if attribute_name is not None:
+                        attrvalue = PyCalendarAttribute(name=attribute_name, value=decodeParameterValue(attribute_value))
+                        self.mAttributes.setdefault(attribute_name.upper(), []).append(attrvalue)
+
+                    # Look for additional values
+                    while txt[0] == ',':
+                        txt = txt[1:]
+                        attribute_value2, txt = stringutils.strduptokenstr(txt, ":;,")
+                        if attribute_value2 is None:
+                            raise PyCalendarInvalidProperty("Invalid property", data)
+                        attrvalue.addValue(decodeParameterValue(attribute_value2))
+                elif txt[0] == ':':
+                    txt = txt[1:]
+                    if stripValueSpaces:
+                        txt = txt.replace(" ", "")
+                    self.createValue(txt)
+                    txt = None
+
+        except IndexError:
+            raise PyCalendarInvalidProperty("Invalid property", data)
+
+        # We must have a value of some kind
+        if self.mValue is None:
+            raise PyCalendarInvalidProperty("Invalid property", data)
+
+        return True
+
+
+    def getText(self):
+        os = StringIO.StringIO()
+        self.generate(os)
+        return os.getvalue()
+
+
+    def generate(self, os):
+
+        # Write it out always with value
+        self.generateValue(os, False)
+
+
+    def generateFiltered(self, os, filter):
+
+        # Check for property in filter and whether value is written out
+        test, novalue = filter.testPropertyValue(self.mName.upper())
+        if test:
+            self.generateValue(os, novalue)
+
+
+    # Write out the actual property, possibly skipping the value
+    def generateValue(self, os, novalue):
+
+        self.setupValueAttribute()
+
+        # Must write to temp buffer and then wrap
+        sout = StringIO.StringIO()
+        if self.mGroup:
+            sout.write(self.mGroup + ".")
+        sout.write(self.mName)
+
+        # Write all attributes
+        for key in sorted(self.mAttributes.keys()):
+            for attr in self.mAttributes[key]:
+                sout.write(";")
+                attr.generate(sout)
+
+        # Write value
+        sout.write(":")
+        if self.mName.upper() == "PHOTO" and self.mValue.getType() == PyCalendarValue.VALUETYPE_BINARY:
+            # Handle AB.app PHOTO values
+            sout.write("\r\n")
+
+            value = self.mValue.getText()
+            value_len = len(value)
+            offset = 0
+            while(value_len > 72):
+                sout.write(" ")
+                sout.write(value[offset:offset + 72])
+                sout.write("\r\n")
+                value_len -= 72
+                offset += 72
+            sout.write(" ")
+            sout.write(value[offset:])
+            os.write(sout.getvalue())
+        else:
+            if self.mValue and not novalue:
+                self.mValue.generate(sout)
+
+            # Get string text
+            temp = sout.getvalue()
+            sout.close()
+
+            # Look for line length exceed
+            if len(temp) < 75:
+                os.write(temp)
+            else:
+                # Look for valid utf8 range and write that out
+                start = 0
+                written = 0
+                lineWrap = 74
+                while written < len(temp):
+                    # Start 74 chars on from where we are
+                    offset = start + lineWrap
+                    if offset >= len(temp):
+                        line = temp[start:]
+                        os.write(line)
+                        written = len(temp)
+                    else:
+                        # Check whether next char is valid utf8 lead byte
+                        while (temp[offset] > 0x7F) and ((ord(temp[offset]) & 0xC0) == 0x80):
+                            # Step back until we have a valid char
+                            offset -= 1
+
+                        line = temp[start:offset]
+                        os.write(line)
+                        os.write("\r\n ")
+                        lineWrap = 73   # We are now adding a space at the start
+                        written += offset - start
+                        start = offset
+
+        os.write("\r\n")
+
+
+    def _init_PyCalendarProperty(self):
+        self.mGroup = None
+        self.mName = ""
+        self.mAttributes = {}
+        self.mValue = None
+
+
+    def createValue(self, data):
+        # Tidy first
+        self.mValue = None
+
+        # Get value type from property name
+        valueType = Property.sDefaultValueTypeMap.get(self.mName.upper(), PyCalendarValue.VALUETYPE_TEXT)
+
+        # Check whether custom value is set
+        if definitions.Parameter_VALUE in self.mAttributes:
+            attr = self.getAttributeValue(definitions.Parameter_VALUE)
+            if attr != definitions.Value_TEXT or self.mName.upper() not in Property.sTextVariants:
+                valueType = Property.sValueTypeMap.get(attr, valueType)
+
+        # Check for multivalued
+        if self.mName.upper() in Property.sMultiValues:
+            self.mValue = PyCalendarMultiValue(valueType)
+        else:
+            # Create the type
+            self.mValue = PyCalendarValue.createFromType(valueType)
+
+        # Now parse the data
+        try:
+            if valueType in (PyCalendarValue.VALUETYPE_DATE, PyCalendarValue.VALUETYPE_DATETIME):
+                # vCard supports a slightly different, expanded form, of date
+                self.mValue.parse(data, fullISO=True)
+            else:
+                self.mValue.parse(data)
+        except ValueError:
+            raise PyCalendarInvalidProperty("Invalid property value", data)
+
+
+    def setValue(self, value):
+        # Tidy first
+        self.mValue = None
+
+        # Get value type from property name
+        valueType = Property.sDefaultValueTypeMap.get(self.mName.upper(), PyCalendarUnknownValue)
+
+        # Check whether custom value is set
+        if definitions.Parameter_VALUE in self.mAttributes:
+            attr = self.getAttributeValue(definitions.Parameter_VALUE)
+            if attr != definitions.Value_TEXT or self.mName.upper() not in Property.sTextVariants:
+                valueType = Property.sValueTypeMap.get(attr, valueType)
+
+        # Check for multivalued
+        if self.mName.upper() in Property.sMultiValues:
+            self.mValue = PyCalendarMultiValue(valueType)
+        else:
+            # Create the type
+            self.mValue = PyCalendarValue.createFromType(valueType)
+
+        self.mValue.setValue(value)
+
+
+    def setupValueAttribute(self):
+        if definitions.Parameter_VALUE in self.mAttributes:
+            del self.mAttributes[definitions.Parameter_VALUE]
+
+        # Only if we have a value right now
+        if self.mValue is None:
+            return
+
+        # See if current type is default for this property. If there is no mapping available,
+        # then always add VALUE if it is not TEXT.
+        default_type = Property.sDefaultValueTypeMap.get(self.mName.upper())
+        actual_type = self.mValue.getType()
+        if default_type is None or default_type != actual_type:
+            actual_value = self.sTypeValueMap.get(actual_type)
+            if actual_value is not None and (default_type is not None or actual_type != PyCalendarValue.VALUETYPE_TEXT):
+                self.mAttributes.setdefault(definitions.Parameter_VALUE, []).append(PyCalendarAttribute(name=definitions.Parameter_VALUE, value=actual_value))
+
+
+    # Creation
+    def _init_attr_value_int(self, ival):
+        # Value
+        self.mValue = PyCalendarIntegerValue(value=ival)
+
+        # Attributes
+        self.setupValueAttribute()
+
+
+    def _init_attr_value_text(self, txt, value_type):
+        # Value
+        self.mValue = PyCalendarValue.createFromType(value_type)
+        if isinstance(self.mValue, PyCalendarPlainTextValue) or isinstance(self.mValue, PyCalendarUnknownValue):
+            self.mValue.setValue(txt)
+
+        # Attributes
+        self.setupValueAttribute()
+
+
+    def _init_attr_value_adr(self, reqstatus):
+        # Value
+        self.mValue = AdrValue(reqstatus)
+
+        # Attributes
+        self.setupValueAttribute()
+
+
+    def _init_attr_value_n(self, reqstatus):
+        # Value
+        self.mValue = NValue(reqstatus)
+
+        # Attributes
+        self.setupValueAttribute()
+
+
+    def _init_attr_value_org(self, reqstatus):
+        # Value
+        self.mValue = OrgValue(reqstatus)
+
+        # Attributes
+        self.setupValueAttribute()
+
+
+    def _init_attr_value_datetime(self, dt):
+        # Value
+        self.mValue = PyCalendarDateTimeValue(value=dt)
+
+        # Attributes
+        self.setupValueAttribute()
+
+
+    def _init_attr_value_utcoffset(self, utcoffset):
+        # Value
+        self.mValue = PyCalendarUTCOffsetValue()
+        self.mValue.setValue(utcoffset.getValue())
+
+        # Attributes
+        self.setupValueAttribute()

Deleted: PyCalendar/trunk/src/pycalendar/vcard/tests/__init__.py
===================================================================
--- PyCalendar/branches/server/src/pycalendar/vcard/tests/__init__.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/vcard/tests/__init__.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,15 +0,0 @@
-#
-#    Copyright (c) 2007-2012 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.
-##

Copied: PyCalendar/trunk/src/pycalendar/vcard/tests/__init__.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/vcard/tests/__init__.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/vcard/tests/__init__.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/vcard/tests/__init__.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,15 @@
+#
+#    Copyright (c) 2007-2012 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.
+##

Deleted: PyCalendar/trunk/src/pycalendar/vcard/tests/test_card.py
===================================================================
--- PyCalendar/branches/server/src/pycalendar/vcard/tests/test_card.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/vcard/tests/test_card.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,571 +0,0 @@
-##
-#    Copyright (c) 2007-2012 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.exceptions import PyCalendarInvalidData
-from pycalendar.parser import ParserContext
-from pycalendar.vcard.card import Card
-from pycalendar.vcard.property import Property
-import cStringIO as StringIO
-import difflib
-import unittest
-
-class TestCard(unittest.TestCase):
-
-    data = (
-        (
-"""BEGIN:VCARD
-VERSION:3.0
-N:Thompson;Default;;;
-FN:Default Thompson
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
-item1.X-ABADR:us
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-END:VCARD
-""".replace("\n", "\r\n"),
-"""BEGIN:VCARD
-VERSION:3.0
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
- SA
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-FN:Default Thompson
-N:Thompson;Default;;;
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.X-ABADR:us
-END:VCARD
-""".replace("\n", "\r\n"),
-        ),
-        (
-"""BEGIN:VCARD
-VERSION:3.0
-N:Thompson;Default;;;
-FN:Default Thompson
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
-item1.X-ABADR:us
-BDAY:2001-01-02
-REV:2011-01-02T12:34:56-0500
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-END:VCARD
-""".replace("\n", "\r\n"),
-"""BEGIN:VCARD
-VERSION:3.0
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
- SA
-BDAY:20010102
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-FN:Default Thompson
-N:Thompson;Default;;;
-REV:20110102T123456-0500
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.X-ABADR:us
-END:VCARD
-""".replace("\n", "\r\n"),
-        ),
-        (
-"""BEGIN:VCARD
-VERSION:3.0
-N:Thompson;Default;;;
-FN:Default Thompson
-EMAIL;type=INTERNET;WORK;type=pref:lthompson at example.com
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
-item1.X-ABADR:us
-BDAY:2001-01-02
-REV:2011-01-02T12:34:56-0500
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-END:VCARD
-""".replace("\n", "\r\n"),
-"""BEGIN:VCARD
-VERSION:3.0
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
- SA
-BDAY:20010102
-EMAIL;type=INTERNET;type=pref;WORK:lthompson at example.com
-FN:Default Thompson
-N:Thompson;Default;;;
-REV:20110102T123456-0500
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.X-ABADR:us
-END:VCARD
-""".replace("\n", "\r\n"),
-        ),
-        (
-"""BEGIN:VCARD
-VERSION:3.0
-N:Picture;With;;;
-FN:With Picture
-EMAIL;type=INTERNET;type=WORK;type=pref:withpicture at example.com
-TEL;type=WORK;type=pref:777-777-7777
-TEL;type=CELL:8888888888
-item1.ADR;type=WORK;type=pref:;;1234 Golly Street;Sunnyside;CA;99999;USA
-item1.X-ABADR:us
-PHOTO;Encoding=b:QkVHSU46VkNBUkQKVkVSU0lPTjozLjAKTjpQaWN0dXJlO1dpdGg7Ozs
- KRk46V2l0aCBQaWN0dXJlCkVNQUlMO3R5cGU9SU5URVJORVQ7dHlwZT1XT1JLO3R5cGU9cH
- JlZjp3aXRocGljdHVyZUBleGFtcGxlLmNvbQpURUw7dHlwZT1XT1JLO3R5cGU9cHJlZjo3N
- zctNzc3LTc3NzcKVEVMO3R5cGU9Q0VMTDo4ODg4ODg4ODg4Cml0ZW0xLkFEUjt0eXBlPVdP
- Uks7dHlwZT1wcmVmOjs7MTIzNCBHb2xseSBTdHJlZXQ7U3VubnlzaWRlO0NBOzk5OTk5O1V
- TQQppdGVtMS5YLUFCQURSOnVzClBIT1RPO0JBU0U2NDoKVUlEOjkzNDczMUM2LTFDOTUtNE
- M0MC1CRTFGLUZBNDIxNUIyMzA3QjpBQlBlcnNvbgpFTkQ6VkNBUkQK
-UID:934731C6-1C95-4C40-BE1F-FA4215B2307B:ABPerson
-END:VCARD
-""".replace("\n", "\r\n"),
-"""BEGIN:VCARD
-VERSION:3.0
-UID:934731C6-1C95-4C40-BE1F-FA4215B2307B:ABPerson
-item1.ADR;type=WORK;type=pref:;;1234 Golly Street;Sunnyside;CA;99999;USA
-EMAIL;type=INTERNET;type=WORK;type=pref:withpicture at example.com
-FN:With Picture
-N:Picture;With;;;
-PHOTO;Encoding=b:
- QkVHSU46VkNBUkQKVkVSU0lPTjozLjAKTjpQaWN0dXJlO1dpdGg7OzsKRk46V2l0aCBQaWN0
- dXJlCkVNQUlMO3R5cGU9SU5URVJORVQ7dHlwZT1XT1JLO3R5cGU9cHJlZjp3aXRocGljdHVy
- ZUBleGFtcGxlLmNvbQpURUw7dHlwZT1XT1JLO3R5cGU9cHJlZjo3NzctNzc3LTc3NzcKVEVM
- O3R5cGU9Q0VMTDo4ODg4ODg4ODg4Cml0ZW0xLkFEUjt0eXBlPVdPUks7dHlwZT1wcmVmOjs7
- MTIzNCBHb2xseSBTdHJlZXQ7U3VubnlzaWRlO0NBOzk5OTk5O1VTQQppdGVtMS5YLUFCQURS
- OnVzClBIT1RPO0JBU0U2NDoKVUlEOjkzNDczMUM2LTFDOTUtNEM0MC1CRTFGLUZBNDIxNUIy
- MzA3QjpBQlBlcnNvbgpFTkQ6VkNBUkQK
-TEL;type=WORK;type=pref:777-777-7777
-TEL;type=CELL:8888888888
-item1.X-ABADR:us
-END:VCARD
-""".replace("\n", "\r\n"),
-        ),
-        (
-"""BEGIN:VCARD
-VERSION:3.0
-N:Thompson;Default;;;
-FN:Default Thompson
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
-item1.X-ABADR:us
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-X-APPLE-STRUCTURED-LOCATION;VALUE=URI:geo:123.123,123.123
-X-Test:Some\, text.
-END:VCARD
-""".replace("\n", "\r\n"),
-"""BEGIN:VCARD
-VERSION:3.0
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
- SA
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-FN:Default Thompson
-N:Thompson;Default;;;
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.X-ABADR:us
-X-APPLE-STRUCTURED-LOCATION;VALUE=URI:geo:123.123,123.123
-X-Test:Some\, text.
-END:VCARD
-""".replace("\n", "\r\n"),
-        ),
-    )
-
-
-    def testRoundtrip(self):
-
-
-        def _doRoundtrip(caldata, resultdata=None):
-            test1 = resultdata if resultdata is not None else caldata
-
-            card = Card()
-            card.parse(StringIO.StringIO(caldata))
-
-            s = StringIO.StringIO()
-            card.generate(s)
-            test2 = s.getvalue()
-
-            self.assertEqual(
-                test1,
-                test2,
-                "\n".join(difflib.unified_diff(test1.splitlines(), test2.splitlines())),
-            )
-
-        for item, result in self.data:
-            _doRoundtrip(item, result)
-
-
-    def testRoundtripDuplicate(self):
-
-
-        def _doDuplicateRoundtrip(caldata, result):
-            card = Card()
-            card.parse(StringIO.StringIO(caldata))
-            card = card.duplicate()
-
-            s = StringIO.StringIO()
-            card.generate(s)
-            test = s.getvalue()
-            self.assertEqual(test, result, "\n".join(difflib.unified_diff(test.splitlines(), result.splitlines())))
-
-        for item, result in self.data:
-            _doDuplicateRoundtrip(item, result)
-
-
-    def testEquality(self):
-
-
-        def _doEquality(caldata):
-            card1 = Card()
-            card1.parse(StringIO.StringIO(caldata))
-
-            card2 = Card()
-            card2.parse(StringIO.StringIO(caldata))
-
-            self.assertEqual(card1, card2, "\n".join(difflib.unified_diff(str(card1).splitlines(), str(card2).splitlines())))
-
-
-        def _doNonEquality(caldata):
-            card1 = Card()
-            card1.parse(StringIO.StringIO(caldata))
-
-            card2 = Card()
-            card2.parse(StringIO.StringIO(caldata))
-            card2.addProperty(Property("X-FOO", "BAR"))
-
-            self.assertNotEqual(card1, card2)
-
-        for item, _ignore in self.data:
-            _doEquality(item)
-            _doNonEquality(item)
-
-
-    def testMultiple(self):
-
-        data = (
-            (
-"""BEGIN:VCARD
-VERSION:3.0
-N:Thompson;Default;;;
-FN:Default Thompson
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
-item1.X-ABADR:us
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-END:VCARD
-""".replace("\n", "\r\n"), (
-"""BEGIN:VCARD
-VERSION:3.0
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
- SA
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-FN:Default Thompson
-N:Thompson;Default;;;
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.X-ABADR:us
-END:VCARD
-""".replace("\n", "\r\n"),
-            )),
-            (
-"""BEGIN:VCARD
-VERSION:3.0
-N:Thompson;Default;;;
-FN:Default Thompson
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
-item1.X-ABADR:us
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-END:VCARD
-BEGIN:VCARD
-VERSION:3.0
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E2:ABPerson
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
- SA
-EMAIL;type=INTERNET;type=WORK;type=pref:athompson at example.com
-FN:Another Thompson
-N:Thompson;Another;;;
-TEL;type=WORK;type=pref:1-555-555-5556
-TEL;type=CELL:1-444-444-4445
-item1.X-ABADR:us
-END:VCARD
-""".replace("\n", "\r\n"), (
-"""BEGIN:VCARD
-VERSION:3.0
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
- SA
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-FN:Default Thompson
-N:Thompson;Default;;;
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.X-ABADR:us
-END:VCARD
-""".replace("\n", "\r\n"),
-"""BEGIN:VCARD
-VERSION:3.0
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E2:ABPerson
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
- SA
-EMAIL;type=INTERNET;type=WORK;type=pref:athompson at example.com
-FN:Another Thompson
-N:Thompson;Another;;;
-TEL;type=WORK;type=pref:1-555-555-5556
-TEL;type=CELL:1-444-444-4445
-item1.X-ABADR:us
-END:VCARD
-""".replace("\n", "\r\n"),
-            )),
-        )
-
-        for item, results in data:
-
-            cards = Card.parseMultiple(StringIO.StringIO(item))
-            self.assertEqual(len(cards), len(results))
-            for card, result in zip(cards, results):
-                self.assertEqual(str(card), result, "\n".join(difflib.unified_diff(str(card).splitlines(), result.splitlines())))
-
-
-    def testABapp(self):
-
-        data = """BEGIN:VCARD
-VERSION:3.0
-N:Card;Test;;;
-FN:Test Card
-EMAIL;type=INTERNET;type=WORK;type=pref:sagen at apple.com
-TEL;type=WORK;type=pref:555-1212
-TEL;type=HOME:532-1234
-PHOTO;BASE64:
-  TU0AKgAAMAj////////////////////////////////////////////////////////////+///+
-  SW1hZ2VNYWdpY2sgNS4zLjkgMTAvMDEvMDEgUToxNiBodHRwOi8vd3d3LmltYWdlbWFnaWNrLm9y
-  ZwA=
-CATEGORIES:My Contacts
-X-ABUID:5B77BC10-E9DB-48C4-8BE1-BAB5E38E1E43\:ABPerson
-UID:128ad7ee-a656-4773-95ce-f07f77e8cc23
-REV:2011-03-23T20:20:04Z
-END:VCARD
-""".replace("\n", "\r\n")
-
-        result = """BEGIN:VCARD
-VERSION:3.0
-UID:128ad7ee-a656-4773-95ce-f07f77e8cc23
-CATEGORIES:My Contacts
-EMAIL;type=INTERNET;type=WORK;type=pref:sagen at apple.com
-FN:Test Card
-N:Card;Test;;;
-PHOTO;ENCODING=B:
- TU0AKgAAMAj////////////////////////////////////////////////////////////+
- ///+SW1hZ2VNYWdpY2sgNS4zLjkgMTAvMDEvMDEgUToxNiBodHRwOi8vd3d3LmltYWdlbWFn
- aWNrLm9yZwA=
-REV:20110323T202004Z
-TEL;type=WORK;type=pref:555-1212
-TEL;type=HOME:532-1234
-X-ABUID:5B77BC10-E9DB-48C4-8BE1-BAB5E38E1E43:ABPerson
-END:VCARD
-""".replace("\n", "\r\n")
-
-        card = Card.parseText(data)
-        self.assertEqual(str(card), result)
-
-
-    def testParseFail(self):
-
-        data = (
-"""BEGIN:VCARD
-VERSION:3.0
-N:Thompson;Default;;;
-FN:Default Thompson
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
-item1.X-ABADR:us
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-""".replace("\n", "\r\n"),
-
-"""BEGIN:VCALENDAR
-PRODID:-//mulberrymail.com//Mulberry v4.0//EN
-VERSION:2.0
-END:VCALENDAR
-""".replace("\n", "\r\n"),
-
-"""BOGUS
-BEGIN:VCARD
-VERSION:3.0
-N:Thompson;Default;;;
-FN:Default Thompson
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
-item1.X-ABADR:us
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-END:VCARD
-""".replace("\n", "\r\n"),
-
-"""BOGUS
-
-BEGIN:VCARD
-VERSION:3.0
-N:Thompson;Default;;;
-FN:Default Thompson
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
-item1.X-ABADR:us
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-END:VCARD
-""".replace("\n", "\r\n"),
-
-"""BEGIN:VCARD
-VERSION:3.0
-N:Thompson;Default;;;
-FN:Default Thompson
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
-item1.X-ABADR:us
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-END:VCARD
-BOGUS
-""".replace("\n", "\r\n"),
-
-"""BEGIN:VCARD
-VERSION:3.0
-N:Thompson;Default;;;
-FN:Default Thompson
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
-item1.X-ABADR:us
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-END:VCARD
-
-BOGUS
-""".replace("\n", "\r\n"),
-
-        )
-
-        for item in data:
-            self.assertRaises(PyCalendarInvalidData, Card.parseText, item)
-
-
-    def testParseBlank(self):
-
-        data = (
-"""
-BEGIN:VCARD
-VERSION:3.0
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
- SA
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-FN:Default Thompson
-N:Thompson;Default;;;
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.X-ABADR:us
-END:VCARD
-""".replace("\n", "\r\n"),
-
-"""
-
-BEGIN:VCARD
-VERSION:3.0
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
- SA
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-FN:Default Thompson
-N:Thompson;Default;;;
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.X-ABADR:us
-END:VCARD
-""".replace("\n", "\r\n"),
-
-"""BEGIN:VCARD
-VERSION:3.0
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
- SA
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-FN:Default Thompson
-N:Thompson;Default;;;
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.X-ABADR:us
-END:VCARD
-
-
-""".replace("\n", "\r\n"),
-
-"""BEGIN:VCARD
-VERSION:3.0
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
- SA
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-
-FN:Default Thompson
-N:Thompson;Default;;;
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.X-ABADR:us
-END:VCARD
-""".replace("\n", "\r\n"),
-
-"""BEGIN:VCARD
-VERSION:3.0
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
- SA
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-
-
-FN:Default Thompson
-N:Thompson;Default;;;
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.X-ABADR:us
-END:VCARD
-""".replace("\n", "\r\n"),
-        )
-
-        save = ParserContext.BLANK_LINES_IN_DATA
-        for item in data:
-            ParserContext.BLANK_LINES_IN_DATA = ParserContext.PARSER_RAISE
-            self.assertRaises(PyCalendarInvalidData, Card.parseText, item)
-
-            ParserContext.BLANK_LINES_IN_DATA = ParserContext.PARSER_IGNORE
-            lines = item.split("\r\n")
-            result = "\r\n".join([line for line in lines if line]) + "\r\n"
-            self.assertEqual(str(Card.parseText(item)), result)
-
-        ParserContext.BLANK_LINES_IN_DATA = save

Copied: PyCalendar/trunk/src/pycalendar/vcard/tests/test_card.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/vcard/tests/test_card.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/vcard/tests/test_card.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/vcard/tests/test_card.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,571 @@
+##
+#    Copyright (c) 2007-2012 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.exceptions import PyCalendarInvalidData
+from pycalendar.parser import ParserContext
+from pycalendar.vcard.card import Card
+from pycalendar.vcard.property import Property
+import cStringIO as StringIO
+import difflib
+import unittest
+
+class TestCard(unittest.TestCase):
+
+    data = (
+        (
+"""BEGIN:VCARD
+VERSION:3.0
+N:Thompson;Default;;;
+FN:Default Thompson
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
+item1.X-ABADR:us
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+END:VCARD
+""".replace("\n", "\r\n"),
+"""BEGIN:VCARD
+VERSION:3.0
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
+ SA
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+FN:Default Thompson
+N:Thompson;Default;;;
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.X-ABADR:us
+END:VCARD
+""".replace("\n", "\r\n"),
+        ),
+        (
+"""BEGIN:VCARD
+VERSION:3.0
+N:Thompson;Default;;;
+FN:Default Thompson
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
+item1.X-ABADR:us
+BDAY:2001-01-02
+REV:2011-01-02T12:34:56-0500
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+END:VCARD
+""".replace("\n", "\r\n"),
+"""BEGIN:VCARD
+VERSION:3.0
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
+ SA
+BDAY:20010102
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+FN:Default Thompson
+N:Thompson;Default;;;
+REV:20110102T123456-0500
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.X-ABADR:us
+END:VCARD
+""".replace("\n", "\r\n"),
+        ),
+        (
+"""BEGIN:VCARD
+VERSION:3.0
+N:Thompson;Default;;;
+FN:Default Thompson
+EMAIL;type=INTERNET;WORK;type=pref:lthompson at example.com
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
+item1.X-ABADR:us
+BDAY:2001-01-02
+REV:2011-01-02T12:34:56-0500
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+END:VCARD
+""".replace("\n", "\r\n"),
+"""BEGIN:VCARD
+VERSION:3.0
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
+ SA
+BDAY:20010102
+EMAIL;type=INTERNET;type=pref;WORK:lthompson at example.com
+FN:Default Thompson
+N:Thompson;Default;;;
+REV:20110102T123456-0500
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.X-ABADR:us
+END:VCARD
+""".replace("\n", "\r\n"),
+        ),
+        (
+"""BEGIN:VCARD
+VERSION:3.0
+N:Picture;With;;;
+FN:With Picture
+EMAIL;type=INTERNET;type=WORK;type=pref:withpicture at example.com
+TEL;type=WORK;type=pref:777-777-7777
+TEL;type=CELL:8888888888
+item1.ADR;type=WORK;type=pref:;;1234 Golly Street;Sunnyside;CA;99999;USA
+item1.X-ABADR:us
+PHOTO;Encoding=b:QkVHSU46VkNBUkQKVkVSU0lPTjozLjAKTjpQaWN0dXJlO1dpdGg7Ozs
+ KRk46V2l0aCBQaWN0dXJlCkVNQUlMO3R5cGU9SU5URVJORVQ7dHlwZT1XT1JLO3R5cGU9cH
+ JlZjp3aXRocGljdHVyZUBleGFtcGxlLmNvbQpURUw7dHlwZT1XT1JLO3R5cGU9cHJlZjo3N
+ zctNzc3LTc3NzcKVEVMO3R5cGU9Q0VMTDo4ODg4ODg4ODg4Cml0ZW0xLkFEUjt0eXBlPVdP
+ Uks7dHlwZT1wcmVmOjs7MTIzNCBHb2xseSBTdHJlZXQ7U3VubnlzaWRlO0NBOzk5OTk5O1V
+ TQQppdGVtMS5YLUFCQURSOnVzClBIT1RPO0JBU0U2NDoKVUlEOjkzNDczMUM2LTFDOTUtNE
+ M0MC1CRTFGLUZBNDIxNUIyMzA3QjpBQlBlcnNvbgpFTkQ6VkNBUkQK
+UID:934731C6-1C95-4C40-BE1F-FA4215B2307B:ABPerson
+END:VCARD
+""".replace("\n", "\r\n"),
+"""BEGIN:VCARD
+VERSION:3.0
+UID:934731C6-1C95-4C40-BE1F-FA4215B2307B:ABPerson
+item1.ADR;type=WORK;type=pref:;;1234 Golly Street;Sunnyside;CA;99999;USA
+EMAIL;type=INTERNET;type=WORK;type=pref:withpicture at example.com
+FN:With Picture
+N:Picture;With;;;
+PHOTO;Encoding=b:
+ QkVHSU46VkNBUkQKVkVSU0lPTjozLjAKTjpQaWN0dXJlO1dpdGg7OzsKRk46V2l0aCBQaWN0
+ dXJlCkVNQUlMO3R5cGU9SU5URVJORVQ7dHlwZT1XT1JLO3R5cGU9cHJlZjp3aXRocGljdHVy
+ ZUBleGFtcGxlLmNvbQpURUw7dHlwZT1XT1JLO3R5cGU9cHJlZjo3NzctNzc3LTc3NzcKVEVM
+ O3R5cGU9Q0VMTDo4ODg4ODg4ODg4Cml0ZW0xLkFEUjt0eXBlPVdPUks7dHlwZT1wcmVmOjs7
+ MTIzNCBHb2xseSBTdHJlZXQ7U3VubnlzaWRlO0NBOzk5OTk5O1VTQQppdGVtMS5YLUFCQURS
+ OnVzClBIT1RPO0JBU0U2NDoKVUlEOjkzNDczMUM2LTFDOTUtNEM0MC1CRTFGLUZBNDIxNUIy
+ MzA3QjpBQlBlcnNvbgpFTkQ6VkNBUkQK
+TEL;type=WORK;type=pref:777-777-7777
+TEL;type=CELL:8888888888
+item1.X-ABADR:us
+END:VCARD
+""".replace("\n", "\r\n"),
+        ),
+        (
+"""BEGIN:VCARD
+VERSION:3.0
+N:Thompson;Default;;;
+FN:Default Thompson
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
+item1.X-ABADR:us
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+X-APPLE-STRUCTURED-LOCATION;VALUE=URI:geo:123.123,123.123
+X-Test:Some\, text.
+END:VCARD
+""".replace("\n", "\r\n"),
+"""BEGIN:VCARD
+VERSION:3.0
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
+ SA
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+FN:Default Thompson
+N:Thompson;Default;;;
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.X-ABADR:us
+X-APPLE-STRUCTURED-LOCATION;VALUE=URI:geo:123.123,123.123
+X-Test:Some\, text.
+END:VCARD
+""".replace("\n", "\r\n"),
+        ),
+    )
+
+
+    def testRoundtrip(self):
+
+
+        def _doRoundtrip(caldata, resultdata=None):
+            test1 = resultdata if resultdata is not None else caldata
+
+            card = Card()
+            card.parse(StringIO.StringIO(caldata))
+
+            s = StringIO.StringIO()
+            card.generate(s)
+            test2 = s.getvalue()
+
+            self.assertEqual(
+                test1,
+                test2,
+                "\n".join(difflib.unified_diff(test1.splitlines(), test2.splitlines())),
+            )
+
+        for item, result in self.data:
+            _doRoundtrip(item, result)
+
+
+    def testRoundtripDuplicate(self):
+
+
+        def _doDuplicateRoundtrip(caldata, result):
+            card = Card()
+            card.parse(StringIO.StringIO(caldata))
+            card = card.duplicate()
+
+            s = StringIO.StringIO()
+            card.generate(s)
+            test = s.getvalue()
+            self.assertEqual(test, result, "\n".join(difflib.unified_diff(test.splitlines(), result.splitlines())))
+
+        for item, result in self.data:
+            _doDuplicateRoundtrip(item, result)
+
+
+    def testEquality(self):
+
+
+        def _doEquality(caldata):
+            card1 = Card()
+            card1.parse(StringIO.StringIO(caldata))
+
+            card2 = Card()
+            card2.parse(StringIO.StringIO(caldata))
+
+            self.assertEqual(card1, card2, "\n".join(difflib.unified_diff(str(card1).splitlines(), str(card2).splitlines())))
+
+
+        def _doNonEquality(caldata):
+            card1 = Card()
+            card1.parse(StringIO.StringIO(caldata))
+
+            card2 = Card()
+            card2.parse(StringIO.StringIO(caldata))
+            card2.addProperty(Property("X-FOO", "BAR"))
+
+            self.assertNotEqual(card1, card2)
+
+        for item, _ignore in self.data:
+            _doEquality(item)
+            _doNonEquality(item)
+
+
+    def testMultiple(self):
+
+        data = (
+            (
+"""BEGIN:VCARD
+VERSION:3.0
+N:Thompson;Default;;;
+FN:Default Thompson
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
+item1.X-ABADR:us
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+END:VCARD
+""".replace("\n", "\r\n"), (
+"""BEGIN:VCARD
+VERSION:3.0
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
+ SA
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+FN:Default Thompson
+N:Thompson;Default;;;
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.X-ABADR:us
+END:VCARD
+""".replace("\n", "\r\n"),
+            )),
+            (
+"""BEGIN:VCARD
+VERSION:3.0
+N:Thompson;Default;;;
+FN:Default Thompson
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
+item1.X-ABADR:us
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+END:VCARD
+BEGIN:VCARD
+VERSION:3.0
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E2:ABPerson
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
+ SA
+EMAIL;type=INTERNET;type=WORK;type=pref:athompson at example.com
+FN:Another Thompson
+N:Thompson;Another;;;
+TEL;type=WORK;type=pref:1-555-555-5556
+TEL;type=CELL:1-444-444-4445
+item1.X-ABADR:us
+END:VCARD
+""".replace("\n", "\r\n"), (
+"""BEGIN:VCARD
+VERSION:3.0
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
+ SA
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+FN:Default Thompson
+N:Thompson;Default;;;
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.X-ABADR:us
+END:VCARD
+""".replace("\n", "\r\n"),
+"""BEGIN:VCARD
+VERSION:3.0
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E2:ABPerson
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
+ SA
+EMAIL;type=INTERNET;type=WORK;type=pref:athompson at example.com
+FN:Another Thompson
+N:Thompson;Another;;;
+TEL;type=WORK;type=pref:1-555-555-5556
+TEL;type=CELL:1-444-444-4445
+item1.X-ABADR:us
+END:VCARD
+""".replace("\n", "\r\n"),
+            )),
+        )
+
+        for item, results in data:
+
+            cards = Card.parseMultiple(StringIO.StringIO(item))
+            self.assertEqual(len(cards), len(results))
+            for card, result in zip(cards, results):
+                self.assertEqual(str(card), result, "\n".join(difflib.unified_diff(str(card).splitlines(), result.splitlines())))
+
+
+    def testABapp(self):
+
+        data = """BEGIN:VCARD
+VERSION:3.0
+N:Card;Test;;;
+FN:Test Card
+EMAIL;type=INTERNET;type=WORK;type=pref:sagen at apple.com
+TEL;type=WORK;type=pref:555-1212
+TEL;type=HOME:532-1234
+PHOTO;BASE64:
+  TU0AKgAAMAj////////////////////////////////////////////////////////////+///+
+  SW1hZ2VNYWdpY2sgNS4zLjkgMTAvMDEvMDEgUToxNiBodHRwOi8vd3d3LmltYWdlbWFnaWNrLm9y
+  ZwA=
+CATEGORIES:My Contacts
+X-ABUID:5B77BC10-E9DB-48C4-8BE1-BAB5E38E1E43\:ABPerson
+UID:128ad7ee-a656-4773-95ce-f07f77e8cc23
+REV:2011-03-23T20:20:04Z
+END:VCARD
+""".replace("\n", "\r\n")
+
+        result = """BEGIN:VCARD
+VERSION:3.0
+UID:128ad7ee-a656-4773-95ce-f07f77e8cc23
+CATEGORIES:My Contacts
+EMAIL;type=INTERNET;type=WORK;type=pref:sagen at apple.com
+FN:Test Card
+N:Card;Test;;;
+PHOTO;ENCODING=B:
+ TU0AKgAAMAj////////////////////////////////////////////////////////////+
+ ///+SW1hZ2VNYWdpY2sgNS4zLjkgMTAvMDEvMDEgUToxNiBodHRwOi8vd3d3LmltYWdlbWFn
+ aWNrLm9yZwA=
+REV:20110323T202004Z
+TEL;type=WORK;type=pref:555-1212
+TEL;type=HOME:532-1234
+X-ABUID:5B77BC10-E9DB-48C4-8BE1-BAB5E38E1E43:ABPerson
+END:VCARD
+""".replace("\n", "\r\n")
+
+        card = Card.parseText(data)
+        self.assertEqual(str(card), result)
+
+
+    def testParseFail(self):
+
+        data = (
+"""BEGIN:VCARD
+VERSION:3.0
+N:Thompson;Default;;;
+FN:Default Thompson
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
+item1.X-ABADR:us
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+""".replace("\n", "\r\n"),
+
+"""BEGIN:VCALENDAR
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+VERSION:2.0
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+
+"""BOGUS
+BEGIN:VCARD
+VERSION:3.0
+N:Thompson;Default;;;
+FN:Default Thompson
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
+item1.X-ABADR:us
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+END:VCARD
+""".replace("\n", "\r\n"),
+
+"""BOGUS
+
+BEGIN:VCARD
+VERSION:3.0
+N:Thompson;Default;;;
+FN:Default Thompson
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
+item1.X-ABADR:us
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+END:VCARD
+""".replace("\n", "\r\n"),
+
+"""BEGIN:VCARD
+VERSION:3.0
+N:Thompson;Default;;;
+FN:Default Thompson
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
+item1.X-ABADR:us
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+END:VCARD
+BOGUS
+""".replace("\n", "\r\n"),
+
+"""BEGIN:VCARD
+VERSION:3.0
+N:Thompson;Default;;;
+FN:Default Thompson
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
+item1.X-ABADR:us
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+END:VCARD
+
+BOGUS
+""".replace("\n", "\r\n"),
+
+        )
+
+        for item in data:
+            self.assertRaises(PyCalendarInvalidData, Card.parseText, item)
+
+
+    def testParseBlank(self):
+
+        data = (
+"""
+BEGIN:VCARD
+VERSION:3.0
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
+ SA
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+FN:Default Thompson
+N:Thompson;Default;;;
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.X-ABADR:us
+END:VCARD
+""".replace("\n", "\r\n"),
+
+"""
+
+BEGIN:VCARD
+VERSION:3.0
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
+ SA
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+FN:Default Thompson
+N:Thompson;Default;;;
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.X-ABADR:us
+END:VCARD
+""".replace("\n", "\r\n"),
+
+"""BEGIN:VCARD
+VERSION:3.0
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
+ SA
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+FN:Default Thompson
+N:Thompson;Default;;;
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.X-ABADR:us
+END:VCARD
+
+
+""".replace("\n", "\r\n"),
+
+"""BEGIN:VCARD
+VERSION:3.0
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
+ SA
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+
+FN:Default Thompson
+N:Thompson;Default;;;
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.X-ABADR:us
+END:VCARD
+""".replace("\n", "\r\n"),
+
+"""BEGIN:VCARD
+VERSION:3.0
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
+ SA
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+
+
+FN:Default Thompson
+N:Thompson;Default;;;
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.X-ABADR:us
+END:VCARD
+""".replace("\n", "\r\n"),
+        )
+
+        save = ParserContext.BLANK_LINES_IN_DATA
+        for item in data:
+            ParserContext.BLANK_LINES_IN_DATA = ParserContext.PARSER_RAISE
+            self.assertRaises(PyCalendarInvalidData, Card.parseText, item)
+
+            ParserContext.BLANK_LINES_IN_DATA = ParserContext.PARSER_IGNORE
+            lines = item.split("\r\n")
+            result = "\r\n".join([line for line in lines if line]) + "\r\n"
+            self.assertEqual(str(Card.parseText(item)), result)
+
+        ParserContext.BLANK_LINES_IN_DATA = save

Deleted: PyCalendar/trunk/src/pycalendar/vcard/tests/test_property.py
===================================================================
--- PyCalendar/branches/server/src/pycalendar/vcard/tests/test_property.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/vcard/tests/test_property.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,116 +0,0 @@
-##
-#    Copyright (c) 2007-2012 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.attribute import PyCalendarAttribute
-from pycalendar.exceptions import PyCalendarInvalidProperty
-from pycalendar.parser import ParserContext
-from pycalendar.vcard.property import Property
-import unittest
-
-class TestProperty(unittest.TestCase):
-
-    test_data = (
-        # Different value types
-        "PHOTO;VALUE=URI:http://example.com/photo.jpg",
-        "photo;VALUE=URI:http://example.com/photo.jpg",
-        "TEL;type=WORK;type=pref:1-555-555-5555",
-        "REV:20060226T120000Z",
-        "X-FOO:BAR",
-        "NOTE:Some \\ntext",
-        "note:Some \\ntext",
-        "item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;CA;11111;USA",
-        "X-Test:Some\, text.",
-        "X-Test;VALUE=URI:geio:123.123,123.123",
-    )
-
-    def testParseGenerate(self):
-
-        for data in TestProperty.test_data:
-            prop = Property()
-            prop.parse(data)
-            propstr = str(prop)
-            self.assertEqual(propstr[:-2], data, "Failed parse/generate: %s to %s" % (data, propstr,))
-
-
-    def testEquality(self):
-
-        for data in TestProperty.test_data:
-            prop1 = Property()
-            prop1.parse(data)
-            prop2 = Property()
-            prop2.parse(data)
-            self.assertEqual(prop1, prop2, "Failed equality: %s" % (data,))
-
-
-    def testParseBad(self):
-
-        test_bad_data = (
-            "REV:20060226T120",
-            "NOTE:Some \\atext",
-        )
-        save = ParserContext.INVALID_ESCAPE_SEQUENCES
-        for data in test_bad_data:
-            ParserContext.INVALID_ESCAPE_SEQUENCES = ParserContext.PARSER_RAISE
-            prop = Property()
-            self.assertRaises(PyCalendarInvalidProperty, prop.parse, data)
-        ParserContext.INVALID_ESCAPE_SEQUENCES = save
-
-
-    def testHash(self):
-
-        hashes = []
-        for item in TestProperty.test_data:
-            prop = Property()
-            prop.parse(item)
-            hashes.append(hash(prop))
-        hashes.sort()
-        for i in range(1, len(hashes)):
-            self.assertNotEqual(hashes[i - 1], hashes[i])
-
-
-    def testDefaultValueCreate(self):
-
-        test_data = (
-            ("SOURCE", "http://example.com/source", "SOURCE:http://example.com/source\r\n"),
-            ("souRCE", "http://example.com/source", "souRCE:http://example.com/source\r\n"),
-            ("PHOTO", "YWJj", "PHOTO:\r\n YWJj\r\n"),
-            ("photo", "YWJj", "photo:\r\n YWJj\r\n"),
-            ("URL", "http://example.com/tz1", "URL:http://example.com/tz1\r\n"),
-        )
-        for propname, propvalue, result in test_data:
-            prop = Property(name=propname, value=propvalue)
-            self.assertEqual(str(prop), result)
-
-
-    def testParameterEncodingDecoding(self):
-
-        prop = Property(name="X-FOO", value="Test")
-        prop.addAttribute(PyCalendarAttribute("X-BAR", "\"Check\""))
-        self.assertEqual(str(prop), "X-FOO;X-BAR=^'Check^':Test\r\n")
-
-        prop.addAttribute(PyCalendarAttribute("X-BAR2", "Check\nThis\tOut\n"))
-        self.assertEqual(str(prop), "X-FOO;X-BAR=^'Check^';X-BAR2=Check^nThis^tOut^n:Test\r\n")
-
-        data = "X-FOO;X-BAR=^'Check^':Test"
-        prop = Property()
-        prop.parse(data)
-        self.assertEqual(prop.getAttributeValue("X-BAR"), "\"Check\"")
-
-        data = "X-FOO;X-BAR=^'Check^';X-BAR2=Check^nThis^tOut^n:Test"
-        prop = Property()
-        prop.parse(data)
-        self.assertEqual(prop.getAttributeValue("X-BAR"), "\"Check\"")
-        self.assertEqual(prop.getAttributeValue("X-BAR2"), "Check\nThis\tOut\n")

Copied: PyCalendar/trunk/src/pycalendar/vcard/tests/test_property.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/vcard/tests/test_property.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/vcard/tests/test_property.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/vcard/tests/test_property.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,116 @@
+##
+#    Copyright (c) 2007-2012 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.attribute import PyCalendarAttribute
+from pycalendar.exceptions import PyCalendarInvalidProperty
+from pycalendar.parser import ParserContext
+from pycalendar.vcard.property import Property
+import unittest
+
+class TestProperty(unittest.TestCase):
+
+    test_data = (
+        # Different value types
+        "PHOTO;VALUE=URI:http://example.com/photo.jpg",
+        "photo;VALUE=URI:http://example.com/photo.jpg",
+        "TEL;type=WORK;type=pref:1-555-555-5555",
+        "REV:20060226T120000Z",
+        "X-FOO:BAR",
+        "NOTE:Some \\ntext",
+        "note:Some \\ntext",
+        "item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;CA;11111;USA",
+        "X-Test:Some\, text.",
+        "X-Test;VALUE=URI:geio:123.123,123.123",
+    )
+
+    def testParseGenerate(self):
+
+        for data in TestProperty.test_data:
+            prop = Property()
+            prop.parse(data)
+            propstr = str(prop)
+            self.assertEqual(propstr[:-2], data, "Failed parse/generate: %s to %s" % (data, propstr,))
+
+
+    def testEquality(self):
+
+        for data in TestProperty.test_data:
+            prop1 = Property()
+            prop1.parse(data)
+            prop2 = Property()
+            prop2.parse(data)
+            self.assertEqual(prop1, prop2, "Failed equality: %s" % (data,))
+
+
+    def testParseBad(self):
+
+        test_bad_data = (
+            "REV:20060226T120",
+            "NOTE:Some \\atext",
+        )
+        save = ParserContext.INVALID_ESCAPE_SEQUENCES
+        for data in test_bad_data:
+            ParserContext.INVALID_ESCAPE_SEQUENCES = ParserContext.PARSER_RAISE
+            prop = Property()
+            self.assertRaises(PyCalendarInvalidProperty, prop.parse, data)
+        ParserContext.INVALID_ESCAPE_SEQUENCES = save
+
+
+    def testHash(self):
+
+        hashes = []
+        for item in TestProperty.test_data:
+            prop = Property()
+            prop.parse(item)
+            hashes.append(hash(prop))
+        hashes.sort()
+        for i in range(1, len(hashes)):
+            self.assertNotEqual(hashes[i - 1], hashes[i])
+
+
+    def testDefaultValueCreate(self):
+
+        test_data = (
+            ("SOURCE", "http://example.com/source", "SOURCE:http://example.com/source\r\n"),
+            ("souRCE", "http://example.com/source", "souRCE:http://example.com/source\r\n"),
+            ("PHOTO", "YWJj", "PHOTO:\r\n YWJj\r\n"),
+            ("photo", "YWJj", "photo:\r\n YWJj\r\n"),
+            ("URL", "http://example.com/tz1", "URL:http://example.com/tz1\r\n"),
+        )
+        for propname, propvalue, result in test_data:
+            prop = Property(name=propname, value=propvalue)
+            self.assertEqual(str(prop), result)
+
+
+    def testParameterEncodingDecoding(self):
+
+        prop = Property(name="X-FOO", value="Test")
+        prop.addAttribute(PyCalendarAttribute("X-BAR", "\"Check\""))
+        self.assertEqual(str(prop), "X-FOO;X-BAR=^'Check^':Test\r\n")
+
+        prop.addAttribute(PyCalendarAttribute("X-BAR2", "Check\nThis\tOut\n"))
+        self.assertEqual(str(prop), "X-FOO;X-BAR=^'Check^';X-BAR2=Check^nThis^tOut^n:Test\r\n")
+
+        data = "X-FOO;X-BAR=^'Check^':Test"
+        prop = Property()
+        prop.parse(data)
+        self.assertEqual(prop.getAttributeValue("X-BAR"), "\"Check\"")
+
+        data = "X-FOO;X-BAR=^'Check^';X-BAR2=Check^nThis^tOut^n:Test"
+        prop = Property()
+        prop.parse(data)
+        self.assertEqual(prop.getAttributeValue("X-BAR"), "\"Check\"")
+        self.assertEqual(prop.getAttributeValue("X-BAR2"), "Check\nThis\tOut\n")

Deleted: PyCalendar/trunk/src/pycalendar/vcard/tests/test_validation.py
===================================================================
--- PyCalendar/branches/server/src/pycalendar/vcard/tests/test_validation.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/vcard/tests/test_validation.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,221 +0,0 @@
-##
-#    Copyright (c) 2011-2012 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.exceptions import PyCalendarValidationError
-from pycalendar.vcard.card import Card
-import unittest
-
-class TestValidation(unittest.TestCase):
-
-    def test_basic(self):
-
-        data = (
-            (
-                "No problems",
-                """BEGIN:VCARD
-VERSION:3.0
-N:Thompson;Default;;;
-FN:Default Thompson
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
-item1.X-ABADR:us
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-END:VCARD
-""".replace("\n", "\r\n"),
-                set(),
-                set(),
-            ),
-            (
-                "No VERSION",
-                """BEGIN:VCARD
-VERSION:3.0
-FN:Default Thompson
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
-item1.X-ABADR:us
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-END:VCARD
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VCARD] Missing or too many required property: N",
-                )),
-            ),
-        )
-
-        for title, item, test_fixed, test_unfixed in data:
-            card = Card.parseText(item)
-            fixed, unfixed = card.validate(doFix=False)
-            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
-
-
-    def test_mode_no_raise(self):
-
-        data = (
-            (
-                "OK",
-                """BEGIN:VCARD
-VERSION:3.0
-N:Thompson;Default;;;
-FN:Default Thompson
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
-item1.X-ABADR:us
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-END:VCARD
-""".replace("\n", "\r\n"),
-                """BEGIN:VCARD
-VERSION:3.0
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
- SA
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-FN:Default Thompson
-N:Thompson;Default;;;
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.X-ABADR:us
-END:VCARD
-""".replace("\n", "\r\n"),
-                set(),
-                set(),
-            ),
-            (
-                "Unfixable only",
-                """BEGIN:VCARD
-VERSION:3.0
-FN:Default Thompson
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
-item1.X-ABADR:us
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-END:VCARD
-""".replace("\n", "\r\n"),
-                """BEGIN:VCARD
-VERSION:3.0
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
- SA
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-FN:Default Thompson
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.X-ABADR:us
-END:VCARD
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VCARD] Missing or too many required property: N",
-                )),
-            ),
-        )
-
-        for title, test_old, test_new, test_fixed, test_unfixed in data:
-            card = Card.parseText(test_old)
-            fixed, unfixed = card.validate(doFix=False, doRaise=False)
-            self.assertEqual(str(card), test_new, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
-            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
-
-
-    def test_mode_raise(self):
-
-        data = (
-            (
-                "OK",
-                """BEGIN:VCARD
-VERSION:3.0
-N:Thompson;Default;;;
-FN:Default Thompson
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
-item1.X-ABADR:us
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-END:VCARD
-""".replace("\n", "\r\n"),
-                """BEGIN:VCARD
-VERSION:3.0
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
- SA
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-FN:Default Thompson
-N:Thompson;Default;;;
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.X-ABADR:us
-END:VCARD
-""".replace("\n", "\r\n"),
-                set(),
-                set(),
-                False,
-            ),
-            (
-                "Unfixable only",
-                """BEGIN:VCARD
-VERSION:3.0
-FN:Default Thompson
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
-item1.X-ABADR:us
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-END:VCARD
-""".replace("\n", "\r\n"),
-                """BEGIN:VCARD
-VERSION:3.0
-UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
-item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
- SA
-EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
-FN:Default Thompson
-TEL;type=WORK;type=pref:1-555-555-5555
-TEL;type=CELL:1-444-444-4444
-item1.X-ABADR:us
-END:VCARD
-""".replace("\n", "\r\n"),
-                set(),
-                set((
-                    "[VCARD] Missing or too many required property: N",
-                )),
-                True,
-            ),
-        )
-
-        for title, test_old, test_new, test_fixed, test_unfixed, test_raises in data:
-            card = Card.parseText(test_old)
-            if test_raises:
-                self.assertRaises(PyCalendarValidationError, card.validate, doFix=False, doRaise=True)
-            else:
-                try:
-                    fixed, unfixed = card.validate(doFix=False, doRaise=False)
-                except:
-                    self.fail(msg="Failed test: %s" % (title,))
-                self.assertEqual(str(card), test_new, msg="Failed test: %s" % (title,))
-                self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
-                self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))

Copied: PyCalendar/trunk/src/pycalendar/vcard/tests/test_validation.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/vcard/tests/test_validation.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/vcard/tests/test_validation.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/vcard/tests/test_validation.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,221 @@
+##
+#    Copyright (c) 2011-2012 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.exceptions import PyCalendarValidationError
+from pycalendar.vcard.card import Card
+import unittest
+
+class TestValidation(unittest.TestCase):
+
+    def test_basic(self):
+
+        data = (
+            (
+                "No problems",
+                """BEGIN:VCARD
+VERSION:3.0
+N:Thompson;Default;;;
+FN:Default Thompson
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
+item1.X-ABADR:us
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+END:VCARD
+""".replace("\n", "\r\n"),
+                set(),
+                set(),
+            ),
+            (
+                "No VERSION",
+                """BEGIN:VCARD
+VERSION:3.0
+FN:Default Thompson
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
+item1.X-ABADR:us
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+END:VCARD
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VCARD] Missing or too many required property: N",
+                )),
+            ),
+        )
+
+        for title, item, test_fixed, test_unfixed in data:
+            card = Card.parseText(item)
+            fixed, unfixed = card.validate(doFix=False)
+            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
+
+
+    def test_mode_no_raise(self):
+
+        data = (
+            (
+                "OK",
+                """BEGIN:VCARD
+VERSION:3.0
+N:Thompson;Default;;;
+FN:Default Thompson
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
+item1.X-ABADR:us
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+END:VCARD
+""".replace("\n", "\r\n"),
+                """BEGIN:VCARD
+VERSION:3.0
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
+ SA
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+FN:Default Thompson
+N:Thompson;Default;;;
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.X-ABADR:us
+END:VCARD
+""".replace("\n", "\r\n"),
+                set(),
+                set(),
+            ),
+            (
+                "Unfixable only",
+                """BEGIN:VCARD
+VERSION:3.0
+FN:Default Thompson
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
+item1.X-ABADR:us
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+END:VCARD
+""".replace("\n", "\r\n"),
+                """BEGIN:VCARD
+VERSION:3.0
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
+ SA
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+FN:Default Thompson
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.X-ABADR:us
+END:VCARD
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VCARD] Missing or too many required property: N",
+                )),
+            ),
+        )
+
+        for title, test_old, test_new, test_fixed, test_unfixed in data:
+            card = Card.parseText(test_old)
+            fixed, unfixed = card.validate(doFix=False, doRaise=False)
+            self.assertEqual(str(card), test_new, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
+            self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))
+
+
+    def test_mode_raise(self):
+
+        data = (
+            (
+                "OK",
+                """BEGIN:VCARD
+VERSION:3.0
+N:Thompson;Default;;;
+FN:Default Thompson
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
+item1.X-ABADR:us
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+END:VCARD
+""".replace("\n", "\r\n"),
+                """BEGIN:VCARD
+VERSION:3.0
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
+ SA
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+FN:Default Thompson
+N:Thompson;Default;;;
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.X-ABADR:us
+END:VCARD
+""".replace("\n", "\r\n"),
+                set(),
+                set(),
+                False,
+            ),
+            (
+                "Unfixable only",
+                """BEGIN:VCARD
+VERSION:3.0
+FN:Default Thompson
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;USA
+item1.X-ABADR:us
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+END:VCARD
+""".replace("\n", "\r\n"),
+                """BEGIN:VCARD
+VERSION:3.0
+UID:ED7A5AEC-AB19-4CE0-AD6A-2923A3E5C4E1:ABPerson
+item1.ADR;type=WORK;type=pref:;;1245 Test;Sesame Street;California;11111;U
+ SA
+EMAIL;type=INTERNET;type=WORK;type=pref:lthompson at example.com
+FN:Default Thompson
+TEL;type=WORK;type=pref:1-555-555-5555
+TEL;type=CELL:1-444-444-4444
+item1.X-ABADR:us
+END:VCARD
+""".replace("\n", "\r\n"),
+                set(),
+                set((
+                    "[VCARD] Missing or too many required property: N",
+                )),
+                True,
+            ),
+        )
+
+        for title, test_old, test_new, test_fixed, test_unfixed, test_raises in data:
+            card = Card.parseText(test_old)
+            if test_raises:
+                self.assertRaises(PyCalendarValidationError, card.validate, doFix=False, doRaise=True)
+            else:
+                try:
+                    fixed, unfixed = card.validate(doFix=False, doRaise=False)
+                except:
+                    self.fail(msg="Failed test: %s" % (title,))
+                self.assertEqual(str(card), test_new, msg="Failed test: %s" % (title,))
+                self.assertEqual(set(fixed), test_fixed, msg="Failed test: %s" % (title,))
+                self.assertEqual(set(unfixed), test_unfixed, msg="Failed test: %s" % (title,))

Deleted: PyCalendar/trunk/src/pycalendar/vcard/validation.py
===================================================================
--- PyCalendar/branches/server/src/pycalendar/vcard/validation.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/vcard/validation.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,23 +0,0 @@
-##
-#    Copyright (c) 2011-2012 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.validation import partial, PropertyValueChecks
-from pycalendar.vcard import definitions
-
-VCARD_VALUE_CHECKS = {
-    definitions.Property_VERSION: partial(PropertyValueChecks.stringValue, "3.0"),
-    definitions.Property_PROFILE: partial(PropertyValueChecks.stringValue, "VCARD"),
-}

Copied: PyCalendar/trunk/src/pycalendar/vcard/validation.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/vcard/validation.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/vcard/validation.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/vcard/validation.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,23 @@
+##
+#    Copyright (c) 2011-2012 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.validation import partial, PropertyValueChecks
+from pycalendar.vcard import definitions
+
+VCARD_VALUE_CHECKS = {
+    definitions.Property_VERSION: partial(PropertyValueChecks.stringValue, "3.0"),
+    definitions.Property_PROFILE: partial(PropertyValueChecks.stringValue, "VCARD"),
+}

Modified: PyCalendar/trunk/src/pycalendar/vevent.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/vevent.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/vevent.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,66 +14,76 @@
 #    limitations under the License.
 ##
 
-from component import PyCalendarComponent
-from componentrecur import PyCalendarComponentRecur
-from property import PyCalendarProperty
-import definitions
-import itipdefinitions
+from pycalendar import definitions
+from pycalendar import itipdefinitions
+from pycalendar.componentrecur import PyCalendarComponentRecur
+from pycalendar.icalendar.validation import ICALENDAR_VALUE_CHECKS
+from pycalendar.property import PyCalendarProperty
 
 class PyCalendarVEvent(PyCalendarComponentRecur):
 
-    sBeginDelimiter = definitions.cICalComponent_BEGINVEVENT
+    propertyCardinality_1 = (
+        definitions.cICalProperty_DTSTAMP,
+        definitions.cICalProperty_UID,
+    )
 
-    sEndDelimiter = definitions.cICalComponent_ENDVEVENT
+    propertyCardinality_0_1 = (
+        definitions.cICalProperty_CLASS,
+        definitions.cICalProperty_CREATED,
+        definitions.cICalProperty_DESCRIPTION,
+        definitions.cICalProperty_GEO,
+        definitions.cICalProperty_LAST_MODIFIED,
+        definitions.cICalProperty_LOCATION,
+        definitions.cICalProperty_ORGANIZER,
+        definitions.cICalProperty_PRIORITY,
+        definitions.cICalProperty_SEQUENCE,
+        # definitions.cICalProperty_STATUS, # Special fix done for multiple STATUS
+        definitions.cICalProperty_SUMMARY,
+        definitions.cICalProperty_TRANSP,
+        definitions.cICalProperty_URL,
+        definitions.cICalProperty_RECURRENCE_ID,
+        definitions.cICalProperty_RRULE,
+        definitions.cICalProperty_DTEND,
+        definitions.cICalProperty_DURATION,
+    )
 
-    @staticmethod
-    def getVBegin():
-        return PyCalendarVEvent.sBeginDelimiter
+    propertyValueChecks = ICALENDAR_VALUE_CHECKS
 
-    @staticmethod
-    def getVEnd():
-        return PyCalendarVEvent.sEndDelimiter
+    def __init__(self, parent=None):
+        super(PyCalendarVEvent, self).__init__(parent=parent)
+        self.mStatus = definitions.eStatus_VEvent_None
 
-    def __init__(self, calendar = None, copyit = None):
-        if calendar is not None:
-            super(PyCalendarVEvent, self).__init__(calendar=calendar)
-            self.mStatus = definitions.eStatus_VEvent_None
-        elif copyit is not None:
-            super(PyCalendarVEvent, self).__init__(copyit=copyit)
-            self.mStatus = copyit.mStatus
 
-    def clone_it(self):
-        return PyCalendarVEvent(copyit=self)
+    def duplicate(self, parent=None):
+        other = super(PyCalendarVEvent, self).duplicate(parent=parent)
+        other.mStatus = self.mStatus
+        return other
 
+
     def getType(self):
-        return PyCalendarComponent.eVEVENT
+        return definitions.cICalComponent_VEVENT
 
-    def getBeginDelimiter(self):
-        return PyCalendarVEvent.sBeginDelimiter
 
-    def getEndDelimiter(self):
-        return PyCalendarVEvent.sEndDelimiter
-
     def getMimeComponentName(self):
         return itipdefinitions.cICalMIMEComponent_VEVENT
 
+
     def addComponent(self, comp):
         # We can embed the alarm components only
-        if comp.getType() == PyCalendarComponent.eVALARM:
-            if self.mEmbedded is None:
-                self.mEmbedded = []
-            self.mEmbedded.append(comp)
-            comp.setEmbedder(self)
-            return True
+        if comp.getType() == definitions.cICalComponent_VALARM:
+            super(PyCalendarVEvent, self).addComponent(comp)
         else:
-            return False
+            raise ValueError
 
+
     def getStatus(self):
         return self.mStatus
 
+
     def setStatus(self, status):
         self.mStatus = status
 
+
     def finalise(self):
         # Do inherited
         super(PyCalendarVEvent, self).finalise()
@@ -87,6 +97,40 @@
             elif temp == definitions.cICalProperty_STATUS_CANCELLED:
                 self.mStatus = definitions.eStatus_VEvent_Cancelled
 
+
+    def validate(self, doFix=False):
+        """
+        Validate the data in this component and optionally fix any problems, else raise. If
+        loggedProblems is not None it must be a C{list} and problem descriptions are appended
+        to that.
+        """
+
+        fixed, unfixed = super(PyCalendarVEvent, self).validate(doFix)
+
+        # Extra constraint: if METHOD not present, DTSTART must be
+        if self.mParentComponent and not self.mParentComponent.hasProperty(definitions.cICalProperty_METHOD):
+            if not self.hasProperty(definitions.cICalProperty_DTSTART):
+                # Cannot fix a missing required property
+                logProblem = "[%s] Missing required property: %s" % (self.getType(), definitions.cICalProperty_DTSTART,)
+                unfixed.append(logProblem)
+
+        # Extra constraint: only one of DTEND or DURATION
+        if self.hasProperty(definitions.cICalProperty_DTEND) and self.hasProperty(definitions.cICalProperty_DURATION):
+            # Fix by removing the DTEND
+            logProblem = "[%s] Properties must not both be present: %s, %s" % (
+                self.getType(),
+                definitions.cICalProperty_DTEND,
+                definitions.cICalProperty_DURATION,
+            )
+            if doFix:
+                self.removeProperties(definitions.cICalProperty_DTEND)
+                fixed.append(logProblem)
+            else:
+                unfixed.append(logProblem)
+
+        return fixed, unfixed
+
+
     # Editing
     def editStatus(self, status):
         # Only if it is different
@@ -113,3 +157,13 @@
             if value is not None:
                 prop = PyCalendarProperty(definitions.cICalProperty_STATUS, value)
                 self.addProperty(prop)
+
+
+    def sortedPropertyKeyOrder(self):
+        return (
+            definitions.cICalProperty_UID,
+            definitions.cICalProperty_RECURRENCE_ID,
+            definitions.cICalProperty_DTSTART,
+            definitions.cICalProperty_DURATION,
+            definitions.cICalProperty_DTEND,
+        )

Modified: PyCalendar/trunk/src/pycalendar/vfreebusy.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/vfreebusy.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/vfreebusy.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,66 +14,66 @@
 #    limitations under the License.
 ##
 
-from component import PyCalendarComponent
-from datetime import PyCalendarDateTime
-from freebusy import PyCalendarFreeBusy
-from period import PyCalendarPeriod
-from periodvalue import PyCalendarPeriodValue
-from property import PyCalendarProperty
-from value import PyCalendarValue
-import definitions
-import itipdefinitions
+from pycalendar import definitions
+from pycalendar import itipdefinitions
+from pycalendar.component import PyCalendarComponent
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.freebusy import PyCalendarFreeBusy
+from pycalendar.icalendar.validation import ICALENDAR_VALUE_CHECKS
+from pycalendar.period import PyCalendarPeriod
+from pycalendar.periodvalue import PyCalendarPeriodValue
+from pycalendar.property import PyCalendarProperty
+from pycalendar.value import PyCalendarValue
 
 class PyCalendarVFreeBusy(PyCalendarComponent):
 
-    sBeginDelimiter = definitions.cICalComponent_BEGINVFREEBUSY
+    propertyCardinality_1 = (
+        definitions.cICalProperty_DTSTAMP,
+        definitions.cICalProperty_UID,
+    )
 
-    sEndDelimiter = definitions.cICalComponent_ENDVFREEBUSY
+    propertyCardinality_0_1 = (
+        definitions.cICalProperty_CONTACT,
+        definitions.cICalProperty_DTSTART,
+        definitions.cICalProperty_DTEND,
+        definitions.cICalProperty_ORGANIZER,
+        definitions.cICalProperty_URL,
+    )
 
-    @staticmethod
-    def getVBegin():
-        return PyCalendarVFreeBusy.sBeginDelimiter
+    propertyValueChecks = ICALENDAR_VALUE_CHECKS
 
-    @staticmethod
-    def gGetVEnd():
-        return PyCalendarVFreeBusy.sEndDelimiter
+    def __init__(self, parent=None):
+        super(PyCalendarVFreeBusy, self).__init__(parent=parent)
+        self.mStart = PyCalendarDateTime()
+        self.mHasStart = False
+        self.mEnd = PyCalendarDateTime()
+        self.mHasEnd = False
+        self.mDuration = False
+        self.mCachedBusyTime = False
+        self.mSpanPeriod = None
+        self.mBusyTime = None
 
-    def __init__(self, calendar=None, copyit=None):
-        if calendar is not None:
-            super(PyCalendarVFreeBusy, self).__init__(calendar=calendar)
-            self.mStart = PyCalendarDateTime()
-            self.mHasStart = False
-            self.mEnd = PyCalendarDateTime()
-            self.mHasEnd = False
-            self.mDuration = False
-            self.mCachedBusyTime = False
-            self.mSpanPeriod = None
-            self.mBusyTime = None
-        elif copyit is not None:
-            super(PyCalendarVFreeBusy, self).__init__(copyit=copyit)
-            self.mStart = PyCalendarDateTime(copyit.mStart)
-            self.mHasStart = copyit.mHasStart
-            self.mEnd = PyCalendarDateTime(copyit.mEnd)
-            self.mHasEnd = copyit.mHasEnd
-            self.mDuration = copyit.mDuration
-            self.mCachedBusyTime = False
-            self.mBusyTime = None
 
-    def clone_it(self):
-        return PyCalendarVFreeBusy(self)
+    def duplicate(self, parent=None):
+        other = super(PyCalendarVFreeBusy, self).duplicate(parent=parent)
+        other.mStart = self.mStart.duplicate()
+        other.mHasStart = self.mHasStart
+        other.mEnd = self.mEnd.duplicate()
+        other.mHasEnd = self.mHasEnd
+        other.mDuration = self.mDuration
+        other.mCachedBusyTime = False
+        other.mBusyTime = None
+        return other
 
+
     def getType(self):
-        return PyCalendarComponent.eVFREEBUSY
+        return definitions.cICalComponent_VFREEBUSY
 
-    def getBeginDelimiter(self):
-        return PyCalendarVFreeBusy.sBeginDelimiter
 
-    def getEndDelimiter(self):
-        return PyCalendarVFreeBusy.sEndDelimiter
-
     def getMimeComponentName(self):
         return itipdefinitions.cICalMIMEComponent_VFREEBUSY
 
+
     def finalise(self):
         # Do inherited
         super(PyCalendarVFreeBusy, self).finalise()
@@ -101,14 +101,15 @@
             self.mDuration = False
             self.mEnd = temp
 
+
     def fixStartEnd(self):
         # End is always greater than start if start exists
         if self.mHasStart and self.mEnd <= self.mStart:
             # Use the start
-            self.mEnd = PyCalendarDateTime(copyit=self.mStart)
+            self.mEnd = self.mStart.duplicate()
             self.mDuration = False
 
-            # Adjust to approriate non-inclusive end point
+            # Adjust to appropiate non-inclusive end point
             if self.mStart.isDateOnly():
                 self.mEnd.offsetDay(1)
 
@@ -119,27 +120,35 @@
                 self.mEnd.offsetDay(1)
                 self.mEnd.setHHMMSS(0, 0, 0)
 
+
     def getStart(self):
         return self.mStart
 
+
     def hasStart(self):
         return self.mHasStart
 
+
     def getEnd(self):
         return self.mEnd
 
+
     def hasEnd(self):
         return self.mHasEnd
 
+
     def useDuration(self):
         return self.mDuration
 
+
     def getSpanPeriod(self):
         return self.mSpanPeriod
 
+
     def getBusyTime(self):
         return self.mBusyTime
 
+
     def editTiming(self):
         # Updated cached values
         self.mHasStart = False
@@ -153,6 +162,7 @@
         self.removeProperties(definitions.cICalProperty_DTEND)
         self.removeProperties(definitions.cICalProperty_DURATION)
 
+
     def editTimingStartEnd(self, start, end):
         # Updated cached values
         self.mHasStart = self.mHasEnd = True
@@ -171,12 +181,13 @@
         self.addProperty(prop)
 
         # If its an all day event and the end one day after the start, ignore it
-        temp = PyCalendarDateTime(copyit=start)
+        temp = start.duplicate()
         temp.offsetDay(1)
         if not start.isDateOnly() or end != temp:
             prop = PyCalendarProperty(definitions.cICalProperty_DTEND, end)
             self.addProperty(prop)
 
+
     def editTimingStartDuration(self, start, duration):
         # Updated cached values
         self.mHasStart = True
@@ -201,6 +212,7 @@
             prop = PyCalendarProperty(definitions.cICalProperty_DURATION, duration)
             self.addProperty(prop)
 
+
     # Generating info
     def expandPeriodComp(self, period, list):
         # Cache the busy-time details if not done already
@@ -211,6 +223,7 @@
         if (self.mBusyTime is not None) and period.isPeriodOverlap(self.mSpanPeriod):
             list.append(self)
 
+
     def expandPeriodFB(self, period, list):
         # Cache the busy-time details if not done already
         if not self.mCachedBusyTime:
@@ -221,6 +234,7 @@
             for fb in self.mBusyTime:
                 list.append(PyCalendarFreeBusy(fb))
 
+
     def cacheBusyTime(self):
 
         # Clear out any existing cache
@@ -277,12 +291,12 @@
                         period = None
                         if isinstance(o, PyCalendarPeriodValue):
                             period = o
-                        
+
                         # Double-check type
                         if period is not None:
 
                             self.mBusyTime.append(PyCalendarFreeBusy(type, period.getValue()))
-                            
+
                             if len(self.mBusyTime) == 1:
 
                                 min_start = period.getValue().getStart()
@@ -302,9 +316,8 @@
 
         else:
 
-        
             # Sort the list by period
-            self.mBusyTime.sort(cmp=lambda x,y: x.getPeriod().getStart().compareDateTime(y.getPeriod().getStart()))
+            self.mBusyTime.sort(cmp=lambda x, y: x.getPeriod().getStart().compareDateTime(y.getPeriod().getStart()))
 
             # Determine range
             start = PyCalendarDateTime()
@@ -317,7 +330,16 @@
                 end = self.mEnd
             else:
                 end = max_end
-            
+
             self.mSpanPeriod = PyCalendarPeriod(start, end)
-        
+
         self.mCachedBusyTime = True
+
+
+    def sortedPropertyKeyOrder(self):
+        return (
+            definitions.cICalProperty_UID,
+            definitions.cICalProperty_DTSTART,
+            definitions.cICalProperty_DURATION,
+            definitions.cICalProperty_DTEND,
+        )

Modified: PyCalendar/trunk/src/pycalendar/vjournal.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/vjournal.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/vjournal.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,45 +14,57 @@
 #    limitations under the License.
 ##
 
-from component import PyCalendarComponent
-from componentrecur import PyCalendarComponentRecur
-import definitions
-import itipdefinitions
+from pycalendar import definitions
+from pycalendar import itipdefinitions
+from pycalendar.componentrecur import PyCalendarComponentRecur
+from pycalendar.icalendar.validation import ICALENDAR_VALUE_CHECKS
 
 class PyCalendarVJournal(PyCalendarComponentRecur):
 
-    sBeginDelimiter = definitions.cICalComponent_BEGINVJOURNAL
+    propertyCardinality_1 = (
+        definitions.cICalProperty_DTSTAMP,
+        definitions.cICalProperty_UID,
+    )
 
-    sEndDelimiter = definitions.cICalComponent_ENDVJOURNAL
+    propertyCardinality_0_1 = (
+        definitions.cICalProperty_CLASS,
+        definitions.cICalProperty_CREATED,
+        definitions.cICalProperty_DTSTART,
+        definitions.cICalProperty_LAST_MODIFIED,
+        definitions.cICalProperty_ORGANIZER,
+        definitions.cICalProperty_RECURRENCE_ID,
+        definitions.cICalProperty_SEQUENCE,
+        # definitions.cICalProperty_STATUS, # Special fix done for multiple STATUS
+        definitions.cICalProperty_SUMMARY,
+        definitions.cICalProperty_URL,
+        definitions.cICalProperty_RRULE,
+    )
 
-    @staticmethod
-    def getVBegin():
-        return PyCalendarVJournal.sBeginDelimiter
+    propertyValueChecks = ICALENDAR_VALUE_CHECKS
 
-    @staticmethod
-    def getVEnd():
-        return PyCalendarVJournal.sEndDelimiter
+    def __init__(self, parent=None):
+        super(PyCalendarVJournal, self).__init__(parent=parent)
 
-    def __init__(self, calendar=None, copyit=None):
-        if calendar is not None:
-            super(PyCalendarVJournal, self).__init__(calendar=calendar)
-        elif copyit is not None:
-            super(PyCalendarVJournal, self).__init__(copyit=copyit)
 
-    def clone_it(self):
-        return PyCalendarVJournal(copyit=self)
+    def duplicate(self, parent=None):
+        return super(PyCalendarVJournal, self).duplicate(parent=parent)
 
+
     def getType(self):
-        return PyCalendarComponent.eVJOURNAL
+        return definitions.cICalComponent_VJOURNAL
 
-    def getBeginDelimiter(self):
-        return PyCalendarVJournal.sBeginDelimiter
 
-    def getEndDelimiter(self):
-        return PyCalendarVJournal.sEndDelimiter
-
     def getMimeComponentName(self):
         return itipdefinitions.cICalMIMEComponent_VJOURNAL
 
+
     def finalise(self):
         super(PyCalendarVJournal, self).finalise()
+
+
+    def sortedPropertyKeyOrder(self):
+        return (
+            definitions.cICalProperty_UID,
+            definitions.cICalProperty_RECURRENCE_ID,
+            definitions.cICalProperty_DTSTART,
+        )

Modified: PyCalendar/trunk/src/pycalendar/vtimezone.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/vtimezone.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/vtimezone.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,65 +14,65 @@
 #    limitations under the License.
 ##
 
-from component import PyCalendarComponent
-from datetime import PyCalendarDateTime
-import definitions
+from pycalendar import definitions
+from pycalendar.component import PyCalendarComponent
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.icalendar.validation import ICALENDAR_VALUE_CHECKS
 
 class PyCalendarVTimezone(PyCalendarComponent):
 
-    sBeginDelimiter = definitions.cICalComponent_BEGINVTIMEZONE
+    propertyCardinality_1 = (
+        definitions.cICalProperty_TZID,
+    )
 
-    sEndDelimiter = definitions.cICalComponent_ENDVTIMEZONE
+    propertyCardinality_0_1 = (
+        definitions.cICalProperty_LAST_MODIFIED,
+        definitions.cICalProperty_TZURL,
+    )
 
-    @staticmethod
-    def getVBegin():
-        return PyCalendarVTimezone.sBeginDelimiter
+    propertyValueChecks = ICALENDAR_VALUE_CHECKS
 
-    @staticmethod
-    def getVEnd():
-        return PyCalendarVTimezone.sEndDelimiter
+    UTCOFFSET_CACHE_MAX_ENTRIES = 100000
 
-    def __init__(self, calendar=None, copyit=None):
-        if calendar is not None:
-            super(PyCalendarVTimezone, self).__init__(calendar=calendar)
-            self.mID = ""
-            self.mSortKey = 1
-        elif copyit is not None:
-            super(PyCalendarVTimezone, self).__init__(copyit=copyit)
-            self.mID = copyit.mID
-            self.mSortKey = copyit.mSortKey
+    sortSubComponents = False
 
-    def clone_it(self):
-        return PyCalendarVTimezone(copyit=self)
+    def __init__(self, parent=None):
+        super(PyCalendarVTimezone, self).__init__(parent=parent)
+        self.mID = ""
+        self.mUTCOffsetSortKey = None
+        self.mCachedExpandAllMaxYear = None
+        self.mCachedOffsets = None
 
+
+    def duplicate(self, parent=None):
+        other = super(PyCalendarVTimezone, self).duplicate(parent=parent)
+        other.mID = self.mID
+        other.mUTCOffsetSortKey = self.mUTCOffsetSortKey
+        return other
+
+
     def getType(self):
-        return PyCalendarComponent.eVTIMEZONE
+        return definitions.cICalComponent_VTIMEZONE
 
-    def getBeginDelimiter(self):
-        return PyCalendarVTimezone.sBeginDelimiter
 
-    def getEndDelimiter(self):
-        return PyCalendarVTimezone.sEndDelimiter
-
     def getMimeComponentName(self):
         # Cannot be sent as a separate MIME object
         return None
 
+
     def addComponent(self, comp):
         # We can embed the timezone components only
-        if ((comp.getType() == PyCalendarComponent.eVTIMEZONESTANDARD)
-                or (comp.getType() == PyCalendarComponent.eVTIMEZONEDAYLIGHT)):
-            if self.mEmbedded is None:
-                self.mEmbedded = []
-            self.mEmbedded.append(comp)
-            comp.setEmbedder(self)
-            return True
+        if ((comp.getType() == definitions.cICalComponent_STANDARD)
+                or (comp.getType() == definitions.cICalComponent_DAYLIGHT)):
+            super(PyCalendarVTimezone, self).addComponent(comp)
         else:
-            return False
+            raise ValueError
 
+
     def getMapKey(self):
         return self.mID
 
+
     def finalise(self):
         # Get TZID
         temp = self.loadValueString(definitions.cICalProperty_TZID)
@@ -80,45 +80,100 @@
             self.mID = temp
 
         # Sort sub-components by DTSTART
-        if self.mEmbedded is not None:
-            self.mEmbedded.sort(key=lambda x:x.getStart())
+        self.mComponents.sort(key=lambda x: x.getStart())
 
         # Do inherited
         super(PyCalendarVTimezone, self).finalise()
 
+
+    def validate(self, doFix=False):
+        """
+        Validate the data in this component and optionally fix any problems, else raise. If
+        loggedProblems is not None it must be a C{list} and problem descriptions are appended
+        to that.
+        """
+
+        fixed, unfixed = super(PyCalendarVTimezone, self).validate(doFix)
+
+        # Must have at least one STANDARD or DAYLIGHT sub-component
+        for component in self.mComponents:
+            if component.getType() in (definitions.cICalComponent_STANDARD, definitions.cICalComponent_DAYLIGHT):
+                break
+        else:
+            # Cannot fix a missing required component
+            logProblem = "[%s] At least one component must be present: %s or %s" % (
+                self.getType(),
+                definitions.cICalComponent_STANDARD,
+                definitions.cICalComponent_DAYLIGHT,
+            )
+            unfixed.append(logProblem)
+
+        return fixed, unfixed
+
+
     def getID(self):
         return self.mID
 
-    def getSortKey(self):
-        if self.mSortKey == 1:
+
+    def getUTCOffsetSortKey(self):
+        if self.mUTCOffsetSortKey is None:
             # Take time from first element
-            if (self.mEmbedded is not None) and (len(self.mEmbedded) > 0):
+            if len(self.mComponents) > 0:
                 # Initial offset provides the primary key
-                utc_offset1 = self.mEmbedded[0].getUTCOffset()
+                utc_offset1 = self.mComponents[0].getUTCOffset()
 
                 # Presence of secondary is the next key
                 utc_offset2 = utc_offset1
-                if len(self.mEmbedded) > 1:
-                    utc_offset2 = self.mEmbedded[1].getUTCOffset()
+                if len(self.mComponents) > 1:
+                    utc_offset2 = self.mComponents[1].getUTCOffset()
 
                 # Create key
-                self.mSortKey = (utc_offset1 + utc_offset2) / 2
+                self.mUTCOffsetSortKey = (utc_offset1 + utc_offset2) / 2
             else:
-                self.mSortKey = 0
+                self.mUTCOffsetSortKey = 0
 
-        return self.mSortKey
+        return self.mUTCOffsetSortKey
 
+
     def getTimezoneOffsetSeconds(self, dt):
-        # Get the closet matching element to the time
-        found = self.findTimezoneElement(dt)
+        """
+        Caching implementation of expansion. We cache the entire set of transitions up to one year ahead
+        of the requested time.
+        """
 
-        # Return it
-        if found is None:
-            return 0
-        else:
-            # Get its offset
-            return found.getUTCOffset()
+        # Need to make the incoming date-time relative to the DTSTART in the
+        # timezone component for proper comparison.
+        # This means making the incoming date-time a floating (no timezone)
+        # item
+        temp = dt.duplicate()
+        temp.setTimezoneID(None)
 
+        # Check whether we need to recache
+        if self.mCachedExpandAllMaxYear is None or temp.mYear >= self.mCachedExpandAllMaxYear:
+            cacheMax = temp.duplicate()
+            cacheMax.setHHMMSS(0, 0, 0)
+            cacheMax.offsetYear(2)
+            cacheMax.setMonth(1)
+            cacheMax.setDay(1)
+            self.mCachedExpandAll = self.expandAll(None, cacheMax)
+            self.mCachedExpandAllMaxYear = cacheMax.mYear
+            self.mCachedOffsets = {}
+
+        # Now search for the transition just below the time we want
+        if len(self.mCachedExpandAll):
+            cacheKey = (temp.mYear, temp.mMonth, temp.mDay, temp.mHours, temp.mMinutes,)
+            i = self.mCachedOffsets.get(cacheKey)
+            if i is None:
+                i = PyCalendarVTimezone.tuple_bisect_right(self.mCachedExpandAll, temp)
+                if len(self.mCachedOffsets) >= self.UTCOFFSET_CACHE_MAX_ENTRIES:
+                    self.mCachedOffsets = {}
+                self.mCachedOffsets[cacheKey] = i
+            if i != 0:
+                return self.mCachedExpandAll[i - 1][2]
+
+        return 0
+
+
     def getTimezoneDescriptor(self, dt):
         result = ""
 
@@ -127,7 +182,7 @@
 
         # Get it
         if found is not None:
-            if found.getTZName().length() == 0:
+            if len(found.getTZName()) == 0:
                 tzoffset = found.getUTCOffset()
                 negative = False
                 if tzoffset < 0:
@@ -149,15 +204,35 @@
 
         return result
 
+
     def mergeTimezone(self, tz):
         pass
 
+
+    @staticmethod
+    def tuple_bisect_right(a, x):
+        """
+        Same as bisect_right except that the values being compared are the first elements
+        of a tuple.
+        """
+
+        lo = 0
+        hi = len(a)
+        while lo < hi:
+            mid = (lo + hi) // 2
+            if x < a[mid][0]:
+                hi = mid
+            else:
+                lo = mid + 1
+        return lo
+
+
     def findTimezoneElement(self, dt):
         # Need to make the incoming date-time relative to the DTSTART in the
         # timezone component for proper comparison.
         # This means making the incoming date-time a floating (no timezone)
         # item
-        temp = PyCalendarDateTime(copyit=dt)
+        temp = dt.duplicate()
         temp.setTimezoneID(None)
 
         # Had to rework this because some VTIMEZONEs have sub-components where the DST instances are interleaved. That
@@ -167,38 +242,44 @@
         found = None
         dt_found = PyCalendarDateTime()
 
-        if self.mEmbedded is not None:
-            for item in self.mEmbedded:
-                dt_item = item.expandBelow(temp)
-                if temp >= dt_item:
-                    if found is not None:
-                        # Compare with the one previously cached and switch to this
-                        # one if newer
-                        if dt_item > dt_found:
-                            found = item
-                            dt_found = dt_item
-                    else:
+        for item in self.mComponents:
+            dt_item = item.expandBelow(temp)
+            if temp >= dt_item:
+                if found is not None:
+                    # Compare with the one previously cached and switch to this
+                    # one if newer
+                    if dt_item > dt_found:
                         found = item
                         dt_found = dt_item
+                else:
+                    found = item
+                    dt_found = dt_item
 
         return found
 
-    def expandAll(self, start, end):
+
+    def expandAll(self, start, end, with_name=False):
         results = []
-        if self.mEmbedded is not None:
-            for item in self.mEmbedded:
-                results.extend(item.expandAll(start, end))
+        for item in self.mComponents:
+            results.extend(item.expandAll(start, end, with_name))
         results = [x for x in set(results)]
-        def sortit(e1, e2):
-            return PyCalendarDateTime.sort(e1[0], e2[0])
-        results.sort(cmp=sortit)
+        results.sort(key=lambda x: x[0].getPosixTime())
         return results
 
+
+    def sortedPropertyKeyOrder(self):
+        return (
+            definitions.cICalProperty_TZID,
+            definitions.cICalProperty_LAST_MODIFIED,
+            definitions.cICalProperty_TZURL,
+        )
+
+
     @staticmethod
-    def sortComparator(tz1, tz2):
-            sort1 = tz1.getSortKey()
-            sort2 = tz2.getSortKey()
-            if sort1 == sort2:
-                return tz1.getID().compareToIgnoreCase(tz2.getID())
-            else:
-                return (1, -1)[sort1 < sort2]
+    def sortByUTCOffsetComparator(tz1, tz2):
+        sort1 = tz1.getUTCOffsetSortKey()
+        sort2 = tz2.getUTCOffsetSortKey()
+        if sort1 == sort2:
+            return tz1.getID().compareToIgnoreCase(tz2.getID())
+        else:
+            return (1, -1)[sort1 < sort2]

Modified: PyCalendar/trunk/src/pycalendar/vtimezonedaylight.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/vtimezonedaylight.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/vtimezonedaylight.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,38 +14,18 @@
 #    limitations under the License.
 ##
 
-from component import PyCalendarComponent
+from pycalendar import definitions
 from vtimezoneelement import PyCalendarVTimezoneElement
-import definitions
 
 class PyCalendarVTimezoneDaylight(PyCalendarVTimezoneElement):
 
-    sBeginDelimiter = definitions.cICalComponent_BEGINDAYLIGHT
+    def __init__(self, parent=None):
+        super(PyCalendarVTimezoneDaylight, self).__init__(parent=parent)
 
-    sEndDelimiter = definitions.cICalComponent_ENDDAYLIGHT
 
-    @staticmethod
-    def getVBegin():
-        return PyCalendarVTimezoneDaylight.sBeginDelimiter
+    def duplicate(self, parent=None):
+        return super(PyCalendarVTimezoneDaylight, self).duplicate(parent=parent)
 
-    @staticmethod
-    def getVEnd():
-        return PyCalendarVTimezoneDaylight.sEndDelimiter
 
-    def __init__(self, calendar=None, copyit=None):
-        if calendar is not None:
-            super(PyCalendarVTimezoneDaylight, self).__init__(calendar=calendar)
-        elif copyit is not None:
-            super(PyCalendarVTimezoneDaylight, self).__init__(copyit=copyit)
-
-    def clone_it(self):
-        return PyCalendarVTimezoneDaylight(copyit=self)
-
     def getType(self):
-        return PyCalendarComponent.eVTIMEZONEDAYLIGHT
-
-    def getBeginDelimiter(self):
-        return PyCalendarVTimezoneDaylight.sBeginDelimiter
-
-    def getEndDelimiter(self):
-        return PyCalendarVTimezoneDaylight.sEndDelimiter
+        return definitions.cICalComponent_DAYLIGHT

Modified: PyCalendar/trunk/src/pycalendar/vtimezoneelement.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/vtimezoneelement.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/vtimezoneelement.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,44 +14,52 @@
 #    limitations under the License.
 ##
 
-from datetime import PyCalendarDateTime
-from period import PyCalendarPeriod
-from recurrenceset import PyCalendarRecurrenceSet
-from value import PyCalendarValue
-from vtimezone import PyCalendarVTimezone
-import definitions
+from bisect import bisect_right
+from pycalendar import definitions
+from pycalendar.component import PyCalendarComponent
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.icalendar.validation import ICALENDAR_VALUE_CHECKS
+from pycalendar.period import PyCalendarPeriod
+from pycalendar.recurrenceset import PyCalendarRecurrenceSet
+from pycalendar.value import PyCalendarValue
 
-class PyCalendarVTimezoneElement(PyCalendarVTimezone):
+class PyCalendarVTimezoneElement(PyCalendarComponent):
 
-    def __init__(self, calendar=None, dt=None, offset=None, copyit=None):
-        if calendar is not None and dt is None:
-            super(PyCalendarVTimezoneElement, self).__init__(calendar=calendar)
-            self.mStart = PyCalendarDateTime()
-            self.mTZName = ""
-            self.mUTCOffset = 0
-            self.mUTCOffsetFrom = 0
-            self.mRecurrences = PyCalendarRecurrenceSet()
-            self.mCachedExpandBelow = None
-            self.mCachedExpandBelowItems = None
-        elif calendar is not None and dt is not None:
-            super(PyCalendarVTimezoneElement, self).__init__(calendar=calendar)
-            self.mStart = dt
-            self.mTZName = ""
-            self.mUTCOffset = offset
-            self.mUTCOffsetFrom = 0
-            self.mRecurrences = PyCalendarRecurrenceSet()
-            self.mCachedExpandBelow = None
-            self.mCachedExpandBelowItems = None
-        elif copyit:
-            super(PyCalendarVTimezoneElement, self).__init__(copyit=copyit)
-            self.mStart = PyCalendarDateTime(copyit=copyit.mStart)
-            self.mTZName = copyit.mTZName
-            self.mUTCOffset = copyit.mUTCOffset
-            self.mUTCOffsetFrom = copyit.mUTCOffsetFrom
-            self.mRecurrences = copyit.mRecurrences
-            self.mCachedExpandBelow = None
-            self.mCachedExpandBelowItems = None
+    propertyCardinality_1 = (
+        definitions.cICalProperty_DTSTART,
+        definitions.cICalProperty_TZOFFSETTO,
+        definitions.cICalProperty_TZOFFSETFROM,
+    )
 
+    propertyCardinality_0_1 = (
+        definitions.cICalProperty_RRULE,
+    )
+
+    propertyValueChecks = ICALENDAR_VALUE_CHECKS
+
+    def __init__(self, parent=None, dt=None, offset=None):
+        super(PyCalendarVTimezoneElement, self).__init__(parent=parent)
+        self.mStart = dt if dt is not None else PyCalendarDateTime()
+        self.mTZName = ""
+        self.mUTCOffset = offset if offset is not None else 0
+        self.mUTCOffsetFrom = 0
+        self.mRecurrences = PyCalendarRecurrenceSet()
+        self.mCachedExpandBelow = None
+        self.mCachedExpandBelowItems = None
+
+
+    def duplicate(self, parent=None):
+        other = super(PyCalendarVTimezoneElement, self).duplicate(parent=parent)
+        other.mStart = self.mStart.duplicate()
+        other.mTZName = self.mTZName
+        other.mUTCOffset = self.mUTCOffset
+        other.mUTCOffsetFrom = self.mUTCOffsetFrom
+        other.mRecurrences = self.mRecurrences.duplicate()
+        other.mCachedExpandBelow = None
+        other.mCachedExpandBelowItems = None
+        return other
+
+
     def finalise(self):
         # Get DTSTART
         temp = self.loadValueDateTime(definitions.cICalProperty_DTSTART)
@@ -82,20 +90,32 @@
         # Do inherited
         super(PyCalendarVTimezoneElement, self).finalise()
 
+
+    def getSortKey(self):
+        """
+        We do not want these components sorted.
+        """
+        return ""
+
+
     def getStart(self):
         return self.mStart
 
+
     def getUTCOffset(self):
         return self.mUTCOffset
 
+
     def getUTCOffsetFrom(self):
         return self.mUTCOffsetFrom
 
+
     def getTZName(self):
         return self.mTZName
 
+
     def expandBelow(self, below):
-        
+
         # Look for recurrences
         if not self.mRecurrences.hasRecurrence() or self.mStart > below:
             # Return DTSTART even if it is newer
@@ -111,39 +131,37 @@
             # that the recurrence
             # cache is invalidated less frequently
 
-            temp = PyCalendarDateTime(copyit=below)
-            temp.setMonth(1)
-            temp.setDay(1)
-            temp.setHHMMSS(0, 0, 0)
-            temp.offsetYear(1)
+            temp = PyCalendarDateTime(below.getYear(), 1, 1, 0, 0, 0)
 
             # Use cache of expansion
             if self.mCachedExpandBelowItems is None:
                 self.mCachedExpandBelowItems = []
             if self.mCachedExpandBelow is None:
-                self.mCachedExpandBelow = PyCalendarDateTime(copyit=self.mStart)
+                self.mCachedExpandBelow = self.mStart.duplicate()
             if temp > self.mCachedExpandBelow:
                 self.mCachedExpandBelowItems = []
                 period = PyCalendarPeriod(self.mStart, temp)
                 self.mRecurrences.expand(self.mStart, period, self.mCachedExpandBelowItems, float_offset=self.mUTCOffsetFrom)
                 self.mCachedExpandBelow = temp
-            
+
             if len(self.mCachedExpandBelowItems) != 0:
                 # List comes back sorted so we pick the element just less than
                 # the dt value we want
-                for i in range(len(self.mCachedExpandBelowItems)):
-                    if self.mCachedExpandBelowItems[i] > below:
-                        if i != 0:
-                            return self.mCachedExpandBelowItems[i - 1]
-                        break
-                
-                # The last one in the list is the one we want
-                return self.mCachedExpandBelowItems[len(self.mCachedExpandBelowItems) - 1]
+                i = bisect_right(self.mCachedExpandBelowItems, below)
+                if i != 0:
+                    return self.mCachedExpandBelowItems[i - 1]
 
+                # The first one in the list is the one we want
+                return self.mCachedExpandBelowItems[0]
+
             return self.mStart
 
-    def expandAll(self, start, end):
 
+    def expandAll(self, start, end, with_name):
+
+        if start is None:
+            start = self.mStart
+
         # Ignore if there is no change in offset
         offsetto = self.loadValueInteger(definitions.cICalProperty_TZOFFSETTO, PyCalendarValue.VALUETYPE_UTC_OFFSET)
         offsetfrom = self.loadValueInteger(definitions.cICalProperty_TZOFFSETFROM, PyCalendarValue.VALUETYPE_UTC_OFFSET)
@@ -157,7 +175,10 @@
         elif not self.mRecurrences.hasRecurrence():
             # Return DTSTART even if it is newer
             if self.mStart >= start:
-                return ((self.mStart, offsetfrom, offsetto),)
+                result = (self.mStart, offsetfrom, offsetto,)
+                if with_name:
+                    result += (self.getTZName(),)
+                return (result,)
             else:
                 return ()
         else:
@@ -171,25 +192,28 @@
             # that the recurrence
             # cache is invalidated less frequently
 
-            temp = PyCalendarDateTime(copyit=end)
-            temp.setMonth(1)
-            temp.setDay(1)
-            temp.setHHMMSS(0, 0, 0)
-            temp.offsetYear(1)
+            temp = PyCalendarDateTime(end.getYear(), 1, 1, 0, 0, 0)
 
             # Use cache of expansion
             if self.mCachedExpandBelowItems is None:
                 self.mCachedExpandBelowItems = []
             if self.mCachedExpandBelow is None:
-                self.mCachedExpandBelow = PyCalendarDateTime(copyit=self.mStart)
+                self.mCachedExpandBelow = self.mStart.duplicate()
             if temp > self.mCachedExpandBelow:
                 self.mCachedExpandBelowItems = []
-                period = PyCalendarPeriod(start, end)
+                period = PyCalendarPeriod(self.mStart, end)
                 self.mRecurrences.expand(self.mStart, period, self.mCachedExpandBelowItems, float_offset=self.mUTCOffsetFrom)
                 self.mCachedExpandBelow = temp
-            
+
             if len(self.mCachedExpandBelowItems) != 0:
-                # The last one in the list is the one we want
-                return [(dt, offsetfrom, offsetto,) for dt in self.mCachedExpandBelowItems]
+                # Return them all within the range
+                results = []
+                for dt in self.mCachedExpandBelowItems:
+                    if dt >= start and dt < end:
+                        result = (dt, offsetfrom, offsetto,)
+                        if with_name:
+                            result += (self.getTZName(),)
+                        results.append(result)
+                return results
 
             return ()

Modified: PyCalendar/trunk/src/pycalendar/vtimezonestandard.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/vtimezonestandard.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/vtimezonestandard.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,38 +14,18 @@
 #    limitations under the License.
 ##
 
-from component import PyCalendarComponent
-from vtimezoneelement import PyCalendarVTimezoneElement
-import definitions
+from pycalendar import definitions
+from pycalendar.vtimezoneelement import PyCalendarVTimezoneElement
 
 class PyCalendarVTimezoneStandard(PyCalendarVTimezoneElement):
 
-    sBeginDelimiter = definitions.cICalComponent_BEGINSTANDARD
+    def __init__(self, parent=None):
+        super(PyCalendarVTimezoneStandard, self).__init__(parent=parent)
 
-    sEndDelimiter = definitions.cICalComponent_ENDSTANDARD
 
-    @staticmethod
-    def getVBegin():
-        return PyCalendarVTimezoneStandard.sBeginDelimiter
+    def duplicate(self, parent=None):
+        return super(PyCalendarVTimezoneStandard, self).duplicate(parent=parent)
 
-    @staticmethod
-    def getVEnd():
-        return PyCalendarVTimezoneStandard.sEndDelimiter
 
-    def __init__(self, calendar=None, copyit=None):
-        if calendar is not None:
-            super(PyCalendarVTimezoneStandard, self).__init__(calendar=calendar)
-        elif copyit is not None:
-            super(PyCalendarVTimezoneStandard, self).__init__(copyit=copyit)
-
-    def clone_it(self):
-        return PyCalendarVTimezoneStandard(copyit=self)
-
     def getType(self):
-        return PyCalendarComponent.eVTIMEZONESTANDARD
-
-    def getBeginDelimiter(self):
-        return PyCalendarVTimezoneStandard.sBeginDelimiter
-
-    def getEndDelimiter(self):
-        return PyCalendarVTimezoneStandard.sEndDelimiter
+        return definitions.cICalComponent_STANDARD

Modified: PyCalendar/trunk/src/pycalendar/vtodo.py
===================================================================
--- PyCalendar/trunk/src/pycalendar/vtodo.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/pycalendar/vtodo.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,12 +1,12 @@
 ##
-#    Copyright (c) 2007 Cyrus Daboo. All rights reserved.
-#    
+#    Copyright (c) 2007-2012 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.
@@ -14,40 +14,23 @@
 #    limitations under the License.
 ##
 
+from pycalendar import definitions
+from pycalendar import itipdefinitions
+from pycalendar.componentrecur import PyCalendarComponentRecur
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.icalendar.validation import ICALENDAR_VALUE_CHECKS
+from pycalendar.property import PyCalendarProperty
 import cStringIO as StringIO
 
-from component import PyCalendarComponent
-from componentrecur import PyCalendarComponentRecur
-from datetime import PyCalendarDateTime
-from property import PyCalendarProperty
-import definitions
-import itipdefinitions
-
 class PyCalendarVToDo(PyCalendarComponentRecur):
 
     OVERDUE = 0
-
-    DUE_NOW= 1
-
+    DUE_NOW = 1
     DUE_LATER = 2
-
     DONE = 3
+    CANCELLED = 4
 
-    CANCELLED= 4
-
-    sBeginDelimiter = definitions.cICalComponent_BEGINVTODO
-
-    sEndDelimiter = definitions.cICalComponent_ENDVTODO
-
     @staticmethod
-    def getVBegin():
-        return PyCalendarVToDo.sBeginDelimiter
-
-    @staticmethod
-    def getVEnd():
-        return PyCalendarVToDo.sEndDelimiter
-
-    @staticmethod
     def sort_for_display(e1, e2):
         s1 = e1.getMaster()
         s2 = e2.getMaster()
@@ -100,54 +83,78 @@
         # Just use start time - older ones at the top
         return s1.mStart < s2.mStart
 
-    def __init__(self, calendar=None, copyit=None):
-        if calendar is not None:
-            super(PyCalendarVToDo, self).__init__(calendar=calendar)
-            self.mPriority = 0
-            self.mStatus = definitions.eStatus_VToDo_None
-            self.mPercentComplete = 0
-            self.mCompleted = PyCalendarDateTime()
-            self.mHasCompleted = False
-        elif copyit is not None:
-            super(PyCalendarVToDo, self).__init__(copyit=copyit)
-            self.mPriority = copyit.mPriority
-            self.mStatus = copyit.mStatus
-            self.mPercentComplete = copyit.mPercentComplete
-            self.mCompleted = PyCalendarDateTime(copyit=copyit.mCompleted)
-            self.mHasCompleted = copyit.mHasCompleted
+    propertyCardinality_1 = (
+        definitions.cICalProperty_DTSTAMP,
+        definitions.cICalProperty_UID,
+    )
 
-    def clone_it(self):
-        return PyCalendarVToDo(copyit=self)
+    propertyCardinality_0_1 = (
+        definitions.cICalProperty_CLASS,
+        definitions.cICalProperty_COMPLETED,
+        definitions.cICalProperty_CREATED,
+        definitions.cICalProperty_DESCRIPTION,
+        definitions.cICalProperty_DTSTART,
+        definitions.cICalProperty_GEO,
+        definitions.cICalProperty_LAST_MODIFIED,
+        definitions.cICalProperty_LOCATION,
+        definitions.cICalProperty_ORGANIZER,
+        definitions.cICalProperty_PERCENT_COMPLETE,
+        definitions.cICalProperty_PRIORITY,
+        definitions.cICalProperty_RECURRENCE_ID,
+        definitions.cICalProperty_SEQUENCE,
+        # definitions.cICalProperty_STATUS, # Special fix done for multiple STATUS
+        definitions.cICalProperty_SUMMARY,
+        definitions.cICalProperty_URL,
+        definitions.cICalProperty_RRULE,
+        definitions.cICalProperty_DUE,
+        definitions.cICalProperty_DURATION,
+    )
 
+    propertyValueChecks = ICALENDAR_VALUE_CHECKS
+
+    def __init__(self, parent=None):
+        super(PyCalendarVToDo, self).__init__(parent=parent)
+        self.mPriority = 0
+        self.mStatus = definitions.eStatus_VToDo_None
+        self.mPercentComplete = 0
+        self.mCompleted = PyCalendarDateTime()
+        self.mHasCompleted = False
+
+
+    def duplicate(self, parent=None):
+        other = super(PyCalendarVToDo, self).duplicate(parent=parent)
+        other.mPriority = self.mPriority
+        other.mStatus = self.mStatus
+        other.mPercentComplete = self.mPercentComplete
+        other.mCompleted = self.mCompleted.duplicate()
+        other.mHasCompleted = self.mHasCompleted
+        return other
+
+
     def getType(self):
-        return PyCalendarComponent.eVTODO
+        return definitions.cICalComponent_VTODO
 
-    def getBeginDelimiter(self):
-        return PyCalendarVToDo.sBeginDelimiter
 
-    def getEndDelimiter(self):
-        return PyCalendarVToDo.sEndDelimiter
-
     def getMimeComponentName(self):
         return itipdefinitions.cICalMIMEComponent_VTODO
 
+
     def addComponent(self, comp):
         # We can embed the alarm components only
-        if comp.getType() == PyCalendarComponent.eVALARM:
-            if self.mEmbedded is None:
-                self.mEmbedded = []
-            self.mEmbedded.append(comp)
-            comp.setEmbedder(self)
-            return True
+        if comp.getType() == definitions.cICalComponent_VALARM:
+            super(PyCalendarVToDo, self).addComponent(comp)
         else:
-            return False
+            raise ValueError
 
+
     def getStatus(self):
         return self.mStatus
 
+
     def setStatus(self, status):
         self.mStatus = status
 
+
     def getStatusText(self):
         sout = StringIO()
 
@@ -188,6 +195,7 @@
 
         return sout.toString()
 
+
     def getCompletionState(self):
         if self.mStatus in (definitions.eStatus_VToDo_NeedsAction, definitions.eStatus_VToDo_InProcess):
             if self.hasEnd():
@@ -207,18 +215,23 @@
         elif self.mStatus == definitions.eStatus_VToDo_Cancelled:
             return PyCalendarVToDo.CANCELLED
 
+
     def getPriority(self):
         return self.mPriority
 
+
     def setPriority(self, priority):
         self.mPriority = priority
 
+
     def getCompleted(self):
         return self.mCompleted
 
+
     def hasCompleted(self):
         return self.mHasCompleted
 
+
     def finalise(self):
         # Do inherited
         super(PyCalendarVToDo, self).finalise()
@@ -261,6 +274,43 @@
         if self.mHasCompleted:
             self.mCompleted = temp
 
+
+    def validate(self, doFix=False):
+        """
+        Validate the data in this component and optionally fix any problems, else raise. If
+        loggedProblems is not None it must be a C{list} and problem descriptions are appended
+        to that.
+        """
+
+        fixed, unfixed = super(PyCalendarVToDo, self).validate(doFix)
+
+        # Extra constraint: only one of DUE or DURATION
+        if self.hasProperty(definitions.cICalProperty_DUE) and self.hasProperty(definitions.cICalProperty_DURATION):
+            # Fix by removing the DURATION
+            logProblem = "[%s] Properties must not both be present: %s, %s" % (
+                self.getType(),
+                definitions.cICalProperty_DUE,
+                definitions.cICalProperty_DURATION,
+            )
+            if doFix:
+                self.removeProperties(definitions.cICalProperty_DURATION)
+                fixed.append(logProblem)
+            else:
+                unfixed.append(logProblem)
+
+        # Extra constraint: DTSTART must be present if DURATION is present
+        if self.hasProperty(definitions.cICalProperty_DURATION) and not self.hasProperty(definitions.cICalProperty_DTSTART):
+            # Cannot fix this one
+            logProblem = "[%s] Property must be present: %s with %s" % (
+                self.getType(),
+                definitions.cICalProperty_DTSTART,
+                definitions.cICalProperty_DURATION,
+            )
+            unfixed.append(logProblem)
+
+        return fixed, unfixed
+
+
     # Editing
     def editStatus(self, status):
         # Only if it is different
@@ -291,14 +341,26 @@
             prop = PyCalendarProperty(definitions.cICalProperty_STATUS, value)
             self.addProperty(prop)
 
+
     def editCompleted(self, completed):
         # Remove existing COMPLETED item
         self.removeProperties(definitions.cICalProperty_COMPLETED)
         self.mHasCompleted = False
 
         # Always UTC
-        self.mCompleted = PyCalendarDateTime(copyit=completed)
+        self.mCompleted = completed.duplicate()
         self.mCompleted.adjustToUTC()
         self.mHasCompleted = True
         prop = PyCalendarProperty(definitions.cICalProperty_STATUS_COMPLETED, self.mCompleted)
         self.addProperty(prop)
+
+
+    def sortedPropertyKeyOrder(self):
+        return (
+            definitions.cICalProperty_UID,
+            definitions.cICalProperty_RECURRENCE_ID,
+            definitions.cICalProperty_DTSTART,
+            definitions.cICalProperty_DURATION,
+            definitions.cICalProperty_DUE,
+            definitions.cICalProperty_COMPLETED,
+        )

Copied: PyCalendar/trunk/src/pycalendar/vunknown.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/vunknown.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/vunknown.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/vunknown.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,66 @@
+##
+#    Copyright (c) 2011-2012 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 import definitions
+from pycalendar.component import PyCalendarComponent
+from pycalendar.icalendar.validation import ICALENDAR_VALUE_CHECKS
+import uuid
+
+class PyCalendarUnknownComponent(PyCalendarComponent):
+
+    propertyValueChecks = ICALENDAR_VALUE_CHECKS
+
+    def __init__(self, parent=None, comptype=""):
+        super(PyCalendarUnknownComponent, self).__init__(parent=parent)
+        self.mType = comptype
+        self.mMapKey = str(uuid.uuid4())
+
+
+    def duplicate(self, parent=None):
+        return super(PyCalendarUnknownComponent, self).duplicate(parent=parent, comptype=self.mType)
+
+
+    def getType(self):
+        return self.mType
+
+
+    def getBeginDelimiter(self):
+        return "BEGIN:" + self.mType
+
+
+    def getEndDelimiter(self):
+        return "END:" + self.mType
+
+
+    def getMimeComponentName(self):
+        return "unknown"
+
+
+    def getMapKey(self):
+        return self.mMapKey
+
+
+    def getSortKey(self):
+        """
+        We do not want unknown components sorted.
+        """
+        return ""
+
+
+    def sortedPropertyKeyOrder(self):
+        return (
+            definitions.cICalProperty_UID,
+        )

Copied: PyCalendar/trunk/src/pycalendar/xmldefs.py (from rev 10553, PyCalendar/branches/server/src/pycalendar/xmldefs.py)
===================================================================
--- PyCalendar/trunk/src/pycalendar/xmldefs.py	                        (rev 0)
+++ PyCalendar/trunk/src/pycalendar/xmldefs.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,104 @@
+##
+#    Copyright (c) 2007-2012 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 xml.etree.cElementTree as XML
+
+# iCalendar/vCard XML definitions
+
+iCalendar20_namespace = "urn:ietf:params:xml:ns:icalendar-2.0"
+
+icalendar = "icalendar"
+components = "components"
+properties = "properties"
+parameters = "parameters"
+
+value_binary = "binary"
+value_boolean = "boolean"
+value_cal_address = "cal-address"
+value_date = "date"
+value_date_time = "date-time"
+value_duration = "duration"
+value_integer = "integer"
+value_period = "period"
+value_recur = "recur"
+value_text = "text"
+value_unknown = "unknown"
+value_uri = "uri"
+value_utc_offset = "utc-offset"
+
+period_start = "start"
+period_end = "end"
+period_duration = "duration"
+
+recur_freq = "freq"
+recur_freq_secondly = "SECONDLY"
+recur_freq_minutely = "MINUTELY"
+recur_freq_hourly = "HOURLY"
+recur_freq_daily = "DAILY"
+recur_freq_weekly = "WEEKLY"
+recur_freq_monthly = "MONTHLY"
+recur_freq_yearly = "YEARLY"
+
+recur_count = "count"
+recur_until = "until"
+recur_interval = "interval"
+
+recur_bysecond = "bysecond"
+recur_byminute = "byminute"
+recur_byhour = "byhour"
+recur_byday = "byday"
+recur_bymonthday = "bymonthday"
+recur_byyearday = "byyearday"
+recur_byweekno = "byweekno"
+recur_bymonth = "bymonth"
+recur_bysetpos = "bysetpos"
+recur_wkst = "wkst"
+
+req_status_code = "code"
+req_status_description = "description"
+req_status_data = "data"
+
+vCard40_namespace = "urn:ietf:params:xml:ns:vcard-4.0"
+
+def makeTag(namespace, name):
+    return "{%s}%s" % (namespace, name.lower(),)
+
+
+
+def toString(root):
+
+    data = """<?xml version="1.0" encoding="utf-8"?>\n"""
+
+    INDENT = 2
+
+    # Generate indentation
+    def _indentNode(node, level=0):
+
+        if node.text is not None and node.text.strip():
+            return
+        elif len(node.getchildren()):
+            indent = "\n" + " " * (level + 1) * INDENT
+            node.text = indent
+            for child in node.getchildren():
+                child.tail = indent
+                _indentNode(child, level + 1)
+            if len(node.getchildren()):
+                node.getchildren()[-1].tail = "\n" + " " * level * INDENT
+
+    _indentNode(root, 0)
+    data += XML.tostring(root) + "\n"
+
+    return data

Deleted: PyCalendar/trunk/src/zonal/__init__.py
===================================================================
--- PyCalendar/branches/server/src/zonal/__init__.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/zonal/__init__.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,15 +0,0 @@
-##
-#    Copyright (c) 2007-2012 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.
-##

Copied: PyCalendar/trunk/src/zonal/__init__.py (from rev 10553, PyCalendar/branches/server/src/zonal/__init__.py)
===================================================================
--- PyCalendar/trunk/src/zonal/__init__.py	                        (rev 0)
+++ PyCalendar/trunk/src/zonal/__init__.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,15 @@
+##
+#    Copyright (c) 2007-2012 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.
+##

Deleted: PyCalendar/trunk/src/zonal/rule.py
===================================================================
--- PyCalendar/branches/server/src/zonal/rule.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/zonal/rule.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,574 +0,0 @@
-##
-#    Copyright (c) 2007-2012 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 import definitions
-from pycalendar.datetime import PyCalendarDateTime
-from pycalendar.utils import daysInMonth
-from pycalendar.vtimezonestandard import PyCalendarVTimezoneStandard
-from pycalendar.utcoffsetvalue import PyCalendarUTCOffsetValue
-from pycalendar.vtimezonedaylight import PyCalendarVTimezoneDaylight
-from pycalendar.property import PyCalendarProperty
-from pycalendar.recurrence import PyCalendarRecurrence
-import utils
-
-"""
-Class that maintains a TZ data Rule.
-"""
-
-__all__ = (
-    "Rule",
-    "RuleSet",
-)
-
-class RuleSet(object):
-    """
-    A set of tzdata rules tied to a specific Rule name
-    """
-
-    def __init__(self):
-        self.name = ""
-        self.rules = []
-
-
-    def __str__(self):
-        return self.generate()
-
-
-    def __eq__(self, other):
-        return other and (
-            self.name == other.name and
-            self.rules == other.rules
-        )
-
-
-    def __ne__(self, other):
-        return not self.__eq__(other)
-
-
-    def parse(self, lines):
-        """
-        Parse the set of Rule lines from tzdata.
-
-        @param lines: the lines to parse.
-        @type lines: C{str}
-        """
-
-        splitlines = lines.split("\n")
-        for line in splitlines:
-            splits = line.expandtabs(1).split(" ")
-            name = splits[1]
-            assert name, "Must have a zone name: '%s'" % (lines,)
-            if not self.name:
-                self.name = name
-            assert self.name == name, "Different zone names %s and %s: %s" % (self.name, name,)
-            rule = Rule()
-            rule.parse(line)
-            self.rules.append(rule)
-
-
-    def generate(self):
-        """
-        Generate a Rule line.
-
-        @return: a C{str} with the Rule.
-        """
-        items = [rule.generate() for rule in self.rules]
-        return "\n".join(items)
-
-
-    def expand(self, results, zoneinfo, maxYear):
-        """
-        Expand the set of rules into transition/offset pairs for the entire RuleSet
-        starting at the beginning and going up to maxYear at most.
-
-        @param results: list to append results to
-        @type results: C{list}
-        @param zoneinfo: the Zone in which this RuleSet is being used
-        @type zoneinfo: L{ZoneRule}
-        @param maxYear: the maximum year to expand out to
-        @type maxYear: C{int}
-        """
-        for rule in self.rules:
-            rule.expand(results, zoneinfo, maxYear)
-
-
-
-class Rule(object):
-    """
-    A tzdata Rule
-    """
-
-    # Some useful mapping tables
-
-    LASTDAY_NAME_TO_DAY = {
-        "lastSun": PyCalendarDateTime.SUNDAY,
-        "lastMon": PyCalendarDateTime.MONDAY,
-        "lastTue": PyCalendarDateTime.TUESDAY,
-        "lastWed": PyCalendarDateTime.WEDNESDAY,
-        "lastThu": PyCalendarDateTime.THURSDAY,
-        "lastFri": PyCalendarDateTime.FRIDAY,
-        "lastSat": PyCalendarDateTime.SATURDAY,
-    }
-
-    DAY_NAME_TO_DAY = {
-        "Sun": PyCalendarDateTime.SUNDAY,
-        "Mon": PyCalendarDateTime.MONDAY,
-        "Tue": PyCalendarDateTime.TUESDAY,
-        "Wed": PyCalendarDateTime.WEDNESDAY,
-        "Thu": PyCalendarDateTime.THURSDAY,
-        "Fri": PyCalendarDateTime.FRIDAY,
-        "Sat": PyCalendarDateTime.SATURDAY,
-    }
-
-    LASTDAY_NAME_TO_RDAY = {
-        "lastSun": definitions.eRecurrence_WEEKDAY_SU,
-        "lastMon": definitions.eRecurrence_WEEKDAY_MO,
-        "lastTue": definitions.eRecurrence_WEEKDAY_TU,
-        "lastWed": definitions.eRecurrence_WEEKDAY_WE,
-        "lastThu": definitions.eRecurrence_WEEKDAY_TH,
-        "lastFri": definitions.eRecurrence_WEEKDAY_FR,
-        "lastSat": definitions.eRecurrence_WEEKDAY_SA,
-    }
-
-    DAY_NAME_TO_RDAY = {
-        PyCalendarDateTime.SUNDAY: definitions.eRecurrence_WEEKDAY_SU,
-        PyCalendarDateTime.MONDAY: definitions.eRecurrence_WEEKDAY_MO,
-        PyCalendarDateTime.TUESDAY: definitions.eRecurrence_WEEKDAY_TU,
-        PyCalendarDateTime.WEDNESDAY: definitions.eRecurrence_WEEKDAY_WE,
-        PyCalendarDateTime.THURSDAY: definitions.eRecurrence_WEEKDAY_TH,
-        PyCalendarDateTime.FRIDAY: definitions.eRecurrence_WEEKDAY_FR,
-        PyCalendarDateTime.SATURDAY: definitions.eRecurrence_WEEKDAY_SA,
-    }
-
-    MONTH_NAME_TO_POS = {
-        "Jan": 1,
-        "Feb": 2,
-        "Mar": 3,
-        "Apr": 4,
-        "May": 5,
-        "Jun": 6,
-        "Jul": 7,
-        "Aug": 8,
-        "Sep": 9,
-        "Oct": 10,
-        "Nov": 11,
-        "Dec": 12,
-    }
-
-    MONTH_POS_TO_NAME = ("", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
-
-
-    def __init__(self):
-        self.name = ""
-        self.fromYear = ""
-        self.toYear = ""
-        self.type = "-"
-        self.inMonth = 0
-        self.onDay = ""
-        self.atTime = 0
-        self.saveTime = 0
-        self.letter = ""
-
-
-    def __str__(self):
-        return self.generate()
-
-
-    def __eq__(self, other):
-        return other and (
-            self.name == other.name and
-            self.fromYear == other.fromYear and
-            self.toYear == other.toYear and
-            self.type == other.type and
-            self.inMonth == other.inMonth and
-            self.onDay == other.onDay and
-            self.atTime == other.atTime and
-            self.saveTime == other.saveTime and
-            self.letter == other.letter
-        )
-
-
-    def __ne__(self, other):
-        return not self.__eq__(other)
-
-
-    def parse(self, line):
-        """
-        Parse the Rule line from tzdata.
-
-        @param line: the line to parse.
-        @type line: C{str}
-        """
-
-        # Simply split the bits up and store them in various properties
-        splits = [x for x in line.expandtabs(1).split(" ") if len(x) > 0]
-        assert len(splits) >= 10, "Wrong number of fields in Rule: '%s'" % (line,)
-        self.name = splits[1]
-        self.fromYear = splits[2]
-        self.toYear = splits[3]
-        self.type = splits[4]
-        self.inMonth = splits[5]
-        self.onDay = splits[6]
-        self.atTime = splits[7]
-        self.saveTime = splits[8]
-        self.letter = splits[9]
-
-
-    def generate(self):
-        """
-        Generate a Rule line.
-
-        @return: a C{str} with the Rule.
-        """
-        items = (
-            "Rule",
-            self.name,
-            self.fromYear,
-            self.toYear,
-            self.type,
-            self.inMonth,
-            self.onDay,
-            self.atTime,
-            self.saveTime,
-            self.letter,
-        )
-
-        return "\t".join(items)
-
-
-    def getOffset(self):
-        """
-        Return the specified rule offset in seconds.
-
-        @return: C{int}
-        """
-        splits = self.saveTime.split(":")
-        hours = int(splits[0])
-        if len(splits) == 2:
-            minutes = int(splits[1])
-        else:
-            minutes = 0
-        negative = hours < 0
-        if negative:
-            return -((-hours * 60) + minutes) * 60
-        else:
-            return ((hours * 60) + minutes) * 60
-
-
-    def startYear(self):
-        return int(self.fromYear)
-
-
-    def endYear(self):
-        if self.toYear == "only":
-            return self.startYear()
-        elif self.toYear == "max":
-            return 9999
-        else:
-            return int(self.toYear)
-
-
-    def datetimeForYear(self, year):
-        """
-        Given a specific year, determine the actual date/time of the transition
-
-        @param year:  the year to determine the transition for
-        @type year: C{int}
-
-        @return: C{tuple} of L{PyCalendarDateTime} and C{str} (which is the special
-            tzdata mode character
-        """
-        # Create a floating date-time
-        dt = PyCalendarDateTime()
-
-        # Setup base year/month/day
-        dt.setYear(year)
-        dt.setMonth(Rule.MONTH_NAME_TO_POS[self.inMonth])
-        dt.setDay(1)
-
-        # Setup base hours/minutes
-        splits = self.atTime.split(":")
-        if len(splits) == 1:
-            splits.append("0")
-        assert len(splits) == 2, "atTime format is wrong: %s, %s" % (self.atTime, self,)
-        hours = int(splits[0])
-        if len(splits[1]) > 2:
-            minutes = int(splits[1][:2])
-            special = splits[1][2:]
-        else:
-            minutes = int(splits[1])
-            special = ""
-
-        # Special case for 24:00
-        if hours == 24 and minutes == 0:
-            dt.setHours(23)
-            dt.setMinutes(59)
-            dt.setSeconds(59)
-        else:
-            dt.setHours(hours)
-            dt.setMinutes(minutes)
-
-        # Now determine the actual start day
-        if self.onDay in Rule.LASTDAY_NAME_TO_DAY:
-            dt.setDayOfWeekInMonth(-1, Rule.LASTDAY_NAME_TO_DAY[self.onDay])
-        elif self.onDay.find(">=") != -1:
-            splits = self.onDay.split(">=")
-            dt.setNextDayOfWeek(int(splits[1]), Rule.DAY_NAME_TO_DAY[splits[0]])
-        else:
-            try:
-                day = int(self.onDay)
-                dt.setDay(day)
-            except:
-                assert False, "onDay value is not recognized: %s" % (self.onDay,)
-
-        return dt, special
-
-
-    def getOnDayDetails(self, start, indicatedDay, indicatedOffset):
-        """
-        Get RRULE BYxxx part items from the Rule data.
-
-        @param start: start date-time for the recurrence set
-        @type start: L{PyCalendarDateTime}
-        @param indicatedDay: the day that the Rule indicates for recurrence
-        @type indicatedDay: C{int}
-        @param indicatedOffset: the offset that the Rule indicates for recurrence
-        @type indicatedOffset: C{int}
-        """
-
-        month = start.getMonth()
-        year = start.getYear()
-        dayOfWeek = start.getDayOfWeek()
-
-        # Need to check whether day has changed due to time shifting
-        # e.g. if "u" mode is specified, the actual localtime may be
-        # shifted to the previous day if the offset is negative
-        if indicatedDay != dayOfWeek:
-            difference = dayOfWeek - indicatedDay
-            if difference in (1, -6,):
-                indicatedOffset += 1
-
-                # Adjust the month down too if needed
-                if start.getDay() == 1:
-                    month -= 1
-                    if month < 1:
-                        month = 12
-            elif difference in (-1, 6,):
-                assert indicatedOffset != 1, "Bad RRULE adjustment"
-                indicatedOffset -= 1
-            else:
-                assert False, "Unknown RRULE adjustment"
-
-        try:
-            # Form the appropriate RRULE bits
-            day = Rule.DAY_NAME_TO_RDAY[dayOfWeek]
-            offset = indicatedOffset
-            bymday = None
-            if offset == 1:
-                offset = 1
-            elif offset == 8:
-                offset = 2
-            elif offset == 15:
-                offset = 3
-            elif offset == 22:
-                offset = 4
-            else:
-                days_in_month = daysInMonth(month, year)
-                if days_in_month - offset == 6:
-                    offset = -1
-                elif days_in_month - offset == 13:
-                    offset = -2
-                elif days_in_month - offset == 20:
-                    offset = -3
-                else:
-                    bymday = [offset + i for i in range(7) if (offset + i) <= days_in_month]
-                    offset = 0
-        except:
-            assert False, "onDay value is not recognized: %s" % (self.onDay,)
-
-        return offset, day, bymday
-
-
-    def expand(self, results, zoneinfo, maxYear):
-        """
-        Expand the Rule into a set of transition date/offset pairs
-
-        @param results: list to append results to
-        @type results: C{list}
-        @param zoneinfo: the Zone in which this RuleSet is being used
-        @type zoneinfo: L{ZoneRule}
-        @param maxYear: the maximum year to expand out to
-        @type maxYear: C{int}
-        """
-
-        if self.startYear() >= maxYear:
-            return
-
-        self.fullExpand(maxYear)
-        zoneoffset = zoneinfo.getUTCOffset()
-        offset = self.getOffset()
-        for dt in self.dt_cache:
-            results.append((dt, zoneoffset + offset, self))
-
-
-    def fullExpand(self, maxYear):
-        """
-        Do a full recurrence expansion for each year in the Rule's range, upto
-        a specified maximum.
-
-        @param maxYear: maximum year to expand to
-        @type maxYear: C{int}
-        """
-        if hasattr(self, "dt_cache"):
-            return self.dt_cache
-        start = self.startYear()
-        end = self.endYear()
-        if end > maxYear:
-            end = maxYear - 1
-        self.dt_cache = []
-        for year in xrange(start, end + 1):
-            dt = utils.DateTime(*self.datetimeForYear(year))
-            self.dt_cache.append(dt)
-
-
-    def vtimezone(self, vtz, zonerule, start, end, offsetfrom, offsetto, instanceCount):
-        """
-        Generate a VTIMEZONE sub-component for this Rule.
-
-        @param vtz: VTIMEZONE to add to
-        @type vtz: L{PyCalendarVTimezone}
-        @param zonerule: the Zone rule line being used
-        @type zonerule: L{ZoneRule}
-        @param start: the start time for the first instance
-        @type start: L{PyCalendarDateTime}
-        @param end: the start time for the last instance
-        @type end: L{PyCalendarDateTime}
-        @param offsetfrom: the UTC offset-from
-        @type offsetfrom: C{int}
-        @param offsetto: the UTC offset-to
-        @type offsetto: C{int}
-        @param instanceCount: the number of instances in the set
-        @type instanceCount: C{int}
-        """
-
-        # Determine type of component based on offset
-        dstoffset = self.getOffset()
-        if dstoffset == 0:
-            comp = PyCalendarVTimezoneStandard(parent=vtz)
-        else:
-            comp = PyCalendarVTimezoneDaylight(parent=vtz)
-
-        # Do offsets
-        tzoffsetfrom = PyCalendarUTCOffsetValue(offsetfrom)
-        tzoffsetto = PyCalendarUTCOffsetValue(offsetto)
-
-        comp.addProperty(PyCalendarProperty(definitions.cICalProperty_TZOFFSETFROM, tzoffsetfrom))
-        comp.addProperty(PyCalendarProperty(definitions.cICalProperty_TZOFFSETTO, tzoffsetto))
-
-        # Do TZNAME
-        if zonerule.format.find("%") != -1:
-            tzname = zonerule.format % (self.letter if self.letter != "-" else "",)
-        else:
-            tzname = zonerule.format
-        comp.addProperty(PyCalendarProperty(definitions.cICalProperty_TZNAME, tzname))
-
-        # Do DTSTART
-        comp.addProperty(PyCalendarProperty(definitions.cICalProperty_DTSTART, start))
-
-        # Now determine the recurrences (use RDATE if only one year or
-        # number of instances is one)
-        if self.toYear != "only" and instanceCount != 1:
-            rrule = PyCalendarRecurrence()
-            rrule.setFreq(definitions.eRecurrence_YEARLY)
-            rrule.setByMonth((Rule.MONTH_NAME_TO_POS[self.inMonth],))
-            if self.onDay in Rule.LASTDAY_NAME_TO_RDAY:
-
-                # Need to check whether day has changed due to time shifting
-                dayOfWeek = start.getDayOfWeek()
-                indicatedDay = Rule.LASTDAY_NAME_TO_DAY[self.onDay]
-
-                if dayOfWeek == indicatedDay:
-                    rrule.setByDay(((-1, Rule.LASTDAY_NAME_TO_RDAY[self.onDay]),))
-                elif dayOfWeek < indicatedDay or dayOfWeek == 6 and indicatedDay == 0:
-                    # This is OK as we have moved back a day and thus no month transition
-                    # could have occurred
-                    fakeOffset = daysInMonth(start.getMonth(), start.getYear()) - 6
-                    offset, rday, bymday = self.getOnDayDetails(start, indicatedDay, fakeOffset)
-                    if bymday:
-                        rrule.setByMonthDay(bymday)
-                    rrule.setByDay(((offset, rday),))
-                else:
-                    # This is bad news as we have moved forward a day possibly into the next month
-                    # What we do is switch to using a BYYEARDAY rule with offset from the end of the year
-                    rrule.setByMonth(())
-                    daysBackStartOfMonth = (
-                        365, 334, 306, 275, 245, 214, 184, 153, 122, 92, 61, 31, 0     # Does not account for leap year
-                    )
-                    rrule.setByYearDay([-(daysBackStartOfMonth[Rule.MONTH_NAME_TO_POS[self.inMonth]] + i) for i in range(7)])
-                    rrule.setByDay(
-                        ((0, divmod(Rule.LASTDAY_NAME_TO_DAY[self.onDay] + 1, 7)[1]),),
-                    )
-
-            elif self.onDay.find(">=") != -1:
-                indicatedDay, dayoffset = self.onDay.split(">=")
-
-                # Need to check whether day has changed due to time shifting
-                dayOfWeek = start.getDayOfWeek()
-                indicatedDay = Rule.DAY_NAME_TO_DAY[indicatedDay]
-
-                if dayOfWeek == indicatedDay:
-                    offset, rday, bymday = self.getOnDayDetails(start, indicatedDay, int(dayoffset))
-                    if bymday:
-                        rrule.setByMonthDay(bymday)
-                    rrule.setByDay(((offset, rday),))
-                elif dayoffset == 1 and divmod(dayoffset - indicatedDay, 7)[1] == 6:
-                    # This is bad news as we have moved backward a day possibly into the next month
-                    # What we do is switch to using a BYYEARDAY rule with offset from the end of the year
-                    rrule.setByMonth(())
-                    daysBackStartOfMonth = (
-                        365, 334, 306, 275, 245, 214, 184, 153, 122, 92, 61, 31, 0     # Does not account for leap year
-                    )
-                    rrule.setByYearDay([-(daysBackStartOfMonth[Rule.MONTH_NAME_TO_POS[self.inMonth]] + i) for i in range(7)])
-                    rrule.setByDay(
-                        ((0, divmod(indicatedDay + 1, 7)[1]),),
-                    )
-                else:
-                    # This is OK as we have moved forward a day and thus no month transition
-                    # could have occurred
-                    offset, rday, bymday = self.getOnDayDetails(start, indicatedDay, int(dayoffset))
-                    if bymday:
-                        rrule.setByMonthDay(bymday)
-                    rrule.setByDay(((offset, rday),))
-            else:
-                try:
-                    _ignore_day = int(self.onDay)
-                except:
-                    assert False, "onDay value is not recognized: %s" % (self.onDay,)
-
-            # Add any UNTIL
-            if zonerule.getUntilDate().dt.getYear() < 9999 or self.endYear() < 9999:
-                until = end.duplicate()
-                until.offsetSeconds(-offsetfrom)
-                until.setTimezoneUTC(True)
-                rrule.setUseUntil(True)
-                rrule.setUntil(until)
-
-            comp.addProperty(PyCalendarProperty(definitions.cICalProperty_RRULE, rrule))
-        else:
-            comp.addProperty(PyCalendarProperty(definitions.cICalProperty_RDATE, start))
-
-        comp.finalise()
-        vtz.addComponent(comp)

Copied: PyCalendar/trunk/src/zonal/rule.py (from rev 10553, PyCalendar/branches/server/src/zonal/rule.py)
===================================================================
--- PyCalendar/trunk/src/zonal/rule.py	                        (rev 0)
+++ PyCalendar/trunk/src/zonal/rule.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,574 @@
+##
+#    Copyright (c) 2007-2012 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 import definitions
+from pycalendar.datetime import PyCalendarDateTime
+from pycalendar.utils import daysInMonth
+from pycalendar.vtimezonestandard import PyCalendarVTimezoneStandard
+from pycalendar.utcoffsetvalue import PyCalendarUTCOffsetValue
+from pycalendar.vtimezonedaylight import PyCalendarVTimezoneDaylight
+from pycalendar.property import PyCalendarProperty
+from pycalendar.recurrence import PyCalendarRecurrence
+import utils
+
+"""
+Class that maintains a TZ data Rule.
+"""
+
+__all__ = (
+    "Rule",
+    "RuleSet",
+)
+
+class RuleSet(object):
+    """
+    A set of tzdata rules tied to a specific Rule name
+    """
+
+    def __init__(self):
+        self.name = ""
+        self.rules = []
+
+
+    def __str__(self):
+        return self.generate()
+
+
+    def __eq__(self, other):
+        return other and (
+            self.name == other.name and
+            self.rules == other.rules
+        )
+
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+
+    def parse(self, lines):
+        """
+        Parse the set of Rule lines from tzdata.
+
+        @param lines: the lines to parse.
+        @type lines: C{str}
+        """
+
+        splitlines = lines.split("\n")
+        for line in splitlines:
+            splits = line.expandtabs(1).split(" ")
+            name = splits[1]
+            assert name, "Must have a zone name: '%s'" % (lines,)
+            if not self.name:
+                self.name = name
+            assert self.name == name, "Different zone names %s and %s: %s" % (self.name, name,)
+            rule = Rule()
+            rule.parse(line)
+            self.rules.append(rule)
+
+
+    def generate(self):
+        """
+        Generate a Rule line.
+
+        @return: a C{str} with the Rule.
+        """
+        items = [rule.generate() for rule in self.rules]
+        return "\n".join(items)
+
+
+    def expand(self, results, zoneinfo, maxYear):
+        """
+        Expand the set of rules into transition/offset pairs for the entire RuleSet
+        starting at the beginning and going up to maxYear at most.
+
+        @param results: list to append results to
+        @type results: C{list}
+        @param zoneinfo: the Zone in which this RuleSet is being used
+        @type zoneinfo: L{ZoneRule}
+        @param maxYear: the maximum year to expand out to
+        @type maxYear: C{int}
+        """
+        for rule in self.rules:
+            rule.expand(results, zoneinfo, maxYear)
+
+
+
+class Rule(object):
+    """
+    A tzdata Rule
+    """
+
+    # Some useful mapping tables
+
+    LASTDAY_NAME_TO_DAY = {
+        "lastSun": PyCalendarDateTime.SUNDAY,
+        "lastMon": PyCalendarDateTime.MONDAY,
+        "lastTue": PyCalendarDateTime.TUESDAY,
+        "lastWed": PyCalendarDateTime.WEDNESDAY,
+        "lastThu": PyCalendarDateTime.THURSDAY,
+        "lastFri": PyCalendarDateTime.FRIDAY,
+        "lastSat": PyCalendarDateTime.SATURDAY,
+    }
+
+    DAY_NAME_TO_DAY = {
+        "Sun": PyCalendarDateTime.SUNDAY,
+        "Mon": PyCalendarDateTime.MONDAY,
+        "Tue": PyCalendarDateTime.TUESDAY,
+        "Wed": PyCalendarDateTime.WEDNESDAY,
+        "Thu": PyCalendarDateTime.THURSDAY,
+        "Fri": PyCalendarDateTime.FRIDAY,
+        "Sat": PyCalendarDateTime.SATURDAY,
+    }
+
+    LASTDAY_NAME_TO_RDAY = {
+        "lastSun": definitions.eRecurrence_WEEKDAY_SU,
+        "lastMon": definitions.eRecurrence_WEEKDAY_MO,
+        "lastTue": definitions.eRecurrence_WEEKDAY_TU,
+        "lastWed": definitions.eRecurrence_WEEKDAY_WE,
+        "lastThu": definitions.eRecurrence_WEEKDAY_TH,
+        "lastFri": definitions.eRecurrence_WEEKDAY_FR,
+        "lastSat": definitions.eRecurrence_WEEKDAY_SA,
+    }
+
+    DAY_NAME_TO_RDAY = {
+        PyCalendarDateTime.SUNDAY: definitions.eRecurrence_WEEKDAY_SU,
+        PyCalendarDateTime.MONDAY: definitions.eRecurrence_WEEKDAY_MO,
+        PyCalendarDateTime.TUESDAY: definitions.eRecurrence_WEEKDAY_TU,
+        PyCalendarDateTime.WEDNESDAY: definitions.eRecurrence_WEEKDAY_WE,
+        PyCalendarDateTime.THURSDAY: definitions.eRecurrence_WEEKDAY_TH,
+        PyCalendarDateTime.FRIDAY: definitions.eRecurrence_WEEKDAY_FR,
+        PyCalendarDateTime.SATURDAY: definitions.eRecurrence_WEEKDAY_SA,
+    }
+
+    MONTH_NAME_TO_POS = {
+        "Jan": 1,
+        "Feb": 2,
+        "Mar": 3,
+        "Apr": 4,
+        "May": 5,
+        "Jun": 6,
+        "Jul": 7,
+        "Aug": 8,
+        "Sep": 9,
+        "Oct": 10,
+        "Nov": 11,
+        "Dec": 12,
+    }
+
+    MONTH_POS_TO_NAME = ("", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
+
+
+    def __init__(self):
+        self.name = ""
+        self.fromYear = ""
+        self.toYear = ""
+        self.type = "-"
+        self.inMonth = 0
+        self.onDay = ""
+        self.atTime = 0
+        self.saveTime = 0
+        self.letter = ""
+
+
+    def __str__(self):
+        return self.generate()
+
+
+    def __eq__(self, other):
+        return other and (
+            self.name == other.name and
+            self.fromYear == other.fromYear and
+            self.toYear == other.toYear and
+            self.type == other.type and
+            self.inMonth == other.inMonth and
+            self.onDay == other.onDay and
+            self.atTime == other.atTime and
+            self.saveTime == other.saveTime and
+            self.letter == other.letter
+        )
+
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+
+    def parse(self, line):
+        """
+        Parse the Rule line from tzdata.
+
+        @param line: the line to parse.
+        @type line: C{str}
+        """
+
+        # Simply split the bits up and store them in various properties
+        splits = [x for x in line.expandtabs(1).split(" ") if len(x) > 0]
+        assert len(splits) >= 10, "Wrong number of fields in Rule: '%s'" % (line,)
+        self.name = splits[1]
+        self.fromYear = splits[2]
+        self.toYear = splits[3]
+        self.type = splits[4]
+        self.inMonth = splits[5]
+        self.onDay = splits[6]
+        self.atTime = splits[7]
+        self.saveTime = splits[8]
+        self.letter = splits[9]
+
+
+    def generate(self):
+        """
+        Generate a Rule line.
+
+        @return: a C{str} with the Rule.
+        """
+        items = (
+            "Rule",
+            self.name,
+            self.fromYear,
+            self.toYear,
+            self.type,
+            self.inMonth,
+            self.onDay,
+            self.atTime,
+            self.saveTime,
+            self.letter,
+        )
+
+        return "\t".join(items)
+
+
+    def getOffset(self):
+        """
+        Return the specified rule offset in seconds.
+
+        @return: C{int}
+        """
+        splits = self.saveTime.split(":")
+        hours = int(splits[0])
+        if len(splits) == 2:
+            minutes = int(splits[1])
+        else:
+            minutes = 0
+        negative = hours < 0
+        if negative:
+            return -((-hours * 60) + minutes) * 60
+        else:
+            return ((hours * 60) + minutes) * 60
+
+
+    def startYear(self):
+        return int(self.fromYear)
+
+
+    def endYear(self):
+        if self.toYear == "only":
+            return self.startYear()
+        elif self.toYear == "max":
+            return 9999
+        else:
+            return int(self.toYear)
+
+
+    def datetimeForYear(self, year):
+        """
+        Given a specific year, determine the actual date/time of the transition
+
+        @param year:  the year to determine the transition for
+        @type year: C{int}
+
+        @return: C{tuple} of L{PyCalendarDateTime} and C{str} (which is the special
+            tzdata mode character
+        """
+        # Create a floating date-time
+        dt = PyCalendarDateTime()
+
+        # Setup base year/month/day
+        dt.setYear(year)
+        dt.setMonth(Rule.MONTH_NAME_TO_POS[self.inMonth])
+        dt.setDay(1)
+
+        # Setup base hours/minutes
+        splits = self.atTime.split(":")
+        if len(splits) == 1:
+            splits.append("0")
+        assert len(splits) == 2, "atTime format is wrong: %s, %s" % (self.atTime, self,)
+        hours = int(splits[0])
+        if len(splits[1]) > 2:
+            minutes = int(splits[1][:2])
+            special = splits[1][2:]
+        else:
+            minutes = int(splits[1])
+            special = ""
+
+        # Special case for 24:00
+        if hours == 24 and minutes == 0:
+            dt.setHours(23)
+            dt.setMinutes(59)
+            dt.setSeconds(59)
+        else:
+            dt.setHours(hours)
+            dt.setMinutes(minutes)
+
+        # Now determine the actual start day
+        if self.onDay in Rule.LASTDAY_NAME_TO_DAY:
+            dt.setDayOfWeekInMonth(-1, Rule.LASTDAY_NAME_TO_DAY[self.onDay])
+        elif self.onDay.find(">=") != -1:
+            splits = self.onDay.split(">=")
+            dt.setNextDayOfWeek(int(splits[1]), Rule.DAY_NAME_TO_DAY[splits[0]])
+        else:
+            try:
+                day = int(self.onDay)
+                dt.setDay(day)
+            except:
+                assert False, "onDay value is not recognized: %s" % (self.onDay,)
+
+        return dt, special
+
+
+    def getOnDayDetails(self, start, indicatedDay, indicatedOffset):
+        """
+        Get RRULE BYxxx part items from the Rule data.
+
+        @param start: start date-time for the recurrence set
+        @type start: L{PyCalendarDateTime}
+        @param indicatedDay: the day that the Rule indicates for recurrence
+        @type indicatedDay: C{int}
+        @param indicatedOffset: the offset that the Rule indicates for recurrence
+        @type indicatedOffset: C{int}
+        """
+
+        month = start.getMonth()
+        year = start.getYear()
+        dayOfWeek = start.getDayOfWeek()
+
+        # Need to check whether day has changed due to time shifting
+        # e.g. if "u" mode is specified, the actual localtime may be
+        # shifted to the previous day if the offset is negative
+        if indicatedDay != dayOfWeek:
+            difference = dayOfWeek - indicatedDay
+            if difference in (1, -6,):
+                indicatedOffset += 1
+
+                # Adjust the month down too if needed
+                if start.getDay() == 1:
+                    month -= 1
+                    if month < 1:
+                        month = 12
+            elif difference in (-1, 6,):
+                assert indicatedOffset != 1, "Bad RRULE adjustment"
+                indicatedOffset -= 1
+            else:
+                assert False, "Unknown RRULE adjustment"
+
+        try:
+            # Form the appropriate RRULE bits
+            day = Rule.DAY_NAME_TO_RDAY[dayOfWeek]
+            offset = indicatedOffset
+            bymday = None
+            if offset == 1:
+                offset = 1
+            elif offset == 8:
+                offset = 2
+            elif offset == 15:
+                offset = 3
+            elif offset == 22:
+                offset = 4
+            else:
+                days_in_month = daysInMonth(month, year)
+                if days_in_month - offset == 6:
+                    offset = -1
+                elif days_in_month - offset == 13:
+                    offset = -2
+                elif days_in_month - offset == 20:
+                    offset = -3
+                else:
+                    bymday = [offset + i for i in range(7) if (offset + i) <= days_in_month]
+                    offset = 0
+        except:
+            assert False, "onDay value is not recognized: %s" % (self.onDay,)
+
+        return offset, day, bymday
+
+
+    def expand(self, results, zoneinfo, maxYear):
+        """
+        Expand the Rule into a set of transition date/offset pairs
+
+        @param results: list to append results to
+        @type results: C{list}
+        @param zoneinfo: the Zone in which this RuleSet is being used
+        @type zoneinfo: L{ZoneRule}
+        @param maxYear: the maximum year to expand out to
+        @type maxYear: C{int}
+        """
+
+        if self.startYear() >= maxYear:
+            return
+
+        self.fullExpand(maxYear)
+        zoneoffset = zoneinfo.getUTCOffset()
+        offset = self.getOffset()
+        for dt in self.dt_cache:
+            results.append((dt, zoneoffset + offset, self))
+
+
+    def fullExpand(self, maxYear):
+        """
+        Do a full recurrence expansion for each year in the Rule's range, upto
+        a specified maximum.
+
+        @param maxYear: maximum year to expand to
+        @type maxYear: C{int}
+        """
+        if hasattr(self, "dt_cache"):
+            return self.dt_cache
+        start = self.startYear()
+        end = self.endYear()
+        if end > maxYear:
+            end = maxYear - 1
+        self.dt_cache = []
+        for year in xrange(start, end + 1):
+            dt = utils.DateTime(*self.datetimeForYear(year))
+            self.dt_cache.append(dt)
+
+
+    def vtimezone(self, vtz, zonerule, start, end, offsetfrom, offsetto, instanceCount):
+        """
+        Generate a VTIMEZONE sub-component for this Rule.
+
+        @param vtz: VTIMEZONE to add to
+        @type vtz: L{PyCalendarVTimezone}
+        @param zonerule: the Zone rule line being used
+        @type zonerule: L{ZoneRule}
+        @param start: the start time for the first instance
+        @type start: L{PyCalendarDateTime}
+        @param end: the start time for the last instance
+        @type end: L{PyCalendarDateTime}
+        @param offsetfrom: the UTC offset-from
+        @type offsetfrom: C{int}
+        @param offsetto: the UTC offset-to
+        @type offsetto: C{int}
+        @param instanceCount: the number of instances in the set
+        @type instanceCount: C{int}
+        """
+
+        # Determine type of component based on offset
+        dstoffset = self.getOffset()
+        if dstoffset == 0:
+            comp = PyCalendarVTimezoneStandard(parent=vtz)
+        else:
+            comp = PyCalendarVTimezoneDaylight(parent=vtz)
+
+        # Do offsets
+        tzoffsetfrom = PyCalendarUTCOffsetValue(offsetfrom)
+        tzoffsetto = PyCalendarUTCOffsetValue(offsetto)
+
+        comp.addProperty(PyCalendarProperty(definitions.cICalProperty_TZOFFSETFROM, tzoffsetfrom))
+        comp.addProperty(PyCalendarProperty(definitions.cICalProperty_TZOFFSETTO, tzoffsetto))
+
+        # Do TZNAME
+        if zonerule.format.find("%") != -1:
+            tzname = zonerule.format % (self.letter if self.letter != "-" else "",)
+        else:
+            tzname = zonerule.format
+        comp.addProperty(PyCalendarProperty(definitions.cICalProperty_TZNAME, tzname))
+
+        # Do DTSTART
+        comp.addProperty(PyCalendarProperty(definitions.cICalProperty_DTSTART, start))
+
+        # Now determine the recurrences (use RDATE if only one year or
+        # number of instances is one)
+        if self.toYear != "only" and instanceCount != 1:
+            rrule = PyCalendarRecurrence()
+            rrule.setFreq(definitions.eRecurrence_YEARLY)
+            rrule.setByMonth((Rule.MONTH_NAME_TO_POS[self.inMonth],))
+            if self.onDay in Rule.LASTDAY_NAME_TO_RDAY:
+
+                # Need to check whether day has changed due to time shifting
+                dayOfWeek = start.getDayOfWeek()
+                indicatedDay = Rule.LASTDAY_NAME_TO_DAY[self.onDay]
+
+                if dayOfWeek == indicatedDay:
+                    rrule.setByDay(((-1, Rule.LASTDAY_NAME_TO_RDAY[self.onDay]),))
+                elif dayOfWeek < indicatedDay or dayOfWeek == 6 and indicatedDay == 0:
+                    # This is OK as we have moved back a day and thus no month transition
+                    # could have occurred
+                    fakeOffset = daysInMonth(start.getMonth(), start.getYear()) - 6
+                    offset, rday, bymday = self.getOnDayDetails(start, indicatedDay, fakeOffset)
+                    if bymday:
+                        rrule.setByMonthDay(bymday)
+                    rrule.setByDay(((offset, rday),))
+                else:
+                    # This is bad news as we have moved forward a day possibly into the next month
+                    # What we do is switch to using a BYYEARDAY rule with offset from the end of the year
+                    rrule.setByMonth(())
+                    daysBackStartOfMonth = (
+                        365, 334, 306, 275, 245, 214, 184, 153, 122, 92, 61, 31, 0     # Does not account for leap year
+                    )
+                    rrule.setByYearDay([-(daysBackStartOfMonth[Rule.MONTH_NAME_TO_POS[self.inMonth]] + i) for i in range(7)])
+                    rrule.setByDay(
+                        ((0, divmod(Rule.LASTDAY_NAME_TO_DAY[self.onDay] + 1, 7)[1]),),
+                    )
+
+            elif self.onDay.find(">=") != -1:
+                indicatedDay, dayoffset = self.onDay.split(">=")
+
+                # Need to check whether day has changed due to time shifting
+                dayOfWeek = start.getDayOfWeek()
+                indicatedDay = Rule.DAY_NAME_TO_DAY[indicatedDay]
+
+                if dayOfWeek == indicatedDay:
+                    offset, rday, bymday = self.getOnDayDetails(start, indicatedDay, int(dayoffset))
+                    if bymday:
+                        rrule.setByMonthDay(bymday)
+                    rrule.setByDay(((offset, rday),))
+                elif dayoffset == 1 and divmod(dayoffset - indicatedDay, 7)[1] == 6:
+                    # This is bad news as we have moved backward a day possibly into the next month
+                    # What we do is switch to using a BYYEARDAY rule with offset from the end of the year
+                    rrule.setByMonth(())
+                    daysBackStartOfMonth = (
+                        365, 334, 306, 275, 245, 214, 184, 153, 122, 92, 61, 31, 0     # Does not account for leap year
+                    )
+                    rrule.setByYearDay([-(daysBackStartOfMonth[Rule.MONTH_NAME_TO_POS[self.inMonth]] + i) for i in range(7)])
+                    rrule.setByDay(
+                        ((0, divmod(indicatedDay + 1, 7)[1]),),
+                    )
+                else:
+                    # This is OK as we have moved forward a day and thus no month transition
+                    # could have occurred
+                    offset, rday, bymday = self.getOnDayDetails(start, indicatedDay, int(dayoffset))
+                    if bymday:
+                        rrule.setByMonthDay(bymday)
+                    rrule.setByDay(((offset, rday),))
+            else:
+                try:
+                    _ignore_day = int(self.onDay)
+                except:
+                    assert False, "onDay value is not recognized: %s" % (self.onDay,)
+
+            # Add any UNTIL
+            if zonerule.getUntilDate().dt.getYear() < 9999 or self.endYear() < 9999:
+                until = end.duplicate()
+                until.offsetSeconds(-offsetfrom)
+                until.setTimezoneUTC(True)
+                rrule.setUseUntil(True)
+                rrule.setUntil(until)
+
+            comp.addProperty(PyCalendarProperty(definitions.cICalProperty_RRULE, rrule))
+        else:
+            comp.addProperty(PyCalendarProperty(definitions.cICalProperty_RDATE, start))
+
+        comp.finalise()
+        vtz.addComponent(comp)

Deleted: PyCalendar/trunk/src/zonal/tests/__init__.py
===================================================================
--- PyCalendar/branches/server/src/zonal/tests/__init__.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/zonal/tests/__init__.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,15 +0,0 @@
-##
-#    Copyright (c) 2007-2012 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.
-##

Copied: PyCalendar/trunk/src/zonal/tests/__init__.py (from rev 10553, PyCalendar/branches/server/src/zonal/tests/__init__.py)
===================================================================
--- PyCalendar/trunk/src/zonal/tests/__init__.py	                        (rev 0)
+++ PyCalendar/trunk/src/zonal/tests/__init__.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,15 @@
+##
+#    Copyright (c) 2007-2012 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.
+##

Deleted: PyCalendar/trunk/src/zonal/tests/test_rule.py
===================================================================
--- PyCalendar/branches/server/src/zonal/tests/test_rule.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/zonal/tests/test_rule.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,87 +0,0 @@
-##
-#    Copyright (c) 2007-2012 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 zonal.rule import Rule, RuleSet
-from pycalendar.datetime import PyCalendarDateTime
-
-class TestRule(unittest.TestCase):
-
-    def test_parse(self):
-        data = (
-            "Rule\tGuat\t2006\tonly\t-\tOct\t1\t0:00\t0\tS",
-            "Rule\tAlgeria\t1916\t1919\t-\tOct\tSun>=1\t23:00s\t0\t-",
-            "Rule\tEgypt\t1945\tonly\t-\tApr\t16\t0:00\t1:00\tS",
-            "Rule\tGhana\t1936\t1942\t-\tSep\t1\t0:00\t0:20\tGHST",
-        )
-
-        for ruletext in data:
-            ruleitem = Rule()
-            ruleitem.parse(ruletext)
-
-            self.assertEqual(str(ruleitem), ruletext)
-
-
-    def test_datetimeforyear(self):
-        data = (
-            ("Rule\tGuat\t2006\tonly\t-\tOct\t1\t0:00\t0\tS", 2006, PyCalendarDateTime(2006, 10, 1, 0, 0, 0), ""),
-            ("Rule\tAlgeria\t1916\t1919\t-\tOct\tSun>=1\t23:00s\t0\t-", 1916, PyCalendarDateTime(1916, 10, 1, 23, 0, 0), "s"),
-            ("Rule\tGhana\t1936\t1942\t-\tSep\t1\t0:00\t0:20\tGHST", 1937, PyCalendarDateTime(1937, 9, 1, 0, 0, 0), ""),
-        )
-
-        for ruletext, year, dt, special in data:
-            ruleitem = Rule()
-            ruleitem.parse(ruletext)
-
-            self.assertEqual(ruleitem.datetimeForYear(year), (dt, special))
-
-
-    def test_getoffset(self):
-        data = (
-            ("Rule\tGuat\t2006\tonly\t-\tOct\t1\t0:00\t0\tS", 0),
-            ("Rule\tEgypt\t1945\tonly\t-\tApr\t16\t0:00\t1:00\tS", 60 * 60),
-            ("Rule\tGhana\t1936\t1942\t-\tSep\t1\t0:00\t0:20\tGHST", 20 * 60),
-        )
-
-        for ruletext, offset in data:
-            ruleitem = Rule()
-            ruleitem.parse(ruletext)
-
-            self.assertEqual(ruleitem.getOffset(), offset)
-
-
-
-class TestRuleSet(unittest.TestCase):
-
-    def test_parse(self):
-        data = """Rule\tUS\t1918\t1919\t-\tMar\tlastSun\t2:00\t1:00\tD
-Rule\tUS\t1918\t1919\t-\tOct\tlastSun\t2:00\t0\tS
-Rule\tUS\t1942\tonly\t-\tFeb\t9\t2:00\t1:00\tW
-Rule\tUS\t1945\tonly\t-\tAug\t14\t23:00u\t1:00\tP
-Rule\tUS\t1945\tonly\t-\tSep\t30\t2:00\t0\tS
-Rule\tUS\t1967\t2006\t-\tOct\tlastSun\t2:00\t0\tS
-Rule\tUS\t1967\t1973\t-\tApr\tlastSun\t2:00\t1:00\tD
-Rule\tUS\t1974\tonly\t-\tJan\t6\t2:00\t1:00\tD
-Rule\tUS\t1975\tonly\t-\tFeb\t23\t2:00\t1:00\tD
-Rule\tUS\t1976\t1986\t-\tApr\tlastSun\t2:00\t1:00\tD
-Rule\tUS\t1987\t2006\t-\tApr\tSun>=1\t2:00\t1:00\tD
-Rule\tUS\t2007\tmax\t-\tMar\tSun>=8\t2:00\t1:00\tD
-Rule\tUS\t2007\tmax\t-\tNov\tSun>=1\t2:00\t0\tS"""
-
-        ruleset = RuleSet()
-        ruleset.parse(data)
-
-        self.assertEqual(str(ruleset), data)

Copied: PyCalendar/trunk/src/zonal/tests/test_rule.py (from rev 10553, PyCalendar/branches/server/src/zonal/tests/test_rule.py)
===================================================================
--- PyCalendar/trunk/src/zonal/tests/test_rule.py	                        (rev 0)
+++ PyCalendar/trunk/src/zonal/tests/test_rule.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,87 @@
+##
+#    Copyright (c) 2007-2012 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 zonal.rule import Rule, RuleSet
+from pycalendar.datetime import PyCalendarDateTime
+
+class TestRule(unittest.TestCase):
+
+    def test_parse(self):
+        data = (
+            "Rule\tGuat\t2006\tonly\t-\tOct\t1\t0:00\t0\tS",
+            "Rule\tAlgeria\t1916\t1919\t-\tOct\tSun>=1\t23:00s\t0\t-",
+            "Rule\tEgypt\t1945\tonly\t-\tApr\t16\t0:00\t1:00\tS",
+            "Rule\tGhana\t1936\t1942\t-\tSep\t1\t0:00\t0:20\tGHST",
+        )
+
+        for ruletext in data:
+            ruleitem = Rule()
+            ruleitem.parse(ruletext)
+
+            self.assertEqual(str(ruleitem), ruletext)
+
+
+    def test_datetimeforyear(self):
+        data = (
+            ("Rule\tGuat\t2006\tonly\t-\tOct\t1\t0:00\t0\tS", 2006, PyCalendarDateTime(2006, 10, 1, 0, 0, 0), ""),
+            ("Rule\tAlgeria\t1916\t1919\t-\tOct\tSun>=1\t23:00s\t0\t-", 1916, PyCalendarDateTime(1916, 10, 1, 23, 0, 0), "s"),
+            ("Rule\tGhana\t1936\t1942\t-\tSep\t1\t0:00\t0:20\tGHST", 1937, PyCalendarDateTime(1937, 9, 1, 0, 0, 0), ""),
+        )
+
+        for ruletext, year, dt, special in data:
+            ruleitem = Rule()
+            ruleitem.parse(ruletext)
+
+            self.assertEqual(ruleitem.datetimeForYear(year), (dt, special))
+
+
+    def test_getoffset(self):
+        data = (
+            ("Rule\tGuat\t2006\tonly\t-\tOct\t1\t0:00\t0\tS", 0),
+            ("Rule\tEgypt\t1945\tonly\t-\tApr\t16\t0:00\t1:00\tS", 60 * 60),
+            ("Rule\tGhana\t1936\t1942\t-\tSep\t1\t0:00\t0:20\tGHST", 20 * 60),
+        )
+
+        for ruletext, offset in data:
+            ruleitem = Rule()
+            ruleitem.parse(ruletext)
+
+            self.assertEqual(ruleitem.getOffset(), offset)
+
+
+
+class TestRuleSet(unittest.TestCase):
+
+    def test_parse(self):
+        data = """Rule\tUS\t1918\t1919\t-\tMar\tlastSun\t2:00\t1:00\tD
+Rule\tUS\t1918\t1919\t-\tOct\tlastSun\t2:00\t0\tS
+Rule\tUS\t1942\tonly\t-\tFeb\t9\t2:00\t1:00\tW
+Rule\tUS\t1945\tonly\t-\tAug\t14\t23:00u\t1:00\tP
+Rule\tUS\t1945\tonly\t-\tSep\t30\t2:00\t0\tS
+Rule\tUS\t1967\t2006\t-\tOct\tlastSun\t2:00\t0\tS
+Rule\tUS\t1967\t1973\t-\tApr\tlastSun\t2:00\t1:00\tD
+Rule\tUS\t1974\tonly\t-\tJan\t6\t2:00\t1:00\tD
+Rule\tUS\t1975\tonly\t-\tFeb\t23\t2:00\t1:00\tD
+Rule\tUS\t1976\t1986\t-\tApr\tlastSun\t2:00\t1:00\tD
+Rule\tUS\t1987\t2006\t-\tApr\tSun>=1\t2:00\t1:00\tD
+Rule\tUS\t2007\tmax\t-\tMar\tSun>=8\t2:00\t1:00\tD
+Rule\tUS\t2007\tmax\t-\tNov\tSun>=1\t2:00\t0\tS"""
+
+        ruleset = RuleSet()
+        ruleset.parse(data)
+
+        self.assertEqual(str(ruleset), data)

Deleted: PyCalendar/trunk/src/zonal/tests/test_zone.py
===================================================================
--- PyCalendar/branches/server/src/zonal/tests/test_zone.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/zonal/tests/test_zone.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,98 +0,0 @@
-##
-#    Copyright (c) 2007-2012 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 zonal.zone import Zone
-from zonal.rule import RuleSet
-from pycalendar.calendar import PyCalendar
-
-class TestZone(unittest.TestCase):
-
-    def test_parse(self):
-        zonedef = """Zone America/New_York\t-4:56:02\t-\tLMT\t1883 Nov 18 12:03:58
-\t\t\t-5:00\tUS\tE%sT\t1920
-\t\t\t-5:00\tNYC\tE%sT\t1942
-\t\t\t-5:00\tUS\tE%sT\t1946
-\t\t\t-5:00\tNYC\tE%sT\t1967
-\t\t\t-5:00\tUS\tE%sT"""
-        zone = Zone()
-        zone.parse(zonedef)
-
-        self.assertEqual(str(zone), zonedef)
-
-
-    def test_vtimezone(self):
-
-        zonedef = """Zone America/New_York\t-4:56:02\t-\tLMT\t1883 Nov 18 12:03:58
-\t\t\t-5:00\tUS\tE%sT"""
-        rules = """Rule\tUS\t1918\t1919\t-\tMar\tlastSun\t2:00\t1:00\tD
-Rule\tUS\t1918\t1919\t-\tOct\tlastSun\t2:00\t0\tS
-Rule\tUS\t1942\tonly\t-\tFeb\t9\t2:00\t1:00\tW
-Rule\tUS\t1945\tonly\t-\tAug\t14\t23:00u\t1:00\tP
-Rule\tUS\t1945\tonly\t-\tSep\t30\t2:00\t0\tS
-Rule\tUS\t1967\t2006\t-\tOct\tlastSun\t2:00\t0\tS
-Rule\tUS\t1967\t1973\t-\tApr\tlastSun\t2:00\t1:00\tD
-Rule\tUS\t1974\tonly\t-\tJan\t6\t2:00\t1:00\tD
-Rule\tUS\t1975\tonly\t-\tFeb\t23\t2:00\t1:00\tD
-Rule\tUS\t1976\t1986\t-\tApr\tlastSun\t2:00\t1:00\tD
-Rule\tUS\t1987\t2006\t-\tApr\tSun>=1\t2:00\t1:00\tD
-Rule\tUS\t2007\tmax\t-\tMar\tSun>=8\t2:00\t1:00\tD
-Rule\tUS\t2007\tmax\t-\tNov\tSun>=1\t2:00\t0\tS"""
-        result = """BEGIN:VTIMEZONE
-TZID:America/New_York
-X-LIC-LOCATION:America/New_York
-BEGIN:DAYLIGHT
-DTSTART:20060402T020000
-RDATE:20060402T020000
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:20061029T020000
-RDATE:20061029T020000
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-BEGIN:DAYLIGHT
-DTSTART:20070311T020000
-RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:20071104T020000
-RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-END:VTIMEZONE
-""".replace("\n", "\r\n")
-
-        zone = Zone()
-        zone.parse(zonedef)
-
-        ruleset = RuleSet()
-        ruleset.parse(rules)
-        rules = {ruleset.name: ruleset}
-
-        cal = PyCalendar()
-        vtz = zone.vtimezone(cal, rules, 2006, 2011)
-
-        self.assertEqual(str(vtz), result)

Copied: PyCalendar/trunk/src/zonal/tests/test_zone.py (from rev 10553, PyCalendar/branches/server/src/zonal/tests/test_zone.py)
===================================================================
--- PyCalendar/trunk/src/zonal/tests/test_zone.py	                        (rev 0)
+++ PyCalendar/trunk/src/zonal/tests/test_zone.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,98 @@
+##
+#    Copyright (c) 2007-2012 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 zonal.zone import Zone
+from zonal.rule import RuleSet
+from pycalendar.calendar import PyCalendar
+
+class TestZone(unittest.TestCase):
+
+    def test_parse(self):
+        zonedef = """Zone America/New_York\t-4:56:02\t-\tLMT\t1883 Nov 18 12:03:58
+\t\t\t-5:00\tUS\tE%sT\t1920
+\t\t\t-5:00\tNYC\tE%sT\t1942
+\t\t\t-5:00\tUS\tE%sT\t1946
+\t\t\t-5:00\tNYC\tE%sT\t1967
+\t\t\t-5:00\tUS\tE%sT"""
+        zone = Zone()
+        zone.parse(zonedef)
+
+        self.assertEqual(str(zone), zonedef)
+
+
+    def test_vtimezone(self):
+
+        zonedef = """Zone America/New_York\t-4:56:02\t-\tLMT\t1883 Nov 18 12:03:58
+\t\t\t-5:00\tUS\tE%sT"""
+        rules = """Rule\tUS\t1918\t1919\t-\tMar\tlastSun\t2:00\t1:00\tD
+Rule\tUS\t1918\t1919\t-\tOct\tlastSun\t2:00\t0\tS
+Rule\tUS\t1942\tonly\t-\tFeb\t9\t2:00\t1:00\tW
+Rule\tUS\t1945\tonly\t-\tAug\t14\t23:00u\t1:00\tP
+Rule\tUS\t1945\tonly\t-\tSep\t30\t2:00\t0\tS
+Rule\tUS\t1967\t2006\t-\tOct\tlastSun\t2:00\t0\tS
+Rule\tUS\t1967\t1973\t-\tApr\tlastSun\t2:00\t1:00\tD
+Rule\tUS\t1974\tonly\t-\tJan\t6\t2:00\t1:00\tD
+Rule\tUS\t1975\tonly\t-\tFeb\t23\t2:00\t1:00\tD
+Rule\tUS\t1976\t1986\t-\tApr\tlastSun\t2:00\t1:00\tD
+Rule\tUS\t1987\t2006\t-\tApr\tSun>=1\t2:00\t1:00\tD
+Rule\tUS\t2007\tmax\t-\tMar\tSun>=8\t2:00\t1:00\tD
+Rule\tUS\t2007\tmax\t-\tNov\tSun>=1\t2:00\t0\tS"""
+        result = """BEGIN:VTIMEZONE
+TZID:America/New_York
+X-LIC-LOCATION:America/New_York
+BEGIN:DAYLIGHT
+DTSTART:20060402T020000
+RDATE:20060402T020000
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20061029T020000
+RDATE:20061029T020000
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:20070311T020000
+RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20071104T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+""".replace("\n", "\r\n")
+
+        zone = Zone()
+        zone.parse(zonedef)
+
+        ruleset = RuleSet()
+        ruleset.parse(rules)
+        rules = {ruleset.name: ruleset}
+
+        cal = PyCalendar()
+        vtz = zone.vtimezone(cal, rules, 2006, 2011)
+
+        self.assertEqual(str(vtz), result)

Deleted: PyCalendar/trunk/src/zonal/tzconvert.py
===================================================================
--- PyCalendar/branches/server/src/zonal/tzconvert.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/zonal/tzconvert.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,310 +0,0 @@
-#!/usr/bin/env python
-##
-#    Copyright (c) 2007-2012 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 __future__ import with_statement
-
-from difflib import unified_diff
-from pycalendar.calendar import PyCalendar
-import cStringIO as StringIO
-import getopt
-import os
-import rule
-import sys
-import zone
-
-"""
-Classes to parse a tzdata files and generate VTIMEZONE data.
-"""
-
-__all__ = (
-    "tzconvert",
-)
-
-class tzconvert(object):
-
-    def __init__(self, verbose=False):
-        self.rules = {}
-        self.zones = {}
-        self.links = {}
-        self.verbose = verbose
-
-
-    def getZoneNames(self):
-        return set(self.zones.keys())
-
-
-    def parse(self, file):
-        try:
-            f = open(file, "r")
-            ctr = 0
-            for line in f:
-                ctr += 1
-                line = line[:-1]
-                while True:
-                    if line.startswith("#") or len(line) == 0:
-                        break
-                    elif line.startswith("Rule"):
-                        self.parseRule(line)
-                        break
-                    elif line.startswith("Zone"):
-                        line = self.parseZone(line, f)
-                        if line is None:
-                            break
-                    elif line.startswith("Link"):
-                        self.parseLink(line)
-                        break
-                    elif len(line.strip()) != 0:
-                        assert False, "Could not parse line %d from tzconvert file: '%s'" % (ctr, line,)
-                    else:
-                        break
-        except:
-            print "Failed to parse file %s" % (file,)
-            raise
-
-
-    def parseRule(self, line):
-        ruleitem = rule.Rule()
-        ruleitem.parse(line)
-        self.rules.setdefault(ruleitem.name, rule.RuleSet()).rules.append(ruleitem)
-
-
-    def parseZone(self, line, f):
-        os = StringIO.StringIO()
-        os.write(line)
-        last_line = None
-        for nextline in f:
-            nextline = nextline[:-1]
-            if nextline.startswith("\t"):
-                os.write("\n")
-                os.write(nextline)
-            elif nextline.startswith("#") or len(nextline) == 0:
-                continue
-            else:
-                last_line = nextline
-                break
-
-        zoneitem = zone.Zone()
-        zoneitem.parse(os.getvalue())
-        self.zones[zoneitem.name] = zoneitem
-
-        return last_line
-
-
-    def parseLink(self, line):
-
-        splits = line.split()
-        linkFrom = splits[1]
-        linkTo = splits[2]
-        self.links[linkTo] = linkFrom
-
-
-    def expandZone(self, zonename, minYear, maxYear=2018):
-        """
-        Expand a zones transition dates up to the specified year.
-        """
-        zone = self.zones[zonename]
-        expanded = zone.expand(self.rules, minYear, maxYear)
-        return [(item[0], item[1], item[2],) for item in expanded]
-
-
-    def vtimezones(self, minYear, maxYear=2018, filterzones=None):
-        """
-        Generate iCalendar data for all VTIMEZONEs or just those specified
-        """
-
-        cal = PyCalendar()
-        for zone in self.zones.itervalues():
-            if filterzones and zone.name not in filterzones:
-                continue
-            vtz = zone.vtimezone(cal, self.rules, minYear, maxYear)
-            cal.addComponent(vtz)
-
-        return cal.getText()
-
-
-    def generateZoneinfoFiles(self, outputdir, minYear, maxYear=2018, links=True, filterzones=None):
-
-        # Empty current directory
-        try:
-            for root, dirs, files in os.walk(outputdir, topdown=False):
-                for name in files:
-                    os.remove(os.path.join(root, name))
-                for name in dirs:
-                    os.rmdir(os.path.join(root, name))
-        except OSError:
-            pass
-
-        for zone in self.zones.itervalues():
-            if filterzones and zone.name not in filterzones:
-                continue
-            cal = PyCalendar()
-            vtz = zone.vtimezone(cal, self.rules, minYear, maxYear)
-            cal.addComponent(vtz)
-
-            icsdata = cal.getText()
-            fpath = os.path.join(outputdir, zone.name + ".ics")
-            if not os.path.exists(os.path.dirname(fpath)):
-                os.makedirs(os.path.dirname(fpath))
-            with open(fpath, "w") as f:
-                f.write(icsdata)
-            if self.verbose:
-                print "Write path: %s" % (fpath,)
-
-        if links:
-            link_list = []
-            for linkTo, linkFrom in self.links.iteritems():
-
-                # Check for existing output file
-                fromPath = os.path.join(outputdir, linkFrom + ".ics")
-                if not os.path.exists(fromPath):
-                    print "Missing link from: %s to %s" % (linkFrom, linkTo,)
-                    continue
-
-                with open(fromPath) as f:
-                    icsdata = f.read()
-                icsdata = icsdata.replace(linkFrom, linkTo)
-
-                toPath = os.path.join(outputdir, linkTo + ".ics")
-                if not os.path.exists(os.path.dirname(toPath)):
-                    os.makedirs(os.path.dirname(toPath))
-                with open(toPath, "w") as f:
-                    f.write(icsdata)
-                if self.verbose:
-                    print "Write link: %s" % (linkTo,)
-
-                link_list.append("%s\t%s" % (linkTo, linkFrom,))
-
-            # Generate link mapping file
-            linkPath = os.path.join(outputdir, "links.txt")
-            open(linkPath, "w").write("\n".join(link_list))
-
-
-
-def usage(error_msg=None):
-    if error_msg:
-        print error_msg
-
-    print """Usage: tzconvert [options] [DIR]
-Options:
-    -h            Print this help and exit
-    --prodid      PROD-ID string to use
-    --start       Start year
-    --end         End year
-
-Arguments:
-    DIR      Directory containing an Olson tzdata directory to read, also
-             where zoneinfo data will be written
-
-Description:
-    This utility convert Olson-style timezone data in iCalendar.
-    VTIMEZONE objects, one .ics file per-timezone.
-
-"""
-
-    if error_msg:
-        raise ValueError(error_msg)
-    else:
-        sys.exit(0)
-
-
-if __name__ == '__main__':
-
-    # Set the PRODID value used in generated iCalendar data
-    prodid = "-//mulberrymail.com//Zonal//EN"
-    rootdir = "../../stuff/temp"
-    startYear = 1800
-    endYear = 2018
-
-    options, args = getopt.getopt(sys.argv[1:], "h", ["prodid=", "root=", "start=", "end=", ])
-
-    for option, value in options:
-        if option == "-h":
-            usage()
-        elif option == "--prodid":
-            prodid = value
-        elif option == "--root":
-            rootdir = value
-        elif option == "--start":
-            startYear = int(value)
-        elif option == "--end":
-            endYear = int(value)
-        else:
-            usage("Unrecognized option: %s" % (option,))
-
-    # Process arguments
-    if len(args) > 1:
-        usage("Must have only one argument")
-    if len(args) == 1:
-        rootdir = os.path.expanduser(args[0])
-
-    PyCalendar.sProdID = prodid
-
-    zonedir = os.path.join(rootdir, "tzdata")
-    zonefiles = (
-        "northamerica",
-        "southamerica",
-        "europe",
-        "africa",
-        "asia",
-        "australasia",
-        "antarctica",
-        "etcetera",
-        "backward",
-    )
-
-    parser = tzconvert(verbose=True)
-    for file in zonefiles:
-        parser.parse(os.path.join(zonedir, file))
-
-    if 1:
-        parser.generateZoneinfoFiles(os.path.join(rootdir, "zoneinfo"), startYear, endYear, filterzones=(
-            #"America/Montevideo",
-            #"Europe/Paris",
-            #"Africa/Cairo",
-        ))
-
-    if 0:
-        checkName = "EST"
-        parsed = parser.vtimezones(1800, 2018, filterzones=(
-            checkName,
-        ))
-
-        icsdir = "../2008i/zoneinfo"
-        cal = PyCalendar()
-        for file in (checkName,):
-            fin = open(os.path.join(icsdir, file + ".ics"), "r")
-            cal.parse(fin)
-
-        for vtz in cal.getVTimezoneDB():
-            #from pycalendar.vtimezoneelement import PyCalendarVTimezoneElement
-            #vtz.mEmbedded.sort(PyCalendarVTimezoneElement.sort_dtstart)
-            for embedded in vtz.mEmbedded:
-                embedded.finalise()
-            vtz.finalise()
-
-        os = StringIO.StringIO()
-        cal.generate(os, False)
-        actual = os.getvalue()
-
-        print "-- ACTUAL --"
-        print actual
-        print
-        print "-- PARSED --"
-        print parsed
-        print
-        print "-- DIFF --"
-        print "\n".join([line for line in unified_diff(actual.split("\n"), parsed.split("\n"))])

Copied: PyCalendar/trunk/src/zonal/tzconvert.py (from rev 10553, PyCalendar/branches/server/src/zonal/tzconvert.py)
===================================================================
--- PyCalendar/trunk/src/zonal/tzconvert.py	                        (rev 0)
+++ PyCalendar/trunk/src/zonal/tzconvert.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,310 @@
+#!/usr/bin/env python
+##
+#    Copyright (c) 2007-2012 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 __future__ import with_statement
+
+from difflib import unified_diff
+from pycalendar.calendar import PyCalendar
+import cStringIO as StringIO
+import getopt
+import os
+import rule
+import sys
+import zone
+
+"""
+Classes to parse a tzdata files and generate VTIMEZONE data.
+"""
+
+__all__ = (
+    "tzconvert",
+)
+
+class tzconvert(object):
+
+    def __init__(self, verbose=False):
+        self.rules = {}
+        self.zones = {}
+        self.links = {}
+        self.verbose = verbose
+
+
+    def getZoneNames(self):
+        return set(self.zones.keys())
+
+
+    def parse(self, file):
+        try:
+            f = open(file, "r")
+            ctr = 0
+            for line in f:
+                ctr += 1
+                line = line[:-1]
+                while True:
+                    if line.startswith("#") or len(line) == 0:
+                        break
+                    elif line.startswith("Rule"):
+                        self.parseRule(line)
+                        break
+                    elif line.startswith("Zone"):
+                        line = self.parseZone(line, f)
+                        if line is None:
+                            break
+                    elif line.startswith("Link"):
+                        self.parseLink(line)
+                        break
+                    elif len(line.strip()) != 0:
+                        assert False, "Could not parse line %d from tzconvert file: '%s'" % (ctr, line,)
+                    else:
+                        break
+        except:
+            print "Failed to parse file %s" % (file,)
+            raise
+
+
+    def parseRule(self, line):
+        ruleitem = rule.Rule()
+        ruleitem.parse(line)
+        self.rules.setdefault(ruleitem.name, rule.RuleSet()).rules.append(ruleitem)
+
+
+    def parseZone(self, line, f):
+        os = StringIO.StringIO()
+        os.write(line)
+        last_line = None
+        for nextline in f:
+            nextline = nextline[:-1]
+            if nextline.startswith("\t"):
+                os.write("\n")
+                os.write(nextline)
+            elif nextline.startswith("#") or len(nextline) == 0:
+                continue
+            else:
+                last_line = nextline
+                break
+
+        zoneitem = zone.Zone()
+        zoneitem.parse(os.getvalue())
+        self.zones[zoneitem.name] = zoneitem
+
+        return last_line
+
+
+    def parseLink(self, line):
+
+        splits = line.split()
+        linkFrom = splits[1]
+        linkTo = splits[2]
+        self.links[linkTo] = linkFrom
+
+
+    def expandZone(self, zonename, minYear, maxYear=2018):
+        """
+        Expand a zones transition dates up to the specified year.
+        """
+        zone = self.zones[zonename]
+        expanded = zone.expand(self.rules, minYear, maxYear)
+        return [(item[0], item[1], item[2],) for item in expanded]
+
+
+    def vtimezones(self, minYear, maxYear=2018, filterzones=None):
+        """
+        Generate iCalendar data for all VTIMEZONEs or just those specified
+        """
+
+        cal = PyCalendar()
+        for zone in self.zones.itervalues():
+            if filterzones and zone.name not in filterzones:
+                continue
+            vtz = zone.vtimezone(cal, self.rules, minYear, maxYear)
+            cal.addComponent(vtz)
+
+        return cal.getText()
+
+
+    def generateZoneinfoFiles(self, outputdir, minYear, maxYear=2018, links=True, filterzones=None):
+
+        # Empty current directory
+        try:
+            for root, dirs, files in os.walk(outputdir, topdown=False):
+                for name in files:
+                    os.remove(os.path.join(root, name))
+                for name in dirs:
+                    os.rmdir(os.path.join(root, name))
+        except OSError:
+            pass
+
+        for zone in self.zones.itervalues():
+            if filterzones and zone.name not in filterzones:
+                continue
+            cal = PyCalendar()
+            vtz = zone.vtimezone(cal, self.rules, minYear, maxYear)
+            cal.addComponent(vtz)
+
+            icsdata = cal.getText()
+            fpath = os.path.join(outputdir, zone.name + ".ics")
+            if not os.path.exists(os.path.dirname(fpath)):
+                os.makedirs(os.path.dirname(fpath))
+            with open(fpath, "w") as f:
+                f.write(icsdata)
+            if self.verbose:
+                print "Write path: %s" % (fpath,)
+
+        if links:
+            link_list = []
+            for linkTo, linkFrom in self.links.iteritems():
+
+                # Check for existing output file
+                fromPath = os.path.join(outputdir, linkFrom + ".ics")
+                if not os.path.exists(fromPath):
+                    print "Missing link from: %s to %s" % (linkFrom, linkTo,)
+                    continue
+
+                with open(fromPath) as f:
+                    icsdata = f.read()
+                icsdata = icsdata.replace(linkFrom, linkTo)
+
+                toPath = os.path.join(outputdir, linkTo + ".ics")
+                if not os.path.exists(os.path.dirname(toPath)):
+                    os.makedirs(os.path.dirname(toPath))
+                with open(toPath, "w") as f:
+                    f.write(icsdata)
+                if self.verbose:
+                    print "Write link: %s" % (linkTo,)
+
+                link_list.append("%s\t%s" % (linkTo, linkFrom,))
+
+            # Generate link mapping file
+            linkPath = os.path.join(outputdir, "links.txt")
+            open(linkPath, "w").write("\n".join(link_list))
+
+
+
+def usage(error_msg=None):
+    if error_msg:
+        print error_msg
+
+    print """Usage: tzconvert [options] [DIR]
+Options:
+    -h            Print this help and exit
+    --prodid      PROD-ID string to use
+    --start       Start year
+    --end         End year
+
+Arguments:
+    DIR      Directory containing an Olson tzdata directory to read, also
+             where zoneinfo data will be written
+
+Description:
+    This utility convert Olson-style timezone data in iCalendar.
+    VTIMEZONE objects, one .ics file per-timezone.
+
+"""
+
+    if error_msg:
+        raise ValueError(error_msg)
+    else:
+        sys.exit(0)
+
+
+if __name__ == '__main__':
+
+    # Set the PRODID value used in generated iCalendar data
+    prodid = "-//mulberrymail.com//Zonal//EN"
+    rootdir = "../../stuff/temp"
+    startYear = 1800
+    endYear = 2018
+
+    options, args = getopt.getopt(sys.argv[1:], "h", ["prodid=", "root=", "start=", "end=", ])
+
+    for option, value in options:
+        if option == "-h":
+            usage()
+        elif option == "--prodid":
+            prodid = value
+        elif option == "--root":
+            rootdir = value
+        elif option == "--start":
+            startYear = int(value)
+        elif option == "--end":
+            endYear = int(value)
+        else:
+            usage("Unrecognized option: %s" % (option,))
+
+    # Process arguments
+    if len(args) > 1:
+        usage("Must have only one argument")
+    if len(args) == 1:
+        rootdir = os.path.expanduser(args[0])
+
+    PyCalendar.sProdID = prodid
+
+    zonedir = os.path.join(rootdir, "tzdata")
+    zonefiles = (
+        "northamerica",
+        "southamerica",
+        "europe",
+        "africa",
+        "asia",
+        "australasia",
+        "antarctica",
+        "etcetera",
+        "backward",
+    )
+
+    parser = tzconvert(verbose=True)
+    for file in zonefiles:
+        parser.parse(os.path.join(zonedir, file))
+
+    if 1:
+        parser.generateZoneinfoFiles(os.path.join(rootdir, "zoneinfo"), startYear, endYear, filterzones=(
+            #"America/Montevideo",
+            #"Europe/Paris",
+            #"Africa/Cairo",
+        ))
+
+    if 0:
+        checkName = "EST"
+        parsed = parser.vtimezones(1800, 2018, filterzones=(
+            checkName,
+        ))
+
+        icsdir = "../2008i/zoneinfo"
+        cal = PyCalendar()
+        for file in (checkName,):
+            fin = open(os.path.join(icsdir, file + ".ics"), "r")
+            cal.parse(fin)
+
+        for vtz in cal.getVTimezoneDB():
+            #from pycalendar.vtimezoneelement import PyCalendarVTimezoneElement
+            #vtz.mEmbedded.sort(PyCalendarVTimezoneElement.sort_dtstart)
+            for embedded in vtz.mEmbedded:
+                embedded.finalise()
+            vtz.finalise()
+
+        os = StringIO.StringIO()
+        cal.generate(os, False)
+        actual = os.getvalue()
+
+        print "-- ACTUAL --"
+        print actual
+        print
+        print "-- PARSED --"
+        print parsed
+        print
+        print "-- DIFF --"
+        print "\n".join([line for line in unified_diff(actual.split("\n"), parsed.split("\n"))])

Deleted: PyCalendar/trunk/src/zonal/tzdump.py
===================================================================
--- PyCalendar/branches/server/src/zonal/tzdump.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/zonal/tzdump.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,133 +0,0 @@
-#!/usr/bin/env python
-##
-#    Copyright (c) 2007-2012 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.calendar import PyCalendar
-from pycalendar.datetime import PyCalendarDateTime
-import os
-import sys
-import getopt
-from pycalendar.exceptions import PyCalendarInvalidData
-
-def loadCalendar(file, verbose):
-
-    cal = PyCalendar()
-    if verbose:
-        print "Parsing calendar data: %s" % (file,)
-    fin = open(file, "r")
-    try:
-        cal.parse(fin)
-    except PyCalendarInvalidData, e:
-        print "Failed to parse bad data: %s" % (e.mData,)
-        raise
-    return cal
-
-
-
-def getExpandedDates(cal, start, end):
-
-    vtz = cal.getComponents()[0]
-    expanded = vtz.expandAll(start, end)
-    expanded.sort(cmp=lambda x, y: PyCalendarDateTime.sort(x[0], y[0]))
-    return expanded
-
-
-
-def formattedExpandedDates(expanded):
-    items = []
-    for item in expanded:
-        utc = item[0].duplicate()
-        utc.setTimezoneUTC(True)
-        utc.offsetSeconds(-item[1])
-        items.append((item[0], utc, secondsToTime(item[1]), secondsToTime(item[2]),))
-    return "\n".join(["(%s, %s, %s, %s)" % item for item in items])
-
-
-
-def secondsToTime(seconds):
-    if seconds < 0:
-        seconds = -seconds
-        negative = "-"
-    else:
-        negative = ""
-    secs = divmod(seconds, 60)[1]
-    mins = divmod(seconds / 60, 60)[1]
-    hours = divmod(seconds / (60 * 60), 60)[1]
-    if secs:
-        return "%s%02d:%02d:%02d" % (negative, hours, mins, secs,)
-    else:
-        return "%s%02d:%02d" % (negative, hours, mins,)
-
-
-
-def usage(error_msg=None):
-    if error_msg:
-        print error_msg
-
-    print """Usage: tzdump [options] FILE
-Options:
-    -h            Print this help and exit
-    -v            Be verbose
-    --start       Start year
-    --end         End year
-
-Arguments:
-    FILE          iCalendar file containing a single VTIMEZONE
-
-Description:
-    This utility will dump the transitions in a VTIMEZONE over
-    the request time range.
-
-"""
-
-    if error_msg:
-        raise ValueError(error_msg)
-    else:
-        sys.exit(0)
-
-
-if __name__ == '__main__':
-
-    verbose = False
-    startYear = 1918
-    endYear = 2018
-    fpath = None
-
-    options, args = getopt.getopt(sys.argv[1:], "hv", ["start=", "end=", ])
-
-    for option, value in options:
-        if option == "-h":
-            usage()
-        elif option == "-v":
-            verbose = True
-        elif option == "--start":
-            startYear = int(value)
-        elif option == "--end":
-            endYear = int(value)
-        else:
-            usage("Unrecognized option: %s" % (option,))
-
-    # Process arguments
-    if len(args) != 1:
-        usage("Must have one argument")
-    fpath = os.path.expanduser(args[0])
-
-    start = PyCalendarDateTime(year=startYear, month=1, day=1)
-    end = PyCalendarDateTime(year=endYear, month=1, day=1)
-
-    cal = loadCalendar(fpath, verbose)
-    dates = getExpandedDates(cal, start, end)
-    print formattedExpandedDates(dates)

Copied: PyCalendar/trunk/src/zonal/tzdump.py (from rev 10553, PyCalendar/branches/server/src/zonal/tzdump.py)
===================================================================
--- PyCalendar/trunk/src/zonal/tzdump.py	                        (rev 0)
+++ PyCalendar/trunk/src/zonal/tzdump.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,133 @@
+#!/usr/bin/env python
+##
+#    Copyright (c) 2007-2012 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.calendar import PyCalendar
+from pycalendar.datetime import PyCalendarDateTime
+import os
+import sys
+import getopt
+from pycalendar.exceptions import PyCalendarInvalidData
+
+def loadCalendar(file, verbose):
+
+    cal = PyCalendar()
+    if verbose:
+        print "Parsing calendar data: %s" % (file,)
+    fin = open(file, "r")
+    try:
+        cal.parse(fin)
+    except PyCalendarInvalidData, e:
+        print "Failed to parse bad data: %s" % (e.mData,)
+        raise
+    return cal
+
+
+
+def getExpandedDates(cal, start, end):
+
+    vtz = cal.getComponents()[0]
+    expanded = vtz.expandAll(start, end)
+    expanded.sort(cmp=lambda x, y: PyCalendarDateTime.sort(x[0], y[0]))
+    return expanded
+
+
+
+def formattedExpandedDates(expanded):
+    items = []
+    for item in expanded:
+        utc = item[0].duplicate()
+        utc.setTimezoneUTC(True)
+        utc.offsetSeconds(-item[1])
+        items.append((item[0], utc, secondsToTime(item[1]), secondsToTime(item[2]),))
+    return "\n".join(["(%s, %s, %s, %s)" % item for item in items])
+
+
+
+def secondsToTime(seconds):
+    if seconds < 0:
+        seconds = -seconds
+        negative = "-"
+    else:
+        negative = ""
+    secs = divmod(seconds, 60)[1]
+    mins = divmod(seconds / 60, 60)[1]
+    hours = divmod(seconds / (60 * 60), 60)[1]
+    if secs:
+        return "%s%02d:%02d:%02d" % (negative, hours, mins, secs,)
+    else:
+        return "%s%02d:%02d" % (negative, hours, mins,)
+
+
+
+def usage(error_msg=None):
+    if error_msg:
+        print error_msg
+
+    print """Usage: tzdump [options] FILE
+Options:
+    -h            Print this help and exit
+    -v            Be verbose
+    --start       Start year
+    --end         End year
+
+Arguments:
+    FILE          iCalendar file containing a single VTIMEZONE
+
+Description:
+    This utility will dump the transitions in a VTIMEZONE over
+    the request time range.
+
+"""
+
+    if error_msg:
+        raise ValueError(error_msg)
+    else:
+        sys.exit(0)
+
+
+if __name__ == '__main__':
+
+    verbose = False
+    startYear = 1918
+    endYear = 2018
+    fpath = None
+
+    options, args = getopt.getopt(sys.argv[1:], "hv", ["start=", "end=", ])
+
+    for option, value in options:
+        if option == "-h":
+            usage()
+        elif option == "-v":
+            verbose = True
+        elif option == "--start":
+            startYear = int(value)
+        elif option == "--end":
+            endYear = int(value)
+        else:
+            usage("Unrecognized option: %s" % (option,))
+
+    # Process arguments
+    if len(args) != 1:
+        usage("Must have one argument")
+    fpath = os.path.expanduser(args[0])
+
+    start = PyCalendarDateTime(year=startYear, month=1, day=1)
+    end = PyCalendarDateTime(year=endYear, month=1, day=1)
+
+    cal = loadCalendar(fpath, verbose)
+    dates = getExpandedDates(cal, start, end)
+    print formattedExpandedDates(dates)

Deleted: PyCalendar/trunk/src/zonal/tzverify.py
===================================================================
--- PyCalendar/branches/server/src/zonal/tzverify.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/zonal/tzverify.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,283 +0,0 @@
-#!/usr/bin/env python
-##
-#    Copyright (c) 2007-2012 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.calendar import PyCalendar
-from pycalendar.datetime import PyCalendarDateTime
-from tzconvert import tzconvert
-import os
-import sys
-import getopt
-from pycalendar.exceptions import PyCalendarInvalidData
-
-def loadCalendarFromZoneinfo(zoneinfopath, skips=(), verbose=False, quiet=False):
-
-    if not quiet:
-        print "Scanning for calendar data in: %s" % (zoneinfopath,)
-    paths = []
-    def scanForICS(dirpath):
-        for fname in os.listdir(dirpath):
-            fpath = os.path.join(dirpath, fname)
-            if os.path.isdir(fpath):
-                scanForICS(fpath)
-            elif fname.endswith(".ics"):
-                for skip in skips:
-                    if skip in fpath:
-                        break
-                else:
-                    if verbose:
-                        print "Found calendar data: %s" % (fpath,)
-                    paths.append(fpath)
-    scanForICS(zoneinfopath)
-
-    if not quiet:
-        print "Parsing calendar data in: %s" % (zoneinfopath,)
-    return loadCalendar(paths, verbose)
-
-
-
-def loadCalendar(files, verbose):
-
-    cal = PyCalendar()
-    for file in files:
-        if verbose:
-            print "Parsing calendar data: %s" % (file,)
-        fin = open(file, "r")
-        try:
-            cal.parse(fin)
-        except PyCalendarInvalidData, e:
-            print "Failed to parse bad data: %s" % (e.mData,)
-            raise
-    return CalendarZonesWrapper(calendar=cal)
-
-
-
-def parseTZData(zonedir, zonefiles):
-    parser = tzconvert()
-    for file in zonefiles:
-        zonefile = os.path.join(zonedir, file)
-        if not os.path.exists(zonefile):
-            print "Zone file '%s' does not exist." % (zonefile,)
-        parser.parse(zonefile)
-    return CalendarZonesWrapper(zones=parser)
-
-
-
-class CalendarZonesWrapper(object):
-
-    def __init__(self, calendar=None, zones=None):
-        self.calendar = calendar
-        self.zones = zones
-        assert self.calendar is not None or self.zones is not None
-
-
-    def getTZIDs(self):
-        if self.calendar:
-            return getTZIDs(self.calendar)
-        elif self.zones:
-            return self.zones.getZoneNames()
-
-
-    def expandTransitions(self, tzid, start, end):
-        if self.calendar:
-            return getExpandedDates(self.calendar, tzid, start, end)
-        elif self.zones:
-            return self.zones.expandZone(tzid, start.getYear(), end.getYear())
-
-
-
-def compareCalendars(calendar1, calendar2, start, end, filterTzids=(), verbose=False, quiet=False):
-
-    # Get all TZIDs from the calendar
-    tzids1 = calendar1.getTZIDs()
-    tzids2 = calendar2.getTZIDs()
-
-    # Find TZIDs that do not have a corresponding zone
-    missing = tzids1.difference(tzids2)
-    if missing:
-        print """TZIDs in calendar 1 not in calendar 2 files: %s
-These cannot be checked.""" % (", ".join(missing),)
-
-    for tzid in tzids1.intersection(tzids2):
-        if filterTzids and tzid not in filterTzids:
-            continue
-        if not quiet:
-            print "\nChecking TZID: %s" % (tzid,)
-        calendardates1 = calendar1.expandTransitions(tzid, start, end)
-        calendardates2 = calendar2.expandTransitions(tzid, start, end)
-        if verbose:
-            print "Calendar 1 dates: %s" % (formattedExpandedDates(calendardates1),)
-            print "Calendar 2 dates: %s" % (formattedExpandedDates(calendardates2),)
-        set1 = set(calendardates1)
-        set2 = set(calendardates2)
-        d1 = set1.difference(set2)
-        for i in set(d1):
-            if i[0] == start:
-                d1.discard(i)
-                break
-            if i[1] == i[2]:
-                d1.discard(i)
-        d2 = set2.difference(set1)
-        for i in set(d2):
-            if i[1] == i[2]:
-                d2.discard(i)
-        if d1:
-            print "In calendar 1 but not in calendar 2 tzid=%s: %s" % (tzid, formattedExpandedDates(d1),)
-        if d2:
-            print "In calendar 2 but not in calendar 1 tzid=%s: %s" % (tzid, formattedExpandedDates(d2),)
-        if not d1 and not d2 and not quiet:
-            print "Matched: %s" % (tzid,)
-
-
-
-def getTZIDs(cal):
-    results = set()
-
-    db = cal.getVTimezoneDB()
-    for vtz in db:
-        tzid = vtz.getID()
-        results.add(tzid)
-
-    return results
-
-
-
-def getExpandedDates(cal, tzid, start, end):
-
-    db = cal.getVTimezoneDB()
-    return db[tzid].expandAll(start, end)
-
-
-
-def sortedList(setdata):
-    l = list(setdata)
-    l.sort(cmp=lambda x, y: PyCalendarDateTime.sort(x[0], y[0]))
-    return l
-
-
-
-def formattedExpandedDates(expanded):
-    items = sortedList([(item[0], secondsToTime(item[1]), secondsToTime(item[2]),) for item in expanded])
-    return ", ".join(["(%s, %s, %s)" % item for item in items])
-
-
-
-def secondsToTime(seconds):
-    if seconds < 0:
-        seconds = -seconds
-        negative = "-"
-    else:
-        negative = ""
-    secs = divmod(seconds, 60)[1]
-    mins = divmod(seconds / 60, 60)[1]
-    hours = divmod(seconds / (60 * 60), 60)[1]
-    if secs:
-        return "%s%02d:%02d:%02d" % (negative, hours, mins, secs,)
-    else:
-        return "%s%02d:%02d" % (negative, hours, mins,)
-
-
-
-def usage(error_msg=None):
-    if error_msg:
-        print error_msg
-
-    print """Usage: tzverify [options] DIR1 DIR2
-Options:
-    -h            Print this help and exit
-    -v            Be verbose
-    -q            Be quiet
-    --start       Start year
-    --end         End year
-
-Arguments:
-    DIR1          Directories containing two sets of zoneinfo data
-    DIR2          to be compared
-
-Description:
-    This utility will compare iCalendar zoneinfo hierarchies by expanding
-    timezone transitions and comparing.
-
-"""
-
-    if error_msg:
-        raise ValueError(error_msg)
-    else:
-        sys.exit(0)
-
-if __name__ == '__main__':
-
-    verbose = False
-    quiet = False
-    startYear = 1933
-    endYear = 2018
-    zonedir1 = None
-    zonedir2 = None
-
-    options, args = getopt.getopt(sys.argv[1:], "hvq", ["start=", "end=", ])
-
-    for option, value in options:
-        if option == "-h":
-            usage()
-        elif option == "-v":
-            verbose = True
-        elif option == "-q":
-            quiet = True
-        elif option == "--start":
-            startYear = int(value)
-        elif option == "--end":
-            endYear = int(value)
-        else:
-            usage("Unrecognized option: %s" % (option,))
-
-    # Process arguments
-    if len(args) != 2:
-        usage("Must have two arguments")
-    zonedir1 = os.path.expanduser(args[0])
-    zonedir2 = os.path.expanduser(args[1])
-
-    start = PyCalendarDateTime(year=startYear, month=1, day=1)
-    end = PyCalendarDateTime(year=endYear, month=1, day=1)
-
-    zonefiles = (
-        "northamerica",
-        "southamerica",
-        "europe",
-        "africa",
-        "asia",
-        "australasia",
-        "antarctica",
-    )
-
-    skips = (
-        #"Europe/Sofia",
-        #"Africa/Cairo",
-    )
-
-    checkcalendar1 = loadCalendarFromZoneinfo(zonedir1, skips, verbose, quiet)
-    checkcalendar2 = loadCalendarFromZoneinfo(zonedir2, skips, verbose, quiet)
-
-    compareCalendars(
-        checkcalendar1,
-        checkcalendar2,
-        start,
-        end,
-        filterTzids=(
-            #"America/Goose_Bay",
-        ),
-        verbose=verbose,
-        quiet=quiet,
-    )

Copied: PyCalendar/trunk/src/zonal/tzverify.py (from rev 10553, PyCalendar/branches/server/src/zonal/tzverify.py)
===================================================================
--- PyCalendar/trunk/src/zonal/tzverify.py	                        (rev 0)
+++ PyCalendar/trunk/src/zonal/tzverify.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,283 @@
+#!/usr/bin/env python
+##
+#    Copyright (c) 2007-2012 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.calendar import PyCalendar
+from pycalendar.datetime import PyCalendarDateTime
+from tzconvert import tzconvert
+import os
+import sys
+import getopt
+from pycalendar.exceptions import PyCalendarInvalidData
+
+def loadCalendarFromZoneinfo(zoneinfopath, skips=(), verbose=False, quiet=False):
+
+    if not quiet:
+        print "Scanning for calendar data in: %s" % (zoneinfopath,)
+    paths = []
+    def scanForICS(dirpath):
+        for fname in os.listdir(dirpath):
+            fpath = os.path.join(dirpath, fname)
+            if os.path.isdir(fpath):
+                scanForICS(fpath)
+            elif fname.endswith(".ics"):
+                for skip in skips:
+                    if skip in fpath:
+                        break
+                else:
+                    if verbose:
+                        print "Found calendar data: %s" % (fpath,)
+                    paths.append(fpath)
+    scanForICS(zoneinfopath)
+
+    if not quiet:
+        print "Parsing calendar data in: %s" % (zoneinfopath,)
+    return loadCalendar(paths, verbose)
+
+
+
+def loadCalendar(files, verbose):
+
+    cal = PyCalendar()
+    for file in files:
+        if verbose:
+            print "Parsing calendar data: %s" % (file,)
+        fin = open(file, "r")
+        try:
+            cal.parse(fin)
+        except PyCalendarInvalidData, e:
+            print "Failed to parse bad data: %s" % (e.mData,)
+            raise
+    return CalendarZonesWrapper(calendar=cal)
+
+
+
+def parseTZData(zonedir, zonefiles):
+    parser = tzconvert()
+    for file in zonefiles:
+        zonefile = os.path.join(zonedir, file)
+        if not os.path.exists(zonefile):
+            print "Zone file '%s' does not exist." % (zonefile,)
+        parser.parse(zonefile)
+    return CalendarZonesWrapper(zones=parser)
+
+
+
+class CalendarZonesWrapper(object):
+
+    def __init__(self, calendar=None, zones=None):
+        self.calendar = calendar
+        self.zones = zones
+        assert self.calendar is not None or self.zones is not None
+
+
+    def getTZIDs(self):
+        if self.calendar:
+            return getTZIDs(self.calendar)
+        elif self.zones:
+            return self.zones.getZoneNames()
+
+
+    def expandTransitions(self, tzid, start, end):
+        if self.calendar:
+            return getExpandedDates(self.calendar, tzid, start, end)
+        elif self.zones:
+            return self.zones.expandZone(tzid, start.getYear(), end.getYear())
+
+
+
+def compareCalendars(calendar1, calendar2, start, end, filterTzids=(), verbose=False, quiet=False):
+
+    # Get all TZIDs from the calendar
+    tzids1 = calendar1.getTZIDs()
+    tzids2 = calendar2.getTZIDs()
+
+    # Find TZIDs that do not have a corresponding zone
+    missing = tzids1.difference(tzids2)
+    if missing:
+        print """TZIDs in calendar 1 not in calendar 2 files: %s
+These cannot be checked.""" % (", ".join(missing),)
+
+    for tzid in tzids1.intersection(tzids2):
+        if filterTzids and tzid not in filterTzids:
+            continue
+        if not quiet:
+            print "\nChecking TZID: %s" % (tzid,)
+        calendardates1 = calendar1.expandTransitions(tzid, start, end)
+        calendardates2 = calendar2.expandTransitions(tzid, start, end)
+        if verbose:
+            print "Calendar 1 dates: %s" % (formattedExpandedDates(calendardates1),)
+            print "Calendar 2 dates: %s" % (formattedExpandedDates(calendardates2),)
+        set1 = set(calendardates1)
+        set2 = set(calendardates2)
+        d1 = set1.difference(set2)
+        for i in set(d1):
+            if i[0] == start:
+                d1.discard(i)
+                break
+            if i[1] == i[2]:
+                d1.discard(i)
+        d2 = set2.difference(set1)
+        for i in set(d2):
+            if i[1] == i[2]:
+                d2.discard(i)
+        if d1:
+            print "In calendar 1 but not in calendar 2 tzid=%s: %s" % (tzid, formattedExpandedDates(d1),)
+        if d2:
+            print "In calendar 2 but not in calendar 1 tzid=%s: %s" % (tzid, formattedExpandedDates(d2),)
+        if not d1 and not d2 and not quiet:
+            print "Matched: %s" % (tzid,)
+
+
+
+def getTZIDs(cal):
+    results = set()
+
+    db = cal.getVTimezoneDB()
+    for vtz in db:
+        tzid = vtz.getID()
+        results.add(tzid)
+
+    return results
+
+
+
+def getExpandedDates(cal, tzid, start, end):
+
+    db = cal.getVTimezoneDB()
+    return db[tzid].expandAll(start, end)
+
+
+
+def sortedList(setdata):
+    l = list(setdata)
+    l.sort(cmp=lambda x, y: PyCalendarDateTime.sort(x[0], y[0]))
+    return l
+
+
+
+def formattedExpandedDates(expanded):
+    items = sortedList([(item[0], secondsToTime(item[1]), secondsToTime(item[2]),) for item in expanded])
+    return ", ".join(["(%s, %s, %s)" % item for item in items])
+
+
+
+def secondsToTime(seconds):
+    if seconds < 0:
+        seconds = -seconds
+        negative = "-"
+    else:
+        negative = ""
+    secs = divmod(seconds, 60)[1]
+    mins = divmod(seconds / 60, 60)[1]
+    hours = divmod(seconds / (60 * 60), 60)[1]
+    if secs:
+        return "%s%02d:%02d:%02d" % (negative, hours, mins, secs,)
+    else:
+        return "%s%02d:%02d" % (negative, hours, mins,)
+
+
+
+def usage(error_msg=None):
+    if error_msg:
+        print error_msg
+
+    print """Usage: tzverify [options] DIR1 DIR2
+Options:
+    -h            Print this help and exit
+    -v            Be verbose
+    -q            Be quiet
+    --start       Start year
+    --end         End year
+
+Arguments:
+    DIR1          Directories containing two sets of zoneinfo data
+    DIR2          to be compared
+
+Description:
+    This utility will compare iCalendar zoneinfo hierarchies by expanding
+    timezone transitions and comparing.
+
+"""
+
+    if error_msg:
+        raise ValueError(error_msg)
+    else:
+        sys.exit(0)
+
+if __name__ == '__main__':
+
+    verbose = False
+    quiet = False
+    startYear = 1933
+    endYear = 2018
+    zonedir1 = None
+    zonedir2 = None
+
+    options, args = getopt.getopt(sys.argv[1:], "hvq", ["start=", "end=", ])
+
+    for option, value in options:
+        if option == "-h":
+            usage()
+        elif option == "-v":
+            verbose = True
+        elif option == "-q":
+            quiet = True
+        elif option == "--start":
+            startYear = int(value)
+        elif option == "--end":
+            endYear = int(value)
+        else:
+            usage("Unrecognized option: %s" % (option,))
+
+    # Process arguments
+    if len(args) != 2:
+        usage("Must have two arguments")
+    zonedir1 = os.path.expanduser(args[0])
+    zonedir2 = os.path.expanduser(args[1])
+
+    start = PyCalendarDateTime(year=startYear, month=1, day=1)
+    end = PyCalendarDateTime(year=endYear, month=1, day=1)
+
+    zonefiles = (
+        "northamerica",
+        "southamerica",
+        "europe",
+        "africa",
+        "asia",
+        "australasia",
+        "antarctica",
+    )
+
+    skips = (
+        #"Europe/Sofia",
+        #"Africa/Cairo",
+    )
+
+    checkcalendar1 = loadCalendarFromZoneinfo(zonedir1, skips, verbose, quiet)
+    checkcalendar2 = loadCalendarFromZoneinfo(zonedir2, skips, verbose, quiet)
+
+    compareCalendars(
+        checkcalendar1,
+        checkcalendar2,
+        start,
+        end,
+        filterTzids=(
+            #"America/Goose_Bay",
+        ),
+        verbose=verbose,
+        quiet=quiet,
+    )

Deleted: PyCalendar/trunk/src/zonal/utils.py
===================================================================
--- PyCalendar/branches/server/src/zonal/utils.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/zonal/utils.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,53 +0,0 @@
-##
-#    Copyright (c) 2007-2012 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.
-##
-
-class DateTime(object):
-    """
-    A date-time object that wraps the tzdb wall-clock/utc style date-time information
-    and that can generate appropriate localtime or UTC offsets based on Zone/Rule offsets.
-    """
-
-    def __init__(self, dt, mode):
-        self.dt = dt
-        self.mode = mode
-
-
-    def __repr__(self):
-        return str(self.dt)
-
-
-    def compareDateTime(self, other):
-        return self.dt.compareDateTime(other.dt)
-
-
-    def getLocaltime(self, offset, stdoffset):
-        new_dt = self.dt.duplicate()
-        if self.mode == "u":
-            new_dt.offsetSeconds(offset)
-        elif self.mode == "s":
-            new_dt.offsetSeconds(-stdoffset + offset)
-        return new_dt
-
-
-    def getUTC(self, offset, stdoffset):
-        new_dt = self.dt.duplicate()
-        if self.mode == "u":
-            pass
-        elif self.mode == "s":
-            new_dt.offsetSeconds(-stdoffset)
-        else:
-            new_dt.offsetSeconds(-offset)
-        return new_dt

Copied: PyCalendar/trunk/src/zonal/utils.py (from rev 10553, PyCalendar/branches/server/src/zonal/utils.py)
===================================================================
--- PyCalendar/trunk/src/zonal/utils.py	                        (rev 0)
+++ PyCalendar/trunk/src/zonal/utils.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,53 @@
+##
+#    Copyright (c) 2007-2012 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.
+##
+
+class DateTime(object):
+    """
+    A date-time object that wraps the tzdb wall-clock/utc style date-time information
+    and that can generate appropriate localtime or UTC offsets based on Zone/Rule offsets.
+    """
+
+    def __init__(self, dt, mode):
+        self.dt = dt
+        self.mode = mode
+
+
+    def __repr__(self):
+        return str(self.dt)
+
+
+    def compareDateTime(self, other):
+        return self.dt.compareDateTime(other.dt)
+
+
+    def getLocaltime(self, offset, stdoffset):
+        new_dt = self.dt.duplicate()
+        if self.mode == "u":
+            new_dt.offsetSeconds(offset)
+        elif self.mode == "s":
+            new_dt.offsetSeconds(-stdoffset + offset)
+        return new_dt
+
+
+    def getUTC(self, offset, stdoffset):
+        new_dt = self.dt.duplicate()
+        if self.mode == "u":
+            pass
+        elif self.mode == "s":
+            new_dt.offsetSeconds(-stdoffset)
+        else:
+            new_dt.offsetSeconds(-offset)
+        return new_dt

Deleted: PyCalendar/trunk/src/zonal/zone.py
===================================================================
--- PyCalendar/branches/server/src/zonal/zone.py	2013-01-26 17:29:10 UTC (rev 10553)
+++ PyCalendar/trunk/src/zonal/zone.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -1,548 +0,0 @@
-##
-#    Copyright (c) 2007-2012 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 PyCalendarDateTime
-from pycalendar.vtimezone import PyCalendarVTimezone
-from pycalendar.property import PyCalendarProperty
-from pycalendar import definitions
-from pycalendar.vtimezonestandard import PyCalendarVTimezoneStandard
-from pycalendar.utcoffsetvalue import PyCalendarUTCOffsetValue
-import utils
-import rule
-
-"""
-Class that maintains a TZ data Zone.
-"""
-
-__all__ = (
-    "Zone",
-    "ZoneRule",
-)
-
-class Zone(object):
-    """
-    A tzdata Zone object containing a set of ZoneRules
-    """
-
-    def __init__(self):
-        self.name = ""
-        self.rules = []
-
-
-    def __str__(self):
-        return self.generate()
-
-
-    def __eq__(self, other):
-        return other and (
-            self.name == other.name and
-            self.rules == other.rules
-        )
-
-
-    def __ne__(self, other):
-        return not self.__eq__(other)
-
-
-    def parse(self, lines):
-        """
-        Parse the Zone lines from tzdata.
-
-        @param lines: the lines to parse.
-        @type lines: C{str}
-        """
-
-        # Parse one line at a time
-        splitlines = lines.split("\n")
-
-        # First line is special
-        line = splitlines[0]
-        splits = [x for x in line.expandtabs(1).split(" ") if len(x) > 0]
-        self.name = splits[1]
-        rule = ZoneRule(self)
-        rule.parse(line, 0)
-        self.rules.append(rule)
-        for line in splitlines[1:]:
-            if len(line) == 0:
-                continue
-            rule = ZoneRule(self)
-            rule.parse(line, 2)
-            if rule.gmtoff != "#":
-                self.rules.append(rule)
-
-
-    def generate(self):
-        """
-        Generate a partial Zone line.
-
-        @return: a C{str} with the Rule.
-        """
-
-        lines = []
-        for count, rule in enumerate(self.rules):
-            if count == 0:
-                items = (
-                    "Zone " + self.name,
-                    rule.generate(),
-                )
-            else:
-                items = (
-                    "",
-                    "",
-                    "",
-                    rule.generate(),
-                )
-            lines.append("\t".join(items))
-        return "\n".join(lines)
-
-
-    def expand(self, rules, minYear, maxYear):
-        """
-        Expand this zone into a set of transitions.
-
-        @param rules: parsed Rules for the tzdb
-        @type rules: C{dict}
-        @param minYear: starting year
-        @type minYear: C{int}
-        @param maxYear: ending year
-        @type maxYear: C{int}
-
-        @return: C{list} of C{tuple} for (
-            transition date-time,
-            offset to,
-            offset from,
-            associated rule,
-        )
-        """
-
-        # Start at 1/1/1800 with the offset from the initial zone rule
-        start = PyCalendarDateTime(year=1800, month=1, day=1, hours=0, minutes=0, seconds=0)
-        start_offset = self.rules[0].getUTCOffset()
-        start_stdoffset = self.rules[0].getUTCOffset()
-        startdt = start.duplicate()
-
-        # Now add each zone rules dates
-        transitions = []
-        lastUntilDateUTC = start.duplicate()
-        last_offset = start_offset
-        last_stdoffset = start_stdoffset
-        first = True
-        for zonerule in self.rules:
-            last_offset, last_stdoffset = zonerule.expand(rules, transitions, lastUntilDateUTC, last_offset, last_stdoffset, maxYear)
-            lastUntilDate = zonerule.getUntilDate()
-            lastUntilDateUTC = lastUntilDate.getUTC(last_offset, last_stdoffset)
-
-            # We typically don't care about the initial one
-            if first and len(self.rules) > 1:
-                transitions = []
-                first = False
-
-        # Sort the results by date
-        transitions.sort(cmp=lambda x, y: x[0].compareDateTime(y[0]))
-
-        # Now scan transitions looking for real changes and note those
-        results = []
-        last_transition = (startdt, start_offset, start_offset)
-        for transition in transitions:
-            dtutc, to_offset, zonerule, rule = transition
-            dt = dtutc.duplicate()
-            dt.offsetSeconds(last_transition[1])
-
-            if dtutc.getYear() >= minYear:
-                if dt > last_transition[0]:
-                    results.append((dt, last_transition[1], to_offset, zonerule, rule))
-                elif dt <= last_transition[0]:
-                    if len(results):
-                        results[-1] = ((results[-1][0], results[-1][1], to_offset, zonerule, None))
-                    else:
-                        results.append((last_transition[0], last_transition[1], last_transition[2], zonerule, None))
-            last_transition = (dt, to_offset, last_transition[2], rule)
-
-        return results
-
-
-    def vtimezone(self, calendar, rules, minYear, maxYear):
-        """
-        Generate a VTIMEZONE for this Zone.
-
-        @param calendar: the L{PyCalendar} object for the VCALENDAR in which the VTIMEZONE
-            will be created.
-        @param rules: the C{dict} containing the set of Rules currently defined.
-        @param startYear: a C{int} containing the first year that should be present
-            in the VTIMEZONE.
-        @return: C{vtimezone} component.
-        """
-
-        # Get a VTIMEZONE component
-        vtz = PyCalendarVTimezone(parent=calendar)
-
-        # Add TZID property
-        vtz.addProperty(PyCalendarProperty(definitions.cICalProperty_TZID, self.name))
-        vtz.addProperty(PyCalendarProperty("X-LIC-LOCATION", self.name))
-
-        transitions = self.expand(rules, minYear, maxYear)
-
-        # Group rules
-        lastZoneRule = None
-        ruleorder = []
-        rulemap = {}
-
-
-        def _generateRuleData():
-            # Generate VTIMEZONE component for last set of rules
-            for rule in ruleorder:
-                if rule:
-                    # Accumulate rule portions with the same offset pairs
-                    lastOffsetPair = (rulemap[rule][0][1], rulemap[rule][0][2],)
-                    startIndex = 0
-                    for index in xrange(len(rulemap[rule])):
-                        offsetPair = (rulemap[rule][index][1], rulemap[rule][index][2],)
-                        if offsetPair != lastOffsetPair:
-                            rule.vtimezone(
-                                vtz,
-                                lastZoneRule,
-                                rulemap[rule][startIndex][0],
-                                rulemap[rule][index - 1][0],
-                                rulemap[rule][startIndex][1],
-                                rulemap[rule][startIndex][2],
-                                index - startIndex,
-                            )
-                            lastOffsetPair = (rulemap[rule][index][1], rulemap[rule][index][2],)
-                            startIndex = index
-
-                    rule.vtimezone(
-                        vtz,
-                        lastZoneRule,
-                        rulemap[rule][startIndex][0],
-                        rulemap[rule][index][0],
-                        rulemap[rule][startIndex][1],
-                        rulemap[rule][startIndex][2],
-                        len(rulemap[rule]),
-                    )
-                else:
-                    lastZoneRule.vtimezone(
-                        vtz,
-                        lastZoneRule,
-                        rulemap[rule][0][0],
-                        rulemap[rule][-1][0],
-                        rulemap[rule][0][1],
-                        rulemap[rule][0][2],
-                    )
-            del ruleorder[:]
-            rulemap.clear()
-
-        for dt, offsetfrom, offsetto, zonerule, rule in transitions:
-
-            # Check for change of rule - we ignore LMT's
-            if zonerule.format != "LMT":
-                if lastZoneRule and lastZoneRule != zonerule:
-                    _generateRuleData()
-                if rule not in ruleorder:
-                    ruleorder.append(rule)
-                rulemap.setdefault(rule, []).append((dt, offsetfrom, offsetto,))
-            lastZoneRule = zonerule
-
-        # Do left overs
-        _generateRuleData()
-
-        self._compressRDateComponents(vtz)
-
-        vtz.finalise()
-        return vtz
-
-
-    def _compressRDateComponents(self, vtz):
-        """
-        Compress sub-components with RDATEs into a single component with multiple
-        RDATEs assuming all other properties are the same.
-
-        @param vtz: the VTIMEZONE object to compress
-        @type vtz: L{PyCalendarVTimezone}
-        """
-
-        # Map the similar sub-components together
-        similarMap = {}
-        for item in vtz.mComponents:
-            item.finalise()
-            key = (
-                item.getType(),
-                item.getTZName(),
-                item.getUTCOffset(),
-                item.getUTCOffsetFrom(),
-            )
-            if item.hasProperty(definitions.cICalProperty_RDATE):
-                similarMap.setdefault(key, []).append(item)
-
-        # Merge similar
-        for values in similarMap.itervalues():
-            if len(values) > 1:
-                mergeTo = values[0]
-                for mergeFrom in values[1:]:
-                    # Copy RDATE from to and remove from actual timezone
-                    prop = mergeFrom.getProperties()[definitions.cICalProperty_RDATE][0]
-                    mergeTo.addProperty(prop)
-                    vtz.mComponents.remove(mergeFrom)
-
-
-
-class ZoneRule(object):
-    """
-    A specific rule for a portion of a Zone
-    """
-
-    def __init__(self, zone):
-        self.zone = zone
-        self.gmtoff = 0
-        self.rule = ""
-        self.format = ""
-        self.until = None
-
-
-    def __str__(self):
-        return self.generate()
-
-
-    def __eq__(self, other):
-        return other and (
-            self.gmtoff == other.gmtoff and
-            self.rule == other.rule and
-            self.format == other.format and
-            self.until == other.until
-        )
-
-
-    def __ne__(self, other):
-        return not self.__eq__(other)
-
-
-    def parse(self, line, offset):
-        """
-        Parse the Zone line from tzdata.
-
-        @param line: a C{str} containing the line to parse.
-        """
-
-        splits = [x for x in line.expandtabs(1).split(" ") if len(x) > 0]
-        assert len(splits) + offset >= 5, "Help: %s" % (line,)
-        self.gmtoff = splits[2 - offset]
-        self.rule = splits[3 - offset]
-        self.format = splits[4 - offset]
-        if len(splits) >= 6 - offset:
-            self.until = " ".join(splits[5 - offset:])
-
-
-    def generate(self):
-        """
-        Generate a partial Zone line.
-
-        @return: a C{str} with the Rule.
-        """
-        items = (
-            self.gmtoff,
-            self.rule,
-            self.format,
-        )
-        if self.until:
-            items = items + (self.until,)
-        return "\t".join(items)
-
-
-    def getUntilDate(self):
-
-        if hasattr(self, "_cached_until"):
-            return self._cached_until
-
-        year = 9999
-        month = 12
-        day = 1
-        hours = 0
-        minutes = 0
-        seconds = 0
-        mode = None
-        if self.until and not self.until.startswith("#"):
-            splits = self.until.split(" ")
-            year = int(splits[0])
-            month = 1
-            day = 1
-            hours = 0
-            minutes = 0
-            seconds = 0
-            mode = None
-            if len(splits) > 1 and not splits[1].startswith("#"):
-                month = int(rule.Rule.MONTH_NAME_TO_POS[splits[1]])
-                if len(splits) > 2 and not splits[2].startswith("#"):
-                    if splits[2] == "lastSun":
-                        dt = PyCalendarDateTime(year=year, month=month, day=1)
-                        dt.setDayOfWeekInMonth(-1, PyCalendarDateTime.SUNDAY)
-                        splits[2] = dt.getDay()
-                    elif splits[2] == "lastSat":
-                        dt = PyCalendarDateTime(year=year, month=month, day=1)
-                        dt.setDayOfWeekInMonth(-1, PyCalendarDateTime.SATURDAY)
-                        splits[2] = dt.getDay()
-                    elif splits[2] == "Sun>=1":
-                        dt = PyCalendarDateTime(year=year, month=month, day=1)
-                        dt.setDayOfWeekInMonth(1, PyCalendarDateTime.SUNDAY)
-                        splits[2] = dt.getDay()
-                    day = int(splits[2])
-                    if len(splits) > 3 and not splits[3].startswith("#"):
-                        splits = splits[3].split(":")
-                        hours = int(splits[0])
-                        minutes = int(splits[1][:2])
-                        if len(splits[1]) > 2:
-                            mode = splits[1][2:]
-                        else:
-                            mode = None
-                        if len(splits) > 2:
-                            seconds = int(splits[2])
-
-        dt = PyCalendarDateTime(year=year, month=month, day=day, hours=hours, minutes=minutes, seconds=seconds)
-        self._cached_until = utils.DateTime(dt, mode)
-        return self._cached_until
-
-
-    def getUTCOffset(self):
-
-        if hasattr(self, "_cached_utc_offset"):
-            return self._cached_uutc_offset
-
-        splits = self.gmtoff.split(":")
-
-        hours = int(splits[0] if splits[0][0] != "-" else splits[0][1:])
-        minutes = int(splits[1]) if len(splits) > 1 else 0
-        seconds = int(splits[2]) if len(splits) > 2 else 0
-        negative = splits[0][0] == "-"
-        self._cached_uutc_offset = ((hours * 60) + minutes) * 60 + seconds
-        if negative:
-            self._cached_uutc_offset = -self._cached_uutc_offset
-        return self._cached_uutc_offset
-
-
-    def expand(self, rules, results, lastUntilUTC, lastOffset, lastStdOffset, maxYear):
-
-        # Expand the rule
-        assert self.rule == "-" or self.rule[0].isdigit() or self.rule in rules, "No rule '%s' found in cache. %s for %s" % (self.rule, self, self.zone,)
-        if self.rule == "-" or self.rule[0].isdigit():
-            return self.expand_norule(results, lastUntilUTC, maxYear)
-        else:
-            tempresults = []
-
-            ruleset = rules[self.rule]
-            ruleset.expand(tempresults, self, maxYear)
-
-            # Sort the results by date
-            tempresults.sort(cmp=lambda x, y: x[0].compareDateTime(y[0]))
-
-            found_one = False
-            found_start = False
-            last_offset = lastOffset
-            last_stdoffset = lastStdOffset
-            finalUntil = self.getUntilDate()
-            for dt, to_offset, rule in tempresults:
-                dtutc = dt.getUTC(last_offset, last_stdoffset)
-                if dtutc >= lastUntilUTC:
-                    if not found_start and dtutc != lastUntilUTC:
-                        # Insert a start item
-                        if not found_one:
-                            last_offset = self.getUTCOffset()
-                            last_stdoffset = self.getUTCOffset()
-                            dtutc = dt.getUTC(last_offset, last_stdoffset)
-                        results.append((lastUntilUTC, last_offset, self, None))
-                    found_start = True
-
-                    if dtutc >= finalUntil.getUTC(last_offset, last_stdoffset):
-                        break
-
-                    results.append((dtutc, to_offset, self, rule))
-
-                last_offset = to_offset
-                last_stdoffset = self.getUTCOffset()
-                found_one = True
-
-            if found_start == 0:
-                results.append((lastUntilUTC, last_offset, self, None))
-
-            return last_offset, last_stdoffset
-
-
-    def expand_norule(self, results, lastUntil, maxYear):
-        to_offset = 0
-        if self.rule[0].isdigit():
-            splits = self.rule.split(":")
-            to_offset = 60 * 60 * int(splits[0])
-            if len(splits) > 1:
-                to_offset += 60 * int(splits[1])
-
-        # Always add a transition for the start of this rule
-        results.append((lastUntil, self.getUTCOffset() + to_offset, self, None))
-        return (self.getUTCOffset() + to_offset, self.getUTCOffset())
-
-
-    def vtimezone(self, vtz, zonerule, start, end, offsetfrom, offsetto):
-
-        # Determine type of component based on offset
-        comp = PyCalendarVTimezoneStandard(parent=vtz)
-
-        # Do offsets
-        tzoffsetfrom = PyCalendarUTCOffsetValue(offsetfrom)
-        tzoffsetto = PyCalendarUTCOffsetValue(offsetto)
-
-        comp.addProperty(PyCalendarProperty(definitions.cICalProperty_TZOFFSETFROM, tzoffsetfrom))
-        comp.addProperty(PyCalendarProperty(definitions.cICalProperty_TZOFFSETTO, tzoffsetto))
-
-        # Do TZNAME
-        if self.format.find("%") != -1:
-            tzname = self.format % ("S",)
-        else:
-            tzname = self.format
-        comp.addProperty(PyCalendarProperty(definitions.cICalProperty_TZNAME, tzname))
-
-        # Do DTSTART
-        comp.addProperty(PyCalendarProperty(definitions.cICalProperty_DTSTART, start))
-
-        # Recurrence
-        comp.addProperty(PyCalendarProperty(definitions.cICalProperty_RDATE, start))
-
-        comp.finalise()
-        vtz.addComponent(comp)
-
-if __name__ == '__main__':
-    rulesdef = """Rule\tUS\t1918\t1919\t-\tMar\tlastSun\t2:00\t1:00\tD
-Rule\tUS\t1918\t1919\t-\tOct\tlastSun\t2:00\t0\tS
-Rule\tUS\t1942\tonly\t-\tFeb\t9\t2:00\t1:00\tW # War
-Rule\tUS\t1945\tonly\t-\tAug\t14\t23:00u\t1:00\tP # Peace
-Rule\tUS\t1945\tonly\t-\tSep\t30\t2:00\t0\tS
-Rule\tUS\t1967\t2006\t-\tOct\tlastSun\t2:00\t0\tS
-Rule\tUS\t1967\t1973\t-\tApr\tlastSun\t2:00\t1:00\tD
-Rule\tUS\t1974\tonly\t-\tJan\t6\t2:00\t1:00\tD
-Rule\tUS\t1975\tonly\t-\tFeb\t23\t2:00\t1:00\tD
-Rule\tUS\t1976\t1986\t-\tApr\tlastSun\t2:00\t1:00\tD
-Rule\tUS\t1987\t2006\t-\tApr\tSun>=1\t2:00\t1:00\tD
-Rule\tUS\t2007\tmax\t-\tMar\tSun>=8\t2:00\t1:00\tD
-Rule\tUS\t2007\tmax\t-\tNov\tSun>=1\t2:00\t0\tS"""
-    rules = {}
-    import rule
-    ruleset = rule.RuleSet()
-    ruleset.parse(rulesdef)
-    rules[ruleset.name] = ruleset
-
-    zonedef = """Zone America/New_York -4:56:02\t-\tLMT\t1883 Nov 18 12:03:58
-\t\t\t-5:00\tUS\tE%sT\t1920
-\t\t\t-5:00\tNYC\tE%sT\t1942
-\t\t\t-5:00\tUS\tE%sT\t1946
-\t\t\t-5:00\tNYC\tE%sT\t1967
-\t\t\t-5:00\tUS\tE%sT"""
-    zone = Zone()
-    zone.parse(zonedef)

Copied: PyCalendar/trunk/src/zonal/zone.py (from rev 10553, PyCalendar/branches/server/src/zonal/zone.py)
===================================================================
--- PyCalendar/trunk/src/zonal/zone.py	                        (rev 0)
+++ PyCalendar/trunk/src/zonal/zone.py	2013-01-26 18:24:59 UTC (rev 10554)
@@ -0,0 +1,548 @@
+##
+#    Copyright (c) 2007-2012 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 PyCalendarDateTime
+from pycalendar.vtimezone import PyCalendarVTimezone
+from pycalendar.property import PyCalendarProperty
+from pycalendar import definitions
+from pycalendar.vtimezonestandard import PyCalendarVTimezoneStandard
+from pycalendar.utcoffsetvalue import PyCalendarUTCOffsetValue
+import utils
+import rule
+
+"""
+Class that maintains a TZ data Zone.
+"""
+
+__all__ = (
+    "Zone",
+    "ZoneRule",
+)
+
+class Zone(object):
+    """
+    A tzdata Zone object containing a set of ZoneRules
+    """
+
+    def __init__(self):
+        self.name = ""
+        self.rules = []
+
+
+    def __str__(self):
+        return self.generate()
+
+
+    def __eq__(self, other):
+        return other and (
+            self.name == other.name and
+            self.rules == other.rules
+        )
+
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+
+    def parse(self, lines):
+        """
+        Parse the Zone lines from tzdata.
+
+        @param lines: the lines to parse.
+        @type lines: C{str}
+        """
+
+        # Parse one line at a time
+        splitlines = lines.split("\n")
+
+        # First line is special
+        line = splitlines[0]
+        splits = [x for x in line.expandtabs(1).split(" ") if len(x) > 0]
+        self.name = splits[1]
+        rule = ZoneRule(self)
+        rule.parse(line, 0)
+        self.rules.append(rule)
+        for line in splitlines[1:]:
+            if len(line) == 0:
+                continue
+            rule = ZoneRule(self)
+            rule.parse(line, 2)
+            if rule.gmtoff != "#":
+                self.rules.append(rule)
+
+
+    def generate(self):
+        """
+        Generate a partial Zone line.
+
+        @return: a C{str} with the Rule.
+        """
+
+        lines = []
+        for count, rule in enumerate(self.rules):
+            if count == 0:
+                items = (
+                    "Zone " + self.name,
+                    rule.generate(),
+                )
+            else:
+                items = (
+                    "",
+                    "",
+                    "",
+                    rule.generate(),
+                )
+            lines.append("\t".join(items))
+        return "\n".join(lines)
+
+
+    def expand(self, rules, minYear, maxYear):
+        """
+        Expand this zone into a set of transitions.
+
+        @param rules: parsed Rules for the tzdb
+        @type rules: C{dict}
+        @param minYear: starting year
+        @type minYear: C{int}
+        @param maxYear: ending year
+        @type maxYear: C{int}
+
+        @return: C{list} of C{tuple} for (
+            transition date-time,
+            offset to,
+            offset from,
+            associated rule,
+        )
+        """
+
+        # Start at 1/1/1800 with the offset from the initial zone rule
+        start = PyCalendarDateTime(year=1800, month=1, day=1, hours=0, minutes=0, seconds=0)
+        start_offset = self.rules[0].getUTCOffset()
+        start_stdoffset = self.rules[0].getUTCOffset()
+        startdt = start.duplicate()
+
+        # Now add each zone rules dates
+        transitions = []
+        lastUntilDateUTC = start.duplicate()
+        last_offset = start_offset
+        last_stdoffset = start_stdoffset
+        first = True
+        for zonerule in self.rules:
+            last_offset, last_stdoffset = zonerule.expand(rules, transitions, lastUntilDateUTC, last_offset, last_stdoffset, maxYear)
+            lastUntilDate = zonerule.getUntilDate()
+            lastUntilDateUTC = lastUntilDate.getUTC(last_offset, last_stdoffset)
+
+            # We typically don't care about the initial one
+            if first and len(self.rules) > 1:
+                transitions = []
+                first = False
+
+        # Sort the results by date
+        transitions.sort(cmp=lambda x, y: x[0].compareDateTime(y[0]))
+
+        # Now scan transitions looking for real changes and note those
+        results = []
+        last_transition = (startdt, start_offset, start_offset)
+        for transition in transitions:
+            dtutc, to_offset, zonerule, rule = transition
+            dt = dtutc.duplicate()
+            dt.offsetSeconds(last_transition[1])
+
+            if dtutc.getYear() >= minYear:
+                if dt > last_transition[0]:
+                    results.append((dt, last_transition[1], to_offset, zonerule, rule))
+                elif dt <= last_transition[0]:
+                    if len(results):
+                        results[-1] = ((results[-1][0], results[-1][1], to_offset, zonerule, None))
+                    else:
+                        results.append((last_transition[0], last_transition[1], last_transition[2], zonerule, None))
+            last_transition = (dt, to_offset, last_transition[2], rule)
+
+        return results
+
+
+    def vtimezone(self, calendar, rules, minYear, maxYear):
+        """
+        Generate a VTIMEZONE for this Zone.
+
+        @param calendar: the L{PyCalendar} object for the VCALENDAR in which the VTIMEZONE
+            will be created.
+        @param rules: the C{dict} containing the set of Rules currently defined.
+        @param startYear: a C{int} containing the first year that should be present
+            in the VTIMEZONE.
+        @return: C{vtimezone} component.
+        """
+
+        # Get a VTIMEZONE component
+        vtz = PyCalendarVTimezone(parent=calendar)
+
+        # Add TZID property
+        vtz.addProperty(PyCalendarProperty(definitions.cICalProperty_TZID, self.name))
+        vtz.addProperty(PyCalendarProperty("X-LIC-LOCATION", self.name))
+
+        transitions = self.expand(rules, minYear, maxYear)
+
+        # Group rules
+        lastZoneRule = None
+        ruleorder = []
+        rulemap = {}
+
+
+        def _generateRuleData():
+            # Generate VTIMEZONE component for last set of rules
+            for rule in ruleorder:
+                if rule:
+                    # Accumulate rule portions with the same offset pairs
+                    lastOffsetPair = (rulemap[rule][0][1], rulemap[rule][0][2],)
+                    startIndex = 0
+                    for index in xrange(len(rulemap[rule])):
+                        offsetPair = (rulemap[rule][index][1], rulemap[rule][index][2],)
+                        if offsetPair != lastOffsetPair:
+                            rule.vtimezone(
+                                vtz,
+                                lastZoneRule,
+                                rulemap[rule][startIndex][0],
+                                rulemap[rule][index - 1][0],
+                                rulemap[rule][startIndex][1],
+                                rulemap[rule][startIndex][2],
+                                index - startIndex,
+                            )
+                            lastOffsetPair = (rulemap[rule][index][1], rulemap[rule][index][2],)
+                            startIndex = index
+
+                    rule.vtimezone(
+                        vtz,
+                        lastZoneRule,
+                        rulemap[rule][startIndex][0],
+                        rulemap[rule][index][0],
+                        rulemap[rule][startIndex][1],
+                        rulemap[rule][startIndex][2],
+                        len(rulemap[rule]),
+                    )
+                else:
+                    lastZoneRule.vtimezone(
+                        vtz,
+                        lastZoneRule,
+                        rulemap[rule][0][0],
+                        rulemap[rule][-1][0],
+                        rulemap[rule][0][1],
+                        rulemap[rule][0][2],
+                    )
+            del ruleorder[:]
+            rulemap.clear()
+
+        for dt, offsetfrom, offsetto, zonerule, rule in transitions:
+
+            # Check for change of rule - we ignore LMT's
+            if zonerule.format != "LMT":
+                if lastZoneRule and lastZoneRule != zonerule:
+                    _generateRuleData()
+                if rule not in ruleorder:
+                    ruleorder.append(rule)
+                rulemap.setdefault(rule, []).append((dt, offsetfrom, offsetto,))
+            lastZoneRule = zonerule
+
+        # Do left overs
+        _generateRuleData()
+
+        self._compressRDateComponents(vtz)
+
+        vtz.finalise()
+        return vtz
+
+
+    def _compressRDateComponents(self, vtz):
+        """
+        Compress sub-components with RDATEs into a single component with multiple
+        RDATEs assuming all other properties are the same.
+
+        @param vtz: the VTIMEZONE object to compress
+        @type vtz: L{PyCalendarVTimezone}
+        """
+
+        # Map the similar sub-components together
+        similarMap = {}
+        for item in vtz.mComponents:
+            item.finalise()
+            key = (
+                item.getType(),
+                item.getTZName(),
+                item.getUTCOffset(),
+                item.getUTCOffsetFrom(),
+            )
+            if item.hasProperty(definitions.cICalProperty_RDATE):
+                similarMap.setdefault(key, []).append(item)
+
+        # Merge similar
+        for values in similarMap.itervalues():
+            if len(values) > 1:
+                mergeTo = values[0]
+                for mergeFrom in values[1:]:
+                    # Copy RDATE from to and remove from actual timezone
+                    prop = mergeFrom.getProperties()[definitions.cICalProperty_RDATE][0]
+                    mergeTo.addProperty(prop)
+                    vtz.mComponents.remove(mergeFrom)
+
+
+
+class ZoneRule(object):
+    """
+    A specific rule for a portion of a Zone
+    """
+
+    def __init__(self, zone):
+        self.zone = zone
+        self.gmtoff = 0
+        self.rule = ""
+        self.format = ""
+        self.until = None
+
+
+    def __str__(self):
+        return self.generate()
+
+
+    def __eq__(self, other):
+        return other and (
+            self.gmtoff == other.gmtoff and
+            self.rule == other.rule and
+            self.format == other.format and
+            self.until == other.until
+        )
+
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+
+    def parse(self, line, offset):
+        """
+        Parse the Zone line from tzdata.
+
+        @param line: a C{str} containing the line to parse.
+        """
+
+        splits = [x for x in line.expandtabs(1).split(" ") if len(x) > 0]
+        assert len(splits) + offset >= 5, "Help: %s" % (line,)
+        self.gmtoff = splits[2 - offset]
+        self.rule = splits[3 - offset]
+        self.format = splits[4 - offset]
+        if len(splits) >= 6 - offset:
+            self.until = " ".join(splits[5 - offset:])
+
+
+    def generate(self):
+        """
+        Generate a partial Zone line.
+
+        @return: a C{str} with the Rule.
+        """
+        items = (
+            self.gmtoff,
+            self.rule,
+            self.format,
+        )
+        if self.until:
+            items = items + (self.until,)
+        return "\t".join(items)
+
+
+    def getUntilDate(self):
+
+        if hasattr(self, "_cached_until"):
+            return self._cached_until
+
+        year = 9999
+        month = 12
+        day = 1
+        hours = 0
+        minutes = 0
+        seconds = 0
+        mode = None
+        if self.until and not self.until.startswith("#"):
+            splits = self.until.split(" ")
+            year = int(splits[0])
+            month = 1
+            day = 1
+            hours = 0
+            minutes = 0
+            seconds = 0
+            mode = None
+            if len(splits) > 1 and not splits[1].startswith("#"):
+                month = int(rule.Rule.MONTH_NAME_TO_POS[splits[1]])
+                if len(splits) > 2 and not splits[2].startswith("#"):
+                    if splits[2] == "lastSun":
+                        dt = PyCalendarDateTime(year=year, month=month, day=1)
+                        dt.setDayOfWeekInMonth(-1, PyCalendarDateTime.SUNDAY)
+                        splits[2] = dt.getDay()
+                    elif splits[2] == "lastSat":
+                        dt = PyCalendarDateTime(year=year, month=month, day=1)
+                        dt.setDayOfWeekInMonth(-1, PyCalendarDateTime.SATURDAY)
+                        splits[2] = dt.getDay()
+                    elif splits[2] == "Sun>=1":
+                        dt = PyCalendarDateTime(year=year, month=month, day=1)
+                        dt.setDayOfWeekInMonth(1, PyCalendarDateTime.SUNDAY)
+                        splits[2] = dt.getDay()
+                    day = int(splits[2])
+                    if len(splits) > 3 and not splits[3].startswith("#"):
+                        splits = splits[3].split(":")
+                        hours = int(splits[0])
+                        minutes = int(splits[1][:2])
+                        if len(splits[1]) > 2:
+                            mode = splits[1][2:]
+                        else:
+                            mode = None
+                        if len(splits) > 2:
+                            seconds = int(splits[2])
+
+        dt = PyCalendarDateTime(year=year, month=month, day=day, hours=hours, minutes=minutes, seconds=seconds)
+        self._cached_until = utils.DateTime(dt, mode)
+        return self._cached_until
+
+
+    def getUTCOffset(self):
+
+        if hasattr(self, "_cached_utc_offset"):
+            return self._cached_uutc_offset
+
+        splits = self.gmtoff.split(":")
+
+        hours = int(splits[0] if splits[0][0] != "-" else splits[0][1:])
+        minutes = int(splits[1]) if len(splits) > 1 else 0
+        seconds = int(splits[2]) if len(splits) > 2 else 0
+        negative = splits[0][0] == "-"
+        self._cached_uutc_offset = ((hours * 60) + minutes) * 60 + seconds
+        if negative:
+            self._cached_uutc_offset = -self._cached_uutc_offset
+        return self._cached_uutc_offset
+
+
+    def expand(self, rules, results, lastUntilUTC, lastOffset, lastStdOffset, maxYear):
+
+        # Expand the rule
+        assert self.rule == "-" or self.rule[0].isdigit() or self.rule in rules, "No rule '%s' found in cache. %s for %s" % (self.rule, self, self.zone,)
+        if self.rule == "-" or self.rule[0].isdigit():
+            return self.expand_norule(results, lastUntilUTC, maxYear)
+        else:
+            tempresults = []
+
+            ruleset = rules[self.rule]
+            ruleset.expand(tempresults, self, maxYear)
+
+            # Sort the results by date
+            tempresults.sort(cmp=lambda x, y: x[0].compareDateTime(y[0]))
+
+            found_one = False
+            found_start = False
+            last_offset = lastOffset
+            last_stdoffset = lastStdOffset
+            finalUntil = self.getUntilDate()
+            for dt, to_offset, rule in tempresults:
+                dtutc = dt.getUTC(last_offset, last_stdoffset)
+                if dtutc >= lastUntilUTC:
+                    if not found_start and dtutc != lastUntilUTC:
+                        # Insert a start item
+                        if not found_one:
+                            last_offset = self.getUTCOffset()
+                            last_stdoffset = self.getUTCOffset()
+                            dtutc = dt.getUTC(last_offset, last_stdoffset)
+                        results.append((lastUntilUTC, last_offset, self, None))
+                    found_start = True
+
+                    if dtutc >= finalUntil.getUTC(last_offset, last_stdoffset):
+                        break
+
+                    results.append((dtutc, to_offset, self, rule))
+
+                last_offset = to_offset
+                last_stdoffset = self.getUTCOffset()
+                found_one = True
+
+            if found_start == 0:
+                results.append((lastUntilUTC, last_offset, self, None))
+
+            return last_offset, last_stdoffset
+
+
+    def expand_norule(self, results, lastUntil, maxYear):
+        to_offset = 0
+        if self.rule[0].isdigit():
+            splits = self.rule.split(":")
+            to_offset = 60 * 60 * int(splits[0])
+            if len(splits) > 1:
+                to_offset += 60 * int(splits[1])
+
+        # Always add a transition for the start of this rule
+        results.append((lastUntil, self.getUTCOffset() + to_offset, self, None))
+        return (self.getUTCOffset() + to_offset, self.getUTCOffset())
+
+
+    def vtimezone(self, vtz, zonerule, start, end, offsetfrom, offsetto):
+
+        # Determine type of component based on offset
+        comp = PyCalendarVTimezoneStandard(parent=vtz)
+
+        # Do offsets
+        tzoffsetfrom = PyCalendarUTCOffsetValue(offsetfrom)
+        tzoffsetto = PyCalendarUTCOffsetValue(offsetto)
+
+        comp.addProperty(PyCalendarProperty(definitions.cICalProperty_TZOFFSETFROM, tzoffsetfrom))
+        comp.addProperty(PyCalendarProperty(definitions.cICalProperty_TZOFFSETTO, tzoffsetto))
+
+        # Do TZNAME
+        if self.format.find("%") != -1:
+            tzname = self.format % ("S",)
+        else:
+            tzname = self.format
+        comp.addProperty(PyCalendarProperty(definitions.cICalProperty_TZNAME, tzname))
+
+        # Do DTSTART
+        comp.addProperty(PyCalendarProperty(definitions.cICalProperty_DTSTART, start))
+
+        # Recurrence
+        comp.addProperty(PyCalendarProperty(definitions.cICalProperty_RDATE, start))
+
+        comp.finalise()
+        vtz.addComponent(comp)
+
+if __name__ == '__main__':
+    rulesdef = """Rule\tUS\t1918\t1919\t-\tMar\tlastSun\t2:00\t1:00\tD
+Rule\tUS\t1918\t1919\t-\tOct\tlastSun\t2:00\t0\tS
+Rule\tUS\t1942\tonly\t-\tFeb\t9\t2:00\t1:00\tW # War
+Rule\tUS\t1945\tonly\t-\tAug\t14\t23:00u\t1:00\tP # Peace
+Rule\tUS\t1945\tonly\t-\tSep\t30\t2:00\t0\tS
+Rule\tUS\t1967\t2006\t-\tOct\tlastSun\t2:00\t0\tS
+Rule\tUS\t1967\t1973\t-\tApr\tlastSun\t2:00\t1:00\tD
+Rule\tUS\t1974\tonly\t-\tJan\t6\t2:00\t1:00\tD
+Rule\tUS\t1975\tonly\t-\tFeb\t23\t2:00\t1:00\tD
+Rule\tUS\t1976\t1986\t-\tApr\tlastSun\t2:00\t1:00\tD
+Rule\tUS\t1987\t2006\t-\tApr\tSun>=1\t2:00\t1:00\tD
+Rule\tUS\t2007\tmax\t-\tMar\tSun>=8\t2:00\t1:00\tD
+Rule\tUS\t2007\tmax\t-\tNov\tSun>=1\t2:00\t0\tS"""
+    rules = {}
+    import rule
+    ruleset = rule.RuleSet()
+    ruleset.parse(rulesdef)
+    rules[ruleset.name] = ruleset
+
+    zonedef = """Zone America/New_York -4:56:02\t-\tLMT\t1883 Nov 18 12:03:58
+\t\t\t-5:00\tUS\tE%sT\t1920
+\t\t\t-5:00\tNYC\tE%sT\t1942
+\t\t\t-5:00\tUS\tE%sT\t1946
+\t\t\t-5:00\tNYC\tE%sT\t1967
+\t\t\t-5:00\tUS\tE%sT"""
+    zone = Zone()
+    zone.parse(zonedef)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130126/9cf31226/attachment-0001.html>


More information about the calendarserver-changes mailing list