Time Tracker Project

Posted on

Problem

I am working on a little Python project to track how much time I am spending coding and doing things outdoors. Coding subtracts and outdoors stuff adds.

My code is looking pretty bad. Any suggestions on how to make it better?

import tkinter as tk
import time
import json
import atexit
from flask import Flask

root = None
start_time = 0
end_time = 0
_add = False
_sub = False
entries_num = 0
_click = False
name = ''
_name = None
run_txt = None


def simplify(_time):
    return time.strftime('%H:%M:%S', time.localtime(_time))


def simple_mode(mode):
    if mode == 0:
        return "Add"
    elif mode == 1:
        return "Subtract"


def start():
    global start_time
    start_time = int(time.time())
    run_txt.configure(text="Running", fg="green")


def end():
    global end_time
    end_time = int(time.time())
    run_txt.configure(text="Not Running", fg="red")
    time_manager()


def find_mode():
    if _add and not _sub:
        return 0
    elif _sub and not _add:
        return 1


# 0 is add 1 is subtract
def time_manager():
    global entries_num
    entries_num += 1
    mode = simple_mode(find_mode())
    entry_raw = {'name': _name.get(), 'mode': mode, 'start': simplify(start_time), 'end': simplify(end_time), 'total_time': simplify(end_time-start_time-(-2211688800))}
    entries.update({str(entries_num): entry_raw})
    display()


def add():
    global _add
    global _sub
    _add = True
    _sub = False


def sub():
    global _add
    global _sub
    _add = False
    _sub = True


def display():
    height = 1
    width = 5
    cells = {}
    for i in range(height):  # Rows
        for j in range(width):  # Columns
            b = tk.Label(root, text="")
            b.grid(row=i + 16 + entries_num, column=j)
            cells[(i, j)] = b
    cells[(0, 0)]['text'] = entries[str(entries_num)]['name']
    cells[(0, 1)]['text'] = entries[str(entries_num)]['total_time']
    cells[(0, 2)]['text'] = entries[str(entries_num)]['mode']
    cells[(0, 3)]['text'] = entries[str(entries_num)]['start']
    cells[(0, 4)]['text'] = entries[str(entries_num)]['end']


class MainApplication(tk.Frame):
    def __init__(self, parent, *args, **kwargs):
        tk.Frame.__init__(self, parent, *args, **kwargs)
        self.parent = parent
        title_lbl = tk.Label(root)
        title_lbl['text'] = 'TimeTracker'
        title_lbl.configure(font=('Roboto', 20))
        title_lbl.grid(row=0, column=0, sticky='ew')
        srt_btn = tk.Button(root, text="Start", command=start, font=('Roboto', 8)).grid(row=10, column=0, sticky='sw')
        end_btn = tk.Button(root, text="End", command=end, font=('Roboto', 8)).grid(row=10, column=0, sticky='se')
        add_btn = tk.Button(root, text="Add Time", command=add, font=('Roboto', 8)).grid(row=11, column=0, sticky='se')
        sub_btn = tk.Button(root, text="Subtract Time", command=sub, font=('Roboto', 8))
        sub_btn.grid(row=12, column=0, sticky='se')
        global _name
        _name = tk.Entry(root, width=50)
        _name.grid(row=10, column=2)
        _name.insert(0, "Entry Name")
        _name.configure(state=tk.DISABLED)

        def on_click(event):
            _name.configure(state=tk.NORMAL)
            _name.delete(0, tk.END)

            # make the callback only work once
            _name.unbind('<Button-1>', on_click_id)

        on_click_id = _name.bind('<Button-1>', on_click)
        global run_txt
        run_txt = tk.Label(root)
        run_txt.configure(text="Not Running", fg="red")
        run_txt.grid(row=11, column=2)


def save():
    with open('data.json', 'w') as fb:
        _json = json.dumps(entries)
        fb.write(_json)


if __name__ == "__main__":
    root = tk.Tk()
    with open('data.json', 'r') as fr:
        text = fr.read()
        entries = json.loads(text)
        keys = entries.keys()
        for key in keys:
            entries_num = int(key)
            display()

    root.title("TimeTracker")
    root.minsize(200, 400)
    atexit.register(save)
    MainApplication(root).grid()
    root.mainloop()

Solution

The way you’re handling add “modes” is a little clunky. It seems that only one of _add or _sub should ever be true at the same time. By making these two separate variables, you need to make sure that invariant is maintained everywhere. You’ve definitely helped make sure it’s safe by using the add and sub functions, but those are band-aids on an awkward design.

The easiest way to simplify this is just to have one variable. Something like:

_is_adding = True

If this is true you’re adding, if it’s not, you’re subtracting. Now find_mode can be much simpler:

def find_mode():
    return 0 if _is_adding else 1

And add and sub simply becomes:

def add():
    global is_adding
    is_adding = True

def sub():
    global is_adding
    is_adding = False

Although,


I think you’re overusing globals here. In some cases, like UI callbacks, they may be the cleaner (but not only) option. find_mode for example though itself does not need to rely on globals. If you can cleanly pass data in instead of relying on globals, you should. I’d change it to:

def mode_number(is_adding: bool) -> int:  # I added some type hints in and renamed it
    return 0 if is_adding else 1

. . .

mode = simple_mode(mode_number(_is_adding))

The gain here is incredibly small because of how simple the functions involved are. In general though, it’s a good habit to get into. Functions that explicitly take as arguments all the data they need, and return all the data that they produce are much easier to understand.

simple_mode and find_mode are kind of odd though if you think about them. Why translate add/sub into a number just to then translate it back to "Add"/"Subtract"? I think this makes more sense:

def format_mode(is_adding: bool) -> str:
    return "Add" if is_adding else "Subtracting"

. . .

mode = format_mode(_is_adding)

I think simplify is a bad name for a function. What’s it “simplifying”? If I’m writing a function that turns something into a string, I usually call it format_* (although that’s just a habit of mine). Give some description though. I’d probably call it format_time, and adding some simple type hints make the types clearer:

def format_time(_time: int) -> str:
    return time.strftime('%H:%M:%S', time.localtime(_time))

I’m not sure I 100% agree with calling the parameter _time to avoid the name collision, since a prefix _ typically means a “private” attribute, but I can’t offhand think of a better name either, since the function is pretty broad in its purpose.


end_time-start_time-(-2211688800))

What the heck is -2211688800? That seems to offset the date by 70.1 years. Whatever it’s purpose I see two problems:

  • If that’s meant to offset today’s date to reach a certain format, it’ll be outdated from the second it’s written. You should see if you can automate the creation of that number.

  • Magic numbers, especially large unusual like that should be named. Something like:

    TIME_OFFSET = 2211688800
    
    . . .
    
    end_time - start_time - (-TIME_OFFSET)
    

Also, x - (-y) is the same as x + y (unless x and y are some custom type with odd math laws). It should just be:

end_time - start_time + TIME_OFFSET

The Flask import seems to be unneeded.

Leave a Reply

Your email address will not be published. Required fields are marked *