Functional Weave
Code in Rust

insurance.rating-factors@1.0.1

impl/rust.rs

7,253 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 std::cmp::Ordering;

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::money_amount::{assert_same_currency, money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune

fn round_quotient(numerator: &BigInt, denominator: &BigInt, mode: &str) -> i64 {
    // Both are non-negative here: a base of 0 or more times multipliers of 0 or more.
    let (q, r) = numerator.div_rem(denominator);
    let twice = r.mul(&BigInt::from_i64(2));
    let one = BigInt::from_i64(1);
    let up = match mode {
        "down" => false,
        "up" => !r.is_zero(),
        "half-up" => twice.cmp(denominator) != Ordering::Less,
        "half-even" => match twice.cmp(denominator) {
            Ordering::Greater => true,
            Ordering::Equal => !q.rem(&BigInt::from_i64(2)).is_zero(),
            Ordering::Less => false,
        },
        other => panic!("unknown rounding mode \"{}\"", other),
    };
    if up {
        q.add(&one).to_i64()
    } else {
        q.to_i64()
    }
}

/// Rate a premium: the base times each table's multiplier for the risk, then
/// each loading or discount, with one rounding at the end, then held within
/// the minimum and maximum premiums.
///
/// Rounding after every factor drifts: 1.01 at 1.005 three times is 1.03
/// exactly but 1.04 rounded at each step. The product is exact integer
/// arithmetic of any size (math.big-integer), so there is no limit on the
/// number of factors.
///
/// # Panics
/// Panics on a negative base, a missing, unknown or duplicated factor value,
/// a bad multiplier, caps out of order or in another currency.
pub fn rate_premium(
    base_premium: &Money,
    tables: &[FactorTable],
    risk: &[(String, String)],
    adjustments: &[PremiumAdjustment],
    minimum_premium: Option<&Money>,
    maximum_premium: Option<&Money>,
    mode: &str,
) -> RatedPremium {
    if base_premium.minor < 0 {
        panic!("basePremium must not be negative, received {}", base_premium.minor);
    }
    if let Some(m) = minimum_premium {
        assert_same_currency(base_premium, m);
    }
    if let Some(m) = maximum_premium {
        assert_same_currency(base_premium, m);
    }
    if let (Some(lo), Some(hi)) = (minimum_premium, maximum_premium) {
        if lo.minor > hi.minor {
            panic!("minimumPremium must not be more than maximumPremium");
        }
    }

    let mut factors: Vec<AppliedFactor> = Vec::new();
    let mut names: Vec<&str> = Vec::new();
    for table in tables {
        if names.contains(&table.name.as_str()) {
            panic!("rating factor \"{}\" appears twice", table.name);
        }
        names.push(&table.name);
        let key = match risk.iter().find(|(k, _)| *k == table.name) {
            Some((_, v)) => v,
            None => panic!("no value for rating factor \"{}\"", table.name),
        };
        let mut found: Option<i64> = None;
        for row in &table.rows {
            if row.key == *key {
                if found.is_some() {
                    panic!("rating factor \"{}\" has two rows for \"{}\"", table.name, key);
                }
                if row.basis_points < 0 {
                    panic!("factor basisPoints must be a whole number of 0 or more, received {}", row.basis_points);
                }
                found = Some(row.basis_points);
            }
        }
        match found {
            Some(bp) => factors.push(AppliedFactor { name: table.name.clone(), basis_points: bp }),
            None => panic!("rating factor \"{}\" has no row for \"{}\"", table.name, key),
        }
    }
    for (name, _) in risk {
        if !names.contains(&name.as_str()) {
            panic!("risk has a value for \"{}\", which no table rates", name);
        }
    }
    for adjustment in adjustments {
        if adjustment.basis_points < -10000 {
            panic!(
                "adjustment basisPoints must be a whole number of -10000 or more, received {}",
                adjustment.basis_points
            );
        }
        factors.push(AppliedFactor {
            name: adjustment.name.clone(),
            basis_points: 10000 + adjustment.basis_points,
        });
    }

    let mut numerator = BigInt::from_i64(base_premium.minor);
    let mut denominator = BigInt::from_i64(1);
    let scale = BigInt::from_i64(10000);
    for f in &factors {
        numerator = numerator.mul(&BigInt::from_i64(f.basis_points));
        denominator = denominator.mul(&scale);
    }
    let c = &base_premium.currency;
    let calculated = money(round_quotient(&numerator, &denominator, mode), c);
    let mut premium = calculated.clone();
    let mut cap = "none";
    if let Some(lo) = minimum_premium.filter(|lo| premium.minor < lo.minor) {
        premium = lo.clone();
        cap = "minimum";
    } else if let Some(hi) = maximum_premium.filter(|hi| premium.minor > hi.minor) {
        premium = hi.clone();
        cap = "maximum";
    }
    RatedPremium {
        factors,
        calculated,
        premium,
        cap: cap.to_string(),
    }
}

fn int_field(v: &Value, key: &str, message: &str) -> i64 {
    if let Value::Float(f) = v.get(key) {
        panic!("{}, received {}", message, f);
    }
    v.get(key).as_i64()
}

pub fn rated_premium_to_value(r: &RatedPremium) -> Value {
    Value::obj(vec![
        (
            "factors",
            Value::Arr(
                r.factors
                    .iter()
                    .map(|f| Value::obj(vec![("name", Value::str(&f.name)), ("basisPoints", Value::Int(f.basis_points))]))
                    .collect(),
            ),
        ),
        ("calculated", money_to_value(&r.calculated)),
        ("premium", money_to_value(&r.premium)),
        ("cap", Value::str(&r.cap)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let tables: Vec<FactorTable> = args[1]
        .as_arr()
        .iter()
        .map(|t| FactorTable {
            name: t.get("name").as_str().to_string(),
            rows: t
                .get("rows")
                .as_arr()
                .iter()
                .map(|r| FactorRow {
                    key: r.get("key").as_str().to_string(),
                    basis_points: int_field(r, "basisPoints", "factor basisPoints must be a whole number of 0 or more"),
                })
                .collect(),
        })
        .collect();
    let risk: Vec<(String, String)> = match &args[2] {
        Value::Obj(pairs) => pairs.iter().map(|(k, v)| (k.clone(), v.as_str().to_string())).collect(),
        _ => Vec::new(),
    };
    let adjustments: Vec<PremiumAdjustment> = args[3]
        .as_arr()
        .iter()
        .map(|a| PremiumAdjustment {
            name: a.get("name").as_str().to_string(),
            basis_points: int_field(a, "basisPoints", "adjustment basisPoints must be a whole number of -10000 or more"),
        })
        .collect();
    let minimum = if args[4].is_null() { None } else { Some(money_from_value(&args[4])) };
    let maximum = if args[5].is_null() { None } else { Some(money_from_value(&args[5])) };
    rated_premium_to_value(&rate_premium(
        &money_from_value(&args[0]),
        &tables,
        &risk,
        &adjustments,
        minimum.as_ref(),
        maximum.as_ref(),
        args[6].as_str(),
    ))
}