energy.estimate-read
Estimate a meter read for a date from the average daily consumption between two earlier reads, with rollover.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 17 tests, run in TypeScript, Python and Rust.
What it does
Estimates what a meter will read on a date, the way a supplier estimates a bill when nobody has read the meter:
daily average = units between the earlier and latest reads / days between them
units = daily average x days from the latest read to the date
read = latest read + units, wrapped past all nines
For example
estimate_read(date 2026-01-01, value 10,000, date 2026-01-31, value 10,300, 2026-02-15, 5, half-up)→ read 10,450, units 150, rolled over false 10 units a day carried 15 days forwardestimate_read(date 2026-03-01, value 99,900, date 2026-03-21, value 100, 2026-03-31, 5, half-up)→ read 200, units 100, rolled over false history across a rollover: 99900 to 00100 is 200 unitsestimate_read(date 2026-01-01, value 99,000, date 2026-02-10, value 99,800, 2026-03-12, 5, half-up)→ read 400, units 600, rolled over true the estimate itself rolls over past 99999
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 estimate_read(earlier_read: &MeterRead, latest_read: &MeterRead, on_date: &str, digits: i64, mode: &str) -> EstimatedRead
| earlier_read | MeterRead | an actual read some time before the latest, setting the daily average |
| latest_read | MeterRead | the most recent actual read, which the estimate carries forward |
| on_date | date | the date to estimate for, on or after the latest read |
| digits | int | whole-unit digits on the register, 1 to 15 |
| mode | RoundingMode | how the estimated units round to a whole unit |
| returns | EstimatedRead |
The types it declares, generated into your project
/// One meter read.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MeterRead {
pub date: String,
/// the register, whole units
pub value: i64,
}
/// The estimate, as the meter would show it, and the units behind it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EstimatedRead {
/// the estimated register, wrapped past all nines if it rolls over
pub read: i64,
/// estimated consumption since the latest read
pub units: i64,
/// true when the estimate passes the top of the register
pub rolled_over: bool,
}
Your code names it in one line, in the file that uses it
fune!(energy.estimate-read@^1); // then call estimate_read(…)
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
use super::dates_days_between::days_between; ← from dates.days-between ^1.0.0 · built alongside by fune
use super::energy_meter_advance::meter_advance; ← from energy.meter-advance ^1.0.0 · built alongside by fune
use super::math_round_div::round_div; ← from math.round-div ^1.0.0 · built alongside by fune
const MAX_SAFE: i128 = 9_007_199_254_740_991;
/// Carry the latest read forward to `on_date` at the average daily
/// consumption between the two reads, rounding once, and wrap past all nines.
///
/// # Panics
/// Panics on reads that do not fit the register, reads in the wrong order, a
/// target date before the latest read, or an estimate too large to be exact.
pub fn estimate_read(
earlier_read: &MeterRead,
latest_read: &MeterRead,
on_date: &str,
digits: i64,
mode: &str,
) -> EstimatedRead {
let consumption = meter_advance(earlier_read.value, latest_read.value, digits);
let history = days_between(&earlier_read.date, &latest_read.date);
if history <= 0 {
panic!(
"the earlier read must be dated before the latest read, received {} and {}",
earlier_read.date, latest_read.date
);
}
let ahead = days_between(&latest_read.date, on_date);
if ahead < 0 {
panic!(
"onDate must not be before the latest read, received {} and {}",
on_date, latest_read.date
);
}
let product = consumption as i128 * ahead as i128;
if product > MAX_SAFE {
panic!("estimate too large to calculate exactly");
}
let units = round_div(product as i64, history, mode);
let span = 10_i64.pow(digits as u32);
let raw = latest_read.value + units;
EstimatedRead {
read: raw % span,
units,
rolled_over: raw >= span,
}
}
pub fn meter_read_from_value(v: &Value) -> MeterRead {
if let Value::Float(f) = v.get("value") {
panic!("reads must be whole numbers, received {}", f);
}
MeterRead {
date: v.get("date").as_str().to_string(),
value: v.get("value").as_i64(),
}
}
pub fn estimated_read_to_value(e: &EstimatedRead) -> Value {
Value::obj(vec![
("read", Value::Int(e.read)),
("units", Value::Int(e.units)),
("rolledOver", Value::Bool(e.rolled_over)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
estimated_read_to_value(&estimate_read(
&meter_read_from_value(&args[0]),
&meter_read_from_value(&args[1]),
args[2].as_str(),
args[3].as_i64(),
args[4].as_str(),
))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 3 dependencies, 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.estimate-read
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./energy.estimate-read-1.0.0-rust.fune, or fetch it from a terminal with fune pull energy.estimate-read@1.0.0:rust.
The whole function, every language, is one file too: energy.estimate-read-1.0.0.fune, 14,637 bytes, sha256 bd37a314f3d4d7631725632979d80c4bbf86d74141a4ebafa8a57187faea246f. 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.estimate-read
after — your function gets the result and the arguments, and returns the final result.
// fune: after energy.estimate-read
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 dates.days-between in energy.estimate-read
// fune: replace energy.meter-advance in energy.estimate-read
// fune: replace math.round-div in energy.estimate-read
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.estimate-read --steps.
// fune: step energy.estimate-read 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 | |
|---|---|---|---|
| 10 units a day carried 15 days forward | date 2026-01-01, value 10,000, date 2026-01-31, value 10,300, 2026-02-15, 5, half-up | → | read 10,450, units 150, rolled over false |
| history across a rollover: 99900 to 00100 is 200 units | date 2026-03-01, value 99,900, date 2026-03-21, value 100, 2026-03-31, 5, half-up | → | read 200, units 100, rolled over false |
| the estimate itself rolls over past 99999 | date 2026-01-01, value 99,000, date 2026-02-10, value 99,800, 2026-03-12, 5, half-up | → | read 400, units 600, rolled over true |
| one rounding: 100 units over 30 days, 7 days ahead is 23.33, so 23 | date 2026-04-01, value 5,000, date 2026-05-01, value 5,100, 2026-05-08, 5, half-up | → | read 5,123, units 23, rolled over false |
| rounding the daily average first would give 21; up gives 24 | date 2026-04-01, value 5,000, date 2026-05-01, value 5,100, 2026-05-08, 5, up | → | read 5,124, units 24, rolled over false |
| an exact half unit under half-up rounds up | date 2026-06-01, value 700, date 2026-06-03, value 705, 2026-06-04, 4, half-up | → | read 708, units 3, rolled over false |
| an exact half unit under half-even goes to the even unit | date 2026-06-01, value 700, date 2026-06-03, value 705, 2026-06-04, 4, half-even | → | read 707, units 2, rolled over false |
| down never over-estimates | date 2026-06-01, value 700, date 2026-06-03, value 705, 2026-06-04, 4, down | → | read 707, units 2, rolled over false |
| estimating on the day of the latest read adds nothing | date 2026-01-01, value 10,000, date 2026-01-31, value 10,300, 2026-01-31, 5, half-up | → | read 10,300, units 0, rolled over false |
| February 2024 has 29 days: 290 units is 10 a day | date 2024-02-01, value 1,000, date 2024-03-01, value 1,290, 2024-03-11, 5, half-up | → | read 1,390, units 100, rolled over false |
Show the other 7 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a meter that did not move estimates no use | date 2025-12-01, value 4,242, date 2026-01-01, value 4,242, 2026-02-01, 4, half-up | → | read 4,242, units 0, rolled over false |
| a long gap over a year end | date 2025-01-01, value 0, date 2025-12-31, value 3,640, 2026-12-31, 6, half-up | → | read 7,290, units 3,650, rolled over false |
| reads in the wrong order are refused | date 2026-02-01, value 100, date 2026-01-01, value 200, 2026-03-01, 5, half-up | → | error: the earlier read must be dated before the latest read |
| two reads on the same day give no average | date 2026-02-01, value 100, date 2026-02-01, value 200, 2026-03-01, 5, half-up | → | error: the earlier read must be dated before the latest read |
| a target date before the latest read is refused | date 2026-01-01, value 100, date 2026-02-01, value 200, 2026-01-15, 5, half-up | → | error: onDate must not be before the latest read |
| a read too big for the register is refused | date 2026-01-01, value 100, date 2026-02-01, value 200,000, 2026-03-01, 5, half-up | → | error: reads must be whole numbers from 0 to 99999 |
| an unknown rounding mode is refused | date 2026-01-01, value 100, date 2026-02-01, value 200, 2026-03-01, 5, nearest | → | error: unknown rounding mode "nearest" |
More from the author
It works for any cumulative meter: electricity kWh, gas units, water m³.
## Decisions
- **One rounding.** The estimate is `consumption x days ahead / days of history`, computed exactly and rounded once to a whole unit by `mode`. Rounding the daily average first (as a spreadsheet often does) drifts: 100 units over 30 days is 3.33 a day, and 30 days of "3" is 90, not 100. - **Rollover both ways.** The history can span a rollover (99,900 to 00,100 on a five-digit meter is 200 units, via `energy.meter-advance`), and so can the estimate: 99,800 plus 600 units shows as 00,400 with `rolledOver` true. - **The average is flat.** Two reads give one average. Seasonal profiles (Elexon profile classes, the gas industry's annual quantity) weigh winter days more heavily; to use one, pass reads a year apart covering the same season, or compute the units yourself. - **Dates.** The earlier read must be strictly before the latest; the target date may equal the latest read (an estimate of zero units) but not precede it. Days come from `dates.days-between`, so leap days count.
## Source
Ofgem describes estimated bills as based on the customer's previous usage; there is no prescribed formula, and this is the simplest defensible one. The rollover rule is that of `energy.meter-advance`.
Files
| Path | Bytes |
|---|---|
| README.md | 1,666 |
| impl/python.py | 1,404 |
| impl/rust.rs | 2,414 |
| impl/typescript.ts | 1,415 |
| vectors.json | 4,017 |