Functional Weave
Code in Rust

payroll.net-to-gross@1.0.2

impl/python.py

1,954 bytes · the Python 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.

from dataclasses import replace

from .money_amount import Money, money  ← from money.amount ^1.0.0 · built alongside by fune
from .payroll_gross_to_net import gross_to_net  ← from payroll.gross-to-net ^1.0.0 · built alongside by fune
from .payroll_gross_to_net_types import Payslip, PayslipInput

# The search refuses to look above 10,000,000 pounds of gross pay for one period.
CEILING = 1_000_000_000


def net_to_gross(target_net: Money, input: PayslipInput) -> Payslip:
    """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.
    """
    if target_net.currency != "GBP":
        raise ValueError("payroll amounts must be in GBP, received %s for targetNet" % (target_net.currency,))
    if target_net.minor <= 0:
        raise ValueError("targetNet must be more than zero, received %s" % (target_net.minor,))

    def slip(gross: int) -> Payslip:
        return gross_to_net(replace(input, gross=money(gross, "GBP")))

    # A refund can make net exceed gross, so zero gross may already be enough.
    at_zero = slip(0)
    if at_zero.net.minor >= target_net.minor:
        return at_zero

    low = 0
    high = target_net.minor
    while slip(high).net.minor < target_net.minor:
        low = high
        high = high * 2
        if high > CEILING:
            raise ValueError("no gross pay up to %d pence gives a net of %d pence" % (CEILING, target_net.minor))
    while high - low > 1:
        middle = (low + high) // 2
        if slip(middle).net.minor >= target_net.minor:
            high = middle
        else:
            low = middle
    return slip(high)