# Source code for icalendar.prop

    # -*- coding: utf-8 -*-
    """This module contains the parser/generators (or coders/encoders if you
    prefer) for the classes/datatypes that are used in iCalendar:

    ###########################################################################
    # This module defines these property value data types and property parameters

    4.2 Defined property parameters are:

         ALTREP, CN, CUTYPE, DELEGATED-FROM, DELEGATED-TO, DIR, ENCODING, FMTTYPE,
         FBTYPE, LANGUAGE, MEMBER, PARTSTAT, RANGE, RELATED, RELTYPE, ROLE, RSVP,
         SENT-BY, TZID, VALUE

    4.3 Defined value data types are:

        BINARY, BOOLEAN, CAL-ADDRESS, DATE, DATE-TIME, DURATION, FLOAT, INTEGER,
        PERIOD, RECUR, TEXT, TIME, URI, UTC-OFFSET

    ###########################################################################

    iCalendar properties have values. The values are strongly typed. This module
    defines these types, calling val.to_ical() on them will render them as defined
    in rfc2445.

    If you pass any of these classes a Python primitive, you will have an object
    that can render itself as iCalendar formatted date.

    Property Value Data Types start with a 'v'. they all have an to_ical() and
    from_ical() method. The to_ical() method generates a text string in the
    iCalendar format. The from_ical() method can parse this format and return a
    primitive Python datatype. So it should always be true that:

        x == vDataType.from_ical(VDataType(x).to_ical())

    These types are mainly used for parsing and file generation. But you can set
    them directly.
    """
    from datetime import date
    from datetime import datetime
    from datetime import time
    from datetime import timedelta
    from datetime import tzinfo

    try:
        from dateutil.tz import tzutc
    except ImportError:
        tzutc = None

    from icalendar import compat
    from icalendar.caselessdict import CaselessDict
    from icalendar.parser import Parameters
    from icalendar.parser import escape_char
    from icalendar.parser import tzid_from_dt
    from icalendar.parser import unescape_char
    from icalendar.parser_tools import DEFAULT_ENCODING
    from icalendar.parser_tools import SEQUENCE_TYPES
    from icalendar.parser_tools import to_unicode
    from icalendar.timezone_cache import _timezone_cache
    from icalendar.windows_to_olson import WINDOWS_TO_OLSON

    import base64
    import binascii
    import pytz
    import re
    import time as _time

    DATE_PART = r'(\d+)D'
    TIME_PART = r'T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?'
    DATETIME_PART = '(?:%s)?(?:%s)?' % (DATE_PART, TIME_PART)
    WEEKS_PART = r'(\d+)W'
    DURATION_REGEX = re.compile(r'([-+]?)P(?:%s|%s)$'
                                % (WEEKS_PART, DATETIME_PART))
    WEEKDAY_RULE = re.compile(r'(?P<signal>[+-]?)(?P<relative>[\d]?)'
                              r'(?P<weekday>[\w]{2})$')

    ####################################################
    # handy tzinfo classes you can use.
    #

    ZERO = timedelta(0)
    HOUR = timedelta(hours=1)
    STDOFFSET = timedelta(seconds=-_time.timezone)
    if _time.daylight:
        DSTOFFSET = timedelta(seconds=-_time.altzone)
    else:
        DSTOFFSET = STDOFFSET
    DSTDIFF = DSTOFFSET - STDOFFSET

    class FixedOffset(tzinfo):
        """Fixed offset in minutes east from UTC.
        """
        def __init__(self, offset, name):
            self.__offset = timedelta(minutes=offset)
            self.__name = name

        def utcoffset(self, dt):
            return self.__offset

        def tzname(self, dt):
            return self.__name

        def dst(self, dt):
            return ZERO

    class LocalTimezone(tzinfo):
        """Timezone of the machine where the code is running.
        """
        def utcoffset(self, dt):
            if self._isdst(dt):
                return DSTOFFSET
            else:
                return STDOFFSET

        def dst(self, dt):
            if self._isdst(dt):
                return DSTDIFF
            else:
                return ZERO

        def tzname(self, dt):
            return _time.tzname[self._isdst(dt)]

        def _isdst(self, dt):
            tt = (dt.year, dt.month, dt.day,
                  dt.hour, dt.minute, dt.second,
                  dt.weekday(), 0, -1)
            stamp = _time.mktime(tt)
            tt = _time.localtime(stamp)
            return tt.tm_isdst > 0

    class vBinary(object):
        """Binary property values are base 64 encoded.
        """

        def __init__(self, obj):
            self.obj = to_unicode(obj)
            self.params = Parameters(encoding='BASE64', value="BINARY")

        def __repr__(self):
            return "vBinary('%s')" % self.to_ical()

        def to_ical(self):
            return binascii.b2a_base64(self.obj.encode('utf-8'))[:-1]

        @staticmethod
        def from_ical(ical):
            try:
                return base64.b64decode(ical)
            except UnicodeError:
                raise ValueError('Not valid base 64 encoding.')

    class vBoolean(int):
        """Returns specific string according to state.
        """
        BOOL_MAP = CaselessDict({'true': True, 'false': False})

        def __new__(cls, *args, **kwargs):
            self = super(vBoolean, cls).__new__(cls, *args, **kwargs)
            self.params = Parameters()
            return self

        def to_ical(self):
            if self:
                return b'TRUE'
            return b'FALSE'

        @classmethod
        def from_ical(cls, ical):
            try:
                return cls.BOOL_MAP[ical]
            except:
                raise ValueError("Expected 'TRUE' or 'FALSE'. Got %s" % ical)

    class vCalAddress(compat.unicode_type):
        """This just returns an unquoted string.
        """
        def __new__(cls, value, encoding=DEFAULT_ENCODING):
            value = to_unicode(value, encoding=encoding)
            self = super(vCalAddress, cls).__new__(cls, value)
            self.params = Parameters()
            return self

        def __repr__(self):
            return "vCalAddress('%s')" % self.to_ical()

        def to_ical(self):
            return self.encode(DEFAULT_ENCODING)

        @classmethod
        def from_ical(cls, ical):
            return cls(ical)

    class vFloat(float):
        """Just a float.
        """
        def __new__(cls, *args, **kwargs):
            self = super(vFloat, cls).__new__(cls, *args, **kwargs)
            self.params = Parameters()
            return self

        def to_ical(self):
            return compat.unicode_type(self).encode('utf-8')

        @classmethod
        def from_ical(cls, ical):
            try:
                return cls(ical)
            except:
                raise ValueError('Expected float value, got: %s' % ical)

    class vInt(int):
        """Just an int.
        """
        def __new__(cls, *args, **kwargs):
            self = super(vInt, cls).__new__(cls, *args, **kwargs)
            self.params = Parameters()
            return self

        def to_ical(self):
            return compat.unicode_type(self).encode('utf-8')

        @classmethod
        def from_ical(cls, ical):
            try:
                return cls(ical)
            except:
                raise ValueError('Expected int, got: %s' % ical)

    class vDDDLists(object):
        """A list of vDDDTypes values.
        """
        def __init__(self, dt_list):
            if not hasattr(dt_list, '__iter__'):
                dt_list = [dt_list]
            vDDD = []
            tzid = None
            for dt in dt_list:
                dt = vDDDTypes(dt)
                vDDD.append(dt)
                if 'TZID' in dt.params:
                    tzid = dt.params['TZID']

            if tzid:
                # NOTE: no support for multiple timezones here!
                self.params = Parameters({'TZID': tzid})
            self.dts = vDDD

        def to_ical(self):
            dts_ical = (dt.to_ical() for dt in self.dts)
            return b",".join(dts_ical)

        @staticmethod
        def from_ical(ical, timezone=None):
            out = []
            ical_dates = ical.split(",")
            for ical_dt in ical_dates:
                out.append(vDDDTypes.from_ical(ical_dt, timezone=timezone))
            return out

    class vCategory(object):

        def __init__(self, c_list):
            if not hasattr(c_list, '__iter__'):
                d_list = [c_list]
            self.cats = [vText(c) for c in c_list]

        def to_ical(self):
            return b",".join([c.to_ical() for c in self.cats])

        @staticmethod
        def from_ical(ical, timezone=None):
            out = unescape_char(ical).split(",")
            return out

    class vDDDTypes(object):
        """A combined Datetime, Date or Duration parser/generator. Their format
        cannot be confused, and often values can be of either types.
        So this is practical.
        """
        def __init__(self, dt):
            if not isinstance(dt, (datetime, date, timedelta, time, tuple)):
                raise ValueError('You must use datetime, date, timedelta, '
                                 'time or tuple (for periods)')
            if isinstance(dt, datetime):
                self.params = Parameters({'value': 'DATE-TIME'})
            elif isinstance(dt, date):
                self.params = Parameters({'value': 'DATE'})
            elif isinstance(dt, time):
                self.params = Parameters({'value': 'TIME'})
            elif isinstance(dt, tuple):
                self.params = Parameters({'value': 'PERIOD'})

            if (isinstance(dt, datetime) or isinstance(dt, time))\
                    and getattr(dt, 'tzinfo', False):
                tzinfo = dt.tzinfo
                if tzinfo is not pytz.utc and\
                   (tzutc is None or not isinstance(tzinfo, tzutc)):
                    # set the timezone as a parameter to the property
                    tzid = tzid_from_dt(dt)
                    if tzid:
                        self.params.update({'TZID': tzid})
            self.dt = dt

        def to_ical(self):
            dt = self.dt
            if isinstance(dt, datetime):
                return vDatetime(dt).to_ical()
            elif isinstance(dt, date):
                return vDate(dt).to_ical()
            elif isinstance(dt, timedelta):
                return vDuration(dt).to_ical()
            elif isinstance(dt, time):
                return vTime(dt).to_ical()
            elif isinstance(dt, tuple) and len(dt) == 2:
                return vPeriod(dt).to_ical()
            else:
                raise ValueError('Unknown date type: {}'.format(type(dt)))

        @classmethod
        def from_ical(cls, ical, timezone=None):
            if isinstance(ical, cls):
                return ical.dt
            u = ical.upper()
            if u.startswith(('P', '-P', '+P')):
                return vDuration.from_ical(ical)
            if '/' in u:
                return vPeriod.from_ical(ical)

            if len(ical) in (15, 16):
                return vDatetime.from_ical(ical, timezone=timezone)
            elif len(ical) == 8:
                return vDate.from_ical(ical)
            elif len(ical) in (6, 7):
                return vTime.from_ical(ical)
            else:
                raise ValueError(
                    "Expected datetime, date, or time, got: '%s'" % ical
                )

    class vDate(object):
        """Render and generates iCalendar date format.
        """
        def __init__(self, dt):
            if not isinstance(dt, date):
                raise ValueError('Value MUST be a date instance')
            self.dt = dt
            self.params = Parameters({'value': 'DATE'})

        def to_ical(self):
            s = "%04d%02d%02d" % (self.dt.year, self.dt.month, self.dt.day)
            return s.encode('utf-8')

        @staticmethod
        def from_ical(ical):
            try:
                timetuple = (
                    int(ical[:4]),  # year
                    int(ical[4:6]),  # month
                    int(ical[6:8]),  # day
                )
                return date(*timetuple)
            except:
                raise ValueError('Wrong date format %s' % ical)

    class vDatetime(object):
        """Render and generates icalendar datetime format.

        vDatetime is timezone aware and uses the pytz library, an implementation of
        the Olson database in Python. When a vDatetime object is created from an
        ical string, you can pass a valid pytz timezone identifier. When a
        vDatetime object is created from a python datetime object, it uses the
        tzinfo component, if present. Otherwise an timezone-naive object is
        created. Be aware that there are certain limitations with timezone naive
        DATE-TIME components in the icalendar standard.
        """
        def __init__(self, dt):
            self.dt = dt
            self.params = Parameters()

        def to_ical(self):
            dt = self.dt
            tzid = tzid_from_dt(dt)

            s = "%04d%02d%02dT%02d%02d%02d" % (
                dt.year,
                dt.month,
                dt.day,
                dt.hour,
                dt.minute,
                dt.second
            )
            if tzid == 'UTC':
                s += "Z"
            elif tzid:
                self.params.update({'TZID': tzid})
            return s.encode('utf-8')

        @staticmethod
        def from_ical(ical, timezone=None):
            tzinfo = None
            if timezone:
                try:
                    tzinfo = pytz.timezone(timezone)
                except pytz.UnknownTimeZoneError:
                    if timezone in WINDOWS_TO_OLSON:
                        tzinfo = pytz.timezone(WINDOWS_TO_OLSON.get(timezone))
                    else:
                        tzinfo = _timezone_cache.get(timezone, None)

            try:
                timetuple = (
                    int(ical[:4]),  # year
                    int(ical[4:6]),  # month
                    int(ical[6:8]),  # day
                    int(ical[9:11]),  # hour
                    int(ical[11:13]),  # minute
                    int(ical[13:15]),  # second
                )
                if tzinfo:
                    return tzinfo.localize(datetime(*timetuple))
                elif not ical[15:]:
                    return datetime(*timetuple)
                elif ical[15:16] == 'Z':
                    return pytz.utc.localize(datetime(*timetuple))
                else:
                    raise ValueError(ical)
            except:
                raise ValueError('Wrong datetime format: %s' % ical)

    class vDuration(object):
        """Subclass of timedelta that renders itself in the iCalendar DURATION
        format.
        """

        def __init__(self, td):
            if not isinstance(td, timedelta):
                raise ValueError('Value MUST be a timedelta instance')
            self.td = td
            self.params = Parameters()

        def to_ical(self):
            sign = ""
            if self.td.days < 0:
                sign = "-"
                self.td = -self.td
            timepart = ""
            if self.td.seconds:
                timepart = "T"
                hours = self.td.seconds // 3600
                minutes = self.td.seconds % 3600 // 60
                seconds = self.td.seconds % 60
                if hours:
                    timepart += "%dH" % hours
                if minutes or (hours and seconds):
                    timepart += "%dM" % minutes
                if seconds:
                    timepart += "%dS" % seconds
            if self.td.days == 0 and timepart:
                return (compat.unicode_type(sign).encode('utf-8') + b'P' +
                        compat.unicode_type(timepart).encode('utf-8'))
            else:
                return (compat.unicode_type(sign).encode('utf-8') + b'P' +
                        compat.unicode_type(abs(self.td.days)).encode('utf-8') +
                        b'D' + compat.unicode_type(timepart).encode('utf-8'))

        @staticmethod
        def from_ical(ical):
            try:
                match = DURATION_REGEX.match(ical)
                sign, weeks, days, hours, minutes, seconds = match.groups()
                if weeks:
                    value = timedelta(weeks=int(weeks))
                else:
                    value = timedelta(days=int(days or 0),
                                      hours=int(hours or 0),
                                      minutes=int(minutes or 0),
                                      seconds=int(seconds or 0))
                if sign == '-':
                    value = -value
                return value
            except:
                raise ValueError('Invalid iCalendar duration: %s' % ical)

    class vPeriod(object):
        """A precise period of time.
        """
        def __init__(self, per):
            start, end_or_duration = per
            if not (isinstance(start, datetime) or isinstance(start, date)):
                raise ValueError('Start value MUST be a datetime or date instance')
            if not (isinstance(end_or_duration, datetime) or
                    isinstance(end_or_duration, date) or
                    isinstance(end_or_duration, timedelta)):
                raise ValueError('end_or_duration MUST be a datetime, '
                                 'date or timedelta instance')
            by_duration = 0
            if isinstance(end_or_duration, timedelta):
                by_duration = 1
                duration = end_or_duration
                end = start + duration
            else:
                end = end_or_duration
                duration = end - start
            if start > end:
                raise ValueError("Start time is greater than end time")

            self.params = Parameters()
            # set the timezone identifier
            # does not support different timezones for start and end
            tzid = tzid_from_dt(start)
            if tzid:
                self.params['TZID'] = tzid

            self.start = start
            self.end = end
            self.by_duration = by_duration
            self.duration = duration

        def __cmp__(self, other):
            if not isinstance(other, vPeriod):
                raise NotImplementedError('Cannot compare vPeriod with %r' % other)
            return cmp((self.start, self.end), (other.start, other.end))

        def overlaps(self, other):
            if self.start > other.start:
                return other.overlaps(self)
            if self.start <= other.start < self.end:
                return True
            return False

        def to_ical(self):
            if self.by_duration:
                return (vDatetime(self.start).to_ical() + b'/' +
                        vDuration(self.duration).to_ical())
            return (vDatetime(self.start).to_ical() + b'/' +
                    vDatetime(self.end).to_ical())

        @staticmethod
        def from_ical(ical):
            try:
                start, end_or_duration = ical.split('/')
                start = vDDDTypes.from_ical(start)
                end_or_duration = vDDDTypes.from_ical(end_or_duration)
                return (start, end_or_duration)
            except:
                raise ValueError('Expected period format, got: %s' % ical)

        def __repr__(self):
            if self.by_duration:
                p = (self.start, self.duration)
            else:
                p = (self.start, self.end)
            return 'vPeriod(%r)' % (p, )

    class vWeekday(compat.unicode_type):
        """This returns an unquoted weekday abbrevation.
        """
        week_days = CaselessDict({
            "SU": 0, "MO": 1, "TU": 2, "WE": 3, "TH": 4, "FR": 5, "SA": 6,
        })

        def __new__(cls, value, encoding=DEFAULT_ENCODING):
            value = to_unicode(value, encoding=encoding)
            self = super(vWeekday, cls).__new__(cls, value)
            match = WEEKDAY_RULE.match(self)
            if match is None:
                raise ValueError('Expected weekday abbrevation, got: %s' % self)
            match = match.groupdict()
            sign = match['signal']
            weekday = match['weekday']
            relative = match['relative']
            if weekday not in vWeekday.week_days or sign not in '+-':
                raise ValueError('Expected weekday abbrevation, got: %s' % self)
            self.relative = relative and int(relative) or None
            self.params = Parameters()
            return self

        def to_ical(self):
            return self.encode(DEFAULT_ENCODING).upper()

        @classmethod
        def from_ical(cls, ical):
            try:
                return cls(ical.upper())
            except:
                raise ValueError('Expected weekday abbrevation, got: %s' % ical)

    class vFrequency(compat.unicode_type):
        """A simple class that catches illegal values.
        """

        frequencies = CaselessDict({
            "SECONDLY": "SECONDLY",
            "MINUTELY": "MINUTELY",
            "HOURLY": "HOURLY",
            "DAILY": "DAILY",
            "WEEKLY": "WEEKLY",
            "MONTHLY": "MONTHLY",
            "YEARLY": "YEARLY",
        })

        def __new__(cls, value, encoding=DEFAULT_ENCODING):
            value = to_unicode(value, encoding=encoding)
            self = super(vFrequency, cls).__new__(cls, value)
            if self not in vFrequency.frequencies:
                raise ValueError('Expected frequency, got: %s' % self)
            self.params = Parameters()
            return self

        def to_ical(self):
            return self.encode(DEFAULT_ENCODING).upper()

        @classmethod
        def from_ical(cls, ical):
            try:
                return cls(ical.upper())
            except:
                raise ValueError('Expected frequency, got: %s' % ical)

    class vRecur(CaselessDict):
        """Recurrence definition.
        """

        frequencies = ["SECONDLY", "MINUTELY", "HOURLY", "DAILY", "WEEKLY",
                       "MONTHLY", "YEARLY"]

        # Mac iCal ignores RRULEs where FREQ is not the first rule part.
        # Sorts parts according to the order listed in RFC 5545, section 3.3.10.
        canonical_order = ("FREQ", "UNTIL", "COUNT", "INTERVAL",
                           "BYSECOND", "BYMINUTE", "BYHOUR", "BYDAY",
                           "BYMONTHDAY", "BYYEARDAY", "BYWEEKNO", "BYMONTH",
                           "BYSETPOS", "WKST")

        types = CaselessDict({
            'COUNT': vInt,
            'INTERVAL': vInt,
            'BYSECOND': vInt,
            'BYMINUTE': vInt,
            'BYHOUR': vInt,
            'BYWEEKNO': vInt,
            'BYMONTHDAY': vInt,
            'BYYEARDAY': vInt,
            'BYMONTH': vInt,
            'UNTIL': vDDDTypes,
            'BYSETPOS': vInt,
            'WKST': vWeekday,
            'BYDAY': vWeekday,
            'FREQ': vFrequency,
        })

        def __init__(self, *args, **kwargs):
            super(vRecur, self).__init__(*args, **kwargs)
            self.params = Parameters()

        def to_ical(self):
            result = []
            for key, vals in self.sorted_items():
                typ = self.types[key]
                if not isinstance(vals, SEQUENCE_TYPES):
                    vals = [vals]
                vals = b','.join(typ(val).to_ical() for val in vals)

                # CaselessDict keys are always unicode
                key = key.encode(DEFAULT_ENCODING)
                result.append(key + b'=' + vals)

            return b';'.join(result)

        @classmethod
        def parse_type(cls, key, values):
            # integers
            parser = cls.types.get(key, vText)
            return [parser.from_ical(v) for v in values.split(',')]

        @classmethod
        def from_ical(cls, ical):
            if isinstance(ical, cls):
                return ical
            try:
                recur = cls()
                for pairs in ical.split(';'):
                    try:
                        key, vals = pairs.split('=')
                    except ValueError:
                        # E.g. incorrect trailing semicolon, like (issue #157):
                        # FREQ=YEARLY;BYMONTH=11;BYDAY=1SU;
                        continue
                    recur[key] = cls.parse_type(key, vals)
                return dict(recur)
            except:
                raise ValueError('Error in recurrence rule: %s' % ical)

    class vText(compat.unicode_type):
        """Simple text.
        """

        def __new__(cls, value, encoding=DEFAULT_ENCODING):
            value = to_unicode(value, encoding=encoding)
            self = super(vText, cls).__new__(cls, value)
            self.encoding = encoding
            self.params = Parameters()
            return self

        def __repr__(self):
            return "vText('%s')" % self.to_ical()

        def to_ical(self):
            return escape_char(self).encode(self.encoding)

        @classmethod
        def from_ical(cls, ical):
            ical_unesc = unescape_char(ical)
            return cls(ical_unesc)

    class vTime(object):
        """Render and generates iCalendar time format.
        """

        def __init__(self, *args):
            if len(args) == 1:
                if not isinstance(args[0], (time, datetime)):
                    raise ValueError('Expected a datetime.time, got: %s' % args[0])
                self.dt = args[0]
            else:
                self.dt = time(*args)
            self.params = Parameters({'value': 'TIME'})

        def to_ical(self):
            return self.dt.strftime("%H%M%S")

        @staticmethod
        def from_ical(ical):
            # TODO: timezone support
            try:
                timetuple = (int(ical[:2]), int(ical[2:4]), int(ical[4:6]))
                return time(*timetuple)
            except:
                raise ValueError('Expected time, got: %s' % ical)

    class vUri(compat.unicode_type):
        """Uniform resource identifier is basically just an unquoted string.
        """

        def __new__(cls, value, encoding=DEFAULT_ENCODING):
            value = to_unicode(value, encoding=encoding)
            self = super(vUri, cls).__new__(cls, value)
            self.params = Parameters()
            return self

        def to_ical(self):
            return self.encode(DEFAULT_ENCODING)

        @classmethod
        def from_ical(cls, ical):
            try:
                return cls(ical)
            except:
                raise ValueError('Expected , got: %s' % ical)

    class vGeo(object):
        """A special type that is only indirectly defined in the rfc.
        """

        def __init__(self, geo):
            try:
                latitude, longitude = (geo[0], geo[1])
                latitude = float(latitude)
                longitude = float(longitude)
            except:
                raise ValueError('Input must be (float, float) for '
                                 'latitude and longitude')
            self.latitude = latitude
            self.longitude = longitude
            self.params = Parameters()

        def to_ical(self):
            return '%s;%s' % (self.latitude, self.longitude)

        @staticmethod
        def from_ical(ical):
            try:
                latitude, longitude = ical.split(';')
                return (float(latitude), float(longitude))
            except:
                raise ValueError("Expected 'float;float' , got: %s" % ical)

    class vUTCOffset(object):
        """Renders itself as a utc offset.
        """

        ignore_exceptions = False   # if True, and we cannot parse this
                                    # component, we will silently ignore
                                    # it, rather than let the exception
                                    # propagate upwards

        def __init__(self, td):
            if not isinstance(td, timedelta):
                raise ValueError('Offset value MUST be a timedelta instance')
            self.td = td
            self.params = Parameters()

        def to_ical(self):

            if self.td < timedelta(0):
                sign = '-%s'
                td = timedelta(0) - self.td  # get timedelta relative to 0
            else:
                # Google Calendar rejects '0000' but accepts '+0000'
                sign = '+%s'
                td = self.td

            days, seconds = td.days, td.seconds

            hours = abs(days * 24 + seconds // 3600)
            minutes = abs((seconds % 3600) // 60)
            seconds = abs(seconds % 60)
            if seconds:
                duration = '%02i%02i%02i' % (hours, minutes, seconds)
            else:
                duration = '%02i%02i' % (hours, minutes)
            return sign % duration

        @classmethod
        def from_ical(cls, ical):
            if isinstance(ical, cls):
                return ical.td
            try:
                sign, hours, minutes, seconds = (ical[0:1],
                                                 int(ical[1:3]),
                                                 int(ical[3:5]),
                                                 int(ical[5:7] or 0))
                offset = timedelta(hours=hours, minutes=minutes, seconds=seconds)
            except:
                raise ValueError('Expected utc offset, got: %s' % ical)
            if not cls.ignore_exceptions and offset >= timedelta(hours=24):
                raise ValueError(
                    'Offset must be less than 24 hours, was %s' % ical)
            if sign == '-':
                return -offset
            return offset

    class vInline(compat.unicode_type):
        """This is an especially dumb class that just holds raw unparsed text and
        has parameters. Conversion of inline values are handled by the Component
        class, so no further processing is needed.
        """
        def __new__(cls, value, encoding=DEFAULT_ENCODING):
            value = to_unicode(value, encoding=encoding)
            self = super(vInline, cls).__new__(cls, value)
            self.params = Parameters()
            return self

        def to_ical(self):
            return self.encode(DEFAULT_ENCODING)

        @classmethod
        def from_ical(cls, ical):
            return cls(ical)

    class TypesFactory(CaselessDict):
        """All Value types defined in rfc 2445 are registered in this factory
        class.

        The value and parameter names don't overlap. So one factory is enough for
        both kinds.
        """

        def __init__(self, *args, **kwargs):
            "Set keys to upper for initial dict"
            super(TypesFactory, self).__init__(*args, **kwargs)
            self.all_types = (
                vBinary,
                vBoolean,
                vCalAddress,
                vDDDLists,
                vDDDTypes,
                vDate,
                vDatetime,
                vDuration,
                vFloat,
                vFrequency,
                vGeo,
                vInline,
                vInt,
                vPeriod,
                vRecur,
                vText,
                vTime,
                vUTCOffset,
                vUri,
                vWeekday,
                vCategory,
            )
            self['binary'] = vBinary
            self['boolean'] = vBoolean
            self['cal-address'] = vCalAddress
            self['date'] = vDDDTypes
            self['date-time'] = vDDDTypes
            self['duration'] = vDDDTypes
            self['float'] = vFloat
            self['integer'] = vInt
            self['period'] = vPeriod
            self['recur'] = vRecur
            self['text'] = vText
            self['time'] = vTime
            self['uri'] = vUri
            self['utc-offset'] = vUTCOffset
            self['geo'] = vGeo
            self['inline'] = vInline
            self['date-time-list'] = vDDDLists
            self['categories'] = vCategory

        #################################################
        # Property types

        # These are the default types
        types_map = CaselessDict({
            ####################################
            # Property value types
            # Calendar Properties
            'calscale': 'text',
            'method': 'text',
            'prodid': 'text',
            'version': 'text',
            # Descriptive Component Properties
            'attach': 'uri',
            'categories': 'categories',
            'class': 'text',
            'comment': 'text',
            'description': 'text',
            'geo': 'geo',
            'location': 'text',
            'percent-complete': 'integer',
            'priority': 'integer',
            'resources': 'text',
            'status': 'text',
            'summary': 'text',
            # Date and Time Component Properties
            'completed': 'date-time',
            'dtend': 'date-time',
            'due': 'date-time',
            'dtstart': 'date-time',
            'duration': 'duration',
            'freebusy': 'period',
            'transp': 'text',
            # Time Zone Component Properties
            'tzid': 'text',
            'tzname': 'text',
            'tzoffsetfrom': 'utc-offset',
            'tzoffsetto': 'utc-offset',
            'tzurl': 'uri',
            # Relationship Component Properties
            'attendee': 'cal-address',
            'contact': 'text',
            'organizer': 'cal-address',
            'recurrence-id': 'date-time',
            'related-to': 'text',
            'url': 'uri',
            'uid': 'text',
            # Recurrence Component Properties
            'exdate': 'date-time-list',
            'exrule': 'recur',
            'rdate': 'date-time-list',
            'rrule': 'recur',
            # Alarm Component Properties
            'action': 'text',
            'repeat': 'integer',
            'trigger': 'duration',
            # Change Management Component Properties
            'created': 'date-time',
            'dtstamp': 'date-time',
            'last-modified': 'date-time',
            'sequence': 'integer',
            # Miscellaneous Component Properties
            'request-status': 'text',
            ####################################
            # parameter types (luckily there is no name overlap)
            'altrep': 'uri',
            'cn': 'text',
            'cutype': 'text',
            'delegated-from': 'cal-address',
            'delegated-to': 'cal-address',
            'dir': 'uri',
            'encoding': 'text',
            'fmttype': 'text',
            'fbtype': 'text',
            'language': 'text',
            'member': 'cal-address',
            'partstat': 'text',
            'range': 'text',
            'related': 'text',
            'reltype': 'text',
            'role': 'text',
            'rsvp': 'boolean',
            'sent-by': 'cal-address',
            'tzid': 'text',
            'value': 'text',
        })

        def for_property(self, name):
            """Returns a the default type for a property or parameter
            """
            return self[self.types_map.get(name, 'text')]

        def to_ical(self, name, value):
            """Encodes a named value from a primitive python type to an icalendar
            encoded string.
            """
            type_class = self.for_property(name)
            return type_class(value).to_ical()

        def from_ical(self, name, value):
            """Decodes a named property or parameter value from an icalendar
            encoded string to a primitive python type.
            """
            type_class = self.for_property(name)
            decoded = type_class.from_ical(value)
            return decoded