diff options
author | Gary Poster <gary@zope.com> | 2005-09-07 18:27:36 +0000 |
---|---|---|
committer | Gary Poster <gary@zope.com> | 2005-09-07 18:27:36 +0000 |
commit | 6e554090402cfe2a75ce26c7f281acfb77c5a9d5 (patch) | |
tree | 08b1a07772e9ad0196be4ca1c0058f637c370e58 | |
download | zope-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.py | 900 | ||||
-rw-r--r-- | tests/test_formats.py | 1146 |
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), + )) |