from typing import Optional from .dates_add_days import add_days from .dates_add_months import add_months from .dates_days_between import days_between from .fleet_service_due_types import ServiceDue, ServiceTrigger from .math_round_div import round_div def _whole_number(name: str, value: int) -> None: if isinstance(value, bool) or not isinstance(value, int) or value < 0: raise ValueError("%s must be a whole number, 0 or more, received %r" % (name, value)) def service_due( last_service_date: str, last_service_mileage: int, interval_months: int, interval_miles: int, on_date: str, current_mileage: int, ) -> ServiceDue: """The next service, by time or mileage, whichever comes first. "Whichever comes first" needs a date for the mileage limit too, so it is projected from the average daily mileage since the last service, rounded up to a whole day: a service booked for the projected day is never late on that estimate. """ _whole_number("lastServiceMileage", last_service_mileage) _whole_number("intervalMonths", interval_months) _whole_number("intervalMiles", interval_miles) _whole_number("currentMileage", current_mileage) if interval_months == 0 and interval_miles == 0: raise ValueError("at least one of intervalMonths and intervalMiles must be above zero") days_since = days_between(last_service_date, on_date) if days_since < 0: raise ValueError("onDate %s is before the last service on %s" % (on_date, last_service_date)) miles_since = current_mileage - last_service_mileage if miles_since < 0: raise ValueError( "currentMileage %d is below the mileage at the last service, %d" % (current_mileage, last_service_mileage) ) due_date = add_months(last_service_date, interval_months) if interval_months > 0 else None days_remaining = None if due_date is None else days_between(on_date, due_date) due_mileage = last_service_mileage + interval_miles if interval_miles > 0 else None miles_remaining = None if due_mileage is None else due_mileage - current_mileage projected: Optional[str] = None mileage_date: Optional[str] = None if miles_remaining is not None: if miles_remaining <= 0: mileage_date = on_date elif days_since > 0 and miles_since > 0: projected = add_days(on_date, round_div(miles_remaining * days_since, miles_since, "up")) mileage_date = projected next_due: Optional[str] = due_date due_by: Optional[ServiceTrigger] = None if due_date is None else "date" if mileage_date is not None and (next_due is None or mileage_date < next_due): next_due = mileage_date due_by = "mileage" return ServiceDue( due_date=due_date, due_mileage=due_mileage, days_remaining=days_remaining, miles_remaining=miles_remaining, projected_mileage_date=projected, next_due_date=next_due, due_by=due_by, overdue=(days_remaining is not None and days_remaining < 0) or (miles_remaining is not None and miles_remaining < 0), )