Skip to content

ACP: Add float constants for min/max limits of consecutive integers that convert to unique floats #713

@okaneco

Description

@okaneco

Proposal

Problem statement

Some ranges of integers can be losslessly round-tripped to and from floating point values but the standard library doesn't easily facilitate doing that.

From<T> for f32 exists for u8, i8, u16, and i16 but TryFrom implementations don't exist for larger integers and are unlikely to be implemented.
as is needed to cast to floating point values but that doesn't offer insight into whether the conversion was lossy or not, likewise for float to integer.

Motivating examples or use cases

These constants can be used to determine whether a value is in range to not lose precision when converting between float/int.

fn to_float_exact(x: u32) -> Option<f32> {
    if x <= f32::MAX_INT.cast_unsigned() {
        Some(x as f32)
    } else {
        None
    }
}

fn to_int_exact(x: f32) -> Option<i32> {
    if x.abs() <= f32::MAX_INT_AS_FLOAT && x.trunc() == x {
        unsafe { Some(x.to_int_unchecked()) }
    } else {
        None
    }
}

Above this range, more than one integer may be rounded to the same floating point value.
For example, 2i32.pow(24) as f32 == (2i32.pow(24) + 1) as f32 evaluates to true.

Solution sketch

Taking f32 as an example, the consecutive range of integers which can be represented exactly in floating point and only have one unique mapping from integer to float consists of the set from -16777215 to 16777215 inclusively, or from -224+1 to 224-1 (with 24 being the mantissa digits constant).

While 16777216 can be exactly represented, both 16777216_i32 and 16777217_i32 will convert to 16777216.0.

This definition aligns with the Javascript definition of Number.MAX_SAFE_INTEGER and MIN_SAFE_INTEGER (which would correspond to f64).

impl fN {
    pub const MAX_UNIQUE_INTEGER: iN = (1 << $float::MANTISSA_DIGITS) - 1;
    pub const MIN_UNIQUE_INTEGER: iN = -Self::MAX_UNIQUE_INTEGER;

    // Alternatively, this could be represented as a range::RangeInclusive<Self>
    pub const MAX_UNIQUE_INTEGER_AS_FLOAT: fN = Self::MAX_UNIQUE_INTEGER as Self;
    pub const MIN_UNIQUE_INTEGER_AS_FLOAT: fN = Self::MIN_UNIQUE_INTEGER as Self;
}

Naming is open for discussion.

Alternatives

  1. Do nothing.
  2. Work on one of the various trait proposals for num conversion in the standard library.
  3. Additionally, provide "exact" converting functions between iN/uN and fN using these constants.

Links and related work

Names for this concept in some other languages:

Enumeration of how integers are represented when converted to float and the rounding that occurs between exponent ranges
https://en.wikipedia.org/wiki/Single-precision_floating-point_format#Precision_limitations_on_integer_values

The accepted answer here is wrong or incomplete, but the user udoprog provides the proper context for doing this correctly as shown in the playground link
Check if a float can be converted to integer without loss - https://stackoverflow.com/a/48500414
https://play.rust-lang.org/?version=stable&mode=debug&edition=2015&gist=6ccbd5d51c8f2ae11f3b3569ecd0ff55

Internals thread that prompted me to finally write this
https://internals.rust-lang.org/t/pre-acp-tryfrom-for-exact-float-int-conversions/23828

[RFC] Conversions: FromLossy and TryFromLossy traits
rust-lang/rfcs#2484

What happens now?

This issue contains an API change proposal (or ACP) and is part of the libs-api team feature lifecycle. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.

Possible responses

The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):

  • We think this problem seems worth solving, and the standard library might be the right place to solve it.
  • We think that this probably doesn't belong in the standard library.

Second, if there's a concrete solution:

  • We think this specific solution looks roughly right, approved, you or someone else should implement this. (Further review will still happen on the subsequent implementation PR.)
  • We're not sure this is the right solution, and the alternatives or other materials don't give us enough information to be sure about that. Here are some questions we have that aren't answered, or rough ideas about alternatives we'd want to see discussed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-libs-apiapi-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions