0

Calling ToString() on imprecise floats produces numbers a human would expect (eg. 4.999999... gets printed as 5). However, when casting to int, the fractional part gets dropped and the result is an integer decremented by 1.

float num = 5f;
num *= 0.01f;
num *= 100f;
num.ToString().Dump(); // produces "5", as expected
((int)num).ToString().Dump(); // produces "4"

How do I cast the float to int, so that I get the human friendly value, that float.ToString() produces?

I'm using this dirty hack:

int ToInt(float value) {
    return (int)(value + Math.Sign(value) * 0.00001);
}

..but surely there must be a more elegant solution.


Edit: I'm well aware of the reasons why floats are truncated the way they are (4.999... to 4, etc.) The question is about casting to int while emulating the default behavior of System.Single.ToString()

To better illustrate the behavior I'm looking for:

-4.99f should be cast to -4
4.01f should be cast to 4
4.99f should be cast to 4
4.999999f should be cast to 4
4.9999993f should be cast to 5

This is the exact same behavior that ToString produces.

Try running this:

float almost5 = 4.9999993f;
Console.WriteLine(almost5); // "5"
Console.WriteLine((int)almost5); // "4"
blade
  • 12,057
  • 7
  • 37
  • 38
  • 1
    How about `Math.Ceiling`? – FindOutIslamNow May 02 '18 at 10:53
  • If the float is 4.9375, what do you want printed, “4” or “5”? Or will that never occur because the float will never be that far from an integer? How far can it be from an integer? At what point do you want the result to change from “5” to “4”? – Eric Postpischil May 02 '18 at 10:55
  • @FindOutIslamNow: Do you think OP wants a value slightly over 5 to print as “6”? – Eric Postpischil May 02 '18 at 10:56
  • I guess you are looking for `(int) Math.Round(num)` – Evk May 02 '18 at 10:57
  • 1
    Use Math.Round --- `(int)Math.Round(num)).ToString()`. The Math.Round function rounds a float value to the nearest integer, and rounds midpoint values to the nearest even number. – user1672994 May 02 '18 at 10:57
  • Possible duplicate of [Why is floating point arithmetic in C# imprecise?](https://stackoverflow.com/questions/753948/why-is-floating-point-arithmetic-in-c-sharp-imprecise) – mjwills May 02 '18 at 12:12
  • I've edited the question and added examples of the behavior I'm looking for. The question is not about floats being imprecise. It's about dealing with the imprecision gracefully, like Single.ToString does. I'm not looking for the standard Math.Round(). – blade May 02 '18 at 12:51
  • So you want 4.xxxxxxxxxxxx rounded to the nearest 0.0000001. How about `49.999993f` should that be 50.00000 or 49.99999? – chux - Reinstate Monica May 02 '18 at 13:06
  • `Console.WriteLine(49.999993f);` prints `49.99999`. I'm not sure where the threshold is and that's really the point of the question. How does `ToString()` do the conversion? Edit: I've tried Single.Epsilon, but that's not it either. – blade May 02 '18 at 13:11
  • @blade Convert the float to a string and then to an int, which will ignore the fraction. – chux - Reinstate Monica May 02 '18 at 13:49
  • @chux That would certainly work, but it's also a very hacky solution. – blade May 02 '18 at 13:54
  • @blade OK call it a reference function then and find ways to improve it. At least it is clear and unambiguous as to your goal. With `almost5 = 4.9999993f` you are experiencing [double rounding](https://en.wikipedia.org/wiki/Rounding#Double_rounding). `4.9999993f` is not one of the 2^32 different representable `float`. The 2 closest are 4.9999990463... and 4.9999995232... with 4.9999995232... being closer (1st round). So its really `almost5 = 4.9999995232f` you are doing and `WriteLine()` is apparently rounding to 7 digits., thus 5. (2nd round) – chux - Reinstate Monica May 02 '18 at 14:02
  • @chux Are you certain about rounding to 7 decimals? When printing a double `4.9999999999`, it doesn't get rounded. Or does the 7 digit rounding apply only to floats? If so, this could be the solution, although I'm not sure how well it deals with imprecisions: `(int)Math.Round(value * 1000000) / 1000000`? – blade May 02 '18 at 14:40
  • @blade I am uncertain about C# details. Recall `printing a double 4.9999999999` is really trying to print 4.99999999989999999173..., not 4.9999999999. `(int)Math.Round(...)` fails for `float()` outside the `int` range. – chux - Reinstate Monica May 02 '18 at 14:52
  • @chux That's a good point. Thanks for the inputs, I think I'll stick with my original solution, while adjusting the error margin value to be 0.000001f. This works for every float I throw at it. Parsing the string would work also, but it's significantly slower. – blade May 02 '18 at 15:06
  • There are 2^32 different `float` - likely a lot more than "every float I throw at it". You can test _every_ float with your speedy solution against the slow "Parsing the string" approach to validate its correctness. – chux - Reinstate Monica May 02 '18 at 16:11

3 Answers3

2

Maybe you are looking for this:

Convert.ToInt32(float)

source

apocalypse
  • 5,764
  • 9
  • 47
  • 95
  • This seems to be the equivalent of `Math.Round()`, which is not what I'm looking for. I've clarified the question. – blade May 02 '18 at 12:46
1

0.05f * 100 is not exactly 5 due to floating point rounding (it's actually the value 4.999998.... when expressed as a float

The answer is that in the case of (int)(.05f * 100), you are taking the float value 4.999998 and truncating it to an integer, which yields 4.

So use Math.Round. The Math.Round function rounds a float value to the nearest integer, and rounds midpoint values to the nearest even number.

    float num = 5f;
    num *= 0.01f;
    num *= 100f;
    Console.WriteLine(num.ToString());
    Console.WriteLine(((int)Math.Round(num)).ToString());
user1672994
  • 10,509
  • 1
  • 19
  • 32
0

So far the best solution seems to be the one from the question.

int ToInt(float value) {
    return (int)(value + Math.Sign(value) * 0.000001f);
}

This effectively snaps the value to the closest int, if the difference is small enough (less than 0.000001). However, this function differs from ToString's behavior and is slightly more tolerant to imprecisions.

Another solution, suggested by @chux is to use ToString and parse the string back. Using Int32.Parse throws an exception when a number has a decimal point (or a comma), so you have to keep only the integer part of the string and it may cause other troubles depending on your default CultureInfo.

blade
  • 12,057
  • 7
  • 37
  • 38