Functional Weave
Code in TypeScript

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 bp
  • fundraisingRatios(£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
fundraisingCostsMoneywhat was spent raising funds in the period
fundsRaisedMoneywhat that fundraising brought in (donations, legacies, events), in the same period
charitableSpendMoneyexpenditure on charitable activities
totalExpenditureMoneyall expenditure, including fundraising, governance and support costs
returnsFundraisingRatios

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";
impl/typescript.ts · 38 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.

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
Download for TypeScript charity.fundraising-ratio-1.0.0-typescript.fune · 10,205 bytes sha256 e3d74847753a7f37767a333035912d0591c6a81805ee7d120adda46a80922741

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.

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