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), ¤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<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(),
))
}