Question

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.

Was it helpful?

Solution

[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

OTHER TIPS

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...

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.)

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;
    }

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.
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top