charity.fundraising-ratio
Cost to raise £1 and charitable spend ratio for a charity, in exact basis points.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 12 tests, run in TypeScript, Python and Rust.
What it does
Two headline efficiency figures from a charity's accounts:
- **Cost to raise £1**: fundraising costs divided by the funds they raised. Returned as money (`costToRaiseOne`, 20p) and as basis points (`costRatioBasisPoints`, 2000), because the money figure is rounded to the minor unit and loses detail in a currency with few decimals (¥0.3 rounds to ¥0, but 3000 basis points does not). - **Charitable spend ratio**: expenditure on charitable activities as a share of total expenditure, in basis points (8000 = 80%).
For example
fundraisingRatios(£20,000.00, £100,000.00, £800,000.00, £1,000,000.00)→ cost to raise one £0.20, cost ratio basis points 20%, charitable spend basis points 80% £20,000 to raise £100,000: 20p per £1; £800,000 of £1m on charitable activities is 80%fundraisingRatios(£0.01, £0.03, £0.02, £0.03)→ cost to raise one £0.33, cost ratio basis points 33.33%, charitable spend basis points 66.67% thirds round half up: 33.33p to 33p, 3333 bp; two thirds is 6667 bpfundraisingRatios(£0.01, £0.08, £0.01, £0.08)→ cost to raise one £0.13, cost ratio basis points 12.5%, charitable spend basis points 12.5% an exact half rounds up: 12.5p per £1 is 13p
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 fundraisingRatios(fundraisingCosts: Money, fundsRaised: Money, charitableSpend: Money, totalExpenditure: Money): FundraisingRatios
| fundraisingCosts | Money | what was spent raising funds in the period |
| fundsRaised | Money | what that fundraising brought in (donations, legacies, events), in the same period |
| charitableSpend | Money | expenditure on charitable activities |
| totalExpenditure | Money | all expenditure, including fundraising, governance and support costs |
| returns | FundraisingRatios |
The type it declares, generated into your project
/** The two headline efficiency figures. */
export interface FundraisingRatios {
/** cost of raising one major unit (£1, $1), rounded half up to the minor unit */
readonly costToRaiseOne: Money;
/** fundraisingCosts / fundsRaised, 2000 = 20p per £1; can exceed 10000 */
readonly costRatioBasisPoints: number;
/** charitableSpend / totalExpenditure, 8000 = 80% */
readonly charitableSpendBasisPoints: number;
}
Your code names it in one line, in the file that uses it
import { fundraisingRatios } from "#fune/charity.fundraising-ratio@^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, money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { currencyDigits } from "./money_currency_digits.ts"; ← from money.currency-digits ^1.0.0 · built alongside by fune
import { type FundraisingRatios } from "./charity_fundraising_ratio_types.ts";
function check(name: string, currency: string, amount: Money): void {
if (amount.currency !== currency) throw new RangeError(`currency mismatch: ${amount.currency} and ${currency}`);
if (amount.minor < 0) throw new RangeError(`${name} must not be negative, received ${amount.minor}`);
}
/**
* Cost to raise £1 and the share of spending that went on the charity's
* purposes. Each ratio is divided once from the integer amounts and rounded
* half up, so the figures do not depend on the order of any float arithmetic.
*/
export function fundraisingRatios(
fundraisingCosts: Money,
fundsRaised: Money,
charitableSpend: Money,
totalExpenditure: Money
): FundraisingRatios {
const currency = fundraisingCosts.currency;
check("fundraisingCosts", currency, fundraisingCosts);
check("fundsRaised", currency, fundsRaised);
check("charitableSpend", currency, charitableSpend);
check("totalExpenditure", currency, totalExpenditure);
if (fundsRaised.minor === 0) throw new RangeError("fundsRaised must be positive to work out a cost to raise");
if (totalExpenditure.minor === 0) throw new RangeError("totalExpenditure must be positive to work out a charitable spend ratio");
if (charitableSpend.minor > totalExpenditure.minor) {
throw new RangeError(`charitableSpend of ${charitableSpend.minor} is more than totalExpenditure of ${totalExpenditure.minor}`);
}
const unit = 10 ** currencyDigits(currency);
return {
costToRaiseOne: money(roundDiv(fundraisingCosts.minor * unit, fundsRaised.minor, "half-up"), currency),
costRatioBasisPoints: roundDiv(fundraisingCosts.minor * 10000, fundsRaised.minor, "half-up"),
charitableSpendBasisPoints: roundDiv(charitableSpend.minor * 10000, totalExpenditure.minor, "half-up"),
};
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 3 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 charity.fundraising-ratio
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./charity.fundraising-ratio-1.0.0-typescript.fune, or fetch it from a terminal with fune pull charity.fundraising-ratio@1.0.0:typescript.
The whole function, every language, is one file too: charity.fundraising-ratio-1.0.0.fune, 15,131 bytes, sha256 6528fc2825b22abe8b0c8f8b6ad4776e35bac2da7b434b2b99a119ee337cd3cc. 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 charity.fundraising-ratio
after — your function gets the result and the arguments, and returns the final result.
// fune: after charity.fundraising-ratio
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 charity.fundraising-ratio
// fune: replace money.amount in charity.fundraising-ratio
// fune: replace money.currency-digits in charity.fundraising-ratio
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 charity.fundraising-ratio --steps.
// fune: step charity.fundraising-ratio 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 | |
|---|---|---|---|
| £20,000 to raise £100,000: 20p per £1; £800,000 of £1m on charitable activities is 80% | £20,000.00, £100,000.00, £800,000.00, £1,000,000.00 | → | cost to raise one £0.20, cost ratio basis points 20%, charitable spend basis points 80% |
| thirds round half up: 33.33p to 33p, 3333 bp; two thirds is 6667 bp | £0.01, £0.03, £0.02, £0.03 | → | cost to raise one £0.33, cost ratio basis points 33.33%, charitable spend basis points 66.67% |
| an exact half rounds up: 12.5p per £1 is 13p | £0.01, £0.08, £0.01, £0.08 | → | cost to raise one £0.13, cost ratio basis points 12.5%, charitable spend basis points 12.5% |
| costs above income: £1.50 to raise £1 | £1,500.00, £1,000.00, £0.00, £1,500.00 | → | cost to raise one £1.50, cost ratio basis points 150%, charitable spend basis points 0% |
| no fundraising costs, all spending charitable | £0.00, £5,000.00, £7,000.00, £7,000.00 | → | cost to raise one £0.00, cost ratio basis points 0%, charitable spend basis points 100% |
| yen have no minor unit: ¥0.3 to raise ¥1 rounds to ¥0, the basis points keep the detail | ¥3,000, ¥10,000, ¥9,000, ¥10,000 | → | cost to raise one ¥0, cost ratio basis points 30%, charitable spend basis points 90% |
| three decimal places: 0.250 dinar to raise 1 dinar | 250.000 KWD, 1,000.000 KWD, 0.001 KWD, 0.002 KWD | → | cost to raise one 0.250 KWD, cost ratio basis points 25%, charitable spend basis points 50% |
| zero funds raised is an error | £1.00, £0.00, £0.01, £0.01 | → | error: fundsRaised must be positive |
| zero total expenditure is an error | £1.00, £1.00, £0.00, £0.00 | → | error: totalExpenditure must be positive |
| charitable spend above total expenditure is an error | £1.00, £1.00, £0.11, £0.10 | → | error: charitableSpend of 11 is more than totalExpenditure of 10 |
Show the other 2 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a negative cost is an error | -£0.01, £1.00, £0.01, £0.01 | → | error: fundraisingCosts must not be negative |
| mixed currencies are an error | £0.01, €1.00, £0.01, £0.01 | → | error: currency mismatch: EUR and GBP |
More from the author
Each ratio is one integer division of the amounts as given, rounded half up; nothing goes through a float. Minor units per major unit come from money.currency-digits, so dinars (3 decimals) and yen (none) work.
Which costs count as fundraising, and which income counts as "raised" by them, is a judgement the figures depend on heavily: the Charities SORP's "expenditure on raising funds" includes trading costs and investment management that a fundraising KPI usually leaves out. Pass the figures your definition uses and publish the definition with the number. Costs above the funds raised (a ratio over 10000) are allowed: a new appeal often costs more than it brings in in its first year.
Files
| Path | Bytes |
|---|---|
| README.md | 1,252 |
| impl/python.py | 1,991 |
| impl/rust.rs | 2,743 |
| impl/typescript.ts | 2,011 |
| vectors.json | 3,783 |