Functional Weave
Code in Rust

finance.compound-interest@1.0.0

impl/rust.rs

3,578 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_add::subtract_money;  ← from money.add ^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

/// A thousand years of daily compounding is already past the point where this
/// is modelling anything; beyond it a caller has passed the wrong unit and is
/// asking for a loop that will not finish.
const MAX_PERIODS: i64 = 120000;

/// Compound interest accrued period by period, in whole minor units.
///
/// The loop is the point. A statement is built by posting a real, rounded entry
/// each period and letting the next period earn on the posted balance, so that
/// is what happens here. `principal * (1 + r/n).powi(n*y)` evaluated once at the
/// end gives a slightly different answer and cannot be reconciled against the
/// statement it disagrees with.
///
/// The division by `periods_per_year` stays inside the same rounding step as the
/// interest itself: a nominal 4.5% compounded monthly is 37.5 basis points per
/// period, and rounding that rate to 37 or 38 first would distort the result far
/// more than rounding the accrued pennies ever does.
///
/// # Panics
/// Panics if `periods_per_year` is below 1, `years` is negative, or the period
/// count exceeds the limit.
pub fn compound_interest(
    principal: &Money,
    annual_rate_basis_points: i64,
    periods_per_year: i64,
    years: i64,
) -> CompoundInterestResult {
    if periods_per_year < 1 {
        panic!(
            "periods_per_year must be at least 1, received {}",
            periods_per_year
        );
    }
    if years < 0 {
        panic!("years must not be negative, received {}", years);
    }

    let periods = periods_per_year * years;
    if periods > MAX_PERIODS {
        panic!(
            "{} periods is beyond the {} period limit",
            periods, MAX_PERIODS
        );
    }

    let mut balance = money(principal.minor, &principal.currency);
    for _ in 0..periods {
        let accrued = round_div(
            balance.minor * annual_rate_basis_points,
            10000 * periods_per_year,
            "half-up",
        );
        balance = money(balance.minor + accrued, &balance.currency);
    }

    CompoundInterestResult {
        principal: money(principal.minor, &principal.currency),
        interest: subtract_money(&balance, principal),
        total: balance,
        periods,
    }
}

pub fn compound_interest_to_value(result: &CompoundInterestResult) -> Value {
    Value::obj(vec![
        ("principal", money_to_value(&result.principal)),
        ("interest", money_to_value(&result.interest)),
        ("total", money_to_value(&result.total)),
        ("periods", Value::Int(result.periods)),
    ])
}

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!("annualRateBasisPoints must be an integer, received {}", f);
        }
    }
    if let Value::Float(f) = args[2] {
        if f.fract() != 0.0 {
            panic!("periodsPerYear must be at least 1, received {}", f);
        }
    }
    if let Value::Float(f) = args[3] {
        if f.fract() != 0.0 {
            panic!("years must not be negative, received {}", f);
        }
    }
    compound_interest_to_value(&compound_interest(
        &money_from_value(&args[0]),
        args[1].as_i64(),
        args[2].as_i64(),
        args[3].as_i64(),
    ))
}