This is a script that can convert any numerical value to any arbitrary radix representation and back, well, technically it can convert a number to any base, but because the output is a string representation with each a character as a digit, and possible digits are 10 Arabic numerals plus 26 basic Latin (lowercase) letters, the base can only be an integer from 2 to 36 (including both ends).
The possible digits are 0..9 + a..z, just like how hexadecimal orders them, I used bit shifting to convert to and from a base that is a power of two, and divmod for every other base. In my testing, bit-shifting is actually a bit slower than divmoding, I don't know why.
I have written a function that converts a decimal number to a base between 2 and 36, and another function that converts a number from a base between 2 and 36 to decimal, both functions validate the possibility of conversion before the actual conversion.
I have also written three sets of function that converts binary data to and from base-36.
The first set encode and decode the hexadecimal value of the binary data as a whole, rather than individual bytes, so the result can be more compact.
The second set encode and decode each character as two base-36 bits, with the highest possible two-bit being
zz which in decimal is 1295 or (36 ^ 2) - 1, as such it can be accurate up to
ԏ, but extended ASCII has only 256 code points the second set is sufficient for non-UNICODE characters.
The third set encode and decode each byte as two base-36 bits, it can therefore represent any UNICODE code point, and its output is same as the second set for code points below 1296.
from string import ascii_lowercase from string import digits def log2(n): return n.bit_length() - 1 def power_of_2(n): return (n & (n-1) == 0) and n != 0 glyphs = digits + ascii_lowercase def parser(s, base): sep = ',' if base == 60: sep = ':' elif base == 256: sep = '.' if 2 <= base <= 36: return [glyphs.index(i) for i in s] return s.split(sep) def to_base(num, base): if base < 2 or not isinstance(base, int): return sep = ',' if base == 60: sep = ':' elif base == 256: sep = '.' if power_of_2(base): l = log2(base) powers = range(num.bit_length() // l + (num.bit_length() % l != 0)) places = [(num >> l * i) % base for i in powers] else: if num == 0: return ('0', base) places =  while num: n, p = divmod(num, base) places.append(p) num = n if base > 36: return (sep.join(map(str, reversed(places))), base) return (''.join([glyphs[p] for p in reversed(places)]), base) def from_base(s, base): if base < 2 or not isinstance(base, int): return sep = ',' if base == 60: sep = ':' elif base == 256: sep = '.' if base <= 36: for i in s: if glyphs.index(i) >= base: return else: for i in s.split(sep): if int(i) >= base: return places = parser(s, base) if power_of_2(base): l = log2(base) return sum([int(n) << l * p for p, n in enumerate(reversed(places))]) powers = reversed([base ** i for i in range(len(places))]) return sum(int(a) * b for a, b in zip(places, powers)) def b36encode(s): msg = s.encode('utf8').hex() n = int(msg, 16) return to_base(n, 36) def b36decode(m): n = from_base(m, 36) h = hex(n).lstrip('0x') return bytes.fromhex(h).decode('utf8') def b36_encode(s): msg = [ord(i) for i in s] return ''.join([to_base(n, 36).zfill(2) for n in msg]) def b36_decode(m): s = [from_base(n, 36) for n in [m[i:i+2] for i in range(0, len(m), 2)]] return ''.join(chr(n) for n in s) def base36_encode(s): msg = s.encode('utf8') return ''.join([to_base(n, 36) for n in msg]) def base36_decode(m): s = [from_base(n, 36) for n in [m[i:i+2] for i in range(0, len(m), 2)]] return bytearray(s).decode('utf8')
In : to_base(16777215, 3) Out: ('1011120101000100', 3) In : from_base(*to_base(16777215, 3)) Out: 16777215 In : to_base(2**32-1, 3) Out: ('102002022201221111210', 3) In : from_base(*to_base(2**32-1, 3)) Out: 4294967295 In : from_base(*to_base(2**64-1, 36)) Out: 18446744073709551615 In : to_base(2**64-1, 36) Out: ('3w5e11264sgsf', 36) In : to_base(46610, 16) Out: ('b612', 16) In : to_base(54, 13) Out: ('42', 13) In : from_base('zzz', 36) Out: 46655 In : from_base('computer', 36) Out: 993986429283 In : to_base(from_base('computer', 36),36) Out: ('computer', 36) In : '\u1800' Out: '᠀' In : b36encode('whoami') Out: '1ajdznl1ex' In : b36decode(b36encode('whoami')) Out: 'whoami' In : b36decode(b36encode('\u1800\u1800\u1800')) Out: '᠀᠀᠀' In : base36_decode(base36_encode('\u1800\u1800\u1800')) Out: '᠀᠀᠀'
What improvement can be made to my code?
I have improved representation scheme to make it able to represent numbers in any base.
For bases bigger than 36, the resultant string is a mixed radix representation, with the digits represented in their decimal form instead of having a single character representing its value, and a separator delimit the digits.
The separator is determined by the base, for base 60 the separator is a colon (
:), similar to how time is represented, for base 256 the separator is a dot (
.), similar to how IPv4 addresses are represented, and a comma (
,) for any other base.
I have considered using the Greek alphabet after Latin alphabet (after Arabic numerals) to make single character notation representation form able to represent numbers in base-60, however many Greek letters and Latin letters are homoglyphs I abandoned the idea to avoid ambiguity.
In : to_base(16777216,64) Out: ('1,0,0,0,0', 64) In : to_base(16777215,64) Out: ('63,63,63,63', 64) In : to_base(33554431,64) Out: ('1,63,63,63,63', 64) In : to_base(86399,60) Out: ('23:59:59', 60) In : to_base(86399,365) Out: ('236,259', 365) In : to_base(2**32-1,256) Out: ('255.255.255.255', 256)