Functional Weave
Code in Rust

finance.proration@1.0.0

impl/rust.rs

2,735 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_allocate::allocate;  ← from money.allocate ^1.0.0 · built alongside by fune
use super::money_amount::{money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune

/// Split a period's charge into the part used and the part not used.
///
/// The mid-cycle upgrade, downgrade and cancellation calculation. Written the
/// obvious way - one rounding for the charge and another for the credit - the
/// two halves do not reliably add back up: 9.99 over two days is 4.995 each way,
/// and two half-up roundings produce 5.00 and 5.00, a penny invented on a line
/// the customer can see.
///
/// One call to `money::allocate` does the whole split instead, so
/// `used + unused` is the original amount for every input, credits included.
///
/// # Panics
/// Panics if `total_days` is not positive, or `used_days` is outside
/// `0..=total_days`.
pub fn prorate(amount: &Money, total_days: i64, used_days: i64) -> ProrationSplit {
    if total_days <= 0 {
        panic!(
            "total days must be greater than zero, received {}",
            total_days
        );
    }
    if used_days < 0 {
        panic!("used days must not be negative, received {}", used_days);
    }
    if used_days > total_days {
        // Clamping here would turn a bug in the caller's period arithmetic into
        // a plausible invoice, which is far more expensive to find later.
        panic!(
            "used days must not exceed total days, received {} of {}",
            used_days, total_days
        );
    }

    // total_days > 0 guarantees the ratios do not sum to zero, so allocate is
    // safe even when one side is zero: [0, n] and [n, 0] are both well defined.
    let parts = allocate(amount, &[used_days, total_days - used_days]);

    ProrationSplit {
        total: amount.clone(),
        used: parts[0].clone(),
        unused: parts[1].clone(),
    }
}

pub fn proration_split_to_value(split: &ProrationSplit) -> Value {
    Value::obj(vec![
        ("total", money_to_value(&split.total)),
        ("used", money_to_value(&split.used)),
        ("unused", money_to_value(&split.unused)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    // Refuse what the typed signature cannot hold, with the wording TypeScript
    // and Python use, rather than let the conversion below quietly change it.
    if let Value::Float(f) = args[1] {
        if f.fract() != 0.0 {
            panic!("total days must be greater than zero, received {}", f);
        }
    }
    if let Value::Float(f) = args[2] {
        if f.fract() != 0.0 {
            panic!("used days must not be negative, received {}", f);
        }
    }
    proration_split_to_value(&prorate(
        &money_from_value(&args[0]),
        args[1].as_i64(),
        args[2].as_i64(),
    ))
}