Functional Weave
Code in TypeScript

agri.growing-degree-days

Growing degree days (thermal time) from daily min/max temperatures, a base and an optional upper cap.

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

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

What it does

Growing degree days (GDD, heat units, thermal time) from daily minimum and maximum temperatures: how much warmth above a base temperature a crop, pest or pasture has had. `growingDegreeDays([{min: 10, max: 20}], 10, null, "average")` is 5.

## The two methods

For example

  • growingDegreeDays(days ×1, 10, —, average) → total 5, daily 5 one day 10 to 20 °C over base 10 is 5 degree days
  • growingDegreeDays(days ×1, 10, 30, average) → total 7.5, daily 7.5 maize 10/30: averaging caps the hot max but keeps the cool min
  • growingDegreeDays(days ×1, 10, 30, clamped) → total 10, daily 10 maize 10/30: clamping raises the cool min to the base as well

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 growingDegreeDays(days: readonly DailyTemperature[], baseCelsius: number, upperCelsius: number | null, method: DegreeDayMethod): DegreeDays
daysDailyTemperature[]one reading per day, in order; may be empty
baseCelsiusfloatno development below this, e.g. 10 for maize, 0 for wheat thermal time; to 0.1 °C
upperCelsiusfloat?no extra development above this (30 for maize); null for no cap
methodDegreeDayMethodaverage: max(0, mean - base); clamped: min and max held within base..upper first
returnsDegreeDays

The types it declares, generated into your project

/** One day's extremes in °C, taken to 0.1 °C. */
export interface DailyTemperature {
  readonly min: number;
  readonly max: number;
}

export type DegreeDayMethod = "average" | "clamped";

/** The total, and each day's contribution. */
export interface DegreeDays {
  /** sum of the exact daily values, then rounded half up to 0.1 */
  readonly total: number;
  /** each day exactly, a multiple of 0.05 */
  readonly daily: readonly number[];
}

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

import { growingDegreeDays } from "#fune/agri.growing-degree-days@^1";
impl/typescript.ts · 56 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 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 };
}

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.growing-degree-days
Download for TypeScript agri.growing-degree-days-1.0.0-typescript.fune · 10,713 bytes sha256 e00facd3db4822a2ff2257010f0b79cb8c233b165f6eb63d81bc4e8641e8680c

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

The whole function, every language, is one file too: agri.growing-degree-days-1.0.0.fune, 16,359 bytes, sha256 32de959c3bba666eb438eecb3ef515d8cd611fdb6ab664a9e7bfaf32c5c31bc8. 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.growing-degree-days

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

// fune: after agri.growing-degree-days

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.growing-degree-days
// fune: replace math.round-float in agri.growing-degree-days

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.growing-degree-days --steps.

// fune: step agri.growing-degree-days 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
one day 10 to 20 °C over base 10 is 5 degree days days ×1, 10, —, average → total 5, daily 5
maize 10/30: averaging caps the hot max but keeps the cool min days ×1, 10, 30, average → total 7.5, daily 7.5
maize 10/30: clamping raises the cool min to the base as well days ×1, 10, 30, clamped → total 10, daily 10
a cold day: the average method floors a negative day at zero days ×1, 5, —, average → total 0, daily 0
the same cold day clamped still counts the warm afternoon days ×1, 5, —, clamped → total 1.5, daily 1.5
three days of 0.05 total 0.15, rounded once to 0.2, not 0.3 from rounding each day days ×3, 10, —, average → total 0.2, daily 0.05, 0.05, 0.05
readings are taken to 0.1 °C before averaging days ×1, 10, —, average → total 5.1, daily 5.05
a day entirely above the cap counts only up to the cap days ×1, 10, 30, average → total 20, daily 20
clamped, a day entirely above the cap is the same days ×1, 10, 30, clamped → total 20, daily 20
base 0 with a frost: average floors, clamped counts the thaw days ×1, 0, —, clamped → total 2.5, daily 2.5
Show the other 8 tests
CaseArgumentsExpected
base 0 with a frost averaged is zero days ×1, 0, —, average → total 0, daily 0
a spring week for grass T-sum at base 0 days ×4, 0, —, average → total 16, daily 5, 3, 0, 8
no days is zero , 10, —, average → total 0, daily
a day with min equal to max days ×1, 10, —, average → total 5, daily 5
min above max is an error days ×2, 10, —, average → error: day 2: min must not be above max
an upper cap at the base is an error days ×1, 10, 10, average → error: upperCelsius must be above baseCelsius
an impossible temperature is an error days ×1, 10, —, average → error: day 1: temperatures must be finite numbers from -100 to 100
an unknown method is an error days ×1, 10, —, triangle → error: unknown method "triangle"

More from the author

McMaster and Wilhelm (1997) showed that "the" GDD equation is read two ways, and that the difference matters most on cool days, so the method is an argument rather than a hidden choice:

- **average** (their method 1): the day is (max + min) / 2 - base, and a negative day counts as 0. With an upper cap, max and min above the cap are first lowered to it. - **clamped** (their method 2, and the US "86/50" maize method): max and min are each held within base..upper first, then averaged and the base subtracted. A day that is 5 °C at dawn and 35 °C in the afternoon counts 10 GDD at base 10, cap 30, where averaging gives 7.5.

The upper cap is optional (null for none). Neither method models the hours above the cap (no triangle or sine methods).

## Exactness

Temperatures and the base are taken to 0.1 °C (half away from zero, `math.round-float`), so each day is an exact multiple of 0.05 and is returned as it is. The total adds the exact daily values and rounds once, half up, to 0.1. Rounding each day first would make three days of 0.05 into 0.3; this gives 0.2 (0.15 rounded).

## Limits

Temperatures, base and cap are -100 to 100 °C; the cap must be above the base; a day's min must not be above its max. An empty list is 0. The function does not know crop bases: 10 °C for maize, 0 °C for wheat thermal time and grass T-sum, 5 or 6 °C for many temperate crops are the caller's choice.

## Source

G. S. McMaster and W. W. Wilhelm, "Growing degree-days: one equation, two interpretations", Agricultural and Forest Meteorology 87 (1997) 291-300, https://doi.org/10.1016/S0168-1923(97)00027-0.

Files

PathBytes
README.md1,909
impl/python.py2,370
impl/rust.rs3,057
impl/typescript.ts2,326
vectors.json3,175