use super::funejson::Value; use super::dates_day_count_fraction::day_count_fraction; use super::dates_days_between::days_between; use super::math_rational::{add_rational, multiply_rational, rational, rational_to_integer, rational_to_value}; use super::money_amount::{money, money_from_value, money_to_value}; /// 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), ¤cy), 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 = 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(), )) }