# HG changeset patch # User Paul Boddie # Date 1268597175 -3600 # Node ID 3b719c63fd172de896991b3c308366ea0efe22ad # Parent 6087dcd15153623cbf144790f3da6fccc4d04560 Changed the time zone pattern to permit UTC offsets. Changed the country code pattern to ensure that the code appears at the end of a value, but can be surrounded by non-alphanumeric characters. Changed the application of location information, employing a number of new functions to encapsulate existing functionality, setting only the zone name on a datetime. Introduced methods to support conversion to UTC, with only explicitly specified UTC offsets permitting such conversions. Improved the handling of time zone output in the action, adding the "/" prefix to time zone identifiers, although this probably doesn't help with most calendar applications. diff -r 6087dcd15153 -r 3b719c63fd17 EventAggregatorSupport.py --- a/EventAggregatorSupport.py Sun Mar 14 02:32:51 2010 +0100 +++ b/EventAggregatorSupport.py Sun Mar 14 21:06:15 2010 +0100 @@ -45,12 +45,12 @@ # Value parsing. -country_code_regexp = re.compile(ur'(?:^|\s)(?P[A-Z]{2})(?:$|\s)', re.UNICODE) +country_code_regexp = re.compile(ur'(?:^|\W)(?P[A-Z]{2})(?:$|\W+$)', re.UNICODE) month_regexp_str = ur'(?P[0-9]{4})-(?P[0-9]{2})' date_regexp_str = ur'(?P[0-9]{4})-(?P[0-9]{2})-(?P[0-9]{2})' time_regexp_str = ur'(?P[0-2][0-9]):(?P[0-5][0-9])(?::(?P[0-6][0-9]))?' -timezone_regexp_str = ur'(?P[A-Z]{3,}|[a-zA-Z]+/[-_a-zA-Z]+)' +timezone_regexp_str = ur'(?P[A-Z]{3,}|[a-zA-Z]+/[-_a-zA-Z]+|[-+][0-9]{1,4})' datetime_regexp_str = date_regexp_str + ur'(?:\s+' + time_regexp_str + ur'(?:\s+' + timezone_regexp_str + ur')?)?' month_regexp = re.compile(month_regexp_str, re.UNICODE) @@ -828,6 +828,9 @@ def __str__(self): return "%04d-%02d" % self.as_tuple()[:2] + def as_datetime(self, day, hour, minute, second, zone): + return DateTime(self.as_tuple() + (day, hour, minute, second, zone)) + def as_date(self, day): return Date(self.as_tuple() + (day,)) @@ -911,6 +914,9 @@ def __str__(self): return "%04d-%02d-%02d" % self.as_tuple()[:3] + def as_datetime(self, hour, minute, second, zone): + return DateTime(self.as_tuple() + (hour, minute, second, zone)) + def as_date(self): return self @@ -960,7 +966,6 @@ def __init__(self, data): Date.__init__(self, data) - self.utc_offset = None def __str__(self): if self.has_time(): @@ -975,6 +980,9 @@ return Date.__str__(self) + time_str + def as_datetime(self): + return self + def as_date(self): return Date(self.data[:3]) @@ -987,9 +995,8 @@ def time_zone(self): return self.data[6] - def set_time_zone(self, value, utc_offset=None): + def set_time_zone(self, value): self.data[6] = value - self.utc_offset = utc_offset def padded(self): @@ -998,53 +1005,139 @@ data = map(lambda x: x or 0, self.data[:6]) + self.data[6:] return DateTime(data) + def to_utc(self): + + """ + Return this object converted to UTC, or None if such a conversion is not + defined. + """ + + offset = self.utc_offset() + if offset: + hours, minutes = offset + + # Invert the offset to get the correction. + + hours, minutes = -hours, -minutes + + # Get the components. + + hour, minute, second, zone = self.as_tuple()[3:] + date = self.as_date() + + # Add the minutes and hours. + + minute += minutes + if minute < 0: + hour -= 1 + minute += 60 + elif minute > 59: + hour += 1 + minute -= 60 + + hour += hours + if hour < 0: + date = date.previous_day() + hour += 24 + elif hour > 23: + date = date.next_day() + hour -= 24 + + return date.as_datetime(hour, minute, second, "UTC") + else: + return None + + def utc_offset(self): + + "Return the UTC offset in hours and minutes." + + zone = self.time_zone() + + # Only attempt to return a UTC offset where an explicit offset has been + # set. + + if zone and zone[0] in "-+": + digits = zone[1:] + if zone[0] == "-": + sign = -1 + else: + sign = 1 + + if 1 <= len(digits) <= 2: + return int(digits) * sign, 0 + elif len(digits) == 3: + hours = int(digits[:1]) * sign + minutes = int(digits[1:]) * sign + return hours, minutes + elif len(digits) == 4: + hours = int(digits[:2]) * sign + minutes = int(digits[2:]) * sign + return hours, minutes + + return None + def apply_location(self, location): """ - Apply 'location' information, setting the time zone if none is already - set. + Apply 'location' information, setting the time zone if none has already + been set. """ if not self.time_zone(): - - # Only try and set a time zone if pytz is present and able to - # suggest one. + zone = getTimeZone(location) + if zone: + self.set_time_zone(zone) - if pytz is not None: +def getTimeZone(location): - # Find a country code in the location. + "Find a time zone for the specified 'location'." + + # Only try and find a time zone if pytz is present and able to suggest one. - match = country_code_regexp.search(location) + if pytz is None: + return None - if match: + code = getCountry(location) - # Attempt to discover zones for that country. + if code is None: + return None - try: - zones = pytz.country_timezones(match.group("code")) + try: + zones = pytz.country_timezones(code) + except KeyError: + return None - # Unambiguous choice of zone. + # No zones... - if len(zones) == 1: - self.set_time_zone(zones[0], pytz.timezone(zones[0]).utcoffset(None)) + if not zones: + return None + + # Many potential zones. - # Many potential zones. + if len(zones) > 1: + for zone in zones: + continent, city = zone.split("/") - elif len(zones) > 1: - for zone in zones: - continent, city = zone.split("/") + # If the specific city is mentioned, choose the + # zone. + + if location.find(city) != -1: + return zone - # If the specific city is mentioned, choose the - # zone. + # Otherwise choose the first or only zone. + + return zones[0] + +def getCountry(s): - if location.find(city) != -1: - self.set_time_zone(zone, pytz.timezone(zone).utcoffset(None)) - break - else: - self.set_time_zone(zones[0], pytz.timezone(zones[0]).utcoffset(None)) + "Find a country code in the given string 's'." + + match = country_code_regexp.search(s) - except KeyError: - pass + if match: + return match.group("code") + else: + return None def getDate(s): diff -r 6087dcd15153 -r 3b719c63fd17 actions/EventAggregatorSummary.py --- a/actions/EventAggregatorSummary.py Sun Mar 14 02:32:51 2010 +0100 +++ b/actions/EventAggregatorSummary.py Sun Mar 14 21:06:15 2010 +0100 @@ -252,21 +252,13 @@ if start.has_time(): request.write("DTSTART") - zone = start.time_zone() - if zone: - request.write(";TZID=%s" % zone) - - request.write(";VALUE=DATE-TIME:%04d%02d%02dT%02d%02d%02d\r\n" % start.padded().as_tuple()[:-1]) + write_calendar_datetime(request, start) else: request.write("DTSTART;VALUE=DATE:%04d%02d%02d\r\n" % start.as_date().as_tuple()) if end.has_time(): request.write("DTEND") - zone = end.time_zone() - if zone: - request.write(";TZID=%s" % zone) - - request.write(";VALUE=DATE-TIME:%04d%02d%02dT%02d%02d%02d\r\n" % end.padded().as_tuple()[:-1]) + write_calendar_datetime(request, end) else: request.write("DTEND;VALUE=DATE:%04d%02d%02d\r\n" % end.next_day().as_date().as_tuple()) @@ -343,6 +335,22 @@ if EventAggregatorSupport.isMoin15(): raise MoinMoin.util.MoinMoinNoFooter +def write_calendar_datetime(request, datetime): + + """ + Write to the given 'request' the 'datetime' using appropriate time zone + information. + """ + + utc_datetime = datetime.to_utc() + if utc_datetime: + request.write(";VALUE=DATE-TIME:%04d%02d%02dT%02d%02d%02dZ\r\n" % utc_datetime.padded().as_tuple()[:-1]) + else: + zone = datetime.time_zone() + if zone: + request.write(";TZID=/%s" % zone) + request.write(";VALUE=DATE-TIME:%04d%02d%02dT%02d%02d%02d\r\n" % datetime.padded().as_tuple()[:-1]) + # Action function. def execute(pagename, request):