use std::cmp::Ordering; use super::funejson::Value; use super::math_big_integer::BigInt; use super::money_amount::{assert_same_currency, money, money_from_value, money_to_value, Money}; 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 = 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 = 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 = 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 = 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(), )) }