Functional Weave
Code in Rust

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 bp
  • fundraising_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_costsMoneywhat was spent raising funds in the period
funds_raisedMoneywhat that fundraising brought in (donations, legacies, events), in the same period
charitable_spendMoneyexpenditure on charitable activities
total_expenditureMoneyall expenditure, including fundraising, governance and support costs
returnsFundraisingRatios

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(…)
impl/rust.rs · 68 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, 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
Download for Rust charity.fundraising-ratio-1.0.0-rust.fune · 10,959 bytes sha256 bcbf037802e6fd9a68e273f89f8718940d946406301f0620d957c4ffee62f170

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.

CaseArgumentsExpected
£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
CaseArgumentsExpected
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

PathBytes
README.md1,252
impl/python.py1,991
impl/rust.rs2,743
impl/typescript.ts2,011
vectors.json3,783