Functional Weave
Code in Rust

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.

pub fn data_allowance(allowance: i64, used: i64, overage_block: i64, price_per_block: &Money, overage_cap: Option<&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

/// Where the customer stands against their bundle.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DataAllowance {
    pub used: i64,
    /// allowance left, never below 0
    pub remaining: i64,
    /// usage beyond the allowance
    pub overage: i64,
    /// overage rounded up to whole blocks
    pub overage_blocks: i64,
    /// blocks x price, limited to the cap
    pub overage_charge: Money,
    /// true when the cap reduced the charge
    pub capped: bool,
    /// used / allowance rounded down; above 10000 once over; null for a zero allowance
    pub percent_used_basis_points: Option<i64>,
}

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

fune!(telecoms.data-allowance@^1);  // then call data_allowance(…)
impl/rust.rs · 81 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.

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::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_amount::{assert_same_currency, money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune

/// 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%.
///
/// # Panics
/// Panics on negative quantities or prices, a block below 1, or a cap in
/// another currency.
pub fn data_allowance(allowance: i64, used: i64, overage_block: i64, price_per_block: &Money, overage_cap: Option<&Money>) -> DataAllowance {
    if allowance < 0 || used < 0 {
        panic!(
            "allowance and usage must be whole numbers of 0 or more, received {} and {}",
            allowance, used
        );
    }
    if overage_block < 1 {
        panic!("overage block must be 1 or more, received {}", overage_block);
    }
    if price_per_block.minor < 0 {
        panic!("price per block must not be negative, received {}", price_per_block.minor);
    }
    if let Some(cap) = overage_cap {
        assert_same_currency(price_per_block, cap);
        if cap.minor < 0 {
            panic!("overage cap must not be negative, received {}", cap.minor);
        }
    }
    let overage = (used - allowance).max(0);
    let overage_blocks = round_div(overage, overage_block, "up");
    let uncapped = overage_blocks * price_per_block.minor;
    let cap_minor = overage_cap.map(|c| c.minor);
    let capped = matches!(cap_minor, Some(c) if uncapped > c);
    DataAllowance {
        used,
        remaining: (allowance - used).max(0),
        overage,
        overage_blocks,
        overage_charge: money(
            if capped { cap_minor.unwrap() } else { uncapped },
            &price_per_block.currency,
        ),
        capped,
        percent_used_basis_points: if allowance == 0 {
            None
        } else {
            Some(round_div(used * 10000, allowance, "down"))
        },
    }
}

pub fn data_allowance_to_value(d: &DataAllowance) -> Value {
    Value::obj(vec![
        ("used", Value::Int(d.used)),
        ("remaining", Value::Int(d.remaining)),
        ("overage", Value::Int(d.overage)),
        ("overageBlocks", Value::Int(d.overage_blocks)),
        ("overageCharge", money_to_value(&d.overage_charge)),
        ("capped", Value::Bool(d.capped)),
        (
            "percentUsedBasisPoints",
            match d.percent_used_basis_points {
                Some(n) => Value::Int(n),
                None => Value::Null,
            },
        ),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let cap = if args[4].is_null() { None } else { Some(money_from_value(&args[4])) };
    data_allowance_to_value(&data_allowance(
        args[0].as_i64(),
        args[1].as_i64(),
        args[2].as_i64(),
        &money_from_value(&args[3]),
        cap.as_ref(),
    ))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 2 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 telecoms.data-allowance
Download for Rust telecoms.data-allowance-1.0.0-rust.fune · 12,829 bytes sha256 e40f7e95b482098c169156d42dd2401dd42f23e30457cd9d8e6811637804e4c6

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

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