import re from typing import List, Optional, Sequence from .dates_add_business_days import add_business_days from .dates_bank_holidays import UkRegion, bank_holidays from .dates_day_of_week import day_of_week from .logistics_delivery_date_types import DeliveryEstimate _TIME = re.compile(r"([01][0-9]|2[0-3]):[0-5][0-9]") def _estimate( order_date: str, order_time: str, cut_off: str, service_days: int, holidays: Sequence[str] ) -> DeliveryEstimate: working = day_of_week(order_date) <= 5 and order_date not in holidays # HH:MM compares correctly as text. Strictly before: 15:00 misses a 15:00 cut-off. same_day = working and order_time < cut_off dispatch = order_date if same_day else add_business_days(order_date, 1, holidays) return DeliveryEstimate( dispatch_date=dispatch, delivery_date=add_business_days(dispatch, service_days, holidays), same_day_dispatch=same_day, ) def delivery_date( order_date: str, order_time: str, cut_off: str, service_days: int, region: Optional[UkRegion], closures: Sequence[str], ) -> DeliveryEstimate: """When an order leaves the warehouse and when it should arrive: same working day if placed before the cut-off, then service_days working days in transit, skipping weekends, the region's bank holidays and the caller's closures.""" if not isinstance(order_time, str) or not _TIME.fullmatch(order_time): raise ValueError('orderTime must be a 24-hour time HH:MM, received "%s"' % (order_time,)) if not isinstance(cut_off, str) or not _TIME.fullmatch(cut_off): raise ValueError('cutOff must be a 24-hour time HH:MM, received "%s"' % (cut_off,)) if isinstance(service_days, bool) or not isinstance(service_days, int): raise TypeError("serviceDays must be an integer, received %r" % (service_days,)) if service_days < 0: raise ValueError("serviceDays must not be negative, received %d" % service_days) day_of_week(order_date) # validates the date before its year is used holidays: List[str] = list(closures) last_year = int(order_date[0:4]) if region is not None: holidays.extend(h.date for h in bank_holidays(region, last_year)) # Load each later year the estimate reaches: an order on 31 December must # skip next year's New Year's Day. More holidays only move the answer later, # so this settles. while True: result = _estimate(order_date, order_time, cut_off, service_days, holidays) reached = int(result.delivery_date[0:4]) if region is None or reached <= last_year: return result for year in range(last_year + 1, reached + 1): holidays.extend(h.date for h in bank_holidays(region, year)) last_year = reached