3

Does anyone know a good way in Python to check if a number is divisible by another in floating point in python?

The first thing I tried was ...

3.5 % 0.1 == 0.0

But this returns False so then maybe

3.5 % 0.1 >= 1e-6 

But also False ... bummer ... it turns out that

3.5 % 0.1
>> 0.099999999924

So then this works:

LAMBDA = 1e-9
def is_divisible_by(x, y):
   m = x % y
   dy = abs(y - m)
   return m < LAMBDA or dy < LAMBDA

is_divisible_by(3.5, 0.1)

But this seems dangerous because I have to pick a LAMBDA. What about if y = LAMBDA / 2...

is_divisible_by(LAMBDA/2, (LAMBDA/2) + 1e-10)
>>> True

So then

  def is_divisible_by(x, y):
      l = y * 1e-2
      m = x % y
      dy = abs(y - m)
      return m < l or dy < l

  is_divisible_by(3.5 * 1e-10, 0.1 * 1e-10)
  >>> True

  is_divisible_by(0.21, 0.211)
  >>> True
  

Bummer.

Is there anyway to solve this without going down a massive rabbit hole?

endolith
  • 25,479
  • 34
  • 128
  • 192
gbtimmon
  • 4,238
  • 1
  • 21
  • 36

3 Answers3

6

Depending on the source of your floating point numbers, the decimal module might be useful.

>>> import decimal
>>> decimal.Decimal("3.5") % decimal.Decimal("0.1")
Decimal('0.0')
Peter DeGlopper
  • 36,326
  • 7
  • 90
  • 83
  • Thanks ... `decimal.Deciaml(str(x)) % deciaml.Decimal(str(y)) == 0.0` seems to work here. – gbtimmon Mar 19 '19 at 21:39
  • 3
    The catch is that if your floating point numbers are already imprecise `decimal.Decimal(str(x))` can't reintroduce the precision. Eg: `decimal.Decimal(str(1.0 / 3)) * 3 == 1` will return `False`. – Peter DeGlopper Mar 19 '19 at 21:42
  • I don't do any operations on the floating point prior to this so the amount of error should be very reasonable. I think this is good. – gbtimmon Mar 19 '19 at 22:02
  • Importang thing to note is that values passed to `Decimal`'s `__init__` should be string types for this (getting the zero) to work. – Daniel Sep 24 '20 at 09:43
3

floating point numbers are "fuzzy". A good high-level mental model for floating point numbers is that they represent a small range of numbers (e.g 1.5 really means some number between 1.4999 and 1.5002). Because of this, there is not good way to check if one is divisible by another. Instead, to check if non-integer numbers are divisible by each other, you might want to use rational-type numbers. In python, there's a module for this, called Fraction. You can use it like this

from fractions import Fraction
a = Fraction(35,10) # 3.5
b = Fraction(1,10) # .1
a%b # evaluates to Fraction(0, 1)

Another answer mentioned the decimal python module. Fraction and decimal are interoperable.

from fractions import Fraction
from decimal import Decimal
a = Fraction(Decimal('3.5')) # 3.5
b = Fraction(Decimal('0.1)) # 0.1
a%b # evaluates to Fraction(0, 1)

I'm going to advocate for using Fraction as it is a bit more flexible

from fractions import Fraction
from decimal import Decimal
c = Decimal('1')
d = Decimal('.3')
c/d  # evaluates to Decimal('3.333333333333333333333333333')
c/d*d  # evaluates to Decimal('0.9999999999999999999999999999')
c = Fraction(c)
d = Fraction(d)
c/d # evaluates to Fraction(10, 3)
c/d*d # evaluates to Fraction(1, 1)
asky
  • 1,520
  • 12
  • 20
  • 1
    I strongly disagree with that mental model of floating-point numbers, but agree with the `fractions.Fraction` recommendation. – user2357112 Mar 19 '19 at 22:58
  • 1
    @user2357112 I agree it isn't perfect. Don't use the proposed mental model if you're intimately familiar with [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754). But, if you just type in a number with decimal (without picking it carefully), the IEEE 745 representation probably carries some error. Floating points do represent exact integers in a certain range, along with a bunch more numbers. This mental model is conservative and beginner-friendly, but ultimately false. There are more accurate models, but they are not as beginner friendly in my opinion. – asky Mar 19 '19 at 23:54
1

A potential solution is to multiply both floats by a "sufficiently large" scaling factor and then cast (round) to integers.

def check_floats_divisible(x: float, y: float, scaling_factor: float = 1e6):
    scaled_x = int(x * scaling_factor)
    scaled_y = int(y * scaling_factor)
    
    return (scaled_x % scaled_y) == 0

check_floats_divisible(3.5, 0.1)
>>> True

Notes:

  1. "Sufficiently large" may not be appropriate for your use case. It works well if, for example your two floats were time durations in seconds, and a particular time precision was acceptable in your use-case (e.g. microseconds).
  2. Need to be careful your scaling is sufficient to ensure the denominator is never zero after conversion to int.
Vaalsta
  • 31
  • 1
  • 3