Functional Weave
Code in Python

hospitality.bill-split@1.0.0

impl/rust.rs

5,450 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::money_allocate::allocate;  ← from money.allocate ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune
use super::money_split_even::split_even;  ← from money.split-even ^1.0.0 · built alongside by fune
use super::money_sum::sum_money;  ← from money.sum ^1.0.0 · built alongside by fune

/// 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<DinerShare> {
    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<Money> = items.iter().map(|i| i.amount.clone()).collect();
    let items_total = sum_money(&amounts, &currency);
    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<i64>;
    let sc_shares: Vec<i64>;
    if method == "by-item" {
        let mut acc = vec![0i64; n];
        for item in items {
            let who: Vec<i64> = 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<i64> = if method == "even" { vec![1; n] } else { shares.to_vec() };
        let grand = money(items_total.minor + service_charge.minor, &currency);
        let totals: Vec<i64> = 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], &currency),
            service_charge: money(sc_shares[d], &currency),
            total: money(item_shares[d] + sc_shares[d], &currency),
        })
        .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<BillItem> = args[0].as_arr().iter().map(bill_item_from_value).collect();
    let shares: Vec<i64> = 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(),
    )
}