Functional Weave
Code in Rust

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