Functional Weave
Code in Python

agri.yield-per-hectare@1.0.1

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