turboladen
turboladen

Reputation: 717

Is there a way to `f64::from(0.23_f32)` and get 0.23_f64?

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).

Upvotes: 3

Views: 1295

Answers (2)

Jmb
Jmb

Reputation: 23339

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.

Upvotes: 1

S&#233;bastien Renauld
S&#233;bastien Renauld

Reputation: 19662

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.

Upvotes: 3

Related Questions