def test_can_parse_decimals(self): self.assertEqual(decimal.Decimal('1099.98'), numbers.parse_decimal('1,099.98', locale='en_US')) self.assertEqual(decimal.Decimal('1099.98'), numbers.parse_decimal('1.099,98', locale='de')) self.assertRaises(numbers.NumberFormatError, lambda: numbers.parse_decimal('2,109,998', locale='de'))
def test_parse_decimal(): assert (numbers.parse_decimal('1,099.98', locale='en_US') == decimal.Decimal('1099.98')) assert numbers.parse_decimal('1.099,98', locale='de') == decimal.Decimal('1099.98') with pytest.raises(numbers.NumberFormatError) as excinfo: numbers.parse_decimal('2,109,998', locale='de') assert excinfo.value.args[0] == "'2,109,998' is not a valid decimal number"
def test_smoke_numbers(locale): locale = Locale.parse(locale) for number in ( decimal.Decimal("-33.76"), # Negative Decimal decimal.Decimal("13.37"), # Positive Decimal 1.2 - 1.0, # Inaccurate float 10, # Plain old integer 0, # Zero ): assert numbers.format_number(number, locale=locale)
def apply(self, value, locale, currency=None, force_frac=None): frac_prec = force_frac or self.frac_prec if not isinstance(value, decimal.Decimal): value = decimal.Decimal(str(value)) value = value.scaleb(self.scale) is_negative = int(value.is_signed()) if self.exp_prec: # Scientific notation exp = value.adjusted() value = abs(value) # 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 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_significant(value, frac_prec[0], 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_significant(abs(value), self.int_prec[0], self.int_prec[1]) a, sep, b = text.partition(".") number = self._format_int(a, 0, 1000, locale) if sep: number += get_decimal_symbol(locale) + b else: # A normal number pattern precision = decimal.Decimal('1.' + '1' * frac_prec[1]) rounded = value.quantize(precision) a, sep, b = str(abs(rounded)).partition(".") number = (self._format_int(a, self.int_prec[0], self.int_prec[1], locale) + self._format_frac(b or '0', locale, force_frac)) retval = u'%s%s%s' % (self.prefix[is_negative], number, self.suffix[is_negative]) if u'¤' in retval: retval = retval.replace(u'¤¤¤', get_currency_name(currency, value, locale)) retval = retval.replace(u'¤¤', currency.upper()) retval = retval.replace(u'¤', get_currency_symbol(currency, locale)) return retval
def test_decimals(self): """Test significant digits patterns""" self.assertEqual(numbers.format_decimal(decimal.Decimal('1.2345'), '#.00', locale='en_US'), '1.23') self.assertEqual(numbers.format_decimal(decimal.Decimal('1.2345000'), '#.00', locale='en_US'), '1.23') self.assertEqual(numbers.format_decimal(decimal.Decimal('1.2345000'), '@@', locale='en_US'), '1.2') self.assertEqual(numbers.format_decimal(decimal.Decimal('12345678901234567890.12345'), '#.00', locale='en_US'), '12345678901234567890.12')
def extract_operands(source): """Extract operands from a decimal, a float or an int, according to `CLDR rules`_. .. _`CLDR rules`: http://www.unicode.org/reports/tr35/tr35-33/tr35-numbers.html#Operands """ n = abs(source) i = int(n) if isinstance(n, float): if i == n: n = i else: # 2.6's Decimal cannot convert from float directly if sys.version_info < (2, 7): n = str(n) n = decimal.Decimal(n) if isinstance(n, decimal.Decimal): dec_tuple = n.as_tuple() exp = dec_tuple.exponent fraction_digits = dec_tuple.digits[exp:] if exp < 0 else () trailing = ''.join(str(d) for d in fraction_digits) no_trailing = trailing.rstrip('0') v = len(trailing) w = len(no_trailing) f = int(trailing or 0) t = int(no_trailing or 0) else: v = w = f = t = 0 return n, i, v, w, f, t
def parse_decimal(string, locale=LC_NUMERIC): """Parse localized decimal string into a decimal. >>> parse_decimal('1,099.98', locale='en_US') Decimal('1099.98') >>> parse_decimal('1.099,98', locale='de') Decimal('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 :raise NumberFormatError: if the string can not be converted to a decimal number """ locale = Locale.parse(locale) try: return decimal.Decimal( string.replace(get_group_symbol(locale), '').replace(get_decimal_symbol(locale), '.')) except decimal.InvalidOperation: raise NumberFormatError('%r is not a valid decimal number' % string)
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, '1.2345E4') # 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(decimal.Decimal('12345'), '#.##E+00 m/s', locale='en_US') self.assertEqual(fmt, '1.23E+04 m/s') # 0 (see ticket #99) fmt = numbers.format_scientific(0, '#E0', locale='en_US') self.assertEqual(fmt, '0E0')
def test_format_currency_precision(input_value, expected_value): # Test precision conservation. assert numbers.format_currency( decimal.Decimal(input_value), 'USD', locale='en_US', decimal_quantization=False) == expected_value
def test_extract_operands(source, n, i, v, w, f, t): e_n, e_i, e_v, e_w, e_f, e_t = plural.extract_operands(source) assert abs(e_n - decimal.Decimal(n)) <= EPSILON # float-decimal conversion inaccuracy assert e_i == i assert e_v == v assert e_w == w assert e_f == f assert e_t == t
def apply(self, value, locale, currency=None, currency_digits=True): """Renders into a string a number following the defined pattern. """ if not isinstance(value, decimal.Decimal): value = decimal.Decimal(str(value)) value = value.scaleb(self.scale) # Separate the absolute value from its sign. is_negative = int(value.is_signed()) value = abs(value).normalize() # Prepare scientific notation metadata. if self.exp_prec: value, exp, exp_sign = self.scientific_notation_elements( value, locale) # Adjust the precision of the fractionnal part and force it to the # currency's if neccessary. frac_prec = self.frac_prec if currency and currency_digits: frac_prec = (get_currency_precision(currency), ) * 2 # Render scientific notation. if self.exp_prec: number = ''.join([ self._quantize_value(value, locale, frac_prec), get_exponential_symbol(locale), exp_sign, self._format_int(str(exp), self.exp_prec[0], self.exp_prec[1], locale) ]) # Is it a siginificant digits pattern? elif '@' in self.pattern: text = self._format_significant(value, self.int_prec[0], self.int_prec[1]) a, sep, b = text.partition(".") number = self._format_int(a, 0, 1000, locale) if sep: number += get_decimal_symbol(locale) + b # A normal number pattern. else: number = self._quantize_value(value, locale, frac_prec) retval = ''.join( [self.prefix[is_negative], number, self.suffix[is_negative]]) if u'¤' in retval: retval = retval.replace(u'¤¤¤', get_currency_name(currency, value, locale)) retval = retval.replace(u'¤¤', currency.upper()) retval = retval.replace(u'¤', get_currency_symbol(currency, locale)) return retval
def test_parse_decimal_nbsp_heuristics(): # Re https://github.com/python-babel/babel/issues/637 – # for locales (of which there are many) that use U+00A0 as the group # separator in numbers, it's reasonable to assume that input strings # with plain spaces actually should have U+00A0s instead. # This heuristic is only applied when strict=False. n = decimal.Decimal("12345.123") assert numbers.parse_decimal("12 345.123", locale="fi") == n assert numbers.parse_decimal(numbers.format_decimal(n, locale="fi"), locale="fi") == n
def extract_operands(source): """Extract operands from a decimal, a float or an int, according to `CLDR rules`_. The result is a 6-tuple (n, i, v, w, f, t), where those symbols are as follows: ====== =============================================================== Symbol Value ------ --------------------------------------------------------------- n absolute value of the source number (integer and decimals). i integer digits of n. v number of visible fraction digits in n, with trailing zeros. w number of visible fraction digits in n, without trailing zeros. f visible fractional digits in n, with trailing zeros. t visible fractional digits in n, without trailing zeros. ====== =============================================================== .. _`CLDR rules`: http://www.unicode.org/reports/tr35/tr35-33/tr35-numbers.html#Operands :param source: A real number :type source: int|float|decimal.Decimal :return: A n-i-v-w-f-t tuple :rtype: tuple[decimal.Decimal, int, int, int, int, int] """ n = abs(source) i = int(n) if isinstance(n, float): if i == n: n = i else: # Cast the `float` to a number via the string representation. # This is required for Python 2.6 anyway (it will straight out fail to # do the conversion otherwise), and it's highly unlikely that the user # actually wants the lossless conversion behavior (quoting the Python # documentation): # > If value is a float, the binary floating point value is losslessly # > converted to its exact decimal equivalent. # > This conversion can often require 53 or more digits of precision. # Should the user want that behavior, they can simply pass in a pre- # converted `Decimal` instance of desired accuracy. n = decimal.Decimal(str(n)) if isinstance(n, decimal.Decimal): dec_tuple = n.as_tuple() exp = dec_tuple.exponent fraction_digits = dec_tuple.digits[exp:] if exp < 0 else () trailing = ''.join(str(d) for d in fraction_digits) no_trailing = trailing.rstrip('0') v = len(trailing) w = len(no_trailing) f = int(trailing or 0) t = int(no_trailing or 0) else: v = w = f = t = 0 return n, i, v, w, f, t
def _format_significant(self, value, minimum, maximum): exp = value.adjusted() scale = maximum - 1 - exp digits = str(value.scaleb(scale).quantize(decimal.Decimal(1))) if scale <= 0: result = digits + '0' * -scale else: intpart = digits[:-scale] i = len(intpart) j = i + max(minimum - i, 0) result = "{intpart}.{pad:0<{fill}}{fracpart}{fracextra}".format( intpart=intpart or '0', pad='', fill=-min(exp + 1, 0), fracpart=digits[i:j], fracextra=digits[j:].rstrip('0'), ).rstrip('.') return result
def test_very_small_decimal_no_quantization(): assert numbers.format_decimal(decimal.Decimal('1E-7'), locale='en', decimal_quantization=False) == '0.0000001'
def apply(self, value, locale, currency=None, currency_digits=True, decimal_quantization=True): """Renders into a string a number following the defined pattern. Forced decimal quantization is active by default so we'll produce a number string that is strictly following CLDR pattern definitions. """ if not isinstance(value, decimal.Decimal): value = decimal.Decimal(str(value)) value = value.scaleb(self.scale) # Separate the absolute value from its sign. is_negative = int(value.is_signed()) value = abs(value).normalize() # Prepare scientific notation metadata. if self.exp_prec: value, exp, exp_sign = self.scientific_notation_elements( value, locale) # Adjust the precision of the fractionnal part and force it to the # currency's if neccessary. frac_prec = self.frac_prec if currency and currency_digits: frac_prec = (get_currency_precision(currency), ) * 2 # Bump decimal precision to the natural precision of the number if it # exceeds the one we're about to use. This adaptative precision is only # triggered if the decimal quantization is disabled or if a scientific # notation pattern has a missing mandatory fractional part (as in the # default '#E0' pattern). This special case has been extensively # discussed at https://github.com/python-babel/babel/pull/494#issuecomment-307649969 . if not decimal_quantization or (self.exp_prec and frac_prec == (0, 0)): frac_prec = (frac_prec[0], max([frac_prec[1], get_decimal_precision(value)])) # Render scientific notation. if self.exp_prec: number = ''.join([ self._quantize_value(value, locale, frac_prec), get_exponential_symbol(locale), exp_sign, self._format_int(str(exp), self.exp_prec[0], self.exp_prec[1], locale) ]) # Is it a siginificant digits pattern? elif '@' in self.pattern: text = self._format_significant(value, self.int_prec[0], self.int_prec[1]) a, sep, b = text.partition(".") number = self._format_int(a, 0, 1000, locale) if sep: number += get_decimal_symbol(locale) + b # A normal number pattern. else: number = self._quantize_value(value, locale, frac_prec) retval = ''.join( [self.prefix[is_negative], number, self.suffix[is_negative]]) if u'¤' in retval: retval = retval.replace(u'¤¤¤', get_currency_name(currency, value, locale)) retval = retval.replace(u'¤¤', currency.upper()) retval = retval.replace(u'¤', get_currency_symbol(currency, locale)) return retval
def test_format_percent_precision(input_value, expected_value): # Test precision conservation. assert numbers.format_percent(decimal.Decimal(input_value), locale='en_US') == expected_value
def test_plural_rule_operands_t(): rule = plural.PluralRule({'one': 't = 5'}) assert rule(decimal.Decimal('1.53')) == 'other' assert rule(decimal.Decimal('1.50')) == 'one' assert rule(1.5) == 'one'
def parse_decimal(string, locale=LC_NUMERIC, strict=False): """Parse localized decimal string into a decimal. >>> parse_decimal('1,099.98', locale='en_US') Decimal('1099.98') >>> parse_decimal('1.099,98', locale='de') Decimal('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 If `strict` is set to `True` and the given string contains a number formatted in an irregular way, an exception is raised: >>> parse_decimal('30.00', locale='de', strict=True) Traceback (most recent call last): ... NumberFormatError: '30.00' is not a properly formatted decimal number. Did you mean '3.000'? Or maybe '30,00'? >>> parse_decimal('0.00', locale='de', strict=True) Traceback (most recent call last): ... NumberFormatError: '0.00' is not a properly formatted decimal number. Did you mean '0'? :param string: the string to parse :param locale: the `Locale` object or locale identifier :param strict: controls whether numbers formatted in a weird way are accepted or rejected :raise NumberFormatError: if the string can not be converted to a decimal number """ locale = Locale.parse(locale) group_symbol = get_group_symbol(locale) decimal_symbol = get_decimal_symbol(locale) try: parsed = decimal.Decimal(string.replace(group_symbol, '') .replace(decimal_symbol, '.')) except decimal.InvalidOperation: raise NumberFormatError('%r is not a valid decimal number' % string) if strict and group_symbol in string: proper = format_decimal(parsed, locale=locale, decimal_quantization=False) if string != proper and string.rstrip('0') != (proper + decimal_symbol): try: parsed_alt = decimal.Decimal(string.replace(decimal_symbol, '') .replace(group_symbol, '.')) except decimal.InvalidOperation: raise NumberFormatError(( "%r is not a properly formatted decimal number. Did you mean %r?" % (string, proper) ), suggestions=[proper]) else: proper_alt = format_decimal(parsed_alt, locale=locale, decimal_quantization=False) if proper_alt == proper: raise NumberFormatError(( "%r is not a properly formatted decimal number. Did you mean %r?" % (string, proper) ), suggestions=[proper]) else: raise NumberFormatError(( "%r is not a properly formatted decimal number. Did you mean %r? Or maybe %r?" % (string, proper, proper_alt) ), suggestions=[proper, proper_alt]) return parsed
def test_formatting_of_very_small_decimals(self): # previously formatting very small decimals could lead to a type error # because the Decimal->string conversion was too simple (see #214) number = decimal.Decimal("7E-7") fmt = numbers.format_decimal(number, format="@@@", locale='en_US') self.assertEqual('0.000000700', fmt)
def get_decimal_quantum(precision): """Return minimal quantum of a number, as defined by precision.""" assert isinstance(precision, (int, long, decimal.Decimal)) return decimal.Decimal(10) ** (-precision)
# All rights reserved. # # This software is licensed as described in the file LICENSE, 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/. import unittest import pytest from babel import plural, localedata from babel._compat import decimal EPSILON = decimal.Decimal("0.0001") def test_plural_rule(): rule = plural.PluralRule({'one': 'n is 1'}) assert rule(1) == 'one' assert rule(2) == 'other' rule = plural.PluralRule({'one': 'n is 1'}) assert rule.rules == {'one': 'n is 1'} def test_plural_rule_operands_i(): rule = plural.PluralRule({'one': 'i is 1'}) assert rule(1.2) == 'one' assert rule(2) == 'other'
def apply( self, value, locale, currency=None, currency_digits=True, decimal_quantization=True, force_frac=None, ): """Renders into a string a number following the defined pattern. Forced decimal quantization is active by default so we'll produce a number string that is strictly following CLDR pattern definitions. :param value: The value to format. If this is not a Decimal object, it will be cast to one. :type value: decimal.Decimal|float|int :param locale: The locale to use for formatting. :type locale: str|babel.core.Locale :param currency: Which currency, if any, to format as. :type currency: str|None :param currency_digits: Whether or not to use the currency's precision. If false, the pattern's precision is used. :type currency_digits: bool :param decimal_quantization: Whether decimal numbers should be forcibly quantized to produce a formatted output strictly matching the CLDR definition for the locale. :type decimal_quantization: bool :param force_frac: DEPRECATED - a forced override for `self.frac_prec` for a single formatting invocation. :return: Formatted decimal string. :rtype: str """ if not isinstance(value, decimal.Decimal): value = decimal.Decimal(str(value)) value = value.scaleb(self.scale) # Separate the absolute value from its sign. is_negative = int(value.is_signed()) value = abs(value).normalize() # Prepare scientific notation metadata. if self.exp_prec: value, exp, exp_sign = self.scientific_notation_elements(value, locale) # Adjust the precision of the fractionnal part and force it to the # currency's if neccessary. if force_frac: # TODO (3.x?): Remove this parameter warnings.warn('The force_frac parameter to NumberPattern.apply() is deprecated.', DeprecationWarning) frac_prec = force_frac elif currency and currency_digits: frac_prec = (get_currency_precision(currency), ) * 2 else: frac_prec = self.frac_prec # Bump decimal precision to the natural precision of the number if it # exceeds the one we're about to use. This adaptative precision is only # triggered if the decimal quantization is disabled or if a scientific # notation pattern has a missing mandatory fractional part (as in the # default '#E0' pattern). This special case has been extensively # discussed at https://github.com/python-babel/babel/pull/494#issuecomment-307649969 . if not decimal_quantization or (self.exp_prec and frac_prec == (0, 0)): frac_prec = (frac_prec[0], max([frac_prec[1], get_decimal_precision(value)])) # Render scientific notation. if self.exp_prec: number = ''.join([ self._quantize_value(value, locale, frac_prec), get_exponential_symbol(locale), exp_sign, self._format_int( str(exp), self.exp_prec[0], self.exp_prec[1], locale)]) # Is it a siginificant digits pattern? elif '@' in self.pattern: text = self._format_significant(value, self.int_prec[0], self.int_prec[1]) a, sep, b = text.partition(".") number = self._format_int(a, 0, 1000, locale) if sep: number += get_decimal_symbol(locale) + b # A normal number pattern. else: number = self._quantize_value(value, locale, frac_prec) retval = ''.join([ self.prefix[is_negative], number, self.suffix[is_negative]]) if u'¤' in retval: retval = retval.replace(u'¤¤¤', get_currency_name(currency, value, locale)) retval = retval.replace(u'¤¤', currency.upper()) retval = retval.replace(u'¤', get_currency_symbol(currency, locale)) return retval
def test_plural_rule_operands_v(): rule = plural.PluralRule({'one': 'v is 2'}) assert rule(decimal.Decimal('1.20')) == 'one' assert rule(decimal.Decimal('1.2')) == 'other' assert rule(2) == 'other'
def test_decimal_precision(): assert get_decimal_precision(decimal.Decimal('0.110')) == 2 assert get_decimal_precision(decimal.Decimal('1.0')) == 0 assert get_decimal_precision(decimal.Decimal('10000')) == 0
def test_plural_rule_operands_f(): rule = plural.PluralRule({'one': 'f is 20'}) assert rule(decimal.Decimal('1.23')) == 'other' assert rule(decimal.Decimal('1.20')) == 'one' assert rule(1.2) == 'other'
def test_format_scientific_precision(input_value, expected_value): # Test precision conservation. assert numbers.format_scientific( decimal.Decimal(input_value), locale='en_US', decimal_quantization=False) == expected_value
def test_extract_operands(source, n, i, v, w, f, t): source = decimal.Decimal(source) if isinstance(source, str) else source assert (plural.extract_operands(source) == decimal.Decimal(n), i, v, w, f, t)