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)
]