Reputation: 453
I'm developing a small app in Python with Flask
I've looked but maybe not hard enough to find a Python library that will help me do the following:
I want to add X hours to the current time. If the result goes outside of business hours then the result will be the time X business open hours from now.
For example, if a business operated between 9 am and 5 pm on weekdays. The current time is Friday 4 pm and I add 6 hours, the result should be 2 pm on the following Monday. If for some reason the Monday is a public holiday it is pushed out to Tuesday.
My use case is that if a job is logged, depending on the priority of the job, it has to be completed within a certain period of time. But that time has to be calculated in working hours.
I'm hoping there is a simple way to do this (pre-existing library) as it seems like such a common thing to do.
Edit: I forgot to mention, I've looked at the following packages:
Edit 2: Still playing with it but I think I have found a library that does exactly what I want:
sla-calculator 1.0.0
There is also this:
sla-checker 0.0.2
Upvotes: 1
Views: 3589
Reputation: 19
Some improvements to brudi4550's answer, mainly to take minutes into account as well and use different times for each day of the week.
The "add_hours" function has been modified to decrement the datetime object by one minute at a time until the variable "minutes" is less than zero.
The "is_in_open_hours" function has been modified to check if the datetime object falls within the business hours, if is not a holiday, and takes into account the time.
The "get_next_open_datetime" function increments the datetime object by one minute at a time instead of one day at a time.
from datetime import datetime, timedelta, date, time
business_hours = {
0: {"from": time(hour=7, minute=30), "to": time(hour=17, minute=30)}, # Monday
1: {"from": time(hour=7, minute=30), "to": time(hour=17, minute=30)}, # Tuesday
2: {"from": time(hour=7, minute=30), "to": time(hour=17, minute=30)}, # Wednesday
3: {"from": time(hour=7, minute=30), "to": time(hour=17, minute=30)}, # Thursday
4: {"from": time(hour=7, minute=30), "to": time(hour=17, minute=30)}, # Friday
5: None, # Saturday
6: None, # Sunday
}
holidays = [
date(2023, 5, 1)
]
def is_in_open_hours(dt) -> bool:
day_hours = business_hours.get(dt.weekday())
if day_hours is None:
return False
return dt.date() not in holidays and \
day_hours["from"] <= dt.time() < day_hours["to"]
def get_next_open_datetime(dt) -> datetime:
while True:
dt = dt + timedelta(minutes=1)
if is_in_open_hours(dt):
dt = datetime.combine(dt.date(), business_hours[dt.weekday()]["from"])
return dt
def add_hours(dt, minutes: int) -> datetime:
while minutes > 0:
if is_in_open_hours(dt):
dt = dt + timedelta(minutes=1)
minutes -= 1
else:
dt = get_next_open_datetime(dt)
return dt
Here's a simple way to test with a few days:
if __name__ == '__main__':
test_cases = [
{"start_time": datetime(2023, 4, 21, 13, 25), "minutes": 121, "expected_output": datetime(2023, 4, 21, 15, 26)}, # Friday, result in the same day
{"start_time": datetime(2023, 4, 21, 13, 25), "minutes": 300, "expected_output": datetime(2023, 4, 24, 8, 25)}, # Friday, result in next Monday
{"start_time": datetime(2023, 4, 22, 13, 25), "minutes": 60, "expected_output": datetime(2023, 4, 24, 8, 30)}, # Saturday, result in next Monday
{"start_time": datetime(2023, 4, 28, 13, 00), "minutes": 300, "expected_output": datetime(2023, 5, 2, 8, 00)}, # Friday, result in next Tuesday (Monday is holiday)
{"start_time": datetime(2023, 5, 30, 20, 00), "minutes": 300, "expected_output": datetime(2023, 5, 31, 12, 30)}, # Tuesday (penultimate day of the month), result on Wednesday
]
for case in test_cases:
result = add_hours(case["start_time"], case["minutes"])
if result == case["expected_output"]:
print(
f"Test case passed: start_time={case['start_time']}, minutes={case['minutes']}, expected_output={case['expected_output']}")
else:
print(
f"Test case failed: start_time={case['start_time']}, minutes={case['minutes']}, expected_output={case['expected_output']}, actual_output={result}")
I haven't tested it in all possible cases but it looks good, I'm using it in a case very similar to yours
Upvotes: 0
Reputation: 592
I don't think there is a need for a library for that. This can be refined, as I haven't thought of any edge cases, but you get the idea.
Example at the end 11.02.2022, 16:00 + 6 hours = 14.02.2022 12:00 (with business hours monday - friday, 8am - 6pm).
from datetime import datetime, timedelta, date, time
business_hours = {
# monday = 0, tuesday = 1, ... same pattern as date.weekday()
"weekdays": [0, 1, 2, 3, 4],
"from": time(hour=8),
"to": time(hour=18)
}
holidays = [date(2022, 2, 25), date(2022, 2, 24)]
def is_in_open_hours(dt):
return dt.weekday() in business_hours["weekdays"] \
and dt.date() not in holidays \
and business_hours["from"].hour <= dt.time().hour < business_hours["to"].hour
def get_next_open_datetime(dt):
while True:
dt = dt + timedelta(days=1)
if dt.weekday() in business_hours["weekdays"] and dt.date() not in holidays:
dt = datetime.combine(dt.date(), business_hours["from"])
return dt
def add_hours(dt, hours):
while hours != 0:
if is_in_open_hours(dt):
dt = dt + timedelta(hours=1)
hours = hours - 1
else:
dt = get_next_open_datetime(dt)
return dt
dt_test = datetime(2022, 2, 11, 16)
print(add_hours(dt_test, 6))
Upvotes: 3