1
\$\begingroup\$

I have written a small bit of code to tell me how many hours I need to work to finish up my 40 hours a week each day.

I can add hours per day and it will calculate total hours left in the work week.

My main issue with it is the way my math is written. It just seems so complicated and unnecessary but I was unable to find a better solution and was hoping someone could point me in the right direct for a better formula.

If you check the hours_left_update(self) method you will see that I am using floats, the Decimal method with str and quantize as well as split. All to try and get an output to a label that shows my results in hours and minutes.

Keep in mind when you put in hours worked the time clock system I get my information from uses a decimal system to display work hours. So if I worked 7 and 1/2 hours one day it would show me 7.50 hours worked. This is the kind of format I am putting into the program and it automatically convert that number to display as 7 hours 30 min.

from decimal import Decimal, ROUND_HALF_UP
from time import strftime
import tkinter as tk
import datetime


class TimeClockApp(tk.Frame):
    def __init__(self, master, *args, **kwargs):
        tk.Frame.__init__(self, master, *args, **kwargs)
        self.master = master
        self.hours_worked = 0.00
        self.dtlbl = tk.Label(self.master, text="Current Date-Time: {} ~ {}".format(datetime.date.today(),strftime("%H:%M:%S")))
        self.dtlbl.grid(row=0, column=0, columnspan=2)

        self.lbl1 = tk.Label(self.master, text="Hours worked").grid(row=1, column=0, columnspan=2)

        weekdays = ["Monday: ", "Tuesday: ", "Wednesday: ", "Thursday: ", "Friday: "]
        for i in range(5):
            tk.Label(self.master, text=weekdays[i], anchor = "e").grid(row=i+2, column=0, sticky="e")

        self.lbl7 = tk.Label(self.master, text="Total Hours worked:").grid(row=7, column=0, sticky="e")
        self.lbl8 = tk.Label(self.master, text="00h 0m")
        self.lbl8.grid(row=7, column=1, sticky="w")

        self.lbl9 = tk.Label(self.master, text="Hours left to 40:").grid(row=8, column=0, sticky="e")
        self.lbl10 = tk.Label(self.master, text="40h 0m")
        self.lbl10.grid(row=8, column=1, sticky="w")

        self.entry_list = []

        for i in range(5):
            self.entry_list.append(tk.Entry(self.master,width=7))
            self.entry_list[i].bind("<Key>", lambda x: self.master.after(500, self.hours_left_update))
            self.entry_list[i].grid(row = i+2, column = 1, sticky = "w")

        self.status_clock()

    def status_clock(self):
        self.dtlbl.config(text ="Current Date-Time: {} ~ {}".format(datetime.date.today(),strftime("%H:%M:%S")))
        self.dtlbl.after(1000, lambda: self.status_clock())

    def hours_left_update(self):
        for entry in self.entry_list:
            if entry.get() != "":
                try:
                    #print(Decimal(entry.get()).quantize(Decimal("0.01"), ROUND_HALF_UP))
                    self.hours_worked += float(entry.get())
                except:
                    pass

        x = str(Decimal(str(self.hours_worked)).quantize(Decimal("0.01"), ROUND_HALF_UP)).split('.')
        m60 = "00"

        if int(x[1]) != 0:
            m60 = Decimal(str(60 * float("0.{}".format(x[1])))).quantize(Decimal("1"))

        self.lbl8.config(text="{}h {}m".format(x[0], m60))

        var = 40 - int(x[0])
        if int(x[1]) != 0:
            var = 39 - int(x[0])

        x2 = "00"

        if Decimal(str(60 * float("0.{}".format(x[1])))).quantize(Decimal("1")) != 0:
            x2 = (60 - Decimal(str(60 * float("0.{}".format(x[1])))).quantize(Decimal("1")))

        self.lbl10.config(text="{}h {}m".format(var,x2))

        self.hours_worked = 0.00


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

It's a nice little program. Intuitively I'd prefer to input the times as 1:30 instead of 1.5, but that's just a personal preference.

The self.hours_worked field is redundant. It should simply be a local variable in the hours_left_update method. It's kind of weird that you reset it at the end of the method instead of initializing it at the beginning of the method. Anyway, just make it a local variable.

I played around a bit and came up with this code:

def hours_left_update(self):
    minutes_worked = 0
    for entry in self.entry_list:
        if entry.get() != "":
            try:
                minutes_worked += 60 * float(entry.get())
            except:
                pass

    minutes_worked = round(minutes_worked)   # just in case
    minutes_left = 40 * 60 - minutes_worked

    self.lbl8.config(text="{:02d}h {:02d}m".format(minutes_worked // 60, minutes_worked % 60))
    self.lbl10.config(text="{:02d}h {:02d}m".format(minutes_left // 60, minutes_left % 60))

Is there any reason for waiting 500 ms before updating the total times after each key press? I generally don't expect programs to have unnecessary delays.

I changed the output format for the two computed fields. As a result, the format changes after I press the first key because the labels are initialized with the old format. To make things consistent, I'd create the labels without any text and call self.hours_left_update() at the end of the constructor.


After cleaning up the rest of the code, it looks like this:

from time import strftime
import tkinter as tk
import datetime


class TimeClockApp(tk.Frame):
    def __init__(self, master, *args, **kwargs):
        tk.Frame.__init__(self, master, *args, **kwargs)

        self.now_lbl = tk.Label(master)
        self.now_lbl.grid(row=0, column=0, columnspan=2)

        tk.Label(master, text="Hours worked").grid(row=1, column=0, columnspan=2)

        self.entries = []
        weekdays = ["Monday: ", "Tuesday: ", "Wednesday: ", "Thursday: ", "Friday: "]
        for i in range(5):
            tk.Label(master, text=weekdays[i], anchor="e").grid(row=i+2, column=0, sticky="e")
            entry = tk.Entry(master, width=7)
            entry.grid(row=i+2, column=1, sticky="w")
            entry.bind("<Key>", lambda event: master.after(0, self.update_totals))
            self.entries.append(entry)

        tk.Label(master, text="Total Hours worked:").grid(row=7, column=0, sticky="e")
        self.time_worked_lbl = tk.Label(master)
        self.time_worked_lbl.grid(row=7, column=1, sticky="w")

        tk.Label(master, text="Hours left to 40:").grid(row=8, column=0, sticky="e")
        self.time_left_lbl = tk.Label(master)
        self.time_left_lbl.grid(row=8, column=1, sticky="w")

        self.update_clock()
        self.update_totals()

    def update_clock(self):
        now = "Current Date-Time: {} ~ {}".format(datetime.date.today(), strftime("%H:%M:%S"))
        self.now_lbl.config(text=now)
        self.now_lbl.after(200, self.update_clock)

    def update_totals(self):
        minutes_worked = 0
        for entry in self.entries:
            try:
                minutes_worked += 60 * float(entry.get())
            except:
                pass

        minutes_worked = round(minutes_worked)
        minutes_left = 40 * 60 - minutes_worked

        self.time_worked_lbl.config(text="{:02d}h {:02d}m".format(minutes_worked // 60, minutes_worked % 60))
        self.time_left_lbl.config(text="{:02d}h {:02d}m".format(minutes_left // 60, minutes_left % 60))

if __name__ == "__main__":
    root = tk.Tk()
    app = TimeClockApp(root)
    root.mainloop()

I did at least the following:

  • Start the method names with a verb.
  • Rename lbl8 to time_worked_lbl, and similar ones.
  • Only store those labels in self that are needed by the other methods.
  • Replace self.master with a simple master.
  • Made the formatting consisten, especially the spacing around named arguments.
  • Removed unnecessary lambdas.
  • Merge the two for loops into one.
  • Build the UI elements strictly from top to bottom.

Too bad that the grid method doesn't return self. That would have saved some more lines. :) By the way, this means that your code did something unexpected. It stored None into self.lbl1.

|improve this answer|||||
\$\endgroup\$
  • \$\begingroup\$ That is far less complicated than what I was doing. I knew I was going to far with my code. I was having an issue with float() that was returning a value of say 8.0100000000034 or something like that and it was difficult to get it to round properly. This issue seams to not exist with your example. It is worth noting that inputting data in decimal format was not by choice. The work time clock displays information in decimal format that is why I needed to have it converted by the program so I did not need to convert it myself. Thanks for your answer It works for me :) \$\endgroup\$ – Mike - SMT Sep 14 '17 at 17:38
  • \$\begingroup\$ As for the 500 ms wait time there was an issue where the self.hours_left_update method was being called instantly and it was reading the value of the entry field before a value was actually being imputed in it. So I added a delay to compensate. \$\endgroup\$ – Mike - SMT Sep 14 '17 at 17:41
  • \$\begingroup\$ I tried a delay of 0, which works fine. \$\endgroup\$ – Roland Illig Sep 14 '17 at 17:47
  • \$\begingroup\$ If you change lambda x: self.master.after(500, self.hours_left_update) to "<Key>", self.hours_left_update and change hours_left_update(self): to def hours_left_update(self, event):and you put say 8 in the first box with my original code you will see the problem with the labels not updated with the correct numbers. \$\endgroup\$ – Mike - SMT Sep 14 '17 at 17:50
  • \$\begingroup\$ actually if you make the change I mentioned even with your updated method there is still that problem. So the use of after() is still needed. \$\endgroup\$ – Mike - SMT Sep 14 '17 at 17:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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