use super::funejson::Value; use super::dates_add_days::parse_iso_date; use super::money_allocate::allocate; use super::money_amount::{money, money_from_value, money_to_value, Money}; /// Split a fee into dated instalments that add up to it exactly. /// /// The split is money.allocate's: floors, then the leftover pennies to the /// largest remainders, ties to the earlier instalment. /// /// # Panics /// Panics on a negative total, mismatched or empty lists, negative or all-zero /// shares, or due dates that are malformed or not strictly ascending. pub fn fee_instalments(total: &Money, shares: &[i64], due_dates: &[String]) -> Vec { if total.minor < 0 { panic!("total must not be negative, received {}", total.minor); } if shares.is_empty() { panic!("shares must name at least one instalment"); } if shares.len() != due_dates.len() { panic!( "shares and dueDates must be the same length, received {} and {}", shares.len(), due_dates.len() ); } for &s in shares { if s < 0 { panic!("each share must be a whole number of 0 or more, received {}", s); } } if shares.iter().sum::() == 0 { panic!("shares must not all be zero"); } for i in 0..due_dates.len() { parse_iso_date(&due_dates[i]); if i > 0 && due_dates[i] <= due_dates[i - 1] { panic!( "dueDates must be in strictly ascending order: {} follows {}", due_dates[i], due_dates[i - 1] ); } } let amounts = allocate(total, shares); let mut paid = 0; amounts .into_iter() .enumerate() .map(|(i, amount)| { paid += amount.minor; FeeInstalment { number: i as i64 + 1, due_date: due_dates[i].clone(), amount, cumulative: money(paid, &total.currency), remaining: money(total.minor - paid, &total.currency), } }) .collect() } pub fn fee_instalment_to_value(f: &FeeInstalment) -> Value { Value::obj(vec![ ("number", Value::Int(f.number)), ("dueDate", Value::str(&f.due_date)), ("amount", money_to_value(&f.amount)), ("cumulative", money_to_value(&f.cumulative)), ("remaining", money_to_value(&f.remaining)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let shares: Vec = args[1] .as_arr() .iter() .map(|v| match v { Value::Float(f) if f.fract() != 0.0 => panic!("each share must be a whole number of 0 or more, received {}", f), other => other.as_i64(), }) .collect(); let dates: Vec = args[2].as_arr().iter().map(|v| v.as_str().to_string()).collect(); Value::Arr( fee_instalments(&money_from_value(&args[0]), &shares, &dates) .iter() .map(fee_instalment_to_value) .collect(), ) }