Functional Weave
Code in Rust

fleet.ev-charging-cost@1.0.0

impl/typescript.ts

3,408 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 { type RoundingMode } from "./math_round_div.ts";  ← from math.round-div ^1.0.0 · built alongside by fune
import { roundDivBig } from "./math_round_div_big.ts";  ← from math.round-div-big ^1.0.0 · built alongside by fune
import { money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { type EvChargingCost, type TariffWindow } from "./fleet_ev_charging_cost_types.ts";

const CLOCK = /^([01][0-9]|2[0-3]):[0-5][0-9]$/;
const DAY = 1440;

function minuteOfDay(name: string, text: string): number {
  if (typeof text !== "string" || !CLOCK.test(text)) {
    throw new RangeError(`${name} must be HH:MM, 00:00 to 23:59, received "${text}"`);
  }
  return Number(text.slice(0, 2)) * 60 + Number(text.slice(3, 5));
}

function whole(name: string, value: number, min: number, max: number, rule: string): void {
  if (!Number.isInteger(value) || value < min || value > max) {
    throw new RangeError(`${name} must be ${rule}, received ${value}`);
  }
}

/**
 * The cost of one charge on a time-of-use tariff.
 *
 * The energy drawn is more than the energy added (the charger and battery lose
 * some), and it flows at the charger's power from the start time, so it falls
 * into the tariff's windows in order. Everything is counted in exact units of
 * 1/(60 x efficiency) Wh, where a minute of charging is a whole number, so the
 * only rounding is the final one.
 */
export function evChargingCost(
  energyAddedWh: number,
  efficiencyBasisPoints: number,
  chargerPowerWatts: number,
  startTime: string,
  tariff: readonly TariffWindow[],
  currency: string,
  mode: RoundingMode
): EvChargingCost {
  whole("energyAddedWh", energyAddedWh, 0, 1000000, "a whole number from 0 to 1000000");
  whole("efficiencyBasisPoints", efficiencyBasisPoints, 1, 10000, "a whole number from 1 to 10000");
  whole("chargerPowerWatts", chargerPowerWatts, 1, 1000000, "a whole number from 1 to 1000000");
  let t = minuteOfDay("startTime", startTime);
  if (tariff.length === 0) {
    throw new RangeError("tariff must have at least one window");
  }
  const starts: number[] = [];
  for (const window of tariff) {
    const start = minuteOfDay("tariff start", window.start);
    if (starts.length > 0 && start <= starts[starts.length - 1]) {
      throw new RangeError("tariff windows must be in ascending order of start time");
    }
    whole("pricePerKwhTenths", window.pricePerKwhTenths, 0, Number.MAX_SAFE_INTEGER, "a whole number, 0 or more");
    starts.push(start);
  }
  money(0, currency);

  const eff = BigInt(efficiencyBasisPoints);
  const perMinute = BigInt(chargerPowerWatts) * eff;
  let remaining = BigInt(energyAddedWh) * 600000n;
  const total = remaining;
  let costNumerator = 0n;
  while (remaining > 0n) {
    let index = starts.length - 1;
    for (let i = 0; i < starts.length; i++) if (starts[i] <= t) index = i;
    const next = index + 1 < starts.length ? starts[index + 1] : starts[0];
    const length = next > t ? next - t : next + DAY - t;
    const capacity = BigInt(length) * perMinute;
    const take = remaining < capacity ? remaining : capacity;
    costNumerator += take * BigInt(tariff[index].pricePerKwhTenths);
    remaining -= take;
    t = (t + length) % DAY;
  }

  const unit = 60n * eff;
  const cost = roundDivBig(costNumerator.toString(), (unit * 10000n).toString(), mode);
  return {
    energyDrawnWh: Number(roundDivBig(total.toString(), unit.toString(), "half-up")),
    minutes: Number(roundDivBig(total.toString(), perMinute.toString(), "up")),
    cost: money(Number(cost), currency),
  };
}