3

I have a list of integers:

var = [1, 5, 4, 17, 231, 89]

If I wanted to convert it to a list of bytes instead, I can do:

[(i).to_bytes(1, byteorder='big') for i in var]

Since each value in var is less than 256, it can fit in one byte per integer. But if I had another list, say:

var = [1, 2, 15, 12]

This can fit an integer into half a byte, or more accurate to what I'm looking for, fit two integers per byte.

How can I specify to combine two integers into a byte if possible, both to and from?

Something like below:

var1 = [1, 5, 4, 17, 231, 89]
var2 = [1, 2, 15, 12]

def foo(var):
  if max(var) < 2**4:
    num_bytes = 0.5
  elif max(var) < 2**8:
    num_bytes = 1
  elif max(var) < 2**16:
    num_bytes = 2

  if num_bytes >= 1:
    return [(i).to_bytes(num_bytes, byteorder='big') for i in var], num_bytes
  elif num_bytes = 0.5:
    # convert var to list of nybbles
    # combine two nybbles to a byte
    # create list of bytes (length is half length of var)
    # return it and num_bytes

def unfoo(var, num_bytes):
  if num_bytes >= 1:
    print([int.from_bytes(i, 'big') for i in var])
  elif num_bytes = 0.5:
    # print original list of integers again

I want to convert a list of integers into a list of bytes, but fitting two nybbles to a byte if it can fit, then do the conversion back.

Desired outcome is:

a, b = foo(var1)
unfoo(a, b) # prints [1, 5, 4, 17, 231, 89]

a, b = foo(var2)
unfoo(a, b) # prints [1, 2, 15, 12]

I don't want a list of smallest number of bits to represent a single number. Note the max(list): if all values in the list can be 8-bits, fit it to 8-bits; if all values can be 16-bits, fit it to 16-bits; if all values can be a nybble, then make two nybble pairs into a list of bytes.

Basically if I have two integers that can fit in a nybble each, how do I concatenate both into a single byte? If I know that bytes need to be split into two, how can I do the split? I can always assume the original list will be divisible by 2.

gator
  • 3,465
  • 8
  • 36
  • 76
  • You will need to manually pack the nybble-size integer values into byte-sized integer values first. This is a simple matter of arithmetic (or bit-shifting). – Karl Knechtel Mar 29 '21 at 15:29

2 Answers2

1

You need to figure out how many numbers can fit in a byte. Then, you need to shift each number by the correct amount and create a new list that contains the combined numbers. Suppose you can fit two numbers in a byte, you'd get new_number = (old_num1 << 4) + old_num2

def foo(var):
    num_bytes = math.ceil(1 + max(math.log2(x) for x in var)) / 8
    if num_bytes >= 1:
        num_bytes = int(num_bytes)
        return [(i).to_bytes(num_bytes, byteorder='big') for i in var], num_bytes
    elif num_bytes == 0.5:
        shift_bits = 4 # or generally, int(num_bytes * 8) 
        new_list = [(a << shift_bits) + b for a, b in zip(var[::2], var[1::2])]
        return [(i).to_bytes(1, byteorder='big') for i in new_list], num_bytes

To unfoo, you need to do the inverse of this operation: when num_bytes < 1, we can find the number of bits you shifted by in the foo() function. Following the same names as the previous explanation, and given new_number, we can get old_num2 as the least-significant four bits (found by new_number & mask) and old_num1 is the most-significant four bits (found by new_number >> shift_bits)

def unfoo(var, num_bytes):
    if num_bytes >= 1:
        return [int.from_bytes(i, 'big') for i in var]
    elif num_bytes == 0.5:
        new_list = [int.from_bytes(i, 'big') for i in var]
        ret = []
        shift_bits = 4 # in general: int(num_bytes * 8)
        mask = int(2**shift_bits - 1)
        for i in new_list:
            b = i & mask
            a = (i >> shift_bits) & mask
            ret.append(a)
            ret.append(b)
        return ret

Checking that this works:

var1 = [1, 5, 4, 17, 231, 89]
var2 = [1, 2, 15, 12]
        
a, b = foo(var1)
c = unfoo(a, b) # prints [1, 5, 4, 17, 231, 89]

print(var1)
print(c)


a2, b2 = foo(var2)
c2 = unfoo(a2, b2) # prints [1, 2, 15, 12]
print(var2)
print(c2)

gives the expected output:

[1, 5, 4, 17, 231, 89]
[1, 5, 4, 17, 231, 89]
[1, 2, 15, 12]
[1, 2, 15, 12]
Pranav Hosangadi
  • 23,755
  • 7
  • 44
  • 70
  • Compute `bit_length = max(s.bit_length() for s in var)`. If `bit_length >= 8`, use `num_bytes = -(-bit_length // 8)` to avoid `math.log2`. (ref: https://stackoverflow.com/questions/33299093/how-to-perform-ceiling-division-in-integer-arithmetic). PS. Assuming nonnegative ints. – Aaron Apr 01 '21 at 16:50
0

I wrote two simple functions, one to pack two numbers into one byte (obviously the numbers would have to fit in half a byte), and another to unpack them:

def concatToByte(num1, num2):
    nybbleSize = 2**4
    if num1 >= nybbleSize or num2 >= nybbleSize:
        raise ValueError("Arguments must fit nybble size")
    return (num1 << 0x4 | num2).to_bytes(1, 'big')
    
def splitByte(byte):
    byteAsInt = int.from_bytes(byte, 'big')
    num1 = (byteAsInt & 0xF0) >> 0x4
    num2 = (byteAsInt & 0x0F)
    return num1, num2

Then you could use it as following:

inNum1 = 5 # hex: 0x05
inNum2 = 12 # hex: 0x0c

byte = concatToByte(inNum1, inNum2)
print(hex(int.from_bytes(byte, 'big'))) # 0x5c

outNum1, outNum2 = splitByte(byte)
print(outNum1, outNum2) # 5 12
lnogueir
  • 1,859
  • 2
  • 10
  • 21