use super::funejson::Value; use super::math_round_div::round_div; use super::money_add::subtract_money; use super::money_amount::{money, money_from_value, money_to_value, Money}; /// 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(), )) }