| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584 |
- # -*- coding: utf-8 -*-
- #
- # Copyright (C) 2007 Edgewall Software
- # All rights reserved.
- #
- # This software is licensed as described in the file COPYING, which
- # you should have received as part of this distribution. The terms
- # are also available at http://babel.edgewall.org/wiki/License.
- #
- # This software consists of voluntary contributions made by many
- # individuals. For the exact contribution history, see the revision
- # history and logs, available at http://babel.edgewall.org/log/.
- """Locale dependent formatting and parsing of numeric data.
- The default locale for the functions in this module is determined by the
- following environment variables, in that order:
- * ``LC_NUMERIC``,
- * ``LC_ALL``, and
- * ``LANG``
- """
- # TODO:
- # Padding and rounding increments in pattern:
- # - http://www.unicode.org/reports/tr35/ (Appendix G.6)
- import math
- import re
- try:
- from decimal import Decimal
- have_decimal = True
- except ImportError:
- have_decimal = False
- from babel.core import default_locale, Locale
- from babel.util import rsplit
- __all__ = ['format_number', 'format_decimal', 'format_currency',
- 'format_percent', 'format_scientific', 'parse_number',
- 'parse_decimal', 'NumberFormatError']
- __docformat__ = 'restructuredtext en'
- LC_NUMERIC = default_locale('LC_NUMERIC')
- def get_currency_name(currency, locale=LC_NUMERIC):
- """Return the name used by the locale for the specified currency.
-
- >>> get_currency_name('USD', 'en_US')
- u'US Dollar'
-
- :param currency: the currency code
- :param locale: the `Locale` object or locale identifier
- :return: the currency symbol
- :rtype: `unicode`
- :since: version 0.9.4
- """
- return Locale.parse(locale).currencies.get(currency, currency)
- def get_currency_symbol(currency, locale=LC_NUMERIC):
- """Return the symbol used by the locale for the specified currency.
-
- >>> get_currency_symbol('USD', 'en_US')
- u'$'
-
- :param currency: the currency code
- :param locale: the `Locale` object or locale identifier
- :return: the currency symbol
- :rtype: `unicode`
- """
- return Locale.parse(locale).currency_symbols.get(currency, currency)
- def get_decimal_symbol(locale=LC_NUMERIC):
- """Return the symbol used by the locale to separate decimal fractions.
-
- >>> get_decimal_symbol('en_US')
- u'.'
-
- :param locale: the `Locale` object or locale identifier
- :return: the decimal symbol
- :rtype: `unicode`
- """
- return Locale.parse(locale).number_symbols.get('decimal', u'.')
- def get_plus_sign_symbol(locale=LC_NUMERIC):
- """Return the plus sign symbol used by the current locale.
-
- >>> get_plus_sign_symbol('en_US')
- u'+'
-
- :param locale: the `Locale` object or locale identifier
- :return: the plus sign symbol
- :rtype: `unicode`
- """
- return Locale.parse(locale).number_symbols.get('plusSign', u'+')
- def get_minus_sign_symbol(locale=LC_NUMERIC):
- """Return the plus sign symbol used by the current locale.
-
- >>> get_minus_sign_symbol('en_US')
- u'-'
-
- :param locale: the `Locale` object or locale identifier
- :return: the plus sign symbol
- :rtype: `unicode`
- """
- return Locale.parse(locale).number_symbols.get('minusSign', 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.
-
- >>> get_group_symbol('en_US')
- u','
-
- :param locale: the `Locale` object or locale identifier
- :return: the group symbol
- :rtype: `unicode`
- """
- return Locale.parse(locale).number_symbols.get('group', u',')
- def format_number(number, locale=LC_NUMERIC):
- """Return the given number formatted for a specific locale.
-
- >>> format_number(1099, locale='en_US')
- u'1,099'
-
- :param number: the number to format
- :param locale: the `Locale` object or locale identifier
- :return: the formatted number
- :rtype: `unicode`
- """
- # Do we really need this one?
- return format_decimal(number, locale=locale)
- def format_decimal(number, format=None, locale=LC_NUMERIC):
- """Return the given decimal number formatted for a specific locale.
-
- >>> format_decimal(1.2345, locale='en_US')
- u'1.234'
- >>> format_decimal(1.2346, locale='en_US')
- u'1.235'
- >>> format_decimal(-1.2346, locale='en_US')
- u'-1.235'
- >>> format_decimal(1.2345, locale='sv_SE')
- u'1,234'
- >>> format_decimal(12345, locale='de')
- u'12.345'
- The appropriate thousands grouping and the decimal separator are used for
- each locale:
-
- >>> format_decimal(12345.5, locale='en_US')
- u'12,345.5'
- :param number: the number to format
- :param format:
- :param locale: the `Locale` object or locale identifier
- :return: the formatted decimal number
- :rtype: `unicode`
- """
- locale = Locale.parse(locale)
- if not format:
- format = locale.decimal_formats.get(format)
- pattern = parse_pattern(format)
- return pattern.apply(number, locale)
- def format_currency(number, currency, format=None, locale=LC_NUMERIC):
- u"""Return formatted currency value.
-
- >>> format_currency(1099.98, 'USD', locale='en_US')
- u'$1,099.98'
- >>> format_currency(1099.98, 'USD', locale='es_CO')
- u'US$\\xa01.099,98'
- >>> format_currency(1099.98, 'EUR', locale='de_DE')
- u'1.099,98\\xa0\\u20ac'
-
- The pattern can also be specified explicitly:
-
- >>> format_currency(1099.98, 'EUR', u'\xa4\xa4 #,##0.00', locale='en_US')
- u'EUR 1,099.98'
-
- :param number: the number to format
- :param currency: the currency code
- :param locale: the `Locale` object or locale identifier
- :return: the formatted currency value
- :rtype: `unicode`
- """
- locale = Locale.parse(locale)
- if not format:
- format = locale.currency_formats.get(format)
- pattern = parse_pattern(format)
- return pattern.apply(number, locale, currency=currency)
- def format_percent(number, format=None, locale=LC_NUMERIC):
- """Return formatted percent value for a specific locale.
-
- >>> format_percent(0.34, locale='en_US')
- u'34%'
- >>> format_percent(25.1234, locale='en_US')
- u'2,512%'
- >>> format_percent(25.1234, locale='sv_SE')
- u'2\\xa0512\\xa0%'
- The format pattern can also be specified explicitly:
-
- >>> format_percent(25.1234, u'#,##0\u2030', locale='en_US')
- u'25,123\u2030'
- :param number: the percent number to format
- :param format:
- :param locale: the `Locale` object or locale identifier
- :return: the formatted percent number
- :rtype: `unicode`
- """
- locale = Locale.parse(locale)
- if not format:
- format = locale.percent_formats.get(format)
- pattern = parse_pattern(format)
- return pattern.apply(number, locale)
- 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):
- """Exception raised when a string cannot be parsed into a number."""
- def parse_number(string, locale=LC_NUMERIC):
- """Parse localized number string into a long integer.
-
- >>> parse_number('1,099', locale='en_US')
- 1099L
- >>> parse_number('1.099', locale='de_DE')
- 1099L
-
- When the given string cannot be parsed, an exception is raised:
-
- >>> parse_number('1.099,98', locale='de')
- Traceback (most recent call last):
- ...
- NumberFormatError: '1.099,98' is not a valid number
-
- :param string: the string to parse
- :param locale: the `Locale` object or locale identifier
- :return: the parsed number
- :rtype: `long`
- :raise `NumberFormatError`: if the string can not be converted to a number
- """
- try:
- return long(string.replace(get_group_symbol(locale), ''))
- except ValueError:
- raise NumberFormatError('%r is not a valid number' % string)
- def parse_decimal(string, locale=LC_NUMERIC):
- """Parse localized decimal string into a float.
-
- >>> parse_decimal('1,099.98', locale='en_US')
- 1099.98
- >>> parse_decimal('1.099,98', locale='de')
- 1099.98
-
- When the given string cannot be parsed, an exception is raised:
-
- >>> parse_decimal('2,109,998', locale='de')
- Traceback (most recent call last):
- ...
- NumberFormatError: '2,109,998' is not a valid decimal number
-
- :param string: the string to parse
- :param locale: the `Locale` object or locale identifier
- :return: the parsed decimal number
- :rtype: `float`
- :raise `NumberFormatError`: if the string can not be converted to a
- decimal number
- """
- locale = Locale.parse(locale)
- try:
- return float(string.replace(get_group_symbol(locale), '')
- .replace(get_decimal_symbol(locale), '.'))
- except ValueError:
- raise NumberFormatError('%r is not a valid decimal number' % string)
- PREFIX_END = r'[^0-9@#.,]'
- NUMBER_TOKEN = r'[0-9@#.\-,E+]'
- PREFIX_PATTERN = r"(?P<prefix>(?:'[^']*'|%s)*)" % PREFIX_END
- NUMBER_PATTERN = r"(?P<number>%s+)" % NUMBER_TOKEN
- SUFFIX_PATTERN = r"(?P<suffix>.*)"
- number_re = re.compile(r"%s%s%s" % (PREFIX_PATTERN, NUMBER_PATTERN,
- SUFFIX_PATTERN))
- def split_number(value):
- """Convert a number into a (intasstring, fractionasstring) tuple"""
- if have_decimal and isinstance(value, Decimal):
- text = str(value)
- else:
- text = ('%.9f' % value).rstrip('0')
- if '.' in text:
- a, b = text.split('.', 1)
- if b == '0':
- b = ''
- else:
- a, b = text, ''
- return a, b
- def bankersround(value, ndigits=0):
- """Round a number to a given precision.
- Works like round() except that the round-half-even (banker's rounding)
- algorithm is used instead of round-half-up.
- >>> bankersround(5.5, 0)
- 6.0
- >>> bankersround(6.5, 0)
- 6.0
- >>> bankersround(-6.5, 0)
- -6.0
- >>> bankersround(1234.0, -2)
- 1200.0
- """
- sign = int(value < 0) and -1 or 1
- value = abs(value)
- a, b = split_number(value)
- digits = a + b
- add = 0
- i = len(a) + ndigits
- if i < 0 or i >= len(digits):
- pass
- elif digits[i] > '5':
- add = 1
- elif digits[i] == '5' and digits[i-1] in '13579':
- add = 1
- scale = 10**ndigits
- if have_decimal and isinstance(value, Decimal):
- return Decimal(int(value * scale + add)) / scale * sign
- else:
- return float(int(value * scale + add)) / scale * sign
- 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
- 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 '
- '"@" or "0"')
- if '.' in number:
- integer, fraction = rsplit(number, '.', 1)
- else:
- integer = number
- fraction = ''
- 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 in '@0':
- min += 1
- max += 1
- elif c == '#':
- max += 1
- elif c == ',':
- continue
- 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_prec = parse_precision(integer)
- frac_prec = parse_precision(fraction)
- if exp:
- frac_prec = parse_precision(integer+fraction)
- exp_plus = exp.startswith('+')
- exp = exp.lstrip('+')
- exp_prec = parse_precision(exp)
- else:
- exp_plus = None
- exp_prec = None
- grouping = parse_grouping(integer)
- return NumberPattern(pattern, (pos_prefix, neg_prefix),
- (pos_suffix, neg_suffix), grouping,
- int_prec, frac_prec,
- exp_prec, exp_plus)
- class NumberPattern(object):
- def __init__(self, pattern, prefix, suffix, grouping,
- int_prec, frac_prec, exp_prec, exp_plus):
- self.pattern = pattern
- self.prefix = prefix
- self.suffix = suffix
- self.grouping = grouping
- self.int_prec = int_prec
- self.frac_prec = frac_prec
- self.exp_prec = exp_prec
- self.exp_plus = exp_plus
- if '%' in ''.join(self.prefix + self.suffix):
- self.scale = 100
- elif u'‰' in ''.join(self.prefix + self.suffix):
- self.scale = 1000
- else:
- self.scale = 1
- def __repr__(self):
- return '<%s %r>' % (type(self).__name__, self.pattern)
- def apply(self, value, locale, currency=None):
- value *= self.scale
- is_negative = int(value < 0)
- if self.exp_prec: # Scientific notation
- value = abs(value)
- if value:
- exp = int(math.floor(math.log(value, 10)))
- else:
- exp = 0
- # Minimum number of integer digits
- if self.int_prec[0] == self.int_prec[1]:
- exp -= self.int_prec[0] - 1
- # Exponent grouping
- elif self.int_prec[1]:
- exp = int(exp) / self.int_prec[1] * self.int_prec[1]
- if not have_decimal or not isinstance(value, Decimal):
- value = float(value)
- if exp < 0:
- value = value * 10**(-exp)
- else:
- value = value / 10**exp
- exp_sign = ''
- if exp < 0:
- exp_sign = get_minus_sign_symbol(locale)
- elif self.exp_plus:
- exp_sign = get_plus_sign_symbol(locale)
- exp = abs(exp)
- number = u'%s%s%s%s' % \
- (self._format_sigdig(value, self.frac_prec[0],
- self.frac_prec[1]),
- get_exponential_symbol(locale), exp_sign,
- self._format_int(str(exp), self.exp_prec[0],
- self.exp_prec[1], locale))
- elif '@' in self.pattern: # Is it a siginificant digits pattern?
- text = self._format_sigdig(abs(value),
- self.int_prec[0],
- self.int_prec[1])
- if '.' in text:
- a, b = text.split('.')
- a = self._format_int(a, 0, 1000, locale)
- if b:
- b = get_decimal_symbol(locale) + b
- number = a + b
- else:
- number = self._format_int(text, 0, 1000, locale)
- else: # A normal number pattern
- a, b = split_number(bankersround(abs(value),
- self.frac_prec[1]))
- b = b or '0'
- a = self._format_int(a, self.int_prec[0],
- self.int_prec[1], locale)
- b = self._format_frac(b, locale)
- number = a + b
- retval = u'%s%s%s' % (self.prefix[is_negative], number,
- self.suffix[is_negative])
- if u'¤' in retval:
- retval = retval.replace(u'¤¤', currency.upper())
- retval = retval.replace(u'¤', get_currency_symbol(currency, locale))
- return retval
- def _format_sigdig(self, value, min, max):
- """Convert value to a string.
- The resulting string will contain between (min, max) number of
- significant digits.
- """
- a, b = split_number(value)
- ndecimals = len(a)
- if a == '0' and b != '':
- ndecimals = 0
- while b.startswith('0'):
- b = b[1:]
- ndecimals -= 1
- a, b = split_number(bankersround(value, max - ndecimals))
- digits = len((a + b).lstrip('0'))
- if not digits:
- digits = 1
- # Figure out if we need to add any trailing '0':s
- if len(a) >= max and a != '0':
- return a
- if digits < min:
- b += ('0' * (min - digits))
- if b:
- return '%s.%s' % (a, b)
- return a
- def _format_int(self, value, min, max, locale):
- width = len(value)
- if width < min:
- value = '0' * (min - width) + value
- 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_prec
- if len(value) < min:
- value += ('0' * (min - len(value)))
- if max == 0 or (min == 0 and int(value) == 0):
- return ''
- width = len(value)
- while len(value) > min and value[-1] == '0':
- value = value[:-1]
- return get_decimal_symbol(locale) + value
|