Functional Weave
Code in TypeScript

invest.drawdown-sustainability@1.0.0

impl/rust.rs

5,078 bytes · the Rust implementation · view raw

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