Functional Weave
Code in Rust

professional.utilisation

Billable utilisation and billing, collection and overall realisation rates, in basis points.

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

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

What it does

The four ratios a professional-services firm (law, accountancy, consulting) reports for a fee earner, a team or the firm:

- **utilisation**: billable time over available time; - **billing realisation**: what was billed over the standard value of the time (the time at standard charge-out rates), so write-downs at billing show up; - **collection realisation**: what was collected over what was billed, so write-offs and bad debts show up; - **overall realisation**: collected over standard value, the product of the two.

For example

  • utilisation_rates(6,000, 8,000, £10,000.00, £9,000.00, £8,550.00) → utilisation basis points 75%, billing realisation basis points 90%, collection realisation basis points 95%, overall realisation basis points 85.5% a typical quarter: 75% utilised, 90% billed, 95% collected, 85.5% overall
  • utilisation_rates(1, 3, £3.00, £1.00, £1.00) → utilisation basis points 33.33%, billing realisation basis points 33.33%, collection realisation basis points 100%, overall realisation basis points 33.33% a third rounds down to 3333 basis points
  • utilisation_rates(2, 3, £0.03, £0.02, £0.02) → utilisation basis points 66.67%, billing realisation basis points 66.67%, collection realisation basis points 100%, overall realisation basis points 66.67% two thirds rounds up to 6667 basis points

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 utilisation_rates(billable_minutes: i64, available_minutes: i64, standard_value: &Money, billed_value: &Money, collected_value: &Money) -> UtilisationRates
billable_minutesintchargeable time recorded in the period
available_minutesintcontracted or target hours for the same period, in minutes; more than zero
standard_valueMoneythe billable time at standard charge-out rates (the WIP value before write-offs)
billed_valueMoneywhat was invoiced for that time
collected_valueMoneywhat the client actually paid of the invoices
returnsUtilisationRates

The type it declares, generated into your project

/// Utilisation and the three realisation rates, in basis points of 100%.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UtilisationRates {
    /// billable over available time
    pub utilisation_basis_points: i64,
    /// billed over standard value; null when standard value is zero
    pub billing_realisation_basis_points: Option<i64>,
    /// collected over billed; null when nothing was billed
    pub collection_realisation_basis_points: Option<i64>,
    /// collected over standard value; null when standard value is zero
    pub overall_realisation_basis_points: Option<i64>,
}

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

fune!(professional.utilisation@^1);  // then call utilisation_rates(…)
impl/rust.rs · 73 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::{money_from_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune

fn ratio(numerator: i64, denominator: i64) -> Option<i64> {
    // A ratio over nothing is not 0% or 100%: None says there was nothing to realise.
    if denominator == 0 {
        None
    } else {
        Some(round_div(numerator * 10000, denominator, "half-up"))
    }
}

fn check_minutes(name: &str, value: i64) {
    if value < 0 {
        panic!("{} must be a non-negative integer, received {}", name, value);
    }
}

/// Utilisation and billing, collection and overall realisation, in basis points.
///
/// # Panics
/// Panics on negative minutes, zero available time, negative amounts or mixed currencies.
pub fn utilisation_rates(
    billable_minutes: i64,
    available_minutes: i64,
    standard_value: &Money,
    billed_value: &Money,
    collected_value: &Money,
) -> UtilisationRates {
    check_minutes("billableMinutes", billable_minutes);
    check_minutes("availableMinutes", available_minutes);
    if available_minutes == 0 {
        panic!("availableMinutes must be greater than zero");
    }
    for (name, amount) in [("standardValue", standard_value), ("billedValue", billed_value), ("collectedValue", collected_value)] {
        if amount.currency != standard_value.currency {
            panic!("currency mismatch: {} and {}", standard_value.currency, amount.currency);
        }
        if amount.minor < 0 {
            panic!("{} must not be negative, received {}", name, amount.minor);
        }
    }
    UtilisationRates {
        utilisation_basis_points: round_div(billable_minutes * 10000, available_minutes, "half-up"),
        billing_realisation_basis_points: ratio(billed_value.minor, standard_value.minor),
        collection_realisation_basis_points: ratio(collected_value.minor, billed_value.minor),
        overall_realisation_basis_points: ratio(collected_value.minor, standard_value.minor),
    }
}

fn optional_int(value: Option<i64>) -> Value {
    value.map_or(Value::Null, Value::Int)
}

pub fn utilisation_rates_to_value(r: &UtilisationRates) -> Value {
    Value::obj(vec![
        ("utilisationBasisPoints", Value::Int(r.utilisation_basis_points)),
        ("billingRealisationBasisPoints", optional_int(r.billing_realisation_basis_points)),
        ("collectionRealisationBasisPoints", optional_int(r.collection_realisation_basis_points)),
        ("overallRealisationBasisPoints", optional_int(r.overall_realisation_basis_points)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    utilisation_rates_to_value(&utilisation_rates(
        args[0].as_i64(),
        args[1].as_i64(),
        &money_from_value(&args[2]),
        &money_from_value(&args[3]),
        &money_from_value(&args[4]),
    ))
}

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 professional.utilisation
Download for Rust professional.utilisation-1.0.0-rust.fune · 13,058 bytes sha256 40ea54d1204f0c901a8fc117ecbacd52ef5ea851aa4bdcaf7193dd963bb36b54

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

The whole function, every language, is one file too: professional.utilisation-1.0.0.fune, 17,094 bytes, sha256 f32bb3b6126fc9e69b155a7c56e388128aef8acae763806758ee20bb1b94c333. 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 professional.utilisation

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

// fune: after professional.utilisation

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 professional.utilisation
// fune: replace money.amount in professional.utilisation

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 professional.utilisation --steps.

// fune: step professional.utilisation 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
a typical quarter: 75% utilised, 90% billed, 95% collected, 85.5% overall 6,000, 8,000, £10,000.00, £9,000.00, £8,550.00 → utilisation basis points 75%, billing realisation basis points 90%, collection realisation basis points 95%, overall realisation basis points 85.5%
a third rounds down to 3333 basis points 1, 3, £3.00, £1.00, £1.00 → utilisation basis points 33.33%, billing realisation basis points 33.33%, collection realisation basis points 100%, overall realisation basis points 33.33%
two thirds rounds up to 6667 basis points 2, 3, £0.03, £0.02, £0.02 → utilisation basis points 66.67%, billing realisation basis points 66.67%, collection realisation basis points 100%, overall realisation basis points 66.67%
exactly half a basis point rounds up, where truncating gives 0 1, 20,000, £200.00, £0.01, £0.01 → utilisation basis points 0.01%, billing realisation basis points 0.01%, collection realisation basis points 100%, overall realisation basis points 0.01%
time beyond contracted hours and premium billing both exceed 100% 9,000, 8,000, £10,000.00, £11,000.00, £11,000.00 → utilisation basis points 112.5%, billing realisation basis points 110%, collection realisation basis points 100%, overall realisation basis points 110%
no billable time is 0% utilisation 0, 7,500, £0.00, £0.00, £0.00 → utilisation basis points 0%, billing realisation basis points —, collection realisation basis points —, overall realisation basis points —
nothing billed: collection realisation is null, not 0% 600, 1,000, £500.00, £0.00, £0.00 → utilisation basis points 60%, billing realisation basis points 0%, collection realisation basis points —, overall realisation basis points 0%
no standard value: billing and overall realisation are null 600, 1,000, £0.00, £50.00, £50.00 → utilisation basis points 60%, billing realisation basis points —, collection realisation basis points 100%, overall realisation basis points —
in euros, part collected 4,200, 6,000, €2,500.00, €2,000.00, €1,500.00 → utilisation basis points 70%, billing realisation basis points 80%, collection realisation basis points 75%, overall realisation basis points 60%
no available time is an error 0, 0, £0.00, £0.00, £0.00 → error: availableMinutes must be greater than zero
Show the other 3 tests
CaseArgumentsExpected
negative billable time is an error -60, 1,000, £0.00, £0.00, £0.00 → error: billableMinutes must be a non-negative integer
mixed currencies are an error 60, 1,000, £1.00, €1.00, £1.00 → error: currency mismatch
a negative collected amount is an error 60, 1,000, £1.00, £1.00, -£0.01 → error: collectedValue must not be negative

More from the author

All four come back in basis points (10000 = 100%), each rounded half-up to the nearest basis point from the exact integer ratio, once. None is capped: time recorded beyond contracted hours gives utilisation over 10000, and billing at a premium gives realisation over 10000; both are real and worth seeing.

A realisation whose denominator is zero is null rather than 0 or 100%: with no standard value there is nothing to realise, and reporting 0% would read as a total write-off. Available time of zero is an error instead, because a fee earner with no available time has no utilisation to report.

The three amounts must share one currency, and nothing may be negative; credit notes belong in the billed figure as a reduction, not as a negative period. How the firm defines "available" (contracted hours less holiday, or a target of chargeable hours) is the caller's choice; this only divides.

Files

PathBytes
README.md1,450
impl/python.py1,937
impl/rust.rs2,787
impl/typescript.ts1,931
vectors.json5,100