3

I'm trying to tie together two pieces of software: one that gives me a f32, and one that expects f64 values. In my code, I use f64::from(my_f32), but in my test, I compare the outcome and the value that I'm comparing has not been converted as expected: the f64 value has a bunch of extra, more precise, digits, such that the values aren't equal.

In my case, the value is 0.23. Is there a way to convert the 0.23_f32 to f64 such that I end up with 0.23_f64 instead of 0.23000000417232513?

fn main() {
    let x = 0.23_f32;
    println!("{}", x);
    println!("{}", f64::from(x));
    println!("---");

    let x = 0.23_f64;
    println!("{}", x);
    println!("{}", f64::from(x));
}

Playground


Edit: I understand that floating-point numbers are stored differently--in fact, I use this handy visualizer on occasion to view the differences in representations between 32-bit and 64-bit floats. I was looking to see if there's some clever way to get around this.


Edit 2: A "clever" example that I just conjured up would be my_32.to_string().parse::<f64>()--that gets me 0.23_f64, but (obviously) requires string parsing. I'd like to think there might be something at least slightly more numbers-related (for lack of a better term).

turboladen
  • 698
  • 1
  • 9
  • 14
  • 3
    [Floating-precision numbers are not stored as exact values but as 2^n closest representation](https://stackoverflow.com/questions/7644699/how-are-floating-point-numbers-stored-in-memory), and this is not something you can change outside of option for an arbitrary-precision library and its types. – Sébastien Renauld Sep 30 '19 at 20:09
  • 1
    Possible duplicate of [Is floating point math broken?](https://stackoverflow.com/questions/588004/is-floating-point-math-broken) – ecstaticm0rse Sep 30 '19 at 20:13
  • The question is not well-formed. It's not clear what process you want to implement that lets you transform `0.23_f32` into `0.23_f64`, which are different values with no particular relationship *other* than that they happen to round to the same value in decimal representation of less than a certain number of digits. – trent Sep 30 '19 at 22:13
  • Thanks for the feedback, @trentcl. I'll update to try to convey that, although I'm not really sure how at the moment to convey it any differently. – turboladen Oct 01 '19 at 00:27
  • Why do you want that? Why don't you just accept the more precisely represented `f64` value? It's the same value as the `f32` value, the issue is that in `f64` there is a different value even closer to `0.23`. – starblue Oct 01 '19 at 05:34
  • @starblue the `f32` data is directly from a database that's full of canonical values for my business (which have legal implications for our users) but the tools I have to work with the data require `f64` values. I need `0.23` as an `f64` (not `0.23000000417232513`) to be able to use the value with other libraries I have in my stack. – turboladen Oct 01 '19 at 18:58
  • 1
    @turboladen If you know the maximal number of decimal digits you might get away with rounding as at the end of Jmb's answer. Otherwise it boils down to what those legal implications are. If legally the floating-point numbers are thought of as their textual representation rather than their floating-point value then reparsing might be the right thing to do. – starblue Oct 03 '19 at 10:18
  • @starblue that's a great point. I started a discussion to see if we can define a maximal number of decimal digits (and even switching to fixed-point numbers). The origin of the floating point (as it comes into our system) is probably thought of as somewhat textual (or maybe "thought about non-technically"), so that makes sense. Thanks for that--that helps. – turboladen Oct 04 '19 at 17:48

2 Answers2

3

Comments have already pointed out why this is happening. This answer exists to give you ways to circumvent this.

The first (and most obvious) is to use arbitrary-precision libraries. A solid example of this in rust is rug. This allows you to express pretty much any number exactly, but it causes some problems across FFI boundaries (amongst other cases).

The second is to do what most people do around floating point numbers, and bracket your equalities. Since you know that most floats will not be stored exactly, and you know your input type, you can use constants such as std::f32::MIN to bracket your type, like so (playground):

use std::cmp::PartialOrd;
use std::ops::{Add, Div, Sub};
fn bracketed_eq<
    I,
    E: From<I> + From<f32> + Clone + PartialOrd + Div<Output = E> + Sub<Output = E> + Add<Output = E>,
>(
    input: E,
    target: I,
    value: I,
) -> bool {
    let target: E = target.into();
    let value: E = value.into();
    let bracket_lhs: E = target.clone() - (value.clone() / (2.0).into());
    let bracket_rhs: E = target.clone() + (value.clone() / (2.0).into());
    bracket_lhs >= input && bracket_rhs <= input
}

#[test]
fn test() {
    let u: f32 = 0.23_f32;
    assert!(bracketed_eq(f64::from(u), 0.23, std::f32::MIN))
}

A large amount of this is boilerplate and a lot of it gets completely optimized away by the compiler; it is also possible to drop the Clone requirement by restricting some trait choices. Add, Sub, Div are there for the operations, From<I> to realize the conversion, From<f32> for the constant 2.0.

Cerberus
  • 8,879
  • 1
  • 25
  • 40
Sébastien Renauld
  • 19,203
  • 2
  • 46
  • 66
0

The right way to compare floating-point values is to bracket them. The question is how to determine the bracketing interval? In your case, since you have a representation of the target value as f32, you have two solutions:

  • The obvious solution is to do the comparison between f32s, so convert your f64 result to f32 to get rid of the extra digits, and compare that to the expected result. Of course, this may still fail if accumulated rounding errors cause the result to be slightly different.

  • The right solution would have been to use the next_after function to get the smallest bracketing interval around your target:

    let result: f64 = 0.23f64;
    let expect: f32 = 0.23;
    
    assert_ne!(result, expect.into());
    assert!(expect.next_after (0.0).into() < result && result < expect.next_after (1.0).into());
    

    but unfortunately this was never stabilized (see #27752).

  • So you will have to determine the precision that is acceptable to you, possibly as a function of f32::EPSILON:

    let result: f64 = 0.23f64;
    let expect: f32 = 0.23;
    
    assert_ne!(result, expect.into());
    assert!(f64::from (expect) - f64::from (std::f32::EPSILON) < result && result < f64::from (expect) + f64::from (std::f32::EPSILON);
    

If you don't want to compare the value, but instead want to truncate it before passing it on to some computation, then the function to use is f64::round:

const PRECISION: f64 = 100.0;
let from_db: f32 = 0.23;
let truncated = (f64::from (from_db) * PRECISION).round() / PRECISION;
println!("f32   : {:.32}", from_db);
println!("f64   : {:.32}", 0.23f64);
println!("output: {:.32}", truncated);

prints:

f32   : 0.23000000417232513427734375000000
f64   : 0.23000000000000000999200722162641
output: 0.23000000000000000999200722162641

A couple of notes:

  • The result is still not equal to 0.23 since that number cannot be represented as an f64 (or as an f32 for that matter), but it is as close as you can get.
  • If there are legal implications as you implied, then you probably shouldn't be using floating point numbers in the first place but you should use either some kind of fixed-point with the legally mandated precision, or some arbitrary precision library.
Jmb
  • 18,893
  • 2
  • 28
  • 55