agri.growing-degree-days@1.0.0
impl/typescript.ts
2,326 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 DailyTemperature, type DegreeDayMethod, type DegreeDays } from "./agri_growing_degree_days_types.ts";
function tenths(value: number, message: string): number {
if (typeof value !== "number" || !Number.isFinite(value) || value < -100 || value > 100) {
throw new RangeError(message);
}
return Math.round(roundFloat(value, 1) * 10);
}
/**
* Growing degree days, McMaster and Wilhelm (1997), in tenths of a degree.
*
* Each day is (max + min) / 2 - base with the temperatures taken to 0.1 °C,
* so it is an exact multiple of 0.05 and the total is summed exactly and
* rounded once. Rounding each day to 0.1 first and adding drifts: three days
* of 0.05 are 0.15 (0.2 to one place), not 0.3.
*/
export function growingDegreeDays(
days: readonly DailyTemperature[],
baseCelsius: number,
upperCelsius: number | null,
method: DegreeDayMethod,
): DegreeDays {
const base = tenths(baseCelsius, "baseCelsius must be a finite number from -100 to 100");
let upper: number | null = null;
if (upperCelsius !== null && upperCelsius !== undefined) {
upper = tenths(upperCelsius, "upperCelsius must be a finite number from -100 to 100");
if (upper <= base) throw new RangeError("upperCelsius must be above baseCelsius");
}
if (method !== "average" && method !== "clamped") {
throw new RangeError(`unknown method "${method}": use average or clamped`);
}
const cap = (t: number): number => (upper !== null && t > upper ? upper : t);
const daily: number[] = [];
let sum = 0;
days.forEach((day, i) => {
const message = `day ${i + 1}: temperatures must be finite numbers from -100 to 100 °C`;
let lo = tenths(day.min, message);
let hi = tenths(day.max, message);
if (lo > hi) throw new RangeError(`day ${i + 1}: min must not be above max`);
lo = cap(lo);
hi = cap(hi);
let twice: number;
if (method === "clamped") {
twice = Math.max(lo, base) + Math.max(hi, base) - 2 * base;
} else {
twice = Math.max(lo + hi - 2 * base, 0);
}
// twice is 2 x (mean - base) in tenths, so the day is twice / 20 degrees.
daily.push(twice / 20);
sum += twice;
});
return { total: roundDiv(sum, 2, "half-up") / 10, daily };
}