7

I wondered whether the values 1/256, 2/256, 3/256, ... 254/256 and 255/256 are exactly representable as f32. Now, someone smart would think about how floating point numbers work and find out that way. But I would like to check that in a program. All numbers I want to check are fractions and I control the values (i.e. no user input).

I started with this:

for n in 1u8..=255 {
    let f = (n as f32) / 256.0;
    println!("{}", f);
}

But now what? I tried printing the number to see if there were a large number of recurring digits, but that doesn't always work. For example, 0.4 which is not exactly representable:

println!("{}", 0.4);     // prints "0.4"
println!("{:.20}", 0.4); // prints "0.40000000000000002220"

Here we have to manually crank up the precision to see the problems. And in any case, looking at the string output seems like a suboptimal solution anyway.

First I thought that there might be a method on f32, but this wouldn't make much sense, would it? Because when a f32 already exists, there is no way to know if its value was intended or not. So we somehow have to find out when creating the float value and compare to the "idealized" value?

Is there any way to check if a value can be exactly represented as f32?

Lukas Kalbertodt
  • 79,749
  • 26
  • 255
  • 305
  • 3
    Here's a test that will "almost always" work, though it needs refining: You start with two arbitrarily large integers representing an irreductible fraction `a/b`. If `b` is not an exact power of 2 (i.e. `b` is not 1,2,4,8,...), then your number will not be exactly representable. If `a` is more than about 2^23 then it will not be exactly representable. Otherwise (i.e. if `a<2^23` and `b` is an exact power of 2), most likely it will be representable. However, I've skipped lots of quirks and it would be far from trivial to take everything into account. – Jojonete Feb 20 '19 at 09:42
  • @Jojonete That's very few values. Maybe f32 is not the encoding to use in this situation. – Boiethios Feb 20 '19 at 10:22
  • @Darth: why *very few values*? You have a numerator and a denominator. Only the denominator must be a power of 2, not the numerator, and then you have the possibility of using the exponent, so you have almost 2^32 possible values. – Rudy Velthuis Feb 20 '19 at 21:17
  • Some extra information to the comment of @Jojonete: (1) reduce `A/B` to an irreducible fraction is done using the greatest common divisor (`A/B =a/b` with `a=A/gcd(A,B)` and `b=B/gcd(A,B)`). (2) [check if `b` is a power of two](https://stackoverflow.com/questions/600293). (3) you have to fiddle around a bit with the exponent to figure out the range of `b` – kvantour Feb 21 '19 at 16:39
  • related: https://stackoverflow.com/questions/12124936/what-types-of-numbers-are-representable-in-binary-floating-point – kvantour Feb 21 '19 at 16:43

2 Answers2

4

The type Rational from the rug crate can represent fractions exactly. It also implements PartialEq<f32> so you can compare the exact representation with your f32 directly to check if they are equal.

for n in 1u8..=255u8 {
    let rat = Rational::from((n, 256));
    let f = (n as f32) / 256.0;
    println!("{}/256 -> {}", n, rat == f);
}

And as you can see from the output, the numbers you want to test are indeed exactly representable as f32.

To get more a more interesting output, try 1 / n:

for n in 1u8..=255u8 {
    let rat = Rational::from((1, n));
    let f = 1.0 / (n as f32);
    println!("1/{} -> {}", n, rat == f);
}

This shows that only fractions with a power-of-2 denominator are exactly representable.

Lukas Kalbertodt
  • 79,749
  • 26
  • 255
  • 305
  • Power-of-2 denominator is one rule. The other two rules are: the numerator has not too many bits (or it won't be representable in float significand) and the denominator power is not too high (causing float underflow). If denominator is 1, then numerator can have trailing 0 bits, but not too many (float overflow). See it implemented in Squeak Smalltalk http://source.squeak.org/trunk/Kernel-nice.853.diff. Here float are 64 bits, but that does not matter and is not hardcoded in the implementation of isAnExactFloat. – aka.nice Feb 24 '19 at 21:14
0

Do the calculations you want with higher precision (f64 is the obvious and fastest, but there are alternatives: e.g. f128, BigDecimal, rug's rational or float, etc.), and then check that the result is equal to itself converted to f32 and back.

Assuming f64 for the sake of example

d.is_finite() && (d as f32) as f64 == d

Of course, the result of these calculations may end up exactly representable as f32 even if the exact result wouldn't be, as Jojonete's comment points out. So the datatype you want will depend on the calculations. E.g. for

1/256, 2/256, 3/256, ... 254/256 and 255/256

rug::rational would certainly be exact (so would f64, but then you need to "think about how floating point numbers work and find out that way" at least a bit).

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487