Finding the next 9am/pm from now

I have some code that needs to return a datetime of the next 9am or 9pm, UTC. I've figured out some code to do this, but it feels unnecessarily complex to me. Is there a way to simplify this?

from datetime import datetime, timezone, timedelta

class NineOClockTimes:
@staticmethod
def get_next_nineoclock_time(current_time: datetime) -> datetime:
if current_time.hour < 9:
return NineOClockTimes._get_nineoclock_time(current_time, hour=9)
elif current_time.hour >= 21:
return NineOClockTimes._get_nineoclock_time(current_time, hour=9, next_day=True)
return NineOClockTimes._get_nineoclock_time(current_time, hour=21)

@staticmethod
def _get_nineoclock_time(current_time, hour, next_day=False):
new_time =  current_time.replace(
hour=hour,
minute=0,
second=0,
microsecond=0,
tzinfo=timezone.utc,
)

if next_day:
new_time += timedelta(days=1)

return new_time
• what if the given current_time was 9:49 AM in UTC+1 timezone? (8:49 AM UTC) Dec 1 '21 at 5:27
• @hjpotter92 I don't understand the question. Dec 1 '21 at 7:54

Your function _get_nineoclock_time does two things, it replaces the timezone and it returns the next 9 O'clock time. The function will return different values depending on whether current_time has a timezone or not. That may be unexpected and seems a likely source of bugs. I'd set the timezone somewhere else.

It is easy to calculate how many hours to add from midnight based on the current hour. Then use timedelta to add that many hours.

def get_next_nineoclock_time(current_time: datetime) -> datetime:
hours = 9 if current_time.hour < 9 else 21 if current_time.hour < 21 else 33

midnight = current_time.replace(
hour=0,
minute=0,
second=0,
microsecond=0,
)

return  midnight + timedelta(hours=hours)

Your could use this formula for hours, but I thinks it is less clear:

hours = 9 + 12*((current_time.hour + 3)//12)
• Coincidentally, we realized that time zone issue today. I like this solution, thanks! Dec 2 '21 at 3:25
• @DanielKaplan this timezone issue was what I had pointed out earlier in the comment on question :) Dec 2 '21 at 16:29
• @hjpotter92 ah, sorry I didn't understand it Dec 2 '21 at 17:16

The class has no data, and only static methods. That suggests we don't need a class at all; plain functions should be perfectly adequate.

• Since the class is used as namespace we can keep the functions in a stand alone file and just import {file} as NineOClockTimes. There are some drawbacks to the approach as you won't be able to subclass NineOClockTimes. Dec 1 '21 at 20:31

There is a much simpler approach using timedelta to do this.

You start by getting the current hour from now (now.hour). If you subtract this from now it will take you to midnight. Then we add/remove an appropriate number of extra hours to get to the 9am/pm you want - in this example I add 3:

now = datetime.now()
print(now)
# datetime.datetime(2021, 12, 1, 10, 23, 32, 830310)
nineish = now - timedelta(days=-1, hours=now.hour - 9) #-18 for 9pm
print(nineish)
datetime.datetime(2021, 12, 2, 9, 23, 32, 830310)

You can optionally also then throw away the mins/secs/usecs to get an exact date:

print(nineish.replace(minute=0, second=0, microsecond=0))
# datetime.datetime(2021, 12, 2, 9, 0)
• How do I figure out the "appropriate number of hours"? Doesn't that depend on what now is? Dec 1 '21 at 19:10
• The first part gets you to midnight - you then need to either add 3, or subtract 9, depending on whether you want 9pm or am. In the code I add 3 to get to the previous 9pm. Dec 1 '21 at 19:12
• Isn't this giving me the previous day's 9pm? I'm asking for the next. Dec 1 '21 at 19:19
• Apologies - misread the question. Have updated with different values to timedelta Dec 1 '21 at 19:24
• This is definitely less code to think about, but I find myself thinking about how this answer works longer than I think about my existing solution. It might be a matter of extracting things into variable names or methods, etc. Dec 2 '21 at 0:52