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
utilisationRates(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% overallutilisationRates(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 pointsutilisationRates(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.
export function utilisationRates(billableMinutes: number, availableMinutes: number, standardValue: Money, billedValue: Money, collectedValue: Money): UtilisationRates
| billableMinutes | int | chargeable time recorded in the period |
| availableMinutes | int | contracted or target hours for the same period, in minutes; more than zero |
| standardValue | Money | the billable time at standard charge-out rates (the WIP value before write-offs) |
| billedValue | Money | what was invoiced for that time |
| collectedValue | Money | what the client actually paid of the invoices |
| returns | UtilisationRates |
The type it declares, generated into your project
/** Utilisation and the three realisation rates, in basis points of 100%. */
export interface UtilisationRates {
/** billable over available time */
readonly utilisationBasisPoints: number;
/** billed over standard value; null when standard value is zero */
readonly billingRealisationBasisPoints: number | null;
/** collected over billed; null when nothing was billed */
readonly collectionRealisationBasisPoints: number | null;
/** collected over standard value; null when standard value is zero */
readonly overallRealisationBasisPoints: number | null;
}
Your code names it in one line, in the file that uses it
import { utilisationRates } from "#fune/professional.utilisation@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { roundDiv } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { type Money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { type UtilisationRates } from "./professional_utilisation_types.ts";
function ratio(numerator: number, denominator: number): number | null {
// A ratio over nothing is not 0% or 100%: null says there was nothing to realise.
return denominator === 0 ? null : roundDiv(numerator * 10000, denominator, "half-up");
}
function checkMinutes(name: string, value: number): void {
if (!Number.isInteger(value) || value < 0) {
throw new RangeError(`${name} must be a non-negative integer, received ${value}`);
}
}
/** Utilisation and billing, collection and overall realisation, in basis points. */
export function utilisationRates(
billableMinutes: number,
availableMinutes: number,
standardValue: Money,
billedValue: Money,
collectedValue: Money,
): UtilisationRates {
checkMinutes("billableMinutes", billableMinutes);
checkMinutes("availableMinutes", availableMinutes);
if (availableMinutes === 0) {
throw new RangeError("availableMinutes must be greater than zero");
}
const amounts: [string, Money][] = [["standardValue", standardValue], ["billedValue", billedValue], ["collectedValue", collectedValue]];
for (const [name, amount] of amounts) {
if (amount.currency !== standardValue.currency) {
throw new RangeError(`currency mismatch: ${standardValue.currency} and ${amount.currency}`);
}
if (amount.minor < 0) {
throw new RangeError(`${name} must not be negative, received ${amount.minor}`);
}
}
return {
utilisationBasisPoints: roundDiv(billableMinutes * 10000, availableMinutes, "half-up"),
billingRealisationBasisPoints: ratio(billedValue.minor, standardValue.minor),
collectionRealisationBasisPoints: ratio(collectedValue.minor, billedValue.minor),
overallRealisationBasisPoints: ratio(collectedValue.minor, standardValue.minor),
};
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the TypeScript 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 professional.utilisation
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./professional.utilisation-1.0.0-typescript.fune, or fetch it from a terminal with fune pull professional.utilisation@1.0.0:typescript.
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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Path | Bytes |
|---|---|
| README.md | 1,450 |
| impl/python.py | 1,937 |
| impl/rust.rs | 2,787 |
| impl/typescript.ts | 1,931 |
| vectors.json | 5,100 |