use super::funejson::Value; use super::money_allocate::allocate; use super::money_amount::{money, money_from_value, money_to_value, Money}; use super::money_split_even::split_even; use super::money_sum::sum_money; /// Split a bill between diners so that the shares add up to the bill exactly. /// /// even and by-share divide the whole bill (items plus service) by weight, so /// even totals never differ by more than a penny. by-item gives each diner what /// they ordered, with shared items split evenly between the people who shared /// them. In every method the service charge is then shared in proportion to /// what each diner pays for, so nobody pays service on someone else's steak. /// /// # Panics /// Panics on a bad diner count, method, share list or diner number, negative /// amounts, mixed currencies, or a service charge on a bill with no items. pub fn bill_split( items: &[BillItem], service_charge: &Money, diners: i64, method: &str, shares: &[i64], ) -> Vec { if diners < 1 { panic!("diners must be at least 1, received {}", diners); } if method != "even" && method != "by-item" && method != "by-share" { panic!("unknown split method \"{}\"", method); } if service_charge.minor < 0 { panic!("serviceCharge must not be negative, received {}", service_charge.minor); } for item in items { if item.amount.minor < 0 { panic!( "item amounts must not be negative, received {} for \"{}\"", item.amount.minor, item.description ); } } let currency = service_charge.currency.clone(); let amounts: Vec = items.iter().map(|i| i.amount.clone()).collect(); let items_total = sum_money(&amounts, ¤cy); let n = diners as usize; if method == "by-share" { if shares.len() != n { panic!( "shares must have one entry per diner: {} diners, {} shares", diners, shares.len() ); } for s in shares { if *s < 0 { panic!("shares must not be negative, received {}", s); } } if shares.iter().all(|s| *s == 0) { panic!("shares must not all be zero"); } } else if !shares.is_empty() { panic!("shares are only used by the by-share method; pass []"); } let item_shares: Vec; let sc_shares: Vec; if method == "by-item" { let mut acc = vec![0i64; n]; for item in items { let who: Vec = if item.diners.is_empty() { (0..diners).collect() } else { item.diners.clone() }; for (i, d) in who.iter().enumerate() { if *d < 0 || *d >= diners { panic!("diner {} is not at the table: diners are numbered 0 to {}", d, diners - 1); } if who.iter().position(|x| x == d) != Some(i) { panic!("item \"{}\" lists diner {} twice", item.description, d); } } let parts = split_even(&item.amount, who.len() as i64); for (i, d) in who.iter().enumerate() { acc[*d as usize] += parts[i].minor; } } if items_total.minor == 0 { if service_charge.minor != 0 { panic!("a service charge cannot be shared on a bill with no items"); } sc_shares = vec![0; n]; } else { sc_shares = allocate(service_charge, &acc).iter().map(|m| m.minor).collect(); } item_shares = acc; } else { let weights: Vec = if method == "even" { vec![1; n] } else { shares.to_vec() }; let grand = money(items_total.minor + service_charge.minor, ¤cy); let totals: Vec = allocate(&grand, &weights).iter().map(|m| m.minor).collect(); sc_shares = if grand.minor == 0 { vec![0; n] } else { allocate(service_charge, &totals).iter().map(|m| m.minor).collect() }; item_shares = totals.iter().zip(sc_shares.iter()).map(|(t, s)| t - s).collect(); } (0..n) .map(|d| DinerShare { diner: d as i64, items: money(item_shares[d], ¤cy), service_charge: money(sc_shares[d], ¤cy), total: money(item_shares[d] + sc_shares[d], ¤cy), }) .collect() } pub fn bill_item_from_value(v: &Value) -> BillItem { BillItem { description: v.get("description").as_str().to_string(), amount: money_from_value(v.get("amount")), diners: v.get("diners").as_arr().iter().map(|d| d.as_i64()).collect(), } } pub fn diner_share_to_value(s: &DinerShare) -> Value { Value::obj(vec![ ("diner", Value::Int(s.diner)), ("items", money_to_value(&s.items)), ("serviceCharge", money_to_value(&s.service_charge)), ("total", money_to_value(&s.total)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let items: Vec = args[0].as_arr().iter().map(bill_item_from_value).collect(); let shares: Vec = args[4].as_arr().iter().map(|s| s.as_i64()).collect(); Value::Arr( bill_split(&items, &money_from_value(&args[1]), args[2].as_i64(), args[3].as_str(), &shares) .iter() .map(diner_share_to_value) .collect(), ) }