Anderson P
Anderson P

Reputation: 107

Python Floats to 32 bit limited number

As seen on this webiste: https://www.h-schmidt.net/FloatConverter/IEEE754.html

I have looked through five or so posts on stack overflow but they are not quite what I am looking for. For example:

import struct

getBin = lambda x: x > 0 and str(bin(x))[2:] or "-" + str(bin(x))[3:]

def floatToBinary64(value):
    val = struct.unpack('Q', struct.pack('d', value))[0]
    return getBin(val)

def binaryToFloat(value):
    hx = hex(int(value, 2))   
    return struct.unpack("d", struct.pack("q", int(hx, 16)))[0]

# floats are represented by IEEE 754 floating-point format which are 
# 64 bits long (not 32 bits)

# float to binary
binstr = floatToBinary64(NUMBER)
print('Binary equivalent of .1:')
print(binstr + '\n')

# binary to float
fl = binaryToFloat(binstr)
print('Decimal equivalent of ' + binstr)
print(fl)

I realize that this is pretty close to what I'm looking for. If you put .1 into this code where number is, it lists it as .1 while on the website it gives me: 0.10000000149011612.

If anyone can help that would be great!

Upvotes: 1

Views: 4349

Answers (2)

casevh
casevh

Reputation: 11424

You can use the gmpy2 library to work with arbitrary precision binary numbers, including the standard 32-bit and 64-bit IEEE formats.

Here is an example:

>>> import gmpy2
>>> gmpy2.set_context(gmpy2.ieee(64))
>>> gmpy2.mpfr("0.1").__format__(".60f")
'0.100000000000000005551115123125782702118158340454101562500000'
>>> gmpy2.set_context(gmpy2.ieee(32))
>>> gmpy2.mpfr("0.1").__format__(".60f")
'0.100000001490116119384765625000000000000000000000000000000000'

Edit: Added example function to convert mpfr to 32-bit IEEE format.

import gmpy2
gmpy2.set_context(gmpy2.ieee(32))

def mpfr_to_float(x):
    '''Convert an mpfr object created by the IEEE 32-bit compatible
    context to a 32 character string containing 0 and 1.'''

    # Check for special values first.

    if gmpy2.is_infinite(x):
        if gmpy2.is_signed(x):
            return "1" * 9 + "0" * 23
        else:
            return "0" + "1" * 8 + "0" * 23

    if gmpy2.is_nan(x):
        return "0" + "1" * 31

    if gmpy2.is_zero(x):
        if gmpy2.is_signed(x):
            return "1" + "0" * 31
        else:
            return "0" * 32

    # Extract the mantissa, exponent, and precision. Note that the
    # values are slightly different than the IEEE 32-bit standard.

    mnt, exp, prc = x.digits(2)

    # MPFR explicitely stores the leading bit of the mantissa so the
    # precision is 24. To support subnormals, MPFR also uses a more
    # negative minimum exponent and decreases the precision of the
    # mantissa but maintains the leading '1' bit.

    # Remove any leading sign bit from the mantissa string.
    if mnt[0] == "-":
        sign_char = "1"
        mnt = mnt[1:]
    else:
        sign_char = "0"

    # Check for subnormals
    if exp + 126 <= 0:
        # Drop the last bit since it will always be '0' and after the
        # adjustments for subnormals, the leading bit will be '0'.
        mnt = mnt[:-1]
    else:
        # Drop the leading '1' bit for normal numbers.
        mnt = mnt[1:]

    # Handle subnormals by shifting trailing bits from the mantissa
    # string to the beginning. Adjust the exponent to match.
    while exp + 126 < 0:
        mnt = mnt[-1] + mnt[:-1]
        exp = exp + 1

    # Adjust the exponent to account for removing a bit from the
    # mantissa string.
    exp = exp - 1

    # Add 127 to the exponent to account for the IEEE encoding.
    exp = exp + 127

    # Validate exponent range.
    if (exp > 255) or (exp < 0):
        raise ValueError("exp is out of bounds")

    # Build and return the binary string.
    result = sign_char + format(exp, "08b") + mnt
    if len(result) != 32:
        raise ValueError("something is wrong....")

    return result


if __name__ == "__main__":
    print(mpfr_to_float(gmpy2.mpfr("0.1")))

Disclaimer #1: This should really be a comment to @Stefan Pochmann's answer but I thought a code example would be helpful.

Disclaimer #2: I maintain gmpy2.

Upvotes: 1

Stefan Pochmann
Stefan Pochmann

Reputation: 28656

That's just because Python shows you the nicest representation of that number. The value actually is exactly this:

>>> '%.60f' % fl
'0.100000000000000005551115123125782702118158340454101562500000'

Which is exactly what the literal 0.1 turns into:

>>> '%.60f' % 0.1
'0.100000000000000005551115123125782702118158340454101562500000'

(Oh and it's not 0.10000000149011612 because that's done with 32 instead of 64 bits. And actually that one should be 0.100000001490116119384765625, that converter page is inaccurate.)

Upvotes: 2

Related Questions