Functional Weave
Code in Rust

hospitality.bill-split@1.0.0

impl/typescript.ts

3,860 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 { allocate } from "./money_allocate.ts";  ← from money.allocate ^1.0.0 · built alongside by fune
import { type Money, money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { splitEven } from "./money_split_even.ts";  ← from money.split-even ^1.0.0 · built alongside by fune
import { sumMoney } from "./money_sum.ts";  ← from money.sum ^1.0.0 · built alongside by fune
import { type BillItem, type DinerShare, type SplitMethod } from "./hospitality_bill_split_types.ts";

/**
 * Split a bill between diners so that the shares add up to the bill exactly.
 *
 * even and by-share divide the whole bill (items plus service) by weight, so
 * even totals never differ by more than a penny. by-item gives each diner what
 * they ordered, with shared items split evenly between the people who shared
 * them. In every method the service charge is then shared in proportion to
 * what each diner pays for, so nobody pays service on someone else's steak.
 */
export function billSplit(
  items: readonly BillItem[],
  serviceCharge: Money,
  diners: number,
  method: SplitMethod,
  shares: readonly number[],
): readonly DinerShare[] {
  if (!Number.isInteger(diners) || diners < 1) throw new RangeError(`diners must be at least 1, received ${diners}`);
  if (method !== "even" && method !== "by-item" && method !== "by-share") {
    throw new RangeError(`unknown split method "${method}"`);
  }
  if (serviceCharge.minor < 0) throw new RangeError(`serviceCharge must not be negative, received ${serviceCharge.minor}`);
  for (const item of items) {
    if (item.amount.minor < 0) throw new RangeError(`item amounts must not be negative, received ${item.amount.minor} for "${item.description}"`);
  }
  const currency = serviceCharge.currency;
  const itemsTotal = sumMoney(
    items.map((item) => item.amount),
    currency,
  );
  if (method === "by-share") {
    if (shares.length !== diners) throw new RangeError(`shares must have one entry per diner: ${diners} diners, ${shares.length} shares`);
    for (const s of shares) {
      if (!Number.isInteger(s) || s < 0) throw new RangeError(`shares must not be negative, received ${s}`);
    }
    if (shares.every((s) => s === 0)) throw new RangeError("shares must not all be zero");
  } else if (shares.length !== 0) {
    throw new RangeError("shares are only used by the by-share method; pass []");
  }

  let itemShares: number[];
  let scShares: number[];
  if (method === "by-item") {
    itemShares = new Array<number>(diners).fill(0);
    for (const item of items) {
      const who = item.diners.length === 0 ? [...Array(diners).keys()] : [...item.diners];
      for (let i = 0; i < who.length; i++) {
        const d = who[i];
        if (!Number.isInteger(d) || d < 0 || d >= diners) {
          throw new RangeError(`diner ${d} is not at the table: diners are numbered 0 to ${diners - 1}`);
        }
        if (who.indexOf(d) !== i) throw new RangeError(`item "${item.description}" lists diner ${d} twice`);
      }
      const parts = splitEven(item.amount, who.length);
      who.forEach((d, i) => (itemShares[d] += parts[i].minor));
    }
    if (itemsTotal.minor === 0) {
      if (serviceCharge.minor !== 0) throw new RangeError("a service charge cannot be shared on a bill with no items");
      scShares = new Array<number>(diners).fill(0);
    } else {
      scShares = allocate(serviceCharge, itemShares).map((m) => m.minor);
    }
  } else {
    const weights = method === "even" ? new Array<number>(diners).fill(1) : [...shares];
    const grand = money(itemsTotal.minor + serviceCharge.minor, currency);
    const totals = allocate(grand, weights).map((m) => m.minor);
    scShares = grand.minor === 0 ? new Array<number>(diners).fill(0) : allocate(serviceCharge, totals).map((m) => m.minor);
    itemShares = totals.map((t, i) => t - scShares[i]);
  }

  return itemShares.map((minor, diner) => ({
    diner,
    items: money(minor, currency),
    serviceCharge: money(scShares[diner], currency),
    total: money(minor + scShares[diner], currency),
  }));
}