Functional Weave
Code in Rust

payroll.overtime@1.0.1

impl/rust.rs

3,074 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::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune
use super::money_sum::sum_money;  ← from money.sum ^1.0.0 · built alongside by fune

/// Price the minutes worked band by band, rounding once per payslip line.
///
/// # Panics
/// Panics on negative minutes, an empty or unordered band list, or a band
/// that does not start at 0.
pub fn overtime_pay(minutes_worked: i64, hourly_rate: &Money, bands: &[OvertimeBand]) -> OvertimePay {
    if minutes_worked < 0 {
        panic!("minutesWorked must be a whole number of minutes, not negative, received {}", minutes_worked);
    }
    if bands.is_empty() {
        panic!("overtime needs at least one band");
    }
    if bands[0].from_minutes != 0 {
        panic!("the first band must start at 0 minutes");
    }
    for i in 1..bands.len() {
        if bands[i].from_minutes <= bands[i - 1].from_minutes {
            panic!("bands must be in strictly ascending order of fromMinutes");
        }
    }
    let mut lines: Vec<OvertimeLine> = Vec::new();
    for i in 0..bands.len() {
        let start = bands[i].from_minutes;
        let end = if i + 1 < bands.len() { bands[i + 1].from_minutes.min(minutes_worked) } else { minutes_worked };
        let minutes = end - start;
        if minutes <= 0 {
            break;
        }
        let multiplier = bands[i].multiplier_basis_points;
        if multiplier < 0 {
            panic!("multiplierBasisPoints must be a whole number, not negative, received {}", multiplier);
        }
        let pay = money(round_div(hourly_rate.minor * minutes * multiplier, 60 * 10000, "half-up"), &hourly_rate.currency);
        lines.push(OvertimeLine { minutes, multiplier_basis_points: multiplier, pay });
    }
    let pays: Vec<Money> = lines.iter().map(|l| l.pay.clone()).collect();
    let total = sum_money(&pays, &hourly_rate.currency);
    OvertimePay { lines, total }
}

pub fn fune_vector(args: &[Value]) -> Value {
    if let Value::Float(f) = &args[0] {
        panic!("minutesWorked must be a whole number of minutes, not negative, received {}", f);
    }
    let bands: Vec<OvertimeBand> = args[2]
        .as_arr()
        .iter()
        .map(|b| OvertimeBand {
            from_minutes: b.get("fromMinutes").as_i64(),
            multiplier_basis_points: b.get("multiplierBasisPoints").as_i64(),
        })
        .collect();
    let result = overtime_pay(args[0].as_i64(), &money_from_value(&args[1]), &bands);
    Value::obj(vec![
        (
            "lines",
            Value::Arr(
                result
                    .lines
                    .iter()
                    .map(|l| {
                        Value::obj(vec![
                            ("minutes", Value::Int(l.minutes)),
                            ("multiplierBasisPoints", Value::Int(l.multiplier_basis_points)),
                            ("pay", money_to_value(&l.pay)),
                        ])
                    })
                    .collect(),
            ),
        ),
        ("total", money_to_value(&result.total)),
    ])
}