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.5degree_days(21, -13, 0, 35, 48, 62, 17, 155)→ heating 915, cooling 0, days 7 a cold week in Januarydegree_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_temperatures | int[] | one mean per day, in tenths of a degree Celsius: 12.3 °C is 123 |
| base_temperature | int | in tenths of a degree: the UK convention is 15.5 °C, 155 |
| returns | DegreeDays |
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(…)
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(°ree_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
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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Path | Bytes |
|---|---|
| README.md | 1,773 |
| impl/python.py | 1,078 |
| impl/rust.rs | 1,819 |
| impl/typescript.ts | 1,015 |
| vectors.json | 1,969 |