Functional Weave
Code in Rust

invest.cagr@1.0.0

impl/rust.rs

3,068 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::math_big_integer::BigInt;  ← from math.big-integer ^1.0.0 · built alongside by fune
use super::math_fractional_power::{fixed_scale, fractional_power_fixed};  ← from math.fractional-power ^1.0.0 · built alongside by fune
use super::money_amount::{money_from_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune

/// The largest integer every language holds exactly (2^53 − 1).
const MAX_SAFE: i64 = 9007199254740991;

fn check_years(name: &str, value: i64) {
    if value < 1 || value > 100000 {
        panic!("{} must be a whole number from 1 to 100000, received {}", name, value);
    }
}

/// n / d rounded half away from zero; d > 0.
fn round_half_away(n: &BigInt, d: &BigInt) -> BigInt {
    let two = BigInt::from_i64(2);
    let magnitude = n.abs().mul(&two).add(d).div(&two.mul(d));
    if n.is_negative() {
        magnitude.neg()
    } else {
        magnitude
    }
}

/// Compound annual growth rate, (end / start)^(1 / years) − 1, in basis
/// points rounded half away from zero. Years are years_numerator /
/// years_denominator so no float ever enters the power.
///
/// # Panics
/// Panics on a start of zero or less, a negative end, mixed currencies, years
/// out of range, or a rate too large to express.
pub fn cagr(start_value: &Money, end_value: &Money, years_numerator: i64, years_denominator: i64) -> i64 {
    if start_value.currency != end_value.currency {
        panic!("currency mismatch: {} and {}", start_value.currency, end_value.currency);
    }
    check_years("yearsNumerator", years_numerator);
    check_years("yearsDenominator", years_denominator);
    if start_value.minor <= 0 {
        panic!("startValue must be greater than zero, received {}", start_value.minor);
    }
    if end_value.minor < 0 {
        panic!("endValue must not be negative, received {}", end_value.minor);
    }
    if end_value.minor == 0 {
        return -10000;
    }
    let scale = fixed_scale();
    let ratio = BigInt::from_i64(end_value.minor).mul(&scale).div(&BigInt::from_i64(start_value.minor));
    // Exponent 1 / years = years_denominator / years_numerator.
    let growth = fractional_power_fixed(&ratio, years_denominator, years_numerator);
    let bp = round_half_away(&growth.sub(&scale).mul(&BigInt::from_i64(10000)), &scale);
    if bp > BigInt::from_i64(MAX_SAFE) {
        panic!("the growth rate is too large to express in basis points");
    }
    bp.to_i64()
}

fn whole(v: &Value, what: &str) {
    if let Value::Float(f) = v {
        if f.fract() != 0.0 {
            panic!("{}, received {}", what, f);
        }
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    // Refuse what the typed signature cannot hold, with the wording TypeScript
    // and Python use, rather than let the conversion quietly truncate it.
    whole(args[0].get("minor"), "amounts must be whole minor units");
    whole(args[1].get("minor"), "amounts must be whole minor units");
    whole(&args[2], "yearsNumerator must be a whole number from 1 to 100000");
    whole(&args[3], "yearsDenominator must be a whole number from 1 to 100000");
    Value::Int(cagr(&money_from_value(&args[0]), &money_from_value(&args[1]), args[2].as_i64(), args[3].as_i64()))
}