From: Aarni Koskela Date: Fri, 15 Jul 2016 12:11:06 +0000 (+0300) Subject: Fix float conversion in `extract_operands` (and the relevant test) X-Git-Tag: v2.4.0~8^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=refs%2Fpull%2F435%2Fhead;p=thirdparty%2Fbabel.git Fix float conversion in `extract_operands` (and the relevant test) Fixes #421 --- diff --git a/babel/plural.py b/babel/plural.py index 0b8e425d..99e9a650 100644 --- a/babel/plural.py +++ b/babel/plural.py @@ -9,7 +9,6 @@ :license: BSD, see LICENSE for more details. """ import re -import sys from babel._compat import decimal @@ -19,10 +18,27 @@ _fallback_tag = 'other' def extract_operands(source): - """Extract operands from a decimal, a float or an int, according to - `CLDR rules`_. + """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) @@ -30,10 +46,17 @@ def extract_operands(source): 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) + # 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() diff --git a/tests/test_plural.py b/tests/test_plural.py index 6406af87..be741499 100644 --- a/tests/test_plural.py +++ b/tests/test_plural.py @@ -16,6 +16,8 @@ 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'}) @@ -240,12 +242,12 @@ class PluralRuleParserTestCase(unittest.TestCase): EXTRACT_OPERANDS_TESTS = ( (1, 1, 1, 0, 0, 0, 0), - ('1.0', '1.0', 1, 1, 0, 0, 0), - ('1.00', '1.00', 1, 2, 0, 0, 0), - ('1.3', '1.3', 1, 1, 1, 3, 3), - ('1.30', '1.30', 1, 2, 1, 30, 3), - ('1.03', '1.03', 1, 2, 2, 3, 3), - ('1.230', '1.230', 1, 3, 2, 230, 23), + (decimal.Decimal('1.0'), '1.0', 1, 1, 0, 0, 0), + (decimal.Decimal('1.00'), '1.00', 1, 2, 0, 0, 0), + (decimal.Decimal('1.3'), '1.3', 1, 1, 1, 3, 3), + (decimal.Decimal('1.30'), '1.30', 1, 2, 1, 30, 3), + (decimal.Decimal('1.03'), '1.03', 1, 2, 2, 3, 3), + (decimal.Decimal('1.230'), '1.230', 1, 3, 2, 230, 23), (-1, 1, 1, 0, 0, 0, 0), (1.3, '1.3', 1, 1, 1, 3, 3), ) @@ -253,9 +255,13 @@ EXTRACT_OPERANDS_TESTS = ( @pytest.mark.parametrize('source,n,i,v,w,f,t', EXTRACT_OPERANDS_TESTS) 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) + 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 @pytest.mark.parametrize('locale', ('ru', 'pl'))