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
fundraising_ratios(£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%fundraising_ratios(£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 bpfundraising_ratios(£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.
pub fn fundraising_ratios(fundraising_costs: &Money, funds_raised: &Money, charitable_spend: &Money, total_expenditure: &Money) -> FundraisingRatios
| fundraising_costs | Money | what was spent raising funds in the period |
| funds_raised | Money | what that fundraising brought in (donations, legacies, events), in the same period |
| charitable_spend | Money | expenditure on charitable activities |
| total_expenditure | Money | all expenditure, including fundraising, governance and support costs |
| returns | FundraisingRatios |
The type it declares, generated into your project
/// The two headline efficiency figures.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FundraisingRatios {
/// cost of raising one major unit (£1, $1), rounded half up to the minor unit
pub cost_to_raise_one: Money,
/// fundraisingCosts / fundsRaised, 2000 = 20p per £1; can exceed 10000
pub cost_ratio_basis_points: i64,
/// charitableSpend / totalExpenditure, 8000 = 80%
pub charitable_spend_basis_points: i64,
}
Your code names it in one line, in the file that uses it
fune!(charity.fundraising-ratio@^1); // then call fundraising_ratios(…)
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, money_from_value, money_to_value, Money}; ← from money.amount ^1.0.0 · built alongside by fune
use super::money_currency_digits::currency_digits; ← from money.currency-digits ^1.0.0 · built alongside by fune
fn check(name: &str, currency: &str, amount: &Money) {
if amount.currency != currency {
panic!("currency mismatch: {} and {}", amount.currency, currency);
}
if amount.minor < 0 {
panic!("{} must not be negative, received {}", name, 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.
///
/// # Panics
/// Panics on negative amounts, mixed currencies, zero funds raised or total
/// expenditure, or charitable spend above total expenditure.
pub fn fundraising_ratios(
fundraising_costs: &Money,
funds_raised: &Money,
charitable_spend: &Money,
total_expenditure: &Money,
) -> FundraisingRatios {
let currency = fundraising_costs.currency.as_str();
check("fundraisingCosts", currency, fundraising_costs);
check("fundsRaised", currency, funds_raised);
check("charitableSpend", currency, charitable_spend);
check("totalExpenditure", currency, total_expenditure);
if funds_raised.minor == 0 {
panic!("fundsRaised must be positive to work out a cost to raise");
}
if total_expenditure.minor == 0 {
panic!("totalExpenditure must be positive to work out a charitable spend ratio");
}
if charitable_spend.minor > total_expenditure.minor {
panic!(
"charitableSpend of {} is more than totalExpenditure of {}",
charitable_spend.minor, total_expenditure.minor
);
}
let unit = 10i64.pow(currency_digits(currency) as u32);
FundraisingRatios {
cost_to_raise_one: money(round_div(fundraising_costs.minor * unit, funds_raised.minor, "half-up"), currency),
cost_ratio_basis_points: round_div(fundraising_costs.minor * 10000, funds_raised.minor, "half-up"),
charitable_spend_basis_points: round_div(charitable_spend.minor * 10000, total_expenditure.minor, "half-up"),
}
}
pub fn fundraising_ratios_to_value(r: &FundraisingRatios) -> Value {
Value::obj(vec![
("costToRaiseOne", money_to_value(&r.cost_to_raise_one)),
("costRatioBasisPoints", Value::Int(r.cost_ratio_basis_points)),
("charitableSpendBasisPoints", Value::Int(r.charitable_spend_basis_points)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
fundraising_ratios_to_value(&fundraising_ratios(
&money_from_value(&args[0]),
&money_from_value(&args[1]),
&money_from_value(&args[2]),
&money_from_value(&args[3]),
))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 3 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 charity.fundraising-ratio
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./charity.fundraising-ratio-1.0.0-rust.fune, or fetch it from a terminal with fune pull charity.fundraising-ratio@1.0.0:rust.
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 |