payroll.net-to-gross Unreviewed
The gross pay that gives at least a target net pay in one UK pay period, found by a deterministic search.
1.0.2 · published 2026-10-03 by charlie · Anterra
Pinned by 13 tests, run in TypeScript, Python and Rust.
Unreviewed. This capability’s implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified payroll specialist has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.
Not professional advice. This capability calculates payroll figures from published rules. It is a software component for developers, not tax or legal advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed in its README, and have a payroll specialist review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.
What it does
Status: needs review by a qualified payroll professional before it is published.
Grossing up: the gross pay that gives an employee at least a target take-home pay in one period, with everything else about them as for `payroll.gross-to-net` (the `gross` in `input` is ignored).
For example
net_to_gross(£2,453.64, gross £0.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans ,…)→ gross £3,000.00, pension employee £0.00, pension employer £0.00, taxable pay £3,000.00, tax £390.20, employee ni £156.16, employer ni £387.45, student loan £0.00, postgraduate loa… grossing up the 3,000 pound payslip's net gives back 3,000net_to_gross(£1,000.00, gross £0.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans ,…)→ gross £1,000.00, pension employee £0.00, pension employer £0.00, taxable pay £1,000.00, tax £0.00, employee ni £0.00, employer ni £87.45, student loan £0.00, postgraduate loan £0.… below the allowance and the threshold, gross equals netnet_to_gross(£2,000.00, gross £0.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans ,…)→ gross £2,369.96, pension employee £0.00, pension employer £0.00, taxable pay £2,369.96, tax £264.20, employee ni £105.76, employer ni £292.94, student loan £0.00, postgraduate loa… 2,000 net needs 2,369.96, not the 2,369.26 an unrounded formula suggests
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 net_to_gross(target_net: &Money, input: &PayslipInput) -> Payslip
| target_net | Money | the take-home pay wanted, in GBP |
| input | PayslipInput | everything about the employee as for payroll.gross-to-net; its gross is ignored |
| returns | Payslip | the payslip for the lowest gross the search finds whose net is at least targetNet |
Your code names it in one line, in the file that uses it
fune!(payroll.net-to-gross@^1); // then call net_to_gross(…)
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, money_from_value, Money}; ← from money.amount ^1.0.0 · built alongside by fune
use super::payroll_gross_to_net::{gross_to_net, payslip_input_from_value, payslip_to_value}; ← from payroll.gross-to-net ^1.0.0 · built alongside by fune
use super::payroll_gross_to_net_types::{Payslip, PayslipInput};
/// The search refuses to look above £10,000,000 of gross pay for one period.
const CEILING: i64 = 1_000_000_000;
/// Gross up a net payment: the gross pay whose payslip nets at least the target.
///
/// Net pay is not a smooth function of gross: tax is on whole pounds, student
/// loans are rounded down to whole pounds, and pension and NI thresholds
/// switch on. So there is no formula to invert, and a search is the honest
/// method. It is a bisection on whole pence, with fixed bounds and midpoints,
/// so it takes the same steps and finds the same answer in every language: a
/// gross G whose net is at least the target while the net at G - 1p is below it.
///
/// # Panics
/// Panics on a non-GBP or non-positive target, when no gross up to the ceiling
/// is enough, or on anything payroll.gross-to-net refuses.
pub fn net_to_gross(target_net: &Money, input: &PayslipInput) -> Payslip {
if target_net.currency != "GBP" {
panic!(
"payroll amounts must be in GBP, received {} for targetNet",
target_net.currency
);
}
if target_net.minor <= 0 {
panic!("targetNet must be more than zero, received {}", target_net.minor);
}
let slip = |gross: i64| -> Payslip {
let mut with_gross = input.clone();
with_gross.gross = money(gross, "GBP");
gross_to_net(&with_gross)
};
// A refund can make net exceed gross, so zero gross may already be enough.
let at_zero = slip(0);
if at_zero.net.minor >= target_net.minor {
return at_zero;
}
let mut low = 0i64;
let mut high = target_net.minor;
while slip(high).net.minor < target_net.minor {
low = high;
high *= 2;
if high > CEILING {
panic!(
"no gross pay up to {} pence gives a net of {} pence",
CEILING, target_net.minor
);
}
}
while high - low > 1 {
let middle = (low + high) / 2;
if slip(middle).net.minor >= target_net.minor {
high = middle;
} else {
low = middle;
}
}
slip(high)
}
pub fn fune_vector(args: &[Value]) -> Value {
payslip_to_value(&net_to_gross(
&money_from_value(&args[0]),
&payslip_input_from_value(&args[1]),
))
}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 payroll.net-to-gross
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./payroll.net-to-gross-1.0.2-rust.fune, or fetch it from a terminal with fune pull payroll.net-to-gross@1.0.2:rust.
The whole function, every language, is one file too: payroll.net-to-gross-1.0.2.fune, 28,658 bytes, sha256 6d4d7589fc87a348906f02e669c8a9c62ad3d58e752a098cb284865d169b75fd. 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 payroll.net-to-gross
after — your function gets the result and the arguments, and returns the final result.
// fune: after payroll.net-to-gross
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 payroll.net-to-gross
// fune: replace payroll.gross-to-net in payroll.net-to-gross
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 payroll.net-to-gross --steps.
// fune: step payroll.net-to-gross 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 | |
|---|---|---|---|
| grossing up the 3,000 pound payslip's net gives back 3,000 | £2,453.64, gross £0.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans ,… | → | gross £3,000.00, pension employee £0.00, pension employer £0.00, taxable pay £3,000.00, tax £390.20, employee ni £156.16, employer ni £387.45, student loan £0.00, postgraduate loa… |
| below the allowance and the threshold, gross equals net | £1,000.00, gross £0.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans ,… | → | gross £1,000.00, pension employee £0.00, pension employer £0.00, taxable pay £1,000.00, tax £0.00, employee ni £0.00, employer ni £87.45, student loan £0.00, postgraduate loan £0.… |
| 2,000 net needs 2,369.96, not the 2,369.26 an unrounded formula suggests | £2,000.00, gross £0.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans ,… | → | gross £2,369.96, pension employee £0.00, pension employer £0.00, taxable pay £2,369.96, tax £264.20, employee ni £105.76, employer ni £292.94, student loan £0.00, postgraduate loa… |
| a tax refund can reach the target with no gross pay at all | £500.00, gross £0.00, frequency monthly, pay date 2026-07-28, tax code NT, previous taxable pay to date £6,000.00, previous tax to date £600.00, ni category A, student loan plans … | → | gross £0.00, pension employee £0.00, pension employer £0.00, taxable pay £0.00, tax -£600.00, employee ni £0.00, employer ni £0.00, student loan £0.00, postgraduate loan £0.00, … |
| a zero target is an error | £0.00, gross £0.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans , pen… | → | error: targetNet must be more than zero |
| a target in euros is an error | €1,000.00, gross £0.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans ,… | → | error: must be in GBP |
| a penny net is a penny gross | £0.01, gross £0.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans , pen… | → | gross £0.01, pension employee £0.00, pension employer £0.00, taxable pay £0.01, tax £0.00, employee ni £0.00, employer ni £0.00, student loan £0.00, postgraduate loan £0.00, net £… |
| up to the primary threshold nothing is deducted: 1,048 net is 1,048 gross | £1,048.00, gross £0.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans ,… | → | gross £1,048.00, pension employee £0.00, pension employer £0.00, taxable pay £1,048.00, tax £0.00, employee ni £0.00, employer ni £94.65, student loan £0.00, postgraduate loan £0.… |
| 1,500 net, with tax on whole pounds and NI to the penny | £1,500.00, gross £0.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans ,… | → | gross £1,675.61, pension employee £0.00, pension employer £0.00, taxable pay £1,675.61, tax £125.40, employee ni £50.21, employer ni £188.79, student loan £0.00, postgraduate loan… |
| 3,000 net, still inside the basic rate band | £3,000.00, gross £0.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans ,… | → | gross £3,758.87, pension employee £0.00, pension employer £0.00, taxable pay £3,758.87, tax £542.00, employee ni £216.87, employer ni £501.28, student loan £0.00, postgraduate loa… |
Show the other 3 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| in month 2 the same net as month 1 needs 3,000.22, not 3,000.00, because tax rounds the year to date down to the pound | £2,453.64, gross £0.00, frequency monthly, pay date 2026-05-28, tax code 1257L, previous taxable pay to date £3,000.00, previous tax to date £390.20, ni category A, student loan p… | → | gross £3,000.22, pension employee £0.00, pension employer £0.00, taxable pay £3,000.22, tax £390.40, employee ni £156.18, employer ni £387.48, student loan £0.00, postgraduate loa… |
| a negative target is an error | -£1.00, gross £0.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans , … | → | error: targetNet must be more than zero |
| a target no gross up to 10 million pounds reaches is an error | £9,000,000.00, gross £0.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan pla… | → | error: no gross pay up to 1000000000 pence gives a net of 900000000 pence |
More from the author
## Why a search
Net pay has no clean inverse. Tax is charged on whole pounds, student loans are rounded down to whole pounds, pension and National Insurance thresholds switch on and off, and the income tax band a penny falls in depends on the year to date. Algebra that inverts the rates works for the middle of a band and is a penny or a pound out at every edge. So this runs `grossToNet` itself and searches.
The search is a plain bisection on whole pence: start at 0 and at the target, double the upper bound until its net reaches the target, then halve the gap until it is one penny. Every step is integer arithmetic with fixed midpoints, so the three languages take identical steps and return identical answers.
## What "the" answer is
The result is a gross `G` whose net is at least the target and where `G - 1p` nets less than the target. Net pay does not only ever rise with gross: tax is charged on whole pounds of taxable pay, so the penny that takes taxable pay into the next pound adds 20p or more of tax and net pay dips; one more penny can also cost a whole pound of student loan. Where the target sits clear of those dips (the usual case, and every vector here) `G` is the smallest gross that reaches it. Where it falls inside one, the bisection still returns a penny-exact boundary, but a smaller gross elsewhere could also reach the target, and the net may exceed the target by up to about £1. Check `net` on the returned payslip rather than assuming it equals the target.
If a tax refund makes the net at zero gross already reach the target, the payslip for zero gross is returned. The search refuses to look above £10,000,000 for one period.
Sources: as `payroll.gross-to-net`.
1.0.1 adds tests; behaviour unchanged.
## Before you rely on this
**Not professional advice.** This capability calculates payroll figures from published rules. It is a software component for developers, not tax or legal advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed above, and have a payroll professional review how you use it, before anyone relies on the output. Provided "as is" under its licence, without warranty.
**Unreviewed.** This capability's implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified payroll professional has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.
1.0.2 marks it unreviewed. The code and the tests are unchanged.
Files
| Path | Bytes |
|---|---|
| README.md | 3,147 |
| impl/python.py | 1,954 |
| impl/rust.rs | 2,546 |
| impl/typescript.ts | 1,942 |
| vectors.json | 14,650 |