Search code examples
pythonfloating-pointroundingprecisionieee-754

Float to String Round-Trip Test


I believe that 17 decimal places should be enough to correctly represent an 8-byte float, such that it is round-trip safe (converted to a string and back without any loss).

But in this test, the number can go as high as 23, and probably higher if you increase the number of iterations.

Is this a flawed test and why?
And how do you ensure a round-trip integrity of a float in Python?

def TestLoop():
    sFormat = ''
    success = True
    ff = [1.0/i for i in range(1,10000000)]
    for n in range(17, 31):
        sFormat = '{{:.{:d}f}}'.format(n)
        success = True
        for f in ff:
            if f != float(sFormat.format(f)):
                success = False
                break
        if success:
            return(n)
    return(-1)

n = TestLoop()   
print('Lossless with ', n, ' decimal places.')

If an IEEE 754 double precision is converted to a decimal string with at least 17 significant digits and then converted back to double, then the final number must match the original.


Solution

  • In my original test, I was operating on small numbers, so there were a lot of leading zeros, which are not significant digits. Floats require 17 significant digits to be represented correctly. By changing one line like so, I made the numbers larger and was able to succeed with only 16 digits after the decimal point.

    ff = [10000000.0/i for i in range(1,10000000)]
    

    The best approach seems to be to not use the format() at all, but use repr() or str() instead.
    This code here succeeds:

    def TestLoop():
        for i in range(1, 10000000):
            f = 1.0 / i
            if f != float(repr(f)):
                print('Failed.')
                return
        print('Succeeded.')
        return
    
    TestLoop()
    

    Another way that worked was to use 17 digits after the decimal point, but use the g formatter instead of f. This uses an exponent, so the leading zeros are eliminated.

    if f != float('{:.17g}'.format(f)):