2
\$\begingroup\$

Often before the lecture starts, and occasionally in the break I like to display a timer with a countdown until the lectures starts/ resumes while playing some lofi-hip hop in the background. While the university students seems to enjoy this, I am ashamed of the code running the timer.

So my lecture schedules is as follows

Monday:    10:15 -- 12:00
Wednesday: 12:15 -- 14:00
Thursday:  08:15 -- 10:00

I am interested in finding when my next lecture is in iso format. I did this using the code below and it works fine. E.g if the current time is 16:00 on a Thursday, the output should be Monday and 10:15. I commented out the actual timer, as it is not important.

  • Is there a better way / cleaner python code to obtain the date and time for my next lecture?

My attempt (which works, albeit a bit ugly) is as follows:

from datetime import datetime, timedelta
from subprocess import call

# Format (day, hour, minute)
Lectures = [(0, 10, 15), (), (2, 12, 15), (3, 8, 15)]


def get_next_lecture(now=datetime.now()):

    next_lecture = now.replace(minute=15, second=0, microsecond=0)

    # monday = 0, tuesday = 1, ...
    current_day = datetime.today().weekday()
    current_hour = now.hour
    lecture_day = current_day

    correct_day = False
    while not correct_day:
        # If the day is tuesday, increment to wednesday
        if current_day == 1:
            lecture_day = current_day + 1
            lecture_hour = Lectures[lecture_day][1]
            lecture_minute = Lectures[lecture_day][2]
            now += timedelta(days=1)
            correct_day = True
            # if the day is friday, increment to monday
        elif current_day == 4:
            lecture_day = 0
            lecture_hour = Lectures[lecture_day][1]
            lecture_minute = Lectures[lecture_day][2]
            now += datetime.today() + timedelta(days=3)
            correct_day = True
        else:
            # If it is not monday or friday, I have a lecture
            # checks if the lecture is in the future, if else increment day and try again
            if now.hour < Lectures[lecture_day][1]:
                if now.minute < Lectures[lecture_day][2]:
                    lecture_hour = Lectures[lecture_day][1]
                    lecture_minute = Lectures[lecture_day][2]
                    correct_day = True
                else:
                    current_day += 1
                    now += timedelta(days=1)
            else:
                current_day += 1
                now += timedelta(days=1)

    next_lecture = now.replace(
        hour=lecture_hour, minute=lecture_minute, second=0, microsecond=0
    )

    return next_lecture


def launch_timer(time):
    call(["termdown", time.isoformat()])


if __name__ == "__main__":

    next_lecture = get_next_lecture()
    print(lecture)
    # launch_timer(next_lecture)
\$\endgroup\$
2
\$\begingroup\$

Your algorithm seems good, but the while loop, and lecture_hour and lecture_minute variables make your code a lot more complicated.

If we KISS then a simple algorithm is to just remove () from Lectures and iterate through it, since it is sorted. The first lecture that is after the current time is the lecture we want.

This is nice and simple:

import datetime


LECTURES = [(0, 10, 15), (2, 12, 15), (3, 8, 15)]


def _get_next_lecture(now):
    today = (now.weekday(), now.hour, now.minute)
    for lecture in LECTURES:
        if today < lecture:
            return lecture


def get_next_lecture(now=None):
    if now is None:
        now = datetime.datetime.now()
    day, hour, minute = _get_next_lecture(now)
    return (
        now.replace(hour=hour, minute=minute, second=0, microsecond=0)
        + datetime.timedelta(day - now.weekday())
    )

From here we can see if the weekday is 4-6 then _get_next_lecture will return nothing and so will error.

This is easy to solve, we just return the first lecture with +7 days.

def _get_next_lecture(now):
    today = (now.weekday(), now.hour, now.minute)
    for lecture in LECTURES:
        if today < lecture:
            return lecture
    day, hour, minute = LECTURES[0]
    return day + 7, hour, minute

With only 3 lectures there's not much point in optimizing further. However if you have more, here is some food for thought:

  • You can use bisect to find where to insert into in \$O(\log n)\$ time.

  • You can change LECTURES into a 7 item list with the weekday as the index and the lectures as the value (always as a list). From here you just find the date using either of the above algorithms.

    This would look like your Lectures. But with a list for each day.

    This has either \$O(d)\$ or \$O(\log d)\$ time where \$d\$ is the maximum amount of lectures in a day.

Test code

def replace(date, changes):
    day, hour, minute = changes
    return date.replace(hour=hour, minute=minute) + datetime.timedelta(days=day)


def test(tests, bases, fn):
    for base in bases:
        date = base.replace(second=0, microsecond=0) - datetime.timedelta(days=base.weekday())
        for test, exp in tests:
            try:
                output = fn(replace(date, test))
            except Exception as e:
                print(f'❌ {test=}, {exp=}')
                print('  ', e)
                continue
            expected = replace(date, exp)
            try:
                assert output == expected
            except AssertionError:
                print(f'❌ {test=}, {exp=}')
                print('  ', date, output, expected)
            else:
                print(f'✔️ {test=}, {exp=}')

TESTS = [
    [(0, 0, 0), (0, 10, 15)],
    [(0, 10, 10), (0, 10, 15)],
    [(0, 10, 15), (2, 12, 15)],
    [(0, 10, 20), (2, 12, 15)],
    [(1, 12, 20), (2, 12, 15)],
    [(1, 13, 20), (2, 12, 15)],
    [(2, 10,  0), (2, 12, 15)],
    [(2, 10,  14), (2, 12, 15)],
    [(2, 12,  15), (3, 8, 15)],
    [(3, 8,  15), (7, 10, 15)],
]
BASES = [
    datetime.datetime.now(),
    datetime.datetime(2020, 9, 1),
    datetime.datetime(2020, 10, 1) - datetime.timedelta(days=1),
    datetime.datetime(2020, 12, 1),
    datetime.datetime(2021, 1, 1) - datetime.timedelta(days=1),
]

test(TESTS, BASES, get_next_lecture)
\$\endgroup\$
8
  • \$\begingroup\$ Great answer, gives me plenty to think about! I was contemplating using the actual weekday names Monday, Tuesday and then Thursday. As it would make it more readable than 0, 2 and 3. I can obtain the name of the current day using now.strftime("%A"), however it seems harder to iterate over. Thoughts on this? \$\endgroup\$ Sep 21 '20 at 16:32
  • \$\begingroup\$ @N3buchadnezzar You could change the last bullet point to "a dictionary with the weekday name as the key ..." and do that. I'd double check if %A is local aware, last thing you want is for it to spit out a non-English name because it's being too nice. (sorry can't remember which ones are) Otherwise it'd be alright, might make iterating a little harder tho if the current day has no lectures. \$\endgroup\$
    – Peilonrayz
    Sep 21 '20 at 16:38
  • \$\begingroup\$ @Pelionrayz There is something wrong with your code. You use day, but this returns the day of the month, you need to use weekday() =) It also seems to fail for the current time? Did you actually test that the code returns the desired output? Seems to be a problem with the and statements \$\endgroup\$ Sep 21 '20 at 17:10
  • \$\begingroup\$ @N3buchadnezzar No I didn't test it. I fixed those issues now and another :) My test case is small so there may still be some hidden ones :) \$\endgroup\$
    – Peilonrayz
    Sep 21 '20 at 17:39
  • \$\begingroup\$ Wont this fail if the month switches? Scratches head \$\endgroup\$ Sep 21 '20 at 17:47

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.