Functional Weave
Code in Python

agri.spray-mix@1.0.0

impl/typescript.ts

3,110 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 DoseUnit, type SprayMix, type TankLoad } from "./agri_spray_mix_types.ts";

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

/**
 * Tank mix for a sprayer.
 *
 * Area (0.0001 ha), water volume (0.1 L/ha), dose (0.001/ha) and tank
 * (0.1 L) are held as integers, so the job's spray volume is exact and the
 * count of full tanks cannot be thrown by a float: 129.2 ha at 250 L/ha is
 * exactly 19 tanks of 1700 L, where 129.2 x 250 in floating point is
 * 32299.999999999996 and makes it 18 and a sliver.
 */
export function sprayMix(
  areaHectares: number,
  waterVolumeLitresPerHectare: number,
  productDosePerHectare: number,
  doseUnit: DoseUnit,
  tankLitres: number,
): SprayMix {
  const area = scaled("areaHectares", areaHectares, 4, 0, 100000);
  const rate = scaled("waterVolumeLitresPerHectare", waterVolumeLitresPerHectare, 1, 0, 10000);
  const dose = scaled("productDosePerHectare", productDosePerHectare, 3, 0, 100);
  const tank = scaled("tankLitres", tankLitres, 1, 0, 100000);
  if (area < 1) throw new RangeError("areaHectares must be at least 0.0001 hectares");
  if (rate < 1) throw new RangeError("waterVolumeLitresPerHectare must be at least 0.1 litres");
  if (tank < 1) throw new RangeError("tankLitres must be at least 0.1 litres");
  if (doseUnit !== "litres" && doseUnit !== "kilograms") {
    throw new RangeError(`unknown doseUnit "${doseUnit}": use litres or kilograms`);
  }
  // A litre of product per litre of spray would leave no room for water.
  if (doseUnit === "litres" && dose >= rate * 100) {
    throw new RangeError("productDosePerHectare must be less than waterVolumeLitresPerHectare for a liquid product");
  }

  const spray = area * rate; // litres x 10^5
  const tankSpray = tank * 10000; // litres x 10^5
  const fullTanks = Math.floor(spray / tankSpray);
  const remainder = spray - fullTanks * tankSpray;

  // A load from its spray volume in litres x 10^5: area = spray / rate,
  // product = spray x dose / (rate x 10^4) in thousandths.
  const load = (s: number): TankLoad => {
    const sprayMl = roundDiv(s, 100, "half-up");
    const product = roundDiv(s * dose, rate * 10000, "half-up");
    return {
      areaHectares: roundDiv(s, rate, "half-up") / 10000,
      sprayLitres: sprayMl / 1000,
      product: product / 1000,
      waterLitres: (doseUnit === "litres" ? sprayMl - product : sprayMl) / 1000,
    };
  };
  return {
    tankFills: fullTanks + (remainder > 0 ? 1 : 0),
    fullTanks,
    fullTank: load(tankSpray),
    partialTank: remainder > 0 ? load(remainder) : null,
    totalSprayLitres: roundDiv(spray, 100, "half-up") / 1000,
    totalProduct: roundDiv(area * dose, 10000, "half-up") / 1000,
  };
}