Repository: python-babel/babel
Pull request #494 (2017-04-20 16:13:46)

Decimal quantization
This PR add a `decimal_quantization` parameter to `format_decimal()`, `format_currency()`, `format_percent()` and `format_scientific()`, to bypass the forced quantization on the fractional part. This PR definitely address #90 and is a continuation of #410.

This PR has been extensively reviewed and is waiting for #538 to be merged, so I can rebase that one on top of it.

codecov-io: # [Codecov](https://codecov.io/gh/python-babel/babel/pull/494?src=pr&el=h1) Report
> Merging [#494](https://codecov.io/gh/python-babel/babel/pull/494?src=pr&el=desc) into [master](https://codecov.io/gh/python-babel/babel/commit/644dbc2eee167407ed437421cd094af3c405d930?src=pr&el=desc) will **increase** coverage by `0.02%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/python-babel/babel/pull/494/graphs/tree.svg?width=650&height=150&src=pr&token=lEYaWCjCnu)](https://codecov.io/gh/python-babel/babel/pull/494?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #494      +/-   ##
==========================================
+ Coverage   90.25%   90.28%   +0.02%     
==========================================
  Files          24       24              
  Lines        4004     4016      +12     
==========================================
+ Hits         3614     3626      +12     
  Misses        390      390
```


| [Impacted Files](https://codecov.io/gh/python-babel/babel/pull/494?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [babel/numbers.py](https://codecov.io/gh/python-babel/babel/pull/494?src=pr&el=tree#diff-YmFiZWwvbnVtYmVycy5weQ==) | `97.95% <100%> (+0.08%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/python-babel/babel/pull/494?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/python-babel/babel/pull/494?src=pr&el=footer). Last update [644dbc2...6e90254](https://codecov.io/gh/python-babel/babel/pull/494?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

kdeldycke: This PR was previously based on #491 and was partially duplicating it. Now that the later was merged, I rebased and squashed the whole code on top of the current `master` branch.

This PR is ready to be reviewed and merged.
kdeldycke: Thanks @benselme for the reviewing effort! I'll address all your comments in one or two weeks as I'm on holidays right now! 😎 
kdeldycke: I just addressed all coding and document issues. The PR has been squashed into one commit.

@benselme : I know this PR is not easy to review. To ease the pain I took the time to write expansive tests, searching for edge-cases and undefined situations.

I'm really happy with the results as I found a way to handle generically any rendering pattern.

Now the only remaining issue concerns the `#E0` situation, with breaks the consistency of the pattern rendering. What should I do here to have this PR accepted?
benselme: Thanks a lot for your work. 

What's here looks good to me. But I think the `#E0` issue needs to be solved before a merge can take place, since it makes scientific formatting useless for dozens of languages. I would write a special case for it, with proper comments and test cases. I think that's what the cldr elixir package [does](https://github.com/kipcole9/cldr/blob/master/lib/cldr/number/formatter/decimal_formatter.ex#L314), but I very well might be wrong. I had a quick look in ICU4J but could not locate the relevant code.

I would also love to have @etanol 's opinion on all this since he's the other person who recently did some work on numbers.

PS: the [CLDR-users mailing list](http://unicode.org/mailman/listinfo/cldr-users) is a very useful resource.

etanol: Sorry to respond so late, but I haven't been involved in Python development for a while.

The changes look good, although they are a bit difficult to review because refactoring and new functionality are **not** in separate commits.  If you have the time, please consider splitting this PR in to two commits, otherwise it's very confusing to tell apart code movements from actual new functionality or bugfixing.

I also have a couple of other comments for specific snippets in case you want to address them.

Overall, I'm okay with the feature as long as it doesn't break backwards compatibility.
kdeldycke: @benselme: I just addressed the issue of the default `#E0` rendering pattern in commit b27569040b67524af676cb71636668e9a2588754. The current PR is now rendering scientific notation like the CLDR elixir package and ICU4J does.
kdeldycke: I just finished addressing all pending comments of this PR.

What needs to be done now:
* Have @etanol, @akx and @benselme do a final pass on the PR and eventually lift their review process.
* Squash all commits into one.
* Split the PR into two: one for decimal quantization, and one for all the other refactoring and bug fixes.


benselme: Thanks ! I'll try and have a look at your changes this week.
benselme: Sorry, weeks go by and I've been quite busy. Not forgetting about this PR though. Thanks for your patience.
kdeldycke: Thanks @benselme for the feedback! I know this PR is kind of tough so take your time.
kdeldycke: Any news on that one? :)
kdeldycke: I just finished splitting this PR. #494 is now waiting to be merged so I can finally rebase that one to add proper decimal quantization control.
kdeldycke: This PR has been rebased on top of the latest `master` branch and is ready to be merged! :)

Patch:
diff --git a/.travis.yml b/.travis.yml
index b7eff7d..f9469c9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -35,6 +35,8 @@ matrix:
       python: 3.5
       env:
       - PYTHON_TEST_FLAGS=-bb
+    - os: linux
+      python: 3.6
 
 install:
   - bash .ci/deps.${TRAVIS_OS_NAME}.sh
diff --git a/babel/numbers.py b/babel/numbers.py
index 0365132..1f77bcd 100644
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -22,7 +22,6 @@ import re
 from datetime import date as date_, datetime as datetime_
 from itertools import chain
 import warnings
-from itertools import chain
 
 from babel.core import default_locale, Locale, get_global
 from babel._compat import decimal, string_types
@@ -324,13 +323,27 @@ def format_number(number, locale=LC_NUMERIC):
     return format_decimal(number, locale=locale)
 
 
+def get_decimal_precision(number):
+    """Return maximum precision of a decimal instance's fractional part.
+
+    Precision is extracted from the fractional part only.
+    """
+    # Copied from: https://github.com/mahmoud/boltons/pull/59
+    assert isinstance(number, decimal.Decimal)
+    decimal_tuple = number.normalize().as_tuple()
+    if decimal_tuple.exponent >= 0:
+        return 0
+    return abs(decimal_tuple.exponent)
+
+
 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)
 
 
-def format_decimal(number, format=None, locale=LC_NUMERIC):
+def format_decimal(
+        number, format=None, locale=LC_NUMERIC, decimal_quantization=True):
     u"""Return the given decimal number formatted for a specific locale.
 
     >>> format_decimal(1.2345, locale='en_US')
@@ -350,23 +363,36 @@ def format_decimal(number, format=None, locale=LC_NUMERIC):
     >>> format_decimal(12345.5, locale='en_US')
     u'12,345.5'
 
+    By default the locale is allowed to truncate and round a high-precision
+    number by forcing its format pattern onto the decimal part. You can bypass
+    this behavior with the `decimal_quantization` parameter:
+
+    >>> format_decimal(1.2346, locale='en_US')
+    u'1.235'
+    >>> format_decimal(1.2346, locale='en_US', decimal_quantization=False)
+    u'1.2346'
+
     :param number: the number to format
     :param format:
     :param locale: the `Locale` object or locale identifier
+    :param decimal_quantization: Truncate and round high-precision numbers to
+                                 the format pattern. Defaults to `True`.
     """
     locale = Locale.parse(locale)
     if not format:
         format = locale.decimal_formats.get(format)
     pattern = parse_pattern(format)
-    return pattern.apply(number, locale)
+    return pattern.apply(
+        number, locale, decimal_quantization=decimal_quantization)
 
 
 class UnknownCurrencyFormatError(KeyError):
     """Exception raised when an unknown currency format is requested."""
 
 
-def format_currency(number, currency, format=None, locale=LC_NUMERIC,
-                    currency_digits=True, format_type='standard'):
+def format_currency(
+        number, currency, format=None, locale=LC_NUMERIC, currency_digits=True,
+        format_type='standard', decimal_quantization=True):
     u"""Return formatted currency value.
 
     >>> format_currency(1099.98, 'USD', locale='en_US')
@@ -416,12 +442,23 @@ def format_currency(number, currency, format=None, locale=LC_NUMERIC,
         ...
     UnknownCurrencyFormatError: "'unknown' is not a known currency format type"
 
+    By default the locale is allowed to truncate and round a high-precision
+    number by forcing its format pattern onto the decimal part. You can bypass
+    this behavior with the `decimal_quantization` parameter:
+
+    >>> format_currency(1099.9876, 'USD', locale='en_US')
+    u'$1,099.99'
+    >>> format_currency(1099.9876, 'USD', locale='en_US', decimal_quantization=False)
+    u'$1,099.9876'
+
     :param number: the number to format
     :param currency: the currency code
     :param format: the format string to use
     :param locale: the `Locale` object or locale identifier
-    :param currency_digits: use the currency's number of decimal digits
+    :param currency_digits: use the currency's natural number of decimal digits
     :param format_type: the currency format type to use
+    :param decimal_quantization: Truncate and round high-precision numbers to
+                                 the format pattern. Defaults to `True`.
     """
     locale = Locale.parse(locale)
     if format:
@@ -434,10 +471,12 @@ def format_currency(number, currency, format=None, locale=LC_NUMERIC,
                 "%r is not a known currency format type" % format_type)
 
     return pattern.apply(
-        number, locale, currency=currency, currency_digits=currency_digits)
+        number, locale, currency=currency, currency_digits=currency_digits,
+        decimal_quantization=decimal_quantization)
 
 
-def format_percent(number, format=None, locale=LC_NUMERIC):
+def format_percent(
+        number, format=None, locale=LC_NUMERIC, decimal_quantization=True):
     """Return formatted percent value for a specific locale.
 
     >>> format_percent(0.34, locale='en_US')
@@ -452,18 +491,31 @@ def format_percent(number, format=None, locale=LC_NUMERIC):
     >>> format_percent(25.1234, u'#,##0\u2030', locale='en_US')
     u'25,123\u2030'
 
+    By default the locale is allowed to truncate and round a high-precision
+    number by forcing its format pattern onto the decimal part. You can bypass
+    this behavior with the `decimal_quantization` parameter:
+
+    >>> format_percent(23.9876, locale='en_US')
+    u'2,399%'
+    >>> format_percent(23.9876, locale='en_US', decimal_quantization=False)
+    u'2,398.76%'
+
     :param number: the percent number to format
     :param format:
     :param locale: the `Locale` object or locale identifier
+    :param decimal_quantization: Truncate and round high-precision numbers to
+                                 the format pattern. Defaults to `True`.
     """
     locale = Locale.parse(locale)
     if not format:
         format = locale.percent_formats.get(format)
     pattern = parse_pattern(format)
-    return pattern.apply(number, locale)
+    return pattern.apply(
+        number, locale, decimal_quantization=decimal_quantization)
 
 
-def format_scientific(number, format=None, locale=LC_NUMERIC):
+def format_scientific(
+        number, format=None, locale=LC_NUMERIC, decimal_quantization=True):
     """Return value formatted in scientific notation for a specific locale.
 
     >>> format_scientific(10000, locale='en_US')
@@ -474,15 +526,27 @@ def format_scientific(number, format=None, locale=LC_NUMERIC):
     >>> format_scientific(1234567, u'##0.##E00', locale='en_US')
     u'1.23E06'
 
+    By default the locale is allowed to truncate and round a high-precision
+    number by forcing its format pattern onto the decimal part. You can bypass
+    this behavior with the `decimal_quantization` parameter:
+
+    >>> format_scientific(1234.9876, u'#.##E0', locale='en_US')
+    u'1.23E3'
+    >>> format_scientific(1234.9876, u'#.##E0', locale='en_US', decimal_quantization=False)
+    u'1.2349876E3'
+
     :param number: the number to format
     :param format:
     :param locale: the `Locale` object or locale identifier
+    :param decimal_quantization: Truncate and round high-precision numbers to
+                                 the format pattern. Defaults to `True`.
     """
     locale = Locale.parse(locale)
     if not format:
         format = locale.scientific_formats.get(format)
     pattern = parse_pattern(format)
-    return pattern.apply(number, locale)
+    return pattern.apply(
+        number, locale, decimal_quantization=decimal_quantization)
 
 
 class NumberFormatError(ValueError):
@@ -702,8 +766,13 @@ class NumberPattern(object):
 
         return value, exp, exp_sign
 
-    def apply(self, value, locale, currency=None, currency_digits=True):
+    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))
@@ -724,6 +793,15 @@ class NumberPattern(object):
         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([
diff --git a/setup.cfg b/setup.cfg
index c2d8f87..d5f180a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -7,3 +7,6 @@ doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE ALLOW_UNICODE IGNORE_EXCEPTI
 
 [bdist_wheel]
 universal = 1
+
+[metadata]
+license_file = LICENSE
diff --git a/setup.py b/setup.py
index 146f38d..684c387 100755
--- a/setup.py
+++ b/setup.py
@@ -44,12 +44,15 @@ setup(
         'License :: OSI Approved :: BSD License',
         'Operating System :: OS Independent',
         'Programming Language :: Python',
+        'Programming Language :: Python :: 2',
         'Programming Language :: Python :: 2.6',
         'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3',
         'Programming Language :: Python :: 3.3',
         'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: Implementation :: CPython',
         'Programming Language :: Python :: Implementation :: PyPy',
         'Topic :: Software Development :: Libraries :: Python Modules',
     ],
diff --git a/tox.ini b/tox.ini
index 5374658..b71dec7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py26, py27, pypy, py33, py34, py26-cdecimal, py27-cdecimal
+envlist = py26, py27, pypy, py33, py34, py35, py36, pypy3, py26-cdecimal, py27-cdecimal
 
 [testenv]
 deps =


Test patch:
diff --git a/tests/test_numbers.py b/tests/test_numbers.py
index 5c8da34..48a260b 100644
--- a/tests/test_numbers.py
+++ b/tests/test_numbers.py
@@ -16,10 +16,10 @@ import pytest
 
 from datetime import date
 
-from babel import numbers
+from babel import Locale, localedata, numbers
 from babel.numbers import (
-    list_currencies, validate_currency, UnknownCurrencyError, is_currency, normalize_currency, get_currency_precision)
-from babel.core import Locale
+    list_currencies, validate_currency, UnknownCurrencyError, is_currency, normalize_currency,
+    get_currency_precision, get_decimal_precision)
 from babel.localedata import locale_identifiers
 from babel._compat import decimal
 
@@ -271,6 +271,12 @@ def test_get_group_symbol():
     assert numbers.get_group_symbol('en_US') == u','
 
 
+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_format_number():
     assert numbers.format_number(1099, locale='en_US') == u'1,099'
     assert numbers.format_number(1099, locale='de_DE') == u'1.099'
@@ -314,7 +320,14 @@ def test_format_decimal():
 def test_format_decimal_precision(input_value, expected_value):
     # Test precision conservation.
     assert numbers.format_decimal(
-        decimal.Decimal(input_value), locale='en_US') == expected_value
+        decimal.Decimal(input_value), locale='en_US', decimal_quantization=False) == expected_value
+
+
+def test_format_decimal_quantization():
+    # Test all locales.
+    for locale_code in localedata.locale_identifiers():
+        assert numbers.format_decimal(
+            '0.9999999999', locale=locale_code, decimal_quantization=False).endswith('9999999999') is True
 
 
 def test_format_currency():
@@ -375,25 +388,32 @@ def test_format_currency_format_type():
     ('1.1', '$1.10'),
     ('1.11', '$1.11'),
     ('1.110', '$1.11'),
-    ('1.001', '$1.00'),
-    ('1.00100', '$1.00'),
-    ('01.00100', '$1.00'),
-    ('101.00100', '$101.00'),
+    ('1.001', '$1.001'),
+    ('1.00100', '$1.001'),
+    ('01.00100', '$1.001'),
+    ('101.00100', '$101.001'),
     ('00000', '$0.00'),
     ('0', '$0.00'),
     ('0.0', '$0.00'),
     ('0.1', '$0.10'),
     ('0.11', '$0.11'),
     ('0.110', '$0.11'),
-    ('0.001', '$0.00'),
-    ('0.00100', '$0.00'),
-    ('00.00100', '$0.00'),
-    ('000.00100', '$0.00'),
+    ('0.001', '$0.001'),
+    ('0.00100', '$0.001'),
+    ('00.00100', '$0.001'),
+    ('000.00100', '$0.001'),
 ])
 def test_format_currency_precision(input_value, expected_value):
     # Test precision conservation.
     assert numbers.format_currency(
-        decimal.Decimal(input_value), 'USD', locale='en_US') == expected_value
+        decimal.Decimal(input_value), 'USD', locale='en_US', decimal_quantization=False) == expected_value
+
+
+def test_format_currency_quantization():
+    # Test all locales.
+    for locale_code in localedata.locale_identifiers():
+        assert numbers.format_currency(
+            '0.9999999999', 'USD', locale=locale_code, decimal_quantization=False).find('9999999999') > -1
 
 
 def test_format_percent():
@@ -412,36 +432,43 @@ def test_format_percent():
     ('100', '10,000%'),
     ('0.01', '1%'),
     ('0.010', '1%'),
-    ('0.011', '1%'),
-    ('0.0111', '1%'),
-    ('0.01110', '1%'),
-    ('0.01001', '1%'),
-    ('0.0100100', '1%'),
-    ('0.010100100', '1%'),
+    ('0.011', '1.1%'),
+    ('0.0111', '1.11%'),
+    ('0.01110', '1.11%'),
+    ('0.01001', '1.001%'),
+    ('0.0100100', '1.001%'),
+    ('0.010100100', '1.01001%'),
     ('0.000000', '0%'),
     ('0', '0%'),
     ('0.00', '0%'),
     ('0.01', '1%'),
-    ('0.011', '1%'),
-    ('0.0110', '1%'),
-    ('0.0001', '0%'),
-    ('0.000100', '0%'),
-    ('0.0000100', '0%'),
-    ('0.00000100', '0%'),
+    ('0.011', '1.1%'),
+    ('0.0110', '1.1%'),
+    ('0.0001', '0.01%'),
+    ('0.000100', '0.01%'),
+    ('0.0000100', '0.001%'),
+    ('0.00000100', '0.0001%'),
 ])
 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
+        decimal.Decimal(input_value), locale='en_US', decimal_quantization=False) == expected_value
+
+
+def test_format_percent_quantization():
+    # Test all locales.
+    for locale_code in localedata.locale_identifiers():
+        assert numbers.format_percent(
+            '0.9999999999', locale=locale_code, decimal_quantization=False).find('99999999') > -1
 
 
 def test_format_scientific():
     assert numbers.format_scientific(10000, locale='en_US') == u'1E4'
     assert numbers.format_scientific(4234567, u'#.#E0', locale='en_US') == u'4.2E6'
-    assert numbers.format_scientific(4234567, u'0E0000', locale='en_US') == u'4E0006'
-    assert numbers.format_scientific(4234567, u'##0E00', locale='en_US') == u'4E06'
-    assert numbers.format_scientific(4234567, u'##00E00', locale='en_US') == u'42E05'
-    assert numbers.format_scientific(4234567, u'0,000E00', locale='en_US') == u'4,235E03'
+    assert numbers.format_scientific(4234567, u'0E0000', locale='en_US') == u'4.234567E0006'
+    assert numbers.format_scientific(4234567, u'##0E00', locale='en_US') == u'4.234567E06'
+    assert numbers.format_scientific(4234567, u'##00E00', locale='en_US') == u'42.34567E05'
+    assert numbers.format_scientific(4234567, u'0,000E00', locale='en_US') == u'4,234.567E03'
     assert numbers.format_scientific(4234567, u'##0.#####E00', locale='en_US') == u'4.23457E06'
     assert numbers.format_scientific(4234567, u'##0.##E00', locale='en_US') == u'4.23E06'
     assert numbers.format_scientific(42, u'00000.000000E0000', locale='en_US') == u'42000.000000E-0003'
@@ -451,29 +478,29 @@ def test_default_scientific_format():
     """ Check the scientific format method auto-correct the rendering pattern
     in case of a missing fractional part.
     """
-    assert numbers.format_scientific(12345, locale='en_US') == u'1E4'
-    assert numbers.format_scientific(12345.678, locale='en_US') == u'1E4'
-    assert numbers.format_scientific(12345, u'#E0', locale='en_US') == u'1E4'
-    assert numbers.format_scientific(12345.678, u'#E0', locale='en_US') == u'1E4'
+    assert numbers.format_scientific(12345, locale='en_US') == u'1.2345E4'
+    assert numbers.format_scientific(12345.678, locale='en_US') == u'1.2345678E4'
+    assert numbers.format_scientific(12345, u'#E0', locale='en_US') == u'1.2345E4'
+    assert numbers.format_scientific(12345.678, u'#E0', locale='en_US') == u'1.2345678E4'
 
 
 @pytest.mark.parametrize('input_value, expected_value', [
     ('10000', '1E4'),
     ('1', '1E0'),
     ('1.0', '1E0'),
-    ('1.1', '1E0'),
-    ('1.11', '1E0'),
-    ('1.110', '1E0'),
-    ('1.001', '1E0'),
-    ('1.00100', '1E0'),
-    ('01.00100', '1E0'),
-    ('101.00100', '1E2'),
+    ('1.1', '1.1E0'),
+    ('1.11', '1.11E0'),
+    ('1.110', '1.11E0'),
+    ('1.001', '1.001E0'),
+    ('1.00100', '1.001E0'),
+    ('01.00100', '1.001E0'),
+    ('101.00100', '1.01001E2'),
     ('00000', '0E0'),
     ('0', '0E0'),
     ('0.0', '0E0'),
     ('0.1', '1E-1'),
-    ('0.11', '1E-1'),
-    ('0.110', '1E-1'),
+    ('0.11', '1.1E-1'),
+    ('0.110', '1.1E-1'),
     ('0.001', '1E-3'),
     ('0.00100', '1E-3'),
     ('00.00100', '1E-3'),
@@ -482,7 +509,14 @@ def test_default_scientific_format():
 def test_format_scientific_precision(input_value, expected_value):
     # Test precision conservation.
     assert numbers.format_scientific(
-        decimal.Decimal(input_value), locale='en_US') == expected_value
+        decimal.Decimal(input_value), locale='en_US', decimal_quantization=False) == expected_value
+
+
+def test_format_scientific_quantization():
+    # Test all locales.
+    for locale_code in localedata.locale_identifiers():
+        assert numbers.format_scientific(
+            '0.9999999999', locale=locale_code, decimal_quantization=False).find('999999999') > -1
 
 
 def test_parse_number():