<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P013-bill-splitter/ | updated: 2026-10-03 -->

# P013 Bill splitter

Share expenses among up to eight people - each paid by one, shared equally or in shares, split to the cent by largest remainder - see who owes whom, and settle up in the fewest transfers possible, found exactly by trying every group - from a text menu.

EML-P project `projects/bill-splitter` in the EML language repo: 4 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/bill-splitter` runs it and `eml project verify projects/bill-splitter` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: money-in-cents (https://efficientnewlanguage.org/cases/191-money-in-cents/), apportionment-remainder (https://efficientnewlanguage.org/cases/231-apportionment-remainder/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
0
2
4
5
1

1
A name that is much too long
Ana
1
ana
Ben
2

2
Lunch
abc
0
12.345
-5
12.5
3
x
1
1 1
3
2,1
1
0 1
a b

3
4
5
1
Chen
1
Dev
1
Eve
1
Fay
1
Gus
1
Hal
1
6
```

Screen:

```text

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 0
Pick a number from 1 to 6.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 2
Add at least two people first.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 4
No people yet.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 5
Everyone is even.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 1
name> 
Cancelled.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 1
name> A name that is much too long
At most 20 characters.
name> Ana
Person 1: Ana.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 1
name> ana
There is already someone called Ana.
name> Ben
Person 2: Ben.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 2
what for> 
Cancelled.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 2
what for> Lunch
amount> abc
Type an amount like 12.50, or nothing to cancel.
amount> 0
An amount is more than 0.
amount> 12.345
Type an amount like 12.50, or nothing to cancel.
amount> -5
Type an amount like 12.50, or nothing to cancel.
amount> 12.5
People:  1) Ana  2) Ben
paid by> 3
Type a person's number from 1 to 2, or nothing to cancel.
paid by> x
Type a person's number from 1 to 2, or nothing to cancel.
paid by> 1
shared by (numbers, nothing = everyone)> 1 1
Type the numbers of the people who shared it, 1 to 2, each once - or nothing for everyone.
shared by (numbers, nothing = everyone)> 3
Type the numbers of the people who shared it, 1 to 2, each once - or nothing for everyone.
shared by (numbers, nothing = everyone)> 2,1
shares (nothing = equal)> 1
Type one share from 1 to 100 for each of the 2 people, or nothing for equal shares.
shares (nothing = equal)> 0 1
Type one share from 1 to 100 for each of the 2 people, or nothing for equal shares.
shares (nothing = equal)> a b
Type one share from 1 to 100 for each of the 2 people, or nothing for equal shares.
shares (nothing = equal)> 
Added:
  1) Lunch 12.50, paid by Ana
     Ben 6.25, Ana 6.25

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 3

-- expenses --
  1) Lunch 12.50, paid by Ana
     Ben 6.25, Ana 6.25

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 4

-- balances --
  Ana                  paid      12.50   share       6.25   is owed 6.25
  Ben                  paid       0.00   share       6.25   owes 6.25

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 5

-- settle up --
  Ben pays Ana 6.25
1 transfer settles everyone - the fewest possible.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 1
name> Chen
Person 3: Chen.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 1
name> Dev
Person 4: Dev.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 1
name> Eve
Person 5: Eve.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 1
name> Fay
Person 6: Fay.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 1
name> Gus
Person 7: Gus.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 1
name> Hal
Person 8: Hal.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 1
At most 8 people.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 6
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
1
Ana
1
Ben
1
Chen
1
Dev
2
Dinner
100
1


2
Taxi
10.01
2
1 2 3

2
Groceries
64.31
3

1 1 2 1
2
Museum
45
4
2 4

3
4
5
6
```

Screen:

```text

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 1
name> Ana
Person 1: Ana.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 1
name> Ben
Person 2: Ben.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 1
name> Chen
Person 3: Chen.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 1
name> Dev
Person 4: Dev.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 2
what for> Dinner
amount> 100
People:  1) Ana  2) Ben  3) Chen  4) Dev
paid by> 1
shared by (numbers, nothing = everyone)> 
shares (nothing = equal)> 
Added:
  1) Dinner 100.00, paid by Ana
     Ana 25.00, Ben 25.00, Chen 25.00, Dev 25.00

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 2
what for> Taxi
amount> 10.01
People:  1) Ana  2) Ben  3) Chen  4) Dev
paid by> 2
shared by (numbers, nothing = everyone)> 1 2 3
shares (nothing = equal)> 
Added:
  2) Taxi 10.01, paid by Ben
     Ana 3.34, Ben 3.34, Chen 3.33
     2 leftover cents to Ana and Ben

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 2
what for> Groceries
amount> 64.31
People:  1) Ana  2) Ben  3) Chen  4) Dev
paid by> 3
shared by (numbers, nothing = everyone)> 
shares (nothing = equal)> 1 1 2 1
Added:
  3) Groceries 64.31, paid by Chen
     Ana 12.86, Ben 12.86, Chen 25.73, Dev 12.86 (shares 1:1:2:1)
     1 leftover cent to Chen

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 2
what for> Museum
amount> 45
People:  1) Ana  2) Ben  3) Chen  4) Dev
paid by> 4
shared by (numbers, nothing = everyone)> 2 4
shares (nothing = equal)> 
Added:
  4) Museum 45.00, paid by Dev
     Ben 22.50, Dev 22.50

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 3

-- expenses --
  1) Dinner 100.00, paid by Ana
     Ana 25.00, Ben 25.00, Chen 25.00, Dev 25.00
  2) Taxi 10.01, paid by Ben
     Ana 3.34, Ben 3.34, Chen 3.33
     2 leftover cents to Ana and Ben
  3) Groceries 64.31, paid by Chen
     Ana 12.86, Ben 12.86, Chen 25.73, Dev 12.86 (shares 1:1:2:1)
     1 leftover cent to Chen
  4) Museum 45.00, paid by Dev
     Ben 22.50, Dev 22.50

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 4

-- balances --
  Ana                  paid     100.00   share      41.20   is owed 58.80
  Ben                  paid      10.01   share      63.70   owes 53.69
  Chen                 paid      64.31   share      54.06   is owed 10.25
  Dev                  paid      45.00   share      60.36   owes 15.36

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 5

-- settle up --
  Ben pays Ana 53.69
  Dev pays Chen 10.25
  Dev pays Ana 5.11
3 transfers settle everyone - the fewest possible.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 6
Bye.
```

### fewest-transfers - interpreter: byte-equal to the golden

Input:

```text
1
Ana
1
Ben
1
Chen
1
Dev
1
Eve
2
Concert tickets
60
2
2 3

2
Boat trip
60
1
1 4 5

2
Snacks for Dev
5
5
4
4
5
6
```

Screen:

```text

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 1
name> Ana
Person 1: Ana.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 1
name> Ben
Person 2: Ben.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 1
name> Chen
Person 3: Chen.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 1
name> Dev
Person 4: Dev.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 1
name> Eve
Person 5: Eve.

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 2
what for> Concert tickets
amount> 60
People:  1) Ana  2) Ben  3) Chen  4) Dev  5) Eve
paid by> 2
shared by (numbers, nothing = everyone)> 2 3
shares (nothing = equal)> 
Added:
  1) Concert tickets 60.00, paid by Ben
     Ben 30.00, Chen 30.00

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 2
what for> Boat trip
amount> 60
People:  1) Ana  2) Ben  3) Chen  4) Dev  5) Eve
paid by> 1
shared by (numbers, nothing = everyone)> 1 4 5
shares (nothing = equal)> 
Added:
  2) Boat trip 60.00, paid by Ana
     Ana 20.00, Dev 20.00, Eve 20.00

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 2
what for> Snacks for Dev
amount> 5
People:  1) Ana  2) Ben  3) Chen  4) Dev  5) Eve
paid by> 5
shared by (numbers, nothing = everyone)> 4
Added:
  3) Snacks for Dev 5.00, paid by Eve
     Dev 5.00

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 4

-- balances --
  Ana                  paid      60.00   share      20.00   is owed 40.00
  Ben                  paid      60.00   share      30.00   is owed 30.00
  Chen                 paid       0.00   share      30.00   owes 30.00
  Dev                  paid       0.00   share      25.00   owes 25.00
  Eve                  paid       5.00   share      20.00   owes 15.00

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 5

-- settle up --
  Dev pays Ana 25.00
  Eve pays Ana 15.00
  Chen pays Ben 30.00
3 transfers settle everyone - the fewest possible (paying the largest debt to the largest credit first would take 4).

== Bill splitter ==
1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit
choice> 6
Bye.
```

## Modules

### main.eml

```eml
# P013 bill splitter: people share expenses - each paid by one of them and
# shared by some or all, equally or in shares, to the cent. The balances say
# who is owed and who owes, and settling up finds the fewest transfers that
# make everyone even.
import split
import settle
import text

8 => most_people

def plural(n, word):
    if n == 1:
        return "1 " + word
    return str(n) + " " + word + "s"

def contains(xs, v):
    for x in xs:
        if x == v:
            return True
    return False

def join_names(names):
    # "Ana", "Ana and Ben", "Ana, Ben and Chen".
    if len(names) == 1:
        return names[0]
    "" => out
    for i in [0:len(names) - 2]:
        if i > 0:
            out + ", " => out
        out + names[i] => out
    return out + " and " + names[len(names) - 1]

def show_expense(n, e, people):
    ("  " + str(n) + ") " + e[0] + " " + text.money(e[1]) + ", paid by " + people[e[2]]) ^0
    "" => line
    False => weighted
    for k in [0:len(e[3]) - 1]:
        if k > 0:
            line + ", " => line
        line + people[e[3][k]] + " " + text.money(e[5][k]) => line
        if e[4][k] != 1:
            True => weighted
    if weighted:
        "" => ratio
        for k in [0:len(e[4]) - 1]:
            if k > 0:
                ratio + ":" => ratio
            ratio + str(e[4][k]) => ratio
        line + " (shares " + ratio + ")" => line
    ("     " + line) ^0
    [] => lucky
    for k in [0:len(e[3]) - 1]:
        if e[6][k]:
            lucky + [people[e[3][k]]] => lucky
    if len(lucky) > 0:
        ("     " + plural(len(lucky), "leftover cent") + " to " + join_names(lucky)) ^0

def ask_text(prompt, longest):
    # Text of at most longest characters, asked again until it fits; "" cancels.
    while True:
        text.trim(input(prompt)) => answer
        if len(answer) <= longest:
            return answer
        ("At most " + str(longest) + " characters.") ^0

def ask_name(people):
    # A new person's name, asked again until a good one is typed; "" cancels.
    while True:
        ask_text("name> ", 20) => name
        if name == "":
            return ""
        0 => clash
        for p in people:
            if text.lower(p) == text.lower(name):
                1 => clash
                ("There is already someone called " + p + ".") ^0
        if clash == 0:
            return name

def ask_amount():
    # An amount in cents, asked again until a good one is typed; -1 cancels.
    while True:
        text.trim(input("amount> ")) => answer
        if answer == "":
            return 0 - 1
        text.parse_cents(answer) => c
        if c == 0 - 1:
            "Type an amount like 12.50, or nothing to cancel." ^0
        elif c == 0:
            "An amount is more than 0." ^0
        elif c > 10000000:
            "An amount is at most 100,000.00." ^0
        else:
            return c

def ask_payer(people):
    # The index of the person who paid; -1 cancels.
    while True:
        text.trim(input("paid by> ")) => answer
        if answer == "":
            return 0 - 1
        text.number(answer) => n
        if n >= 1 and n <= len(people):
            return n - 1
        ("Type a person's number from 1 to " + str(len(people)) + ", or nothing to cancel.") ^0

def ask_sharers(people):
    # The indexes of the people who shared it, in the order typed; nothing
    # typed means everyone.
    while True:
        text.words(input("shared by (numbers, nothing = everyone)> ")) => ws
        [] => out
        if len(ws) == 0:
            for i in [0:len(people) - 1]:
                out + [i] => out
            return out
        True => good
        for w in ws:
            text.number(w) => n
            if n < 1 or n > len(people) or contains(out, n - 1):
                False => good
            else:
                out + [n - 1] => out
        if good:
            return out
        ("Type the numbers of the people who shared it, 1 to " + str(len(people)) + ", each once - or nothing for everyone.") ^0

def ask_shares(count):
    # One share for each of count people; nothing typed means equal shares.
    [] => equal
    for i in [1:count]:
        equal + [1] => equal
    if count == 1:
        return equal
    while True:
        text.words(input("shares (nothing = equal)> ")) => ws
        if len(ws) == 0:
            return equal
        [] => out
        for w in ws:
            text.number(w) => n
            if n >= 1 and n <= 100:
                out + [n] => out
        if len(ws) == count and len(out) == count:
            return out
        ("Type one share from 1 to 100 for each of the " + str(count) + " people, or nothing for equal shares.") ^0

[] => people
[] => expenses
True => running
while running:
    "" ^0
    "== Bill splitter ==" ^0
    "1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit" ^0
    text.trim(input("choice> ")) => choice
    if choice == "1":
        if len(people) == most_people:
            ("At most " + str(most_people) + " people.") ^0
        else:
            ask_name(people) => name
            if name == "":
                "Cancelled." ^0
            else:
                people + [name] => people
                ("Person " + str(len(people)) + ": " + name + ".") ^0
    elif choice == "2":
        if len(people) < 2:
            "Add at least two people first." ^0
        else:
            ask_text("what for> ", 30) => what
            0 - 1 => amount
            0 - 1 => payer
            if what != "":
                ask_amount() => amount
            if amount != 0 - 1:
                "" => listing
                for i in [0:len(people) - 1]:
                    listing + "  " + str(i + 1) + ") " + people[i] => listing
                ("People:" + listing) ^0
                ask_payer(people) => payer
            if payer == 0 - 1:
                "Cancelled." ^0
            else:
                ask_sharers(people) => sharers
                ask_shares(len(sharers)) => weights
                split.split(amount, weights) => r
                expenses + [[what, amount, payer, sharers, weights, r[0], r[1]]] => expenses
                "Added:" ^0
                show_expense(len(expenses), expenses[len(expenses) - 1], people)
    elif choice == "3":
        if len(expenses) == 0:
            "No expenses yet." ^0
        else:
            "" ^0
            "-- expenses --" ^0
            for i in [0:len(expenses) - 1]:
                show_expense(i + 1, expenses[i], people)
    elif choice == "4":
        if len(people) == 0:
            "No people yet." ^0
        else:
            split.balances(len(people), expenses) => bals
            "" ^0
            "-- balances --" ^0
            for i in [0:len(people) - 1]:
                bals[i][0] - bals[i][1] => diff
                "even" => status
                if diff > 0:
                    "is owed " + text.money(diff) => status
                elif diff < 0:
                    "owes " + text.money(0 - diff) => status
                ("  " + ("%-21s" % people[i]) + "paid " + ("%10s" % text.money(bals[i][0])) + "   share " + ("%10s" % text.money(bals[i][1])) + "   " + status) ^0
    elif choice == "5":
        split.balances(len(people), expenses) => bals
        [] => owed
        for i in [0:len(people) - 1]:
            if bals[i][0] != bals[i][1]:
                owed + [[i, bals[i][0] - bals[i][1]]] => owed
        if len(owed) == 0:
            "Everyone is even." ^0
        else:
            settle.fewest(owed) => plan
            "" ^0
            "-- settle up --" ^0
            for t in plan:
                ("  " + people[t[0]] + " pays " + people[t[1]] + " " + text.money(t[2])) ^0
            if len(plan) == 1:
                "1 transfer settles everyone - the fewest possible" => line
            else:
                str(len(plan)) + " transfers settle everyone - the fewest possible" => line
            len(settle.pay_largest_first(owed)) => naive
            if naive > len(plan):
                line + " (paying the largest debt to the largest credit first would take " + str(naive) + ")" => line
            (line + ".") ^0
    elif choice == "6":
        False => running
    else:
        "Pick a number from 1 to 6." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import split
import settle
import text
most_people = 8

def plural(n, word):
    if n == 1:
        return "1 " + word
    return str(n) + " " + word + "s"

def contains(xs, v):
    for x in xs:
        if x == v:
            return True
    return False

def join_names(names):
    if len(names) == 1:
        return names[0]
    out = ""
    for i in range(0, len(names) - 2+1):
        if i > 0:
            out = out + ", "
        out = out + names[i]
    return out + " and " + names[len(names) - 1]

def show_expense(n, e, people):
    print("  " + str(n) + ") " + e[0] + " " + text.money(e[1]) + ", paid by " + people[e[2]])
    line = ""
    weighted = False
    for k in range(0, len(e[3])):
        if k > 0:
            line = line + ", "
        line = line + people[e[3][k]] + " " + text.money(e[5][k])
        if e[4][k] != 1:
            weighted = True
    if weighted:
        ratio = ""
        for k in range(0, len(e[4])):
            if k > 0:
                ratio = ratio + ":"
            ratio = ratio + str(e[4][k])
        line = line + " (shares " + ratio + ")"
    print("     " + line)
    lucky = []
    for k in range(0, len(e[3])):
        if e[6][k]:
            lucky = lucky + [people[e[3][k]]]
    if len(lucky) > 0:
        print("     " + plural(len(lucky), "leftover cent") + " to " + join_names(lucky))

def ask_text(prompt, longest):
    while True:
        answer = text.trim(input(prompt))
        if len(answer) <= longest:
            return answer
        print("At most " + str(longest) + " characters.")

def ask_name(people):
    while True:
        name = ask_text("name> ", 20)
        if name == "":
            return ""
        clash = 0
        for p in people:
            if text.lower(p) == text.lower(name):
                clash = 1
                print("There is already someone called " + p + ".")
        if clash == 0:
            return name

def ask_amount():
    while True:
        answer = text.trim(input("amount> "))
        if answer == "":
            return 0 - 1
        c = text.parse_cents(answer)
        if c == 0 - 1:
            print("Type an amount like 12.50, or nothing to cancel.")
        elif c == 0:
            print("An amount is more than 0.")
        elif c > 10000000:
            print("An amount is at most 100,000.00.")
        else:
            return c

def ask_payer(people):
    while True:
        answer = text.trim(input("paid by> "))
        if answer == "":
            return 0 - 1
        n = text.number(answer)
        if n >= 1 and n <= len(people):
            return n - 1
        print("Type a person's number from 1 to " + str(len(people)) + ", or nothing to cancel.")

def ask_sharers(people):
    while True:
        ws = text.words(input("shared by (numbers, nothing = everyone)> "))
        out = []
        if len(ws) == 0:
            for i in range(0, len(people)):
                out = out + [i]
            return out
        good = True
        for w in ws:
            n = text.number(w)
            if n < 1 or n > len(people) or contains(out, n - 1):
                good = False
            else:
                out = out + [n - 1]
        if good:
            return out
        print("Type the numbers of the people who shared it, 1 to " + str(len(people)) + ", each once - or nothing for everyone.")

def ask_shares(count):
    equal = []
    for i in range(1, count+1):
        equal = equal + [1]
    if count == 1:
        return equal
    while True:
        ws = text.words(input("shares (nothing = equal)> "))
        if len(ws) == 0:
            return equal
        out = []
        for w in ws:
            n = text.number(w)
            if n >= 1 and n <= 100:
                out = out + [n]
        if len(ws) == count and len(out) == count:
            return out
        print("Type one share from 1 to 100 for each of the " + str(count) + " people, or nothing for equal shares.")

people = []
expenses = []
running = True
while running:
    print("")
    print("== Bill splitter ==")
    print("1) add person  2) add expense  3) expenses  4) balances  5) settle up  6) quit")
    choice = text.trim(input("choice> "))
    if choice == "1":
        if len(people) == most_people:
            print("At most " + str(most_people) + " people.")
        else:
            name = ask_name(people)
            if name == "":
                print("Cancelled.")
            else:
                people = people + [name]
                print("Person " + str(len(people)) + ": " + name + ".")
    elif choice == "2":
        if len(people) < 2:
            print("Add at least two people first.")
        else:
            what = ask_text("what for> ", 30)
            amount = 0 - 1
            payer = 0 - 1
            if what != "":
                amount = ask_amount()
            if amount != 0 - 1:
                listing = ""
                for i in range(0, len(people)):
                    listing = listing + "  " + str(i + 1) + ") " + people[i]
                print("People:" + listing)
                payer = ask_payer(people)
            if payer == 0 - 1:
                print("Cancelled.")
            else:
                sharers = ask_sharers(people)
                weights = ask_shares(len(sharers))
                r = split.split(amount, weights)
                expenses = expenses + [[what, amount, payer, sharers, weights, r[0], r[1]]]
                print("Added:")
                show_expense(len(expenses), expenses[len(expenses) - 1], people)
    elif choice == "3":
        if len(expenses) == 0:
            print("No expenses yet.")
        else:
            print("")
            print("-- expenses --")
            for i in range(0, len(expenses)):
                show_expense(i + 1, expenses[i], people)
    elif choice == "4":
        if len(people) == 0:
            print("No people yet.")
        else:
            bals = split.balances(len(people), expenses)
            print("")
            print("-- balances --")
            for i in range(0, len(people)):
                diff = bals[i][0] - bals[i][1]
                status = "even"
                if diff > 0:
                    status = "is owed " + text.money(diff)
                elif diff < 0:
                    status = "owes " + text.money(0 - diff)
                print("  " + "%-21s" % people[i] + "paid " + "%10s" % text.money(bals[i][0]) + "   share " + "%10s" % text.money(bals[i][1]) + "   " + status)
    elif choice == "5":
        bals = split.balances(len(people), expenses)
        owed = []
        for i in range(0, len(people)):
            if bals[i][0] != bals[i][1]:
                owed = owed + [[i, bals[i][0] - bals[i][1]]]
        if len(owed) == 0:
            print("Everyone is even.")
        else:
            plan = settle.fewest(owed)
            print("")
            print("-- settle up --")
            for t in plan:
                print("  " + people[t[0]] + " pays " + people[t[1]] + " " + text.money(t[2]))
            if len(plan) == 1:
                line = "1 transfer settles everyone - the fewest possible"
            else:
                line = str(len(plan)) + " transfers settle everyone - the fewest possible"
            naive = len(settle.pay_largest_first(owed))
            if naive > len(plan):
                line = line + " (paying the largest debt to the largest credit first would take " + str(naive) + ")"
            print(line + ".")
    elif choice == "6":
        running = False
    else:
        print("Pick a number from 1 to 6.")
print("Bye.")
```

### split.eml

```eml
# P013 bill splitter - sharing one expense out to the cent, and what everyone
# has paid and owes. Money is whole cents.

def split(amount, weights):
    # The parts of amount in proportion to weights, adding up to amount
    # exactly, as [parts, extra]. Every part is first rounded down; the cents
    # left over then go one each to the parts that lost the most in the
    # rounding (largest remainder), and on a tie to the one listed first.
    # extra[i] says whether part i got one of those cents.
    0 => total
    for w in weights:
        total + w => total
    [] => parts
    [] => rems
    [] => extra
    0 => given
    for w in weights:
        amount * w => x
        int((x - x % total) / total) => part
        parts + [part] => parts
        rems + [x % total] => rems
        extra + [False] => extra
        given + part => given
    amount - given => left
    while left > 0:
        0 - 1 => best
        for i in [0:len(weights) - 1]:
            if not extra[i] and (best == 0 - 1 or rems[i] > rems[best]):
                i => best
        parts[best] + 1 => parts[best]
        True => extra[best]
        left - 1 => left
    return [parts, extra]

def balances(count, expenses):
    # [paid, share] for each of count people. An expense is
    # [what, amount, payer, sharers, weights, parts, extra].
    [] => out
    for i in [0:count - 1]:
        out + [[0, 0]] => out
    for e in expenses:
        out[e[2]][0] + e[1] => out[e[2]][0]
        for k in [0:len(e[3]) - 1]:
            out[e[3][k]][1] + e[5][k] => out[e[3][k]][1]
    return out
```

Python projection of split.eml:

```python
def split(amount, weights):
    total = 0
    for w in weights:
        total = total + w
    parts = []
    rems = []
    extra = []
    given = 0
    for w in weights:
        x = amount * w
        part = int((x - x % total) / total)
        parts = parts + [part]
        rems = rems + [x % total]
        extra = extra + [False]
        given = given + part
    left = amount - given
    while left > 0:
        best = 0 - 1
        for i in range(0, len(weights)):
            if not extra[i] and (best == 0 - 1 or rems[i] > rems[best]):
                best = i
        parts[best] = parts[best] + 1
        extra[best] = True
        left = left - 1
    return [parts, extra]

def balances(count, expenses):
    out = []
    for i in range(0, count):
        out = out + [[0, 0]]
    for e in expenses:
        out[e[2]][0] = out[e[2]][0] + e[1]
        for k in range(0, len(e[3])):
            out[e[3][k]][1] = out[e[3][k]][1] + e[5][k]
    return out
```

### settle.eml

```eml
# P013 bill splitter - settling up in as few transfers as possible. People
# whose balances add up to zero among themselves can settle inside their own
# group, and a group of g people needs g - 1 transfers. So the fewest
# transfers is the number of people with a balance minus the largest number
# of groups they can be split into, each adding up to zero. With at most
# eight people that is found exactly by trying every subset: a subset is a
# whole number whose bits say who is in it, and pow2[i] is person i's bit.

def has(mask, i, pow2):
    return int(mask / pow2[i]) % 2 == 1

def groups(bal):
    # bal is a list of [person, cents], none of them zero, adding up to 0.
    # The largest set of zero-sum groups it splits into, as lists of entries.
    len(bal) => k
    [1] => pow2
    for i in [1:k]:
        pow2 + [pow2[i - 1] * 2] => pow2
    pow2[k] => size
    # sums[mask]: the balances in mask added up; most[mask]: the most
    # zero-sum groups that mask can be split into, counting mask itself.
    [0] => sums
    [0] => most
    for mask in [1:size - 1]:
        0 => low
        while not has(mask, low, pow2):
            low + 1 => low
        sums + [sums[mask - pow2[low]] + bal[low][1]] => sums
        0 => b
        for j in [0:k - 1]:
            if has(mask, j, pow2) and most[mask - pow2[j]] > b:
                most[mask - pow2[j]] => b
        if sums[mask] == 0:
            b + 1 => b
        most + [b] => most
    # Walk back from everyone, taking one person out at a time along a best
    # path; each time what is left adds up to zero, the people taken out
    # since the last time form one group.
    [] => found
    [] => current
    size - 1 => mask
    while mask > 0:
        most[mask] => target
        if sums[mask] == 0:
            target - 1 => target
        0 => j
        while not (has(mask, j, pow2) and most[mask - pow2[j]] == target):
            j + 1 => j
        current + [bal[j]] => current
        mask - pow2[j] => mask
        if sums[mask] == 0:
            found + [current] => found
            [] => current
    return found

def pay_largest_first(group):
    # Transfers [from, to, cents] that settle a zero-sum group: the largest
    # debt pays the largest credit, again and again, until all are even.
    [] => left
    for g in group:
        left + [[g[0], g[1]]] => left
    [] => out
    while True:
        0 - 1 => c
        0 - 1 => d
        for i in [0:len(left) - 1]:
            if left[i][1] > 0 and (c == 0 - 1 or left[i][1] > left[c][1]):
                i => c
            if left[i][1] < 0 and (d == 0 - 1 or left[i][1] < left[d][1]):
                i => d
        if c == 0 - 1:
            return out
        left[c][1] => amount
        if 0 - left[d][1] < amount:
            0 - left[d][1] => amount
        out + [[left[d][0], left[c][0], amount]] => out
        left[c][1] - amount => left[c][1]
        left[d][1] + amount => left[d][1]

def fewest(bal):
    # The fewest transfers that settle bal: every zero-sum group settles
    # inside itself.
    [] => out
    for g in groups(bal):
        out + pay_largest_first(g) => out
    return out
```

Python projection of settle.eml:

```python
def has(mask, i, pow2):
    return int(mask / pow2[i]) % 2 == 1

def groups(bal):
    k = len(bal)
    pow2 = [1]
    for i in range(1, k+1):
        pow2 = pow2 + [pow2[i - 1] * 2]
    size = pow2[k]
    sums = [0]
    most = [0]
    for mask in range(1, size):
        low = 0
        while not has(mask, low, pow2):
            low = low + 1
        sums = sums + [sums[mask - pow2[low]] + bal[low][1]]
        b = 0
        for j in range(0, k):
            if has(mask, j, pow2) and most[mask - pow2[j]] > b:
                b = most[mask - pow2[j]]
        if sums[mask] == 0:
            b = b + 1
        most = most + [b]
    found = []
    current = []
    mask = size - 1
    while mask > 0:
        target = most[mask]
        if sums[mask] == 0:
            target = target - 1
        j = 0
        while not (has(mask, j, pow2) and most[mask - pow2[j]] == target):
            j = j + 1
        current = current + [bal[j]]
        mask = mask - pow2[j]
        if sums[mask] == 0:
            found = found + [current]
            current = []
    return found

def pay_largest_first(group):
    left = []
    for g in group:
        left = left + [[g[0], g[1]]]
    out = []
    while True:
        c = 0 - 1
        d = 0 - 1
        for i in range(0, len(left)):
            if left[i][1] > 0 and (c == 0 - 1 or left[i][1] > left[c][1]):
                c = i
            if left[i][1] < 0 and (d == 0 - 1 or left[i][1] < left[d][1]):
                d = i
        if c == 0 - 1:
            return out
        amount = left[c][1]
        if 0 - left[d][1] < amount:
            amount = 0 - left[d][1]
        out = out + [[left[d][0], left[c][0], amount]]
        left[c][1] = left[c][1] - amount
        left[d][1] = left[d][1] + amount

def fewest(bal):
    out = []
    for g in groups(bal):
        out = out + pay_largest_first(g)
    return out
```

### text.eml

```eml
# P013 bill splitter - reading what is typed and writing money. Money is
# whole cents from the moment it is typed: "12.50" is read digit by digit into
# 1250 and never passes through a float. The interpreter that checks every
# session does not run string methods yet, so the text handling is written
# out here.

def trim(s):
    # s without the spaces at either end.
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def lower(s):
    # s with A-Z turned into a-z; everything else as it was.
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ" => upper_letters
    "abcdefghijklmnopqrstuvwxyz" => lower_letters
    "" => out
    for c in s:
        c => d
        for i in [0:25]:
            if upper_letters[i] == c:
                lower_letters[i] => d
        out + d => out
    return out

def number(s):
    # The value of s if it is digits only (at least one), otherwise -1.
    if s == "":
        return 0 - 1
    0 => n
    for c in s:
        if not (c in "0123456789"):
            return 0 - 1
        n * 10 + int(c) => n
    return n

def words(s):
    # The words of s, split at spaces and commas.
    [] => out
    "" => word
    for c in s:
        if c == " " or c == ",":
            if word != "":
                out + [word] => out
            "" => word
        else:
            word + c => word
    if word != "":
        out + [word] => out
    return out

def parse_cents(s):
    # The amount in s in whole cents, or -1 if s is not written like 4, 4.5
    # or 4.50: digits, then a point and one or two digits if any.
    0 => whole
    0 => whole_digits
    0 => frac
    0 => frac_digits
    False => point
    for c in s:
        if c == ".":
            if point:
                return 0 - 1
            True => point
        elif c in "0123456789":
            if point:
                frac * 10 + int(c) => frac
                frac_digits + 1 => frac_digits
            else:
                whole * 10 + int(c) => whole
                whole_digits + 1 => whole_digits
        else:
            return 0 - 1
    if whole_digits == 0:
        return 0 - 1
    if point and (frac_digits == 0 or frac_digits > 2):
        return 0 - 1
    if frac_digits == 1:
        frac * 10 => frac
    return whole * 100 + frac

def money(c):
    # Whole cents (not negative) as 1,234.50, built from the digits.
    str(c) => s
    while len(s) < 3:
        "0" + s => s
    s[0:len(s) - 2] => whole
    s[len(s) - 2:len(s)] => cents
    "" => grouped
    len(whole) => i
    while i > 3:
        "," + whole[i - 3:i] + grouped => grouped
        i - 3 => i
    return whole[0:i] + grouped + "." + cents
```

Python projection of text.eml:

```python
def trim(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def lower(s):
    upper_letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    lower_letters = "abcdefghijklmnopqrstuvwxyz"
    out = ""
    for c in s:
        d = c
        for i in range(0, 26):
            if upper_letters[i] == c:
                d = lower_letters[i]
        out = out + d
    return out

def number(s):
    if s == "":
        return 0 - 1
    n = 0
    for c in s:
        if not c in "0123456789":
            return 0 - 1
        n = n * 10 + int(c)
    return n

def words(s):
    out = []
    word = ""
    for c in s:
        if c == " " or c == ",":
            if word != "":
                out = out + [word]
            word = ""
        else:
            word = word + c
    if word != "":
        out = out + [word]
    return out

def parse_cents(s):
    whole = 0
    whole_digits = 0
    frac = 0
    frac_digits = 0
    point = False
    for c in s:
        if c == ".":
            if point:
                return 0 - 1
            point = True
        elif c in "0123456789":
            if point:
                frac = frac * 10 + int(c)
                frac_digits = frac_digits + 1
            else:
                whole = whole * 10 + int(c)
                whole_digits = whole_digits + 1
        else:
            return 0 - 1
    if whole_digits == 0:
        return 0 - 1
    if point and (frac_digits == 0 or frac_digits > 2):
        return 0 - 1
    if frac_digits == 1:
        frac = frac * 10
    return whole * 100 + frac

def money(c):
    s = str(c)
    while len(s) < 3:
        s = "0" + s
    whole = s[0:len(s) - 2]
    cents = s[len(s) - 2:len(s)]
    grouped = ""
    i = len(whole)
    while i > 3:
        grouped = "," + whole[i - 3:i] + grouped
        i = i - 3
    return whole[0:i] + grouped + "." + cents
```

## README

# P013 - Bill splitter

People share expenses: each expense is paid by one of them and shared by some
or all of them, equally or in shares. The program splits every expense to the
cent, shows what each person has paid and owes, and settles up in the fewest
transfers possible. Up to eight people; everything lives while the program
runs.

- `main.eml` - the menu and its questions, with their checks, and what the
  screen shows
- `split.eml` - splitting one expense to the cent, and each person's paid and
  share totals
- `settle.eml` - the fewest transfers that make everyone even
- `text.eml` - trimming, lower case, whole numbers, words, and money

Splitting to the cent: every part is first rounded down, and the cents left
over go one each to the parts that lost the most in the rounding (largest
remainder); on a tie - always the case for equal shares - to the person listed
first. So the parts always add up to the expense, and the screen says who got
the leftover cents: 10.01 split three ways is 3.34, 3.34 and 3.33, and 64.31
in shares 1:1:2:1 gives its one leftover cent to the double share, whose
remainder is the largest.

Settling up: people whose balances add up to zero among themselves can settle
inside their own group, and a group of g people needs g - 1 transfers. So the
fewest transfers is the number of people with a balance minus the largest
number of zero-sum groups they can be split into. With at most eight people
that is found exactly by trying every subset (at most 256), and each group
then settles by the largest debt paying the largest credit. Paying the largest
debt to the largest credit across everyone at once is not always enough: when
it needs more transfers, the screen says how many.

What is checked: a name has at most 20 characters and is not already taken in
any case; an expense needs at least two people, a description of at most 30
characters, and an amount written like 12.50 that is more than 0 and at most
100,000.00; the payer is a person's number; the sharers are people's numbers,
each once (nothing means everyone); the shares are one whole number from 1 to
100 per sharer (nothing means equal). Anything else asks again; an empty
description, amount or payer cancels.

Sessions: `sessions/basic.in` has four friends share a dinner equally, a taxi
three ways (two leftover cents), groceries in shares 1:1:2:1 (one leftover
cent, to the largest remainder) and a museum visit two ways, then lists the
expenses, the balances and three transfers; `sessions/fewest-transfers.in`
has five people whose balances fall into two groups that each add up to zero,
so three transfers settle everyone where paying largest debt to largest
credit would take four; `sessions/bad-input.in` asks for an expense, the
balances and settling up too early, types names that are empty, too long or
taken, amounts that are words, 0, negative or with three decimals, payers and
sharers that do not exist or repeat, shares of the wrong number or out of
range, and goes past eight people.

Built on the verified corpus cases `money-in-cents` (amounts kept in whole
cents so the parts equal the whole) and `apportionment-remainder` (largest
remainder: parts that add back up to the whole, with the tie broken in a
stated order).
