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(),
))
}