Skip to content

Instantly share code, notes, and snippets.

@the-moog
Created January 4, 2022 12:10
Show Gist options
  • Save the-moog/db87287845ef877afb691b8452def43d to your computer and use it in GitHub Desktop.
Save the-moog/db87287845ef877afb691b8452def43d to your computer and use it in GitHub Desktop.
A Python class that produces proper 2's compliment binary/hex representations of integers.
"""
Python's handling of negative hex numbers is rubbish
It has a two's compliment but that does not know about bytes
This class fixes that by allowing a negative hex number that is not just a hex number
with a minus in front of it
doctest below here....
Negative of 127
>>> test(127) # 127 (0x7F) becomes 0x81 (-127)
01111111(127), 10000001(-127)
>>> test(0) #0 stays at 0
00000000(0), 00000000(0)
>>> test(1) # +1 becomes -1
00000001(1), 11111111(-1)
>>> test(-1) # -1 becomes +1
11111111(-1), 00000001(1)
>>> test(0x80) # 0x80 (128) becomes -128
0000000010000000(128), 10000000(-128)
>>> test(0x81) # 0x81 (129) becomes -129
0000000010000001(129), 1111111101111111(-129)
>>> test(0xFFFF) # 0xFFFF (65535) becomes -65535
000000001111111111111111(65535), 111111110000000000000001(-65535)
>>> test(65535) # 65535 becomes -65535
000000001111111111111111(65535), 111111110000000000000001(-65535)
>>> test(-0xFFFF) # -0xFFFF (-65535) becomes 65535
111111110000000000000001(-65535), 000000001111111111111111(65535)
>>> test(-65535) # -65535 becomes 65535
111111110000000000000001(-65535), 000000001111111111111111(65535)
>>> test(0xFFFF) == test(65535) # 0xFFFF == 65535
000000001111111111111111(65535), 111111110000000000000001(-65535)
000000001111111111111111(65535), 111111110000000000000001(-65535)
True
>>> test(-0xFFFF) == test(-65535) # -0xFFFF == -65535
111111110000000000000001(-65535), 000000001111111111111111(65535)
111111110000000000000001(-65535), 000000001111111111111111(65535)
True
>>> negneg(-65535) # Repeated application
True
>>> test_lots_of_negneg(1000000)
True
>>> test(-16711935) # FF00FF (-16711935) becomes
11111111000000001111111100000001(-16711935), 00000000111111110000000011111111(16711935)
>>> test(16711681)
00000000111111110000000000000001(16711681), 11111111000000001111111111111111(-16711681)
"""
class SignedHex(int):
"""A class that expresses proper 2's compliment hex values"""
from math import ceil
def __new__(cls, x=0):
return super().__new__(cls, x)
def __init__(self, *args, **kw):
self.__to_bytes()
def __to_bytes(self):
"""Used on object creation to deal with negative or positive arguments
Internally data is little endian as it's easier to index
"""
length = self.__n_bytes(self)
try:
self.bytes = super().to_bytes(length=length, byteorder='little', signed=True)
except OverflowError:
# If we overflow then the msb was set while the number was negative, add a byte
self.bytes = super().to_bytes(length=length + 1, byteorder='little', signed=True)
return self.bytes
def __n_bytes(self, x):
"""Return number of bytes required to store x as unsigned"""
nb = self.ceil((abs(x)).bit_length() / 8)
return nb if nb > 0 else 1
@property
def n_bytes(self):
"""This object is represented by n_bytes"""
return len(self.bytes)
@property
def n_bits(self):
"""This object is represented by n_bits"""
return self.n_bytes * 8
@property
def is_signed_negative(self):
"""If the msb is set then this is a signed negative number"""
return (self.bytes[-1] >> 7) == 1
def sign_extend(self, to_n_bits):
"""Sign extend this object instance to to_n_bits if that is more than the current size
to_n_bits is aligned to byte boundaries, i.e. +6 bits -> +8 bits
"""
xtra = (self.ceil(to_n_bits / 8) * 8) - self.n_bits
if xtra > 0:
ext = b"\xff" if self.is_signed_negative else b"\x00"
self.bytes += (ext * self.ceil(xtra / 8))
return self
def __int__(self):
"""The data model int(x)"""
return int.from_bytes(self.bytes, byteorder="little", signed=True)
def __bytes__(self):
"""The data model bytes(x)"""
return self.bytes
def __hash__(self):
"""The data model hash(x)"""
return hash(self.bytes)
def __invert__(self):
"""The data model ~x"""
b = bytes(int(x) ^ 0xff for x in self.bytes)
r = int.from_bytes(b, byteorder="little", signed=True)
return self.__class__(r + 1)
def __neg__(self):
"""The data model -x"""
return self.__invert__()
def as_bin(self, gaps=True):
"""Return a binary representation as a string, optionally split at byte boundaries"""
if gaps:
sep = " "
else:
sep = ""
return sep.join(bin(b)[2:].zfill(8) for b in self.bytes[::-1])
def as_hex(self):
"""Return a hexadecimal representation as a string"""
return "".join("%02x" % x for x in self.bytes[::-1])
#
#
# As this is simple, yet complicated, we do lots of tests
def test_lots_of_negneg(count):
"""Test random values"""
import random
for n in range(count):
x = random.randrange(-1e6, 1e6)
assert negneg(x) is True, f"Failed at {x}"
return True
def negneg(x):
"""Is a single negate the same as odd multiples?"""
return neg(x) == neg(neg(neg(x)))
def test(x):
"""Run a basic test and format the result"""
r1 = pos(x)
r2 = neg(x)
print(f"{r1.as_bin(gaps=False)}({int(r1)}), {r2.as_bin(gaps=False)}({int(r2)})")
def neg(x):
"""From an negative or positive integer or hex value
return an object that is it's 2's compliment negative"""
return SignedHex(-x)
def pos(x):
return SignedHex(x)
# Use doctest as it makes the module it's own test harness
if __name__ == "__main__":
import doctest
doctest.testmod(verbose=True)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment