Detect overflow when type casting Float to Integer

Here is my problem, I have a f32 value which I want to type cast to u8. In certain conditions the float value may go out of range (less than 0.0 or greater than 255.0), I want to panic in these conditions.

Using as u8 do not work because it silently accepts invalid values and truncates them. And try_from, from cannot be used as 'From<{f32}> is not implemented for u8'

Currently the way-around is to write a function for this. But I am not happy and want to know if the language provides something (without using external crates)?

fn checked_cast(v: f32) -> u8 {
    match v {
        u if u >= 0.0 && u <= 255.0 => v as u8,
        _ => panic!("Out of range: {v}")
    }
}
...
let ri = checked_cast(r * 255.0); // r is of type f32.

Thanks

I don't know how to do this in a more built-in way, but I would have written the check < 256.0 rather than <= 255.0 in order to maintain the truncate semantics of as u8:

    match v {
-        u if u >= 0.0 && u <= 255.0 => v as u8,
+        u if u >= 0.0 && u < 256.0 => v as u8,
        _ => panic!("Out of range: {v}")
    }
1 Like

If you absolutely want trait methods, you can use num_traits::NumCast. But honestly, I wouldn't pull in a dependency just for this (although do use it if you are already depending on num_traits for some other reason).

Oh, and (x.floor() as i16).try_into::<u8>() handles every case except NaN correctly, if you want an std-only approach.

4 Likes

Thanks. Now the checked_cast can be shortened to

fn checked_cast(v: f32) -> u8 {
    (v.floor() as i32).try_into().expect(&format!("Out of range: {}", v))
}

Well not really, 255.1 is still invalid.

Although I would write the assertion as .unwrap_or_else(|| panic!("out of range: {x}")), because currently it allocates a format string for every single conversion, unconditionally.

2 Likes

That depends on the application, so it's up to you really.

One thing I don't like with the original <= 255.0 check is that it lacks symmetry. There are lots of f32 values that map to 0u8, 1u8, ..., 254u8, but only exactly one value that maps to 255u8. In particular, everything between 254.0 and 254.9999847412109375 included will map to 254, only 255.0 will map to 255, and anything from 255.0000152587890625 and above will panic.

If that's what your application requires, that's fine. But keep in mind that also H2CO3's proposed solution works like the < 256.0 check:

fn checked_cast(v: f32) -> u8 {
    (v.floor() as i16)
        .try_into()
        .unwrap_or_else(|_| panic!("out of range: {v}"))
}

fn main() {
    assert_eq!(254, checked_cast(254.9999847412109375));
    assert_eq!(255, checked_cast(255.0));
    assert_eq!(255, checked_cast(255.1));
    assert_eq!(255, checked_cast(255.9));
    assert_eq!(255, checked_cast(255.9999847412109375));
    // checked_cast(256.0); // <-- panics with "out of range: 256"
}

(Playground link)

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.