1

I would like to check if a float is a multiple of another float, but am running into issues with machine precision. For example:

t1 = 0.02
factor = 0.01
print(t1%factor==0)

The above outputs True, but

t2 = 0.030000000000000002
print(round(t2,5)%factor==0)

This outputs False. At some points in my code the number I am checking develops these machine precision errors, and I thought I could fix the issue simply by rounding it (I need 5 decimal places for later in my code, but it also doesn't work if I just round it to 2 decimal places).

Any ideas why the above check round(t2,5)%factor==0 doesn't work as expected, and how I can fix it?

curious_cosmo
  • 1,184
  • 1
  • 18
  • 36
  • 1
    For me, t1%factor == 0 returns false – Zaid Al Shattle Sep 16 '21 at 14:42
  • @ZaidAlShattle Sorry, I meant to type t1=0.02 (see edit), which returns True. But this uncovers something very weird. Why would 0.02%0.01==0 return True, while 0.03%0.01==0 return False? – curious_cosmo Sep 16 '21 at 14:46
  • I think since `round(t2,5)%factor` and `t1%factor` will return a floating point number so you need to round it also to `round(round(t2,5)%factor)` and `round(t1%factor)` – tax evader Sep 16 '21 at 14:46
  • @user696969 's answer can also be correct, however, it will not work if the difference is 0.4999, which should return False, but if you round it, it will return True. – Zaid Al Shattle Sep 16 '21 at 14:47
  • [What Every Computer Scientist Should Know About Floating-Point Arithmetic](//docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html) – Pranav Hosangadi Sep 16 '21 at 14:50
  • 1
    This is an excellent explanation: https://stackoverflow.com/a/14763891/3047101 – Stephan Sep 16 '21 at 14:50
  • Does this answer your question? [Python modulo on floats](https://stackoverflow.com/questions/14763722/python-modulo-on-floats) – Pranav Hosangadi Sep 16 '21 at 14:51

3 Answers3

3

It doesn't work as expected because checking floats for equality almost never works as expected. A quick fix would be to use math.isclose. This allows you to adjust your tolerance as well. Remember that when doing arithmetic mod r, r is equivalent to 0, so you should check if you're close to 0 or r.

import math

t1 = 0.02
factor = 0.01
res = t1 % factor
print(math.isclose(res, 0) or math.isclose(res, factor))

This is pretty quick and dirty and you will want to make sure your tolerances are working correctly and equivalently for both of those checks.

Kyle Parsons
  • 1,475
  • 6
  • 14
2

You should use the decimal module. The decimal module provides support for fast correctly-rounded decimal floating point arithmetic.

import decimal
print( decimal.Decimal('0.03') % decimal.Decimal('0.01') == decimal.Decimal('0') )

Gives :

True
manu190466
  • 1,557
  • 1
  • 9
  • 17
0

Generally, floats in Python are... messed up, for the lack of a better word. And they can act in very unexpected ways. (You can read more about that behaviour here.)

For your goal however, a better way is this:

t2 = 0.03000003
factor = 0.01
precision = 10000  # 4 digits
print(int(t2*precision)%int(factor*precision)==0)

Moving the maths to an integer-based calculation solves most of those issues.

Mark Dickinson
  • 29,088
  • 9
  • 83
  • 120
Zaid Al Shattle
  • 1,454
  • 1
  • 12
  • 21
  • 3
    It's not really Python's fault though. It's just following IEEE specifications for float arithmetic by delegating to hardware (is my understanding of how it works). – Kyle Parsons Sep 16 '21 at 14:49
  • 1
    I am not disagreeing that it is not, but it is a fact that floats do not act the way most people expect them to, in other languages, especially for people who have not experienced the issue before (Similar to the way parameters work in functions e.g. `def myfunc(newList =[])`.) It is not an unintended behaviour, but it is a common "gotcha!". @KyleParsons But perhaps I could've used better wording. – Zaid Al Shattle Sep 16 '21 at 14:51
  • @ZaidAlShattle: It has absolutely nothing to do with Python. JavaScript, C, C++, C#, Java, Haskell, Rust, R, MATLAB, etc. - anything that uses the hardware-supported floating-point - will _all_ have the exact same behaviours. Your suggested solution isn't a good one - it happens to work for this particular example, but will fail in other cases (try `t2 = 0.57`). (BTW, `t1` is undefined in your code snippet.) – Mark Dickinson Sep 20 '21 at 13:27