Last active
December 5, 2019 09:09
-
-
Save mastermatt/887d41bf7d30a72baee27a30661fd77a to your computer and use it in GitHub Desktop.
Python round compat (bankers round for python2).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from __future__ import absolute_import | |
from __future__ import division | |
from __future__ import print_function | |
import decimal | |
import fractions | |
import math | |
import six | |
def _new_round(number, ndigits=None): | |
""" | |
Python implementation of Python 3 round(). | |
Round a number to a given precision in decimal digits (default 0 digits). | |
This returns an int when called with one argument, otherwise the | |
same type as the number. ndigits may be negative. | |
Delegates to the __round__ method if for some reason this exists. | |
Docs: https://docs.python.org/3/library/functions.html#round | |
Basically all borrowed from https://github.com/python/cpython | |
and https://bitbucket.org/pypy/pypy | |
Note: different interpreters handle Inf, Nan, and overflow issues differently | |
so it's hard to say this fully compatible with the future. | |
""" | |
try: | |
return number.__round__(ndigits) | |
except (AttributeError, NotImplementedError): | |
pass | |
# Decimals and Fractions implement `__round__` on their class in py3. | |
if isinstance(number, decimal.Decimal): | |
return _decimal_round(number, ndigits) | |
if isinstance(number, fractions.Fraction): | |
return _fractions_round(number, ndigits) | |
return_int = False | |
if ndigits is None: | |
return_int = True | |
ndigits = 0 | |
elif not isinstance(ndigits, six.integer_types): | |
raise TypeError("Second argument to round should be integral") | |
# handle nans and infinities as cPython does | |
if math.isnan(number): | |
if return_int: | |
return int(number) | |
raise ValueError("cannot round a NaN") | |
if math.isinf(number): | |
if return_int: | |
return int(number) | |
raise OverflowError("cannot round an infinity") | |
# At this point we assume that `number` is a basic scalar like an `int` or a `float`. | |
# Unfortunately, the interpreters I looked into all use their own version of dtoa | |
# logic that isn't exposed. Instead we look to piggyback the `Decimal` logic because | |
# it seems to have enough of what we want except some edge case error handling. | |
# We'll just have to make due. | |
d = decimal.Decimal.from_float(number) | |
context = decimal.getcontext() | |
# Deal with extreme values for ndigits. | |
# For too many ndigits, x always rounds to itself. | |
# For too few, x always rounds to +-0.0. | |
if d.adjusted() + ndigits + 1 > context.prec: | |
result = number | |
elif ndigits <= context.Etiny(): | |
# return 0.0, but with sign of x | |
result = 0.0 * number | |
else: | |
exp = object.__new__(decimal.Decimal) | |
exp._sign = 0 | |
exp._int = '1' | |
exp._exp = -ndigits | |
exp._is_special = False | |
result = d.quantize(exp, rounding=decimal.ROUND_HALF_EVEN) | |
if return_int: | |
return int(result) | |
return type(number)(result) | |
def _decimal_round(dec_obj, n=None): | |
"""Round self to the nearest integer, or to a given precision. | |
If only one argument is supplied, round a finite Decimal | |
instance self to the nearest integer. If self is infinite or | |
a NaN then a Python exception is raised. If self is finite | |
and lies exactly halfway between two integers then it is | |
rounded to the integer with even last digit. | |
>>> round(decimal.Decimal('123.456')) | |
123 | |
>>> round(decimal.Decimal('-456.789')) | |
-457 | |
>>> round(decimal.Decimal('-3.0')) | |
-3 | |
>>> round(decimal.Decimal('2.5')) | |
2 | |
>>> round(decimal.Decimal('3.5')) | |
4 | |
>>> round(decimal.Decimal('Inf')) | |
Traceback (most recent call last): | |
... | |
OverflowError: cannot round an infinity | |
>>> round(decimal.Decimal('NaN')) | |
Traceback (most recent call last): | |
... | |
ValueError: cannot round a NaN | |
If a second argument n is supplied, self is rounded to n | |
decimal places using the rounding mode for the current | |
context. | |
For an integer n, round(self, -n) is exactly equivalent to | |
self.quantize(Decimal('1En')). | |
>>> round(decimal.Decimal('123.456'), 0) | |
Decimal('123') | |
>>> round(decimal.Decimal('123.456'), 2) | |
Decimal('123.46') | |
>>> round(decimal.Decimal('123.456'), -2) | |
Decimal('1E+2') | |
>>> round(decimal.Decimal('-Infinity'), 37) | |
Decimal('NaN') | |
>>> round(decimal.Decimal('sNaN123'), 0) | |
Decimal('NaN123') | |
""" | |
if n is not None: | |
# two-argument form: use the equivalent quantize call | |
if not isinstance(n, six.integer_types): | |
raise TypeError("Second argument to round should be integral") | |
exp = object.__new__(decimal.Decimal) | |
exp._sign = 0 | |
exp._int = '1' | |
exp._exp = -n | |
exp._is_special = False | |
return dec_obj.quantize(exp) | |
# one-argument form | |
if dec_obj._is_special: | |
if dec_obj.is_nan(): | |
raise ValueError("cannot round a NaN") | |
else: | |
raise OverflowError("cannot round an infinity") | |
return int(dec_obj._rescale(0, decimal.ROUND_HALF_EVEN)) | |
def _fractions_round(frac_obj, ndigits=None): | |
"""Will be round(self, ndigits) in 3.0. | |
Rounds half toward even. | |
""" | |
if ndigits is None: | |
floor, remainder = divmod(frac_obj.numerator, frac_obj.denominator) | |
if remainder * 2 < frac_obj.denominator: | |
return floor | |
elif remainder * 2 > frac_obj.denominator: | |
return floor + 1 | |
# Deal with the half case: | |
elif floor % 2 == 0: | |
return floor | |
else: | |
return floor + 1 | |
shift = 10 ** abs(ndigits) | |
# See _operator_fallbacks.forward to check that the results of | |
# these operations will always be Fraction and therefore have | |
# round(). | |
if ndigits > 0: | |
return fractions.Fraction(round(frac_obj * shift), shift) | |
else: | |
return fractions.Fraction(round(frac_obj / shift) * shift) | |
if round(2.5) != 2: | |
round = _new_round | |
else: | |
round = round |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment