Functional Weave
Code in Rust

agri.fertiliser-application@1.0.0

impl/typescript.ts

3,050 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 { roundDiv } from "./math_round_div.ts";  ← from math.round-div ^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 FertiliserPlan, type Nutrient, type NutrientAnalysis, type NutrientTargets } from "./agri_fertiliser_application_types.ts";

const NUTRIENTS: readonly Nutrient[] = ["nitrogen", "phosphate", "potash"];

function tenthsOfPercent(name: string, value: number): number {
  if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 100) {
    throw new RangeError(`analysis ${name} must be a finite percentage from 0 to 100, received ${value}`);
  }
  return Math.round(roundFloat(value, 1) * 10);
}

/**
 * Product rate for a fertiliser, set by the limiting nutrient.
 *
 * For each targeted nutrient the product needed is target / analysis; the
 * rate is the smallest of those, so no nutrient is applied beyond its target,
 * and the others show as a shortfall. The arithmetic is exact in tenths (of a
 * percent and of a kilogram) and the rate is rounded once, half up.
 */
export function fertiliserApplication(analysis: NutrientAnalysis, targets: NutrientTargets, areaHectares: number): FertiliserPlan {
  const a = NUTRIENTS.map((n) => tenthsOfPercent(n, analysis[n]));
  if (a[0] + a[1] + a[2] > 1000) throw new RangeError("analysis must not total more than 100%");
  const t = NUTRIENTS.map((n) => {
    const v = targets[n];
    if (typeof v !== "number" || !Number.isInteger(v) || v < 0 || v > 1000) {
      throw new RangeError(`target ${n} must be a whole number of kg/ha from 0 to 1000, received ${v}`);
    }
    return v;
  });
  if (typeof areaHectares !== "number" || !Number.isFinite(areaHectares) || areaHectares <= 0 || areaHectares > 10000) {
    throw new RangeError(`areaHectares must be a finite number greater than 0 and at most 10000, received ${areaHectares}`);
  }
  const area = Math.round(roundFloat(areaHectares, 4) * 10000);
  if (area < 1) throw new RangeError(`areaHectares must be at least 0.0001 hectares, received ${areaHectares}`);
  if (t[0] + t[1] + t[2] === 0) throw new RangeError("at least one target must be above zero");

  // Least product: target_i / a_i smallest, compared by cross-multiplying.
  let limit = -1;
  for (let i = 0; i < 3; i++) {
    if (t[i] === 0 || a[i] === 0) continue;
    if (limit < 0 || t[i] * a[limit] < t[limit] * a[i]) limit = i;
  }
  if (limit < 0) throw new RangeError("the product contains none of the targeted nutrients");

  // kg/ha in tenths = target x 100 / (a / 10) x 10.
  const rate = roundDiv(t[limit] * 10000, a[limit], "half-up");
  const supplied = a.map((ai) => roundDiv(rate * ai, 1000, "half-up"));
  return {
    limitingNutrient: NUTRIENTS[limit],
    productKgPerHectare: rate / 10,
    totalProductKg: roundDiv(rate * area, 10000, "half-up") / 10,
    supplied: { nitrogen: supplied[0] / 10, phosphate: supplied[1] / 10, potash: supplied[2] / 10 },
    balance: {
      nitrogen: (supplied[0] - t[0] * 10) / 10,
      phosphate: (supplied[1] - t[1] * 10) / 10,
      potash: (supplied[2] - t[2] * 10) / 10,
    },
  };
}