use super::funejson::Value; use super::math_round_div::round_div; use super::money_amount::{money, money_from_value, money_to_value, Money}; const MAX_RENT: i64 = 1_000_000_000; const MAX_INDEX: i64 = 1_000_000; fn check_basis_points(name: &str, value: Option) { if let Some(v) = value { if v <= -10000 || v > 100000 { panic!("{} must be an integer from -9999 to 100000, received {}", name, v); } } } /// The rent after an index-linked review, held between an optional collar and /// cap, and not below the passing rent when upward-only. Every comparison is /// on exact fractions and the rent is rounded half-up once. /// /// # Panics /// Panics on a rent or index out of range, a collar or cap out of range, or a /// collar above the cap. pub fn rent_review( passing_rent: &Money, base_index: i64, review_index: i64, collar_basis_points: Option, cap_basis_points: Option, upward_only: bool, ) -> RentReviewResult { if passing_rent.minor < 0 || passing_rent.minor > MAX_RENT { panic!("passingRent must be between 0 and {} minor units, received {}", MAX_RENT, passing_rent.minor); } for (name, value) in [("baseIndex", base_index), ("reviewIndex", review_index)] { if value <= 0 || value > MAX_INDEX { panic!("{} must be an integer from 1 to {}, received {}", name, MAX_INDEX, value); } } check_basis_points("collarBasisPoints", collar_basis_points); check_basis_points("capBasisPoints", cap_basis_points); if let (Some(collar), Some(cap)) = (collar_basis_points, cap_basis_points) { if collar > cap { panic!("collar {} is above cap {}", collar, cap); } } let p = passing_rent.minor; // The multiplier as a fraction num/den: the index ratio, or 1 + collar/cap. let (mut num, mut den, mut rule) = (review_index, base_index, "index"); match (cap_basis_points, collar_basis_points) { (Some(cap), _) if review_index * 10000 > base_index * (10000 + cap) => { num = 10000 + cap; den = 10000; rule = "cap"; } (_, Some(collar)) if review_index * 10000 < base_index * (10000 + collar) => { num = 10000 + collar; den = 10000; rule = "collar"; } _ => {} } if upward_only && num < den { num = 1; den = 1; rule = "upward-only"; } RentReviewResult { new_rent: money(round_div(p * num, den, "half-up"), &passing_rent.currency), index_rent: money(round_div(p * review_index, base_index, "half-up"), &passing_rent.currency), index_change_basis_points: round_div((review_index - base_index) * 10000, base_index, "half-up"), applied_by: rule.to_string(), } } pub fn rent_review_result_to_value(result: &RentReviewResult) -> Value { Value::obj(vec![ ("newRent", money_to_value(&result.new_rent)), ("indexRent", money_to_value(&result.index_rent)), ("indexChangeBasisPoints", Value::Int(result.index_change_basis_points)), ("appliedBy", Value::str(&result.applied_by)), ]) } fn optional_int(value: &Value, name: &str) -> Option { match value { Value::Null => None, Value::Int(v) => Some(*v), _ => panic!("{} must be an integer from -9999 to 100000, received {:?}", name, value), } } fn int_arg(value: &Value, name: &str) -> i64 { match value { Value::Int(v) => *v, _ => panic!("{} must be an integer from 1 to {}, received {:?}", name, MAX_INDEX, value), } } pub fn fune_vector(args: &[Value]) -> Value { rent_review_result_to_value(&rent_review( &money_from_value(&args[0]), int_arg(&args[1], "baseIndex"), int_arg(&args[2], "reviewIndex"), optional_int(&args[3], "collarBasisPoints"), optional_int(&args[4], "capBasisPoints"), args[5].as_bool(), )) }