9

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

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
yong
  • 3,583
  • 16
  • 32

3 Answers3

11

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.

Chris Emerson
  • 13,041
  • 3
  • 44
  • 66
  • It seems you would need a way to obtain the lower and upper limits of the contiguous range of integral values that fits into a `f64` to be able to either clamp or raise an exception. Otherwise, because of undefined behavior, you cannot actually check the conversion. Maybe the language should expose [those](http://stackoverflow.com/questions/3793838/which-is-the-first-integer-that-an-ieee-754-float-is-incapable-of-representing-e). – Matthieu M. Dec 14 '16 at 09:57
  • I agree, in the general case. But there's probably a limit below those which makes sense for any (ok, most) given application. – Chris Emerson Dec 14 '16 at 10:05
  • Given that we are talking about 253, yep, there's probably a good limit! – Matthieu M. Dec 14 '16 at 10:08
  • This bug was finally fixed in Rust 1.45, released July 2020: https://blog.rust-lang.org/2020/07/16/Rust-1.45.0.html#whats-in-1450-stable – Jason Orendorff Aug 05 '22 at 16:38
7

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(..))
DK.
  • 55,277
  • 5
  • 189
  • 162
1

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.

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722