use super::funejson::Value; use super::dates_add_days::{civil_from_days, days_in_month, epoch_day_from_iso}; use super::math_round_div::round_div; use super::money_amount::{money, money_from_value, money_to_value, Money}; const MAX_MINOR: i64 = 1_000_000_000; // lcm(28, 29, 30, 31): every month's daily rate is a whole number of these parts. const MONTH_PARTS: i64 = 377_580; /// Rent for the days from_date to to_date inclusive, from a monthly rent, as /// one exact fraction rounded half-up once. The daily rate is never rounded /// on its own, which is the usual source of a pound's error on a part-month /// invoice. /// /// # Panics /// Panics on an out-of-range rent, a malformed date, a period that runs /// backwards or is longer than 366 days, or an unknown method. pub fn prorate_rent(monthly_rent: &Money, from_date: &str, to_date: &str, method: &str) -> Money { if monthly_rent.minor.abs() > MAX_MINOR { panic!( "monthlyRent must be at most {} minor units either way, received {}", MAX_MINOR, monthly_rent.minor ); } let start = epoch_day_from_iso(from_date); let end = epoch_day_from_iso(to_date); if end < start { panic!("toDate {} is before fromDate {}", to_date, from_date); } let days = end - start + 1; if days > 366 { panic!("the period must be at most 366 days, received {}", days); } match method { "annual-daily" => money(round_div(monthly_rent.minor * 12 * days, 365, "half-up"), &monthly_rent.currency), "calendar-month" => { // Each day is worth MONTH_PARTS / (days in its month) parts of a month. let mut parts = 0i64; let mut day = start; while day <= end { let date = civil_from_days(day); let length = days_in_month(date.year, date.month); let segment_end = end.min(day + (length - date.day)); parts += (segment_end - day + 1) * (MONTH_PARTS / length); day = segment_end + 1; } money(round_div(monthly_rent.minor * parts, MONTH_PARTS, "half-up"), &monthly_rent.currency) } other => panic!("unknown method \"{}\": use annual-daily or calendar-month", other), } } pub fn fune_vector(args: &[Value]) -> Value { money_to_value(&prorate_rent( &money_from_value(&args[0]), args[1].as_str(), args[2].as_str(), args[3].as_str(), )) }