Created
January 4, 2022 12:10
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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