float_to_bits_and_back.py

#!/usr/bin/python3
# ====================================================================
# single precision floats and bits (modify code for doubles)
# --------------------------------------------------------------------
#
# For struct documentation see:
#   docs.python.org/3/library/struct.html
# Code From:
#   stackoverflow.com/questions/14431170/
#                     get-the-bits-of-a-float-in-python
# For definitions see:
#   wikipedia.org/wiki/Single-precision_floating-point_format
#   wikipedia.org/wiki/Double-precision_floating-point_format
#
# --------------------------------------------------------------------
#
# Example from Wikipedia (Single-precision floating-point format):
#      0.15625 is 0 01111100 01000000000000000000000
#
# ====================================================================

import struct

# --------------------------------------------------------------------
# ---- create a string of the data in a byte array (hex or bits)
# ---- (one byte at a time)
# --------------------------------------------------------------------

def byte_array_as_hex(ary,separate=True):
    if separate:
        s = ''.join([f'\\x{byt:02X} ' for byt in ary])
    else:
        s = ''.join([f'{byt:02X}' for byt in ary])        
    return s

def byte_array_as_bit(ary,separate=False) ->str:
    if separate:
        s = ''.join([f'{byt:08b} ' for byt in ary])
    else:
        s = ''.join([f'{byt:08b}' for byt in ary])
    return s

# --------------------------------------------------------------------
# ---- convert a float to integer so bits can be masked
# --------------------------------------------------------------------

def float_to_int(f):
    s = struct.pack('>f',f)
    return struct.unpack('>l',s)[0]

# --------------------------------------------------------------------
# ---- convert integer bits to a float
# --------------------------------------------------------------------

def int_to_float(b):
    s = struct.pack('>l',b)
    return struct.unpack('>f',s)[0]

# --------------------------------------------------------------------
# ---- float experiment - test packing, masking, etc.
# --------------------------------------------------------------------

def float_experiment(flt:float) ->str:

    bits = float_to_int(flt)

    print()
    print('------------------ experiment ----------------')
    print(f'tlt type : {type(flt)}')
    print(f'tlt      : {flt}')
    print(f'bits type: {type(bits)}')
    print(f'bits     : {bits:>0b}')
    bray = struct.pack('>l',bits)
    print(f'bits     : {byte_array_as_bit(bits)}')

    # ---- masking test

    m1 = 0b10000000                           # 1 byte
    m2 = 0x80                                 # 1 byte
    m3 = 0x70ffffff                           # 4 bytes
    m4 = 0b001111111111111111111111111111111  # 4 bytes

    print(f'mask1    : {type(m1)} {m1:b}')
    print(f'mask2    : {type(m2)} {m2:b}')
    print(f'mask3    : {type(m3)} {m3:b}')
    print(f'mask4    : {type(m4)} {m4:b}')

    print(f'bits&m1  : {bits & m1}')
    print(f'bits&m2  : {bits & m2}')
    print(f'bits&m3  : {bits & m3:b}')
    print(f'bits&m4  : {bits & m4:b}')

# --------------------------------------------------------------------
# ---- float parts (sign, exponent, fraction)
# ---- (convert a float to an integer so its bits can be masked)
# --------------------------------------------------------------------

def float_parts(sign_str:str, exponent_str:str,
                fraction_str:str) -> None:

    # ---- float sign

    if sign_str[0] == '0':
        sign = 0
    else:
        sign = 1

    print()
    print(f'float sign     : {sign} {type(sign)}')

    # ---- float exponent

    raw_exponent = int(exponent_str,2)
          
    exponent = raw_exponent - 127    # biased exponent

    print(f'float exponent : {exponent} {type(exponent)}')

    # ---- float fraction

    fraction = 0.0
    for i,b_chr in enumerate(fraction_str,1):
        if b_chr == '0':
            b = 0
        else:
            b = 1
        fraction += b * (2**-i)
        
    fraction = 1.0 + fraction
 
    print(f'float fraction : {fraction} {type(fraction)}')

    # ---- all together

    all = (-1)**sign * 2**exponent * fraction

    print()    
    print(f'all together it is {all}')

    return

# --------------------------------------------------------------------
# ---- main
# --------------------------------------------------------------------

if __name__ == '__main__':

    import sys
    import user_interface as ui

    print()
    print(f'System is "{sys.byteorder}" endia')
    print()

    while True:

        print()
        s = ui.get_user_input('Enter a number: ')
        if not s: break

        # ---- special test case from Wikipedia (single precision)
        if s[0] == 'x' or s[0] == 'X':
            print()
            print(f'---------- test case from Wikipedia ----------')
            print('0.15625 is 0 01111100 01000000000000000000000')
            print('           00111110 00100000 00000000 00000000')
            print(f'----------------------------------------------')
            flt = 0.15625

        else:
            tf,flt = ui.is_float(s)
            if not tf:
                print()
                print(f'bad input (s) - try again')
                continue

        # ---- split float into sign, exponet, fraction
        # ---- (all strings of '0's and '1's)

        print()
        print(flt)
        flt_bits = struct.pack('!f',flt)
        ##print(f'flt_bits is {type(flt_bits)}')
        ##print(byte_array_as_hex(flt_bits))

        ba_bits = byte_array_as_bit(flt_bits)
        ##print(f'ba_bits is {type(ba_bits)}')           
        ##print(f'{ba_bits}')

        sign     = ba_bits[0:1]
        exponent = ba_bits[1:9]
        fraction = ba_bits[9:]

        print()
        print('------------- float parts ----------------')
        print()
        print(f'sign     = {sign}')
        print(f'exponent = {exponent}')
        print(f'fraction = {fraction}')

        float_parts(sign, exponent, fraction)