3

What is the maximum number of consecutive non-leading non-trailing zeros (resp. nines) in the exact decimal representation of an IEEE 754 double-precision number?

Context

Consider the problem of converting a double to decimal, rounding up (resp. down), when the only primitive you are able to use is an existing function that converts to the nearest (correctly rounded to any desired number of digits).

You could get a few additional digits and remove them yourself. For instance, to round 1.875 down to one digit after the dot, you could convert it to the nearest decimal representation with two or three digits after the dot (1.87 or 1.875) and then erase the digits yourself in order to obtain the expected answer, 1.8.

For some numbers and choices of an additional number of digits to print, this method produces the wrong result. For instance, for the double nearest to 0.799999996, converting to decimal, rounding to the nearest, to 2, 3 or 4 digits after the dot produces 0.80, 0.800 and 0.8000. Erasing the additional digits after the conversion produces the result 0.8, when the desired result was 0.7.

There being a finite number of double, there exists a number of additional digits that it is enough to print in the initial conversion in order to always compute the correct result after truncation of the obtained decimal representation. This number is related to the maximal number of nines or zeros that can occur in the exact decimal representation of a double.

Related

This question is related to this question about rounding down in the conversion of a double to decimal, and is dual of this question about the correctly rounded conversion of decimal representations to doubles.

Community
  • 1
  • 1
Pascal Cuoq
  • 79,187
  • 7
  • 161
  • 281
  • How do you keep coming up with these hard questions about floating-point arithmetic? :) – tmyklebu Nov 28 '13 at 20:48
  • @tmyklebu I am not enough of a mathematician to answer most of the interesting questions about floating-point myself, but I am attracted to the subject for the way it connects bit-twiddling and arithmetic. Some of the interesting floating-point results, that hold for complicated reasons, have simple, memorable expressions that are just as fun to collect as trading cards. – Pascal Cuoq Nov 28 '13 at 22:31
  • How many decimal digits can a floating point represent (almost 16). You need at least one "real" digit, so that would say 15 zeros, maybe only 14 if the "real" digit is 9. (Although @tmyklebu points out that there may be others that exceed the resolution of the IEEE number but "work out" with large exponents.) – Hot Licks Nov 28 '13 at 22:46
  • 1
    @HotLicks The exact decimal representation of a `double` number can have more than 750 digits. You can only “choose” the first 16 or so, but the others are there and are revealed when using a correctly rounded conversion function to convert to decimal. There are so many `double` numbers, some of them with so many digits in decimal, that one cannot summarily dismiss the possibility that long sequences of 0s or 9s appear in the decimal representation of some of them. – Pascal Cuoq Nov 28 '13 at 22:50
  • @HotLicks: There's a ton of them, though, and a ton of different exponents. You'd almost expect a coincidence or three to give you some extra consecutive zeroes. – tmyklebu Nov 28 '13 at 22:51
  • I suggest changing the question slightly. What you need for truncation is not the length of the longest run. It is the length of a run long enough to ensure that generating more digits would not change any of the digits before the run. – Patricia Shanahan Nov 29 '13 at 16:36
  • @PatriciaShanahan I am not sure I understand your comment. Could you illustrate it on an example (or edit the question at the bottom if it is easier with more space/editing capabilities)? – Pascal Cuoq Nov 29 '13 at 16:44
  • @PascalCuoq Never mind. I've changed my mind. – Patricia Shanahan Nov 30 '13 at 04:00
  • 2
    +1 for nerd sniping; I've just wasted half my Saturday morning on this! For zeros, I believe the answer to be 20: there are exactly 10 positive IEEE 754 binary64 floats whose decimal expansion includes 20 consecutive non-leading non-trailing zeros, the smallest of which is 0x1.9527560bfbed8p-1000. There are no examples of binary64 numbers whose decimal expansion has 21 consecutive internal zeros. I'm cleaning up a proper answer and will post it later this afternoon (UTC). – Mark Dickinson Nov 30 '13 at 13:30
  • 2
    I had to see that for myself -- the first 331 digits of 0x1.9527560bfbed8p-1000 are "1.477012373908101575832232661339769380031937878886222568639663847578915738904402685093081763578918086880369974166811882659004450391286591500093106533326541096734395895637095523633076069664624790127807433173880682800315681861858968243277845522401259472373130330434329222431733172090251166174832460421937841944270000000000000000000074...e-301". There are your 20 zeros! (I can't wait to see your answer.) – Rick Regan Nov 30 '13 at 16:05

5 Answers5

11

[Short version: The answer is 20. Recast the problem in terms of finding good rational approximations to numbers of the form 2^e / 10^d; then use continued fractions to find the best such approximation for each suitable d and e.]

The answer appears to be 20: that is, there are examples of IEEE 754 binary64 floats whose decimal expansion has 20 consecutive zeros, but there are none with 21 consecutive zeros in their decimal expansion (excluding leading and trailing zeros). The same is true for strings of nines.

For the first part, all I need to do is exhibit such a float. The value 0x1.9527560bfbed8p-1000 is exactly representable as a binary64 float, and its decimal expansion contains a string of 20 zeros:

1.4770123739081015758322326613397693800319378788862225686396638475789157389044026850930817635789180868803699741668118826590044503912865915000931065333265410967343958956370955236330760696646247901278074331738806828003156818618589682432778455224012594723731303304343292224317331720902511661748324604219378419442700000000000000000000740694966568985212687104794747958616712153948337746429554804241586090095019654323133732729258896166004754316995632195371041441104566613036026346868128222593894931067078171989365490315525401375255259854894072456336393577718955037826961967325532389800834191597056333066925969522850884268136311674777047673845172073566950098844307658716553833345849153012040436628485227928616281881622762650607683099224232137203216552734375E-301

For the part of the question about nines, the decimal expansion of 0x1.c23142c9da581p-405 contains a string of 20 nines:

2.12818792307269553358078502102171540639252016258831784842556110831434197718043638405555406495645619729155240037555858106390933161420388023706431461384056688295540725831155392678607931808851292893574214797681879999999999999999999941026584542575391157788777223962620780080784703190447744595561259568772261019375946489162743091583251953125E-122

To explain how I found the numbers above, and to show that there are no examples with 21 consecutive zeros, we'll need to work a bit harder. A real number with a long string of 9s or 0s in its decimal expansion has the form (a + eps)*10^d for some integers a and d and real number eps, with a nonzero (we might as well assume a positive) and eps nonzero and small. For example, if 0 < abs(eps) < 10^-10 then a + eps has at least 10 zeros following the decimal point (if eps is positive), or 10 nines following the decimal point (if eps is negative); multiplying by 10^d allows for shifting the location of the string of zeros or nines.

But we're interested in numbers of the above form that are simultaneously representable as an IEEE 754 binary64 float; in other words, numbers that are also of the form b*2^e for integers b and e satisfying 2^52 <= b <= 2^53, with e limited in range (and with some additional restrictions on b once we get into the subnormal range, but we can worry about that later).

So combining this, we're looking for solutions to (a + eps) * 10^d = b * 2^e in integers a, b, d and e such that eps is small, a is positive and 2^52 <= b <= 2^53 (and we'll worry about ranges for d and e later). Rearranging, we get eps / b = 2^e / 10^d - a / b. In other words, we're looking for good rational approximations to 2^e / 10^d, with limited denominator. That's a classic application of continued fractions: given d and e, one can efficiently find the best rational approximation with denominator bounded by 2^53.

So the solution strategy in general is:

for each appropriate d and e:
    find the best rational approximation a / b to 2^e / 10^d with denominator <= 2^53
    if (the error in this rational approximation is small enough):
        # we've got a candidate
        examine the decimal expansion of b*2^e

We have only around 2 thousand values for e to check, and at worst a few hundred d for each such e, so the whole thing is computationally very feasible.

Now to details: what does "small enough" mean? Which d and e are "appropriate"?

As to "small enough": let's say that we're looking for strings of at least 19 zeros or nines, so we're looking for solutions with 0 < abs(eps) <= 10^-19. So it's enough to find, for each d and e, all a and b such that abs(2^e / 10^d - a / b) <= 10^-19 * 2^-52. Note that because of the limit on b there can be only one such fraction a / b; if there were another such a' / b' then we have 1 / 2^106 <= 1 / (b *b') <= abs(a / b - a' / b') <= 2 * 10^-19 * 2^-52, a contradiction. So if such a fraction exists it's necessarily the best rational approximation with the given denominator bound.

For d and e: to cover the binary64 range including subnormals, we want e to range from -1126 to 971 inclusive. If d is too large then 2^e / 10^d will be much smaller than 2^-53 and there's no hope of a solution; d <= 16 + floor(e*log10(2)) is a practical bound. If d is too small (or too negative) then 2^e / 10^d will be an integer and there's no solution; to avoid that, we want d > min(e, 0).

With all that covered, let's write some code. The Python solution is pretty straightforward, thanks in part to the existence of the Fraction.limit_deminator method, which does exactly the job of finding the best rational approximation within limits.

from fractions import Fraction
from itertools import groupby
from math import floor, log10

def longest_run(s, c):
    """Length of the longest run of a given character c in the string s."""
    runs = [list(g) for v, g in groupby(s, lambda k: k == c) if v]
    return max(len(run) for run in runs) if runs else 0

def closest_fraction(d, e):
    """Closest rational to 2**e/10**d with denominator at most 2**53."""
    f = Fraction(2**max(e-d, 0) * 5**max(-d, 0), 2**max(0, d-e) * 5**max(0, d))
    approx = f.limit_denominator(2**53)
    return approx.numerator, approx.denominator

seen = set()
emin = -1126
emax = 971
for e in range(emin, emax+1):
    dmin = min(e, 0) + 1
    dmax = int(floor(e*log10(2))) + 16
    for d in range(dmin, dmax+1):
        num, den = closest_fraction(d, e)
        x = float.fromhex('0x{:x}p{}'.format(den, e))
        # Avoid duplicates.
        if x in seen:
            continue
        seen.add(x)
        digits = '{:.1000e}'.format(x).split('e')[0].replace('.','').strip('0')
        zero_run = longest_run(digits, '0')
        if zero_run >= 20:
            print "{} has {} zeros in its expansion".format(x.hex(), zero_run)
        nine_run = longest_run(digits, '9')
        if nine_run >= 20:
            print "{} has {} nines in its expansion".format(x.hex(), nine_run)

There's plenty of scope for performance improvements there (not using Python's fractions module would be a good start :-); as it stands, it takes a few minutes to run to completion. And here are the results:

0x1.9527560bfbed8p-1000 has 20 zeros in its expansion
0x1.fa712b8efae8ep-997 has 20 zeros in its expansion
0x1.515476ae79b24p-931 has 20 nines in its expansion
0x1.a5a9945a181edp-928 has 20 nines in its expansion
0x1.86049d3311305p-909 has 20 zeros in its expansion
0x1.69c08f3dd8742p-883 has 20 zeros in its expansion
0x1.1b41d80091820p-861 has 20 zeros in its expansion
0x1.62124e00b5e28p-858 has 20 zeros in its expansion
0x1.ba96e180e35b2p-855 has 20 zeros in its expansion
0x1.31c5be6377c48p-786 has 20 zeros in its expansion
0x1.7e372dfc55b5ap-783 has 20 zeros in its expansion
0x1.7e89dc1c3860ap-555 has 20 nines in its expansion
0x1.7e89dc1c3860ap-554 has 20 nines in its expansion
0x1.7e89dc1c3860ap-553 has 20 nines in its expansion
0x1.7e89dc1c3860ap-552 has 20 nines in its expansion
0x1.30bd91ea994cbp-548 has 20 zeros in its expansion
0x1.4a5f9de9ee064p-468 has 20 nines in its expansion
0x1.9cf785646987dp-465 has 20 nines in its expansion
0x1.c23142c9da581p-408 has 20 nines in its expansion
0x1.c23142c9da581p-407 has 20 nines in its expansion
0x1.c23142c9da581p-406 has 20 nines in its expansion
0x1.c23142c9da581p-405 has 20 nines in its expansion
0x1.ba431f4e34be9p+738 has 20 nines in its expansion
Mark Dickinson
  • 29,088
  • 9
  • 83
  • 120
1

Obviously, it is at least 15, as illustrated by this Smalltalk/Squeak code:

1.0 successor asTrueFraction printShowingMaxDecimalPlaces: 100.
-> '1.0000000000000002220446049250313080847263336181640625'

1.0 predecessor asTrueFraction printShowingMaxDecimalPlaces: 100.
-> '0.99999999999999988897769753748434595763683319091796875'

Now, it's a bit more involved to demonstrate that there can't be more than 15 consecutive zeros. You search a floating point number f=(A*10^n+B)*10^p where

  • n > 15
  • A is integer
  • 0 < B < 1

But the float must also be expressed as integer significand and biased exponent f=s*2^e where

  • s is integer
  • 0 < s < 2^53 < 5^23

We thus have: s=(A*2^n*5^n+B)*2^(p-e)*5^p with s < 2^53.

My first answer was false, this is to be finished...

In all generality, s can be written, s=2^a*5^b+c, c not divisible by 2 nor 5.

A*2^(n+p-e)*5^(n+p)+B*2^(p-e)*5^p = 2^a*5^b+c

We can search a construct with A=1, B=c*2^(e-p)/5^p<1, n+p-e=a, n+p=b, e-p=n-a.

B=c*2^(n-a)/5^(b-n)

I tried all pairs a,b, such that 2^52 < 2^a*5^b < 2^53, but could not find any n>15 satisfying B<1... Trying with A > 1 will only make things worse (it involves reducing a and b).

Thus I don't think that there is any double with 16 consecutive zeros, but it's not a beautiful demonstration...

aka.nice
  • 9,100
  • 1
  • 28
  • 40
  • I don't understand your argument at all. Why is `s = (A*10^n+B)*10^(p-e)`? `e` is the binary exponent, and converting it to a decimal exponent would seem to put you in a position where you need to argue something about `log_10(2)`. – tmyklebu Nov 28 '13 at 20:43
  • You probably need to use something about there being finitely many `double `exponents. For any `k`, I can probably find a power of two that contains `k` consecutive zeroes in its decimal expansion. – tmyklebu Nov 28 '13 at 20:44
  • No, I wanted to extract inequalities from the limitation of significand s, without even invoking the limits of exponent e – aka.nice Nov 28 '13 at 21:25
1

I don't have a solution to this, but here's an approach I might follow:

  • Keep track of the longest string of zeroes found so far. Call the length L. It's at least 15 long, thanks to aka.nice's answer.
  • For each possible exponent and each location 10^k where L+1 consecutive zeroes could occur, you get a messy little knapsack problem.
  • Compute the relevant 53 powers of two modulo 10^{k+L}, and round them so that they have about L+1 significant figures.
  • Find all combinations of the first 26 powers of two modulo 10^{k+L} using native integer math.
  • Find all combinations of the last 26 powers of two modulo 10^{k+L} similarly.
  • Sort both halves, and look for pairs that give you something very close to the negative of the implied bit using a linear scan. You probably only get a few matches doing this.
  • Check each match using sprintf or something.

Seems like you have to run this loop a few million times, but it should be doable on a few dozen modern computers in a few dozen hours.

I'd also add that there's an integer exactly representable in floating point that has a lot of trailing zeroes: 87960930222080000000000000000000000 has 22 trailing zeroes and is 10F0CF064DD5920000000000000000 in hex. (In fact, 10^22 is exactly representable as a double and it obviously has 22 trailing zeroes. You can't do any better since 5^23 would need to divide such a significand and that's impossible. Oh well.)

tmyklebu
  • 13,915
  • 3
  • 28
  • 57
  • Maybe an approach based on filtering the possible significands is also possible using some fancy number theory. I haven't worked that out, but it might be worth doing so. – tmyklebu Nov 28 '13 at 21:03
  • Interesting. Take 87960930222080000000000000000000000 and keep multiplying by 3. It appears that you get longer sequences such as 34077866599533100000000000000000000000000000, though I can't verify that they're exact. – Hot Licks Nov 28 '13 at 23:54
  • @HotLicks: They aren't; you were multiplying by 3. (To expand: multiplying by 3 leaves the low bit unchanged and moves the high bit left; after doing it a couple of times, the low and high bits are too far apart.) – tmyklebu Nov 28 '13 at 23:59
  • The problem is that ((5 raisedTo: 22)*(2 raisedTo: 22)) asFloat ulp -> 2.097152e6, so you can't easily add a trailing non zero digit without wasting 7 zeroes... – aka.nice Dec 06 '13 at 15:06
  • @aka.nice: Yup. Mainly intended that to indicate plausibility---though Mark Dickinson's answer has done that and much more. – tmyklebu Dec 06 '13 at 15:08
0

This code will remove 0 in the end of decimal.

 private void button1_Click(object sender, EventArgs e)
        {
            String Str = textBox1.Text;
            String lstVal = GetDecimalString(Str);
           textBox2.Text = lstVal;
        }

    private static string GetDecimalString(String Str)
    {
        String lstVal = "";
        if (Str.IndexOf(".") > -1)
        {
            String[] Last = Str.Split('.');
            if (Last.Length == 1)
            {
                lstVal = Last[0];
            }
            else
            {
                lstVal = Last[1];
            }
            String TrimedData = lstVal;
            for (int i = lstVal.Length - 1; i >= 0; i--)
            {
                if (TrimedData.EndsWith("0") && TrimedData.Length > 3)
                {
                    TrimedData = TrimedData.Substring(0, i - 1);
                }
            }
            lstVal = TrimedData;

            if (lstVal.Length < 3)
                lstVal = lstVal.PadRight(3, '0');

            lstVal = String.Join(".", Last[0], lstVal);
        }
        else
        {
            lstVal = Str;
        }
        return lstVal;
    }
shauryachats
  • 9,975
  • 4
  • 35
  • 48
  • The question is not how to remove a trailing zero from a decimal representation. The question is about the maximum number of consecutive non-leading non-trailing zeroes in the exact representation of an IEEE 754 double-precision number. – Pascal Cuoq Feb 01 '15 at 11:07
  • … the answer has been provided: it is 20. Before answering an old question, you should always read the provided answers: they may clarify what the question was if you had missed it. – Pascal Cuoq Feb 01 '15 at 11:35
-2

The pragmatic solution is:

  • call your existing function to return a string containing just the number of decimal places that you need;
  • convert the result back to a double-precision value;
  • if this value is greater than the original, decrement the final digit.
TonyK
  • 16,761
  • 4
  • 37
  • 72
  • This would be an answer to the question http://stackoverflow.com/questions/20264681/avoid-c-setprecision-rounding that I linked to, and it does not work: rounding happens again when converting back from the obtained decimal representation to `double`, and this rounding can be in the other direction, making it impossible to conclude. – Pascal Cuoq Nov 28 '13 at 22:55
  • Note that the pragmatic solution, if all you have is a black-box double->decimal conversion function that only rounds to nearest, is to implement your own double->decimal conversion function. It is surprisingly easy if one does not care about performance, and it is trivial to make it round down or up. – Pascal Cuoq Nov 28 '13 at 23:03