use super::funejson::Value; use super::dates_add_days::add_days; use super::dates_days_between::days_between; use super::money_amount::{assert_same_currency, money, money_from_value, money_to_value, Money}; use super::money_sum::sum_money; /// Demurrage or detention for one container: calendar days from start_date /// to end_date inclusive, the first free_days free, every later day at the /// rate of the tier whose day numbers contain it. /// /// # Panics /// Panics on bad dates, negative free days, and a tariff with no tiers, /// overlapping or backwards tiers, negative rates, mixed currencies or a gap /// that a chargeable day falls in. pub fn demurrage(start_date: &str, end_date: &str, free_days: i64, tiers: &[DemurrageTier]) -> DemurrageCharge { if free_days < 0 { panic!("freeDays must not be negative, received {}", free_days); } let total_days = days_between(start_date, end_date) + 1; if total_days < 1 { panic!("endDate must not be before startDate: {} to {}", start_date, end_date); } if tiers.is_empty() { panic!("tiers must not be empty"); } for tier in tiers { if tier.from_day < 1 { panic!("tier fromDay must be 1 or more, received {}", tier.from_day); } if let Some(to) = tier.to_day { if to < tier.from_day { panic!("tier toDay must not be before fromDay, received {} to {}", tier.from_day, to); } } if tier.daily_rate.minor < 0 { panic!("dailyRate must not be negative, received {}", tier.daily_rate.minor); } assert_same_currency(&tiers[0].daily_rate, &tier.daily_rate); } let mut sorted: Vec<&DemurrageTier> = tiers.iter().collect(); sorted.sort_by_key(|t| t.from_day); for i in 1..sorted.len() { let overlaps = match sorted[i - 1].to_day { None => true, Some(to) => to >= sorted[i].from_day, }; if overlaps { panic!("tiers overlap at day {}", sorted[i].from_day); } } let mut lines: Vec = Vec::new(); let mut day = free_days + 1; for tier in &sorted { if day > total_days { break; } if let Some(to) = tier.to_day { if to < day { continue; // wholly inside free time } } // A gap is a mistake in the tariff; charging nothing for it would hide it. if tier.from_day > day { panic!("no rate for day {}: no tier covers it", day); } let last = match tier.to_day { None => total_days, Some(to) => to.min(total_days), }; let days = last - day + 1; lines.push(DemurrageLine { from_day: day, to_day: last, days, daily_rate: tier.daily_rate.clone(), amount: money(tier.daily_rate.minor * days, &tier.daily_rate.currency), }); day = last + 1; } if day <= total_days { panic!("no rate for day {}: no tier covers it", day); } let amounts: Vec = lines.iter().map(|l| l.amount.clone()).collect(); DemurrageCharge { total_days, free_days_used: free_days.min(total_days), chargeable_days: (total_days - free_days).max(0), free_time_ends: if free_days > 0 { Some(add_days(start_date, free_days - 1)) } else { None }, total: sum_money(&amounts, &tiers[0].daily_rate.currency), lines, } } pub fn demurrage_tier_from_value(v: &Value) -> DemurrageTier { DemurrageTier { from_day: v.get("fromDay").as_i64(), to_day: if v.get("toDay").is_null() { None } else { Some(v.get("toDay").as_i64()) }, daily_rate: money_from_value(v.get("dailyRate")), } } pub fn demurrage_line_to_value(l: &DemurrageLine) -> Value { Value::obj(vec![ ("fromDay", Value::Int(l.from_day)), ("toDay", Value::Int(l.to_day)), ("days", Value::Int(l.days)), ("dailyRate", money_to_value(&l.daily_rate)), ("amount", money_to_value(&l.amount)), ]) } pub fn demurrage_charge_to_value(c: &DemurrageCharge) -> Value { Value::obj(vec![ ("totalDays", Value::Int(c.total_days)), ("freeDaysUsed", Value::Int(c.free_days_used)), ("chargeableDays", Value::Int(c.chargeable_days)), ( "freeTimeEnds", match &c.free_time_ends { Some(d) => Value::str(d), None => Value::Null, }, ), ("lines", Value::Arr(c.lines.iter().map(demurrage_line_to_value).collect())), ("total", money_to_value(&c.total)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let tiers: Vec = args[3].as_arr().iter().map(demurrage_tier_from_value).collect(); demurrage_charge_to_value(&demurrage(args[0].as_str(), args[1].as_str(), args[2].as_i64(), &tiers)) }