impl/typescript.ts
3,357 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 { convertUnits } from "./units_convert.ts"; ← from units.convert ^1.0.0 · built alongside by fune
import { type YieldPerArea } from "./agri_yield_per_hectare_types.ts";
const DECIMAL = /^[0-9]+(\.[0-9]+)?$/;
interface Decimal {
digits: bigint;
scale: number;
}
function parse(name: string, text: string, limited: boolean): Decimal {
if (typeof text !== "string" || !DECIMAL.test(text)) {
throw new RangeError(`${name} must be plain non-negative decimal text like "12.5", received "${text}"`);
}
const [whole, rawFraction = ""] = text.split(".");
const fraction = rawFraction.replace(/0+$/, "");
const significant = (whole + fraction).replace(/^0+/, "");
if (limited && (significant.length > 12 || fraction.length > 6)) {
throw new RangeError(`${name} "${text}" has too many digits: at most 12 significant digits and 6 decimal places`);
}
return { digits: BigInt(significant === "" ? "0" : significant), scale: fraction.length };
}
// (product of top) / (product of bottom), each a decimal, as integers.
function ratio(top: readonly Decimal[], bottom: readonly Decimal[]): [bigint, bigint] {
let num = 1n;
let den = 1n;
let exponent = 0; // power of ten on the numerator
for (const d of top) {
num *= d.digits;
exponent -= d.scale;
}
for (const d of bottom) {
den *= d.digits;
exponent += d.scale;
}
if (exponent > 0) num *= 10n ** BigInt(exponent);
else den *= 10n ** BigInt(-exponent);
return [num, den];
}
// Long division, one digit at a time, rounded half up, trailing zeros trimmed.
function toText([num, den]: [bigint, bigint], decimals: number): string {
let whole = num / den;
let remainder = num % den;
let fraction = 0n;
for (let i = 0; i < decimals; i++) {
remainder *= 10n;
fraction = fraction * 10n + remainder / den;
remainder %= den;
}
if (remainder * 2n >= den) {
fraction += 1n;
if (fraction === 10n ** BigInt(decimals)) {
fraction = 0n;
whole += 1n;
}
}
let text = whole.toString();
if (decimals > 0) {
const shown = fraction.toString().padStart(decimals, "0").replace(/0+$/, "");
if (shown !== "") text += "." + shown;
}
return text;
}
/**
* Yield per hectare and per acre, exactly.
*
* The area unit's size comes from units.convert as exact decimal text
* (1 acre = 4046.8564224 m²), so every answer is one exact fraction rounded
* once. In floats, 1.005 t from 1 ha is 1.00 t/ha to two places; here it is
* 1.01.
*/
export function yieldPerHectare(quantity: string, area: string, areaUnit: string, decimals: number): YieldPerArea {
const q = parse("quantity", quantity, true);
const a = parse("area", area, true);
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 12) {
throw new RangeError(`decimals must be a whole number from 0 to 12, received ${decimals}`);
}
const unit = parse("unit size", convertUnits("1", areaUnit, "m2", 12), false);
if (a.digits === 0n) throw new RangeError("area must be greater than zero");
const acre = parse("acre", convertUnits("1", "acre", "m2", 12), false);
const hectare: Decimal = { digits: 10000n, scale: 0 };
return {
perHectare: toText(ratio([q, hectare], [a, unit]), decimals),
perAcre: toText(ratio([q, acre], [a, unit]), decimals),
hectares: toText(ratio([a, unit], [hectare]), decimals),
acres: toText(ratio([a, unit], [acre]), decimals),
};
}