Double to string conversion without scientific notation
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));
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, 2021Comments
-
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
or1e-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 over 14 yearsSeeing your answer I must have misunderstood your question, sorry.
-
Lucero over 14 yearsNo, 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 over 14 yearsThe 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 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 over 14 yearsAdding 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 over 14 yearsFind a way to calculate 1e-200 on runtime instead of setting a "constant" value, i will vote it up.
-
Lucero over 14 yearsNo problem.
double x = 1.0; for (int i = 0; i < 200; i++) x /= 10; Console.WriteLine(x);
-
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 over 14 yearsBy the way can you add double x stuff with editing your answer? I cannot vote up (I want to).
-
Lucero over 14 yearsThat'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 over 14 yearsSo 1e-200 + 1e-199 can be converted to string by the method you provide. I got it. Thank you very very much.
-
Lucero over 14 yearsYou're welcome, and thank you for being open to the discussion. :)
-
JCasso over 14 yearsI always bow down to the knowledge :)
-
Lucero over 14 yearsThanks 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 over 14 yearsThanks for the input, I'll try to implement a fully working solution like this and compare it to mine.
-
Paul Sasik over 14 yearsHuh. 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 over 14 yearsNot yet, will do it soon... ;)
-
Lucero over 14 yearsYeah, 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 over 14 yearsThis one is more easily read, as you don't have to grok the regex.
-
Paul Sasik over 14 years+1 LOL @ "grok the regex" i love it. i will make it part of my development vernacular! Thanks.
-
Lucero over 14 yearsWell, the Regex one at least has nicely named groups instead of unspecific indexes in some arrays... ;)
-
Lucero over 14 yearsThis is the same as already posted by ebpower, see the comments there... ;)
-
Lucero over 13 yearsThat doesn't even compile, can you post something that compiles?
-
Lucero almost 13 years99 is not enough, and it has to work for both before and behind the comma.
-
Lucero over 12 yearsTry with the test numbers I gave above:
d = 1.5E200
andd = 1E-200
. The resulting string should have almost 2000
characters in it, or your solution doesn't work. -
Radu M. about 12 yearsstring test = ((double)0.00000007).ToString("f20"); the number (20) may vary
-
Julie in Austin about 12 yearsThe 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 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 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 to1e-10000
(which is out of range for doubles anyways) and not the wanted1e-200
. -
BrainSlugs83 about 11 yearsYou can also add more after the decimal place (i.e. "n8", or "n50", etc).
-
Grault about 9 years
TrimEnd('0')
is sufficient, because thechar
array isparams
. That is, anychar
s passed toTrimEnd
will be automatically grouped into an array. -
Ed Avis over 8 yearsI think you could simplify slightly by using the
InvariantCulture
on the initialToString
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 over 8 yearsAnother improvement would be to make an extension method:
public static class MyClass
and thenpublic static string ToLongString(this double input)
. That lets you calld.ToLongString()
in the same way you can calld.ToString()
. -
jnm2 over 8 years99 is not enough for a general-purpose solution.
doubleValue.ToString("0." + new string('#', 339))
is lossless. Compare these methods using the valuedouble.Epsilon
. -
jnm2 over 8 years9 decimal places is not enough for a general-purpose solution.
doubleValue.ToString("0." + new string('#', 339))
is lossless. Compare these methods using the valuedouble.Epsilon
. -
jnm2 over 8 yearsWhy wouldn't you use the simpler
doubleValue.ToString("0." + new string('#', 339))
? Less bugs, better performance. -
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 over 8 years@hannesRSA I'm not able to duplicate your benchmark. With
double.Epsilon
, yours is 7.85% slower thandoubleValue.ToString("0." + new string('#', 339))
and 11.44% slower thandoubleValue.ToString(constFormatString)
. Gist -
jnm2 over 8 yearsIn 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 fore-0
numbers. I still stand by the flexibility and simplicity of the fool-proof format string method, however. -
jnm2 over 8 yearsIn 64-bit apps, yours is 38.05% slower for
double.Epsilon
and 250.00% faster fore-0
. -
ygoe about 8 yearsNice 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 about 8 yearsThis does unfortunately not work for the given specification of very large or small numbers.
((decimal)(1e-200)).ToString()
for instance returns0
which is wrong. -
jnm2 about 8 yearsTo 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 below1e-28
and that your double is not large, both of which are not constraints in the original question. -
Snoop over 7 yearsThank you, worked perfectly. You are a wonderful human being. Upvoted.
-
JJJ about 7 yearsCould you explain briefly what this code does and how it's different from the other 15 or so answers?
-
kayess about 7 yearsWelcome 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 over 6 yearsThis is a pretty good solution given that you know the value range
-
Loathing about 6 yearsThis 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 about 6 yearsThis code should use
String strOrig = input.ToString("r");
-
Nick Vaccaro about 5 yearsHello! I've come here from 10 years in the future to let you know that the hyperlink to Jon's article has broken.
-
YantingChen about 3 yearsAccording to .NET documentation, double.ToString("G17") is better than double.ToString("r")
-
Loathing about 3 years@YantingChen I disagree about using
G17
. In their own example,0.6822871999174.ToString("G17")
outputs:0.68228719991739994
. -
Loathing about 3 yearsHere are two links discussing the issues with
double.Parse(...)
: github.com/dotnet/runtime/issues/4406 and github.com/dotnet/roslyn/issues/4221