text.number-to-words
Write a money amount in words for a cheque: "one thousand two hundred and thirty-four pounds and fifty-six pence".
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 16 tests, run in TypeScript, Python and Rust.
What it does
Writes a money amount the way it goes on a cheque or a remittance advice: 123456 pence is "one thousand two hundred and thirty-four pounds and fifty-six pence". The number words come from `text.integer-to-words`, so the British "and" rules are the same in both: "one hundred and one pounds", "one thousand and one pounds".
## Why it takes Money
For example
amount_in_words(£1,234.56)→ one thousand two hundred and thirty-four pounds and fifty-six pence the cheque exampleamount_in_words(£1.00)→ one pound one pound is singularamount_in_words(£1.01)→ one pound and one penny one penny is singular, and not one pence
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 amount_in_words(amount: &Money) -> String
| amount | Money | GBP, EUR or USD, in integer minor units |
| returns | string | lower case British English, major units then minor units |
Your code names it in one line, in the file that uses it
fune!(text.number-to-words@^1); // then call amount_in_words(…)
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::money_amount::{money_from_value, Money}; ← from money.amount ^1.0.0 · built alongside by fune
use super::text_integer_to_words::integer_to_words; ← from text.integer-to-words ^1.0.0 · built alongside by fune
/// English unit names: (code, major one, major many, minor one, minor many).
/// All three currencies have two decimal places, so the split below is by 100.
/// A currency is not added here without its real unit names: they cannot be
/// derived from a code.
const UNITS: [(&str, &str, &str, &str, &str); 3] = [
("GBP", "pound", "pounds", "penny", "pence"),
("EUR", "euro", "euros", "cent", "cents"),
("USD", "dollar", "dollars", "cent", "cents"),
];
/// Write a money amount in words, cheque style: 123456 GBP minor units is
/// "one thousand two hundred and thirty-four pounds and fifty-six pence".
///
/// # Panics
/// Panics on a currency without listed unit names, or an out-of-range amount.
pub fn amount_in_words(amount: &Money) -> String {
let units = match UNITS.iter().find(|u| u.0 == amount.currency) {
Some(found) => found,
None => panic!("no words for currency \"{}\"", amount.currency),
};
// Validate the whole amount first, so an out-of-range value fails with the
// same message it would as a plain number.
integer_to_words(amount.minor);
let magnitude = amount.minor.abs();
let major = magnitude / 100;
let minor = magnitude % 100;
let mut parts: Vec<String> = Vec::new();
if major > 0 || minor == 0 {
let name = if major == 1 { units.1 } else { units.2 };
parts.push(format!("{} {}", integer_to_words(major), name));
}
if minor > 0 {
let name = if minor == 1 { units.3 } else { units.4 };
parts.push(format!("{} {}", integer_to_words(minor), name));
}
let body = parts.join(" and ");
if amount.minor < 0 {
format!("minus {}", body)
} else {
body
}
}
pub fn fune_vector(args: &[Value]) -> Value {
Value::Str(amount_in_words(&money_from_value(&args[0])))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 2 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 text.number-to-words
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./text.number-to-words-1.0.0-rust.fune, or fetch it from a terminal with fune pull text.number-to-words@1.0.0:rust.
The whole function, every language, is one file too: text.number-to-words-1.0.0.fune, 11,719 bytes, sha256 fdaf116aa82fea8fd199e36476f6a67f745c9cc3fc9eac5e96dae653996723b7. 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 text.number-to-words
after — your function gets the result and the arguments, and returns the final result.
// fune: after text.number-to-words
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 money.amount in text.number-to-words
// fune: replace text.integer-to-words in text.number-to-words
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 text.number-to-words --steps.
// fune: step text.number-to-words 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 | |
|---|---|---|---|
| the cheque example | £1,234.56 | → | one thousand two hundred and thirty-four pounds and fifty-six pence |
| one pound is singular | £1.00 | → | one pound |
| one penny is singular, and not one pence | £1.01 | → | one pound and one penny |
| a single penny on its own | £0.01 | → | one penny |
| under a pound leaves the pounds out | £0.50 | → | fifty pence |
| a whole amount leaves the pence out | £10.00 | → | ten pounds |
| zero is zero pounds | £0.00 | → | zero pounds |
| the British and inside the pounds | £101.00 | → | one hundred and one pounds |
| a hundred thousand pounds | £100,000.00 | → | one hundred thousand pounds |
| a million pounds and a penny | £1,000,000.01 | → | one million pounds and one penny |
Show the other 6 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a credit note reads as minus | -£25.99 | → | minus twenty-five pounds and ninety-nine pence |
| minus a single penny | -£0.01 | → | minus one penny |
| one euro and fifty cents | €1.50 | → | one euro and fifty cents |
| euros are plural and one cent singular | €2,500.01 | → | two thousand five hundred euros and one cent |
| US dollars | $1.99 | → | one dollar and ninety-nine cents |
| a currency without listed unit names is an error, not a guess | ¥100 | → | error: no words for currency "JPY" |
More from the author
A cheque amount is money, and money in this registry is never a float. Taking `Money` (integer minor units) means 0.1 + 0.2 can never turn into "thirty pence and a bit", and the pence are always exactly what was paid.
## Wording
- Major units, then " and ", then minor units: "one pound and one penny". - Singular for exactly one: "one pound", "one penny", "one euro", "one cent", "one dollar". Otherwise plural: "pounds", "pence", "euros", "cents", "dollars". - A whole amount leaves the minor units out: "ten pounds", not "ten pounds and zero pence". An amount under one major unit leaves the major units out: "fifty pence". Zero is "zero pounds". - No "only" is added. Cheque writers traditionally end with "only" to stop words being added afterwards; append it yourself if your stationery wants it. - Negative amounts (credit notes) read as "minus ...". - Everything is lower case.
## Currencies
Only the three currencies whose English unit names are listed below are supported; any other currency is an error, not a guess, because the words for a currency's units are not derivable from its code.
| Code | Major (one / many) | Minor (one / many) | |------|--------------------|--------------------| | GBP | pound / pounds | penny / pence | | EUR | euro / euros | cent / cents | | USD | dollar / dollars | cent / cents |
All three have two decimal places. EU legislation spells the plural "euro" with no "s" in English; everyday British and Irish usage, and most invoices, say "euros", which is what this writes.
## Limits
The amount's minor units must be within 2^53 - 1 (the range all three languages agree on); `text.integer-to-words` rejects anything larger.
Files
| Path | Bytes |
|---|---|
| README.md | 2,088 |
| impl/python.py | 1,550 |
| impl/rust.rs | 1,962 |
| impl/typescript.ts | 1,858 |
| vectors.json | 2,163 |