Timmmm
Timmmm

Reputation: 96546

How to safely convert float to int in Rust

How can I safely convert a floating point type (e.g. f64) to and integer type (e.g. u64)? In other words I want to convert the number but only if it actually can be represented by the target type.

I found a couple of questions that don't cover this and are not duplicates:

The solution is not to use as - that performs a saturating cast. Also u64::try_from(f64) is not implemented.

The closest seems to be f64::to_int_unchecked() but unfortunately it's unsafe. You can easily check the first two safety requirements (not NaN or infinity), but the third is a bit tedious: Be representable in the return type Int, after truncating off its fractional part.

The best I can come up with is to use as to convert it back to f64 and check equality, i.e.

fn convert(x: f64) -> Option<u64> {
    let y = x as u64;
    if y as f64 == x {
        Some(y)
    } else {
        None
    }
}

Is that the best option? Is it implemented anywhere?

Upvotes: 16

Views: 3834

Answers (2)

Thomas
Thomas

Reputation: 181735

For fun, I made an implementation based on the raw f64 bits:

const F64_BITS: u64 = 64;
const F64_EXPONENT_BITS: u64 = 11;
const F64_EXPONENT_MAX: u64 = (1 << F64_EXPONENT_BITS) - 1;
const F64_EXPONENT_BIAS: u64 = 1023;
const F64_FRACTION_BITS: u64 = 52;

pub fn f64_to_u64(f: f64) -> Option<u64> {
    let bits = f.to_bits();
    let sign = bits & (1 << (F64_EXPONENT_BITS + F64_FRACTION_BITS)) != 0;
    let exponent = (bits >> F64_FRACTION_BITS) & ((1 << F64_EXPONENT_BITS) - 1);
    let fraction = bits & ((1 << F64_FRACTION_BITS) - 1);
    eprintln!("Input: {f}, bits: {bits:b}, sign: {sign}, exponent: {exponent}, fraction: {fraction}");
    match (sign, exponent, fraction) {
        (_, 0, 0) => {
            debug_assert!(f == 0.0);
            Some(0)
        },
        (true, _, _) => {
            debug_assert!(f < 0.0);
            None
        },
        (_, F64_EXPONENT_MAX, 0) => {
            debug_assert!(f.is_infinite());
            None
        },
        (_, F64_EXPONENT_MAX, _) => {
            debug_assert!(f.is_nan());
            None
        },
        (_, 0, _) => {
            debug_assert!(f.is_subnormal());
            None
        },
        _ => {
            if exponent < F64_EXPONENT_BIAS {
                debug_assert!(f < 1.0);
                None
            } else {
                let mantissa = fraction | (1 << F64_FRACTION_BITS);
                let left_shift = exponent as i64 - (F64_EXPONENT_BIAS + F64_FRACTION_BITS) as i64;
                if left_shift < 0 {
                    let right_shift = (-left_shift) as u64;
                    if mantissa & (1 << right_shift - 1) != 0 {
                        debug_assert!(f.fract() != 0.0);
                        None
                    } else {
                        Some(mantissa >> right_shift)
                    }
                } else {
                    if left_shift > (F64_BITS - F64_FRACTION_BITS - 1) as i64 {
                        debug_assert!(f > 2.0f64.powi(63));
                        None
                    } else {
                        Some(mantissa << left_shift)
                    }
                }
            }
        },
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn zero() {
        assert_eq!(f64_to_u64(0.0), Some(0));
        assert_eq!(f64_to_u64(-0.0), Some(0));
    }

    #[test]
    fn positive() {
        assert_eq!(f64_to_u64(1.0), Some(1));
        assert_eq!(f64_to_u64(2.0), Some(2));
        assert_eq!(f64_to_u64(3.0), Some(3));
        assert_eq!(f64_to_u64(2.0f64.powi(52)), Some(1 << 52));
        assert_eq!(f64_to_u64(2.0f64.powi(53)), Some(1 << 53));
        assert_eq!(f64_to_u64(2.0f64.powi(63)), Some(1 << 63));
        assert_eq!(f64_to_u64(1.5 * 2.0f64.powi(63)), Some(11 << 62));
        assert_eq!(f64_to_u64(1.75 * 2.0f64.powi(63)), Some(111 << 61));
    }

    #[test]
    fn too_big() {
        assert_eq!(f64_to_u64(2.0f64.powi(64)), None);
    }

    #[test]
    fn fractional() {
        assert_eq!(f64_to_u64(0.5), None);
        assert_eq!(f64_to_u64(1.5), None);
        assert_eq!(f64_to_u64(2.5), None);
    }

    #[test]
    fn negative() {
        assert_eq!(f64_to_u64(-1.0), None);
        assert_eq!(f64_to_u64(-2.0), None);
        assert_eq!(f64_to_u64(-3.0), None);
        assert_eq!(f64_to_u64(-(2.0f64.powi(f64::MANTISSA_DIGITS as i32))), None);
    }

    #[test]
    fn infinity() {
        assert_eq!(f64_to_u64(f64::INFINITY), None);
        assert_eq!(f64_to_u64(-f64::INFINITY), None);
    }

    #[test]
    fn nan() {
        assert_eq!(f64_to_u64(f64::NAN), None);
    }
}

Not sure whether this is useful. It's, ahem, slightly more complex than the solutions proposed so far. It may be faster on some hardware, but I doubt it, and haven't bothered to write a benchmark.

Upvotes: 1

Locke
Locke

Reputation: 8944

Generally, I would defer what is best practice to the relevant lints used by Clippy. Clippy does a good job outlining the possible pitfalls of using x as y and offers possible solutions. These are all of the relevant lints I could find on the subject:

However if all you want is to find an answer of mapping f64 onto u64 without any precision loss, there are two conditions you will want to check:

  • x is an integer
  • x is within the bounds of the target type
pub fn strict_f64_to_u64(x: f64) -> Option<u64> {
    // Check if fractional component is 0 and that it can map to an integer in the f64
    // Using fract() is equivalent to using `as u64 as f64` and checking it matches
    if x.fract() == 0.0 && x >= u64::MIN as f64 && x <= u64::MAX as f64 {
        return Some(x.trunc())
    }

    None
}

Upvotes: 0

Related Questions