Functional Weave
Code in Python

telecoms.data-allowance

Mobile data allowance used and remaining, and the overage charge in whole blocks with an optional spend cap.

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

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

What it does

A mobile or broadband bundle for one period: how much of the allowance is used, how much is left, and what the usage beyond it costs.

Units are the caller's, as long as all three quantities use the same one: megabytes, kilobytes, or anything else. (Operators disagree on whether a GB is 1000 or 1024 MB, so this capability never converts; do it once, before calling, with the operator's own definition.)

For example

  • data_allowance(10,000, 7,500, 1, £0.10, —) → used 7,500, remaining 2,500, overage 0, overage blocks 0, overage charge £0.00, capped false, percent used basis points 75% three quarters of a 10 GB (10,000 MB) bundle used
  • data_allowance(10,000, 10,000, 1, £0.10, —) → used 10,000, remaining 0, overage 0, overage blocks 0, overage charge £0.00, capped false, percent used basis points 100% the whole bundle used exactly, nothing over
  • data_allowance(30,000, 29,999, 1, £0.10, —) → used 29,999, remaining 1, overage 0, overage blocks 0, overage charge £0.00, capped false, percent used basis points 99.99% 1 MB left of 30,000 reads 99.99%, never 100%

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 data_allowance(allowance: int, used: int, overage_block: int, price_per_block: Money, overage_cap: Optional[Money]) -> DataAllowance
allowanceintthe bundle for the period, 0 or more, in one unit: MB, say
usedintusage in the period, in the same unit
overage_blockintusage beyond the allowance is charged in whole blocks of this size, 1 or more
price_per_blockMoneythe price of each started block
overage_capMoney?the most the overage may cost in the period, or null for no cap
returnsDataAllowance

The type it declares, generated into your project

@dataclass(frozen=True)
class DataAllowance:
    """Where the customer stands against their bundle."""

    used: int
    #: allowance left, never below 0
    remaining: int
    #: usage beyond the allowance
    overage: int
    #: overage rounded up to whole blocks
    overage_blocks: int
    #: blocks x price, limited to the cap
    overage_charge: Money
    #: true when the cap reduced the charge
    capped: bool
    #: used / allowance rounded down; above 10000 once over; null for a zero allowance
    percent_used_basis_points: Optional[int]

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

from fune.telecoms.data_allowance import data_allowance  # telecoms.data-allowance@^1
impl/python.py · 43 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 typing import Optional

from .math_round_div import round_div  ← from math.round-div ^1.0.0 · built alongside by fune
from .money_amount import Money, assert_same_currency, money  ← from money.amount ^1.0.0 · built alongside by fune
from .telecoms_data_allowance_types import DataAllowance


def _is_count(value: object) -> bool:
    return not isinstance(value, bool) and isinstance(value, int) and value >= 0


def data_allowance(
    allowance: int, used: int, overage_block: int, price_per_block: Money, overage_cap: Optional[Money]
) -> DataAllowance:
    """Usage against a bundle: remaining allowance, overage in whole blocks
    priced and limited to an optional cap, and the share used, rounded down so
    a bundle with anything left never reads 100%.
    """
    if not _is_count(allowance) or not _is_count(used):
        raise ValueError(
            "allowance and usage must be whole numbers of 0 or more, received %r and %r" % (allowance, used)
        )
    if not _is_count(overage_block) or overage_block < 1:
        raise ValueError("overage block must be 1 or more, received %r" % (overage_block,))
    if price_per_block.minor < 0:
        raise ValueError("price per block must not be negative, received %d" % (price_per_block.minor,))
    if overage_cap is not None:
        assert_same_currency(price_per_block, overage_cap)
        if overage_cap.minor < 0:
            raise ValueError("overage cap must not be negative, received %d" % (overage_cap.minor,))
    overage = max(used - allowance, 0)
    overage_blocks = round_div(overage, overage_block, "up")
    uncapped = overage_blocks * price_per_block.minor
    capped = overage_cap is not None and uncapped > overage_cap.minor
    return DataAllowance(
        used=used,
        remaining=max(allowance - used, 0),
        overage=overage,
        overage_blocks=overage_blocks,
        overage_charge=money(overage_cap.minor if capped else uncapped, price_per_block.currency),
        capped=capped,
        percent_used_basis_points=None if allowance == 0 else round_div(used * 10000, allowance, "down"),
    )

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 2 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 telecoms.data-allowance
Download for Python telecoms.data-allowance-1.0.0-python.fune · 11,871 bytes sha256 09873ffafcd045e74eb7cf0122f976e7c58c546fa76a7b059b4e0fcd3045e056

The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./telecoms.data-allowance-1.0.0-python.fune, or fetch it from a terminal with fune pull telecoms.data-allowance@1.0.0:python.

The whole function, every language, is one file too: telecoms.data-allowance-1.0.0.fune, 16,774 bytes, sha256 1303c6b9048c3779a132c0ecf21d9968ae11ccd7831036a9704c4efd489e8939. 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 telecoms.data-allowance

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

# fune: after telecoms.data-allowance

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 math.round-div in telecoms.data-allowance
# fune: replace money.amount in telecoms.data-allowance

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 telecoms.data-allowance --steps.

# fune: step telecoms.data-allowance 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
three quarters of a 10 GB (10,000 MB) bundle used 10,000, 7,500, 1, £0.10, — → used 7,500, remaining 2,500, overage 0, overage blocks 0, overage charge £0.00, capped false, percent used basis points 75%
the whole bundle used exactly, nothing over 10,000, 10,000, 1, £0.10, — → used 10,000, remaining 0, overage 0, overage blocks 0, overage charge £0.00, capped false, percent used basis points 100%
1 MB left of 30,000 reads 99.99%, never 100% 30,000, 29,999, 1, £0.10, — → used 29,999, remaining 1, overage 0, overage blocks 0, overage charge £0.00, capped false, percent used basis points 99.99%
two thirds used rounds down to 66.66% 3, 2, 1, £0.10, — → used 2, remaining 1, overage 0, overage blocks 0, overage charge £0.00, capped false, percent used basis points 66.66%
250 MB over in 100 MB blocks is three blocks 5,000, 5,250, 100, £0.50, — → used 5,250, remaining 0, overage 250, overage blocks 3, overage charge £1.50, capped false, percent used basis points 105%
300 MB over in 100 MB blocks is exactly three blocks 5,000, 5,300, 100, £0.50, — → used 5,300, remaining 0, overage 300, overage blocks 3, overage charge £1.50, capped false, percent used basis points 106%
one MB over still starts a whole block 5,000, 5,001, 100, £0.50, — → used 5,001, remaining 0, overage 1, overage blocks 1, overage charge £0.50, capped false, percent used basis points 100.02%
a runaway month is limited by the spend cap 1,000, 11,000, 1, £0.01, £45.00 → used 11,000, remaining 0, overage 10,000, overage blocks 10,000, overage charge £45.00, capped true, percent used basis points 1100%
under the cap, the cap does nothing 1,000, 1,100, 1, £0.01, £45.00 → used 1,100, remaining 0, overage 100, overage blocks 100, overage charge £1.00, capped false, percent used basis points 110%
a charge exactly equal to the cap is not capped 1,000, 1,100, 100, £0.45, £0.45 → used 1,100, remaining 0, overage 100, overage blocks 1, overage charge £0.45, capped false, percent used basis points 110%
Show the other 8 tests
CaseArgumentsExpected
no allowance: pay as you go, and no percentage 0, 30, 1, £0.05, — → used 30, remaining 0, overage 30, overage blocks 30, overage charge £1.50, capped false, percent used basis points —
nothing used against no allowance 0, 0, 1, £0.05, — → used 0, remaining 0, overage 0, overage blocks 0, overage charge £0.00, capped false, percent used basis points —
nothing used yet 20,000, 0, 1,024, £1.00, — → used 0, remaining 20,000, overage 0, overage blocks 0, overage charge £0.00, capped false, percent used basis points 0%
negative usage is an error 1,000, -1, 1, £0.10, — → error: allowance and usage must be whole numbers of 0 or more
a block of zero is an error 1,000, 2,000, 0, £0.10, — → error: overage block must be 1 or more
a negative price is an error 1,000, 2,000, 1, -£0.10, — → error: price per block must not be negative
a cap in another currency is an error 1,000, 2,000, 1, £0.10, €45.00 → error: currency mismatch
a negative cap is an error 1,000, 2,000, 1, £0.10, -£0.01 → error: overage cap must not be negative

More from the author

Overage is charged in whole blocks, rounded up: with 100 MB blocks, 250 MB over the allowance is three blocks. Use a block of 1 to charge per unit. An optional cap limits the overage charge for the period (a customer-set spend cap, or a regulatory bill-shock limit); `capped` says whether it bit, so the bill can say "capped at 45.00". A charge exactly equal to the cap is not capped.

The percentage used is rounded **down** to a basis point, so a bundle is never shown as 100% used while there is still something left: 29,999 MB of a 30,000 MB allowance is 99.99%, not 100%. It carries on past 10000 when the customer is over (11000 is 110%), and is null for a zero allowance, where a percentage means nothing; pay-as-you-go usage is then all overage.

Errors: negative quantities or prices, a block size below 1, and a cap in a different currency from the price.

Files

PathBytes
README.md1,299
impl/python.py1,992
impl/rust.rs2,908
impl/typescript.ts1,811
vectors.json4,848