rust-lang / rfcs

RFCs for changes to Rust
https://rust-lang.github.io/rfcs/
Apache License 2.0
5.89k stars 1.56k forks source link

std/core feature request: Converting floats to ints without `as` #3304

Open DrSloth opened 2 years ago

DrSloth commented 2 years ago

Currently there are only two ways to cast floats to integers

  1. as casts
  2. to_int_unchecked

Adding explicit conversion functions could make casting a bit easier. I would propose the functions to_int_saturating and to_int_checked. Where to_int_saturating works like as conversions, and to_int_checked is a safe version of to_int_unchecked which returns an Option and does not accept any fractional part.

Examples:

// Saturating conversion
assert_eq!(257.64f64.to_int_saturating::<u8>(), 255);
assert_eq!(232.7f64.to_int_saturating::<u8>(), 232);

// Checked conversions
assert!(257.64f64.to_int_checked::<u8>().is_none());
assert!(232.74f64.to_int_checked::<i32>().is_none());
assert!(232.73f64.to_int_checked::<i32>().is_none());
assert_eq!(232.0f64.to_int_checked::<u8>(), Some(232));

I think the to_int_checked function would be a great new feature and using a more explicit version of as casts also helps. I think creating alterntives to as many as casts as possible is desirable.

With some pointers i might also be able to help implementing this, if there is interest in implementing this feature.

lebensterben commented 2 years ago

Your proposal would make 0.30000000000000004f64. to_int_checked::() a None.

But it's well known it should be 0.3.

DrSloth commented 2 years ago

I don't understand? What do you mean with 0.3 it is not an int anyways. The idea is that to_int_checked forces you to think about rounding yourself for instance with .ceil(), .floor(), .trunc() or whatever.

Even with really close floats like 2.999999999999999 it is not clear that it is exactly 3 doing something like (3.0-0.000000000000001).to_int_checked::<i32>() i wouldn't expect it to be 3 again. For me 2 would be more intuitive because rounding towards zero is common in other languages. This discrepancy is exactly why there should be a super strict version which just denies all values which aren't exactly convertible.

lebensterben commented 2 years ago

(0.1 + 0.2) * 10.0 = 3.0000000000000004

With to_int_checked you will get a None. That's counterintuitive.

thomcc commented 2 years ago

Yes, I think it's a reasonable concern that this encourages overly fragile code. Especially when floating point implementations do vary some in the wild (32bit x86 and arm both have "weird" float implementations, which are weird in different ways).

DrSloth commented 2 years ago

Working against this weirdness is exactly the point. To make my implementation uniform i have to think about using ceil, trunc, round or floor. Its better to force a "correct" implementation than implicitly accepting some arbitrary conversion. The to_int_saturating still exists and it will yield what you "would expect". I don't think its counterintuitive that an inaccurate calculation yields None when converted to an int, it just forces me to explicitly round.

Another model of more explicit float to int conversions would be functions like to_int_ceiled, to_int_truncated, to_int_rounded and to_int_floored. This would be even more explicit but adds more functions to std.

Ekleog commented 2 years ago

Another model of more explicit float to int conversions would be functions like to_int_ceiled, to_int_truncated, to_int_rounded and to_int_floored.

This would sound like a better idea to me, modulo bikeshedding on the names and the exact type of integer being returned — I've always felt uncomfortable writing .round() as uXX. The problem, I guess, would be to define properly what should happen when the float is outside the range of representable integers.

truppelito commented 2 years ago

This is a +1 for me. I would prefer .to_int_rounded() to .round() as XX.

Also, maybe we could have instead .into_rounded() that would convert into the correct integer type? Like the From/Into traits and the .into() function.

lebensterben commented 2 years ago

Also, maybe we could have instead .into_rounded() that would convert into the correct integer type? Like the From/Into traits and the .into() function.

This is agianst IEEE 754-2008, where it specifies multiple ways of converting floating point numbers to integers, instead of just one.

See 5.8 Details of conversions from floating-point to integer formats

truppelito commented 2 years ago

This is agianst IEEE 754-2008, where it specifies multiple ways of converting floating point numbers to integers, instead of just one.

Maybe I wasn't clear. I'm not proposing only one way to convert floats to ints, but instead a family of .into_xxx() functions. Taking the examples already provided from above: .into_rounded(), .into_ceiled(), .into_floored(), .into_truncated(). I.e. .into_xxx() instead of .to_int_xxx().

lebensterben commented 2 years ago

IEEE 754-2008 defined five ways of converting from floating-points to integers:

In Rust

When the number resulting from a primitive operation (addition, subtraction, multiplication, or division) on this type is not exactly representable as f32, it is rounded according to the roundTiesToEven direction defined in IEEE 754-2008.

So convertToIntegerTiesToEven(x) is implied.

For the other four operations required by the standard, they corresponds to


The aforementioned operations perform the conversion without signaling an exception.

But IEEE 754-2008 also requires operations that signal exceptions. That's what I think STD lacks of.

programmerjake commented 2 years ago

When the number resulting from a primitive operation (addition, subtraction, multiplication, or division) on this type is not exactly representable as f32, it is rounded according to the roundTiesToEven direction defined in IEEE 754-2008.

So convertToIntegerTiesToEven(x) is implied.

no, conversion to integer uses truncation, it is different than add, sub, mul, div, etc... https://doc.rust-lang.org/reference/expressions/operator-expr.html#numeric-cast

Casting from a float to an integer will round the float towards zero

lebensterben commented 2 years ago

@programmerjake

Thanks for pointing this out but I was confused here.

DrSloth commented 2 years ago

I guess returning an Option for some operations is inevitable. The floor and ceil could just return the nearest integer (saturating at integer bounds) maybe round too but trunc not really. I would like having a function that gives me an option if the float is outside the integer boundaries. When converting 357.65 to a u8 i would like be signaled that whatever my conversion is, it is not accurate not just in the sense of loosing the decimal point but in the sense of rounding at the integers boundaries, that is if the integer can be given by generics (e.g. by reusing the trait that to_int_unchecked uses) which i think would make sense.

programmerjake commented 2 years ago

I guess returning an Option for some operations is inevitable. The floor and ceil could just return the nearest integer (saturating at integer bounds) maybe round too but trunc not really.

i see no problem with having a saturating version of trunc, it would always return the nearest value of the return type that has magnitude <= the input value -- this gives the correct answer for all non-NaN inputs. trunc fp -> int doesn't need to return an Option -- it can if you want it to though. imho we should provide both versions -- saturating and checked.

DrSloth commented 2 years ago

Maybe providing something along the lines of to_int_exact would make sense. Then all other functions "just work" and give you the nearest int. With to_int_exact this would return an Option for any float which is not exactly an integer of the target size, you ofc have to use round, trunc etc. by yourself when using this function.