3
\$\begingroup\$

I'm trying to come up with a general way to correctly round floats with Python, given the known limitation of round():

Note: The behavior of round() for floats can be surprising: for example, round(2.675, 2) gives 2.67 instead of the expected 2.68. This is not a bug: it’s a result of the fact that most decimal fractions can’t be represented exactly as a float. See Floating Point Arithmetic: Issues and Limitations for more information.

I thought that a simple way to correct for this effect is to add a small value to every float that ends with a 5, like so:

a = 7.85
b = a + 0.001
round(b, 1)
> 7.9

Below is the function I've written to do this. I've tested it and it seems to work fine, but I'd like some input on possible caveats I might be overlooking (or ways to improve it)

import numpy as np
from decimal import Decimal
from random import randint


def floats_5(N):
    """
    Generate random floats between a random range, where all floats
    end with a '5'.
    """
    rang = randint(1, 10000)
    flts = np.random.uniform(-1.*rang, 1.*rang, N)

    # Add '5' to the end of each random float, with different lengths.
    fl_5 = []
    for f in flts:
        # Trim float.
        i = randint(2, len(str(f).split('.')[1]))
        # Create trimmed float that ends with a '5' .
        f = Decimal(str(f).split('.')[0] + '.' + str(f).split('.')[1][:i] +
                    '5')
        fl_5.append(f)

    return fl_5


def format_5(f):
    """
    Take float and add a decimal '1' to the end of it.

    Return the number of decimal paces and the new float.
    """
    # Count number of decimal places.
    n = len(str(f).split('.')[1])

    # Decimal '1'  to add to the end of the float.
    d = '0.' + '0'*n + '1'

    # Add or subtract depending on the sign of the float.
    c = -1. if str(f)[0] == '-' else 1.

    # New augmented float.
    new_float = f + Decimal(c)*Decimal(d)

    # Return number of decimals and float with the small value added.
    return n, new_float


# Get some random floats.
fl_5 = floats_5(10)

# Compare float, round(float), and round(new_float)
print 'Original   round(f)   round(new_f)'
for f in fl_5:
    n, new_float = format_5(f)
    print f, round(f, n-1), round(new_float, n-1), '\n'
\$\endgroup\$
  • \$\begingroup\$ After you are satisfied with this code, please take a look at the decimal for actual production usage. \$\endgroup\$ – ferada Mar 8 '16 at 14:07
  • \$\begingroup\$ Is there an equivalent of round(f, n) (where f is the float and n is the number of digits to round to) with Decimal? \$\endgroup\$ – Gabriel Mar 8 '16 at 14:11
  • 2
    \$\begingroup\$ That's why I suggested to read it. Take a look at the quantize method and the description of rounding modes and the context. \$\endgroup\$ – ferada Mar 8 '16 at 14:17
4
\$\begingroup\$

Following ferada's advice I looked up the decimal module and it simplifies things quite a bit.

This is a function that does what format_5() does but much succinctly:

def format_5_dec(f):
    new_float = Decimal.quantize(f, Decimal(str(f)[:-1]), rounding='ROUND_UP')
    return new_float
\$\endgroup\$
  • \$\begingroup\$ And you can even return directly: return Decimal.quantize(f, Decimal(str(f)[:-1]), rounding='ROUND_UP') \$\endgroup\$ – Caridorc Mar 8 '16 at 19:05
  • \$\begingroup\$ I don't like direct returns, I prefer expressing things I calculate as variables first. It makes things more explicit I think. Old habit from Fortran perhaps... \$\endgroup\$ – Gabriel Mar 8 '16 at 19:07
  • 2
    \$\begingroup\$ I don't like intermediate variables. I prefer expressing things I calculate as expressions only. It makes things more concise and to-the-point I think. Old habit from Python/Ruby/Haskell perhaps... \$\endgroup\$ – Caridorc Mar 8 '16 at 19:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.