rfcs
rfcs copied to clipboard
std/core feature request: Converting floats to ints without `as`
Currently there are only two ways to cast floats to integers
-
as
casts -
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.
Your proposal would make 0.30000000000000004f64. to_int_checked::
But it's well known it should be 0.3.
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.
(0.1 + 0.2) * 10.0 = 3.0000000000000004
With to_int_checked you will get a None. That's counterintuitive.
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).
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.
Another model of more explicit float to int conversions would be functions like
to_int_ceiled
,to_int_truncated
,to_int_rounded
andto_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.
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.
Also, maybe we could have instead
.into_rounded()
that would convert into the correct integer type? Like theFrom
/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
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()
.
IEEE 754-2008 defined five ways of converting from floating-points to integers:
-
convertToIntegerTiesToEven(x)
roundsx
to the nearest integral value, with halfway cases rounded to even. -
convertToIntegerTowardZero(x)
roundsx
to an integral value toward zero. -
convertToIntegerTowardPositive(x)
roundsx
to an integral value toward positive infinity. -
convertToIntegerTowardNegative(x)
roundsx
to an integral value toward negative infinity. -
convertToIntegerTiesToAway(x)
roundsx
to the nearest integral value, with halfway cases rounded away from zero.
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
-
trunc
-
ceil
-
floor
-
round
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.
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
@programmerjake
Thanks for pointing this out but I was confused here.
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.
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.
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.