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::money_amount::{money, money_from_value, money_to_value, Money}; ← from money.amount ^1.0.0 · built alongside by fune
use super::money_apply_rate::apply_rate; ← from money.apply-rate ^1.0.0 · built alongside by fune
/// Draw an income from a pot year by year, withdrawal first and growth on
/// what is left, until the pot runs out or max_years is reached.
///
/// # Panics
/// Panics on a negative pot, a withdrawal of 0 or less, growth or an increase
/// below -10000, max_years outside 1 to 100, or mixed currencies.
pub fn drawdown_sustainability(
pot: &Money,
annual_withdrawal: &Money,
annual_growth_basis_points: i64,
withdrawal_increase_basis_points: i64,
max_years: i64,
) -> Drawdown {
let currency = pot.currency.as_str();
if annual_withdrawal.currency != currency {
panic!("currency mismatch: {} and {}", currency, annual_withdrawal.currency);
}
if pot.minor < 0 {
panic!("pot must be whole minor units, 0 or more; received {}", pot.minor);
}
if annual_withdrawal.minor <= 0 {
panic!(
"annualWithdrawal must be whole minor units greater than zero; received {}",
annual_withdrawal.minor
);
}
if annual_growth_basis_points < -10000 {
panic!(
"annualGrowthBasisPoints must be a whole number, -10000 or more; received {}",
annual_growth_basis_points
);
}
if withdrawal_increase_basis_points < -10000 {
panic!(
"withdrawalIncreaseBasisPoints must be a whole number, -10000 or more; received {}",
withdrawal_increase_basis_points
);
}
if !(1..=100).contains(&max_years) {
panic!("maxYears must be a whole number from 1 to 100; received {}", max_years);
}
let mut balance = pot.minor;
let mut planned = annual_withdrawal.minor;
let mut full_years: i64 = 0;
let mut total: i64 = 0;
let mut exhausted = false;
let mut schedule: Vec<DrawdownYear> = Vec::new();
for year in 1..=max_years {
let opening = balance;
let withdrawal = planned.min(opening);
if withdrawal == planned {
full_years += 1;
}
total += withdrawal;
let remaining = opening - withdrawal;
let growth = apply_rate(&money(remaining, currency), annual_growth_basis_points, "half-up").minor;
balance = remaining + growth;
schedule.push(DrawdownYear {
year,
opening: money(opening, currency),
withdrawal: money(withdrawal, currency),
growth: money(growth, currency),
closing: money(balance, currency),
});
if balance == 0 {
exhausted = true;
break;
}
planned += apply_rate(&money(planned, currency), withdrawal_increase_basis_points, "half-up").minor;
}
Drawdown {
full_years,
exhausted,
total_withdrawn: money(total, currency),
closing_balance: money(balance, currency),
schedule,
}
}
pub fn drawdown_to_value(result: &Drawdown) -> Value {
Value::obj(vec![
("fullYears", Value::Int(result.full_years)),
("exhausted", Value::Bool(result.exhausted)),
("totalWithdrawn", money_to_value(&result.total_withdrawn)),
("closingBalance", money_to_value(&result.closing_balance)),
(
"schedule",
Value::Arr(
result
.schedule
.iter()
.map(|y| {
Value::obj(vec![
("year", Value::Int(y.year)),
("opening", money_to_value(&y.opening)),
("withdrawal", money_to_value(&y.withdrawal)),
("growth", money_to_value(&y.growth)),
("closing", money_to_value(&y.closing)),
])
})
.collect(),
),
),
])
}
fn fraction(v: &Value) -> Option<f64> {
match v {
Value::Float(f) if f.fract() != 0.0 => Some(*f),
_ => None,
}
}
pub fn fune_vector(args: &[Value]) -> Value {
if let Some(f) = fraction(args[0].get("minor")) {
panic!("pot must be whole minor units, 0 or more; received {}", f);
}
if let Some(f) = fraction(args[1].get("minor")) {
panic!("annualWithdrawal must be whole minor units greater than zero; received {}", f);
}
if let Some(f) = fraction(&args[2]) {
panic!("annualGrowthBasisPoints must be a whole number, -10000 or more; received {}", f);
}
if let Some(f) = fraction(&args[3]) {
panic!("withdrawalIncreaseBasisPoints must be a whole number, -10000 or more; received {}", f);
}
if let Some(f) = fraction(&args[4]) {
panic!("maxYears must be a whole number from 1 to 100; received {}", f);
}
drawdown_to_value(&drawdown_sustainability(
&money_from_value(&args[0]),
&money_from_value(&args[1]),
args[2].as_i64(),
args[3].as_i64(),
args[4].as_i64(),
))
}