Functional Weave
Code in Python

lending.daily-interest@1.0.1

impl/rust.rs

3,769 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_day_count_fraction::day_count_fraction;  ← from dates.day-count-fraction ^1.0.0 · built alongside by fune
use super::dates_days_between::days_between;  ← from dates.days-between ^1.0.0 · built alongside by fune
use super::math_rational::{add_rational, multiply_rational, rational, rational_to_integer, rational_to_value};  ← from math.rational ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value};  ← from money.amount ^1.0.0 · built alongside by fune

/// Interest on a balance that changes during the period: each balance earns
/// (or is charged) balance × rate × year fraction for the days it was held,
/// the year fractions come from dates.day-count-fraction, and the pieces are
/// added as exact fractions. Only the total is rounded, once, so splitting a
/// month into more balance changes never moves the answer by a stray penny.
///
/// # Panics
/// Panics on an empty or unordered balance list, mixed currencies, a first
/// balance after fromIso, bad dates, a rate out of range, or an exact total
/// too large for math.rational.
pub fn daily_interest(
    balances: &[DatedBalance],
    from_iso: &str,
    to_iso: &str,
    annual_rate_basis_points: i64,
    convention: &str,
    mode: &str,
) -> DailyInterest {
    let b = annual_rate_basis_points;
    if b < -100000 || b > 100000 {
        panic!("annualRateBasisPoints must be between -100000 and 100000, received {}", b);
    }
    if balances.is_empty() {
        panic!("balances must not be empty");
    }
    let days = days_between(from_iso, to_iso);
    if days < 0 {
        panic!("fromIso {} must not be after toIso {}", from_iso, to_iso);
    }
    let currency = balances[0].balance.currency.clone();
    for i in 0..balances.len() {
        days_between(&balances[i].date, &balances[i].date);
        if balances[i].balance.currency != currency {
            panic!("currency mismatch: {} and {}", currency, balances[i].balance.currency);
        }
        if i > 0 && balances[i].date <= balances[i - 1].date {
            panic!("balances must be in strictly ascending date order");
        }
    }
    if balances[0].date.as_str() > from_iso {
        panic!("the first balance starts on {}, after fromIso {}", balances[0].date, from_iso);
    }
    let rate = rational(b, 10000);
    let mut total = rational(0, 1);
    for i in 0..balances.len() {
        let date = balances[i].date.as_str();
        let start = if date > from_iso { date } else { from_iso };
        let next = if i + 1 < balances.len() { balances[i + 1].date.as_str() } else { to_iso };
        let end = if next < to_iso { next } else { to_iso };
        if start >= end {
            continue;
        }
        let fraction = day_count_fraction(start, end, convention);
        let amount = multiply_rational(&rational(balances[i].balance.minor, 1), &rate);
        total = add_rational(
            &total,
            &multiply_rational(&amount, &rational(fraction.numerator, fraction.denominator)),
        );
    }
    DailyInterest {
        interest: money(rational_to_integer(&total, mode), &currency),
        exact: total,
        days,
    }
}

pub fn dated_balance_from_value(v: &Value) -> DatedBalance {
    DatedBalance {
        date: v.get("date").as_str().to_string(),
        balance: money_from_value(v.get("balance")),
    }
}

pub fn daily_interest_to_value(result: &DailyInterest) -> Value {
    Value::obj(vec![
        ("interest", money_to_value(&result.interest)),
        ("exact", rational_to_value(&result.exact)),
        ("days", Value::Int(result.days)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let balances: Vec<DatedBalance> = args[0].as_arr().iter().map(dated_balance_from_value).collect();
    daily_interest_to_value(&daily_interest(
        &balances,
        args[1].as_str(),
        args[2].as_str(),
        args[3].as_i64(),
        args[4].as_str(),
        args[5].as_str(),
    ))
}