Suppose there is an order which we have to ship on 28/04/2023 12:00 PM and to finish that order we need 11d:03h:36m:10s time. Find datetime when we should start working on that order.
Conditions:
In short we don't have to consider Friday 22:00 PM to Sunday 22:00 PM at any cost.
I tried with this code. It uses an initial guess for the start date by subtracting the required time of the work order from the shipping date and then tries to adjust it by taking weekends into account:
import datetime
from datetime import timedelta, time
import pytz
excluded_start_time = time(22, 0)
excluded_end_time = time(22, 0)
excluded_days = {4, 5, 6}
# straight subtraction of ship date and operation time.
# no friday and sunday logic applied
start_date = datetime.datetime(2023, 4, 17, 8, 23, 50)
end_date = datetime.datetime(2023, 4, 28, 12, 0, 0)
def count_weekends(start_date, end_date):
while end_date.date() > start_date.date() or (start_date.weekday() in excluded_days and excluded_start_time <= start_date <=excluded_end_time):
friday = datetime.datetime.combine(start_date + timedelta(days=(4-start_date.weekday())), datetime.time(hour=22))
sunday = datetime.datetime.combine(start_date + timedelta(days=(6-start_date.weekday())), datetime.time(hour=22))
if friday <= end_date <= sunday:
start_date = start_date - timedelta(hours=48)
end_date = end_date - timedelta(hours=24)
return start_date
print(count_weekends(start_date, end_date))
But I got incorrect results, it returns 11/04/2023 08:23 AM, the correct answer should be 13/04/2023 8:23 AM.
from datetime import datetime, timedelta, time
# Constants for weekdays
MONDAY = 0
TUESDAY = 1
WEDNESDAY = 2
THURSDAY = 3
FRIDAY = 4
SATURDAY = 5
SUNDAY = 6
NON_WORKING_START_TIME = time(22, 0)
NON_WORKING_END_TIME = time(22, 0)
def is_within_non_working_period(dt: datetime) -> bool:
"""Checks if the date is within the non-working period."""
return any([dt.weekday() == FRIDAY and dt.time() >= NON_WORKING_START_TIME,
dt.weekday() == SATURDAY,
dt.weekday() == SUNDAY and dt.time() < NON_WORKING_END_TIME])
def shift_to_working_date(dt: datetime) -> datetime:
"""Shifts the date to the nearest working date."""
if is_within_non_working_period(dt):
days_to_shift = dt.weekday() - FRIDAY
return datetime.combine(dt, NON_WORKING_START_TIME) - timedelta(days=days_to_shift, seconds=1)
return dt
def calculate_start_date(ship_date: datetime, required_time: timedelta) -> datetime:
"""Calculates the start date for the order."""
ship_date = shift_to_working_date(ship_date)
start_date = ship_date - required_time
working_time = count_working_time(start_date, ship_date)
while working_time < required_time:
# Add more time to the start date to meet the required time
start_date -= required_time - working_time
# Shift the start date to the nearest allowed date
start_date = shift_to_working_date(start_date)
# Recalculate the working time for the next iteration
working_time = count_working_time(start_date, ship_date)
return start_date
def next_non_working_date(dt: datetime) -> datetime:
"""Returns the next non-working date.
>>> next_non_working_date(datetime(2021, 4, 30, 23, 0)) # Sunday 23:00
datetime.datetime(2021, 5, 5, 22, 0) # next Friday 22:00
"""
if dt.weekday() == SUNDAY and dt.time() >= NON_WORKING_END_TIME:
return datetime.combine(dt, NON_WORKING_START_TIME) + timedelta(days=5)
elif dt.weekday() < FRIDAY or dt.weekday() == FRIDAY and dt.time() < NON_WORKING_START_TIME:
days_to_shift = FRIDAY - dt.weekday()
return datetime.combine(dt, NON_WORKING_START_TIME) + timedelta(days=days_to_shift)
# The date is within the non-working period
return dt
def next_working_date(dt: datetime) -> datetime:
"""Returns the next working date.
>>> next_working_date(datetime(2023, 4, 30, 21, 0)) # Sunday 21:00
datetime.datetime(2023, 4, 30, 22, 0) # Sunday 22:00
"""
if is_within_non_working_period(dt):
days_to_shift = SUNDAY - dt.weekday()
return datetime.combine(dt, NON_WORKING_END_TIME) + timedelta(days=days_to_shift)
# The date is within the working period
return dt
def split_working_ranges(start_date: datetime, end_date: datetime) -> list[tuple[datetime, datetime]]:
"""Splits the date range into working ranges."""
assert start_date <= end_date
working_ranges = []
current_end_date = start_date
while current_end_date < end_date:
current_start_date = next_working_date(current_end_date)
current_end_date = next_non_working_date(current_start_date)
current_end_date = min(current_end_date, end_date)
if current_start_date < current_end_date:
working_ranges.append((current_start_date, current_end_date))
return working_ranges
def count_working_time(start_date: datetime, end_date: datetime) -> timedelta:
working_ranges = split_working_ranges(start_date, end_date)
return sum([end - start for start, end in working_ranges], timedelta())
if __name__ == "__main__":
# Ship date and required time
ship_date_str = "28/04/2023 12:00 PM"
required_time_str = "11:03:36:10"
# Convert required time string to timedelta
days, hours, minutes, seconds = map(int, required_time_str.split(':'))
required_time = timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
# Convert ship date string to datetime
ship_date = datetime.strptime(ship_date_str, "%d/%m/%Y %I:%M %p")
# Calculate the start date
start_date = calculate_start_date(ship_date, required_time)
total_time = ship_date - start_date
working_time = count_working_time(start_date, ship_date)
non_working_time = total_time - working_time
print("Start working on the order at:", start_date)
print("Total time:", total_time)
print("Working time:", working_time)
print("Non-working time:", non_working_time)
Result:
Start working on the order at: 2023-04-13 08:23:50
Total time: 15 days, 3:36:10
Working time: 11 days, 3:36:10
Non-working time: 4 days, 0:00:00