TL;DR: Byłem tak blisko i tak daleko od znalezienia rozwiązania. Po prostu całe zamieszanie wyniknęło z tego, że lista wpisów była tworzona odwrotnie. Tzn. ostatni element dodawany był na początku a nie na końcu przez co pętla wykonywała się w drugą stronę. Dlaczego tak się stało oraz wnioski we wpisie.
Jeśli nie wiesz o czym piszę i masz trochę czasu do stracenia to zajrzyj do poprzedniego wpisu w którym błądzę i motam się w poszukiwaniu rozwiązania.
Najdziwniejsze i jednocześnie dające sporo do myślenia jest to, że byłem pewien, że lista tworzy się po bożemu za pomocą .append(), więc nawet w swoich próbach rozwiązania problemu nie brałem pod uwagę, że może być inaczej.
Pokaż się
I tutaj nasuwa się pierwszy wniosek. Warto pokazać komuś kod i poprosić, żeby mu się przyjrzał świeżym okiem. Coś co mi zajęło mnóstwo godzin, slackowiczom zajęło kilkanaście minut. Swoją drogą ciekawe czy bym na to sam wpadł? Pewnie tak, ale ciekawe kiedy?
Nie jest tak źle
Kiedy tak błądziłem i nie rozumiałem dlaczego ten kod robi dziwne rzeczy podstawową rzecz którą założyłem to to, że prawdopodobnie nie mam pojęcia jak działa pętla i w ogóle robię wszystko bez sensu i każdy sensowny programista rozwiązałby ten problem jedną wbudowaną metodą. Wniosek taki, że nie jest ze mną tak źle a plus taki, że znów się zmotywowałem do działania.
Nie gryzą
Powiem szczerze, że trochę bałem się pytać o porady. Nie chciałem być uznany za spamera. Druga sprawa, że wcale nie jest łatwo w kilku zdaniach wytłumaczyć na czym polega problem i o co ci chodzi. Wniosek taki, że lepiej poświęcić trochę czasu na wytłumaczenie problemu (czasem samo sformułowanie pytania może przynieść odpowiedź) niż kręcić się wkoło Macieju. No i okazuje się, że ludzie są bardzo pomocni i nie wysyłają do Google (tam warto zajrzeć wcześniej i ja to robię zawsze wielkrotnie).
A dlaczego kod dodawania entry wygląda tak:
def add(self): # Entries are added on the beginning of list. This is because I want them show up on top of the listbox. # I can reversed it, but then there is an issue with adding item on current position. if str(top_frame_input.get()) != '00:00:00': index = 0 entry = Entry(select_mode.getvalue(), top_frame_input.get()) try: index = entries_list.curselection()[0] # try get position where entry should be added except IndexError: pass self.records.insert(index, entry) top_frame_input.setentry('00:00:00') top_frame_input.select_range(3, 5) # entry field should be focus on minutes top_frame_input.icursor(5) self.info() self.update()
Otóż na początku miałem dodawanie elementu przez .append, dopiero potem w momencie tworzenia się listbox lista była odwracana. Chciałem po prostu, żeby użytkownik miał najnowszy wpis na górze.
Chciałem dobrze
Problem pojawił się jednak kiedy zechciałem dodać możliwość dodania entry gdzieś w środku listy. Przez to, że lista jest odwrócona jeśli zaznaczam np. trzecią pozycję od góry to w praktyce zaznaczam pozycję x od dołu.
Cały ten bałagan wynika z tego, ze nie chciałem, żeby użytkownik pracował na listboxie. Tak naprawdę wszystko co on robi, robi od razu na self.records a dopiero na tej podstawie tworzony jest listbox. Dlaczego tak? Ano dlatego, że self.records będzie docelowo zawierał dane z 28 dni a listbox będzie wyświetlać tylko tyle ile użytkownik będzie w danym momencie potrzebował.
Zostawiam i idę dalej
Na razie postanowiłem zostawić kod taki jakim jest i po prostu przy obliczaniu czy była przerwa odwracam listę za pomocą for x in reversed(self.records):.
def was_break(self): driving_time = 0 # can't be more than 4,5h before break working_time = 0 # it's time of work or driving and can't be more than 6h infringements_list = [] first_break = False second_break = False for x in reversed(self.records): if x.get_mode() == 'D': driving_time += x.get_value() if x.get_mode() == 'D' or x.get_mode() == 'W': working_time += x.get_value() if x.get_mode() == 'R' and (900 <= x.get_value() < 1800): first_break = True # First break must be at least 15 minutes and second 30 minutes. if x.get_mode() == 'R' and (1800 <= x.get_value() < 2700): if first_break is True: second_break = True else: first_break = True if (x.get_mode() == 'R' and x.get_value() >= 2700) or (second_break is True): if driving_time > 16200: info = "Break after 4,5h driving needed" infringements_list.append(info) driving_time = 0 if working_time > 21600: info_break = "Break after 6h work needed" infringements_list.append(info_break) working_time = 0 first_break = False second_break = False if driving_time > 16200: info = "Break after 4,5h driving needed" infringements_list.append(info) driving_time = 0 if working_time > 21600: info_break = "Break after 6h work needed" infringements_list.append(info_break) working_time = 0 if not infringements_list: return "No infringements found" else: all_infringements = '' for x in set(infringements_list): all_infringements += "{0}: {1}\n".format(x,infringements_list.count(x)) return "Infringements:\n"+all_infringements
Nie wszystko musi być od razu idealne
Być może będę musiał się tym mimo wszystko zająć albo po prostu zrezygnuję z dodawania nowego entry na górze, bo może wcale nie jest to dobre rozwiązanie. W każdym razie jest to coś co może poczekać. Taki wniosek też z tego wyciągnałem. Nie ma co robić wszystkiego na raz. Czasem można przymknąć oko na małe niedociągnięcia, które nie są kluczowe. Inaczej wpada się w pułapkę perfekcjonizmu, która może nawet doprowadzić do rzucenia wszystkiego w cholerę. A tego nie chcemy.
Ostatni wniosek jest taki, że czas wielki nauczyć się czym jest debuggowanie i jak się je robi. Lista zadań do zrobienia już zaktualizowana. Będę się dzielić jak się czegoś dowiem.
Taka to krótka historia i rozwiązanie mojego problemu. Dziękuję każdemu kto zechciał mi pomóc. Super. Jedziemy dalej.
Cały kod:
from tkinter import * import re import datetime import Pmw def start(): global current_data current_data = Data() current_data.update() current_data.info() def add_entry(): current_data.add() def clear_all(): # clear top_frame_input top_frame_input.delete(0, END) top_frame_input.focus() def clear_one(): # clear last digit from top_frame_input user_input = top_frame_input.get()[:-1] top_frame_input.delete(0, END) top_frame_input.insert(0, user_input) def num_press(num): # num pad button action if top_frame_input.get() == '00:00:00': clear_all() if num == "C": clear_all() elif num == ".": # doesn't work because counter doesn't allow insert other thinks than numbers and ":" top_frame_input.insert(END, ":") else: top_frame_input.insert(END, num) top_frame_input.focus() # =======tkinter window=========== win = Tk() # win.geometry("775x325") # Force window size win.wm_title("Tacho 0.0.4") win.resizable(width=FALSE, height=FALSE) Pmw.initialise(win) ''' topFrame = Frame(win) # topFrame.pack(fill=BOTH) topFrame.grid(row=0, column=0, columnspan=2) ''' topLeftFrame = Frame(win) topLeftFrame.grid(row=0, column=0) topRightFrame = Frame(win) topRightFrame.grid(row=0, column=1) leftFrame = Frame(win) leftFrame.grid(row=1, column=0) rightFrame = Frame(win) rightFrame.grid(row=1, column=1) bottomFrame = Frame(win) bottomFrame.grid(row=2, columnspan=2) # -------keys actions----- win.bind("<Return>", lambda a: add_entry()) win.bind("<KP_Enter>", lambda a: add_entry()) win.bind("<KP_Add>", lambda a: add_entry()) # win.bind("<KP_Decimal>", lambda a: num_press(":")) # ==========input entry=========== top_frame_input = Pmw.Counter(topRightFrame, entry_font="Helvetica 20 bold", entry_width=12, autorepeat=True, datatype='time', entryfield_validate={'validator': 'time'}, entryfield_value='00:00:00', increment=60) top_frame_input.grid(column=0) top_frame_input.component('entry').focus_set() top_frame_input.select_range(3, 5) top_frame_input.icursor(5) # ===========Listbox with scrollbar================= entries_list = Pmw.ScrolledListBox(leftFrame, hscrollmode='none', vscrollmode='static', listbox_height=15, listbox_width=40) ''' entries_list = Pmw.ComboBox(leftFrame, dropdown = 0, scrolledlist_vscrollmode = 'static', scrolledlist_hscrollmode = 'none', scrolledlist_listbox_height = 15, scrolledlist_listbox_width=40, entryfield_validate = {'validator' : 'time'}, entryfield_value = '00:00:00', ) ''' entries_list.grid(row=0, column=0) # =======buttons=========== # -------num pad----------- keyboard = [] keys = "789456123C0:" i = 0 for j in range(1, 5): for k in range(3): keyboard.append(Button(rightFrame, text=keys[i], font="Helvetica 15 bold", height=1, width=2)) keyboard[i].grid(row=j, column=k, pady=2, padx=2) keyboard[i]["command"] = lambda x=keys[i]: num_press(x) i += 1 # --------other buttons in num pad--------- add_entry_button = Button(rightFrame, text='+', font="Helvetica 15 bold", height=6, width=7, command=add_entry) add_entry_button.grid(row=1, column=3, rowspan=4, columnspan=2, pady=2, padx=2) clear_one_button = Button(rightFrame, text="←", font="Helvetica 15 bold", height=1, width=2, command=clear_one) clear_one_button.grid(row=0, column=4, pady=2, padx=2) # ------buttons to change mode of entry------- select_mode = Pmw.RadioSelect(rightFrame, Button_height=1, Button_width=2, Button_font="Helvetica 15 bold", pady=2, padx=2) select_mode.grid(row=0, column=0, columnspan=4) # Add some buttons to the horizontal RadioSelect - mode selection buttons. for name, symbol, background in (('D', u'\u2609', 'green'), ('W', u'\u2692', 'blue'), ('P', u'\u26DD', 'yellow'), ('R', u'\u29E6', 'red')): select_mode.add(name, text=symbol, background=background) select_mode.invoke(3) # select break/rest as default # ------top left buttons------- top_left_buttons = Pmw.ButtonBox(topLeftFrame, Button_height=1, # Button_width=2, Button_font="Helvetica 15 bold", pady=1, padx=1) top_left_buttons.grid(row=0, column=0, columnspan=2) # Add some buttons to the horizontal RadioSelect. top_left_buttons.add('Delete', command=lambda: current_data.delete_item()) top_left_buttons.add('Edit') top_left_buttons.add('Save') top_left_buttons.add('Clear', command=start) # =====bottom status============= status = StringVar() bottom_status_total = Label(bottomFrame, textvariable=status, bd=1, relief=SUNKEN, font="Helvetica 15 bold", width=54) status.set("") bottom_status_total.pack(fill=X, expand=True, side=TOP, ipady=10, ipadx=10) # ============================================ class Data: def __init__(self): self.records = [] @staticmethod # this let this method be called in class or outside def converter(sec): if sec < 0: # This is temporary solution, there is a problem with negative modulo sec *= -1 conversion = '%d:%02d:%02d' % (sec / 3600, sec / 60 % 60, sec % 60) return '-'+str(conversion) else: conversion = '%d:%02d:%02d' % (sec / 3600, sec / 60 % 60, sec % 60) # convert to HH:MM:SS return conversion def add(self): # Entries are added on the beginning of list. This is because I want them show up on top of the listbox. # I can reversed it, but then there is an issue with adding item on current position. if str(top_frame_input.get()) != '00:00:00': index = 0 entry = Entry(select_mode.getvalue(), top_frame_input.get()) try: index = entries_list.curselection()[0] # try get position where entry should be added except IndexError: pass self.records.insert(index, entry) top_frame_input.setentry('00:00:00') top_frame_input.select_range(3, 5) # entry field should be focus on minutes top_frame_input.icursor(5) self.info() self.update() def delete_item(self): # delete a selected line from the listbox and from entries try: # get selected line index index = entries_list.curselection()[0] entries_list.delete(index) # delete item from listbox in GUI self.records.pop(index) # delete item from data list self.update() self.info() except IndexError: pass def sum(self, v): total = 0 if v == 'total': for v in self.records: total += v.get_value() return total else: # sum for mode summary = 0 for x in self.records: if v == x.get_mode(): summary += x.get_value() return summary def time_remaining(self, v): if v == 'total': total_remaining = 54000 # 15h = 54000 seconds total_remaining -= self.sum('total') if total_remaining < 0: total_remaining = self.converter(total_remaining) return str(total_remaining)+' TIME OUT!' return self.converter(total_remaining) # remaining time for modes elif v == 'D': driving_remaining = 36000 # 10h driving_remaining -= self.sum('D') if driving_remaining < 0: return str(self.converter(driving_remaining))+' TIME OUT!' else: return self.converter(driving_remaining) def was_break(self): driving_time = 0 # can't be more than 4,5h before break working_time = 0 # it's time of work or driving and can't be more than 6h infringements_list = [] first_break = False second_break = False for x in reversed(self.records): if x.get_mode() == 'D': driving_time += x.get_value() if x.get_mode() == 'D' or x.get_mode() == 'W': working_time += x.get_value() if x.get_mode() == 'R' and (900 <= x.get_value() < 1800): first_break = True # First break must be at least 15 minutes and second 30 minutes. if x.get_mode() == 'R' and (1800 <= x.get_value() < 2700): if first_break is True: second_break = True else: first_break = True if (x.get_mode() == 'R' and x.get_value() >= 2700) or (second_break is True): if driving_time > 16200: info = "Break after 4,5h driving needed" infringements_list.append(info) driving_time = 0 if working_time > 21600: info_break = "Break after 6h work needed" infringements_list.append(info_break) working_time = 0 first_break = False second_break = False if driving_time > 16200: info = "Break after 4,5h driving needed" infringements_list.append(info) driving_time = 0 if working_time > 21600: info_break = "Break after 6h work needed" infringements_list.append(info_break) working_time = 0 if not infringements_list: return "No infringements found" else: all_infringements = '' for x in set(infringements_list): all_infringements += "{0}: {1}\n".format(x,infringements_list.count(x)) return "Infringements:\n"+all_infringements def update(self): entries_list.delete(0, END) line_number = len(self.records) for record in self.records: entries_list.insert(END, str(line_number) + ') ' + str(record)) # color specific lines: if record.get_mode() == 'R': entries_list.itemconfig(END, {'bg': 'red'}, foreground='white') elif record.get_mode() == 'P': entries_list.itemconfig(END, {'bg': 'yellow'}) elif record.get_mode() == 'W': entries_list.itemconfig(END, {'bg': 'blue'}, foreground='white') elif record.get_mode() == 'D': entries_list.itemconfig(END, {'bg': 'green'}, foreground='white') line_number -= 1 def info(self): status.set('Total time: ' + str(self.converter(self.sum('total')))+' / day time remaining: ' + str(self.time_remaining('total'))+'\n' + '\n' + 'Driving: '+str(self.converter(self.sum('D'))) + ' / time remaining: '+str(self.time_remaining('D'))+'\n' + 'Work: '+str(self.converter(self.sum('W')))+' | ' + 'POA: '+str(self.converter(self.sum('P')))+' | ' + 'Rest: '+str(self.converter(self.sum('R')))+'\n' + '\n' + str(self.was_break()) ) class Entry: def __init__(self, mode, value): self.mode = mode self.value = value self.cleaner() # Before entry will be added it has to be cleaned from user mistakes and converted from HH:MM:SS to seconds def cleaner(self): user_input = str(self.value) user_input = re.sub('[^0-9:]', '', user_input) # leaves only digits and ":" into input try: h, m, s = re.split(':', user_input) self.value = int(datetime.timedelta(hours=int(h), minutes=int(m), seconds=int(s)).total_seconds()) except ValueError: try: m, s = re.split(':', user_input) self.value = int(datetime.timedelta(minutes=int(m), seconds=int(s)).total_seconds()) except ValueError: self.value = re.sub('\D', '', user_input) # clean input from non-digit characters self.value = int(datetime.timedelta(minutes=int(self.value)).total_seconds()) # self.input = self.input.replace("+", "") # simplest method (replace just one character) # in case this one above will make a troubles def get_value(self): return self.value def get_mode(self): return self.mode def __str__(self): mode_names = {'D': 'Driving', 'W': 'Work', 'P': 'POA/availability', 'R': 'Rest/Break'} conversion = Data().converter(self.value) # call static method from class Data # sec = self.value # conversion = '%d:%02d:%02d' % (sec / 3600, sec / 60 % 60, sec % 60) # convert to HH:MM:SS return conversion + ' ' + mode_names[self.mode] start() win.mainloop()
2 uwagi do wpisu “Ulżyło! Dzięki za pomoc”