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