4
\$\begingroup\$

This program converts from number system A to number system B as soon as you input something in it, as a rookie it took a lot of thinking to make it work a seamless and fool-proof way.

It converts the number from base A to base 10 and then to base B because the formulas are always the same.

It is based on this website's version: https://codebeautify.org/all-number-converter

That said i still wanted to post the code here to receive feedback on the way it's structured so i don't pick up some bad habits:

import tkinter as tk
from tkinter import ttk


class MainApplication:
    numtoletter = {
        0: "0",
        1: "1",
        2: "2",
        3: "3",
        4: "4",
        5: "5",
        6: "6",
        7: "7",
        8: "8",
        9: "9",
        10: "A",
        11: "B",
        12: "C",
        13: "D",
        14: "E",
        15: "F",
        16: "G",
        17: "H",
        18: "I",
        19: "J",
        20: "K",
        21: "L",
        22: "M",
        23: "N",
        24: "O",
        25: "P",
        26: "Q",
        27: "R",
        28: "S",
        29: "T",
        30: "U",
        31: "V",
        32: "W",
        33: "X",
        34: "Y",
        35: "Z",
    }

    lettertonum = {
        "0": 0,
        "1": 1,
        "2": 2,
        "3": 3,
        "4": 4,
        "5": 5,
        "6": 6,
        "7": 7,
        "8": 8,
        "9": 9,
        "A": 10,
        "B": 11,
        "C": 12,
        "D": 13,
        "E": 14,
        "F": 15,
        "G": 16,
        "H": 17,
        "I": 18,
        "J": 19,
        "K": 20,
        "L": 21,
        "M": 22,
        "N": 23,
        "O": 24,
        "P": 25,
        "Q": 26,
        "R": 27,
        "S": 28,
        "T": 29,
        "U": 30,
        "V": 31,
        "W": 32,
        "X": 33,
        "Y": 34,
        "Z": 35,
    }

    numbercheck = {
        2: "01",
        3: "012",
        4: "0123",
        5: "01234",
        6: "012345",
        7: "0123456",
        8: "01234567",
        9: "012345678",
        10: "0123456789",
        11: "0123456789A",
        12: "0123456789AB",
        13: "0123456789ABC",
        14: "0123456789ABCD",
        15: "0123456789ABCDE",
        16: "0123456789ABCDEF",
        17: "0123456789ABCDEFG",
        18: "0123456789ABCDEFGH",
        19: "0123456789ABCDEFGHI",
        20: "0123456789ABCDEFGHIJ",
        21: "0123456789ABCDEFGHIJK",
        22: "0123456789ABCDEFGHIJKL",
        23: "0123456789ABCDEFGHIJKLM",
        24: "0123456789ABCDEFGHIJKLMN",
        25: "0123456789ABCDEFGHIJKLMNO",
        26: "0123456789ABCDEFGHIJKLMNOP",
        27: "0123456789ABCDEFGHIJKLMNOPQ",
        28: "0123456789ABCDEFGHIJKLMNOPQR",
        29: "0123456789ABCDEFGHIJKLMNOPQRS",
        30: "0123456789ABCDEFGHIJKLMNOPQRST",
        31: "0123456789ABCDEFGHIJKLMNOPQRSTU",
        32: "0123456789ABCDEFGHIJKLMNOPQRSTUV",
        33: "0123456789ABCDEFGHIJKLMNOPQRSTUVW",
        34: "0123456789ABCDEFGHIJKLMNOPQRSTUVWX",
        35: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXY",
        36: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ",
    }

    base = ["Base-2 (Binary)", "Base-3", "Base-4", "Base-5", "Base-6", "Base-7", "Base-8 (Octal)", "Base-9", "Base-10 (Decimal)", "Base-11", "Base-12", "Base-13", "Base-14", "Base-15", "Base-16 (Hexadecimal)", "Base-17",
            "Base-18", "Base-19", "Base-20", "Base-21", "Base-22", "Base-23", "Base-24", "Base-25", "Base-26", "Base-27", "Base-28", "Base-29", "Base-30", "Base-31", "Base-32", "Base-33", "Base-34", "Base-35", "Base-36", ]

    inputnum = "" # number to be converted
    outputnum = "" # result of the conversion
    inputbase = "" # base of the inputnum
    outputbase = "" # base of the outputnum
    outputnumto = 0 # where the outputnum goes
    varx = 0 # to make it so the function "convert" doesn't run when the value of stringvar is changed not through user input
    vary = 0 # to make it so the "convert" function doesn't run infinitely at line 233

    def __init__(self, master): # setting up the window and calling functions for gui creation
        self.master = master
        self.master.title("Number system converter")
        self.master.geometry("600x200")
        self.master.resizable(False, False)

        self.label_title, self.label_equals = self.create_text()
        self.stringvar_number1, self.entry_number1, self.stringvar_number2, self.entry_number2 = self.create_number_input()
        self.combobox_base1, self.combobox_base2 = self.create_base_selection()

    def create_text(self):
        label_title = tk.Label(self.master, text="NUMBER SYSTEM CONVERTER", font=(
            "Calibri", 20, "bold"), anchor=tk.CENTER,)
        label_title.place(x=100, y=0, width=400, height=50)

        label_equals = tk.Label(self.master, text="=", font=(
            "Calibri", 20), anchor=tk.CENTER, justify=tk.CENTER)
        label_equals.place(x=275, y=65, width=50, height=30)

        return label_title, label_equals

    def create_number_input(self): # entry(s) for input and stringvars
        stringvar_number1 = tk.StringVar()
        stringvar_number1.set("1")
        stringvar_number1.trace("w", lambda x, y, z: self.convert(2))

        entry_number1 = tk.Entry(self.master, textvariable=stringvar_number1, font=(
            "Calibri", 16), borderwidth=0, bg="white",)
        entry_number1.place(x=50, y=65, width=225, height=30)

        stringvar_number2 = tk.StringVar()
        stringvar_number2.set("1")
        stringvar_number2.trace("w", lambda x, y, z: self.convert(1))

        entry_number2 = tk.Entry(self.master, textvariable=stringvar_number2, font=(
            "Calibri", 16), borderwidth=0, bg="white",)
        entry_number2.place(x=325, y=65, width=225, height=30)

        return stringvar_number1, entry_number1, stringvar_number2, entry_number2

    def create_base_selection(self): # combobox(s) for selecting the base
        combobox_base1 = ttk.Combobox(
            self.master, values=self.base, font=("Calibri", 14), state="readonly")
        combobox_base1.place(x=50, y=100, width=225, height=30)

        popdown_base1 = self.master.tk.call(
            "ttk::combobox::PopdownWindow", combobox_base1)
        self.master.tk.call(f"{popdown_base1}.f.l",
                            "configure", "-font", "Calibri 14")

        combobox_base1.current(0)

        combobox_base1.bind("<<ComboboxSelected>>",
                            lambda x: self.inputbase_or_outputbase())

        combobox_base2 = ttk.Combobox(
            self.master, values=self.base, font=("Calibri", 14), state="readonly")
        combobox_base2.place(x=325, y=100, width=225, height=30)

        popdown_base2 = self.master.tk.call(
            "ttk::combobox::PopdownWindow", combobox_base2)
        self.master.tk.call(f"{popdown_base2}.f.l",
                            "configure", "-font", "Calibri 14")

        combobox_base2.current(0)

        combobox_base2.bind("<<ComboboxSelected>>",
                            lambda x: self.inputbase_or_outputbase())

        return combobox_base1, combobox_base2

    def inputbase_or_outputbase(self): # determining if a base belongs to the input or output number
        if self.outputnumto == 2:
            self.inputbase = self.combobox_base1.get()[5:7]
            self.outputbase = self.combobox_base2.get()[5:7]
        elif self.outputnumto == 1:
            self.inputbase = self.combobox_base2.get()[5:7]
            self.outputbase = self.combobox_base1.get()[5:7]

        if self.vary == 0:
            self.convert(self.outputnumto)

    def convert(self, n): # determining which entry modified last (through stringvar tracing) + the process of converting input number in output base (outputbase determined through the previous function)
        if self.varx == 1:
            self.varx = 0
            return

        self.outputnumto = n

        if self.outputnumto == 2:
            self.inputnum = self.entry_number1.get().upper()

        elif self.outputnumto == 1:
            self.inputnum = self.entry_number2.get().upper()

        self.vary = 1
        self.inputbase_or_outputbase()
        self.vary = 0

        if not self.inputbase or not self.outputbase:
            return

        for x in self.inputnum:
            if not x in self.numbercheck[int(self.inputbase)]:
                if self.outputnumto == 1:
                    self.varx = 1
                    self.stringvar_number1.set("Invalid input")
                elif self.outputnumto == 2:
                    self.varx = 1
                    self.stringvar_number2.set("Invalid input")
                return

        base10num = self.convert_to_base10(self.inputbase, self.inputnum)
        outputnum = self.convert_to_baseo(self.outputbase, base10num)

        if self.outputnumto == 1:
            self.varx = 1
            self.stringvar_number1.set(outputnum)
        elif self.outputnumto == 2:
            self.varx = 1
            self.stringvar_number2.set(outputnum)

    def convert_to_base10(self, inputbase, inputnum): # converting inputnum to base10
        inputbase = int(inputbase)
        inputnum = inputnum[::-1]
        multiply = 1
        base10num = 0

        for x in inputnum:
            x = self.lettertonum[x]
            base10num = (x * multiply) + base10num
            multiply = multiply * inputbase

        return base10num

    def convert_to_baseo(self, outputbase, base10num): # converting inputnum to outputbase
        outputbase = int(outputbase)
        base10num = int(base10num)
        outputnum = ""

        while base10num > 0:
            outputnum = outputnum + self.numtoletter[base10num % outputbase]
            base10num = base10num // outputbase

        outputnum = outputnum[::-1]

        return outputnum


if __name__ == "__main__":
    root = tk.Tk()
    app = MainApplication(root)
    root.mainloop()
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

In no particular order:

  • numtoletter and similar variables and method names should be num_to_letter (lower_snake_case)
  • numtoletter and lettertonum should not be written out like that, and should be formed via loops - which computers are good at
  • numbercheck is not necessary if you listen for ValueErrors from int()
  • base should also be formed via a loop
  • you have a bunch of symbols initialized in static scope of your class. Moving your constants between there and global scope is debatable, but inputnum through varx should not exist at all.
  • Don't call things master.
  • You've accepted a parent widget parameter to your constructor - this is good. In theory it means that you could populate any widget with your converter. However, it also means that your class instance does not own the parent, and so it should not be responsible for setting title and geometry.
  • Your title label is unnecessary - you've already set a title on the parent window decorator.
  • You should generally avoid place and instead favour grid-like placement. This is less fragile and manual. You can still add padding, etc. if you want to.
  • Some of your visual modifications such as giant font aren't all that useful. Generic UI scaling in tkinter is sort of a mess, and would still require that you modify your fonts; but my opinion is: the user has control of their window manager, and all self-respecting window managers have some kind of utility to apply zoom.
  • You do not need to save references to any of your widgets - just your variables.
  • You should always pass parent, name and value parameters to your tk variables on construction. This will obviate your first call to set.
  • Avoid lambdas. You can just make a bound method on your class for your traces. Do not fake the names x, y and z; those are actually name, index and mode.
  • When tk sets the background colour of a combo box to grey, it's trying communicate something: that the input half of the combo box is read-only, and can only be changed via dropdown. My opinion is that it's confusing to set this to white, and the default should be left.
  • You used StringVar on your inputs - good! You should do the same with your combo boxes! This will obviate your call to current().
  • Don't call bind at all. This whole machinery of input-or-output-base and last-known-edited-input is too complicated. You can figure out which input was just edited by which trace was called; and assuming that a change in base of the left-hand side auto-converts the right-hand value is intuitive enough that I don't find it useful to do anything more complex.
  • The n parameter to convert is only ever one of two values, so I recommend changing it to a forward: bool. Or, as I've shown, just accept a source and destination group object that holds references to your number and base variables.
  • Add PEP484 type hints.
  • trace() is deprecated and should be trace_add().

Suggested

from string import ascii_uppercase
import tkinter as tk
from tkinter import ttk
from typing import Tuple, Iterator, Dict, List, Callable


def base_pairs() -> Iterator[Tuple[str, int]]:
    for base in range(2, 37):
        desc = f'Base-{base}'
        name = BASE_NAMES.get(base)
        if name is not None:
            desc += f' ({name})'
        yield desc, base


BASE_NAMES = {
    2: 'Binary',
    8: 'Octal',
    10: 'Decimal',
    16: 'Hexadecimal',
}
BASES: Dict[str, int] = dict(base_pairs())
DEFAULT_BASE = next(iter(BASES.keys()))
NUM_TO_LETTER: List[str] = [
    *(str(x) for x in range(10)),
    *ascii_uppercase,
]


def convert_to_base(output_base: int, base10_num: int) -> str:
    """converting inputnum to outputbase"""
    output_num = ''

    while base10_num > 0:
        base10_num, remainder = divmod(base10_num, output_base)
        output_num = NUM_TO_LETTER[remainder] + output_num

    return output_num


class InputGroup:
    def __init__(
        self, parent: tk.Tk, index: int,
        convert: Callable[[], None],
    ) -> None:
        self.convert = convert

        self.number_var = tk.StringVar(
            parent, name=f'number{index}', value='1',
        )
        self.set_trace()
        tk.Entry(
            parent, textvariable=self.number_var,
        ).grid(row=0, column=2*index)

        self.base_var = tk.StringVar(
            parent, name=f'base{index}', value=DEFAULT_BASE,
        )
        self.base_var.trace_add(mode='write', callback=self.trace)

        names = tuple(BASES.keys())
        ttk.Combobox(
            parent, values=names, state='readonly',
            textvariable=self.base_var,
        ).grid(row=1, column=2*index)

    def set_trace(self) -> None:
        self.trace_id = self.number_var.trace_add(mode='write', callback=self.trace)

    def trace(self, name: str, index: str, mode: str) -> None:
        self.convert()

    @property
    def base(self) -> int:
        return BASES[self.base_var.get()]

    @property
    def value(self) -> int:
        value = self.number_var.get().upper()
        return int(value, self.base)

    @value.setter
    def value(self, value: int) -> None:
        as_base = convert_to_base(self.base, value)
        self.set_output(as_base)

    def set_output(self, value: str) -> None:
        self.number_var.trace_remove(mode='write', cbname=self.trace_id)
        try:
            self.number_var.set(value)
        finally:
            self.set_trace()


class MainApplication:
    def __init__(self, parent: tk.Tk) -> None:
        """setting up the window and calling functions for gui creation"""
        self.parent = parent

        tk.Label(self.parent, text='=').grid(row=0, column=1)

        self.groups = (
            InputGroup(parent, index=0, convert=self.convert_forward),
            InputGroup(parent, index=1, convert=self.convert_backward),
        )

    def convert_forward(self) -> None:
        self.convert(*self.groups)

    def convert_backward(self) -> None:
        self.convert(*self.groups[::-1])

    def convert(self, source: InputGroup, dest: InputGroup) -> None:
        try:
            dest.value = source.value
        except ValueError:
            dest.set_output('Invalid input')


def main() -> None:
    root = tk.Tk()
    root.title('Number system converter')
    root.resizable(width=False, height=False)
    MainApplication(root)
    root.mainloop()


if __name__ == '__main__':
    main()
\$\endgroup\$
0

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.