Functional Weave
Code in TypeScript

payroll.net-to-gross@1.0.0

impl/rust.rs

2,546 bytes · the Rust 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.

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]),
    ))
}