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.
def estimate_read(earlier_read: MeterRead, latest_read: MeterRead, on_date: str, digits: int, mode: RoundingMode) -> 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
@dataclass(frozen=True)
class MeterRead:
"""One meter read."""
date: str
#: the register, whole units
value: int
@dataclass(frozen=True)
class EstimatedRead:
"""The estimate, as the meter would show it, and the units behind it."""
#: the estimated register, wrapped past all nines if it rolls over
read: int
#: estimated consumption since the latest read
units: int
#: true when the estimate passes the top of the register
rolled_over: bool
Your code names it in one line, in the file that uses it
from fune.energy.estimate_read import estimate_read # energy.estimate-read@^1
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from .dates_days_between import days_between ← from dates.days-between ^1.0.0 · built alongside by fune
from .energy_estimate_read_types import EstimatedRead, MeterRead
from .energy_meter_advance import meter_advance ← from energy.meter-advance ^1.0.0 · built alongside by fune
from .math_round_div import RoundingMode, round_div ← from math.round-div ^1.0.0 · built alongside by fune
MAX_SAFE = 9007199254740991
def estimate_read(
earlier_read: MeterRead, latest_read: MeterRead, on_date: str, digits: int, mode: RoundingMode
) -> EstimatedRead:
"""Carry the latest read forward to ``on_date`` at the average daily
consumption between the two reads, rounding once, and wrap past all nines.
"""
consumption = meter_advance(earlier_read.value, latest_read.value, digits)
history = days_between(earlier_read.date, latest_read.date)
if history <= 0:
raise ValueError(
"the earlier read must be dated before the latest read, received %s and %s"
% (earlier_read.date, latest_read.date)
)
ahead = days_between(latest_read.date, on_date)
if ahead < 0:
raise ValueError(
"onDate must not be before the latest read, received %s and %s" % (on_date, latest_read.date)
)
product = consumption * ahead
if product > MAX_SAFE:
raise ValueError("estimate too large to calculate exactly")
units = round_div(product, history, mode)
span = 10**digits
raw = latest_read.value + units
return EstimatedRead(read=raw % span, units=units, rolled_over=raw >= span)Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 3 dependencies, pins them in fune.lock, downloads only the Python 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 energy.estimate-read
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./energy.estimate-read-1.0.0-python.fune, or fetch it from a terminal with fune pull energy.estimate-read@1.0.0:python.
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 |