[Zope3-checkins] SVN: Zope3/trunk/ - Improvements to zope.i18n.format.

Stephan Richter srichter at cosmos.phy.tufts.edu
Wed Feb 16 09:27:13 EST 2005


Log message for revision 29158:
  - Improvements to zope.i18n.format.
  
    * Support for parsing and formatting timezones based on pytz.
  
    * Reinterpretation of of ICU documentation, revealed that any number
      of formatting fields should be accepted by the formatter and parser.
  
    * Implemented formatting of several missing formatting fields. Now all
      fields are correctly implemented. 
  
  

Changed:
  U   Zope3/trunk/doc/CHANGES.txt
  U   Zope3/trunk/src/zope/i18n/format.py
  U   Zope3/trunk/src/zope/i18n/interfaces/__init__.py
  U   Zope3/trunk/src/zope/i18n/tests/test_formats.py

-=-
Modified: Zope3/trunk/doc/CHANGES.txt
===================================================================
--- Zope3/trunk/doc/CHANGES.txt	2005-02-16 11:19:32 UTC (rev 29157)
+++ Zope3/trunk/doc/CHANGES.txt	2005-02-16 14:27:13 UTC (rev 29158)
@@ -10,6 +10,19 @@
 
     New features
 
+      - Improvements to zope.i18n.format.
+
+        * Support for parsing and formatting timezones based on pytz.
+
+        * Reinterpretation of of ICU documentation, revealed that any number
+          of formatting fields should be accepted by the formatter and parser.
+
+        * Implemented formatting of several missing formatting fields. Now all
+          fields are correctly implemented. 
+
+      - Added pytz to the repository. Stuart Bishop licensed it for us under
+        ZPL 2.1. Thanks!
+
       - New schema field: Timedelta.
 
       - Implemented some initial deprecation framework, see
@@ -437,7 +450,8 @@
 
       Jim Fulton, Fred Drake, Philipp von Weitershausen, Stephan Richter,
       Gustavo Niemeyer, Daniel Nouri, Volker Bachschneider, Roger Ineichen,
-      Shane Hathaway, Bjorn Tillenius, Garrett Smith, Marius Gedminas
+      Shane Hathaway, Bjorn Tillenius, Garrett Smith, Marius Gedminas, Stuart
+      Bishop
 
       Note: If you are not listed and contributed, please add yourself. This
       note will be deleted before the release.

Modified: Zope3/trunk/src/zope/i18n/format.py
===================================================================
--- Zope3/trunk/src/zope/i18n/format.py	2005-02-16 11:19:32 UTC (rev 29157)
+++ Zope3/trunk/src/zope/i18n/format.py	2005-02-16 14:27:13 UTC (rev 29158)
@@ -21,10 +21,16 @@
 import re
 import math
 import datetime
+import pytz
+import pytz.reference
 
 from zope.i18n.interfaces import IDateTimeFormat, INumberFormat
 from zope.interface import implements
 
+def _findFormattingCharacterInPattern(char, pattern):
+    return [entry for entry in pattern
+            if isinstance(entry, tuple) and entry[0] == char]
+
 class DateTimeParseError(Exception):
     """Error is raised when parsing of datetime failed."""
 
@@ -61,9 +67,10 @@
             bin_pattern = parseDateTimePattern(pattern)
         else:
             bin_pattern = self._bin_pattern
+
         # Generate the correct regular expression to parse the date and parse.
         regex = ''
-        info = buildDateTimeParseInfo(self.calendar)
+        info = buildDateTimeParseInfo(self.calendar, bin_pattern)
         for elem in bin_pattern:
             regex += info.get(elem, elem)
         try:
@@ -74,10 +81,12 @@
         # Sometimes you only want the parse results
         if not asObject:
             return results
+        
         # Map the parsing results to a datetime object
-        ordered = [0, 0, 0, 0, 0, 0, 0]
+        ordered = [None, None, None, None, None, None, None]
         bin_pattern = filter(lambda x: isinstance(x, tuple), bin_pattern)
-        # Handle years
+
+        # Handle years; note that only 'yy' and 'yyyy' are allowed
         if ('y', 2) in bin_pattern:
             year = int(results[bin_pattern.index(('y', 2))])
             if year > 30:
@@ -86,38 +95,72 @@
                 ordered[0] = 2000 + year
         if ('y', 4) in bin_pattern:
             ordered[0] = int(results[bin_pattern.index(('y', 4))])
-        # Handle months
-        if ('M', 3) in bin_pattern:
-            abbr = results[bin_pattern.index(('M', 3))]
+
+        # Handle months (text)
+        month_entry = _findFormattingCharacterInPattern('M', bin_pattern)
+        if month_entry and month_entry[0][1] == 3:
+            abbr = results[bin_pattern.index(month_entry[0])]
             ordered[1] = self.calendar.getMonthTypeFromAbbreviation(abbr)
-        if ('M', 4) in bin_pattern:
-            name = results[bin_pattern.index(('M', 4))]
+        elif month_entry and month_entry[0][1] >= 4:
+            name = results[bin_pattern.index(month_entry[0])]
             ordered[1] = self.calendar.getMonthTypeFromName(name)
-        # Handle AM/PM hours
-        for length in (1, 2):
-            id = ('h', length)
-            if id in bin_pattern:
-                hour = int(results[bin_pattern.index(id)])
-                ampm = self.calendar.pm == results[
-                    bin_pattern.index(('a', 1))]
-                if hour == 12:
-                    ampm = not ampm
-                ordered[3] = (hour + 12*ampm)%24
+        elif month_entry and month_entry[0][1] <= 2:
+            ordered[1] = int(results[bin_pattern.index(month_entry[0])])
+
+        # Handle hours with AM/PM
+        hour_entry = _findFormattingCharacterInPattern('h', bin_pattern)
+        if hour_entry:
+            hour = int(results[bin_pattern.index(hour_entry[0])])
+            ampm_entry = _findFormattingCharacterInPattern('a', bin_pattern)
+            if not ampm_entry:
+                raise DateTimeParseError, \
+                      'Cannot handle 12-hour format without am/pm marker.'
+            ampm = self.calendar.pm == results[bin_pattern.index(ampm_entry[0])]
+            if hour == 12:
+                ampm = not ampm
+            ordered[3] = (hour + 12*ampm)%24
+
         # Shortcut for the simple int functions
-        dt_fields_map = {'M': 1, 'd': 2, 'H': 3, 'm': 4, 's': 5, 'S': 6}
+        dt_fields_map = {'d': 2, 'H': 3, 'm': 4, 's': 5, 'S': 6}
         for field in dt_fields_map.keys():
-            for length in (1, 2):
-                id = (field, length)
-                if id in bin_pattern:
-                    pos = dt_fields_map[field]
-                    ordered[pos] = int(results[bin_pattern.index(id)])
+            entry = _findFormattingCharacterInPattern(field, bin_pattern)
+            if not entry: continue
+            pos = dt_fields_map[field]
+            ordered[pos] = int(results[bin_pattern.index(entry[0])])
 
-        if ordered[3:] == [0, 0, 0, 0]:
-            return datetime.date(*ordered[:3])
-        elif ordered[:3] == [0, 0, 0]:
-            return datetime.time(*ordered[3:])
+        # Handle timezones
+        tzinfo = None
+        tz_entry = _findFormattingCharacterInPattern('z', bin_pattern)
+        if ordered[3:] != [None, None, None, None] and tz_entry:
+            length = tz_entry[0][1]
+            value = results[bin_pattern.index(tz_entry[0])]
+            if length == 1:
+                hours, mins = int(value[:-2]), int(value[-2:])
+                delta = datetime.timedelta(hours=hours, minutes=mins)
+                tzinfo = pytz.tzinfo.StaticTzInfo()
+                tzinfo._utcoffset = delta
+            elif length == 2:
+                hours, mins = int(value[:-3]), int(value[-2:])
+                delta = datetime.timedelta(hours=hours, minutes=mins)
+                tzinfo = pytz.tzinfo.StaticTzInfo()
+                tzinfo._utcoffset = delta
+            else:
+                if value in pytz.all_timezones:
+                    tzinfo = pytz.timezone(value)
+                else:
+                    # TODO: Find timezones using locale information
+                    pass
+                    
+
+        # Create a date/time object from the data
+        if ordered[3:] == [None, None, None, None]:
+            return datetime.date(*[e or 0 for e in ordered[:3]])
+        elif ordered[:3] == [None, None, None]:
+            return datetime.time(*[e or 0 for e in ordered[3:]],
+                                 **{'tzinfo' :tzinfo})
         else:
-            return datetime.datetime(*ordered)
+            return datetime.datetime(*[e or 0 for e in ordered],
+                                     **{'tzinfo' :tzinfo})
 
 
     def format(self, obj, pattern=None):
@@ -128,8 +171,8 @@
         else:
             bin_pattern = self._bin_pattern
 
-        text = ''
-        info = buildDateTimeInfo(obj, self.calendar)
+        text = u''
+        info = buildDateTimeInfo(obj, self.calendar, bin_pattern)
         for elem in bin_pattern:
             text += info.get(elem, elem)
 
@@ -389,19 +432,10 @@
 class DateTimePatternParseError(Exception):
     """DateTime Pattern Parse Error"""
 
-class BinaryDateTimePattern(list):
 
-    def append(self, item):
-        if isinstance(item, tuple) and item[1] > 4:
-            raise DateTimePatternParseError, \
-                  ('A datetime field character sequence can never be '
-                   'longer than 4 characters. You have: %i' %item[1])
-        super(BinaryDateTimePattern, self).append(item)
-
-
 def parseDateTimePattern(pattern, DATETIMECHARS="aGyMdEDFwWhHmsSkKz"):
     """This method can handle everything: time, date and datetime strings."""
-    result = BinaryDateTimePattern()
+    result = []
     state = DEFAULT
     helper = ''
     char = ''
@@ -473,55 +507,72 @@
     return result
 
 
-
-def buildDateTimeParseInfo(calendar):
+def buildDateTimeParseInfo(calendar, pattern):
     """This method returns a dictionary that helps us with the parsing.
     It also depends on the locale of course."""
-    return {
-        ('a', 1): r'(%s|%s)' %(calendar.am, calendar.pm),
-        # TODO: works for gregorian only right now
-        ('G', 1): r'(%s|%s)' %(calendar.eras[1][1], calendar.eras[2][1]),
-        ('y', 2): r'([0-9]{2})',
-        ('y', 4): r'([0-9]{4})',
-        ('M', 1): r'([0-9]{1,2})',
-        ('M', 2): r'([0-9]{2})',
-        ('M', 3): r'('+'|'.join(calendar.getMonthAbbreviations())+')',
-        ('M', 4): r'('+'|'.join(calendar.getMonthNames())+')',
-        ('d', 1): r'([0-9]{1,2})',
-        ('d', 2): r'([0-9]{2})',
-        ('E', 1): r'([0-9])',
-        ('E', 2): r'([0-9]{2})',
-        ('E', 3): r'('+'|'.join(calendar.getDayAbbreviations())+')',
-        ('E', 4): r'('+'|'.join(calendar.getDayNames())+')',
-        ('D', 1): r'([0-9]{1,3})',
-        ('w', 1): r'([0-9])',
-        ('w', 2): r'([0-9]{2})',
-        ('h', 1): r'([0-9]{1,2})',
-        ('h', 2): r'([0-9]{2})',
-        ('H', 1): r'([0-9]{1,2})',
-        ('H', 2): r'([0-9]{2})',
-        ('m', 1): r'([0-9]{1,2})',
-        ('m', 2): r'([0-9]{2})',
-        ('s', 1): r'([0-9]{1,2})',
-        ('s', 2): r'([0-9]{2})',
-        ('S', 1): r'([0-9]{0,6})',
-        ('S', 2): r'([0-9]{6})',
-        ('F', 1): r'([0-9])',
-        ('F', 2): r'([0-9]{1,2})',
-        ('W', 1): r'([0-9])',
-        ('W', 2): r'([0-9]{2})',
-        ('k', 1): r'([0-9]{1,2})',
-        ('k', 2): r'([0-9]{2})',
-        ('K', 1): r'([0-9]{1,2})',
-        ('K', 2): r'([0-9]{2})',
-        ('z', 1): r'([\+-][0-9]{3,4})',
-        ('z', 2): r'([\+-][0-9]{2}:[0-9]{2})',
-        ('z', 3): r'([a-zA-Z]{3})',
-        ('z', 4): r'([a-zA-Z /\.]*)',
-        }
+    info = {}
+    # Generic Numbers
+    for field in 'dDFkKhHmsSwW':
+        for entry in _findFormattingCharacterInPattern(field, pattern):
+            # The maximum amount of digits should be infinity, but 1000 is
+            # close enough here. 
+            info[entry] = r'([0-9]{%i,1000})' %entry[1]
 
+    # year (Number)
+    for entry in _findFormattingCharacterInPattern('y', pattern):
+        if entry[1] == 2:
+            info[entry] = r'([0-9]{2})'
+        elif entry[1] == 4:
+            info[entry] = r'([0-9]{4})'
+        else:
+            raise DateTimePatternParseError, "Only 'yy' and 'yyyy' allowed." 
 
-def buildDateTimeInfo(dt, calendar):
+    # am/pm marker (Text)
+    for entry in _findFormattingCharacterInPattern('a', pattern):
+        info[entry] = r'(%s|%s)' %(calendar.am, calendar.pm)
+
+    # era designator (Text)
+    # TODO: works for gregorian only right now
+    for entry in _findFormattingCharacterInPattern('G', pattern):
+        info[entry] = r'(%s|%s)' %(calendar.eras[1][1], calendar.eras[2][1])
+
+    # time zone (Text)
+    for entry in _findFormattingCharacterInPattern('z', pattern):
+        if entry[1] == 1:
+            info[entry] = r'([\+-][0-9]{3,4})'
+        elif entry[1] == 2:
+            info[entry] = r'([\+-][0-9]{2}:[0-9]{2})'
+        elif entry[1] == 3:
+            info[entry] = r'([a-zA-Z]{3})'
+        else:
+            info[entry] = r'([a-zA-Z /\.]*)'
+
+    # month in year (Text and Number)
+    for entry in _findFormattingCharacterInPattern('M', pattern):
+        if entry[1] == 1:
+            info[entry] = r'([0-9]{1,2})'
+        elif entry[1] == 2:
+            info[entry] = r'([0-9]{2})'
+        elif entry[1] == 3:
+            info[entry] = r'('+'|'.join(calendar.getMonthAbbreviations())+')'
+        else:
+            info[entry] = r'('+'|'.join(calendar.getMonthNames())+')'
+
+    # day in week (Text and Number)
+    for entry in _findFormattingCharacterInPattern('E', pattern):
+        if entry[1] == 1:
+            info[entry] = r'([0-9])'
+        elif entry[1] == 2:
+            info[entry] = r'([0-9]{2})'
+        elif entry[1] == 3:
+            info[entry] = r'('+'|'.join(calendar.getDayAbbreviations())+')'
+        else:
+            info[entry] = r'('+'|'.join(calendar.getDayNames())+')'
+
+    return info
+
+
+def buildDateTimeInfo(dt, calendar, pattern):
     """Create the bits and pieces of the datetime object that can be put
     together."""
     if isinstance(dt, datetime.time):
@@ -546,51 +597,76 @@
 
     week_in_month = (dt.day + 6 - dt.weekday()) / 7 + 1
 
-    return {
-        ('a', 1): ampm,
-        ('G', 1): 'AD',
-        ('y', 2): str(dt.year)[2:],
-        ('y', 4): str(dt.year),
-        ('M', 1): str(dt.month),
-        ('M', 2): "%.2i" %dt.month,
-        ('M', 3): calendar.months[dt.month][1],
-        ('M', 4): calendar.months[dt.month][0],
-        ('d', 1): str(dt.day),
-        ('d', 2): "%.2i" %dt.day,
-        ('E', 1): str(weekday),
-        ('E', 2): "%.2i" %weekday,
-        ('E', 3): calendar.days[dt.weekday() + 1][1],
-        ('E', 4): calendar.days[dt.weekday() + 1][0],
-        ('D', 1): dt.strftime('%j'),
-        ('w', 1): dt.strftime('%W'),
-        ('w', 2): dt.strftime('%.2W'),
-        ('W', 1): "%i" %week_in_month,
-        ('W', 2): "%.2i" %week_in_month,
-        ('F', 1): "%i" %day_of_week_in_month,
-        ('F', 2): "%.2i" %day_of_week_in_month,
-        ('h', 1): str(h),
-        ('h', 2): "%.2i" %(h),
-        ('K', 1): str(dt.hour%12),
-        ('K', 2): "%.2i" %(dt.hour%12),
-        ('H', 1): str(dt.hour),
-        ('H', 2): "%.2i" %dt.hour,
-        ('k', 1): str(dt.hour or 24),
-        ('k', 2): "%.2i" %(dt.hour or 24),
-        ('m', 1): str(dt.minute),
-        ('m', 2): "%.2i" %dt.minute,
-        ('s', 1): str(dt.second),
-        ('s', 2): "%.2i" %dt.second,
-        ('S', 1): str(dt.microsecond),
-        ('S', 2): "%.6i" %dt.microsecond,
-        # TODO: Implement the following symbols. This requires the handling of
-        # timezones.
-        ('z', 1): "+000",
-        ('z', 2): "+00:00",
-        ('z', 3): "UTC",
-        ('z', 4): "Greenwich Time",
+    # Getting the timezone right
+    tzinfo = dt.tzinfo or pytz.reference.utc
+    tz_secs = tzinfo.utcoffset(dt).seconds
+    tz_secs = (tz_secs > 12*3600) and tz_secs-24*3600 or tz_secs
+    tz_mins = int(math.fabs(tz_secs % 3600 / 60))
+    tz_hours = int(math.fabs(tz_secs / 3600))
+    tz_sign = (tz_secs < 0) and '-' or '+'
+    tz_defaultname = "%s%i%.2i" %(tz_sign, tz_hours, tz_mins)
+    tz_name = tzinfo.tzname(dt) or tz_defaultname
+    tz_fullname = getattr(tzinfo, 'zone', None) or tz_name
+
+    info = {('y', 2): unicode(dt.year)[2:],
+            ('y', 4): unicode(dt.year),
             }
 
+    # Generic Numbers
+    for field, value in (('d', dt.day), ('D', int(dt.strftime('%j'))),
+                         ('F', day_of_week_in_month), ('k', dt.hour or 24),
+                         ('K', dt.hour%12), ('h', h), ('H', dt.hour),
+                         ('m', dt.minute), ('s', dt.second),
+                         ('S', dt.microsecond), ('w', int(dt.strftime('%W'))),
+                         ('W', week_in_month)):
+        for entry in _findFormattingCharacterInPattern(field, pattern):
+            info[entry] = (u'%%.%ii' %entry[1]) %value
 
+    # am/pm marker (Text)
+    for entry in _findFormattingCharacterInPattern('a', pattern):
+        info[entry] = ampm
+
+    # era designator (Text)
+    # TODO: works for gregorian only right now
+    for entry in _findFormattingCharacterInPattern('G', pattern):
+        info[entry] = calendar.eras[2][1]
+
+    # time zone (Text)
+    for entry in _findFormattingCharacterInPattern('z', pattern):
+        if entry[1] == 1:
+            info[entry] = u"%s%i%.2i" %(tz_sign, tz_hours, tz_mins)
+        elif entry[1] == 2:
+            info[entry] = u"%s%.2i:%.2i" %(tz_sign, tz_hours, tz_mins)
+        elif entry[1] == 3:
+            info[entry] = tz_name
+        else:
+            info[entry] = tz_fullname
+
+    # month in year (Text and Number)
+    for entry in _findFormattingCharacterInPattern('M', pattern):
+        if entry[1] == 1:
+            info[entry] = u'%i' %dt.month
+        elif entry[1] == 2:
+            info[entry] = u'%.2i' %dt.month
+        elif entry[1] == 3:
+            info[entry] = calendar.months[dt.month][1]
+        else:
+            info[entry] = calendar.months[dt.month][0]
+
+    # day in week (Text and Number)
+    for entry in _findFormattingCharacterInPattern('E', pattern):
+        if entry[1] == 1:
+            info[entry] = u'%i' %weekday
+        elif entry[1] == 2:
+            info[entry] = u'%.2i' %weekday
+        elif entry[1] == 3:
+            info[entry] = calendar.days[dt.weekday() + 1][1]
+        else:
+            info[entry] = calendar.days[dt.weekday() + 1][0]
+
+    return info
+
+
 # Number Pattern Parser States
 BEGIN = 0
 READ_PADDING_1 = 1

Modified: Zope3/trunk/src/zope/i18n/interfaces/__init__.py
===================================================================
--- Zope3/trunk/src/zope/i18n/interfaces/__init__.py	2005-02-16 11:19:32 UTC (rev 29157)
+++ Zope3/trunk/src/zope/i18n/interfaces/__init__.py	2005-02-16 14:27:13 UTC (rev 29158)
@@ -359,7 +359,7 @@
       m      minute in hour        (Number)          30
       s      second in minute      (Number)          55
       S      millisecond           (Number)          978
-      E      day in week           (Text)            Tuesday
+      E      day in week           (Text and Number) Tuesday
       D      day in year           (Number)          189
       F      day of week in month  (Number)          2 (2nd Wed in July)
       w      week in year          (Number)          27

Modified: Zope3/trunk/src/zope/i18n/tests/test_formats.py
===================================================================
--- Zope3/trunk/src/zope/i18n/tests/test_formats.py	2005-02-16 11:19:32 UTC (rev 29157)
+++ Zope3/trunk/src/zope/i18n/tests/test_formats.py	2005-02-16 14:27:13 UTC (rev 29158)
@@ -17,6 +17,7 @@
 """
 import os
 import datetime
+import pytz
 from unittest import TestCase, TestSuite, makeSuite
 
 from zope.i18n.interfaces import IDateTimeFormat
@@ -101,6 +102,8 @@
                          [('m', 2), ':', ('s', 2)])
         self.assertEqual(parseDateTimePattern('H:m:s'),
                          [('H', 1), ':', ('m', 1), ':', ('s', 1)])
+        self.assertEqual(parseDateTimePattern('HHH:mmmm:sssss'),
+                         [('H', 3), ':', ('m', 4), ':', ('s', 5)])
 
     def testParseGermanTimePattern(self):
         # German full
@@ -183,30 +186,66 @@
     method with the German locale.
     """
 
-    info = buildDateTimeParseInfo(LocaleCalendarStub())
+    def info(self, entry):
+        info = buildDateTimeParseInfo(LocaleCalendarStub(), [entry])
+        return info[entry]
 
+    def testGenericNumbers(self):
+        for char in 'dDFkKhHmsSwW':
+            for length in range(1, 6):
+                self.assertEqual(self.info((char, length)),
+                                 '([0-9]{%i,1000})' %length)
+    def testYear(self):
+        self.assertEqual(self.info(('y', 2)), '([0-9]{2})')
+        self.assertEqual(self.info(('y', 4)), '([0-9]{4})')
+        self.assertRaises(DateTimePatternParseError, self.info, ('y', 1))
+        self.assertRaises(DateTimePatternParseError, self.info, ('y', 3))
+        self.assertRaises(DateTimePatternParseError, self.info, ('y', 5))
+
+    def testAMPMMarker(self):
+        names = ['vorm.', 'nachm.']
+        for length in range(1, 6):
+            self.assertEqual(self.info(('a', length)), '('+'|'.join(names)+')')
+
     def testEra(self):
-        self.assertEqual(self.info[('G', 1)], '(v. Chr.|n. Chr.)')
+        self.assertEqual(self.info(('G', 1)), '(v. Chr.|n. Chr.)')
 
+    def testTimeZone(self):
+        self.assertEqual(self.info(('z', 1)), r'([\+-][0-9]{3,4})')
+        self.assertEqual(self.info(('z', 2)), r'([\+-][0-9]{2}:[0-9]{2})')
+        self.assertEqual(self.info(('z', 3)), r'([a-zA-Z]{3})')
+        self.assertEqual(self.info(('z', 4)), r'([a-zA-Z /\.]*)')
+        self.assertEqual(self.info(('z', 5)), r'([a-zA-Z /\.]*)')
+
+    def testMonthNumber(self):
+        self.assertEqual(self.info(('M', 1)), '([0-9]{1,2})')
+        self.assertEqual(self.info(('M', 2)), '([0-9]{2})')
+
     def testMonthNames(self):
         names = [u'Januar', u'Februar', u'Maerz', u'April',
                  u'Mai', u'Juni', u'Juli', u'August', u'September', u'Oktober',
                  u'November', u'Dezember']
-        self.assertEqual(self.info[('M', 4)], '('+'|'.join(names)+')')
+        self.assertEqual(self.info(('M', 4)), '('+'|'.join(names)+')')
 
     def testMonthAbbr(self):
         names = ['Jan', 'Feb', 'Mrz', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug',
                  'Sep', 'Okt', 'Nov', 'Dez']
-        self.assertEqual(self.info[('M', 3)], '('+'|'.join(names)+')')
+        self.assertEqual(self.info(('M', 3)), '('+'|'.join(names)+')')
 
+    def testWeekdayNumber(self):
+        self.assertEqual(self.info(('E', 1)), '([0-9])')
+        self.assertEqual(self.info(('E', 2)), '([0-9]{2})')
+
     def testWeekdayNames(self):
         names = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag',
                  'Freitag', 'Samstag', 'Sonntag']
-        self.assertEqual(self.info[('E', 4)], '('+'|'.join(names)+')')
+        self.assertEqual(self.info(('E', 4)), '('+'|'.join(names)+')')
+        self.assertEqual(self.info(('E', 5)), '('+'|'.join(names)+')')
+        self.assertEqual(self.info(('E', 10)), '('+'|'.join(names)+')')
 
     def testWeekdayAbbr(self):
         names = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']
-        self.assertEqual(self.info[('E', 3)], '('+'|'.join(names)+')')
+        self.assertEqual(self.info(('E', 3)), '('+'|'.join(names)+')')
 
 
 class TestDateTimeFormat(TestCase):
@@ -235,20 +274,47 @@
         self.assertEqual(self.format.parse(
             '2. Januar 2003 21:48:01 +100',
             'd. MMMM yyyy HH:mm:ss z'),
-            datetime.datetime(2003, 01, 02, 21, 48, 01))
+            datetime.datetime(2003, 01, 02, 21, 48, 01,
+                              tzinfo=pytz.timezone('Europe/Berlin')))
 
         # German full
         # TODO: The parser does not support timezones yet.
         self.assertEqual(self.format.parse(
             'Donnerstag, 2. Januar 2003 21:48 Uhr +100',
             "EEEE, d. MMMM yyyy H:mm' Uhr 'z"),
-            datetime.datetime(2003, 01, 02, 21, 48))
+            datetime.datetime(2003, 01, 02, 21, 48,
+                              tzinfo=pytz.timezone('Europe/Berlin')))
 
     def testParseAMPMDateTime(self):
         self.assertEqual(
             self.format.parse('02.01.03 09:48 nachm.', 'dd.MM.yy hh:mm a'),
             datetime.datetime(2003, 01, 02, 21, 48))
 
+    def testParseTimeZone(self):
+        dt = self.format.parse('09:48 -600', 'HH:mm z')
+        self.assertEqual(dt.tzinfo.utcoffset(dt), datetime.timedelta(hours=-6))
+        self.assertEqual(dt.tzinfo.zone, None)
+        self.assertEqual(dt.tzinfo.tzname(dt), None)
+
+        dt = self.format.parse('09:48 -06:00', 'HH:mm zz')
+        self.assertEqual(dt.tzinfo.utcoffset(dt), datetime.timedelta(hours=-6))
+        self.assertEqual(dt.tzinfo.zone, None)
+        self.assertEqual(dt.tzinfo.tzname(dt), None)
+
+    def testParseTimeZoneNames(self):
+        dt = self.format.parse('01.01.2003 09:48 EST', 'dd.MM.yyyy HH:mm zzz')
+        self.assertEqual(dt.tzinfo.utcoffset(dt), datetime.timedelta(hours=-6))
+        self.assertEqual(dt.tzinfo.zone, 'EST')
+        # I think this is wrong due to a bug in pytz
+        self.assertEqual(dt.tzinfo.tzname(dt), 'CST')
+
+        dt = self.format.parse('01.01.2003 09:48 US/Eastern',
+                               'dd.MM.yyyy HH:mm zzzz')
+        self.assertEqual(dt.tzinfo.utcoffset(dt), datetime.timedelta(hours=-5))
+        self.assertEqual(dt.tzinfo.zone, 'US/Eastern')
+        # I think this is wrong due to a bug in pytz
+        self.assertEqual(dt.tzinfo.tzname(dt), 'EST')
+
     def testDateTimeParseError(self):
         self.assertRaises(DateTimeParseError,
             self.format.parse, '02.01.03 21:48', 'dd.MM.yyyy HH:mm')
@@ -258,6 +324,16 @@
             self.format.parse('01.01.03 12:00 nachm.', 'dd.MM.yy hh:mm a'),
             datetime.datetime(2003, 01, 01, 12, 00, 00, 00))
 
+    def testParseUnusualFormats(self):
+        self.assertEqual(
+            self.format.parse('001. Januar 03 0012:00',
+                              'ddd. MMMMM yy HHHH:mm'),
+            datetime.datetime(2003, 01, 01, 12, 00, 00, 00))
+        self.assertEqual(
+            self.format.parse('0001. Jan 2003 0012:00 vorm.',
+                              'dddd. MMM yyyy hhhh:mm a'),
+            datetime.datetime(2003, 01, 01, 00, 00, 00, 00))
+
     def testFormatSimpleDateTime(self):
         # German short
         self.assertEqual(
@@ -266,25 +342,22 @@
             '02.01.03 21:48')
 
     def testFormatRealDateTime(self):
+        tz = pytz.timezone('Europe/Berlin')
+        dt = datetime.datetime(2003, 01, 02, 21, 48, 01, tzinfo=tz)
         # German medium
         self.assertEqual(
-            self.format.format(datetime.datetime(2003, 01, 02, 21, 48, 01),
-                              'dd.MM.yyyy HH:mm:ss'),
+            self.format.format(dt, 'dd.MM.yyyy HH:mm:ss'),
             '02.01.2003 21:48:01')
 
         # German long
-        # TODO: The parser does not support timezones yet.
-        self.assertEqual(self.format.format(
-            datetime.datetime(2003, 01, 02, 21, 48, 01),
-            'd. MMMM yyyy HH:mm:ss z'),
-            '2. Januar 2003 21:48:01 +000')
+        self.assertEqual(
+            self.format.format(dt, 'd. MMMM yyyy HH:mm:ss z'),
+            '2. Januar 2003 21:48:01 +100')
 
         # German full
-        # TODO: The parser does not support timezones yet.
         self.assertEqual(self.format.format(
-            datetime.datetime(2003, 01, 02, 21, 48),
-            "EEEE, d. MMMM yyyy H:mm' Uhr 'z"),
-            'Donnerstag, 2. Januar 2003 21:48 Uhr +000')
+            dt, "EEEE, d. MMMM yyyy H:mm' Uhr 'z"),
+            'Donnerstag, 2. Januar 2003 21:48 Uhr +100')
 
     def testFormatAMPMDateTime(self):
         self.assertEqual(self.format.format(
@@ -300,6 +373,33 @@
                 '%s, %i. Januar 2003 21:48 Uhr +000' %(
                 self.format.calendar.days[day][0], day+5))
 
+    def testFormatTimeZone(self):
+        self.assertEqual(self.format.format(
+            datetime.datetime(2003, 01, 02, 12, 00), 'z'),
+            '+000')
+        self.assertEqual(self.format.format(
+            datetime.datetime(2003, 01, 02, 12, 00), 'zz'),
+            '+00:00')
+        self.assertEqual(self.format.format(
+            datetime.datetime(2003, 01, 02, 12, 00), 'zzz'),
+            'UTC')
+        self.assertEqual(self.format.format(
+            datetime.datetime(2003, 01, 02, 12, 00), 'zzzz'),
+            'UTC')
+        tz = pytz.timezone('US/Eastern')
+        self.assertEqual(self.format.format(
+            datetime.datetime(2003, 01, 02, 12, tzinfo=tz), 'z'),
+            '-500')
+        self.assertEqual(self.format.format(
+            datetime.datetime(2003, 01, 02, 12, tzinfo=tz), 'zz'),
+            '-05:00')
+        self.assertEqual(self.format.format(
+            datetime.datetime(2003, 01, 02, 12, tzinfo=tz), 'zzz'),
+            'EST')
+        self.assertEqual(self.format.format(
+            datetime.datetime(2003, 01, 02, 12, tzinfo=tz), 'zzzz'),
+            'US/Eastern')
+
     def testFormatWeekDay(self):
         date = datetime.date(2003, 01, 02)
         self.assertEqual(self.format.format(date, "E"),
@@ -414,7 +514,60 @@
             self.format.format(datetime.time(13, 15), 'h:mm a'), 
             '1:15 nachm.')
 
+    def testFormatDayInYear(self):
+        self.assertEqual(
+            self.format.format(datetime.date(2003, 1, 3), 'D'), 
+            u'3')
+        self.assertEqual(
+            self.format.format(datetime.date(2003, 1, 3), 'DD'), 
+            u'03')
+        self.assertEqual(
+            self.format.format(datetime.date(2003, 1, 3), 'DDD'), 
+            u'003')
+        self.assertEqual(
+            self.format.format(datetime.date(2003, 12, 31), 'D'), 
+            u'365')
+        self.assertEqual(
+            self.format.format(datetime.date(2003, 12, 31), 'DD'), 
+            u'365')
+        self.assertEqual(
+            self.format.format(datetime.date(2003, 12, 31), 'DDD'), 
+            u'365')
+        self.assertEqual(
+            self.format.format(datetime.date(2004, 12, 31), 'DDD'), 
+            u'366')
 
+    def testFormatDayOfWeekInMOnth(self):
+        self.assertEqual(
+            self.format.format(datetime.date(2003, 1, 3), 'F'), 
+            u'1')
+        self.assertEqual(
+            self.format.format(datetime.date(2003, 1, 10), 'F'), 
+            u'2')
+        self.assertEqual(
+            self.format.format(datetime.date(2003, 1, 17), 'F'), 
+            u'3')
+        self.assertEqual(
+            self.format.format(datetime.date(2003, 1, 24), 'F'), 
+            u'4')
+        self.assertEqual(
+            self.format.format(datetime.date(2003, 1, 31), 'F'), 
+            u'5')
+        self.assertEqual(
+            self.format.format(datetime.date(2003, 1, 6), 'F'), 
+            u'1')
+
+    def testFormatUnusualFormats(self):
+        self.assertEqual(
+            self.format.format(datetime.date(2003, 1, 3), 'DDD-yyyy'), 
+            u'003-2003')
+        self.assertEqual(
+            self.format.format(datetime.date(2003, 1, 10),
+                               "F. EEEE 'im' MMMM, yyyy"), 
+            u'2. Freitag im Januar, 2003')
+
+
+
 class TestNumberPatternParser(TestCase):
     """Extensive tests for the ICU-based-syntax number pattern parser."""
 



More information about the Zope3-Checkins mailing list