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

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Wyloguj /  Zmień )

Zdjęcie na Google

Komentujesz korzystając z konta Google. Wyloguj /  Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Wyloguj /  Zmień )

Zdjęcie na Facebooku

Komentujesz korzystając z konta Facebook. Wyloguj /  Zmień )

Połączenie z %s