Functional Weave
Code in TypeScript

agri.feed-ration@1.0.0

impl/typescript.ts

3,414 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 { roundFloat } from "./math_round_float.ts";  ← from math.round-float ^1.0.0 · built alongside by fune
import { type RationCost, type RationFeed, type RationLine } from "./agri_feed_ration_types.ts";

const MAX_SAFE = 9007199254740991n;

// Half up for a non-negative numerator and a positive denominator.
function halfUp(numerator: bigint, denominator: bigint): number {
  const q = (2n * numerator + denominator) / (2n * denominator);
  if (q > MAX_SAFE) throw new RangeError("total cost is too large");
  return Number(q);
}

/**
 * Ration cost and dry matter intake.
 *
 * Weights are grams and prices minor units per tonne, so a feed's daily cost
 * is exactly grams x price / 1,000,000 and every total is rounded once from
 * the exact sum. Costing the group from a rounded daily cost multiplies the
 * rounding: 467.5p a head a day is 1,402,500p for 100 cows over 30 days, not
 * 468 x 3000 = 1,404,000p.
 */
export function feedRation(feeds: readonly RationFeed[], head: number, days: number, currency: string): RationCost {
  if (typeof head !== "number" || !Number.isInteger(head) || head < 1 || head > 1000000) {
    throw new RangeError(`head must be a whole number from 1 to 1000000, received ${head}`);
  }
  if (typeof days !== "number" || !Number.isInteger(days) || days < 1 || days > 3660) {
    throw new RangeError(`days must be a whole number from 1 to 3660, received ${days}`);
  }
  money(0, currency);

  let grams = 0;
  let dryMatter = 0n; // grams x basis points
  let cost = 0n; // grams x minor units per tonne
  const lines: RationLine[] = feeds.map((feed, i) => {
    const n = i + 1;
    const kg = feed.freshKgPerHead;
    if (typeof kg !== "number" || !Number.isFinite(kg) || kg < 0 || kg > 1000) {
      throw new RangeError(`feed ${n}: freshKgPerHead must be a finite number from 0 to 1000, received ${kg}`);
    }
    const g = Math.round(roundFloat(kg, 3) * 1000);
    const bp = feed.dryMatterBasisPoints;
    if (typeof bp !== "number" || !Number.isInteger(bp) || bp < 0 || bp > 10000) {
      throw new RangeError(`feed ${n}: dryMatterBasisPoints must be a whole number from 0 to 10000, received ${bp}`);
    }
    const price = feed.pricePerTonne;
    if (price.currency !== currency) {
      throw new RangeError(`feed ${n}: currency mismatch: ${price.currency} and ${currency}`);
    }
    if (!Number.isInteger(price.minor) || price.minor < 0 || price.minor > 100000000) {
      throw new RangeError(`feed ${n}: pricePerTonne must be from 0 to 100000000 minor units, received ${price.minor}`);
    }
    const lineDm = BigInt(g) * BigInt(bp);
    const lineCost = BigInt(g) * BigInt(price.minor);
    grams += g;
    dryMatter += lineDm;
    cost += lineCost;
    return {
      name: feed.name,
      freshKgPerHead: g / 1000,
      dryMatterKgPerHead: halfUp(lineDm, 10000n) / 1000,
      costPerHead: money(halfUp(lineCost, 1000000n), currency),
    };
  });

  const perTonneDm: Money | null = dryMatter === 0n ? null : money(halfUp(cost * 10000n, dryMatter), currency);
  return {
    lines,
    freshKgPerHead: grams / 1000,
    dryMatterKgPerHead: halfUp(dryMatter, 10000n) / 1000,
    dryMatterBasisPoints: grams === 0 ? null : halfUp(dryMatter, BigInt(grams)),
    costPerHeadPerDay: money(halfUp(cost, 1000000n), currency),
    totalCost: money(halfUp(cost * BigInt(head) * BigInt(days), 1000000n), currency),
    costPerTonneDryMatter: perTonneDm,
  };
}