Double to string conversion without scientific notation

113,261

Solution 1

For a general-purpose¹ solution you need to preserve 339 places:

doubleValue.ToString("0." + new string('#', 339))

The maximum number of non-zero decimal digits is 16. 15 are on the right side of the decimal point. The exponent can move those 15 digits a maximum of 324 places to the right. (See the range and precision.)

It works for double.Epsilon, double.MinValue, double.MaxValue, and anything in between.

The performance will be much greater than the regex/string manipulation solutions since all formatting and string work is done in one pass by unmanaged CLR code. Also, the code is much simpler to prove correct.

For ease of use and even better performance, make it a constant:

public static class FormatStrings
{
    public const string DoubleFixedPoint = "0.###################################################################################################################################################################################################################################################################################################################################################";
}

¹ Update: I mistakenly said that this was also a lossless solution. In fact it is not, since ToString does its normal display rounding for all formats except r. Live example. Thanks, @Loathing! Please see Lothing’s answer if you need the ability to roundtrip in fixed point notation (i.e, if you’re using .ToString("r") today).

Solution 2

I had a similar problem and this worked for me:

doubleValue.ToString("F99").TrimEnd('0')

F99 may be overkill, but you get the idea.

Solution 3

This is a string parsing solution where the source number (double) is converted into a string and parsed into its constituent components. It is then reassembled by rules into the full-length numeric representation. It also accounts for locale as requested.

Update: The tests of the conversions only include single-digit whole numbers, which is the norm, but the algorithm also works for something like: 239483.340901e-20

using System;
using System.Text;
using System.Globalization;
using System.Threading;

public class MyClass
{
    public static void Main()
    {
        Console.WriteLine(ToLongString(1.23e-2));            
        Console.WriteLine(ToLongString(1.234e-5));           // 0.00010234
        Console.WriteLine(ToLongString(1.2345E-10));         // 0.00000001002345
        Console.WriteLine(ToLongString(1.23456E-20));        // 0.00000000000000000100023456
        Console.WriteLine(ToLongString(5E-20));
        Console.WriteLine("");
        Console.WriteLine(ToLongString(1.23E+2));            // 123
        Console.WriteLine(ToLongString(1.234e5));            // 1023400
        Console.WriteLine(ToLongString(1.2345E10));          // 1002345000000
        Console.WriteLine(ToLongString(-7.576E-05));         // -0.00007576
        Console.WriteLine(ToLongString(1.23456e20));
        Console.WriteLine(ToLongString(5e+20));
        Console.WriteLine("");
        Console.WriteLine(ToLongString(9.1093822E-31));        // mass of an electron
        Console.WriteLine(ToLongString(5.9736e24));            // mass of the earth 

        Console.ReadLine();
    }

    private static string ToLongString(double input)
    {
        string strOrig = input.ToString();
        string str = strOrig.ToUpper();

        // if string representation was collapsed from scientific notation, just return it:
        if (!str.Contains("E")) return strOrig;

        bool negativeNumber = false;

        if (str[0] == '-')
        {
            str = str.Remove(0, 1);
            negativeNumber = true;
        }

        string sep = Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator;
        char decSeparator = sep.ToCharArray()[0];

        string[] exponentParts = str.Split('E');
        string[] decimalParts = exponentParts[0].Split(decSeparator);

        // fix missing decimal point:
        if (decimalParts.Length==1) decimalParts = new string[]{exponentParts[0],"0"};

        int exponentValue = int.Parse(exponentParts[1]);

        string newNumber = decimalParts[0] + decimalParts[1];

        string result;

        if (exponentValue > 0)
        {
            result = 
                newNumber + 
                GetZeros(exponentValue - decimalParts[1].Length);
        }
        else // negative exponent
        {
            result = 
                "0" + 
                decSeparator + 
                GetZeros(exponentValue + decimalParts[0].Length) + 
                newNumber;

            result = result.TrimEnd('0');
        }

        if (negativeNumber)
            result = "-" + result;

        return result;
    }

    private static string GetZeros(int zeroCount)
    {
        if (zeroCount < 0) 
            zeroCount = Math.Abs(zeroCount);

        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < zeroCount; i++) sb.Append("0");    

        return sb.ToString();
    }
}

Solution 4

You could cast the double to decimal and then do ToString().

(0.000000005).ToString()   // 5E-09
((decimal)(0.000000005)).ToString()   // 0,000000005

I haven't done performance testing which is faster, casting from 64-bit double to 128-bit decimal or a format string of over 300 chars. Oh, and there might possibly be overflow errors during conversion, but if your values fit a decimal this should work fine.

Update: The casting seems to be a lot faster. Using a prepared format string as given in the other answer, formatting a million times takes 2.3 seconds and casting only 0.19 seconds. Repeatable. That's 10x faster. Now it's only about the value range.

Solution 5

This is what I've got so far, seems to work, but maybe someone has a better solution:

private static readonly Regex rxScientific = new Regex(@"^(?<sign>-?)(?<head>\d+)(\.(?<tail>\d*?)0*)?E(?<exponent>[+\-]\d+)$", RegexOptions.IgnoreCase|RegexOptions.ExplicitCapture|RegexOptions.CultureInvariant);

public static string ToFloatingPointString(double value) {
    return ToFloatingPointString(value, NumberFormatInfo.CurrentInfo);
}

public static string ToFloatingPointString(double value, NumberFormatInfo formatInfo) {
    string result = value.ToString("r", NumberFormatInfo.InvariantInfo);
    Match match = rxScientific.Match(result);
    if (match.Success) {
        Debug.WriteLine("Found scientific format: {0} => [{1}] [{2}] [{3}] [{4}]", result, match.Groups["sign"], match.Groups["head"], match.Groups["tail"], match.Groups["exponent"]);
        int exponent = int.Parse(match.Groups["exponent"].Value, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
        StringBuilder builder = new StringBuilder(result.Length+Math.Abs(exponent));
        builder.Append(match.Groups["sign"].Value);
        if (exponent >= 0) {
            builder.Append(match.Groups["head"].Value);
            string tail = match.Groups["tail"].Value;
            if (exponent < tail.Length) {
                builder.Append(tail, 0, exponent);
                builder.Append(formatInfo.NumberDecimalSeparator);
                builder.Append(tail, exponent, tail.Length-exponent);
            } else {
                builder.Append(tail);
                builder.Append('0', exponent-tail.Length);
            }
        } else {
            builder.Append('0');
            builder.Append(formatInfo.NumberDecimalSeparator);
            builder.Append('0', (-exponent)-1);
            builder.Append(match.Groups["head"].Value);
            builder.Append(match.Groups["tail"].Value);
        }
        result = builder.ToString();
    }
    return result;
}

// test code
double x = 1.0;
for (int i = 0; i < 200; i++) {
    x /= 10;
}
Console.WriteLine(x);
Console.WriteLine(ToFloatingPointString(x));
Share:
113,261
Lucero
Author by

Lucero

Long-time Senior Software Developer and now CTO for a company located in Basel, Switzerland. If you want to contact me, you can do so by e-mail avw at gmx dot ch

Updated on September 15, 2021

Comments

  • Lucero
    Lucero over 2 years

    How to convert a double into a floating-point string representation without scientific notation in the .NET Framework?

    "Small" samples (effective numbers may be of any size, such as 1.5E200 or 1e-200) :

    3248971234698200000000000000000000000000000000
    0.00000000000000000000000000000000000023897356978234562
    

    None of the standard number formats are like this, and a custom format also doesn't seem to allow having an open number of digits after the decimal separator.

    This is not a duplicate of How to convert double to string without the power to 10 representation (E-05) because the answers given there do not solve the issue at hand. The accepted solution in this question was to use a fixed point (such as 20 digits), which is not what I want. A fixed point formatting and trimming the redundant 0 doesn't solve the issue either because the max width for fixed width is 99 characters.

    Note: the solution has to deal correctly with custom number formats (e.g. other decimal separator, depending on culture information).

    Edit: The question is really only about displaing aforementioned numbers. I'm aware of how floating point numbers work and what numbers can be used and computed with them.

  • csharptest.net
    csharptest.net over 14 years
    Seeing your answer I must have misunderstood your question, sorry.
  • Lucero
    Lucero over 14 years
    No, first I don't want the thousand separator and second there seems to be always a fixed number of digits after the comma. See also MSDN help for N format: msdn.microsoft.com/en-us/library/dwhawy9k.aspx#NFormatString
  • Lucero
    Lucero over 14 years
    The exponent in the IEEE floating point numbers is 2-base, but the decimal numbers are 10-base. Therefore, this just doesn't work. This is also the reason why you cannot store 0.1 as exact value in a double. Or please just provide some sample (code) if you think that I misunderstood your answer.
  • JCasso
    JCasso over 14 years
    -1 since does not provide solution for the following stuation (and it cannot): double d1 = 1e-200; d = d + 1; ToFloatingPointString(d) just returns 1 here. Not 1,000...........000001.
  • Lucero
    Lucero over 14 years
    Adding one to a very small double is just your idea, and has nothing to do with the question at hand. If you just run it without the d=d+1, you'll see that it does in fact display 0.000.....0001.
  • JCasso
    JCasso over 14 years
    Find a way to calculate 1e-200 on runtime instead of setting a "constant" value, i will vote it up.
  • Lucero
    Lucero over 14 years
    No problem. double x = 1.0; for (int i = 0; i < 200; i++) x /= 10; Console.WriteLine(x);
  • JCasso
    JCasso over 14 years
    @Lucero: That's it. Works perfect. This post is also the answer of your question. I am sorry for the misinformation i provided. However I really don't understand why division works but putting 1 does not work here.
  • JCasso
    JCasso over 14 years
    By the way can you add double x stuff with editing your answer? I cannot vote up (I want to).
  • Lucero
    Lucero over 14 years
    That's because only 15 digits are in fact meaningful, but you can "shift" them with the exponent to be very large or very small. But you cannot add a very small number with a number which is more than about 15 digits larger, because doing so exceeds the number of significant digits and since the larger number is more significant, the small portion will be lost. Therefore, computing with numbers in a similar range (like adding 1e-200 and 1e-200, or 1+1, or 1e200+1e200) does work, but mixing such values will result in rounding the smaller value away.
  • JCasso
    JCasso over 14 years
    So 1e-200 + 1e-199 can be converted to string by the method you provide. I got it. Thank you very very much.
  • Lucero
    Lucero over 14 years
    You're welcome, and thank you for being open to the discussion. :)
  • JCasso
    JCasso over 14 years
    I always bow down to the knowledge :)
  • Lucero
    Lucero over 14 years
    Thanks for the link, I've tried the code from Jon already, however for my purpose it's kind of too exact; for instance, 0.1 does not show as 0.1 (which is technically correct, but not what I'd need)...
  • Lucero
    Lucero over 14 years
    Thanks for the input, I'll try to implement a fully working solution like this and compare it to mine.
  • Paul Sasik
    Paul Sasik over 14 years
    Huh. Honestly, i noticed that it got voted down so i didn't examine the code very closely. i did read it just now and you're right. They are close, i just chose to not use RegEx in my process and did my own string parsing. Have you tested this solution? It's a complete console app.
  • Lucero
    Lucero over 14 years
    Not yet, will do it soon... ;)
  • Lucero
    Lucero over 14 years
    Yeah, but you see, the whole point of Jon's code is to display the number EXACTLY and this is kind of too much for my case. Rounding as done by the runtime when doing ToString() is just fine for me, and that's probably also why most solutions proposed here use ToString() as base for further processing.
  • Gregory
    Gregory over 14 years
    This one is more easily read, as you don't have to grok the regex.
  • Paul Sasik
    Paul Sasik over 14 years
    +1 LOL @ "grok the regex" i love it. i will make it part of my development vernacular! Thanks.
  • Lucero
    Lucero over 14 years
    Well, the Regex one at least has nicely named groups instead of unspecific indexes in some arrays... ;)
  • Lucero
    Lucero over 14 years
    This is the same as already posted by ebpower, see the comments there... ;)
  • Lucero
    Lucero over 13 years
    That doesn't even compile, can you post something that compiles?
  • Lucero
    Lucero almost 13 years
    99 is not enough, and it has to work for both before and behind the comma.
  • Lucero
    Lucero over 12 years
    Try with the test numbers I gave above: d = 1.5E200 and d = 1E-200. The resulting string should have almost 200 0 characters in it, or your solution doesn't work.
  • Radu M.
    Radu M. about 12 years
    string test = ((double)0.00000007).ToString("f20"); the number (20) may vary
  • Julie in Austin
    Julie in Austin about 12 years
    The solution "double x = 1.0; for (int i = 0; i < 200; i++) x /= 10; Console.WriteLine(x);" suffers from impression in the result because cumulative division by an inexact value accumulates the errors in the result. A better solution is to reduce the number of divisions so that the cumulative error is reduced. For example, if you need to divide by powers of 10, create an array of powers of 10 by multiples of 10 -- 10, 1e10, 1e100, 1e1000, etc. -- then use those in the loop, decrementing the loop control variable according. For 1e-200, ((1.0 / 1e100) / 1e100) is exact. The other way isn't.
  • Lucero
    Lucero about 12 years
    @Julie, that's actually not a solution, it's just to prove that it is possible to get to numbers that small through computation (and not just by defining a constant value). You're of course right about the inexact value accumulation, but it is irrelevant for this example.
  • Lucero
    Lucero about 12 years
    @JulieinAustin, not at all stupid, but without enough knowledge about how floating-point numbers work. See the 3rd comment (written by JCasso). Also note that your computation is incorrect, ((1.0 / 1e100) / 1e100) would theoretically compute to 1e-10000 (which is out of range for doubles anyways) and not the wanted 1e-200.
  • BrainSlugs83
    BrainSlugs83 about 11 years
    You can also add more after the decimal place (i.e. "n8", or "n50", etc).
  • Grault
    Grault about 9 years
    TrimEnd('0') is sufficient, because the char array is params. That is, any chars passed to TrimEnd will be automatically grouped into an array.
  • Ed Avis
    Ed Avis over 8 years
    I think you could simplify slightly by using the InvariantCulture on the initial ToString call. That lets you assume the separator is . and makes sure your code still works even in truly weird locales where the decimal separator is more than one character long.
  • Ed Avis
    Ed Avis over 8 years
    Another improvement would be to make an extension method: public static class MyClass and then public static string ToLongString(this double input). That lets you call d.ToLongString() in the same way you can call d.ToString().
  • jnm2
    jnm2 over 8 years
    99 is not enough for a general-purpose solution. doubleValue.ToString("0." + new string('#', 339)) is lossless. Compare these methods using the value double.Epsilon.
  • jnm2
    jnm2 over 8 years
    9 decimal places is not enough for a general-purpose solution. doubleValue.ToString("0." + new string('#', 339)) is lossless. Compare these methods using the value double.Epsilon.
  • jnm2
    jnm2 over 8 years
    Why wouldn't you use the simpler doubleValue.ToString("0." + new string('#', 339))? Less bugs, better performance.
  • hannesRSA
    hannesRSA over 8 years
    @jnm2 Actually I tested that it is ~20% faster than doubleValue.ToString("0." + new string('#', 339)) for worst-case scenarios (E-320). For numbers not needing scientific notation, it is ~300% faster.
  • jnm2
    jnm2 over 8 years
    @hannesRSA I'm not able to duplicate your benchmark. With double.Epsilon, yours is 7.85% slower than doubleValue.ToString("0." + new string('#', 339)) and 11.44% slower than doubleValue.ToString(constFormatString). Gist
  • jnm2
    jnm2 over 8 years
    In fairness, the break-even point is numbers in the range e-255 which is surprising to me since you're running native code in both scenarios and managed code on top of that in your scenario. Native formatting is slower than your method for larger numbers; yours is 417.66% faster for e-0 numbers. I still stand by the flexibility and simplicity of the fool-proof format string method, however.
  • jnm2
    jnm2 over 8 years
    In 64-bit apps, yours is 38.05% slower for double.Epsilon and 250.00% faster for e-0.
  • ygoe
    ygoe about 8 years
    Nice and pretty short, but if you don't need extremely large values, you could do 10x faster. See my answer: stackoverflow.com/a/36204442/143684
  • Lucero
    Lucero about 8 years
    This does unfortunately not work for the given specification of very large or small numbers. ((decimal)(1e-200)).ToString() for instance returns 0 which is wrong.
  • jnm2
    jnm2 about 8 years
    To be fair and compare apples to apples, you should be comparing this method to double.ToString("0.############################"). According to my test, yours is only 3x faster. Either way it's only a valid answer if you know for sure that you don't need to print digits below 1e-28 and that your double is not large, both of which are not constraints in the original question.
  • Snoop
    Snoop over 7 years
    Thank you, worked perfectly. You are a wonderful human being. Upvoted.
  • JJJ
    JJJ about 7 years
    Could you explain briefly what this code does and how it's different from the other 15 or so answers?
  • kayess
    kayess about 7 years
    Welcome to Stack Overflow! While this code snippet may solve the question, including an explanation really helps to improve the quality of your post. Remember that you are answering the question for readers in the future, and those people might not know the reasons for your code suggestion. Please also try not to crowd your code with explanatory comments, this reduces the readability of both the code and the explanations!
  • Artur Udod
    Artur Udod over 6 years
    This is a pretty good solution given that you know the value range
  • Loathing
    Loathing about 6 years
    This solution is not "loseless". Example: String t1 = (0.0001/7).ToString("0." + new string('#', 339)); // 0.0000142857142857143 versus: String t2 = (0.0001/7).ToString("r"); // 1.4285714285714287E-05 Precision is lost at the ending decimal places.
  • Loathing
    Loathing about 6 years
    This code should use String strOrig = input.ToString("r");
  • Nick Vaccaro
    Nick Vaccaro about 5 years
    Hello! I've come here from 10 years in the future to let you know that the hyperlink to Jon's article has broken.
  • YantingChen
    YantingChen about 3 years
    According to .NET documentation, double.ToString("G17") is better than double.ToString("r")
  • Loathing
    Loathing about 3 years
    @YantingChen I disagree about using G17. In their own example, 0.6822871999174.ToString("G17") outputs: 0.68228719991739994.
  • Loathing
    Loathing about 3 years
    Here are two links discussing the issues with double.Parse(...): github.com/dotnet/runtime/issues/4406 and github.com/dotnet/roslyn/issues/4221