summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGary Poster <gary@zope.com>2005-09-07 18:27:36 +0000
committerGary Poster <gary@zope.com>2005-09-07 18:27:36 +0000
commit6e554090402cfe2a75ce26c7f281acfb77c5a9d5 (patch)
tree08b1a07772e9ad0196be4ca1c0058f637c370e58
downloadzope-i18n-monolithic-zope3-Zope-3.1.tar.gz
Merge the pytz upgrades and related i18n fixes that Stuart Bishop did on themonolithic-zope3-Zope-3.1
trunk (revisions 38302, 38333, and 38336)
-rw-r--r--format.py900
-rw-r--r--tests/test_formats.py1146
2 files changed, 2046 insertions, 0 deletions
diff --git a/format.py b/format.py
new file mode 100644
index 0000000..02765b4
--- /dev/null
+++ b/format.py
@@ -0,0 +1,900 @@
+##############################################################################
+#
+# Copyright (c) 2002, 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Basic Object Formatting
+
+This module implements basic object formatting functionality, such as
+date/time, number and money formatting.
+
+$Id$
+"""
+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."""
+
+class DateTimeFormat(object):
+ __doc__ = IDateTimeFormat.__doc__
+
+ implements(IDateTimeFormat)
+
+ _DATETIMECHARS = "aGyMdEDFwWhHmsSkKz"
+
+ def __init__(self, pattern=None, calendar=None):
+ if calendar is not None:
+ self.calendar = calendar
+ self._pattern = pattern
+ self._bin_pattern = None
+ if self._pattern is not None:
+ self._bin_pattern = parseDateTimePattern(self._pattern,
+ self._DATETIMECHARS)
+
+ def setPattern(self, pattern):
+ "See zope.i18n.interfaces.IFormat"
+ self._pattern = pattern
+ self._bin_pattern = parseDateTimePattern(self._pattern,
+ self._DATETIMECHARS)
+
+ def getPattern(self):
+ "See zope.i18n.interfaces.IFormat"
+ return self._pattern
+
+ def parse(self, text, pattern=None, asObject=True):
+ "See zope.i18n.interfaces.IFormat"
+ # Make or get binary form of datetime pattern
+ if pattern is not None:
+ 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, bin_pattern)
+ for elem in bin_pattern:
+ regex += info.get(elem, elem)
+ try:
+ results = re.match(regex, text).groups()
+ except AttributeError:
+ raise DateTimeParseError(
+ 'The datetime string did not match the pattern.')
+ # Sometimes you only want the parse results
+ if not asObject:
+ return results
+
+ # Map the parsing results to a datetime object
+ ordered = [None, None, None, None, None, None, None]
+ bin_pattern = filter(lambda x: isinstance(x, tuple), bin_pattern)
+
+ # 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:
+ ordered[0] = 1900 + year
+ else:
+ ordered[0] = 2000 + year
+ if ('y', 4) in bin_pattern:
+ ordered[0] = int(results[bin_pattern.index(('y', 4))])
+
+ # 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)
+ elif month_entry and month_entry[0][1] >= 4:
+ name = results[bin_pattern.index(month_entry[0])]
+ ordered[1] = self.calendar.getMonthTypeFromName(name)
+ 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 = {'d': 2, 'H': 3, 'm': 4, 's': 5, 'S': 6}
+ for field in dt_fields_map.keys():
+ entry = _findFormattingCharacterInPattern(field, bin_pattern)
+ if not entry: continue
+ pos = dt_fields_map[field]
+ ordered[pos] = int(results[bin_pattern.index(entry[0])])
+
+ # Handle timezones
+ tzinfo = None
+ pytz_tzinfo = False # If True, we should use pytz specific syntax
+ 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)
+ # XXX: I think this is making an unpickable tzinfo.
+ # Note that StaticTzInfo is not part of the exposed pytz API.
+ tzinfo = pytz.tzinfo.StaticTzInfo()
+ tzinfo._utcoffset = delta
+ pytz_tzinfo = True
+ elif length == 2:
+ hours, mins = int(value[:-3]), int(value[-2:])
+ delta = datetime.timedelta(hours=hours, minutes=mins)
+ # XXX: I think this is making an unpickable tzinfo.
+ # Note that StaticTzInfo is not part of the exposed pytz API.
+ tzinfo = pytz.tzinfo.StaticTzInfo()
+ tzinfo._utcoffset = delta
+ pytz_tzinfo = True
+ else:
+ try:
+ tzinfo = pytz.timezone(value)
+ pytz_tzinfo = True
+ except KeyError:
+ # TODO: Find timezones using locale information
+ pass
+
+ # Create a date/time object from the data
+ # If we have a pytz tzinfo, we need to invoke localize() as per
+ # the pytz documentation on creating local times.
+ # NB. If we are in an end-of-DST transition period, we have a 50%
+ # chance of getting a time 1 hour out here, but that is the price
+ # paid for dealing with localtimes.
+ if ordered[3:] == [None, None, None, None]:
+ return datetime.date(*[e or 0 for e in ordered[:3]])
+ elif ordered[:3] == [None, None, None]:
+ if pytz_tzinfo:
+ return tzinfo.localize(
+ datetime.time(*[e or 0 for e in ordered[3:]])
+ )
+ else:
+ return datetime.time(
+ *[e or 0 for e in ordered[3:]], **{'tzinfo' :tzinfo}
+ )
+ else:
+ if pytz_tzinfo:
+ return tzinfo.localize(datetime.datetime(
+ *[e or 0 for e in ordered]
+ ))
+ else:
+ return datetime.datetime(
+ *[e or 0 for e in ordered], **{'tzinfo' :tzinfo}
+ )
+
+ def format(self, obj, pattern=None):
+ "See zope.i18n.interfaces.IFormat"
+ # Make or get binary form of datetime pattern
+ if pattern is not None:
+ bin_pattern = parseDateTimePattern(pattern)
+ else:
+ bin_pattern = self._bin_pattern
+
+ text = u''
+ info = buildDateTimeInfo(obj, self.calendar, bin_pattern)
+ for elem in bin_pattern:
+ text += info.get(elem, elem)
+
+ return text
+
+
+class NumberParseError(Exception):
+ """Error that can be raised when smething unexpected happens during the
+ number parsing process."""
+
+
+class NumberFormat(object):
+ __doc__ = INumberFormat.__doc__
+
+ implements(INumberFormat)
+
+ def __init__(self, pattern=None, symbols={}):
+ # setup default symbols
+ self.symbols = {
+ u'decimal': u'.',
+ u'group': u',',
+ u'list': u';',
+ u'percentSign': u'%',
+ u'nativeZeroDigit': u'0',
+ u'patternDigit': u'#',
+ u'plusSign': u'+',
+ u'minusSign': u'-',
+ u'exponential': u'E',
+ u'perMille': u'\xe2\x88\x9e',
+ u'infinity': u'\xef\xbf\xbd',
+ u'nan': '' }
+ self.symbols.update(symbols)
+ self._pattern = pattern
+ self._bin_pattern = None
+ if self._pattern is not None:
+ self._bin_pattern = parseNumberPattern(self._pattern)
+
+ def setPattern(self, pattern):
+ "See zope.i18n.interfaces.IFormat"
+ self._pattern = pattern
+ self._bin_pattern = parseNumberPattern(self._pattern)
+
+ def getPattern(self):
+ "See zope.i18n.interfaces.IFormat"
+ return self._pattern
+
+ def parse(self, text, pattern=None):
+ "See zope.i18n.interfaces.IFormat"
+ # Make or get binary form of datetime pattern
+ if pattern is not None:
+ bin_pattern = parseNumberPattern(pattern)
+ else:
+ bin_pattern = self._bin_pattern
+ # Determine sign
+ num_res = [None, None]
+ for sign in (0, 1):
+ regex = ''
+ if bin_pattern[sign][PADDING1] is not None:
+ regex += '[' + bin_pattern[sign][PADDING1] + ']+'
+ if bin_pattern[sign][PREFIX] != '':
+ regex += '[' + bin_pattern[sign][PREFIX] + ']'
+ if bin_pattern[sign][PADDING2] is not None:
+ regex += '[' + bin_pattern[sign][PADDING2] + ']+'
+ regex += '([0-9'
+ min_size = bin_pattern[sign][INTEGER].count('0')
+ if bin_pattern[sign][GROUPING]:
+ regex += self.symbols['group']
+ min_size += min_size/3
+ regex += ']{%i,100}' %(min_size)
+ if bin_pattern[sign][FRACTION]:
+ max_precision = len(bin_pattern[sign][FRACTION])
+ min_precision = bin_pattern[sign][FRACTION].count('0')
+ regex += '['+self.symbols['decimal']+']'
+ regex += '[0-9]{%i,%i}' %(min_precision, max_precision)
+ if bin_pattern[sign][EXPONENTIAL] != '':
+ regex += self.symbols['exponential']
+ min_exp_size = bin_pattern[sign][EXPONENTIAL].count('0')
+ pre_symbols = self.symbols['minusSign']
+ if bin_pattern[sign][EXPONENTIAL][0] == '+':
+ pre_symbols += self.symbols['plusSign']
+ regex += '[%s]?[0-9]{%i,100}' %(pre_symbols, min_exp_size)
+ regex +=')'
+ if bin_pattern[sign][PADDING3] is not None:
+ regex += '[' + bin_pattern[sign][PADDING3] + ']+'
+ if bin_pattern[sign][SUFFIX] != '':
+ regex += '[' + bin_pattern[sign][SUFFIX] + ']'
+ if bin_pattern[sign][PADDING4] is not None:
+ regex += '[' + bin_pattern[sign][PADDING4] + ']+'
+ num_res[sign] = re.match(regex, text)
+
+ if num_res[0] is not None:
+ num_str = num_res[0].groups()[0]
+ sign = +1
+ elif num_res[1] is not None:
+ num_str = num_res[1].groups()[0]
+ sign = -1
+ else:
+ raise NumberParseError, 'Not a valid number for this pattern.'
+ # Remove possible grouping separators
+ num_str = num_str.replace(self.symbols['group'], '')
+ # Extract number
+ type = int
+ if self.symbols['decimal'] in num_str:
+ type = float
+ num_str = num_str.replace(self.symbols['decimal'], '.')
+ if self.symbols['exponential'] in num_str:
+ type = float
+ num_str = num_str.replace(self.symbols['exponential'], 'E')
+ return sign*type(num_str)
+
+ def _format_integer(self, integer, pattern):
+ size = len(integer)
+ min_size = pattern.count('0')
+ if size < min_size:
+ integer = self.symbols['nativeZeroDigit']*(min_size-size) + integer
+ return integer
+
+ def _format_fraction(self, fraction, pattern):
+ max_precision = len(pattern)
+ min_precision = pattern.count('0')
+ precision = len(fraction)
+ roundInt = False
+ if precision > max_precision:
+ round = int(fraction[max_precision]) >= 5
+ fraction = fraction[:max_precision]
+ if round:
+ if fraction != '':
+ # add 1 to the fraction, maintaining the decimal
+ # precision; if the result >= 1, need to roundInt
+ fractionLen = len(fraction)
+ rounded = int(fraction) + 1
+ fraction = ('%0' + str(fractionLen) + 'i') % rounded
+ if len(fraction) > fractionLen: # rounded fraction >= 1
+ roundInt = True
+ fraction = fraction[1:]
+ else:
+ # fraction missing, e.g. 1.5 -> 1. -- need to roundInt
+ roundInt = True
+
+ if precision < min_precision:
+ fraction += self.symbols['nativeZeroDigit']*(min_precision -
+ precision)
+ if fraction != '':
+ fraction = self.symbols['decimal'] + fraction
+ return fraction, roundInt
+
+ def format(self, obj, pattern=None):
+ "See zope.i18n.interfaces.IFormat"
+ # Make or get binary form of datetime pattern
+ if pattern is not None:
+ bin_pattern = parseNumberPattern(pattern)
+ else:
+ bin_pattern = self._bin_pattern
+ # Get positive or negative sub-pattern
+ if obj >= 0:
+ bin_pattern = bin_pattern[0]
+ else:
+ bin_pattern = bin_pattern[1]
+
+
+ if bin_pattern[EXPONENTIAL] != '':
+ obj_int_frac = str(obj).split('.')
+ # The exponential might have a mandatory sign; remove it from the
+ # bin_pattern and remember the setting
+ exp_bin_pattern = bin_pattern[EXPONENTIAL]
+ plus_sign = u''
+ if exp_bin_pattern.startswith('+'):
+ plus_sign = self.symbols['plusSign']
+ exp_bin_pattern = exp_bin_pattern[1:]
+ # We have to remove the possible '-' sign
+ if obj < 0:
+ obj_int_frac[0] = obj_int_frac[0][1:]
+ if obj_int_frac[0] == '0':
+ # abs() of number smaller 1
+ if len(obj_int_frac) > 1:
+ res = re.match('(0*)[0-9]*', obj_int_frac[1]).groups()[0]
+ exponent = self._format_integer(str(len(res)+1),
+ exp_bin_pattern)
+ exponent = self.symbols['minusSign']+exponent
+ number = obj_int_frac[1][len(res):]
+ else:
+ # We have exactly 0
+ exponent = self._format_integer('0', exp_bin_pattern)
+ number = self.symbols['nativeZeroDigit']
+ else:
+ exponent = self._format_integer(str(len(obj_int_frac[0])-1),
+ exp_bin_pattern)
+ number = ''.join(obj_int_frac)
+
+ fraction, roundInt = self._format_fraction(number[1:],
+ bin_pattern[FRACTION])
+ if roundInt:
+ number = str(int(number[0]) + 1) + fraction
+ else:
+ number = number[0] + fraction
+
+ # We might have a plus sign in front of the exponential integer
+ if not exponent.startswith('-'):
+ exponent = plus_sign + exponent
+
+ pre_padding = len(bin_pattern[FRACTION]) - len(number) + 2
+ post_padding = len(exp_bin_pattern) - len(exponent)
+ number += self.symbols['exponential'] + exponent
+
+ else:
+ obj_int_frac = str(obj).split('.')
+ if len(obj_int_frac) > 1:
+ fraction, roundInt = self._format_fraction(obj_int_frac[1],
+ bin_pattern[FRACTION])
+ else:
+ fraction = ''
+ roundInt = False
+ if roundInt:
+ obj = round(obj)
+ integer = self._format_integer(str(int(math.fabs(obj))),
+ bin_pattern[INTEGER])
+ # Adding grouping
+ if bin_pattern[GROUPING] == 1:
+ help = ''
+ for pos in range(1, len(integer)+1):
+ if (pos-1)%3 == 0 and pos != 1:
+ help = self.symbols['group'] + help
+ help = integer[-pos] + help
+ integer = help
+ pre_padding = len(bin_pattern[INTEGER]) - len(integer)
+ post_padding = len(bin_pattern[FRACTION]) - len(fraction)+1
+ number = integer + fraction
+
+ # Put it all together
+ text = ''
+ if bin_pattern[PADDING1] is not None and pre_padding > 0:
+ text += bin_pattern[PADDING1]*pre_padding
+ text += bin_pattern[PREFIX]
+ if bin_pattern[PADDING2] is not None and pre_padding > 0:
+ if bin_pattern[PADDING1] is not None:
+ text += bin_pattern[PADDING2]
+ else:
+ text += bin_pattern[PADDING2]*pre_padding
+ text += number
+ if bin_pattern[PADDING3] is not None and post_padding > 0:
+ if bin_pattern[PADDING4] is not None:
+ text += bin_pattern[PADDING3]
+ else:
+ text += bin_pattern[PADDING3]*post_padding
+ text += bin_pattern[SUFFIX]
+ if bin_pattern[PADDING4] is not None and post_padding > 0:
+ text += bin_pattern[PADDING4]*post_padding
+
+ # TODO: Need to make sure unicode is everywhere
+ return unicode(text)
+
+
+
+DEFAULT = 0
+IN_QUOTE = 1
+IN_DATETIMEFIELD = 2
+
+class DateTimePatternParseError(Exception):
+ """DateTime Pattern Parse Error"""
+
+
+def parseDateTimePattern(pattern, DATETIMECHARS="aGyMdEDFwWhHmsSkKz"):
+ """This method can handle everything: time, date and datetime strings."""
+ result = []
+ state = DEFAULT
+ helper = ''
+ char = ''
+ quote_start = -2
+
+ for pos in range(len(pattern)):
+ prev_char = char
+ char = pattern[pos]
+ # Handle quotations
+ if char == "'":
+ if state == DEFAULT:
+ quote_start = pos
+ state = IN_QUOTE
+ elif state == IN_QUOTE and prev_char == "'":
+ helper += char
+ state = DEFAULT
+ elif state == IN_QUOTE:
+ # Do not care about putting the content of the quote in the
+ # result. The next state is responsible for that.
+ quote_start = -1
+ state = DEFAULT
+ elif state == IN_DATETIMEFIELD:
+ result.append((helper[0], len(helper)))
+ helper = ''
+ quote_start = pos
+ state = IN_QUOTE
+ elif state == IN_QUOTE:
+ helper += char
+
+ # Handle regular characters
+ elif char not in DATETIMECHARS:
+ if state == IN_DATETIMEFIELD:
+ result.append((helper[0], len(helper)))
+ helper = char
+ state = DEFAULT
+ elif state == DEFAULT:
+ helper += char
+
+ # Handle special formatting characters
+ elif char in DATETIMECHARS:
+ if state == DEFAULT:
+ # Clean up helper first
+ if helper:
+ result.append(helper)
+ helper = char
+ state = IN_DATETIMEFIELD
+
+ elif state == IN_DATETIMEFIELD and prev_char == char:
+ helper += char
+
+ elif state == IN_DATETIMEFIELD and prev_char != char:
+ result.append((helper[0], len(helper)))
+ helper = char
+
+ # Some cleaning up
+ if state == IN_QUOTE:
+ if quote_start == -1:
+ raise DateTimePatternParseError, \
+ 'Waaa: state = IN_QUOTE and quote_start = -1!'
+ else:
+ raise DateTimePatternParseError, \
+ ('The quote starting at character %i is not closed.' %
+ quote_start)
+ elif state == IN_DATETIMEFIELD:
+ result.append((helper[0], len(helper)))
+ elif state == DEFAULT:
+ result.append(helper)
+
+ return result
+
+
+def buildDateTimeParseInfo(calendar, pattern):
+ """This method returns a dictionary that helps us with the parsing.
+ It also depends on the locale of course."""
+ 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."
+
+ # 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):
+ dt = datetime.datetime(1969, 01, 01, dt.hour, dt.minute, dt.second,
+ dt.microsecond)
+ elif (isinstance(dt, datetime.date) and
+ not isinstance(dt, datetime.datetime)):
+ dt = datetime.datetime(dt.year, dt.month, dt.day)
+
+ if dt.hour >= 12:
+ ampm = calendar.pm
+ else:
+ ampm = calendar.am
+
+ h = dt.hour%12
+ if h == 0:
+ h = 12
+
+ weekday = (dt.weekday() + (8 - calendar.week['firstDay'])) % 7 + 1
+
+ day_of_week_in_month = (dt.day - 1) / 7 + 1
+
+ week_in_month = (dt.day + 6 - dt.weekday()) / 7 + 1
+
+ # Getting the timezone right
+ tzinfo = dt.tzinfo or pytz.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
+READ_PREFIX = 2
+READ_PREFIX_STRING = 3
+READ_PADDING_2 = 4
+READ_INTEGER = 5
+READ_FRACTION = 6
+READ_EXPONENTIAL = 7
+READ_PADDING_3 = 8
+READ_SUFFIX = 9
+READ_SUFFIX_STRING = 10
+READ_PADDING_4 = 11
+READ_NEG_SUBPATTERN = 12
+
+# Binary Pattern Locators
+PADDING1 = 0
+PREFIX = 1
+PADDING2 = 2
+INTEGER = 3
+FRACTION = 4
+EXPONENTIAL = 5
+PADDING3 = 6
+SUFFIX = 7
+PADDING4 = 8
+GROUPING = 9
+
+class NumberPatternParseError(Exception):
+ """Number Pattern Parse Error"""
+
+
+def parseNumberPattern(pattern):
+ """Parses all sorts of number pattern."""
+ prefix = ''
+ padding_1 = None
+ padding_2 = None
+ padding_3 = None
+ padding_4 = None
+ integer = ''
+ fraction = ''
+ exponential = ''
+ suffix = ''
+ grouping = 0
+ neg_pattern = None
+
+ SPECIALCHARS = "*.,#0;E'"
+
+ length = len(pattern)
+ state = BEGIN
+ helper = ''
+ for pos in range(length):
+ char = pattern[pos]
+ if state == BEGIN:
+ if char == '*':
+ state = READ_PADDING_1
+ elif char not in SPECIALCHARS:
+ state = READ_PREFIX
+ prefix += char
+ elif char == "'":
+ state = READ_PREFIX_STRING
+ elif char in '#0':
+ state = READ_INTEGER
+ helper += char
+ else:
+ raise NumberPatternParseError, \
+ 'Wrong syntax at beginning of pattern.'
+
+ elif state == READ_PADDING_1:
+ padding_1 = char
+ state = READ_PREFIX
+
+ elif state == READ_PREFIX:
+ if char == "*":
+ state = READ_PADDING_2
+ elif char == "'":
+ state = READ_PREFIX_STRING
+ elif char == "#" or char == "0":
+ state = READ_INTEGER
+ helper += char
+ else:
+ prefix += char
+
+ elif state == READ_PREFIX_STRING:
+ if char == "'":
+ state = READ_PREFIX
+ else:
+ prefix += char
+
+ elif state == READ_PADDING_2:
+ padding_2 = char
+ state = READ_INTEGER
+
+ elif state == READ_INTEGER:
+ if char == "#" or char == "0":
+ helper += char
+ elif char == ",":
+ grouping = 1
+ elif char == ".":
+ integer = helper
+ helper = ''
+ state = READ_FRACTION
+ elif char == "E":
+ integer = helper
+ helper = ''
+ state = READ_EXPONENTIAL
+ elif char == "*":
+ integer = helper
+ helper = ''
+ state = READ_PADDING_3
+ elif char == ";":
+ integer = helper
+ state = READ_NEG_SUBPATTERN
+ elif char == "'":
+ integer = helper
+ state = READ_SUFFIX_STRING
+ else:
+ integer = helper
+ suffix += char
+ state = READ_SUFFIX
+
+ elif state == READ_FRACTION:
+ if char == "#" or char == "0":
+ helper += char
+ elif char == "E":
+ fraction = helper
+ helper = ''
+ state = READ_EXPONENTIAL
+ elif char == "*":
+ fraction = helper
+ helper = ''
+ state = READ_PADDING_3
+ elif char == ";":
+ fraction = helper
+ state = READ_NEG_SUBPATTERN
+ elif char == "'":
+ fraction = helper
+ state = READ_SUFFIX_STRING
+ else:
+ fraction = helper
+ suffix += char
+ state = READ_SUFFIX
+
+ elif state == READ_EXPONENTIAL:
+ if char in ('0', '#', '+'):
+ helper += char
+ elif char == "*":
+ exponential = helper
+ helper = ''
+ state = READ_PADDING_3
+ elif char == ";":
+ exponential = helper
+ state = READ_NEG_SUBPATTERN
+ elif char == "'":
+ exponential = helper
+ state = READ_SUFFIX_STRING
+ else:
+ exponential = helper
+ suffix += char
+ state = READ_SUFFIX
+
+ elif state == READ_PADDING_3:
+ padding_3 = char
+ state = READ_SUFFIX
+
+ elif state == READ_SUFFIX:
+ if char == "*":
+ state = READ_PADDING_4
+ elif char == "'":
+ state = READ_SUFFIX_STRING
+ elif char == ";":
+ state = READ_NEG_SUBPATTERN
+ else:
+ suffix += char
+
+ elif state == READ_SUFFIX_STRING:
+ if char == "'":
+ state = READ_SUFFIX
+ else:
+ suffix += char
+
+ elif state == READ_PADDING_4:
+ if char == ';':
+ state = READ_NEG_SUBPATTERN
+ else:
+ padding_4 = char
+
+ elif state == READ_NEG_SUBPATTERN:
+ neg_pattern = parseNumberPattern(pattern[pos:])[0]
+ break
+
+ # Cleaning up states after end of parsing
+ if state == READ_INTEGER:
+ integer = helper
+ if state == READ_FRACTION:
+ fraction = helper
+ if state == READ_EXPONENTIAL:
+ exponential = helper
+
+ pattern = (padding_1, prefix, padding_2, integer, fraction, exponential,
+ padding_3, suffix, padding_4, grouping)
+
+ if neg_pattern is None:
+ neg_pattern = pattern
+
+ return pattern, neg_pattern
+
+
diff --git a/tests/test_formats.py b/tests/test_formats.py
new file mode 100644
index 0000000..3a6d262
--- /dev/null
+++ b/tests/test_formats.py
@@ -0,0 +1,1146 @@
+##############################################################################
+#
+# Copyright (c) 2002, 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""This module tests the Formats and everything that goes with it.
+
+$Id$
+"""
+import os
+import datetime
+import pytz
+from unittest import TestCase, TestSuite, makeSuite
+
+from zope.i18n.interfaces import IDateTimeFormat
+from zope.i18n.format import DateTimeFormat
+from zope.i18n.format import parseDateTimePattern, buildDateTimeParseInfo
+from zope.i18n.format import DateTimePatternParseError, DateTimeParseError
+
+from zope.i18n.interfaces import INumberFormat
+from zope.i18n.format import NumberFormat
+from zope.i18n.format import parseNumberPattern
+
+class LocaleStub(object):
+ pass
+
+class LocaleCalendarStub(object):
+
+ type = u'gregorian'
+
+ months = { 1: ('Januar', 'Jan'), 2: ('Februar', 'Feb'),
+ 3: ('Maerz', 'Mrz'), 4: ('April', 'Apr'),
+ 5: ('Mai', 'Mai'), 6: ('Juni', 'Jun'),
+ 7: ('Juli', 'Jul'), 8: ('August', 'Aug'),
+ 9: ('September', 'Sep'), 10: ('Oktober', 'Okt'),
+ 11: ('November', 'Nov'), 12: ('Dezember', 'Dez')}
+
+ days = {1: ('Montag', 'Mo'), 2: ('Dienstag', 'Di'),
+ 3: ('Mittwoch', 'Mi'), 4: ('Donnerstag', 'Do'),
+ 5: ('Freitag', 'Fr'), 6: ('Samstag', 'Sa'),
+ 7: ('Sonntag', 'So')}
+
+ am = 'vorm.'
+ pm = 'nachm.'
+
+ eras = {1: (None, 'v. Chr.'), 2: (None, 'n. Chr.')}
+
+ week = {'firstDay': 1, 'minDays': 1}
+
+ def getMonthNames(self):
+ return [self.months.get(type, (None, None))[0] for type in range(1, 13)]
+
+ def getMonthTypeFromName(self, name):
+ for item in self.months.items():
+ if item[1][0] == name:
+ return item[0]
+
+ def getMonthAbbreviations(self):
+ return [self.months.get(type, (None, None))[1] for type in range(1, 13)]
+
+ def getMonthTypeFromAbbreviation(self, abbr):
+ for item in self.months.items():
+ if item[1][1] == abbr:
+ return item[0]
+
+ def getDayNames(self):
+ return [self.days.get(type, (None, None))[0] for type in range(1, 8)]
+
+ def getDayTypeFromName(self, name):
+ for item in self.days.items():
+ if item[1][0] == name:
+ return item[0]
+
+ def getDayAbbreviations(self):
+ return [self.days.get(type, (None, None))[1] for type in range(1, 8)]
+
+ def getDayTypeFromAbbreviation(self, abbr):
+ for item in self.days.items():
+ if item[1][1] == abbr:
+ return item[0]
+
+
+class TestDateTimePatternParser(TestCase):
+ """Extensive tests for the ICU-based-syntax datetime pattern parser."""
+
+ def testParseSimpleTimePattern(self):
+ self.assertEqual(parseDateTimePattern('HH'),
+ [('H', 2)])
+ self.assertEqual(parseDateTimePattern('HH:mm'),
+ [('H', 2), ':', ('m', 2)])
+ self.assertEqual(parseDateTimePattern('HH:mm:ss'),
+ [('H', 2), ':', ('m', 2), ':', ('s', 2)])
+ self.assertEqual(parseDateTimePattern('mm:ss'),
+ [('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
+ self.assertEqual(parseDateTimePattern("H:mm' Uhr 'z"),
+ [('H', 1), ':', ('m', 2), ' Uhr ', ('z', 1)])
+ # German long
+ self.assertEqual(parseDateTimePattern("HH:mm:ss z"),
+ [('H', 2), ':', ('m', 2), ':', ('s', 2), ' ',
+ ('z', 1)])
+ # German medium
+ self.assertEqual(parseDateTimePattern("HH:mm:ss"),
+ [('H', 2), ':', ('m', 2), ':', ('s', 2)])
+ # German short
+ self.assertEqual(parseDateTimePattern("HH:mm"),
+ [('H', 2), ':', ('m', 2)])
+
+ def testParseRealDate(self):
+ # German full
+ self.assertEqual(parseDateTimePattern("EEEE, d. MMMM yyyy"),
+ [('E', 4), ', ', ('d', 1), '. ', ('M', 4),
+ ' ', ('y', 4)])
+ # German long
+ self.assertEqual(parseDateTimePattern("d. MMMM yyyy"),
+ [('d', 1), '. ', ('M', 4), ' ', ('y', 4)])
+ # German medium
+ self.assertEqual(parseDateTimePattern("dd.MM.yyyy"),
+ [('d', 2), '.', ('M', 2), '.', ('y', 4)])
+ # German short
+ self.assertEqual(parseDateTimePattern("dd.MM.yy"),
+ [('d', 2), '.', ('M', 2), '.', ('y', 2)])
+
+ def testParseRealDateTime(self):
+ # German full
+ self.assertEqual(
+ parseDateTimePattern("EEEE, d. MMMM yyyy H:mm' Uhr 'z"),
+ [('E', 4), ', ', ('d', 1), '. ', ('M', 4), ' ', ('y', 4),
+ ' ', ('H', 1), ':', ('m', 2), ' Uhr ', ('z', 1)])
+ # German long
+ self.assertEqual(
+ parseDateTimePattern("d. MMMM yyyy HH:mm:ss z"),
+ [('d', 1), '. ', ('M', 4), ' ', ('y', 4),
+ ' ', ('H', 2), ':', ('m', 2), ':', ('s', 2), ' ', ('z', 1)])
+ # German medium
+ self.assertEqual(
+ parseDateTimePattern("dd.MM.yyyy HH:mm:ss"),
+ [('d', 2), '.', ('M', 2), '.', ('y', 4),
+ ' ', ('H', 2), ':', ('m', 2), ':', ('s', 2)])
+ # German short
+ self.assertEqual(
+ parseDateTimePattern("dd.MM.yy HH:mm"),
+ [('d', 2), '.', ('M', 2), '.', ('y', 2),
+ ' ', ('H', 2), ':', ('m', 2)])
+
+ def testParseQuotesInPattern(self):
+ self.assertEqual(parseDateTimePattern("HH''mm"),
+ [('H', 2), "'", ('m', 2)])
+ self.assertEqual(parseDateTimePattern("HH'HHmm'mm"),
+ [('H', 2), 'HHmm', ('m', 2)])
+ self.assertEqual(parseDateTimePattern("HH':'''':'mm"),
+ [('H', 2), ":':", ('m', 2)])
+ self.assertEqual(parseDateTimePattern("HH':' ':'mm"),
+ [('H', 2), ": :", ('m', 2)])
+
+ def testParseDateTimePatternError(self):
+ # Quote not closed
+ try:
+ parseDateTimePattern("HH' Uhr")
+ except DateTimePatternParseError, err:
+ self.assertEqual(
+ str(err), 'The quote starting at character 2 is not closed.')
+ # Test correct length of characters in datetime fields
+ try:
+ parseDateTimePattern("HHHHH")
+ except DateTimePatternParseError, err:
+ self.assert_(str(err).endswith('You have: 5'))
+
+
+class TestBuildDateTimeParseInfo(TestCase):
+ """This class tests the functionality of the buildDateTimeParseInfo()
+ method with the German locale.
+ """
+
+ 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.)')
+
+ 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)+')')
+
+ def testMonthAbbr(self):
+ names = ['Jan', 'Feb', 'Mrz', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug',
+ 'Sep', 'Okt', 'Nov', 'Dez']
+ 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', 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)+')')
+
+
+class TestDateTimeFormat(TestCase):
+ """Test the functionality of an implmentation of the ILocaleProvider
+ interface."""
+
+ format = DateTimeFormat(calendar=LocaleCalendarStub())
+
+ def testInterfaceConformity(self):
+ self.assert_(IDateTimeFormat.providedBy(self.format))
+
+ def testParseSimpleDateTime(self):
+ # German short
+ self.assertEqual(
+ self.format.parse('02.01.03 21:48', 'dd.MM.yy HH:mm'),
+ datetime.datetime(2003, 01, 02, 21, 48))
+
+ def testParseRealDateTime(self):
+ # German medium
+ self.assertEqual(
+ self.format.parse('02.01.2003 21:48:01', 'dd.MM.yyyy HH:mm:ss'),
+ datetime.datetime(2003, 01, 02, 21, 48, 01))
+
+ # German long
+ # TODO: The parser does not support timezones yet.
+ 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,
+ 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,
+ 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):
+ # Note that EST is a deprecated timezone name since it is a US
+ # interpretation (other countries also use the EST timezone
+ # abbreviation)
+ 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=-5))
+ self.assertEqual(dt.tzinfo.zone, 'EST')
+ self.assertEqual(dt.tzinfo.tzname(dt), 'EST')
+
+ 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')
+ self.assertEqual(dt.tzinfo.tzname(dt), 'EST')
+
+ dt = self.format.parse('01.01.2003 09:48 Australia/Sydney',
+ 'dd.MM.yyyy HH:mm zzzz')
+ self.assertEqual(dt.tzinfo.utcoffset(dt), datetime.timedelta(hours=11))
+ self.assertEqual(dt.tzinfo.zone, 'Australia/Sydney')
+ self.assertEqual(dt.tzinfo.tzname(dt), 'EST')
+
+ # Note that historical and future (as far as known)
+ # timezones are handled happily using the pytz timezone database
+ # US DST transition points are changing in 2007
+ dt = self.format.parse('01.04.2006 09:48 US/Eastern',
+ 'dd.MM.yyyy HH:mm zzzz')
+ self.assertEqual(dt.tzinfo.zone, 'US/Eastern')
+ self.assertEqual(dt.tzinfo.tzname(dt), 'EST')
+ self.assertEqual(dt.tzinfo.utcoffset(dt), datetime.timedelta(hours=-5))
+ dt = self.format.parse('01.04.2007 09:48 US/Eastern',
+ 'dd.MM.yyyy HH:mm zzzz')
+ self.assertEqual(dt.tzinfo.zone, 'US/Eastern')
+ self.assertEqual(dt.tzinfo.tzname(dt), 'EDT')
+ self.assertEqual(dt.tzinfo.utcoffset(dt), datetime.timedelta(hours=-4))
+
+
+ def testDateTimeParseError(self):
+ self.assertRaises(DateTimeParseError,
+ self.format.parse, '02.01.03 21:48', 'dd.MM.yyyy HH:mm')
+
+ def testParse12PM(self):
+ self.assertEqual(
+ 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(
+ self.format.format(datetime.datetime(2003, 01, 02, 21, 48),
+ 'dd.MM.yy HH:mm'),
+ '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(dt, 'dd.MM.yyyy HH:mm:ss'),
+ '02.01.2003 21:48:01')
+
+ # German long
+ self.assertEqual(
+ self.format.format(dt, 'd. MMMM yyyy HH:mm:ss z'),
+ '2. Januar 2003 21:48:01 +100')
+
+ # German full
+ self.assertEqual(self.format.format(
+ 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(
+ datetime.datetime(2003, 01, 02, 21, 48),
+ 'dd.MM.yy hh:mm a'),
+ '02.01.03 09:48 nachm.')
+
+ def testFormatAllWeekdays(self):
+ for day in range(1, 8):
+ self.assertEqual(self.format.format(
+ datetime.datetime(2003, 01, day+5, 21, 48),
+ "EEEE, d. MMMM yyyy H:mm' Uhr 'z"),
+ '%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"),
+ '4')
+ self.assertEqual(self.format.format(date, "EE"),
+ '04')
+ self.assertEqual(self.format.format(date, "EEE"),
+ 'Do')
+ self.assertEqual(self.format.format(date, "EEEE"),
+ 'Donnerstag')
+
+ # Create custom calendar, which has Sunday as the first day of the
+ # week. I am assigning a totally new dict here, since dicts are
+ # mutable and the value would be changed for the class and all its
+ # instances.
+ calendar = LocaleCalendarStub()
+ calendar.week = {'firstDay': 7, 'minDays': 1}
+ format = DateTimeFormat(calendar=calendar)
+
+ self.assertEqual(format.format(date, "E"),
+ '5')
+ self.assertEqual(format.format(date, "EE"),
+ '05')
+
+ def testFormatDayOfWeekInMonth(self):
+ date = datetime.date(2003, 01, 02)
+ self.assertEqual(self.format.format(date, "F"),
+ '1')
+ self.assertEqual(self.format.format(date, "FF"),
+ '01')
+ self.assertEqual(
+ self.format.format(datetime.date(2003, 1, 9), "F"),
+ '2')
+ self.assertEqual(
+ self.format.format(datetime.date(2003, 1, 16), "F"),
+ '3')
+ self.assertEqual(
+ self.format.format(datetime.date(2003, 1, 23), "F"),
+ '4')
+
+ def testFormatWeekInMonth(self):
+ self.assertEqual(
+ self.format.format(datetime.date(2003, 1, 3), "W"),
+ '1')
+ self.assertEqual(
+ self.format.format(datetime.date(2003, 1, 3), "WW"),
+ '01')
+ self.assertEqual(
+ self.format.format(datetime.date(2003, 1, 8), "W"),
+ '2')
+ self.assertEqual(
+ self.format.format(datetime.date(2003, 1, 19), "W"),
+ '3')
+ self.assertEqual(
+ self.format.format(datetime.date(2003, 1, 20), "W"),
+ '4')
+ self.assertEqual(
+ self.format.format(datetime.date(2003, 1, 31), "W"),
+ '5')
+
+ def testFormatHourInDayOneTo24(self):
+ self.assertEqual(
+ self.format.format(datetime.time(5, 0), "k"),
+ '5')
+ self.assertEqual(
+ self.format.format(datetime.time(5, 0), "kk"),
+ '05')
+ self.assertEqual(
+ self.format.format(datetime.time(0, 0), "k"),
+ '24')
+ self.assertEqual(
+ self.format.format(datetime.time(1, 0), "k"),
+ '1')
+
+ def testFormatHourInDayZeroToEleven(self):
+ self.assertEqual(
+ self.format.format(datetime.time(5, 0), "K"),
+ '5')
+ self.assertEqual(
+ self.format.format(datetime.time(5, 0), "KK"),
+ '05')
+ self.assertEqual(
+ self.format.format(datetime.time(0, 0), "K"),
+ '0')
+ self.assertEqual(
+ self.format.format(datetime.time(12, 0), "K"),
+ '0')
+ self.assertEqual(
+ self.format.format(datetime.time(11, 0), "K"),
+ '11')
+ self.assertEqual(
+ self.format.format(datetime.time(23, 0), "K"),
+ '11')
+
+ def testFormatSimpleHourRepresentation(self):
+ self.assertEqual(
+ self.format.format(datetime.datetime(2003, 01, 02, 23, 00),
+ 'dd.MM.yy h:mm:ss a'),
+ '02.01.03 11:00:00 nachm.')
+ self.assertEqual(
+ self.format.format(datetime.datetime(2003, 01, 02, 02, 00),
+ 'dd.MM.yy h:mm:ss a'),
+ '02.01.03 2:00:00 vorm.')
+ self.assertEqual(
+ self.format.format(datetime.time(0, 15), 'h:mm a'),
+ '12:15 vorm.')
+ self.assertEqual(
+ self.format.format(datetime.time(1, 15), 'h:mm a'),
+ '1:15 vorm.')
+ self.assertEqual(
+ self.format.format(datetime.time(12, 15), 'h:mm a'),
+ '12:15 nachm.')
+ self.assertEqual(
+ 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."""
+
+ def testParseSimpleIntegerPattern(self):
+ self.assertEqual(
+ parseNumberPattern('###0'),
+ ((None, '', None, '###0', '', '', None, '', None, 0),
+ (None, '', None, '###0', '', '', None, '', None, 0)))
+
+ def testParseScientificIntegerPattern(self):
+ self.assertEqual(
+ parseNumberPattern('###0E#0'),
+ ((None, '', None, '###0', '', '#0', None, '', None, 0),
+ (None, '', None, '###0', '', '#0', None, '', None, 0)))
+ self.assertEqual(
+ parseNumberPattern('###0E+#0'),
+ ((None, '', None, '###0', '', '+#0', None, '', None, 0),
+ (None, '', None, '###0', '', '+#0', None, '', None, 0)))
+
+ def testParsePosNegAlternativeIntegerPattern(self):
+ self.assertEqual(
+ parseNumberPattern('###0;#0'),
+ ((None, '', None, '###0', '', '', None, '', None, 0),
+ (None, '', None, '#0', '', '', None, '', None, 0)))
+
+ def testParsePrefixedIntegerPattern(self):
+ self.assertEqual(
+ parseNumberPattern('+###0'),
+ ((None, '+', None, '###0', '', '', None, '', None, 0),
+ (None, '+', None, '###0', '', '', None, '', None, 0)))
+
+ def testParsePosNegIntegerPattern(self):
+ self.assertEqual(
+ parseNumberPattern('+###0;-###0'),
+ ((None, '+', None, '###0', '', '', None, '', None, 0),
+ (None, '-', None, '###0', '', '', None, '', None, 0)))
+
+ def testParseScientificPosNegIntegerPattern(self):
+ self.assertEqual(
+ parseNumberPattern('+###0E0;-###0E#0'),
+ ((None, '+', None, '###0', '', '0', None, '', None, 0),
+ (None, '-', None, '###0', '', '#0', None, '', None, 0)))
+
+ def testParseThousandSeparatorIntegerPattern(self):
+ self.assertEqual(
+ parseNumberPattern('#,##0'),
+ ((None, '', None, '###0', '', '', None, '', None, 1),
+ (None, '', None, '###0', '', '', None, '', None, 1)))
+
+ def testParseSimpleDecimalPattern(self):
+ self.assertEqual(
+ parseNumberPattern('###0.00#'),
+ ((None, '', None, '###0', '00#', '', None, '', None, 0),
+ (None, '', None, '###0', '00#', '', None, '', None, 0)))
+
+ def testParseScientificDecimalPattern(self):
+ self.assertEqual(
+ parseNumberPattern('###0.00#E#0'),
+ ((None, '', None, '###0', '00#', '#0', None, '', None, 0),
+ (None, '', None, '###0', '00#', '#0', None, '', None, 0)))
+
+ def testParsePosNegAlternativeFractionPattern(self):
+ self.assertEqual(
+ parseNumberPattern('###0.00#;#0.0#'),
+ ((None, '', None, '###0', '00#', '', None, '', None, 0),
+ (None, '', None, '#0', '0#', '', None, '', None, 0)))
+
+ def testParsePosNegFractionPattern(self):
+ self.assertEqual(
+ parseNumberPattern('+###0.0##;-###0.0##'),
+ ((None, '+', None, '###0', '0##', '', None, '', None, 0),
+ (None, '-', None, '###0', '0##', '', None, '', None, 0)))
+
+ def testParseScientificPosNegFractionPattern(self):
+ self.assertEqual(
+ parseNumberPattern('+###0.0##E#0;-###0.0##E0'),
+ ((None, '+', None, '###0', '0##', '#0', None, '', None, 0),
+ (None, '-', None, '###0', '0##', '0', None, '', None, 0)))
+
+ def testParseThousandSeparatorFractionPattern(self):
+ self.assertEqual(
+ parseNumberPattern('#,##0.0#'),
+ ((None, '', None, '###0', '0#', '', None, '', None, 1),
+ (None, '', None, '###0', '0#', '', None, '', None, 1)))
+
+ def testParsePadding1WithoutPrefixPattern(self):
+ self.assertEqual(
+ parseNumberPattern('* ###0'),
+ ((' ', '', None, '###0', '', '', None, '', None, 0),
+ (' ', '', None, '###0', '', '', None, '', None, 0)))
+ self.assertEqual(
+ parseNumberPattern('* ###0.0##'),
+ ((' ', '', None, '###0', '0##', '', None, '', None, 0),
+ (' ', '', None, '###0', '0##', '', None, '', None, 0)))
+ self.assertEqual(
+ parseNumberPattern('* ###0.0##;*_###0.0##'),
+ ((' ', '', None, '###0', '0##', '', None, '', None, 0),
+ ('_', '', None, '###0', '0##', '', None, '', None, 0)))
+
+ def testParsePadding1WithPrefixPattern(self):
+ self.assertEqual(
+ parseNumberPattern('* +###0'),
+ ((' ', '+', None, '###0', '', '', None, '', None, 0),
+ (' ', '+', None, '###0', '', '', None, '', None, 0)))
+ self.assertEqual(
+ parseNumberPattern('* +###0.0##'),
+ ((' ', '+', None, '###0', '0##', '', None, '', None, 0),
+ (' ', '+', None, '###0', '0##', '', None, '', None, 0)))
+ self.assertEqual(
+ parseNumberPattern('* +###0.0##;*_-###0.0##'),
+ ((' ', '+', None, '###0', '0##', '', None, '', None, 0),
+ ('_', '-', None, '###0', '0##', '', None, '', None, 0)))
+
+ def testParsePadding1Padding2WithPrefixPattern(self):
+ self.assertEqual(
+ parseNumberPattern('* +* ###0'),
+ ((' ', '+', ' ', '###0', '', '', None, '', None, 0),
+ (' ', '+', ' ', '###0', '', '', None, '', None, 0)))
+ self.assertEqual(
+ parseNumberPattern('* +* ###0.0##'),
+ ((' ', '+', ' ', '###0', '0##', '', None, '', None, 0),
+ (' ', '+', ' ', '###0', '0##', '', None, '', None, 0)))
+ self.assertEqual(
+ parseNumberPattern('* +* ###0.0##;*_-*_###0.0##'),
+ ((' ', '+', ' ', '###0', '0##', '', None, '', None, 0),
+ ('_', '-', '_', '###0', '0##', '', None, '', None, 0)))
+
+ def testParsePadding3WithoutSufffixPattern(self):
+ self.assertEqual(
+ parseNumberPattern('###0* '),
+ ((None, '', None, '###0', '', '', ' ', '', None, 0),
+ (None, '', None, '###0', '', '', ' ', '', None, 0)))
+ self.assertEqual(
+ parseNumberPattern('###0.0##* '),
+ ((None, '', None, '###0', '0##', '', ' ', '', None, 0),
+ (None, '', None, '###0', '0##', '', ' ', '', None, 0)))
+ self.assertEqual(
+ parseNumberPattern('###0.0##* ;###0.0##*_'),
+ ((None, '', None, '###0', '0##', '', ' ', '', None, 0),
+ (None, '', None, '###0', '0##', '', '_', '', None, 0)))
+
+ def testParsePadding3InScientificPattern(self):
+ self.assertEqual(
+ parseNumberPattern('###0E#0* '),
+ ((None, '', None, '###0', '', '#0', ' ', '', None, 0),
+ (None, '', None, '###0', '', '#0', ' ', '', None, 0)))
+ self.assertEqual(
+ parseNumberPattern('###0.0##E0* '),
+ ((None, '', None, '###0', '0##', '0', ' ', '', None, 0),
+ (None, '', None, '###0', '0##', '0', ' ', '', None, 0)))
+ self.assertEqual(
+ parseNumberPattern('###0.0##E#0* ;###0.0##E0*_'),
+ ((None, '', None, '###0', '0##', '#0', ' ', '', None, 0),
+ (None, '', None, '###0', '0##', '0', '_', '', None, 0)))
+
+ def testParsePadding3WithSufffixPattern(self):
+ self.assertEqual(
+ parseNumberPattern('###0* /'),
+ ((None, '', None, '###0', '', '', ' ', '/', None, 0),
+ (None, '', None, '###0', '', '', ' ', '/', None, 0)))
+ self.assertEqual(
+ parseNumberPattern('###0.0#* /'),
+ ((None, '', None, '###0', '0#', '', ' ', '/', None, 0),
+ (None, '', None, '###0', '0#', '', ' ', '/', None, 0)))
+ self.assertEqual(
+ parseNumberPattern('###0.0#* /;###0.0#*_/'),
+ ((None, '', None, '###0', '0#', '', ' ', '/', None, 0),
+ (None, '', None, '###0', '0#', '', '_', '/', None, 0)))
+
+ def testParsePadding3And4WithSuffixPattern(self):
+ self.assertEqual(
+ parseNumberPattern('###0* /* '),
+ ((None, '', None, '###0', '', '', ' ', '/', ' ', 0),
+ (None, '', None, '###0', '', '', ' ', '/', ' ', 0)))
+ self.assertEqual(
+ parseNumberPattern('###0* /* ;###0*_/*_'),
+ ((None, '', None, '###0', '', '', ' ', '/', ' ', 0),
+ (None, '', None, '###0', '', '', '_', '/', '_', 0)))
+
+ def testParseMultipleCharacterPrefix(self):
+ self.assertEqual(
+ parseNumberPattern('DM###0'),
+ ((None, 'DM', None, '###0', '', '', None, '', None, 0),
+ (None, 'DM', None, '###0', '', '', None, '', None, 0)))
+ self.assertEqual(
+ parseNumberPattern('DM* ###0'),
+ ((None, 'DM', ' ', '###0', '', '', None, '', None, 0),
+ (None, 'DM', ' ', '###0', '', '', None, '', None, 0)))
+
+ def testParseStringEscapedPrefix(self):
+ self.assertEqual(
+ parseNumberPattern("'DEM'###0"),
+ ((None, 'DEM', None, '###0', '', '', None, '', None, 0),
+ (None, 'DEM', None, '###0', '', '', None, '', None, 0)))
+ self.assertEqual(
+ parseNumberPattern("D'EM'###0"),
+ ((None, 'DEM', None, '###0', '', '', None, '', None, 0),
+ (None, 'DEM', None, '###0', '', '', None, '', None, 0)))
+ self.assertEqual(
+ parseNumberPattern("D'E'M###0"),
+ ((None, 'DEM', None, '###0', '', '', None, '', None, 0),
+ (None, 'DEM', None, '###0', '', '', None, '', None, 0)))
+
+ def testParseStringEscapedSuffix(self):
+ self.assertEqual(
+ parseNumberPattern("###0'DEM'"),
+ ((None, '', None, '###0', '', '', None, 'DEM', None, 0),
+ (None, '', None, '###0', '', '', None, 'DEM', None, 0)))
+ self.assertEqual(
+ parseNumberPattern("###0D'EM'"),
+ ((None, '', None, '###0', '', '', None, 'DEM', None, 0),
+ (None, '', None, '###0', '', '', None, 'DEM', None, 0)))
+ self.assertEqual(
+ parseNumberPattern("###0D'E'M"),
+ ((None, '', None, '###0', '', '', None, 'DEM', None, 0),
+ (None, '', None, '###0', '', '', None, 'DEM', None, 0)))
+
+
+class TestNumberFormat(TestCase):
+ """Test the functionality of an implmentation of the NumberFormat."""
+
+ format = NumberFormat(symbols={
+ 'decimal': '.', 'group': ',', 'list': ';', 'percentSign': '%',
+ 'nativeZeroDigit': '0', 'patternDigit': '#', 'plusSign': '+',
+ 'minusSign': '-', 'exponential': 'E', 'perMille': 'o/oo',
+ 'infinity': 'oo', 'nan': 'N/A'})
+
+ def testInterfaceConformity(self):
+ self.assert_(INumberFormat.providedBy(self.format))
+
+ def testParseSimpleInteger(self):
+ self.assertEqual(self.format.parse('23341', '###0'),
+ 23341)
+ self.assertEqual(self.format.parse('041', '#000'),
+ 41)
+
+ def testParseScientificInteger(self):
+ self.assertEqual(self.format.parse('2.3341E4', '0.0###E0'),
+ 23341)
+ self.assertEqual(self.format.parse('4.100E01', '0.000##E00'),
+ 41)
+ self.assertEqual(self.format.parse('1E0', '0E0'),
+ 1)
+ self.assertEqual(self.format.parse('0E0', '0E0'),
+ 0)
+ # This is a special case I found not working, but is used frequently
+ # in the new LDML Locale files.
+ self.assertEqual(self.format.parse('2.3341E+04', '0.000###E+00'),
+ 23341)
+
+ def testParsePosNegAlternativeInteger(self):
+ self.assertEqual(self.format.parse('23341', '#000;#00'),
+ 23341)
+ self.assertEqual(self.format.parse('041', '#000;#00'),
+ 41)
+ self.assertEqual(self.format.parse('41', '#000;#00'),
+ -41)
+ self.assertEqual(self.format.parse('01', '#000;#00'),
+ -1)
+
+ def testParsePrefixedInteger(self):
+ self.assertEqual(self.format.parse('+23341', '+###0'),
+ 23341)
+ self.assertEqual(self.format.parse('+041', '+#000'),
+ 41)
+
+ def testParsePosNegInteger(self):
+ self.assertEqual(self.format.parse('+23341', '+###0;-###0'),
+ 23341)
+ self.assertEqual(self.format.parse('+041', '+#000;-#000'),
+ 41)
+ self.assertEqual(self.format.parse('-23341', '+###0;-###0'),
+ -23341)
+ self.assertEqual(self.format.parse('-041', '+#000;-#000'),
+ -41)
+
+ def testParseThousandSeparatorInteger(self):
+ self.assertEqual(self.format.parse('+23,341', '+#,##0;-#,##0'),
+ 23341)
+ self.assertEqual(self.format.parse('-23,341', '+#,##0;-#,##0'),
+ -23341)
+ self.assertEqual(self.format.parse('+0,041', '+#0,000;-#0,000'),
+ 41)
+ self.assertEqual(self.format.parse('-0,041', '+#0,000;-#0,000'),
+ -41)
+
+ def testParseDecimal(self):
+ self.assertEqual(self.format.parse('23341.02', '###0.0#'),
+ 23341.02)
+ self.assertEqual(self.format.parse('23341.1', '###0.0#'),
+ 23341.1)
+ self.assertEqual(self.format.parse('23341.020', '###0.000#'),
+ 23341.02)
+
+ def testParseScientificDecimal(self):
+ self.assertEqual(self.format.parse('2.334102E04', '0.00####E00'),
+ 23341.02)
+ self.assertEqual(self.format.parse('2.3341020E004', '0.0000000E000'),
+ 23341.02)
+ self.assertEqual(self.format.parse('0.0E0', '0.0#E0'),
+ 0.0)
+
+ def testParseScientificDecimalSmallerOne(self):
+ self.assertEqual(self.format.parse('2.357E-02', '0.00####E00'),
+ 0.02357)
+ self.assertEqual(self.format.parse('2.0000E-02', '0.0000E00'),
+ 0.02)
+
+ def testParsePadding1WithoutPrefix(self):
+ self.assertEqual(self.format.parse(' 41', '* ##0;*_##0'),
+ 41)
+ self.assertEqual(self.format.parse('_41', '* ##0;*_##0'),
+ -41)
+
+ def testParsePadding1WithPrefix(self):
+ self.assertEqual(self.format.parse(' +41', '* +##0;*_-##0'),
+ 41)
+ self.assertEqual(self.format.parse('_-41', '* +##0;*_-##0'),
+ -41)
+
+ def testParsePadding1Padding2WithPrefix(self):
+ self.assertEqual(self.format.parse(' + 41', '* +* ###0;*_-*_###0'),
+ +41)
+ self.assertEqual(self.format.parse('__-_41', '* +* ###0;*_-*_###0'),
+ -41)
+
+ def testParsePadding1Scientific(self):
+ self.assertEqual(self.format.parse(' 4.102E1',
+ '* 0.0####E0;*_0.0####E0'),
+ 41.02)
+ self.assertEqual(self.format.parse('__4.102E1',
+ '* 0.0####E0;*_0.0####E0'),
+ -41.02)
+ self.assertEqual(self.format.parse(' +4.102E1',
+ '* +0.0###E0;*_-0.0###E0'),
+ 41.02)
+ self.assertEqual(self.format.parse('_-4.102E1',
+ '* +0.0###E0;*_-0.0###E0'),
+ -41.02)
+
+ def testParsePadding3WithoutSufffix(self):
+ self.assertEqual(self.format.parse('41.02 ', '#0.0###* ;#0.0###*_'),
+ 41.02)
+ self.assertEqual(self.format.parse('41.02__', '#0.0###* ;#0.0###*_'),
+ -41.02)
+
+ def testParsePadding3WithSufffix(self):
+ self.assertEqual(
+ self.format.parse('[41.02 ]', '[#0.0###* ];(#0.0###*_)'),
+ 41.02)
+ self.assertEqual(
+ self.format.parse('(41.02__)', '[#0.0###* ];(#0.0###*_)'),
+ -41.02)
+
+ def testParsePadding3Scientific(self):
+ self.assertEqual(self.format.parse('4.102E1 ',
+ '0.0##E0##* ;0.0##E0##*_'),
+ 41.02)
+ self.assertEqual(self.format.parse('4.102E1__',
+ '0.0##E0##* ;0.0##E0##*_'),
+ -41.02)
+ self.assertEqual(self.format.parse('(4.102E1 )',
+ '(0.0##E0##* );0.0E0'),
+ 41.02)
+ self.assertEqual(self.format.parse('[4.102E1__]',
+ '0.0E0;[0.0##E0##*_]'),
+ -41.02)
+
+ def testParsePadding3Padding4WithSuffix(self):
+ self.assertEqual(self.format.parse('(41.02 ) ', '(#0.0###* )* '),
+ 41.02)
+ self.assertEqual(self.format.parse('(4.102E1 ) ', '(0.0##E0##* )* '),
+ 41.02)
+
+ def testParseDecimalWithGermanDecimalSeparator(self):
+ format = NumberFormat(symbols={'decimal': ',', 'group': '.'})
+ self.assertEqual(format.parse('1.234,567', '#,##0.000'), 1234.567)
+
+ def testParseWithAlternativeExponentialSymbol(self):
+ format = NumberFormat(
+ symbols={'decimal': '.', 'group': ',', 'exponential': 'X'})
+ self.assertEqual(format.parse('1.2X11', '#.#E0'), 1.2e11)
+
+ def testFormatSimpleInteger(self):
+ self.assertEqual(self.format.format(23341, '###0'),
+ '23341')
+ self.assertEqual(self.format.format(41, '#000'),
+ '041')
+
+ def testFormatScientificInteger(self):
+ self.assertEqual(self.format.format(23341, '0.000#E0'),
+ '2.3341E4')
+ self.assertEqual(self.format.format(23341, '0.000#E00'),
+ '2.3341E04')
+ self.assertEqual(self.format.format(1, '0.##E0'),
+ '1E0')
+ self.assertEqual(self.format.format(1, '0.00E00'),
+ '1.00E00')
+ # This is a special case I found not working, but is used frequently
+ # in the new LDML Locale files.
+ self.assertEqual(self.format.format(23341, '0.000###E+00'),
+ '2.3341E+04')
+
+ def testFormatScientificZero(self):
+ self.assertEqual(self.format.format(0, '0.00E00'),
+ '0.00E00')
+ self.assertEqual(self.format.format(0, '0E0'),
+ '0E0')
+
+ def testFormatPosNegAlternativeInteger(self):
+ self.assertEqual(self.format.format(23341, '#000;#00'),
+ '23341')
+ self.assertEqual(self.format.format(41, '#000;#00'),
+ '041')
+ self.assertEqual(self.format.format(-23341, '#000;#00'),
+ '23341')
+ self.assertEqual(self.format.format(-41, '#000;#00'),
+ '41')
+ self.assertEqual(self.format.format(-1, '#000;#00'),
+ '01')
+
+ def testFormatPrefixedInteger(self):
+ self.assertEqual(self.format.format(23341, '+###0'),
+ '+23341')
+ self.assertEqual(self.format.format(41, '+#000'),
+ '+041')
+ self.assertEqual(self.format.format(-23341, '+###0'),
+ '+23341')
+ self.assertEqual(self.format.format(-41, '+#000'),
+ '+041')
+
+ def testFormatPosNegInteger(self):
+ self.assertEqual(self.format.format(23341, '+###0;-###0'),
+ '+23341')
+ self.assertEqual(self.format.format(41, '+#000;-#000'),
+ '+041')
+ self.assertEqual(self.format.format(-23341, '+###0;-###0'),
+ '-23341')
+ self.assertEqual(self.format.format(-41, '+#000;-#000'),
+ '-041')
+
+ def testFormatPosNegScientificInteger(self):
+ self.assertEqual(self.format.format(23341, '+0.00###E00;-0.00###E00'),
+ '+2.3341E04')
+ self.assertEqual(self.format.format(23341, '-0.00###E00;-0.00###E00'),
+ '-2.3341E04')
+
+ def testFormatThousandSeparatorInteger(self):
+ self.assertEqual(self.format.format(23341, '+#,##0;-#,##0'),
+ '+23,341')
+ self.assertEqual(self.format.format(-23341, '+#,##0;-#,##0'),
+ '-23,341')
+ self.assertEqual(self.format.format(41, '+#0,000;-#0,000'),
+ '+0,041')
+ self.assertEqual(self.format.format(-41, '+#0,000;-#0,000'),
+ '-0,041')
+
+ def testFormatDecimal(self):
+ self.assertEqual(self.format.format(23341.02357, '###0.0#'),
+ '23341.02')
+ self.assertEqual(self.format.format(23341.02357, '###0.000#'),
+ '23341.0236')
+ self.assertEqual(self.format.format(23341.02, '###0.000#'),
+ '23341.020')
+
+ def testRounding(self):
+ self.assertEqual(self.format.format(0.5, '#'), '1')
+ self.assertEqual(self.format.format(0.49, '#'), '0')
+ self.assertEqual(self.format.format(0.45, '0.0'), '0.5')
+ self.assertEqual(self.format.format(150, '0E0'), '2E2')
+ self.assertEqual(self.format.format(149, '0E0'), '1E2')
+ self.assertEqual(self.format.format(1.9999, '0.000'), '2.000')
+ self.assertEqual(self.format.format(1.9999, '0.0000'), '1.9999')
+
+
+ def testFormatScientificDecimal(self):
+ self.assertEqual(self.format.format(23341.02357, '0.00####E00'),
+ '2.334102E04')
+ self.assertEqual(self.format.format(23341.02, '0.0000000E000'),
+ '2.3341020E004')
+
+ def testFormatScientificDecimalSmallerOne(self):
+ self.assertEqual(self.format.format(0.02357, '0.00####E00'),
+ '2.357E-02')
+ self.assertEqual(self.format.format(0.02, '0.0000E00'),
+ '2.0000E-02')
+
+ def testFormatPadding1WithoutPrefix(self):
+ self.assertEqual(self.format.format(41, '* ##0;*_##0'),
+ ' 41')
+ self.assertEqual(self.format.format(-41, '* ##0;*_##0'),
+ '_41')
+
+ def testFormatPadding1WithPrefix(self):
+ self.assertEqual(self.format.format(41, '* +##0;*_-##0'),
+ ' +41')
+ self.assertEqual(self.format.format(-41, '* +##0;*_-##0'),
+ '_-41')
+
+ def testFormatPadding1Scientific(self):
+ self.assertEqual(self.format.format(41.02, '* 0.0####E0;*_0.0####E0'),
+ ' 4.102E1')
+ self.assertEqual(self.format.format(-41.02, '* 0.0####E0;*_0.0####E0'),
+ '__4.102E1')
+ self.assertEqual(self.format.format(41.02, '* +0.0###E0;*_-0.0###E0'),
+ ' +4.102E1')
+ self.assertEqual(self.format.format(-41.02, '* +0.0###E0;*_-0.0###E0'),
+ '_-4.102E1')
+
+ def testFormatPadding1Padding2WithPrefix(self):
+ self.assertEqual(self.format.format(41, '* +* ###0;*_-*_###0'),
+ ' + 41')
+ self.assertEqual(self.format.format(-41, '* +* ###0;*_-*_###0'),
+ '__-_41')
+
+ def testFormatPadding3WithoutSufffix(self):
+ self.assertEqual(self.format.format(41.02, '#0.0###* ;#0.0###*_'),
+ '41.02 ')
+ self.assertEqual(self.format.format(-41.02, '#0.0###* ;#0.0###*_'),
+ '41.02__')
+
+ def testFormatPadding3WithSufffix(self):
+ self.assertEqual(self.format.format(41.02, '[#0.0###* ];(#0.0###*_)'),
+ '[41.02 ]')
+ self.assertEqual(self.format.format(-41.02, '[#0.0###* ];(#0.0###*_)'),
+ '(41.02__)')
+
+ def testFormatPadding3Scientific(self):
+ self.assertEqual(self.format.format(41.02, '0.0##E0##* ;0.0##E0##*_'),
+ '4.102E1 ')
+ self.assertEqual(self.format.format(-41.02, '0.0##E0##* ;0.0##E0##*_'),
+ '4.102E1__')
+ self.assertEqual(self.format.format(41.02, '(0.0##E0##* );0.0E0'),
+ '(4.102E1 )')
+ self.assertEqual(self.format.format(-41.02, '0.0E0;[0.0##E0##*_]'),
+ '[4.102E1__]')
+
+ def testFormatPadding3Padding4WithSuffix(self):
+ self.assertEqual(self.format.format(41.02, '(#0.0###* )* '),
+ '(41.02 ) ')
+ self.assertEqual(self.format.format(41.02, '(0.0##E0##* )* '),
+ '(4.102E1 ) ')
+
+
+def test_suite():
+ return TestSuite((
+ makeSuite(TestDateTimePatternParser),
+ makeSuite(TestBuildDateTimeParseInfo),
+ makeSuite(TestDateTimeFormat),
+ makeSuite(TestNumberPatternParser),
+ makeSuite(TestNumberFormat),
+ ))