4
\$\begingroup\$

I'm doing some Python practice for fun and I wrote some code that converts between two bases. I'm thinking about expanding on this later, so the docstrings are pretty redundant from function to function.

Is this too much? Is there a way to make the documentation more clear or are there parts that I could just the let code speak for itself? Are there variables that could have been named better? Is the formatting okay? Is the way I split up the functions too disorganized? I'm assuming there are plenty of code optimizations here, but I haven't been coding too long, so I'm still getting a feel for a lot of formatting conventions as well.

def base_to_base(starting_base, starting_num, ending_base):
    """ Takes a number of a base between base 2 and 36 and converts it to
    another number of a base within the same constrictions.  Starting_base and
    ending_base are taken as integers and starting_num is taken as a string (as
    to handle alpha characters). The initial number is converted from the starting
    base to base ten and that number is then converted to the ending base.
    Returns the ending number as a string. Bases larger than 10 are handled with
    capital letters. Negative numbers and decimals are not handled. """
    # prevents unnecessary conversion
    if starting_base == 10:
        base_ten_num = int(starting_num)
    else:
        base_ten_num = base_to_ten(starting_num, starting_base)

    # prevents unnecessary conversion and returns result
    if ending_base == 10:
        return str(base_ten_num)
    else:
        return ten_to_base(base_ten_num, ending_base)


def base_to_ten(number, base):
    """ Takes a number of a base between base 2 and 36 and converts it to
    base 10. Number is taken as a string (as to handle alpha characters) and
    base is taken as an integer. The initial number is converted from its base
    to base ten and returned as an integer. Bases larger than 10 are handled
    with capital letters. Negative numbers and decimals are not handled. """
    # initial values
    base_ten_num = 0
    current_place = 0

    # iterates backwards through the number string. Each character's value is
    # converted to base 10 and the cumulative value is stored in base_ten_num
    for num_index in range(len(number)-1, -1, -1):
        # Assigns integer value to characters. Alpha characters are assigned
        # using unicode values. A's unicode value is 65 and its numberic value
        # in base ten is 10, so 55 would be subtracted from the unicode value.
        # The numbers progress from there. Current_value is always an integer
        # after the if statement.
        if number[num_index].isalpha():
            current_value = ord(number[num_index]) - 55
        else:
            current_value = int(number[num_index])

        # Each place value from right to left is worth the base to the next
        # power starting with 0. For example, when 101 (base 2) is converted to
        # base 10, it is 5. That's 1*2^0 + 0*2^1 + 1*2^2. 1+0+4=5.
        base_ten_num += current_value * base ** current_place
        current_place += 1

    return base_ten_num


def ten_to_base(base_ten_num, base):
    """ Takes a number of base ten and converts it to another number of a base
    between base 2 and base 36. Base_ten_num and base are taken as integers. The
    number of place values are determined and added to the ending_num string,
    which is returned. Bases larger than 10 are handled with capital letters.
    Negative numbers and decimals are not handled."""
    current_power = 0
    # continues to add 1 to current_power until current_power+1 is the number of
    # places in the converted number.
    while base ** (current_power+1) < base_ten_num:
        current_power += 1

    # inital value
    ending_num = ""

    # finds each place value
    while current_power >= 0:
        # floor division by place value. Base ** current_power is the value of
        # 1 in that place value, so base_ten_num divided by the power with no
        # remainder is that place value
        current_value = base_ten_num // base ** current_power

        # updates values for next loop
        base_ten_num -= current_value * base ** current_power
        current_power -= 1

        # Assigns a character to the place value. Values larger than 10 are
        # assigned alpha characters with unicode. A's unicode value is 65 and it
        # is worth 10, so 55 is added to get the unicode value. The values
        # progress from there. Current_value is always a string after the if
        # statement.
        if current_value > 9:
            current_value = chr(current_value + 55)
        else:
            current_value = str(current_value)

        # Adds the value's character to the number's string
        ending_num += current_value

    return ending_num


def get_starting_base():
    """ Input and verification of starting base, that it is a positive integer
    between 2 and 36. Returns starting_base as an integer."""
    # Gets initial input
    starting_base = input("starting base: ")

    # Prompts the user again if the input is a not a positive integer between 2
    # and 36.
    while not (starting_base.isdigit() and 1 < int(starting_base) < 37):
        print("Please enter a positive integer between 2 and 36")
        starting_base = input("starting base: ")

    return int(starting_base)


def get_starting_num(starting_base):
    """ Input and verification of starting number, that it is a positive integer
    using only characters from its base. Returns starting_num as a string."""
    # Assigns string which only contains characters from the given base
    base_members = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"[0:starting_base]

    # Gets initial input
    starting_num = input("starting number: ")

    # Assumes the input is proper, then gives it a change to prove us wrong.
    # If it uses characters that are not available in that base, the user will
    # be prompted again
    is_proper = True
    for character in starting_num:
        if character not in base_members:
            is_proper = False

    while not is_proper:
        print("Please only use characters in your base (Capital letters for " +
              "bases larger than than 10)" )
        starting_num = input("starting number: ")

        is_proper = True
        for character in starting_num:
            if character not in base_members:
                is_proper = False

    return starting_num


def get_ending_base():
    """ Input and verification of ending base, that it is a positive integer
    between 2 and 36. Returns ending_base as an integer."""
    # Gets initial input
    ending_base = input("ending base: ")

    # Prompts the user again if the input is not a positive integer between 2
    # and 36.
    while not (ending_base.isdigit() and 1 < int(ending_base) < 37):
        print("Please enter a positive integer between 2 and 36")
        ending_base = input("ending base: ")
    return int(ending_base)


if __name__ == "__main__":
    starting_base = get_starting_base()
    starting_num = get_starting_num(starting_base)
    ending_base = get_ending_base()
    ending_num = base_to_base(starting_base, starting_num, ending_base)
    print("ending number: " + ending_num)
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Using all

Your check for proper string :

is_proper = True
for character in starting_num:
    if character not in base_members:
        is_proper = False

can easily be rewritten with an additional break as there is not point in continuing once you've set is_proper to False.

Even better, you can rewrite this in a clean and efficient way using all :

is_proper = all(c in base_members for c in starting_num)

Logic flow

Then, the whole get_starting_num function can be rewritten :

def get_starting_num(starting_base):
    """ Input and verification of starting number, that it is a positive integer
    using only characters from its base. Returns starting_num as a string."""
    # Assigns string which only contains characters from the given base
    base_members = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"[0:starting_base]

    while True:
        starting_num = input("starting number: ")
        if all(c in base_members for c in starting_num):
            return starting_num
        print("Please only use characters in your base (Capital letters for " +
              "bases larger than than 10)")

The reasons why I prefer that way to write it :

  • we ask the user in a consistent way : the same line of code is used every time
  • we check for validity in a single place
  • it is easy that we will return only when we get a valid string.

Similarly, I'd rather write get_starting_base:

def get_starting_base():
    """ Input and verification of starting base, that it is a positive integer
    between 2 and 36. Returns starting_base as an integer."""
    while True:
        starting_base = input("starting base: ")
        if starting_base.isdigit() and 1 < int(starting_base) < 37:
            return int(starting_base)
        print("Please enter a positive integer between 2 and 36")

and get_ending_base:

def get_ending_base():
    """ Input and verification of ending base, that it is a positive integer
    between 2 and 36. Returns ending_base as an integer."""
    while True:
        ending_base = input("ending base: ")
        if ending_base.isdigit() and 1 < int(ending_base) < 37:
            return int(ending_base)
        print("Please enter a positive integer between 2 and 36")

Reusing code

Now that we've played a bit with get_(starting|ending)_base, we notice that they look very similar. It might be worth writing a more generic function to handle this :

def get_integer_in_range(prompt, mini, maxi):
    while True:
        starting_base = input(prompt)
        if starting_base.isdigit() and mini <= int(starting_base) <= maxi:
            return int(starting_base)
        print("Please enter a positive integer between %d and %d" % (mini, maxi))


def get_starting_base():
    return get_integer_in_range("starting base: ", 2, 36)

def get_ending_base():
    return get_integer_in_range("ending base: ", 2, 36)

Premature optimisation

Your check on if starting/ending_base == 10: makes your code more complicated and adds little. Also, it makes testing akward as it bypasses pretty much the only case I can check without any electronic device.

Naming

A few things are not that great in the naming of your functions and variables especially because it does not convey much information about the actual type of the data we are handling :

  • starting_num sounds like a integer to me. Surprise, it is a string.
  • base_ten_num seems to be a string representation of the number in base 10. It is not, it is actually an integer (the fact that it is in base 10 is irrelevant and actually wrong as it is probably not how it is stored internally). I think n is usually a good enough name. Similarly, base_to_ten and ten_to_base could be int_to_string or string_to_int.
  • number is actually a string and so is ending_num.

At this point, the code looks like :

def base_to_base(starting_base, string, ending_base):
    """ Takes a number of a base between base 2 and 36 and converts it to
    another number of a base within the same constrictions.  Starting_base and
    ending_base are taken as integers and starting_num is taken as a string (as
    to handle alpha characters). The initial number is converted from the starting
    base to base ten and that number is then converted to the ending base.
    Returns the ending number as a string. Bases larger than 10 are handled with
    capital letters. Negative numbers and decimals are not handled. """
    n = string_to_int(string, starting_base)
    return int_to_string(n, ending_base)


def string_to_int(string, base):
    """ Takes a number of a base between base 2 and 36 and converts it to
    base 10. Number is taken as a string (as to handle alpha characters) and
    base is taken as an integer. The initial number is converted from its base
    to base ten and returned as an integer. Bases larger than 10 are handled
    with capital letters. Negative numbers and decimals are not handled. """
    # initial values
    num = 0
    current_place = 0

    # iterates backwards through the number string. Each character's value is
    # converted to base 10 and the cumulative value is stored in base_ten_num
    for num_index in range(len(string)-1, -1, -1):
        # Assigns integer value to characters. Alpha characters are assigned
        # using unicode values. A's unicode value is 65 and its numberic value
        # in base ten is 10, so 55 would be subtracted from the unicode value.
        # The numbers progress from there. Current_value is always an integer
        # after the if statement.
        if string[num_index].isalpha():
            current_value = ord(string[num_index]) - 55
        else:
            current_value = int(string[num_index])

        # Each place value from right to left is worth the base to the next
        # power starting with 0. For example, when 101 (base 2) is converted to
        # base 10, it is 5. That's 1*2^0 + 0*2^1 + 1*2^2. 1+0+4=5.
        num += current_value * base ** current_place
        current_place += 1

    return num


def int_to_string(n, base):
    """ Takes a number of base ten and converts it to another number of a base
    between base 2 and base 36. Base_ten_num and base are taken as integers. The
    number of place values are determined and added to the ending_num string,
    which is returned. Bases larger than 10 are handled with capital letters.
    Negative numbers and decimals are not handled."""
    current_power = 0
    # continues to add 1 to current_power until current_power+1 is the number of
    # places in the converted number.
    while base ** (current_power+1) < n:
        current_power += 1

    # inital value
    string = ""

    # finds each place value
    while current_power >= 0:
        # floor division by place value. Base ** current_power is the value of
        # 1 in that place value, so n divided by the power with no
        # remainder is that place value
        current_value = n // base ** current_power

        # updates values for next loop
        n -= current_value * base ** current_power
        current_power -= 1

        # Assigns a character to the place value. Values larger than 10 are
        # assigned alpha characters with unicode. A's unicode value is 65 and it
        # is worth 10, so 55 is added to get the unicode value. The values
        # progress from there. Current_value is always a string after the if
        # statement.
        if current_value > 9:
            current_value = chr(current_value + 55)
        else:
            current_value = str(current_value)

        # Adds the value's character to the number's string
        string += current_value

    return string

(I couldn't be bothered updated the comments.)

Finding a bug

Before changing anything, I wanted to write a few tests to be sure I am aware if I break something :

for s in ["1", "4", "6", "10", "23", "7257"]:
    assert string_to_int(s, 10) == int(s)
for n in [1, 4, 6, 10, 23, 7257]:
    assert int_to_string(n, 10) == str(n)

I guess I got lucky (or inspired) because the value 10 leads to a bug.

I guess that while base ** (current_power+1) < n should be while base ** (current_power+1) <= n.

Different algorithms - string_to_int

When you convert, let's say "1234" to an integer, you are currently saying something like :

  • I have 4 digits therefore
  • First digit 1 is worth 1 * 10^(4-1)
  • Second digit 2 is worth 2 * 10^(4-2)
  • etc

There is an easier way : you just go through numbers and do something like :

  • My current total is initially 0
  • I have found a 1 : it is worth 1 and my current total is 0 + 1 = 1
  • I have found a 2 : it is worth 2 and my current total is 1*10 + 2 = 12
  • I have found a 3 : it is worth 3 and my current total is 12*10 + 3 = 123
  • etc

Corresponding code is :

def string_to_int(string, base):
    num = 0
    for digit in string:
        if digit.isalpha():
            current_value = ord(digit) - 55
        else:
            current_value = int(digit)
        num = base * num + current_value
    return num

Different algorithms - int_to_string

Here again you went for a complicated approach. The easy approach is to generate the string backward. When you are given a number, it is easy to know what the last digit it, you just perform a % base operation and that's it. Then you divide by base and you continue.

Using divmod, this can be concisely written :

def int_to_string(n, base):
    if n==0:
        return "0"

    string = ""
    while n:
        n, current_value = divmod(n, base)
        if current_value > 9:
            current_value = chr(current_value + 55)
        else:
            current_value = str(current_value)
        string = current_value + string
    return string

Various details

Because I have thrown-away a lot of you code, I didn't get a chance to comment on it much. It is usually a bad idea to use range and len to loop over something in Python : it is hard to get it right, it is not efficient, it is hard to read and there is a lot more to write.

Just look at these examples and tell me which one is simple to read/write :

>>> number = "358"
>>> for num_index in range(len(number)-1, -1, -1):
...     print(number[num_index])
>>> for num in reversed(number):
...     print(num)

Also, for your string slicing, 0 is the default value so you can write : base_members = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"[:starting_base]

Final code

At the end, my code looks like this. There is still a lot to improve (from a documentation point of view for instance).

def base_to_base(starting_base, string, ending_base):
    n = string_to_int(string, starting_base)
    return int_to_string(n, ending_base)

def string_to_int(string, base):
    num = 0
    for digit in string:
        if digit.isalpha():
            current_value = ord(digit) - 55
        else:
            current_value = int(digit)
        num = base * num + current_value
    return num

def int_to_string(n, base):
    if n==0:
        return "0"

    string = ""
    while n:
        n, current_value = divmod(n, base)
        if current_value > 9:
            current_value = chr(current_value + 55)
        else:
            current_value = str(current_value)
        string = current_value + string
    return string

def get_integer_in_range(prompt, mini, maxi):
    while True:
        starting_base = input(prompt)
        if starting_base.isdigit() and mini <= int(starting_base) <= maxi:
            return int(starting_base)
        print("Please enter a positive integer between %d and %d" % (mini, maxi))

def get_starting_num(starting_base):
    """ Input and verification of starting number, that it is a positive integer
    using only characters from its base. Returns starting_num as a string."""
    # Assigns string which only contains characters from the given base
    base_members = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"[:starting_base]

    while True:
        starting_num = input("starting number: ")
        if all(c in base_members for c in starting_num): 
            return starting_num
        print("Please only use characters in your base (Capital letters for " +
              "bases larger than than 10)")

if __name__ == "__main__":
    for s in ["0", "1", "4", "6", "10", "23", "7257"]:
        assert string_to_int(s, 10) == int(s)
    for n in [0, 1, 4, 6, 10, 23, 7257]:
        assert int_to_string(n, 10) == str(n)
    starting_base = get_integer_in_range("starting base: ", 2, 36)
    starting_num = get_starting_num(starting_base)
    ending_base = get_integer_in_range("ending base: ", 2, 36)
    ending_num = base_to_base(starting_base, starting_num, ending_base)
    print("ending number: " + ending_num)
\$\endgroup\$
2
  • \$\begingroup\$ is using this way better approach or using the default class int(str,base=10) taking this advantage to solve this problem . \$\endgroup\$ Jan 18, 2019 at 20:01
  • 1
    \$\begingroup\$ @prana it would be shorter and faster (and more correct). I handled the question as if it had a "reinvent the wheel" tag... \$\endgroup\$
    – SylvainD
    Jan 19, 2019 at 12:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.