4

I would like to find the sum of a list of floats and check if it is a whole number or not:

l = [0.85, 0.85, 0.15, 0.15]

The sum of l is 2.0, obviously. But Python does not agree, due to floating point limitations:

> sum(l)
1.9999999999999998

Therefore my method of choice, sum(l).is_integer(), will return False.

What would be a better method to evaluate if lists sum to a whole number?

CDJB
  • 14,043
  • 5
  • 29
  • 55
n1000
  • 5,058
  • 10
  • 37
  • 65
  • 3
    are you aware about floating point limitations? – Paritosh Singh Dec 09 '19 at 17:03
  • The sum of `l` is 1? – Dani Mesejo Dec 09 '19 at 17:03
  • relevant, not dupe: https://stackoverflow.com/questions/588004/is-floating-point-math-broken – Paritosh Singh Dec 09 '19 at 17:04
  • 1
    This seems like a duplicat of this question: https://stackoverflow.com/questions/21583758/how-to-check-if-a-float-value-is-a-whole-number – Dani Mesejo Dec 09 '19 at 17:05
  • @RaySteam: This is not a duplicate of that. – Eric Postpischil Dec 09 '19 at 18:36
  • @EricPostpischil How so? He basically knows how to sum, so what he needs is how to check if the result is a whole number. The marked answer basically tells the OP what he needs. – Dani Mesejo Dec 09 '19 at 18:38
  • 2
    @RaySteam: The problem here is not to determine whether a number is an integer or to find an integer within certain bounds, both of which are issues in [that question](https://stackoverflow.com/questions/21583758/how-to-check-if-a-float-value-is-a-whole-number) but to determine whether the exact mathematical sum of a list of values is an integer. The fundamental problem is doing the exact mathematics in light of the rounding errors introduced by floating-point operations, which is not addressed in that question. – Eric Postpischil Dec 09 '19 at 18:40

4 Answers4

6

You can use the decimal package.

>>> from decimal import Decimal
>>> l = [Decimal(x) for x in ['0.85', '0.85', '0.15', '0.15']]
>>> s = sum(l)
>>> s
Decimal('2.00')
>>> s == int(s)
True
Dave Radcliffe
  • 644
  • 4
  • 11
3

You can use a combination of math.isclose and the inbuilt round function.

>>> from math import isclose  
>>> l = [0.85, 0.85, 0.15, 0.15]
>>> s = sum(l)
>>> isclose(s, round(s))
True
CDJB
  • 14,043
  • 5
  • 29
  • 55
  • This is a good practical solution, but note that it breaks down in the case of large lists (e.g. `l = [0.05] * 1000000`) and gives false positives (e.g. `l = [0.0000000001]`) – Ben Jones Dec 09 '19 at 21:33
  • As I said, it's a good solution, but I wanted to note this since this is a fundamental limitation of floating point. Changing `rel_tol` can only change which edge cases fail. – Ben Jones Dec 10 '19 at 22:07
2

When you need exact arithmetic, the best solution is to work with integers only. In this case you could round each of your inputs to 2 or 3 decimal places for the check.

l = [0.85, 0.85, 0.15, 0.15]
number_of_places = 3
multiplier = 10 ** number_of_places
>>> sum(int(round(x*multiplier)) for x in l)
2000
>>> sum(int(round(x*multiplier)) for x in l) % multiplier == 0
True
Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
1

Apart Decimal, you can also use the fractions module, which will enable handling arbitrary denominators like:

from fractions import Fraction
print(Fraction(1,3) + Fraction(4,7) + Fraction(2,21))

Also works with Fraction('0.85') + Fraction('0.15') or Fraction('2/6').

Note that the result of adding Fractions is a Fraction, eventually with denominator == 1, so instead of testing is_integer(), you have to replace with a denominator check, or with equality of int conversion:

collection = [Fraction(each) for each in ['0.85', '0.85', '0.15', '0.15']]
sum = sum(collection)
print(sum.denominator == 1)
print(int(sum) == sum)

In some languages including Smalltalk, Fractions are automatically reduced to Integer when denominator is 1, making the code a bit less brittle...

^((1/3) + (4/7) + (2/21)) isInteger
aka.nice
  • 9,100
  • 1
  • 28
  • 40