rust-lang / rust-lang/libs-team

ACP: Missing floating point conversions

Open
#810 24 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

ACP-accepted api-change-proposal
Dominant language
Rust
Stars
178
Forks
28
Avg merge
15m
Merged PRs (30d)
1

Description

Proposal

Problem statement

Currently the default way when converting between integers and floats, or between floats is using as. However so many behaviors are encoded in this one little word. There long have been calls for dedicated functions which encode intent better.

Furthermore, as silently converts NaNs to zeroes, and does not offer fallible conversion (nor does TryInto, as it's not defined for floating point types).

Motivating examples or use cases

Honestly, these conversions are so ubiquitous and uncontroversial I don't think they need further motivation.

Solution sketch

I propose the following set of functions to be added to all primitive integer and floating point types respectively:

impl IntT {
    /// Converts this floating point value to the target floating point type,
    /// rounding as defined in IEEE-754.
    ///
    /// This is equivalent to `self as Flt`.
    fn to_float<Flt>(self) -> Flt
    where Self: IntToFloat<Flt>;
}

impl FloatT {
    /// Converts this floating point value to the target floating point type,
    /// rounding as defined in IEEE-754.
    /// 
    /// This is equivalent to `self as Flt`.
    fn to_float<Flt>(self) -> Flt
    where Self: FloatToFloat<Flt>;

    /// Rounds toward zero and converts to any primitive integer type, saturating
    /// at the target type's boundaries.
    ///
    /// # Panics
    /// 
    /// Panics if `self` is NaN.
    fn to_int_saturating<Int>(self) -> Int
    where Self: FloatToInt<Int>;
    
    /// Rounds toward zero and converts to any primitive integer type, returning
    /// None if the value is NaN, infinite or doesn't fit in the target type.
    fn to_int_checked<Int>(self) -> Option<Int>
    where Self: FloatToInt<Int>;
    
    /// Equivalent to `to_int_checked().unwrap()`.
    fn to_int_strict<Int>(self) -> Int
    where Self: FloatToInt<Int>;


    // Already exists, but shown for completeness:
    
    /// Rounds toward zero and converts to any primitive integer type, assuming
    /// that the value is finite and fits in that type.
    // unsafe fn to_int_unchecked<Int>(self) -> Int
    // where Self: FloatToInt<Int>;
}

These would use forever sealed (and potentially forever-unstable) methods from core::convert::{FloatToFloat, IntToFloat, FloatToInt}, the latter of which already exists.

Some notes:

  • We follow the principle of least surprise. Where reasonable these conversions follow as, with the only exception being NaNs, see the open question section.

  • This means that conversions to float use the IEEE-754 round-to-nearest-even with-same-exponent behavior. This can result in infinities being generated when converting a finite value to a small floating point type (e.g. f16), but this is generally expected behavior from floats.

  • Conversions from float to int first round-to-zero, and only then check for bounds. E.g. (255.5f64).to_int_checked::<u8>() == Some(255). This is also how to_int_unchecked is defined and leads to the most efficient and generally useful implementation.

  • Other roundings than round-to-zero for float -> int do not need to be supported. They would blow up the API surface a lot, are rarely useful, and any other rounding mode is trivially done by pre-rounding before the conversion, e.g. x.round().to_int_saturating(). This also matches most other languages, see the related work section.

Open questions

  • One potentially controversial choice I have made I believe is the choice for to_int_saturating() to panic on NaN. This is a departure from as, which silently converts NaN to 0. This choice makes some sense for a built-in infallible keyword, but I think for a library function the choice to panic on what is ultimately fundamentally an error value makes more sense. I think returning an Option would result in less readable code in almost all cases.

    Should one still want to convert NaN to zero (or any other specific value) you will be able to use the proposed and accepted nan_to API:

    x.nan_to(0.0).to_int_saturating()
    
  • Should fn to_int_wrapping<Int>(self) -> Int be added for floats? It is technically speaking well-defined for finite values, although I personally have never needed it. It would need to also panic on infinities, in addition to NaN.

Alternatives

None that I'm aware of, not even TryInto. Besides as, but obviously we're trying to replace that with clearer and safer alternatives.

Links and related work

I mainly followed the design of to_int_unchecked and extended it, also following my work on the integer casts (https://github.com/rust-lang/libs-team/issues/788).

I also took a look at what other languages do in their default float -> int conversion:

  • Python: int(x) truncates x to 0.
  • C/C++: (int) x truncates x to 0.
  • Java: (int) x truncates x to 0.
  • C#: (int) x truncates x to 0.
  • Go: int(x) truncates x to 0.
  • SQL: SELECT cast(x as int) truncates x to 0.
  • Visual Basic: it depends, Int(x) truncates to 0, CInt(x) rounds to nearest even.
  • R: as.integer(x) truncates x to 0.
  • PHP: intval(x) truncates x to 0.
  • Swift: Int(x) truncates x to 0.
  • Perl: int(x) truncates x to 0.
  • Kotlin: x.toInt() truncates x to 0.
  • Dart: x.toInt() truncates x to 0.
  • Scala: x.toInt() truncates x to 0.

Haskell and Julia were the two languages I looked at which do not have a 'default float to int' conversion, and require explicit methods.

I personally think this is sufficient prior art to feel comfortable in simply offering truncation as the only choice in to_int_ methods.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading the existing core::convert::FloatToInt trait and the design of to_int_unchecked, then review the proposed IntToFloat and FloatToFloat APIs. The issue leaves NaN behavior, wrapping conversions, and the final API surface open for discussion; done would require agreement on the design before implementation.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
api
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.