yong
yong

Reputation: 3633

Rounding a f64 to nearest i64 in Rust

Rust's f64 type provides the function round(), which rounds to the nearest integer, but it returns a f64. Java's Math.round(double), on the other hand, returns a long. I can call round() and then cast to i64, but will this guarantee that I get the correct result? Here, "correct" means getting the closest i64 — Java's round() returns the "closest long".

Upvotes: 12

Views: 8213

Answers (3)

Matthieu M.
Matthieu M.

Reputation: 299969

Here is a simple "back of the envelope" implementation:

const INTEGRAL_LIMIT: f64 = 9007199254740992.0;

#[derive(Debug, PartialEq, Eq)]
enum Error {
    NaN,
    Overflow,
    Underflow,
}

fn try_from(f: f64) -> Result<i64, Error> {
    let f = f.round();

    if f.is_nan() { return Err(Error::NaN); }

    if f < -INTEGRAL_LIMIT { return Err(Error::Underflow); }
    if f > INTEGRAL_LIMIT { return Err(Error::Overflow); }

    Ok(f as i64)
}

And it comes with a minimal test suite which passes:

fn main() {
    assert_eq!(try_from(std::f64::NAN), Err(Error::NaN));

    assert_eq!(try_from(std::f64::NEG_INFINITY), Err(Error::Underflow));
    assert_eq!(try_from(-9007199254740994.0), Err(Error::Underflow));

    assert_eq!(try_from( 9007199254740994.0), Err(Error::Overflow));
    assert_eq!(try_from(std::f64::INFINITY), Err(Error::Overflow));

    assert_eq!(try_from(-INTEGRAL_LIMIT), Ok(-9007199254740992));
    assert_eq!(try_from( INTEGRAL_LIMIT), Ok( 9007199254740992));
}

I was actually expecting a TryFrom implementation to be available, but found none.

Upvotes: 1

DK.
DK.

Reputation: 59075

You can use the conv crate for this:

use conv::prelude::*;

let x = 9_223_371_487_098_961_920i64 as f64;
println!("{:?}", x.approx_as_by::<i64, RoundToNearest>());
// Ok(9223371487098962944)

let x = 9_223_372_036_854_775_807i64 as f64;
println!("{:?}", x.approx_as_by::<i64, RoundToNearest>());
// Err(FloatError::PosOverflow(..))

Upvotes: 8

Chris Emerson
Chris Emerson

Reputation: 14051

From the book, conversions from floating point to integer types round towards zero, so rounding first is nearly correct: f.round() as i64.

However, it's also currently undefined behaviour (but this is a bug) if the f64 is out of range (huge magnitude) of i64. Therefore you should clamp the value first (or possibly better, raise an error or assert). The possibly obvious answer doesn't work:

f.max(std::i64::MIN as f64).min(std::i64::MAX as f64).round() as i64

because the conversions of i64::MAX to f64 aren't exact, and applying the above to 1e100 ends up with a large negative value (in my test; as mentioned it's actually undefined).

The best option seems to be to return an error of some if the floating point value is out of the reasonable range your application expects.

Upvotes: 12

Related Questions