[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