Functional Weave
Code in Rust

energy.degree-days

Heating and cooling degree days from daily mean temperatures and a base temperature, exact to 0.1.

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

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

What it does

Heating degree days (HDD) and cooling degree days (CDD) for a run of days, from each day's mean temperature and a base temperature:

HDD = sum over days of max(0, base - mean)
CDD = sum over days of max(0, mean - base)

For example

  • degree_days(100, 155, 200, 155) → heating 55, cooling 45, days 3 one cold, one at the base, one warm day at 15.5
  • degree_days(21, -13, 0, 35, 48, 62, 17, 155) → heating 915, cooling 0, days 7 a cold week in January
  • degree_days(221, 254, 198, 155) → heating 0, cooling 208, days 3 a heatwave counts only cooling

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.

pub fn degree_days(daily_mean_temperatures: &[i64], base_temperature: i64) -> DegreeDays
daily_mean_temperaturesint[]one mean per day, in tenths of a degree Celsius: 12.3 °C is 123
base_temperatureintin tenths of a degree: the UK convention is 15.5 °C, 155
returnsDegreeDays

The type it declares, generated into your project

/// Heating and cooling degree days over the same run of days.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DegreeDays {
    /// sum of how far each day's mean fell below the base, in tenths of a degree day
    pub heating: i64,
    /// sum of how far each day's mean rose above the base, in tenths of a degree day
    pub cooling: i64,
    /// how many daily means were counted
    pub days: i64,
}

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

fune!(energy.degree-days@^1);  // then call degree_days(…)
impl/rust.rs · 58 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.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one

const LIMIT: i64 = 1000; // 100.0 °C in tenths

fn check_temperature(what: &str, value: i64) {
    if value < -LIMIT || value > LIMIT {
        panic!("{} must be whole tenths of a degree from -1000 to 1000, received {}", what, value);
    }
}

/// Heating and cooling degree days in tenths: each day adds how far its mean
/// fell below (heating) or rose above (cooling) the base.
///
/// # Panics
/// Panics if a temperature is outside -100.0 to 100.0 °C.
pub fn degree_days(daily_mean_temperatures: &[i64], base_temperature: i64) -> DegreeDays {
    check_temperature("base temperature", base_temperature);
    let mut heating = 0;
    let mut cooling = 0;
    for &mean in daily_mean_temperatures {
        check_temperature("daily mean temperature", mean);
        if mean < base_temperature {
            heating += base_temperature - mean;
        } else if mean > base_temperature {
            cooling += mean - base_temperature;
        }
    }
    DegreeDays {
        heating,
        cooling,
        days: daily_mean_temperatures.len() as i64,
    }
}

pub fn degree_days_to_value(d: &DegreeDays) -> Value {
    Value::obj(vec![
        ("heating", Value::Int(d.heating)),
        ("cooling", Value::Int(d.cooling)),
        ("days", Value::Int(d.days)),
    ])
}

fn whole_tenths(what: &str, v: &Value) -> i64 {
    if let Value::Float(f) = v {
        panic!("{} must be whole tenths of a degree from -1000 to 1000, received {}", what, f);
    }
    v.as_i64()
}

pub fn fune_vector(args: &[Value]) -> Value {
    let temps: Vec<i64> = args[0]
        .as_arr()
        .iter()
        .map(|v| whole_tenths("daily mean temperature", v))
        .collect();
    let base = whole_tenths("base temperature", &args[1]);
    degree_days_to_value(&degree_days(&temps, base))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add energy.degree-days
Download for Rust energy.degree-days-1.0.0-rust.fune · 7,971 bytes sha256 4673508e1e0f2e609bb1f54fc77ee3f7240315026c4460e60b3487faf221a46d

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

The whole function, every language, is one file too: energy.degree-days-1.0.0.fune, 10,167 bytes, sha256 dadb9334811da41846ae4e0017ec98006bd554a04830a1c1dd1ed49719b483e0. 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 energy.degree-days

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

// fune: after energy.degree-days

replace — it requires no other capability, so there is no dependency to replace.

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

// fune: step energy.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 cold, one at the base, one warm day at 15.5 100, 155, 200, 155 → heating 55, cooling 45, days 3
a cold week in January 21, -13, 0, 35, 48, 62, 17, 155 → heating 915, cooling 0, days 7
a heatwave counts only cooling 221, 254, 198, 155 → heating 0, cooling 208, days 3
warm and cold days do not cancel out 55, 255, 155 → heating 100, cooling 100, days 2
a day exactly at the base counts for neither 155, 155 → heating 0, cooling 0, days 1
tenths below the base are kept, not rounded away 154, 154, 154, 155 → heating 3, cooling 0, days 3
below freezing adds the full distance to the base -25, 155 → heating 180, cooling 0, days 1
a lower base for a well-insulated building 100, 120, 130, 120 → heating 20, cooling 10, days 3
a cooling base of 22 °C 230, 215, 260, 220 → heating 5, cooling 50, days 3
no days is no degree days , 155 → heating 0, cooling 0, days 0
Show the other 4 tests
CaseArgumentsExpected
the extremes allowed -1,000, 1,000, 0 → heating 1,000, cooling 1,000, days 2
a temperature in hundredths is out of range 1,550, 155 → error: daily mean temperature must be whole tenths of a degree
a fractional tenth is refused 123.5, 155 → error: daily mean temperature must be whole tenths of a degree
a base in Fahrenheit tenths is out of range 100, 1,500 → error: base temperature must be whole tenths of a degree

More from the author

They are the standard way to normalise heating (or cooling) energy for the weather: kWh per degree day is comparable between a mild and a cold winter where raw kWh is not.

## Units

Temperatures and results are integers in **tenths of a degree**: 12.3 °C is `123`, and 45 degree days is `450`. Temperatures are recorded to a tenth of a degree, and with integer tenths every sum is exact, so the answer is exact to 0.1 of a degree day in all three languages rather than approximately so.

## Decisions

- **The base is an argument.** 15.5 °C (`155`) is the long-standing UK convention for heating degree days (CIBSE TM41, and the base of the published UK regional series); buildings with better insulation or higher internal gains are often analysed at a lower base, and cooling is often counted above 15.5 °C or 18 °C. The same base is used for both sums. - **Mean-temperature method only.** Each day contributes `base - mean` when positive. The Met Office / UKMO method that estimates degree days from a day's maximum and minimum (for days where the base falls between them) is a different formula and is not what this does; pass the true daily mean. - **A day at exactly the base counts for neither.** - An empty list gives zeros. A mean outside -100.0 to 100.0 °C is refused as a unit mistake (Fahrenheit, or whole degrees times 100).

## Source

CIBSE TM41, "Degree-days: theory and application" (Chartered Institution of Building Services Engineers), for the definition and the 15.5 °C UK base.

Files

PathBytes
README.md1,773
impl/python.py1,078
impl/rust.rs1,819
impl/typescript.ts1,015
vectors.json1,969