The “exact” value of a floating-point number

In Python, to see how a floating-point number is stored, you can use .hex() which returns a string. For example, we have:

float('2.2').hex() == float('2.2000000000000002').hex() == '0x1.199999999999ap+1'

In this output, the part from 0x to p is in hexadecimal. For example (dropping the .):

int('1199999999999a', 16) == 4953959590107546

so the string '0x1.199999999999ap+1' represents the fraction (dyadic rational)

\[\frac{4953959590107546}{2^{51}}\]

In other words, when you write 2.2 or 2.2000000000000002 in Python, the exact number stored internally is 4953959590107546 / 2^51, which is exactly 2.20000000000000017763568394002504646778106689453125. (You can get this “exact” number more directly with something like print '%.100f' % 2.2.)

Printing a floating-point number

In Python before 2.7, the printing routine was sub-optimal: it printed the above number as the longer 2.2000000000000002 instead of the shorter 2.2, even though both of these round to the same exact value (2.20000000000000017763568394002504646778106689453125).

Printing numbers as their shortest decimal representation is a solved problem since 1990, although there continue to be developments even in 2016. (See this post for some links.) It’s just that Python until 2.7 didn’t bother to find shortest representation.

For references to this change, see

Next and previous floating-point numbers

Given a floating-point number f, can we find the next and previous floating-point numbers? See the answers on StackOverflow to this question and to this question. Below is the code by Mark Dickinson:

import math
import struct

def next_up(x):
    # NaNs and positive infinity map to themselves.
    if math.isnan(x) or (math.isinf(x) and x > 0):
        return x

    # 0.0 and -0.0 both map to the smallest +ve float.
    if x == 0.0:
        x = 0.0

    n = struct.unpack('<q', struct.pack('<d', x))[0]
    if n >= 0:
        n += 1
    else:
        n -= 1
    return struct.unpack('<d', struct.pack('<q', n))[0]

def next_down(x):
    return -next_up(-x)

For example, something like (with another function not shown here, because I haven’t implemented it properly):

f = 2.2
print('%.100f' % next_down(f))
print('%.100f' % f)
print('%.100f' % next_up(f))

outputs:

2.199999999999999733546474089962430298328399658203125000000000 4953959590107545/2**51
2.200000000000000177635683940025046467781066894531250000000000 4953959590107546/2**51
2.200000000000000621724893790087662637233734130859375000000000 4953959590107547/2**51

while the same with f = 8.0 outputs:

7.999999999999999111821580299874767661094665527343750000000000 9007199254740991/2**50
8.000000000000000000000000000000000000000000000000000000000000 4503599627370496/2**49
8.000000000000001776356839400250464677810668945312500000000000 4503599627370497/2**49

Oh well, the improperly implemented function used above (only tested for positive and large (greater than $1/2^{1024}$) numbers is:

def dyadic(f):
    """Given the float 2.2, returns the string "4953959590107546/2**51", etc."""
    s = f.hex()
    if f <= 0.0:
        raise NotImplementedError
    assert s.startswith('0x1.'), f.hex()
    s = '1' + s[4:]
    foo = 1
    if '+' in s:
        where = s.find('+')
    elif '-' in s:
        where = s.find('-')
        foo = -1
    else:
        raise NotImplementedError
    before = s[:where]
    after = s[where + 1:]
    assert before.endswith('p')
    before = before[:-1]
    prec = int(after)
    assert s.endswith('p+%d' % prec if foo == 1 else 'p-%d' % prec), f.hex()
    prec = 52 - prec if foo == 1 else 52 + prec

    numerator = int(before, 16)
    denominator = 2**prec
    lhs = float(numerator) / denominator
    assert lhs == f, (f, numerator, denominator, lhs)
    return '%d/2**%d' % (numerator, prec)

See also