impl/typescript.ts
1,942 bytes · the TypeScript implementation · view raw
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { type Money, money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { grossToNet } from "./payroll_gross_to_net.ts"; ← from payroll.gross-to-net ^1.0.0 · built alongside by fune
import { type Payslip, type PayslipInput } from "./payroll_gross_to_net_types.ts";
/** The search refuses to look above £10,000,000 of gross pay for one period. */
const CEILING = 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.
*/
export function netToGross(targetNet: Money, input: PayslipInput): Payslip {
if (targetNet.currency !== "GBP") {
throw new RangeError(`payroll amounts must be in GBP, received ${targetNet.currency} for targetNet`);
}
if (targetNet.minor <= 0) {
throw new RangeError(`targetNet must be more than zero, received ${targetNet.minor}`);
}
const slip = (gross: number): Payslip => grossToNet({ ...input, gross: money(gross, "GBP") });
// A refund can make net exceed gross, so zero gross may already be enough.
const atZero = slip(0);
if (atZero.net.minor >= targetNet.minor) return atZero;
let low = 0;
let high = targetNet.minor;
while (slip(high).net.minor < targetNet.minor) {
low = high;
high = high * 2;
if (high > CEILING) {
throw new RangeError(`no gross pay up to ${CEILING} pence gives a net of ${targetNet.minor} pence`);
}
}
while (high - low > 1) {
const middle = Math.floor((low + high) / 2);
if (slip(middle).net.minor >= targetNet.minor) high = middle;
else low = middle;
}
return slip(high);
}