Skip to content

Instantly share code, notes, and snippets.

@mastermatt
Last active December 5, 2019 09:09
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mastermatt/887d41bf7d30a72baee27a30661fd77a to your computer and use it in GitHub Desktop.
Save mastermatt/887d41bf7d30a72baee27a30661fd77a to your computer and use it in GitHub Desktop.
Python round compat (bankers round for python2).
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