|
#!/usr/bin/env python3 |
|
# -*- coding: utf-8 -*- |
|
|
|
# Author: Etienne Collin |
|
# Date: 2022/09/15 |
|
# Copyright: Copyright 2022, Etienne Collin |
|
# Email: [email protected] |
|
# License: GPL-3.0 |
|
# Version: 1.3.0 |
|
|
|
"""This program converts the base encoding of a number. |
|
|
|
Through this program, the encoding of a number can be converted from a BaseX to a BaseY encoding where x and y are each |
|
between [2, 36]. |
|
|
|
Typical usage example: |
|
Run the script and use answer the terminal input prompts. |
|
""" |
|
|
|
|
|
def convertASCII(character: str = None, number: int = None): |
|
"""Converts characters <--> integers using the ASCII table. |
|
|
|
Converts an hexadecimal character to its decimal value or a number between 10 and 15 to its hexadecimal character |
|
using the ASCII table. This function requires one of its arguments to be set to None. |
|
|
|
Example: |
|
Set the argument <character> to None and assign a value to the argument <number> in order to convert a number to |
|
a character. |
|
|
|
Args: |
|
character (str, optional): The character to be converted. Defaults to None. |
|
number (int, optional): The number to be converted. Defaults to None. |
|
|
|
Returns either: |
|
int: The decimal value of the input. |
|
str: The hexadecimal char of the input. |
|
""" |
|
|
|
if (character is not None) and (number is None): |
|
# Example: |
|
# If <character> = "C", then ord("C")==67 |
|
# ===> ord("C") - ord("A") == 67 - 65 == 2 |
|
############################################# |
|
|
|
try: |
|
# +10 offsets values from [0, 5] to [10, 15]. |
|
number = ord(character) - ord("A") + 10 |
|
except TypeError: |
|
print("\nERROR: The first argument should be a character in ASCII conversion function.\n") |
|
exit() |
|
return number |
|
|
|
elif (character is None) and (number is not None): |
|
# Example: |
|
# If <number> = "12" |
|
# ===> chr((<number> - 10) + ord("A")) == chr((12 - 10) + 65) == chr(67) == "C" |
|
############################################# |
|
|
|
try: |
|
# -10 offsets values from [10, 15]. ord("A") is ASCII value 65 |
|
character = chr((number - 10) + ord("A")) |
|
except TypeError: |
|
print("\nERROR: The second argument should be an integer in ASCII conversion function.\n") |
|
exit() |
|
return character |
|
|
|
else: |
|
print("\nERROR: Exactly one argument should set to None in ASCII conversion function.\n") |
|
|
|
|
|
def inputCheck(number: str, inputBase: int, outputBase: int = 10): |
|
"""Checks user input for errors and illegal digits/characters. |
|
|
|
Args: |
|
number (str): Number to be converted. |
|
inputBase (int): Base in which the input is encoded. |
|
outputBase (int, optional): Base in which to encode the output. Defaults to 10. |
|
""" |
|
|
|
# Check that input base and output base are valid |
|
if not (inputBase >= 2 and inputBase <= 36): |
|
print("\nERROR: Input base encoding values are between [2, 36]\n") |
|
exit() |
|
elif not (outputBase >= 2 and outputBase <= 36) and (outputBase != -1 and outputBase != -2 and outputBase != -3): |
|
print( |
|
"\nERROR: Output base encoding values are between [2, 36].\ |
|
\nSpecial case for -1, -2 and -3 which output Two's Complement notation, IEEE754 Single Precision notation and IEEE754 Double Precision notation.\n" |
|
) |
|
exit() |
|
|
|
if number == "" or number == ".": |
|
print("\nERROR: Input number is empty\n") |
|
exit() |
|
|
|
# Check that each digit in input number is valid for specified base |
|
index = 0 |
|
letterPresent = False |
|
for digit in number: |
|
if (digit >= "0" and digit <= str(min((inputBase - 1), 9))) or ( |
|
digit >= "A" and digit <= convertASCII(None, inputBase - 1) and inputBase > 10 |
|
): |
|
pass |
|
elif inputBase == 10 and digit == "E" and letterPresent == False: |
|
# Check that only one E is present in a Base10 number to specify scientific notation. |
|
letterPresent = True |
|
pass |
|
elif digit == "-" and index == 0: |
|
pass |
|
elif digit == ".": |
|
pass |
|
else: |
|
print("\nERROR: Illegal character/digit in input number according to specified input base.\n") |
|
exit() |
|
index += 1 |
|
|
|
|
|
def processNumber(number: str, inputBase: int): |
|
"""Split integer and decimal part of number, and gives its sign. |
|
|
|
Args: |
|
number (str): Number to be processed. |
|
inputBase (int): Base in which the input is encoded. |
|
|
|
Returns: |
|
str, str, bool: The integer part of the number, the decimal part of the number, number isNegative. |
|
""" |
|
|
|
numberInt = "" |
|
numberDecimal = "" |
|
numberExponent = "" |
|
isNegative = False |
|
|
|
# Store if number is negative |
|
if "-" in number: |
|
isNegative = True |
|
number = number.strip("-") |
|
else: |
|
isNegative = False |
|
|
|
# Check if number is in Base10 and uses scientific notation |
|
# If so, convert it to a non-scientific form |
|
if inputBase == 10 and "E" in number: |
|
number, numberExponent = number.split("E") |
|
number = str(float(number) * 10 ** int(numberExponent)) |
|
|
|
# Check if number has a decimal part |
|
if "." in number: |
|
numberInt, numberDecimal = number.split(".") |
|
else: |
|
numberInt = number |
|
|
|
return numberInt, numberDecimal, isNegative |
|
|
|
|
|
def convertToBase10(number: str, inputBase: int): |
|
"""Converts a number from BaseX notation with x between [2, 36] to a Base10 notation. |
|
|
|
Args: |
|
number (str): Number to be converted. |
|
inputBase (int): BaseX with x between [2, 36] in which the input is encoded between. |
|
|
|
Returns: |
|
str: The converted number in Base10 notation. |
|
""" |
|
|
|
answerInt = 0 |
|
answerDecimal = 0 |
|
numberInt, numberDecimal, isNegative = processNumber(number, inputBase) |
|
|
|
# Convert integer part of number |
|
for index in range(len(numberInt)): |
|
# Weight of the position of the current digit in the number |
|
power = len(numberInt) - index - 1 |
|
|
|
# Check if digit is numerical or a hexadecimal char |
|
if numberInt[index] >= "0" and numberInt[index] <= "9": |
|
answerInt += int(numberInt[index]) * inputBase**power |
|
else: |
|
answerInt += convertASCII(numberInt[index], None) * inputBase**power |
|
|
|
# Convert decimal part of number |
|
if "." in number: |
|
for index in range(len(numberDecimal)): |
|
# Weight of the position of the current digit in the number |
|
power = -index - 1 |
|
|
|
# Check if digit is numerical or a hexadecimal char |
|
if numberDecimal[index] >= "0" and numberDecimal[index] <= "9": |
|
answerDecimal += int(numberDecimal[index]) * inputBase**power |
|
else: |
|
answerDecimal += convertASCII(numberDecimal[index], None) * inputBase**power |
|
|
|
if isNegative: |
|
return str(-1 * (answerInt + answerDecimal)) |
|
else: |
|
return str(answerInt + answerDecimal) |
|
|
|
|
|
def convertFromBase10(number: str, outputBase: int = 10, maxDecimals: int = 30): |
|
"""Convert a number from Base10 notation to BaseX notation with x between [2, 36]. |
|
|
|
Args: |
|
number (str): Number in Base10 to be converted. |
|
outputBase (int, optional): Base in which to encode the output. Defaults to 10. |
|
maxDecimals (int, optional): Limit number of decimals computed in irrational number. Defaults to 30. |
|
|
|
Returns: |
|
str: The converted number in BaseX notation with x between [2, 36]. |
|
""" |
|
|
|
answerInt = "" |
|
answerDecimal = "" |
|
numberInt, numberDecimal, isNegative = processNumber(number, 10) |
|
|
|
# Makes sure that answerInt isn't empty if number == 0 |
|
if number == "0": |
|
answerInt += "0" |
|
|
|
# Convert integer part of number |
|
dividend = int(numberInt) |
|
while dividend > 0: |
|
remainder = str(dividend % outputBase) |
|
|
|
# Check if digit is numerical or a hexadecimal char |
|
if remainder >= "0" and remainder <= "9" and len(remainder) == 1: |
|
answerInt += remainder |
|
else: |
|
answerInt += convertASCII(None, int(remainder)) |
|
|
|
# Updating dividend for next loop iteration by keeping only the quotient of the division |
|
dividend //= outputBase |
|
# Reversing computed list according to the conversion algorithm |
|
answerInt = answerInt[::-1] |
|
|
|
# Convert decimal part of number |
|
if "." in number: |
|
repetition = 0 |
|
|
|
# Multiplicand is currently stored in an int format. |
|
# It is converted to a decimal number here. |
|
# Example: 1234 --> 0.1234 |
|
multiplicand = int(numberDecimal) * 10 ** (-len(numberDecimal)) |
|
|
|
# Loop until algorithm is done or until maxDecimals reached in an irrational number |
|
while repetition < maxDecimals and multiplicand != 0: |
|
# The int() is added to convert the float resulting from the floor division (removes the .0 after number). |
|
product = str(int((multiplicand * outputBase) // 1)) |
|
|
|
# Check if digit is numerical or a hexadecimal char |
|
if product >= "0" and product <= "9" and len(product) == 1: |
|
answerDecimal += product |
|
else: |
|
answerDecimal += convertASCII(None, int(product)) |
|
|
|
# Updating multiplicand for next loop iteration by keeping only the decimal section of the multiplication |
|
multiplicand = (multiplicand * outputBase) - (multiplicand * outputBase) // 1 |
|
repetition += 1 |
|
|
|
if isNegative: |
|
if answerInt == "": |
|
return str("-0." + answerDecimal) |
|
elif answerDecimal == "": |
|
return str("-" + answerInt) |
|
else: |
|
return str("-" + answerInt + "." + answerDecimal) |
|
else: |
|
if answerInt == "": |
|
return str("0." + answerDecimal) |
|
elif answerDecimal == "": |
|
return str(answerInt) |
|
else: |
|
return str(answerInt + "." + answerDecimal) |
|
|
|
|
|
def convertToTwoComplement(number: str): |
|
"""Converts a Base2 integer to its Two's Complement encoding. |
|
|
|
Args: |
|
number (str): Integer in Base2 to be converted. |
|
|
|
Returns: |
|
str: The converted number in Two's Complement encoding. |
|
""" |
|
number, numberDecimal, isNegative = processNumber(number, 2) |
|
number = number |
|
# Check that number is an integer |
|
if numberDecimal != "": |
|
print("ERROR: Only integers may be converted to Two's Complement encoding.") |
|
exit() |
|
|
|
# Check if number is in the range of Two's Complement encoding by looking at the length of the binary number. |
|
if isNegative == True: |
|
if (len(number) > 8) or (len(number) == 8 and "1" in number[1:]): |
|
print("ERROR: Only integers between [-128, 127] may be converted to Two's Complement encoding.") |
|
exit() |
|
|
|
# Make binary number 8 bits long |
|
while len(number) < 8: |
|
number = "0" + number |
|
|
|
# Simulate a bitwise operator NOT |
|
notNumber = "" |
|
for digit in number: |
|
if digit == "1": |
|
notNumber += "0" |
|
else: |
|
notNumber += "1" |
|
|
|
# Add 1 to the binary number |
|
notNumberAddition = "" |
|
index = 1 # Not 0 because string is read backwards |
|
for digit in notNumber[::-1]: |
|
if digit == "1": |
|
notNumberAddition = "0" + notNumberAddition |
|
if digit == "0": |
|
notNumberAddition = "1" + notNumberAddition |
|
break |
|
index += 1 |
|
# Merge digits that were not affected by addition to affected digits |
|
notNumberAddition = notNumber[0 : (len(notNumber) - index)] + notNumberAddition |
|
|
|
return notNumberAddition |
|
|
|
elif isNegative == False: |
|
if len(number) > 7: |
|
print("ERROR: Only integers between [-128, 127] may be converted to Two's Complement encoding.") |
|
exit() |
|
|
|
# Make binary number 8 bits long |
|
while len(number) < 8: |
|
number = "0" + number |
|
|
|
return number |
|
|
|
|
|
def convertToIEEE754SinglePrecision(number: str): |
|
"""Converts a Base2 number to its IEEE 754 Single Precision encoding. |
|
|
|
Args: |
|
number (str): Number in Base2 to be converted. |
|
|
|
Returns: |
|
str: The converted number in IEEE 754 Single Precision encoding. |
|
""" |
|
|
|
numberInt, numberDecimal, isNegative = processNumber(number, 2) |
|
|
|
if isNegative: |
|
sign = "1" |
|
else: |
|
sign = "0" |
|
|
|
exponent = convertFromBase10(str(127 + (len(numberInt) - 1)), 2, 8) |
|
|
|
# Check if exponent is bigger than max range of IEEE754 Single Precision |
|
if len(exponent) > 8: |
|
exponent = "" |
|
for i in range(8): |
|
exponent += "1" |
|
while len(exponent) < 8: |
|
exponent = exponent + "0" |
|
|
|
numberInt = numberInt[1:] |
|
mantissa = numberInt + numberDecimal |
|
|
|
# Check if mantissa is bigger than max range of IEEE754 Double Precision |
|
if len(mantissa) > 23: |
|
mantissa = "" |
|
for i in range(23): |
|
mantissa += "1" |
|
while len(mantissa) < 23: |
|
mantissa += "0" |
|
|
|
# Check for special cases of IEEE754 |
|
if not ("0" in exponent) and not ("1" in mantissa): |
|
return str(int(sign) * -1) + "\u221e" |
|
elif not ("0" in exponent) and ("1" in mantissa): |
|
return "NaN" |
|
elif not ("1" in exponent) and not ("1" in mantissa): |
|
return str(int(sign) * -1) + "0" |
|
else: |
|
return sign + " " + exponent + " " + mantissa |
|
|
|
|
|
def convertToIEEE754DoublePrecision(number: str): |
|
"""Converts a Base2 number to its IEEE 754 Double Precision encoding. |
|
|
|
Args: |
|
number (str): Number in Base2 to be converted. |
|
|
|
Returns: |
|
str: The converted number in IEEE 754 Double Precision encoding. |
|
""" |
|
|
|
numberInt, numberDecimal, isNegative = processNumber(number, 2) |
|
|
|
if isNegative: |
|
sign = "1" |
|
else: |
|
sign = "0" |
|
|
|
exponent = convertFromBase10(str(1023 + (len(numberInt) - 1)), 2, 11) |
|
|
|
# Check if exponent is bigger than max range of IEEE754 Double Precision |
|
if len(exponent) > 11: |
|
exponent = "" |
|
for i in range(11): |
|
exponent += "1" |
|
while len(exponent) < 11: |
|
exponent = exponent + "0" |
|
|
|
numberInt = numberInt[1:] |
|
mantissa = numberInt + numberDecimal |
|
|
|
# Check if mantissa is bigger than max range of IEEE754 Double Precision |
|
if len(mantissa) > 52: |
|
mantissa = "" |
|
for i in range(52): |
|
mantissa += "1" |
|
while len(mantissa) < 52: |
|
mantissa += "0" |
|
|
|
# Check for special cases of IEEE754 |
|
if not ("0" in exponent) and not ("1" in mantissa): |
|
return str(int(sign) * -1) + "\u221e" |
|
elif not ("0" in exponent) and ("1" in mantissa): |
|
return "NaN" |
|
elif not ("1" in exponent) and not ("1" in mantissa): |
|
return str(int(sign) * -1) + "0" |
|
else: |
|
return sign + " " + exponent + " " + mantissa |
|
|
|
|
|
def convertFromTwoComplement(number: str, outputBase: int = 10, maxDecimals: int = 30): |
|
answerNumber = "" |
|
|
|
# CONVERT HERE TO BINARY |
|
|
|
if outputBase == 2: |
|
return answerNumber |
|
else: |
|
return convertBase(answerNumber, 2, outputBase, maxDecimals) |
|
|
|
|
|
def convertFromIEEE754SinglePrecision(number: str, outputBase: int = 10, maxDecimals: int = 30): |
|
answerNumber = "" |
|
|
|
# CONVERT HERE TO BINARY |
|
|
|
if outputBase == 2: |
|
return answerNumber |
|
else: |
|
return convertBase(answerNumber, 2, outputBase, maxDecimals) |
|
|
|
|
|
def convertFromIEEE754DoublePrecision(number: str, outputBase: int = 10, maxDecimals: int = 30): |
|
answerNumber = "" |
|
|
|
# CONVERT HERE TO BINARY |
|
|
|
if outputBase == 2: |
|
return answerNumber |
|
else: |
|
return convertBase(answerNumber, 2, outputBase, maxDecimals) |
|
|
|
|
|
def convertBase(number: str, inputBase: int, outputBase: int = 10, maxDecimals: int = 30): |
|
"""Main function to call in order to convert a number from BaseX to Base Y with x and y between [2, 36]. |
|
|
|
Args: |
|
number (str): Number to be converted. |
|
inputBase (int): Base in which the input is encoded. |
|
outputBase (int, optional): Base in which to encode the output. Defaults to 10. |
|
maxDecimals (int, optional): Limit number decimals computed in irrational number. Defaults to 30. |
|
|
|
Returns: |
|
str: The converted number in BaseX notation with x between [2, 36]. |
|
""" |
|
|
|
# Checking input for errors and illegal digits/characters |
|
inputCheck(number, inputBase, outputBase) |
|
|
|
# Optimize conversions according to input and output base |
|
if inputBase == 10 and outputBase == 10: |
|
return number |
|
elif inputBase != 10 and outputBase == 10: |
|
return convertToBase10(number, inputBase) |
|
elif inputBase == 10 and outputBase != 10: |
|
if outputBase == -1: |
|
numberBinary = convertFromBase10(number, 2, 8) |
|
return convertToTwoComplement(numberBinary) |
|
elif outputBase == -2: |
|
numberBinary = convertFromBase10(number, 2, 23) |
|
return convertToIEEE754SinglePrecision(numberBinary) |
|
elif outputBase == -3: |
|
numberBinary = convertFromBase10(number, 2, 52) |
|
return convertToIEEE754DoublePrecision(numberBinary) |
|
else: |
|
return convertFromBase10(number, outputBase, maxDecimals) |
|
elif inputBase != 10 and outputBase != 10: |
|
number = convertToBase10(number, inputBase) |
|
if outputBase == -1: |
|
numberBinary = convertFromBase10(number, 2, 8) |
|
return convertToTwoComplement(numberBinary) |
|
elif outputBase == -2: |
|
numberBinary = convertFromBase10(number, 2, 23) |
|
return convertToIEEE754SinglePrecision(numberBinary) |
|
elif outputBase == -3: |
|
numberBinary = convertFromBase10(number, 2, 52) |
|
return convertToIEEE754DoublePrecision(numberBinary) |
|
else: |
|
return convertFromBase10(number, outputBase, maxDecimals) |
|
|
|
|
|
def main(): |
|
number = str(input("Number to convert: ")).strip().upper() |
|
|
|
try: |
|
inputBase = int(input("Convert from base x: ")) |
|
outputBase = int(input("Convert to base y: ")) |
|
except ValueError: |
|
print("\nValid base should be an integer between [2, 36].\n") |
|
exit() |
|
|
|
convertedNumber = convertBase(number, inputBase, outputBase, 50) |
|
print( |
|
f"\nBase {inputBase}: {number}\ |
|
\nBase {outputBase}: {convertedNumber}\n" |
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
# execute only if run as a script |
|
main() |
|
|
|
|
|
# To add: |
|
# Accept E for scientific notation in base 10 input |