0

I've been happily using the Truncate extension method as posted by @P Daddy here : https://stackoverflow.com/a/374470/4123190

static double TruncateToSignificantDigits(this double d, int digits){
    if(d == 0)
        return 0;

    double scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(d))) + 1 - digits);
    return scale * Math.Truncate(d / scale);
}

This mostly works as expected but I've hit a case where it fails, 999999999999999 to 3 digits gives 990000000000000. This is a fiddle showing the fail: https://dotnetfiddle.net/4tN7nv

For my specific use-case I need to use doubles and need to use the full range they provide. While the failure case looks to be caused by some floating point weirdness it clearly is possible to turn the number into a truncated version as converting it to a string, truncating that, then converting it back to a double would hold the expected result.

How can this be improved to work for all doubles?

Community
  • 1
  • 1
Ben Roberts
  • 199
  • 1
  • 8
  • I'm willing to bet that there's an intermediate step that can't be represented exactly as a double, and thus it's not giving you a result you're expecting. – jdphenix Aug 27 '16 at 03:37
  • 1
    Possible duplicate of [Is floating point math broken?](http://stackoverflow.com/questions/588004/is-floating-point-math-broken) – jdphenix Aug 27 '16 at 03:44
  • @jdphenix that seems like a reasonable bet but it doesn't answer the question ;) – Ben Roberts Aug 27 '16 at 14:40
  • **Why** doesn't it answer the question? – Lasse V. Karlsen Aug 27 '16 at 21:28
  • But it really is. You are depending on exact math on floating point numbers to do something, which is inherently broken because of how floating point numbers work. – jdphenix Aug 27 '16 at 23:22
  • The question is 'how do I improve this method to give expected output'. Clearly it's possible, at least for the failure case I listed, because you can do it with a string format. – Ben Roberts Aug 28 '16 at 20:05

2 Answers2

0

OK. I found out that it's a problem with the accuracy of the various math functions, so I was screwing with a bunch of complicated ways to get around the problem when I realized we could circumvent those issues by using string input as opposed to double input. The output is still double - only the input has been changed to string. So you would call the function using your value.ToString().

Note that this will only produce up to nine digits of accuracy. I figured that was more than enough since you were talking about truncating to three digits. If you need more than nine digits, let me know and I'll go back to the drawing board (it's possible).

Here is the modified method:

public void testTruncate()
    {

        for (int i = 3; i < 1000; i++)
        {
            string testNumber = "";
            if (Math.Log10(Math.Pow(10, i)) > 9)
            {
                testNumber = (Math.Pow(10, i) / Math.Pow(10, i - 9) - 1).ToString();
                for (int d = 0; d < Math.Log10(Math.Pow(10, i)) - testNumber.Length; d++)
                {
                    testNumber += "0";
                }
                if (double.Parse(testNumber) > double.MaxValue)
                {
                    break;
                }
            }
            else
            {
                 testNumber = (Math.Pow(10, i) - 1).ToString();
            }
          Console.WriteLine(testNumber+" truncated to "+  doTruncate(testNumber, 3, true).ToString("N0")+"\n");
        }
    }
    public double doTruncate(string bigNumString, double numberOfDigitsToTruncateTo, bool addZeroesBack)
    {
        if (bigNumString.Length <= numberOfDigitsToTruncateTo)
        {
            return double.Parse(bigNumString);
        }
        string ret = bigNumString.Substring(0,(int)numberOfDigitsToTruncateTo);
        if (addZeroesBack)
        {
            for (int i = 0; i < bigNumString.Length - numberOfDigitsToTruncateTo; i++)
            {
                ret += "0";
            }
        }
        double answer = double.Parse(ret);
        if (answer > double.MaxValue)
        {
            return -1;//value too large 
        }
        return answer;

    }

And below is the result of testTruncate from 999 through the limit of double.MaxValue

999 truncated to 999

9999 truncated to 9,990

99999 truncated to 99,900

999999 truncated to 999,000

9999999 truncated to 9,990,000

99999999 truncated to 99,900,000

999999999 truncated to 999,000,000

9999999990 truncated to 9,990,000,000

9999999990 truncated to 9,990,000,000

99999999900 truncated to 99,900,000,000

99999999900 truncated to 99,900,000,000

999999999000 truncated to 999,000,000,000

999999999000 truncated to 999,000,000,000

9999999990000 truncated to 9,990,000,000,000

9999999990000 truncated to 9,990,000,000,000

99999999900000 truncated to 99,900,000,000,000

99999999900000 truncated to 99,900,000,000,000

999999999000000 truncated to 999,000,000,000,000

999999999000000 truncated to 999,000,000,000,000

9999999990000000 truncated to 9,990,000,000,000,000

9999999990000000 truncated to 9,990,000,000,000,000

99999999900000000 truncated to 99,900,000,000,000,000

99999999900000000 truncated to 99,900,000,000,000,000

999999999000000000 truncated to 999,000,000,000,000,000

999999999000000000 truncated to 999,000,000,000,000,000

9999999990000000000 truncated to 9,990,000,000,000,000,000

9999999990000000000 truncated to 9,990,000,000,000,000,000

99999999900000000000 truncated to 99,900,000,000,000,000,000

99999999900000000000 truncated to 99,900,000,000,000,000,000

999999999000000000000 truncated to 999,000,000,000,000,000,000

999999999000000000000 truncated to 999,000,000,000,000,000,000

9999999990000000000000 truncated to 9,990,000,000,000,000,000,000

9999999990000000000000 truncated to 9,990,000,000,000,000,000,000

99999999900000000000000 truncated to 99,900,000,000,000,000,000,000

99999999900000000000000 truncated to 99,900,000,000,000,000,000,000

... and so on through ... 999999999000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 truncated to 999,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000

Shannon Holsinger
  • 2,293
  • 1
  • 15
  • 21
  • Thanks for trying - this has the same fail case as the code initially posted though (999999999999999). – Ben Roberts Aug 27 '16 at 14:50
  • Odd - it works for me, and the math is correct. It might be some machine-specific problem. Although there are a lot of Math calls, the math itself is very simple, there is no reason it should fail (and it doesn't on the two machines I've tested it on). It's like saying 2+2=5. – Shannon Holsinger Aug 27 '16 at 19:50
  • I do notice that, after a specific number, double will not recognize 999 but only 10^x. My results are: – Shannon Holsinger Aug 27 '16 at 20:03
  • To future readers - please note that the first few comments related to a different tact. The edited answer above works now. – Shannon Holsinger Aug 28 '16 at 11:45
  • Thanks for the updated version. Using ToString() will get us there but having to go double > string > substring > another string > double to do this is clearly crazy :) – Ben Roberts Aug 28 '16 at 20:30
  • I know, right? I can't remember but I'm pretty sure the culprit is in the precision of Math.Log10(). After nine sigDig, it rounds to the nearest bazillion. I guess they figure when you're that rich, a dollar doesn't mean much, but as we all know, every penny counts. Perhaps they'll fix in future update - might revisit the mathematical solution every now and then or keep your eye on the revision updates. Anyway I appreciate your accepting this answer. – Shannon Holsinger Aug 29 '16 at 01:10
0

Imho, you should not do that at all, because of the nature of a flat point number representation. If you need to round just to display in UI, then you have nice built-in ToString function which provides you the precision feature. If you need to compare two numbers approximately, you can normalize two numbers and round bits in significand field.

Serge Semenov
  • 9,232
  • 3
  • 23
  • 24
  • This isn't for UI and I don't need to compare two numbers approximately. – Ben Roberts Aug 27 '16 at 14:37
  • @Aurigan, can you explain then what the use case is? If it's really the only solution, maybe you can try to use `decimal` instead of `double` – Serge Semenov Aug 27 '16 at 14:45
  • I could ... but others clearly have the same need (a reliable way to truncate doubles) for a variety of use cases, should it really be up to the poster to justify a *use* for working code? doubles are needed because I need the range ... possibly this *can* be improved by using decimals for smaller input values, or as some intermediary step though. Thanks for the thought. – Ben Roberts Aug 27 '16 at 14:58