Functional Weave
Code in Rust

hospitality.bill-split@1.0.0

impl/python.py

3,969 bytes · the Python 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.

from typing import List, Sequence

from .hospitality_bill_split_types import BillItem, DinerShare, SplitMethod
from .money_allocate import allocate  ← from money.allocate ^1.0.0 · built alongside by fune
from .money_amount import Money, money  ← from money.amount ^1.0.0 · built alongside by fune
from .money_split_even import split_even  ← from money.split-even ^1.0.0 · built alongside by fune
from .money_sum import sum_money  ← from money.sum ^1.0.0 · built alongside by fune


def _whole(value: object) -> bool:
    return isinstance(value, int) and not isinstance(value, bool)


def bill_split(
    items: Sequence[BillItem],
    service_charge: Money,
    diners: int,
    method: SplitMethod,
    shares: Sequence[int],
) -> List[DinerShare]:
    """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.
    """
    if not _whole(diners) or diners < 1:
        raise ValueError("diners must be at least 1, received %s" % (diners,))
    if method not in ("even", "by-item", "by-share"):
        raise ValueError('unknown split method "%s"' % (method,))
    if service_charge.minor < 0:
        raise ValueError("serviceCharge must not be negative, received %d" % (service_charge.minor,))
    for item in items:
        if item.amount.minor < 0:
            raise ValueError(
                'item amounts must not be negative, received %d for "%s"' % (item.amount.minor, item.description)
            )
    currency = service_charge.currency
    items_total = sum_money([item.amount for item in items], currency)
    if method == "by-share":
        if len(shares) != diners:
            raise ValueError(
                "shares must have one entry per diner: %d diners, %d shares" % (diners, len(shares))
            )
        for s in shares:
            if not _whole(s) or s < 0:
                raise ValueError("shares must not be negative, received %s" % (s,))
        if all(s == 0 for s in shares):
            raise ValueError("shares must not all be zero")
    elif len(shares) != 0:
        raise ValueError("shares are only used by the by-share method; pass []")

    if method == "by-item":
        item_shares = [0] * diners
        for item in items:
            who = list(range(diners)) if len(item.diners) == 0 else list(item.diners)
            for i, d in enumerate(who):
                if not _whole(d) or d < 0 or d >= diners:
                    raise ValueError(
                        "diner %s is not at the table: diners are numbered 0 to %d" % (d, diners - 1)
                    )
                if who.index(d) != i:
                    raise ValueError('item "%s" lists diner %d twice' % (item.description, d))
            parts = split_even(item.amount, len(who))
            for i, d in enumerate(who):
                item_shares[d] += parts[i].minor
        if items_total.minor == 0:
            if service_charge.minor != 0:
                raise ValueError("a service charge cannot be shared on a bill with no items")
            sc_shares = [0] * diners
        else:
            sc_shares = [m.minor for m in allocate(service_charge, item_shares)]
    else:
        weights = [1] * diners if method == "even" else list(shares)
        grand = money(items_total.minor + service_charge.minor, currency)
        totals = [m.minor for m in allocate(grand, weights)]
        sc_shares = [0] * diners if grand.minor == 0 else [m.minor for m in allocate(service_charge, totals)]
        item_shares = [t - sc_shares[i] for i, t in enumerate(totals)]

    return [
        DinerShare(
            diner=d,
            items=money(item_shares[d], currency),
            service_charge=money(sc_shares[d], currency),
            total=money(item_shares[d] + sc_shares[d], currency),
        )
        for d in range(diners)
    ]