Functional Weave
Code in TypeScript

education.fee-instalments@1.0.0

impl/rust.rs

3,058 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::dates_add_days::parse_iso_date;  ← from dates.add-days ^1.0.0 · built alongside by fune
use super::money_allocate::allocate;  ← from money.allocate ^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

/// Split a fee into dated instalments that add up to it exactly.
///
/// The split is money.allocate's: floors, then the leftover pennies to the
/// largest remainders, ties to the earlier instalment.
///
/// # Panics
/// Panics on a negative total, mismatched or empty lists, negative or all-zero
/// shares, or due dates that are malformed or not strictly ascending.
pub fn fee_instalments(total: &Money, shares: &[i64], due_dates: &[String]) -> Vec<FeeInstalment> {
    if total.minor < 0 {
        panic!("total must not be negative, received {}", total.minor);
    }
    if shares.is_empty() {
        panic!("shares must name at least one instalment");
    }
    if shares.len() != due_dates.len() {
        panic!(
            "shares and dueDates must be the same length, received {} and {}",
            shares.len(),
            due_dates.len()
        );
    }
    for &s in shares {
        if s < 0 {
            panic!("each share must be a whole number of 0 or more, received {}", s);
        }
    }
    if shares.iter().sum::<i64>() == 0 {
        panic!("shares must not all be zero");
    }
    for i in 0..due_dates.len() {
        parse_iso_date(&due_dates[i]);
        if i > 0 && due_dates[i] <= due_dates[i - 1] {
            panic!(
                "dueDates must be in strictly ascending order: {} follows {}",
                due_dates[i],
                due_dates[i - 1]
            );
        }
    }
    let amounts = allocate(total, shares);
    let mut paid = 0;
    amounts
        .into_iter()
        .enumerate()
        .map(|(i, amount)| {
            paid += amount.minor;
            FeeInstalment {
                number: i as i64 + 1,
                due_date: due_dates[i].clone(),
                amount,
                cumulative: money(paid, &total.currency),
                remaining: money(total.minor - paid, &total.currency),
            }
        })
        .collect()
}

pub fn fee_instalment_to_value(f: &FeeInstalment) -> Value {
    Value::obj(vec![
        ("number", Value::Int(f.number)),
        ("dueDate", Value::str(&f.due_date)),
        ("amount", money_to_value(&f.amount)),
        ("cumulative", money_to_value(&f.cumulative)),
        ("remaining", money_to_value(&f.remaining)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let shares: Vec<i64> = args[1]
        .as_arr()
        .iter()
        .map(|v| match v {
            Value::Float(f) if f.fract() != 0.0 => panic!("each share must be a whole number of 0 or more, received {}", f),
            other => other.as_i64(),
        })
        .collect();
    let dates: Vec<String> = args[2].as_arr().iter().map(|v| v.as_str().to_string()).collect();
    Value::Arr(
        fee_instalments(&money_from_value(&args[0]), &shares, &dates)
            .iter()
            .map(fee_instalment_to_value)
            .collect(),
    )
}