4

I'm trying to write a conversion function that takes a float and returns an int, which basically does a saturated conversion. If the it's greater than int.MaxValue then int.MaxValue should be returned, likewise for int.MinValue.

I'd rather not catch exceptions as part of my normal flow control and instead just explicitly check the bounds except I'm not sure what the upper bound is as the largest float that can be stored in a int is smaller than int.MaxValue as a float has less precision than an int for values that size.

Basically I'm looking for the ... in:

float largestFloatThatCanBeStoredInAnInt = ...
Clinton
  • 22,361
  • 15
  • 67
  • 163
  • 2
    The highest value you can store that is *less than* `int.MaxValue` in a `float` is `2147483000f`. The largest *constant* `float` literal that you can use before it tips over is `2147483456f`, adding even a fraction to this will make it go over. I found these by experimenting so I cannot answer a *good* way to obtain these values. – Lasse V. Karlsen Jul 03 '19 at 08:33
  • 2
    @LasseVågsætherKarlsen I'm not sure that `2147483000f`is the largest float less than `int.MaxValue`. What about `2147483520f`? See [here](https://dotnetfiddle.net/3qxVWt) – Sebastian Schumann Jul 03 '19 at 09:13
  • I agree, that is even larger, I got as high as `2147483583f` before it tips over, I guess I had a bug in my previous code that tested this. This ends up as the bytes ff-ff-ff-4e, increasing the value by 1 goes to `00-00-00-4f` which is considered higher. And `3583` vs `3520` is only on the literal level, it is stored as `3520` either way it seems. – Lasse V. Karlsen Jul 03 '19 at 10:58
  • Related: [Can a conversion from double to int be written in portable C](https://stackoverflow.com/questions/51104995/can-a-conversion-from-double-to-int-be-written-in-portable-c/51107035#51107035). My answer there includes a C routine that calculates the biggest `double` less than a given `int` `x` plus 1, hence the biggest double less than or equal to `x`. But it relies on information provided by C, such as `DBL_MANT_DIG`. – Eric Postpischil Jul 03 '19 at 11:51

4 Answers4

3

Let's carry out an experiment:

  float best = 0f;

  for (int i = 2147483000; ; ++i)
  {
    float f = (float)i;

    try
    {
      checked
      {
        int v = (int)f;
      }

      best = f;
    }
    catch (OverflowException)
    {
      string report = string.Join(Environment.NewLine, 
        $"   max float = {best:g10}",
        $"min overflow = {f:g10}",
        $"     max int = {i - 1}");

      Console.Write(report);

      break;
    }
  }

The outcome is

   max float = 2147483520
min overflow = 2147483650
     max int = 2147483583

So we can conclude that the max float which can be cast to int is 2147483520. The max int which can be cast into float and back to int is 2147483583; if we try to cast 2147483583 + 1 = 2147483584 we'll get 2147483650f which will throw excpetion if we try to cast it back to int.

int overflow = 2147483583 + 1;
int back = checked((int)(float) overflow); // <- throws exception

or even

float f_1 = 2147483583f;        // f_1 == 2147483520f (rounding)
  int i_1 = checked((int) f_1); // OK

float f_2 = 2147483584f;        // f_2 == 2147483650f (rounding) > int.MaxValue
  int i_2 = checked((int) f_2); // throws exception

Finally, float to int conversion (no exceptions; int.MaxValue or int.MinValue if float is out of range):

  // float: round errors (mantissa has 23 bits only: 23 < 32) 
  public static int ClampToInt(this float x) =>
      x >  2147483520f ? int.MaxValue 
    : x < -2147483650f ? int.MinValue
    : (int) x;

  // double: no round errors (mantissa has 52 bits: 52 > 32)
  public static int ClampToInt(this double x) =>
      x > int.MaxValue ? int.MaxValue 
    : x < int.MinValue ? int.MinValue
    : (int) x;
Dmitry Bychenko
  • 180,369
  • 20
  • 160
  • 215
  • Thanks for that experiment. I got the same value using [this](https://www.h-schmidt.net/FloatConverter/IEEE754.html), input `2147483000` to get the exponent and activate all bits in mantissa. – Sebastian Schumann Jul 03 '19 at 09:23
2

I would suggest you just hard-code it as the right data type:

var largestFloatThatCanBeStoredInAnInt = 2147483000f;

2,147,483,000 is the highest value you can store in a float which is less than int.MaxValue

Jamiec
  • 133,658
  • 13
  • 134
  • 193
  • Actually, you *can* go higher, but it is a trick. If you use `2147483456f` it will still not go over but it will be stored as `...3000`, so it won't change the outcome. – Lasse V. Karlsen Jul 03 '19 at 08:38
  • 2
    Currently I'm of the opinion that `2147483520f` is the largest possible float. It was calculated using [this calculator](https://www.h-schmidt.net/FloatConverter/IEEE754.html) by setting all the bits in mantissa and use the same exponent like `2147483000f`. There might be a possibility that even larger values exist. – Sebastian Schumann Jul 03 '19 at 09:15
  • The largest value representable in a C# `float` that is less than `int.MaxValue` is exactly 2147483520. – Eric Postpischil Jul 03 '19 at 11:50
1

This approach works around the issue:

public static int ClampToInt(this float x)
{
    const float maxFloat = int.MaxValue;
    const float minFloat = int.MinValue;

    return x >= maxFloat ? int.MaxValue : x <= minFloat ? int.MinValue : (int) x;
}

The use of >= is important here. Use > only and you'll miss (float) int.MaxValue, and then when you do the ordinary cast you'll find (int) (float) int.MaxValue == int.MinValue which as a result would make this function return the wrong value.

Clinton
  • 22,361
  • 15
  • 67
  • 163
  • @TheGeneral maybe I've mucked it up. What's the counterexample that fails for this? – Clinton Jul 03 '19 at 08:57
  • 2
    Oh bugger this is more complicated than I thought – Clinton Jul 03 '19 at 09:56
  • Patch: `maxFloat = 2147483520f` and `minFloat = -2147483650f;` – Dmitry Bychenko Jul 03 '19 at 10:06
  • @DmitryBychenko the code I've got seems to not have a problem with your counter-example: https://ideone.com/We3IK4 . Can you explain under what circumstances it would be an issue? – Clinton Jul 04 '19 at 07:15
  • I don't understand your comment. Ideone seems to work fine even with your example. Are you saying this example throws locally but works on ideone? – Clinton Jul 04 '19 at 07:37
  • I very sorry, it's my misunderstanding. `int.MaxValue` will be cast into `2147483560f` as well as `2147483584` will be `2147483560f` (rounding up). Then since you've put `>=` the correct `int.MaxValue` will be returned. – Dmitry Bychenko Jul 04 '19 at 07:48
-1

Won't that work?

float largestFloatThatCanBeStoredInAnInt = (float)int.MaxValue - 1;

This expression is true:

(float)int.MaxValue - 1 < int.MaxValue
Ilya Chernomordik
  • 27,817
  • 27
  • 121
  • 207
  • I don't think so because `(int) (float) int.MaxValue != int.MaxValue` – Clinton Jul 03 '19 at 08:23
  • Well, you cannot represent any integer precisely with float, so you would just have to subtract some small number to be sure it is less that int.MaxValue, won't that do the trick? – Ilya Chernomordik Jul 03 '19 at 08:25
  • @IlyaChernomordik: I'm not sure what you mean by "Well, you cannot represent any integer precisely with float". You can't represent *every* integer precisely with float, but there are lots that you *can*. For example, 1f is precisely 1. – Jon Skeet Jul 03 '19 at 08:26
  • Floats only have like 24 bit integer portion, so once you get up to 30 bit integers the ints you can directly represent with floats become rare. – Clinton Jul 03 '19 at 08:28
  • 1
    @JonSkeet I just wrote it wrong in English, I mean "every" of course :) – Ilya Chernomordik Jul 03 '19 at 08:28
  • I have edited the answer a bit, there are probably some more precise floats, but would that difference matter? – Ilya Chernomordik Jul 03 '19 at 08:35
  • If I do this: `Console.WriteLine((float)int.MaxValue - 1 < int.MaxValue);` in LINQPad I get `False`. Actually, this is a compiler optimization as the compiled code is the same as `Console.WriteLine(false);`. – Lasse V. Karlsen Jul 03 '19 at 08:37
  • dotnetfiddle returns true, strange, probably version of .net means something. Actually 4.7.2 returns true and .net core returns false, that's really interesting :) – Ilya Chernomordik Jul 03 '19 at 08:38
  • If you switch dotnetfiddle to Roslyn it says False, so you're right, this must be a compiler difference. **However**, if you lift the values into variables, you'll see that it is false. One thing that perhaps changed here is that in-cpu floating point registers are actually higher precision than the memory storage layouts, which means that if you put all the values into the cpu it may be true, but if you adhere to "actually fits into a float type", it is false. – Lasse V. Karlsen Jul 03 '19 at 08:40
  • Since it is version dependent, probably the best way would be to subtract 1 until the expression becomes true? – Ilya Chernomordik Jul 03 '19 at 08:41
  • 2
    If you subtract `1f` from `2147484000f` you get `2147484000f`. – Lasse V. Karlsen Jul 03 '19 at 08:42
  • We can subtract 1, 2, 3, 4 in a row then to achieve the same goal? – Ilya Chernomordik Jul 03 '19 at 08:48