calabash
calabash

Reputation: 453

Python Business hours calculation - add hours and result should be business opening hours

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

Answers (2)

Luan
Luan

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

brudi4550
brudi4550

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

Related Questions