How to test if one float is an integer multiple of another?

What is the best way to test if one floating point number is an integer multiple of another floating number? e.g. I want this to return true for 5.5 / 0.5, and false for 5.5 / 0.6.

Naively I would expect to do this by testing for remainder 0 after division using %, i.e.

0.2 % 0.1 == 0.0 true
0.21 % 0.1 == 0.0 false

This works on some numbers, but fails on others, presumably due to the magic of floating point precision:

0.3 % 0.1 = 0.09999999999999998 # should be 0.0

What’s the most accurate, and/or simplest, way to test this?

Worth noting I don’t know the numerator or denominator a priori so cannot use Rational type, though this gives the correct answer

3//10 % 1//10 = 0//1

The moment you represent your numbers in floating point, it’s already too late. The number 1/10 cannot be represented in binary floating point with any finite precision for the same reason that 1/3 cannot be represented in any finite precision decimal value (they’re both just infinitely repeating fractions in their respective bases).

You can use Rational numbers, which will give you the exact right answer.

But if you’re somehow stuck with floating-point inputs, then you’re going to have to make up your own tolerances. For example, you could check something like:

julia> check(a, b) = isapprox(round(b / a) * a, b)
check (generic function with 1 method)

julia> check(0.1, 0.2)
true

julia> check(0.1, 0.25)
false

julia> check(0.1, 0.3)
true

Of course, this has edge cases too:

julia> check(0.1, 0.0)  # should be true, and is
true

julia> check(0.1, 1e-16)  # what should this be? Not obvious...
false

julia> check(0.1 + 1e-16, 0.2)   # seems like this should match the one above...
true

What is preventing you from taking exact rational inputs if you actually want exact numerical results here?

7 Likes

Okay thanks, makes sense.

Strictly speaking nothing is preventing me from taking exact rational inputs, but I was just interested in the best way to handle this problem given an arbitrary collection of floating-point numbers.

For the specific example I have in mind though I’m taking elements in a range object e.g. 0:0.01:1 and testing them with another number, so this would be amenable to rationals.

One thing that might help you here is the rationalize function. E.g.

julia> rationalize(0.3) % rationalize(0.1)
0//1

But not always. As @rdeits correctly said, “The moment you represent your numbers in floating point, it’s already too late.”

2 Likes

rationalize is mathematically strict and chokes on pi.
The following variant uses it differently:

function ismult(x,y)
    (x, y) = x < y ? (y, x) : (x, y)
    # numerator(rationalize(x/y % 1)) == 0
    # or simply
    x/y % 1 == 0
end

ismult(pi, 1e6*pi) == true

What do you mean by ‘chokes’? Are you referring to this MethodError?

julia> rationalize(pi)
ERROR: MethodError: no method matching rationalize(::Irrational{:π})
Closest candidates are:
  rationalize(::Type{T}, ::AbstractFloat; tol) where T<:Integer at rational.jl:216
  rationalize(::Type{T}, ::AbstractFloat, ::Real) where T<:Integer at rational.jl:156
  rationalize(::AbstractFloat; kvs...) at rational.jl:217
  ...
Stacktrace:
 [1] top-level scope
   @ REPL[2]:1

You can only sensibly rationalize a concrete floating-point representation of pi:

julia> rationalize(1.0pi)
165707065//52746197

julia> rationalize(big(1.0)pi)
2646693125139304345//842468587426513207

julia> rationalize(1.0f0*pi)
355//113
3 Likes

Here is a bug fix:

Base.rationalize(::Irrational{:π}) = 22//7
7 Likes

Another suggestion that hope it is ok:

function aremultiples(a, b; ϵ=1e-16)
    (abs(a) > abs(b)) && ((a, b) = (b, a))
    (a == 0) && (return true)
    x = b/a
    abs(a*(x - round(x))) < ϵ
end

# TESTS:
aremultiples(0.1, 0.3)             # true
aremultiples(0.1, 0.0)             # true
aremultiples(0.1, 1e-16)           # true
aremultiples(0.1 + 1e-16, 0.2)     # false

aremultiples(1, -4)                # true
aremultiples(1, pi)                # false
aremultiples(0, 0)                 # true

aremultiples(a,b) = isapprox(rem(a, b, RoundToNearest) , 0; atol=0.01)?