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)
1 Answer 1
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)
-
\$\begingroup\$ Great answer, gives me plenty to think about! I was contemplating using the actual weekday names
Monday
,Tuesday
and thenThursday
. As it would make it more readable than0
,2
and3
. I can obtain the name of the current day usingnow.strftime("%A")
, however it seems harder to iterate over. Thoughts on this? \$\endgroup\$N3buchadnezzar– N3buchadnezzar2020年09月21日 16:32:54 +00:00Commented 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\$2020年09月21日 16:38:05 +00:00Commented 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\$N3buchadnezzar– N3buchadnezzar2020年09月21日 17:10:20 +00:00Commented 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\$2020年09月21日 17:39:10 +00:00Commented Sep 21, 2020 at 17:39
-
\$\begingroup\$ Wont this fail if the month switches? Scratches head \$\endgroup\$N3buchadnezzar– N3buchadnezzar2020年09月21日 17:47:48 +00:00Commented Sep 21, 2020 at 17:47