use super::funejson::Value; use super::money_amount::{money, money_from_value, money_to_value, Money}; use super::money_apply_rate::apply_rate; /// 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 = 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 { 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(), )) }