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,
};
}