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)
asked Sep 21, 2020 at 15:24
\$\endgroup\$

1 Answer 1

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)
answered Sep 21, 2020 at 16:15
\$\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\$ Commented Sep 21, 2020 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\$ Commented Sep 21, 2020 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\$ Commented Sep 21, 2020 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\$ Commented Sep 21, 2020 at 17:39
  • \$\begingroup\$ Wont this fail if the month switches? Scratches head \$\endgroup\$ Commented Sep 21, 2020 at 17:47

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.