Changing Bases

Changing Bases

We’ll discuss representation of numbers in different bases. We will also write a collection of programs to convert between various bases. We’ll examine more and less elegant solutions to a variety of problems.

1. Base 10 to Base 16 in python

The numerical base of a number determines how we write the number. A base 10 number can be written like $\Sigma_{n=1}^k a_n \cdot 10^n$.

For instance $127 = 1\cdot100 + 2\cdot10 + 7\cdot1$. This is probably not revolutionary to you. But I could write this same number in any base. Let us consider what 127 looks like in base 2 (binary) for instance. First we must determine the greatest power of 2 that we can subtract from 127 and still have a positive remainder. Then, we repeat with the next largest power.

$127 = 1\cdot2^6 + 1\cdot2^5 + 1\cdot2^4 + 1\cdot2^3 + 1\cdot2^2 +1\cdot2^1 + 1\cdot2^0$
$127 = 1\cdot64 + 1\cdot32 + 1\cdot16 + 1\cdot8 + 1\cdot4 +1\cdot2 + 1\cdot1$
$127 = 1111111_{binary}$

We could use the same sort of process to determine a base 16 representation for this number.

$127 = 7\cdot16^1 + 15\cdot16^0$
$127 = 7\cdot16 + 15\cdot1$

And in base 16 we run into the issue that we must use an extended number system. Traditionally, we notate 10 = a, 11 = b, 12 = c, 13 = d, 14 = e, and 15 = f. We need this because otherwise our representation would become difficult to read. Each digit should occupy EXACTLY ONE COLUMN otherwise it is not fulfilling the job of a digit.

$127 = 7f$

# base 10 to 16
import math

hex = []
n = 127
original_number = n
i = math.floor(math.log(n,16))
#the above statement captures the largest power of 16 that divides n

while (i >= 0) :
 x = math.floor(n / 16**i)
 # the statement in line 11 captures the number of times a given power of 16 divides x
 n = n % (16**i)
 # uses modular division to reduce n to a smaller power of 16
 
 if x == 15:
    hex.append('f')
 if x == 14: 
    hex.append('e')
 if x == 13: 
    hex.append('d')
 if x == 12: 
    hex.append('c')
 if x == 11: 
    hex.append('b')   
 if x == 10: 
    hex.append('a')
 if (x < 10):
    hex.append(x)
 i = i - 1

print(original_number, "in decimal is", "".join(map(str,hex)), "in hexidecimal.")

The function on the left converts the number n in decimal to hexadecimal. It features several interesting elements of (python) programming. We have a common header library referenced with import math. The math library contains a large number of potentially useful mathematic functions.

Other libraries such as numpy contain many of the same definitions, however, math is preferable as a reference if you need only simple tools. The reason is efficiency. Smaller libraries occupy less memory space and programs with few, small libraries run faster than programs with many, large libraries. We’ll explore this efficiency gap in some detail.

Functions in a library are generally referenced in the following way:

library.function(inputs)

The math.floor function returns the expected value of the floor function from your algebra classes.

** the double star is the symbolic reference for exponentiation so 16**i can be read as $16^i$.


% references modular division. Modular division is a very efficient operation in bitwise arithmetic and returns the remainder. For instance 25 % 5 = 0 or 25 % 11 = 3.

Finally, map() returns a map object which is a an iterator. It works a bit like a for loop that applies a particular function to every item in a list. Here, map() turns all the array elements of hex into string type elements and “”.join() takes strings as arguments and concatenates them with a “” between them. ” ” would put a space for instance whereas “” puts no space.

2. Base 16 to Binary

The above would work in general for any base. But sometimes there is a more elegant observation to be made. We might notice the following correspondence:

01234567
011011100101110111
89abcdef
10001001101010111100110111101111

This doesn’t seem to have added much. But sometimes mathematics is all about looking at a familiar through a different lens:

01234567
00000001001000110100010101100111
89abcdef
10001001101010111100110111101111

and suddenly the elegance of byte to bit conversions is made clear. Try to create your own program that converts base 16 to binary.

3. Binary to Hexadecimal
def TWOto16(binary_input):
 #This is my pair of lookup tables
 base2 = ['0000', '0001', '0010', '0011', '0100', '0101', '0110','0111', '1000', '1001', '1010', '1011', '1100', '1101', '1110', '1111']
 base16 = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'a', 'b', 'c', 'd', 'e', 'f']
 hex_string = []

 #this determines if my input string has the correct number of bits to use the above lookup table
 if(len(binary_input)%4 == 0): 
     padding = 0
 else:
     padding = 4- len(binary_input) % 4

 #this converts my string to having the correct number of bits so that I can parse it using the lookup table 
 for i in range(padding):
     binary_input = '0' + binary_input

 #The length of the hex string resulting from the conversion
 hex_length = len(binary_input) // 4

 #converts our binary blocks into hex strings
 for i in range(hex_length):
     hex_string.append(base16[base2.index(binary_input[4*i:4*i+4])])
 print(''.join(hex_string))

TWOto16("10101")
TWOto16("1010101")
TWOto16("1010111111")
TWOto16("111")
TWOto16("1111")

On the left, we see an implementation of a lookup table in a manner that exploits some of the better features of python. We take advantage of pythons huge array of optimized list operations and we find the meat of our program in this singular line of code:

hex_string.append(base16[base2.index(binary_input[4*i:4*i+4])])

This references strings of binary digits from our input string, searches for them in the base 2 lookup table, and then corroborates their index with the hex lookup table appending that digit to the new hex string. All of this is done in a single line of code in python. Without the use an additional library, this is a much more involved piece of code in either C or C++.

4. Functional Programming: Base 10 to Binary

Above, we were given a program that converts base 10 numbers to hexadecimal numbers. Afterwards, we wrote a program that converts hexadecimal numbers to binary numbers. Now, we want to write a number to convert decimal numbers to binary. Wouldn’t it be nice if we could just leverage what we’ve already done? This will be our introduction to functional programming.

import math

def decimal_to_hexadecimal(m):
  hex = []
  original_number = int(m)
  n = int(m)
  i = math.floor(math.log(original_number,16))
  
  while (i >= 0):
    x = math.floor(n / 16**i)
    n = n % (16**i)

    if x == 15:
      hex.append('f')
    if x == 14: 
      hex.append('e')
    if x == 13: 
      hex.append('d')
    if x == 12: 
      hex.append('c')
    if x == 11: 
      hex.append('b')   
    if x == 10: 
      hex.append('a')
    if (x < 10):
      hex.append(x)
    i = i - 1
  print(original_number, "in decimal is", "".join(map(str,hex)), "in hexidecimal.")
 
decimal_to_hexadecimal(127)
decimal_to_hexadecimal(128)
decimal_to_hexadecimal(256)
decimal_to_hexadecimal(100000)

Notice what’s changed in the code to right. To declare a function in python we use the def statement. At the very bottom, we see four separate calls to the function.

Pay attention to the definition line reproduced below. The function has a reasonable and thoughtful name. It takes a parameter called m. Functions is python generally assume all their parameters are strings. m is the decimal integer we would like to convert. A better variable name then might be:

def decimal_to_hexadecimal(m):

def decimal_to_hexadecimal(decimal_integer_to_convert):

We would also need to convert every instance of m to this new variable name in the code. n would be better written as current_decimal_integer. hex might be better described as hexidecimal_integer_string. Using underscores and descriptive phrases in your variable names is good programming practice.

What should we call x?

A good variable name might be result_of_dividing_by_a_power_of_16.

What’s a better variable name for i?

highest_remaining_power_of_16

We should also build some resilience into our program in the case that the user inputs something incorrectly.

So the next idea is can we write a program that leverages the combination of this function and a funtional version of the program from section 2 so we can solve this problem. In theory we should be able to write something like hexademical_to_binary(decimal_to_hexadecimal(number)) and obtain our result straight away!