Functional Weave
Code in Rust

lending.ltv@1.0.0

impl/rust.rs

2,770 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_round_div::round_div;  ← from math.round-div ^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

/// Above this, loan × 10000 is no longer exact as a JavaScript number.
const MAX_LOAN: i64 = 900_719_925_474;

/// Loan-to-value, and the band from the lender's table. The band is chosen on
/// the exact ratio, loan × 10000 <= ceiling × value, never on a rounded
/// percentage, so 75.004% is not "75%". The reported basis points are rounded
/// up, which agrees: a whole ceiling is met exactly when the rounded-up ratio
/// is within it.
///
/// # Panics
/// Panics on mismatched currencies, a value of zero or less, a negative or
/// oversized loan, or an empty or unordered band table.
pub fn loan_to_value(loan: &Money, property_value: &Money, bands: &[LtvBand]) -> LtvResult {
    if loan.currency != property_value.currency {
        panic!("currency mismatch: {} and {}", loan.currency, property_value.currency);
    }
    if property_value.minor <= 0 {
        panic!("propertyValue must be greater than zero, received {}", property_value.minor);
    }
    if loan.minor < 0 {
        panic!("loan must not be negative, received {}", loan.minor);
    }
    if loan.minor > MAX_LOAN {
        panic!("loan must be at most {} minor units", MAX_LOAN);
    }
    if bands.is_empty() {
        panic!("bands must not be empty");
    }
    for i in 0..bands.len() {
        let ceiling = bands[i].max_basis_points;
        if ceiling < 1 {
            panic!("band ceilings must be whole basis points of 1 or more, received {}", ceiling);
        }
        if i > 0 && ceiling <= bands[i - 1].max_basis_points {
            panic!("band ceilings must be in strictly ascending order");
        }
    }
    let basis_points = round_div(loan.minor * 10000, property_value.minor, "up");
    let band = bands
        .iter()
        .find(|band| basis_points <= band.max_basis_points)
        .map(|band| band.label.clone());
    LtvResult { basis_points, band }
}

pub fn ltv_band_from_value(v: &Value) -> LtvBand {
    LtvBand {
        max_basis_points: v.get("maxBasisPoints").as_i64(),
        label: v.get("label").as_str().to_string(),
    }
}

pub fn ltv_result_to_value(result: &LtvResult) -> Value {
    Value::obj(vec![
        ("basisPoints", Value::Int(result.basis_points)),
        (
            "band",
            match &result.band {
                Some(label) => Value::str(label),
                None => Value::Null,
            },
        ),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let bands: Vec<LtvBand> = args[2].as_arr().iter().map(ltv_band_from_value).collect();
    ltv_result_to_value(&loan_to_value(
        &money_from_value(&args[0]),
        &money_from_value(&args[1]),
        &bands,
    ))
}