Functional Weave
Code in Rust

payroll.net-to-gross@1.0.2

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);
}