Functional Weave
Code in Rust

retail.price-per-unit@1.0.0

impl/typescript.ts

2,235 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 Money, money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { convertUnits } from "./units_convert.ts";  ← from units.convert ^1.0.0 · built alongside by fune
import { type UnitPriceBasis } from "./retail_price_per_unit_types.ts";

// Each basis is a units.convert symbol and how many of it the price is for.
const BASES: Record<string, [string, number]> = {
  kg: ["kg", 1],
  "100g": ["g", 100],
  "10g": ["g", 10],
  L: ["L", 1],
  "100mL": ["mL", 100],
  "10mL": ["mL", 10],
  "75cL": ["mL", 750],
  m: ["m", 1],
  m2: ["m2", 1],
  m3: ["m3", 1],
  item: ["item", 1],
};

const DECIMAL = /^[0-9]+(\.[0-9]+)?$/;

/**
 * The price of one `per` of the product, rounded half up to the minor unit.
 *
 * The quantity is converted exactly (units.convert, 12 decimal places), then
 * price x multiplier / quantity is done in integers, so the one rounding step
 * is the last one.
 */
export function unitPrice(price: Money, quantity: string, unit: string, per: UnitPriceBasis): Money {
  if (price.minor < 0) throw new RangeError(`price must not be negative, received ${price.minor}`);
  const basis = BASES[per];
  if (basis === undefined) throw new RangeError(`unknown unit price basis "${per}"`);
  const [target, multiplier] = basis;

  let converted: string;
  if (target === "item" || unit === "item") {
    if (target !== "item") throw new RangeError(`a quantity in items can only be priced per "item"`);
    if (unit !== "item") throw new RangeError(`per "item" needs the quantity in "item"`);
    if (!DECIMAL.test(quantity)) throw new RangeError(`quantity must be more than 0, received "${quantity}"`);
    converted = quantity;
  } else {
    converted = convertUnits(quantity, unit, target, 12);
  }
  if (converted.startsWith("-") || !/[1-9]/.test(converted)) {
    throw new RangeError(`quantity must be more than 0, received "${quantity}"`);
  }

  // converted = digits / 10^places, so price / converted = price x 10^places / digits.
  const [whole, fraction = ""] = converted.split(".");
  const digits = BigInt(whole + fraction);
  const numerator = BigInt(price.minor) * BigInt(multiplier) * 10n ** BigInt(fraction.length);
  let quotient = numerator / digits;
  if ((numerator % digits) * 2n >= digits) quotient += 1n;
  return money(Number(quotient), price.currency);
}