From: Jonas Borgström Date: Wed, 30 May 2007 21:30:14 +0000 (+0000) Subject: Add basic support for number format patterns. X-Git-Tag: 1.0~619 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=eba003477b33b23ff7568d7777a49ee1c63b3f59;p=thirdparty%2Fbabel.git Add basic support for number format patterns. --- diff --git a/babel/core.py b/babel/core.py index 5b9913ad..c07b9457 100644 --- a/babel/core.py +++ b/babel/core.py @@ -351,6 +351,17 @@ class Locale(object): :type: `dict` """) + def decimal_formats(self): + return self._data['decimal_formats'] + decimal_formats = property(decimal_formats, doc="""\ + Locale patterns for decimal number formatting. + + > Locale('en', 'US').decimal_formats[None] + + + :type: `dict` + """) + def negotiate(preferred, available): """Find the best match between available and requested locale strings. diff --git a/babel/numbers.py b/babel/numbers.py index ec9f83e6..5e3f1774 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -56,7 +56,7 @@ def get_group_symbol(locale=LC_NUMERIC): :return: the group symbol :rtype: `unicode` """ - return Locale.parse(locale).number_symbols.get('group', u'.') + return Locale.parse(locale).number_symbols.get('group', u',') def format_number(number, locale=LC_NUMERIC): """Returns the given number formatted for a specific locale. @@ -69,45 +69,42 @@ def format_number(number, locale=LC_NUMERIC): :return: the formatted number :rtype: `unicode` """ - group = get_group_symbol(locale) - if not group: - return unicode(number) - thou = re.compile(r'([0-9])([0-9][0-9][0-9]([%s]|$))' % group).search - v = str(number) - mo = thou(v) - while mo is not None: - l = mo.start(0) - v = v[:l+1] + group + v[l+1:] - mo = thou(v) - return unicode(v) - -def format_decimal(number, places=2, locale=LC_NUMERIC): + # Do we really need this one? + return format_decimal(number, locale=locale) + +def format_decimal(number, format=None, locale=LC_NUMERIC): """Returns the given decimal number formatted for a specific locale. - >>> format_decimal(1099.98, locale='en_US') - u'1,099.98' - + >>> format_decimal(1, locale='en_US') + u'1' + >>> format_decimal(1.2345, locale='en_US') + u'1.234' + >>> format_decimal(1.2345, locale='sv_SE') + u'1,234' + >>> format_decimal(12345, locale='de_DE') + u'12.345' + >>> format_decimal(-1.2345, format='#,##0.##;-#', locale='sv_SE') + u'-1,23' + >>> format_decimal(-1.2345, format='#,##0.##;(#)', locale='sv_SE') + u'(1,23)' + The appropriate thousands grouping and the decimal separator are used for each locale: - >>> format_decimal(1099.98, locale='de_DE') - u'1.099,98' - - The number of decimal places defaults to 2, but can also be specified - explicitly: - - >>> format_decimal(1099.98, places=4, locale='en_US') - u'1,099.9800' - + >>> format_decimal(12345, locale='en_US') + u'12,345' + :param number: the number to format - :param places: the number of digit behind the decimal point + :param format: :param locale: the `Locale` object or locale identifier :return: the formatted decimal number :rtype: `unicode` """ locale = Locale.parse(locale) - a, b = (('%%.%df' % places) % number).split('.') - return unicode(format_number(a, locale) + get_decimal_symbol(locale) + b) + pattern = locale.decimal_formats.get(format) + if not pattern: + pattern = parse_pattern(format) + return pattern.apply(number, locale) def format_currency(value, locale=LC_NUMERIC): """Returns formatted currency value. @@ -120,7 +117,7 @@ def format_currency(value, locale=LC_NUMERIC): :return: the formatted currency value :rtype: `unicode` """ - return format_decimal(value, places=2, locale=locale) + return format_decimal(value, locale=locale) def format_percent(value, places=2, locale=LC_NUMERIC): raise NotImplementedError @@ -162,3 +159,126 @@ def parse_decimal(string, locale=LC_NUMERIC): string = string.replace(get_group_symbol(locale), '') \ .replace(get_decimal_symbol(locale), '.') return float(string) + + +PREFIX_END = r'[^0-9@#.,]' +NUMBER_TOKEN = r'[0-9@#.\-,E]' + +PREFIX_PATTERN = r"(?P(?:'[^']*'|%s)*)" % PREFIX_END +NUMBER_PATTERN = r"(?P%s+)" % NUMBER_TOKEN +SUFFIX_PATTERN = r"(?P.*)" + +number_re = re.compile(r"%s%s%s" % (PREFIX_PATTERN, NUMBER_PATTERN, + SUFFIX_PATTERN)) + +# TODO: +# Filling +# Rounding +# Scientific notation +# Significant Digits +def parse_pattern(pattern): + """Parse number format patterns""" + if isinstance(pattern, NumberPattern): + return pattern + + # Do we have a negative subpattern? + if ';' in pattern: + pattern, neg_pattern = pattern.split(';', 1) + pos_prefix, number, pos_suffix = number_re.search(pattern).groups() + neg_prefix, _, neg_suffix = number_re.search(neg_pattern).groups() + else: + pos_prefix, number, pos_suffix = number_re.search(pattern).groups() + neg_prefix = '-' + pos_prefix + neg_suffix = pos_suffix + integer, fraction = number.rsplit('.', 1) + min_frac = max_frac = 0 + + def parse_precision(p): + """Calculate the min and max allowed digits""" + min = max = 0 + for c in p: + if c == '0': + min += 1 + max += 1 + elif c == '#': + max += 1 + else: + break + return min, max + + def parse_grouping(p): + """Parse primary and secondary digit grouping + + >>> parse_grouping('##') + 0, 0 + >>> parse_grouping('#,###') + 3, 3 + >>> parse_grouping('#,####,###') + 3, 4 + """ + width = len(p) + g1 = p.rfind(',') + if g1 == -1: + return 1000, 1000 + g1 = width - g1 - 1 + g2 = p[:-g1 - 1].rfind(',') + if g2 == -1: + return g1, g1 + g2 = width - g1 - g2 - 2 + return g1, g2 + + int_precision = parse_precision(integer) + frac_precision = parse_precision(fraction) + grouping = parse_grouping(integer) + int_precision = (int_precision[0], 1000) # Unlimited + return NumberPattern(pattern, (pos_prefix, neg_prefix), + (pos_suffix, neg_suffix), grouping, + int_precision, frac_precision) + + +class NumberPattern(object): + def __init__(self, pattern, prefix, suffix, grouping, + int_precision, frac_precision): + self.pattern = pattern + self.prefix = prefix + self.suffix = suffix + self.grouping = grouping + self.int_precision = int_precision + self.frac_precision = frac_precision + + def __repr__(self): + return '<%s %r>' % (type(self).__name__, self.pattern) + + def apply(self, value, locale): + negative = int(value < 0) + a, b = str(value * 1.0).split('.') + a = a.lstrip('-') + return '%s%s%s%s' % (self.prefix[negative], + self._format_int(a, locale), + self._format_frac(b, locale), + self.suffix[negative]) + + def _format_int(self, value, locale): + min, max = self.int_precision + width = len(value) + if width < min: + value += '0' * (min - width) + gsize = self.grouping[0] + ret = '' + symbol = get_group_symbol(locale) + while len(value) > gsize: + ret = symbol + value[-gsize:] + ret + value = value[:-gsize] + gsize = self.grouping[1] + return value + ret + + def _format_frac(self, value, locale): + min, max = self.frac_precision + if min == 0 and int(value) == 0: + return '' + width = len(value) + if width < min: + value += '0' * (min - width) + if width > max: + value = value[:max] # FIXME: Rounding?!? + return get_decimal_symbol(locale) + value diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index 5bb852df..8ac0e8ab 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -22,7 +22,7 @@ try: except ImportError: from elementtree.ElementTree import parse -from babel.dates import parse_pattern +from babel import dates, numbers weekdays = {'mon': 1, 'tue': 2, 'wed': 3, 'thu': 4, 'fri': 5, 'sat': 6, 'sun': 7} @@ -225,7 +225,7 @@ def main(): continue try: date_formats[elem.attrib.get('type')] = \ - parse_pattern(unicode(elem.findtext('dateFormat/pattern'))) + dates.parse_pattern(unicode(elem.findtext('dateFormat/pattern'))) except ValueError, e: print e @@ -235,7 +235,7 @@ def main(): continue try: time_formats[elem.attrib.get('type')] = \ - parse_pattern(unicode(elem.findtext('timeFormat/pattern'))) + dates.parse_pattern(unicode(elem.findtext('timeFormat/pattern'))) except ValueError, e: print e @@ -249,7 +249,7 @@ def main(): for elem in tree.findall('//decimalFormats/decimalFormatLength'): if 'draft' in elem.attrib and elem.attrib.get('type') in decimal_formats: continue - decimal_formats[elem.attrib.get('type')] = unicode(elem.findtext('decimalFormat/pattern')) + decimal_formats[elem.attrib.get('type')] = numbers.parse_pattern(unicode(elem.findtext('decimalFormat/pattern'))) scientific_formats = data.setdefault('scientific_formats', {}) for elem in tree.findall('//scientificFormats/scientificFormatLength'):