Functional Weave
Code in TypeScript

agri.spray-mix Unreviewed

Sprayer tank mix: tank fills, and product and water per full and part tank, for an area, water volume and dose.

1.0.1 · published 2026-10-03 by charlie · Anterra

Pinned by 14 tests, run in TypeScript, Python and Rust.

Unreviewed. This capability’s implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified BASIS-qualified agronomist has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.

Not professional advice. This capability calculates pesticide figures from published rules. It is a software component for developers, not agronomic advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed in its README, and have a BASIS-qualified agronomist review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.

What it does

**Status: needs review by a qualified agronomist/BASIS adviser before it is published; always follow the product label.**

How to fill a sprayer for a job: how many tank fills, and how much product and water go into each full tank and into the last, part-filled one. 24 ha at a water volume of 200 L/ha with a product at 2.5 L/ha and a 1000 L tank is 5 fills: four full tanks of 12.5 L product and 987.5 L water, each covering 5 ha, and a last tank of 10 L product and 790 L water for the remaining 4 ha.

For example

  • sprayMix(24, 200, 2.5, litres, 1,000) → tank fills 5, full tanks 4, full tank …, partial tank …, total spray litres 4,800, total product 60 24 ha at 200 L/ha and 2.5 L/ha with a 1000 L tank: four full tanks and 800 L
  • sprayMix(10, 100, 1, litres, 500) → tank fills 2, full tanks 2, full tank …, partial tank —, total spray litres 1,000, total product 10 a job of exactly two tanks has no part tank
  • sprayMix(2, 200, 1.5, litres, 1,000) → tank fills 1, full tanks 0, full tank …, partial tank …, total spray litres 400, total product 3 less than one tank: the full-tank recipe is still given

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

export function sprayMix(areaHectares: number, waterVolumeLitresPerHectare: number, productDosePerHectare: number, doseUnit: DoseUnit, tankLitres: number): SprayMix
areaHectaresfloatarea to treat, taken to 0.0001 ha
waterVolumeLitresPerHectarefloatthe application rate: total spray liquid per hectare, taken to 0.1 L
productDosePerHectarefloatproduct per hectare from the label, taken to 0.001 (1 mL or 1 g); 0 to 100
doseUnitDoseUnitlitres (a liquid, which displaces water) or kilograms (a solid, which does not)
tankLitresfloatusable tank capacity, taken to 0.1 L
returnsSprayMix

The types it declares, generated into your project

export type DoseUnit = "litres" | "kilograms";

/** What goes into one tank and what it covers. */
export interface TankLoad {
  /** area it covers, to 0.0001 ha */
  readonly areaHectares: number;
  /** total liquid, to 0.001 L */
  readonly sprayLitres: number;
  /** litres or kilograms of product, to 0.001 */
  readonly product: number;
  /** spray less product for a liquid, all of it for a solid */
  readonly waterLitres: number;
}

/** The whole job. */
export interface SprayMix {
  /** full tanks plus a part tank if there is one */
  readonly tankFills: number;
  readonly fullTanks: number;
  /** the recipe for a full tank, even when the job needs less than one */
  readonly fullTank: TankLoad;
  /** the last, part-filled tank; null when the job is whole tanks */
  readonly partialTank: TankLoad | null;
  /** to 0.001 L */
  readonly totalSprayLitres: number;
  /** to 0.001 */
  readonly totalProduct: number;
}

Your code names it in one line, in the file that uses it

import { sprayMix } from "#fune/agri.spray-mix@^1";
impl/typescript.ts · 68 lines · open · 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,
  };
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add agri.spray-mix
Download for TypeScript agri.spray-mix-1.0.1-typescript.fune · 16,216 bytes sha256 1323f9f79f8ade52b8969b797baaf382405595b0719003ddbf4bfe790754a3cb

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./agri.spray-mix-1.0.1-typescript.fune, or fetch it from a terminal with fune pull agri.spray-mix@1.0.1:typescript.

The whole function, every language, is one file too: agri.spray-mix-1.0.1.fune, 23,979 bytes, sha256 865c75eea05b131780d875c1023f8d889054971285cf0ac22b48355d174d2bda. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before agri.spray-mix

after — your function gets the result and the arguments, and returns the final result.

// fune: after agri.spray-mix

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

// fune: replace math.round-div in agri.spray-mix
// fune: replace math.round-float in agri.spray-mix

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show agri.spray-mix --steps.

// fune: step agri.spray-mix after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
24 ha at 200 L/ha and 2.5 L/ha with a 1000 L tank: four full tanks and 800 L 24, 200, 2.5, litres, 1,000 → tank fills 5, full tanks 4, full tank …, partial tank …, total spray litres 4,800, total product 60
a job of exactly two tanks has no part tank 10, 100, 1, litres, 500 → tank fills 2, full tanks 2, full tank …, partial tank —, total spray litres 1,000, total product 10
less than one tank: the full-tank recipe is still given 2, 200, 1.5, litres, 1,000 → tank fills 1, full tanks 0, full tank …, partial tank …, total spray litres 400, total product 3
a solid product in kg does not displace water 12, 150, 0.25, kilograms, 600 → tank fills 3, full tanks 3, full tank …, partial tank —, total spray litres 1,800, total product 3
awkward numbers: the full and part tank products add up to the job's 7.3, 120, 0.67, litres, 800 → tank fills 2, full tanks 1, full tank …, partial tank …, total spray litres 876, total product 4.891
129.2 ha at 250 L/ha is exactly 19 tanks of 1700 L, not 18 and a sliver 129.2, 250, 1, litres, 1,700 → tank fills 19, full tanks 19, full tank …, partial tank —, total spray litres 32,300, total product 129.2
water only (no product) 5, 100, 0, litres, 300 → tank fills 2, full tanks 1, full tank …, partial tank …, total spray litres 500, total product 0
the smallest area, one square metre 0, 200, 1, litres, 1,000 → tank fills 1, full tanks 0, full tank …, partial tank …, total spray litres 0.02, total product 0
zero area is an error 0, 200, 1, litres, 1,000 → error: areaHectares must be at least 0.0001 hectares
a negative dose is an error 10, 200, -1, litres, 1,000 → error: productDosePerHectare must be a finite number from 0 to 100
Show the other 4 tests
CaseArgumentsExpected
zero water volume is an error 10, 0, 1, litres, 1,000 → error: waterVolumeLitresPerHectare must be at least 0.1 litres
zero tank is an error 10, 200, 1, litres, 0 → error: tankLitres must be at least 0.1 litres
a liquid dose as large as the water volume is an error 10, 50, 50, litres, 1,000 → error: productDosePerHectare must be less than waterVolumeLitresPerHectare for a liquid product
an unknown dose unit is an error 10, 200, 1, gallons, 1,000 → error: unknown doseUnit "gallons"

More from the author

## Terms

- **Water volume** (the application rate) is the total spray liquid applied per hectare, product included, as sprayer calibration sets it. - **Dose** is the label rate of product per hectare, in litres for a liquid or kilograms for a solid (`doseUnit`). - **Water** in a tank is the spray volume less the product for a liquid, since the product takes up room; for a solid it is the whole spray volume (the powder or granules dissolve or disperse into it).

## Exactness

Inputs are taken to 0.0001 ha, 0.1 L/ha, 0.001 L or kg per ha and 0.1 L (`math.round-float`) and held as integers, so the job's total spray volume is exact and the full-tank count is a whole-number division. In floating point, 129.2 ha x 250 L/ha is 32299.999999999996, which makes 18 full 1700 L tanks and a sliver; this gives exactly 19.

Each load's area, spray, product and water are worked out from its own spray volume and rounded half up (area to 0.0001 ha, liquids and product to 0.001). Water is the rounded spray less the rounded product, so each load adds up.

## What it does not do

It does not check the dose, water volume, number of applications or buffer zones against the label, account for rinsate, the tank's unusable residue (give the usable capacity) or a tank-mix order, or convert units.

## Limits

Area more than 0 and at most 100,000 ha; water volume 0.1 to 10,000 L/ha; dose 0 to 100 per ha, and for a liquid less than the water volume; tank 0.1 to 100,000 L.

## Before you rely on this

**Not professional advice.** This capability calculates pesticide figures from published rules. It is a software component for developers, not agronomic advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed above, and have a BASIS-qualified agronomist review how you use it, before anyone relies on the output. Provided "as is" under its licence, without warranty.

**Unreviewed.** This capability's implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified BASIS-qualified agronomist has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.

1.0.1 marks it unreviewed. The code and the tests are unchanged.

Files

PathBytes
README.md3,110
impl/python.py3,177
impl/rust.rs4,283
impl/typescript.ts3,110
vectors.json5,364