Functional Weave
Code in Python

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 forward
  • estimate_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 units
  • estimate_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_readMeterReadan actual read some time before the latest, setting the daily average
latest_readMeterReadthe most recent actual read, which the estimate carries forward
on_datedatethe date to estimate for, on or after the latest read
digitsintwhole-unit digits on the register, 1 to 15
modeRoundingModehow the estimated units round to a whole unit
returnsEstimatedRead

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
impl/python.py · 33 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.

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
Download for Python energy.estimate-read-1.0.0-python.fune · 10,638 bytes sha256 152678782663c090f786b9a573f0fd954c7ad41c1f7a7f78aa18dbe483bb8231

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.

CaseArgumentsExpected
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
CaseArgumentsExpected
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

PathBytes
README.md1,666
impl/python.py1,404
impl/rust.rs2,414
impl/typescript.ts1,415
vectors.json4,017