Functional Weave
Code in TypeScript

fleet.service-due@1.0.0

impl/typescript.ts

2,992 bytes · the TypeScript implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { addDays } from "./dates_add_days.ts";  ← from dates.add-days ^1.0.0 · built alongside by fune
import { addMonths } from "./dates_add_months.ts";  ← from dates.add-months ^1.0.0 · built alongside by fune
import { daysBetween } from "./dates_days_between.ts";  ← from dates.days-between ^1.0.0 · built alongside by fune
import { roundDiv } from "./math_round_div.ts";  ← from math.round-div ^1.0.0 · built alongside by fune
import { type ServiceDue, type ServiceTrigger } from "./fleet_service_due_types.ts";

function wholeNumber(name: string, value: number): void {
  if (!Number.isInteger(value) || value < 0) {
    throw new RangeError(`${name} must be a whole number, 0 or more, received ${value}`);
  }
}

/**
 * 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.
 */
export function serviceDue(
  lastServiceDate: string,
  lastServiceMileage: number,
  intervalMonths: number,
  intervalMiles: number,
  onDate: string,
  currentMileage: number
): ServiceDue {
  wholeNumber("lastServiceMileage", lastServiceMileage);
  wholeNumber("intervalMonths", intervalMonths);
  wholeNumber("intervalMiles", intervalMiles);
  wholeNumber("currentMileage", currentMileage);
  if (intervalMonths === 0 && intervalMiles === 0) {
    throw new RangeError("at least one of intervalMonths and intervalMiles must be above zero");
  }
  const daysSince = daysBetween(lastServiceDate, onDate);
  if (daysSince < 0) {
    throw new RangeError(`onDate ${onDate} is before the last service on ${lastServiceDate}`);
  }
  const milesSince = currentMileage - lastServiceMileage;
  if (milesSince < 0) {
    throw new RangeError(`currentMileage ${currentMileage} is below the mileage at the last service, ${lastServiceMileage}`);
  }

  const dueDate = intervalMonths > 0 ? addMonths(lastServiceDate, intervalMonths) : null;
  const daysRemaining = dueDate === null ? null : daysBetween(onDate, dueDate);
  const dueMileage = intervalMiles > 0 ? lastServiceMileage + intervalMiles : null;
  const milesRemaining = dueMileage === null ? null : dueMileage - currentMileage;

  let projectedMileageDate: string | null = null;
  let mileageDate: string | null = null;
  if (milesRemaining !== null) {
    if (milesRemaining <= 0) {
      mileageDate = onDate;
    } else if (daysSince > 0 && milesSince > 0) {
      projectedMileageDate = addDays(onDate, roundDiv(milesRemaining * daysSince, milesSince, "up"));
      mileageDate = projectedMileageDate;
    }
  }

  let nextDueDate: string | null = dueDate;
  let dueBy: ServiceTrigger | null = dueDate === null ? null : "date";
  if (mileageDate !== null && (nextDueDate === null || mileageDate < nextDueDate)) {
    nextDueDate = mileageDate;
    dueBy = "mileage";
  }

  return {
    dueDate,
    dueMileage,
    daysRemaining,
    milesRemaining,
    projectedMileageDate,
    nextDueDate,
    dueBy,
    overdue: (daysRemaining !== null && daysRemaining < 0) || (milesRemaining !== null && milesRemaining < 0),
  };
}