use super::funejson::Value; 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 = 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(), )) }