Functional Weave
Code in Python

insurance.no-claims-discount@1.0.1

impl/rust.rs

4,525 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

fn whole_at_least_zero(value: i64, name: &str) {
    if value < 0 {
        panic!("{} must be a whole number of 0 or more, received {}", name, value);
    }
}

fn level_for(scale: &[NcdLevel], years: i64) -> &NcdLevel {
    let mut found = &scale[0];
    for level in scale {
        if level.years <= years {
            found = level;
        }
    }
    found
}

/// The no-claims discount held at renewal.
///
/// A claim-free year earns a year. A year with claims steps back to what the
/// scale says for the level held and the number of claims, unless protected
/// NCD covers them, in which case the years stay where they were (a year with
/// a claim earns nothing). Years keep counting past the top of the scale; the
/// discount is the highest level not above them.
///
/// # Panics
/// Panics on a malformed scale, negative counts, or a protection window that
/// leaves out this year's claims.
pub fn next_no_claims_discount(
    scale: &[NcdLevel],
    current_years: i64,
    claims: i64,
    protection: Option<&NcdProtection>,
) -> NcdOutcome {
    if scale.is_empty() {
        panic!("the NCD scale must not be empty");
    }
    if scale[0].years != 0 {
        panic!("the NCD scale must start at 0 years");
    }
    for (i, level) in scale.iter().enumerate() {
        whole_at_least_zero(level.years, "years");
        if i > 0 && level.years <= scale[i - 1].years {
            panic!("the NCD scale must be in ascending order of years");
        }
        if !(0..=10000).contains(&level.discount_basis_points) {
            panic!("discountBasisPoints must be from 0 to 10000, received {}", level.discount_basis_points);
        }
        let mut previous = level.years;
        for back in &level.step_back {
            whole_at_least_zero(*back, "stepBack");
            if *back > previous {
                panic!(
                    "stepBack from {} years must not rise above the level or the step before it",
                    level.years
                );
            }
            previous = *back;
        }
    }
    whole_at_least_zero(current_years, "currentYears");
    whole_at_least_zero(claims, "claims");

    let mut protection_applied = false;
    let years = if claims == 0 {
        current_years + 1
    } else {
        let mut covered = false;
        if let Some(p) = protection {
            whole_at_least_zero(p.claim_limit, "claimLimit");
            whole_at_least_zero(p.claims_in_window, "claimsInWindow");
            if p.claims_in_window < claims {
                panic!("claimsInWindow must include this year's claims");
            }
            covered = p.claims_in_window <= p.claim_limit;
        }
        if covered {
            protection_applied = true;
            current_years
        } else {
            let steps = &level_for(scale, current_years).step_back;
            if steps.is_empty() {
                0
            } else {
                steps[(claims as usize).min(steps.len()) - 1]
            }
        }
    };
    NcdOutcome {
        years,
        discount_basis_points: level_for(scale, years).discount_basis_points,
        protection_applied,
    }
}

fn int_arg(v: &Value, name: &str) -> i64 {
    if let Value::Float(f) = v {
        panic!("{} must be a whole number of 0 or more, received {}", name, f);
    }
    v.as_i64()
}

pub fn ncd_level_from_value(v: &Value) -> NcdLevel {
    NcdLevel {
        years: int_arg(v.get("years"), "years"),
        discount_basis_points: v.get("discountBasisPoints").as_i64(),
        step_back: v.get("stepBack").as_arr().iter().map(|s| int_arg(s, "stepBack")).collect(),
    }
}

pub fn ncd_outcome_to_value(o: &NcdOutcome) -> Value {
    Value::obj(vec![
        ("years", Value::Int(o.years)),
        ("discountBasisPoints", Value::Int(o.discount_basis_points)),
        ("protectionApplied", Value::Bool(o.protection_applied)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let scale: Vec<NcdLevel> = args[0].as_arr().iter().map(ncd_level_from_value).collect();
    let protection = if args[3].is_null() {
        None
    } else {
        Some(NcdProtection {
            claim_limit: int_arg(args[3].get("claimLimit"), "claimLimit"),
            claims_in_window: int_arg(args[3].get("claimsInWindow"), "claimsInWindow"),
        })
    };
    ncd_outcome_to_value(&next_no_claims_discount(
        &scale,
        int_arg(&args[1], "currentYears"),
        int_arg(&args[2], "claims"),
        protection.as_ref(),
    ))
}