# HG changeset patch # User jonas # Date 1186831865 0 # Node ID 5e4b1fd6b3481fc2730ae361dfe7d6657c24c4d7 # Parent 059b74719f6b8b2c7c4e241084076b8be6def516 Added initial support for scientific notation patterns. diff --git a/babel/numbers.py b/babel/numbers.py --- a/babel/numbers.py +++ b/babel/numbers.py @@ -20,8 +20,9 @@ * ``LC_ALL``, and * ``LANG`` """ -# TODO: percent and scientific formatting +# TODO: scientific formatting +import math import re try: from decimal import Decimal @@ -63,6 +64,18 @@ """ return Locale.parse(locale).number_symbols.get('decimal', u'.') +def get_exponential_symbol(locale=LC_NUMERIC): + """Return the symbol used by the locale to separate mantissa and exponent. + + >>> get_exponential_symbol('en_US') + u'E' + + :param locale: the `Locale` object or locale identifier + :return: the exponential symbol + :rtype: `unicode` + """ + return Locale.parse(locale).number_symbols.get('exponential', u'E') + def get_group_symbol(locale=LC_NUMERIC): """Return the symbol used by the locale to separate groups of thousands. @@ -175,9 +188,28 @@ pattern = parse_pattern(format) return pattern.apply(number, locale) -def format_scientific(number, locale=LC_NUMERIC): - # TODO: implement - raise NotImplementedError +def format_scientific(number, format=None, locale=LC_NUMERIC): + """Return value formatted in scientific notation for a specific locale. + + >>> format_scientific(10000, locale='en_US') + u'1E4' + + The format pattern can also be specified explicitly: + + >>> format_scientific(1234567, u'##0E00', locale='en_US') + u'1.23E06' + + :param number: the number to format + :param format: + :param locale: the `Locale` object or locale identifier + :return: value formatted in scientific notation. + :rtype: `unicode` + """ + locale = Locale.parse(locale) + if not format: + format = locale.scientific_formats.get(format) + pattern = parse_pattern(format) + return pattern.apply(number, locale) class NumberFormatError(ValueError): @@ -241,7 +273,7 @@ PREFIX_END = r'[^0-9@#.,]' -NUMBER_TOKEN = r'[0-9@#.\-,E]' +NUMBER_TOKEN = r'[0-9@#.\-,E+]' PREFIX_PATTERN = r"(?P(?:'[^']*'|%s)*)" % PREFIX_END NUMBER_PATTERN = r"(?P%s+)" % NUMBER_TOKEN @@ -315,6 +347,10 @@ pos_prefix, number, pos_suffix = number_re.search(pattern).groups() neg_prefix = '-' + pos_prefix neg_suffix = pos_suffix + if 'E' in number: + number, exp = number.split('E', 1) + else: + exp = None if '@' in number: if '.' in number and '0' in number: raise ValueError('Significant digit patterns can not contain ' @@ -364,22 +400,33 @@ int_precision = parse_precision(integer) frac_precision = parse_precision(fraction) + if exp: + frac_precision = parse_precision(integer+fraction) + exp_plus = exp.startswith('+') + exp = exp.lstrip('+') + exp_precision = parse_precision(exp) + else: + exp_plus = None + exp_precision = None grouping = parse_grouping(integer) return NumberPattern(pattern, (pos_prefix, neg_prefix), (pos_suffix, neg_suffix), grouping, - int_precision, frac_precision) + int_precision, frac_precision, + exp_precision, exp_plus) class NumberPattern(object): def __init__(self, pattern, prefix, suffix, grouping, - int_precision, frac_precision): + int_precision, frac_precision, exp_precision, exp_plus): self.pattern = pattern self.prefix = prefix self.suffix = suffix self.grouping = grouping self.int_precision = int_precision self.frac_precision = frac_precision + self.exp_precision = exp_precision + self.exp_plus = exp_plus if '%' in ''.join(self.prefix + self.suffix): self.scale = 100 elif u'‰' in ''.join(self.prefix + self.suffix): @@ -393,7 +440,32 @@ def apply(self, value, locale, currency=None): value *= self.scale negative = int(value < 0) - if '@' in self.pattern: # Is it a siginificant digits pattern? + if self.exp_precision: + value = abs(value) + exp = int(math.floor(math.log(value, 10))) + # Minimum number of integer digits + if self.int_precision[0] == self.int_precision[1]: + exp -= self.int_precision[0] - 1 + # Exponent grouping + elif self.int_precision[1]: + exp = int(exp) / self.int_precision[1] * self.int_precision[1] + value = value / 10.0**exp + exp_negative = exp < 0 + exp = abs(exp) + exp_sign = ['', '-'] + if self.exp_plus: + exp_sign[0] = '+' + return u'%s%s%s%s%s%s' % \ + (self.prefix[negative], + self._format_sigdig(value, self.frac_precision[0], + self.frac_precision[1]), + get_exponential_symbol(locale), + exp_sign[exp_negative], + self._format_int(str(exp), self.exp_precision[0], + self.exp_precision[1], locale), + self.suffix[negative]) + + elif '@' in self.pattern: # Is it a siginificant digits pattern? text = self._format_sigdig(abs(value), self.int_precision[0], self.int_precision[1]) @@ -447,7 +519,7 @@ def _format_int(self, value, min, max, locale): width = len(value) if width < min: - value += '0' * (min - width) + value = '0' * (min - width) + value gsize = self.grouping[0] ret = '' symbol = get_group_symbol(locale) diff --git a/babel/tests/numbers.py b/babel/tests/numbers.py --- a/babel/tests/numbers.py +++ b/babel/tests/numbers.py @@ -86,7 +86,7 @@ '0.1') self.assertEqual(numbers.format_decimal(0.1, '@#',locale='en_US'), '0.1') - self.assertEqual(numbers.format_decimal(0.1, '@@',locale='en_US'), + self.assertEqual(numbers.format_decimal(0.1, '@@', locale='en_US'), '0.10') if have_decimal: @@ -105,6 +105,39 @@ '#.00', locale='en_US'), '12345678901234567890.12') + def test_scientific_notation(self): + fmt = numbers.format_scientific(0.1, '#E0', locale='en_US') + self.assertEqual(fmt, '1E-1') + fmt = numbers.format_scientific(0.01, '#E0', locale='en_US') + self.assertEqual(fmt, '1E-2') + fmt = numbers.format_scientific(10, '#E0', locale='en_US') + self.assertEqual(fmt, '1E1') + fmt = numbers.format_scientific(1234, '0.###E0', locale='en_US') + self.assertEqual(fmt, '1.234E3') + fmt = numbers.format_scientific(1234, '0.#E0', locale='en_US') + self.assertEqual(fmt, '1.2E3') + # Exponent grouping + fmt = numbers.format_scientific(12345, '##0.####E0', locale='en_US') + self.assertEqual(fmt, '12.345E3') + # Minimum number of int digits + fmt = numbers.format_scientific(12345, '00.###E0', locale='en_US') + self.assertEqual(fmt, '12.345E3') + fmt = numbers.format_scientific(-12345.6, '00.###E0', locale='en_US') + self.assertEqual(fmt, '-12.346E3') + fmt = numbers.format_scientific(-0.01234, '00.###E0', locale='en_US') + self.assertEqual(fmt, '-12.34E-3') + # Custom pattern suffic + fmt = numbers.format_scientific(123.45, '#.##E0 m/s', locale='en_US') + self.assertEqual(fmt, '1.23E2 m/s') + # Exponent patterns + fmt = numbers.format_scientific(123.45, '#.##E00 m/s', locale='en_US') + self.assertEqual(fmt, '1.23E02 m/s') + fmt = numbers.format_scientific(0.012345, '#.##E00 m/s', locale='en_US') + self.assertEqual(fmt, '1.23E-02 m/s') + fmt = numbers.format_scientific(12345, '#.##E+00 m/s', locale='en_US') + self.assertEqual(fmt, '1.23E+04 m/s') + + def suite(): suite = unittest.TestSuite() suite.addTest(doctest.DocTestSuite(numbers))