<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P011-shopping-cart/ | updated: 2026-10-03 -->

# P011 Shopping cart

Fill a cart from a small catalog, change quantities, enter a discount code and check out with a receipt: the subtotal, the one discount taken (the largest that applies - discounts never stack), the tax and the total - from a text menu.

EML-P project `projects/shopping-cart` in the EML language repo: 5 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/shopping-cart` runs it and `eml project verify projects/shopping-cart` 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: shopping-cart-total (https://efficientnewlanguage.org/cases/038-shopping-cart-total/), receipt-formatter (https://efficientnewlanguage.org/cases/147-receipt-formatter/), the-two-discounts-were-added (https://efficientnewlanguage.org/cases/805-the-two-discounts-were-added/).

## Sessions

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

Input:

```text
8
4
3
6
2
0
x
7

2
2
0
100
-1
two
4
2
2
96
2
2

5

5
-
5
half
5
 take5
5
TAKE5
4
3
4
3
2
abc

2
5
5
4
5
-
3
2
0
3
5
0
4
7
```

Screen:

```text

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 8
Pick a number from 1 to 7.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 4
The cart is empty.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 3
The cart is empty.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 6
The cart is empty.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 2
item number> 0
Type an item number from 1 to 6, or nothing to cancel.
item number> x
Type an item number from 1 to 6, or nothing to cancel.
item number> 7
Type an item number from 1 to 6, or nothing to cancel.
item number> 
Cancelled.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 2
item number> 2
quantity> 0
Type a quantity from 1 to 99, or nothing to cancel.
quantity> 100
Type a quantity from 1 to 99, or nothing to cancel.
quantity> -1
Type a quantity from 1 to 99, or nothing to cancel.
quantity> two
Type a quantity from 1 to 99, or nothing to cancel.
quantity> 4
Added 4 x Pens, pack of 3.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 2
item number> 2
quantity> 96
At most 99 of one item; the cart has 4.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 2
item number> 2
quantity> 
Cancelled.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 5
code (- removes it)> 
No code entered.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 5
code (- removes it)> -
No code to remove.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 5
code (- removes it)> half
There is no code HALF.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 5
code (- removes it)>  take5
Code TAKE5 entered: 5.00 off orders of 30.00 or more.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 5
code (- removes it)> TAKE5
Code TAKE5 is already entered.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 4

Your cart
Qty  Item                    Price     Total
  4  Pens, pack of 3          2.25      9.00
--------------------------------------------
Subtotal                                9.00
Tax 8.25%                               0.74
Total                                   9.74
Code TAKE5 is not used: it needs a subtotal of 30.00.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 3
item number> 4
Green tea, 50 bags is not in the cart.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 3
item number> 2
new quantity (0 removes)> abc
Type a quantity from 0 to 99, or nothing to cancel.
new quantity (0 removes)> 
Cancelled.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 2
item number> 5
quantity> 5
Added 5 x Desk lamp.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 4

Your cart
Qty  Item                    Price     Total
  4  Pens, pack of 3          2.25      9.00
  5  Desk lamp               24.00    120.00
--------------------------------------------
Subtotal                              129.00
Discount (5% off from 50.00)           -6.45
After discount                        122.55
Tax 8.25%                              10.11
Total                                 132.66
Not added: TAKE5, 5.00 off (5.00) - discounts do not stack.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 5
code (- removes it)> -
Code TAKE5 removed.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 3
item number> 2
new quantity (0 removes)> 0
Removed Pens, pack of 3.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 3
item number> 5
new quantity (0 removes)> 0
Removed Desk lamp.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 4
The cart is empty.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 7
Bye.
```

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

Input:

```text
1
2
1
3
2
3
2
4
5
take5
4
2
5
1
5
SAVE10
4
3
1
2
6
4
7
```

Screen:

```text

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 1

-- catalog --
  1) Notebook                3.50
  2) Pens, pack of 3         2.25
  3) Coffee beans 1kg       12.99
  4) Green tea, 50 bags      6.40
  5) Desk lamp              24.00
  6) USB cable               7.95

Offers - only the largest one that applies is taken:
  5% off orders of 50.00 or more
  Notebooks: 3 for the price of 2
  code SAVE10: 10% off
  code TAKE5: 5.00 off orders of 30.00 or more

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 2
item number> 1
quantity> 3
Added 3 x Notebook.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 2
item number> 3
quantity> 2
Added 2 x Coffee beans 1kg.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 4

Your cart
Qty  Item                    Price     Total
  3  Notebook                 3.50     10.50
  2  Coffee beans 1kg        12.99     25.98
--------------------------------------------
Subtotal                               36.48
Discount (notebooks 3 for 2)           -3.50
After discount                         32.98
Tax 8.25%                               2.72
Total                                  35.70

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 5
code (- removes it)> take5
Code TAKE5 entered: 5.00 off orders of 30.00 or more.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 4

Your cart
Qty  Item                    Price     Total
  3  Notebook                 3.50     10.50
  2  Coffee beans 1kg        12.99     25.98
--------------------------------------------
Subtotal                               36.48
Discount (TAKE5, 5.00 off)             -5.00
After discount                         31.48
Tax 8.25%                               2.60
Total                                  34.08
Not added: notebooks 3 for 2 (3.50) - discounts do not stack.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 2
item number> 5
quantity> 1
Added 1 x Desk lamp.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 5
code (- removes it)> SAVE10
Code SAVE10 replaces TAKE5 - one code per order.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 4

Your cart
Qty  Item                    Price     Total
  3  Notebook                 3.50     10.50
  2  Coffee beans 1kg        12.99     25.98
  1  Desk lamp               24.00     24.00
--------------------------------------------
Subtotal                               60.48
Discount (SAVE10, 10% off)             -6.05
After discount                         54.43
Tax 8.25%                               4.49
Total                                  58.92
Not added: 5% off from 50.00 (3.02) - discounts do not stack.
Not added: notebooks 3 for 2 (3.50) - discounts do not stack.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 3
item number> 1
new quantity (0 removes)> 2
Notebook: now 2.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 6

Order 1
Qty  Item                    Price     Total
  2  Notebook                 3.50      7.00
  2  Coffee beans 1kg        12.99     25.98
  1  Desk lamp               24.00     24.00
--------------------------------------------
Subtotal                               56.98
Discount (SAVE10, 10% off)             -5.70
After discount                         51.28
Tax 8.25%                               4.23
Total                                  55.51
Not added: 5% off from 50.00 (2.85) - discounts do not stack.
Paid - thank you. Order 1 is done.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 4
The cart is empty.

== Shop ==
1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit
choice> 7
Bye.
```

## Modules

### main.eml

```eml
# P011 shopping cart: fill a cart from the catalog, change quantities, enter
# a discount code, and check out with a receipt. Only one discount is ever
# taken - the largest that applies; discounts are never added together.
import catalog
import cart
import receipt
import text

def ask_number(prompt, low, high, message):
    # A whole number from low to high, asked again until one is typed; -1 if
    # the answer is empty, which cancels.
    while True:
        text.trim(input(prompt)) => answer
        if answer == "":
            return 0 - 1
        text.number(answer) => n
        if n >= low and n <= high:
            return n
        message ^0

("Type an item number from 1 to " + str(len(catalog.products)) + ", or nothing to cancel.") => item_message
[] => lines
None => code
1 => order
True => running
while running:
    "" ^0
    "== Shop ==" ^0
    "1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit" ^0
    text.trim(input("choice> ")) => choice
    if choice == "1":
        receipt.show_catalog()
    elif choice == "2":
        ask_number("item number> ", 1, len(catalog.products), item_message) => n
        if n == 0 - 1:
            "Cancelled." ^0
        else:
            cart.product(n) => p
            ask_number("quantity> ", 1, 99, "Type a quantity from 1 to 99, or nothing to cancel.") => q
            cart.line_for(lines, n) => line
            if q == 0 - 1:
                "Cancelled." ^0
            elif line == None:
                lines + [[p, q]] => lines
                ("Added " + str(q) + " x " + p[1] + ".") ^0
            elif line[1] + q > 99:
                ("At most 99 of one item; the cart has " + str(line[1]) + ".") ^0
            else:
                line[1] + q => line[1]
                ("Added " + str(q) + " x " + p[1] + " (now " + str(line[1]) + ").") ^0
    elif choice == "3":
        if len(lines) == 0:
            "The cart is empty." ^0
        else:
            ask_number("item number> ", 1, len(catalog.products), item_message) => n
            if n == 0 - 1:
                "Cancelled." ^0
            elif cart.line_for(lines, n) == None:
                (cart.product(n)[1] + " is not in the cart.") ^0
            else:
                cart.line_for(lines, n) => line
                ask_number("new quantity (0 removes)> ", 0, 99, "Type a quantity from 0 to 99, or nothing to cancel.") => q
                if q == 0 - 1:
                    "Cancelled." ^0
                elif q == 0:
                    cart.without(lines, n) => lines
                    ("Removed " + line[0][1] + ".") ^0
                else:
                    q => line[1]
                    (line[0][1] + ": now " + str(q) + ".") ^0
    elif choice == "4":
        if len(lines) == 0:
            "The cart is empty." ^0
        else:
            "" ^0
            receipt.show("Your cart", lines, code)
    elif choice == "5":
        text.upper(text.trim(input("code (- removes it)> "))) => typed
        if typed == "":
            "No code entered." ^0
        elif typed == "-":
            if code == None:
                "No code to remove." ^0
            else:
                ("Code " + code[0] + " removed.") ^0
                None => code
        elif catalog.find_code(typed) == None:
            ("There is no code " + typed + ".") ^0
        elif code != None and code[0] == typed:
            ("Code " + typed + " is already entered.") ^0
        elif code != None:
            ("Code " + typed + " replaces " + code[0] + " - one code per order.") ^0
            catalog.find_code(typed) => code
        else:
            catalog.find_code(typed) => code
            ("Code " + typed + " entered: " + code[2] + ".") ^0
    elif choice == "6":
        if len(lines) == 0:
            "The cart is empty." ^0
        else:
            "" ^0
            receipt.show("Order " + str(order), lines, code)
            ("Paid - thank you. Order " + str(order) + " is done.") ^0
            [] => lines
            None => code
            order + 1 => order
    elif choice == "7":
        False => running
    else:
        "Pick a number from 1 to 7." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import catalog
import cart
import receipt
import text

def ask_number(prompt, low, high, message):
    while True:
        answer = text.trim(input(prompt))
        if answer == "":
            return 0 - 1
        n = text.number(answer)
        if n >= low and n <= high:
            return n
        print(message)

item_message = "Type an item number from 1 to " + str(len(catalog.products)) + ", or nothing to cancel."
lines = []
code = None
order = 1
running = True
while running:
    print("")
    print("== Shop ==")
    print("1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit")
    choice = text.trim(input("choice> "))
    if choice == "1":
        receipt.show_catalog()
    elif choice == "2":
        n = ask_number("item number> ", 1, len(catalog.products), item_message)
        if n == 0 - 1:
            print("Cancelled.")
        else:
            p = cart.product(n)
            q = ask_number("quantity> ", 1, 99, "Type a quantity from 1 to 99, or nothing to cancel.")
            line = cart.line_for(lines, n)
            if q == 0 - 1:
                print("Cancelled.")
            elif line == None:
                lines = lines + [[p, q]]
                print("Added " + str(q) + " x " + p[1] + ".")
            elif line[1] + q > 99:
                print("At most 99 of one item; the cart has " + str(line[1]) + ".")
            else:
                line[1] = line[1] + q
                print("Added " + str(q) + " x " + p[1] + " (now " + str(line[1]) + ").")
    elif choice == "3":
        if len(lines) == 0:
            print("The cart is empty.")
        else:
            n = ask_number("item number> ", 1, len(catalog.products), item_message)
            if n == 0 - 1:
                print("Cancelled.")
            elif cart.line_for(lines, n) == None:
                print(cart.product(n)[1] + " is not in the cart.")
            else:
                line = cart.line_for(lines, n)
                q = ask_number("new quantity (0 removes)> ", 0, 99, "Type a quantity from 0 to 99, or nothing to cancel.")
                if q == 0 - 1:
                    print("Cancelled.")
                elif q == 0:
                    lines = cart.without(lines, n)
                    print("Removed " + line[0][1] + ".")
                else:
                    line[1] = q
                    print(line[0][1] + ": now " + str(q) + ".")
    elif choice == "4":
        if len(lines) == 0:
            print("The cart is empty.")
        else:
            print("")
            receipt.show("Your cart", lines, code)
    elif choice == "5":
        typed = text.upper(text.trim(input("code (- removes it)> ")))
        if typed == "":
            print("No code entered.")
        elif typed == "-":
            if code == None:
                print("No code to remove.")
            else:
                print("Code " + code[0] + " removed.")
                code = None
        elif catalog.find_code(typed) == None:
            print("There is no code " + typed + ".")
        elif code != None and code[0] == typed:
            print("Code " + typed + " is already entered.")
        elif code != None:
            print("Code " + typed + " replaces " + code[0] + " - one code per order.")
            code = catalog.find_code(typed)
        else:
            code = catalog.find_code(typed)
            print("Code " + typed + " entered: " + code[2] + ".")
    elif choice == "6":
        if len(lines) == 0:
            print("The cart is empty.")
        else:
            print("")
            receipt.show("Order " + str(order), lines, code)
            print("Paid - thank you. Order " + str(order) + " is done.")
            lines = []
            code = None
            order = order + 1
    elif choice == "7":
        running = False
    else:
        print("Pick a number from 1 to 7.")
print("Bye.")
```

### catalog.eml

```eml
# P011 shopping cart - what the shop sells and the discounts it offers. A
# product is [number, name, price]; every price is whole cents.

[
    [1, "Notebook", 350],
    [2, "Pens, pack of 3", 225],
    [3, "Coffee beans 1kg", 1299],
    [4, "Green tea, 50 bags", 640],
    [5, "Desk lamp", 2400],
    [6, "USB cable", 795],
] => products

# Tax on what is left after the discount: 8.25%, in parts per ten thousand.
825 => tax_per_myriad

# A discount is [code, short name, description, kind, value, minimum, product].
# Kind "percent" takes value percent off the subtotal and kind "amount" takes
# value cents off it, either only from a subtotal of minimum cents; kind
# "3for2" makes every third one of product free. Offers count by themselves;
# a code counts only once it has been entered, and one code at a time.
[
    ["", "5% off from 50.00", "5% off orders of 50.00 or more", "percent", 5, 5000, 0],
    ["", "notebooks 3 for 2", "Notebooks: 3 for the price of 2", "3for2", 0, 0, 1],
] => offers

[
    ["SAVE10", "SAVE10, 10% off", "10% off", "percent", 10, 0, 0],
    ["TAKE5", "TAKE5, 5.00 off", "5.00 off orders of 30.00 or more", "amount", 500, 3000, 0],
] => codes

def find_code(typed):
    # The code called typed (already in capitals), or None.
    for d in codes:
        if d[0] == typed:
            return d
    return None
```

Python projection of catalog.eml:

```python
products = [[1, "Notebook", 350], [2, "Pens, pack of 3", 225], [3, "Coffee beans 1kg", 1299], [4, "Green tea, 50 bags", 640], [5, "Desk lamp", 2400], [6, "USB cable", 795]]
tax_per_myriad = 825
offers = [["", "5% off from 50.00", "5% off orders of 50.00 or more", "percent", 5, 5000, 0], ["", "notebooks 3 for 2", "Notebooks: 3 for the price of 2", "3for2", 0, 0, 1]]
codes = [["SAVE10", "SAVE10, 10% off", "10% off", "percent", 10, 0, 0], ["TAKE5", "TAKE5, 5.00 off", "5.00 off orders of 30.00 or more", "amount", 500, 3000, 0]]

def find_code(typed):
    for d in codes:
        if d[0] == typed:
            return d
    return None
```

### cart.eml

```eml
# P011 shopping cart - the cart and its sums. The cart is a list of lines
# [product, quantity] in the order they were added. Only one discount is ever
# taken: every discount that applies is worked out, and the largest wins.
import catalog

def product(n):
    # The product numbered n, or None.
    for p in catalog.products:
        if p[0] == n:
            return p
    return None

def line_for(lines, n):
    # The cart line for product n, or None.
    for line in lines:
        if line[0][0] == n:
            return line
    return None

def without(lines, n):
    # The cart lines without the one for product n.
    [] => kept
    for line in lines:
        if line[0][0] != n:
            kept + [line] => kept
    return kept

def subtotal(lines):
    0 => s
    for line in lines:
        s + line[0][2] * line[1] => s
    return s

def half_up(n, d):
    # n / d rounded half up to a whole number, for n >= 0 and d > 0.
    return int((2 * n + d - (2 * n + d) % (2 * d)) / (2 * d))

def amount_off(d, lines):
    # What discount d takes off these lines, in cents; 0 if it does not apply.
    if d[3] == "3for2":
        line_for(lines, d[6]) => line
        if line == None:
            return 0
        return int((line[1] - line[1] % 3) / 3) * line[0][2]
    subtotal(lines) => s
    if s < d[5]:
        return 0
    if d[3] == "percent":
        return half_up(s * d[4], 100)
    if d[4] > s:
        return s
    return d[4]

def choices(lines, code):
    # Every discount that takes something off, as [discount, cents]: the code
    # first if one has been entered, then the offers.
    [] => found
    if code != None:
        amount_off(code, lines) => c
        if c > 0:
            found + [[code, c]] => found
    for d in catalog.offers:
        amount_off(d, lines) => c
        if c > 0:
            found + [[d, c]] => found
    return found

def best(found):
    # Where the largest choice is in found (the earlier one on a tie), or -1.
    0 - 1 => b
    for i in [0:len(found) - 1]:
        if b == 0 - 1 or found[i][1] > found[b][1]:
            i => b
    return b
```

Python projection of cart.eml:

```python
import catalog

def product(n):
    for p in catalog.products:
        if p[0] == n:
            return p
    return None

def line_for(lines, n):
    for line in lines:
        if line[0][0] == n:
            return line
    return None

def without(lines, n):
    kept = []
    for line in lines:
        if line[0][0] != n:
            kept = kept + [line]
    return kept

def subtotal(lines):
    s = 0
    for line in lines:
        s = s + line[0][2] * line[1]
    return s

def half_up(n, d):
    return int((2 * n + d - (2 * n + d) % (2 * d)) / (2 * d))

def amount_off(d, lines):
    if d[3] == "3for2":
        line = line_for(lines, d[6])
        if line == None:
            return 0
        return int((line[1] - line[1] % 3) / 3) * line[0][2]
    s = subtotal(lines)
    if s < d[5]:
        return 0
    if d[3] == "percent":
        return half_up(s * d[4], 100)
    if d[4] > s:
        return s
    return d[4]

def choices(lines, code):
    found = []
    if code != None:
        c = amount_off(code, lines)
        if c > 0:
            found = found + [[code, c]]
    for d in catalog.offers:
        c = amount_off(d, lines)
        if c > 0:
            found = found + [[d, c]]
    return found

def best(found):
    b = 0 - 1
    for i in range(0, len(found)):
        if b == 0 - 1 or found[i][1] > found[b][1]:
            b = i
    return b
```

### receipt.eml

```eml
# P011 shopping cart - what the screen shows: the catalog with its offers, and
# the receipt - the lines, the subtotal, the one discount taken, the tax and
# the total, then a note on every discount that was left out.
import catalog
import cart
import text

def show_catalog():
    "" ^0
    "-- catalog --" ^0
    for p in catalog.products:
        ("  " + str(p[0]) + ") " + ("%-20s" % p[1]) + ("%8s" % text.money(p[2]))) ^0
    "" ^0
    "Offers - only the largest one that applies is taken:" ^0
    for d in catalog.offers:
        ("  " + d[2]) ^0
    for d in catalog.codes:
        ("  code " + d[0] + ": " + d[2]) ^0

def row(label, amount):
    return ("%-34s" % label) + ("%10s" % amount)

def show(title, lines, code):
    title ^0
    (("%3s" % "Qty") + "  " + ("%-20s" % "Item") + " " + ("%8s" % "Price") + " " + ("%9s" % "Total")) ^0
    for line in lines:
        (("%3d" % line[1]) + "  " + ("%-20s" % line[0][1]) + " " + ("%8s" % text.money(line[0][2])) + " " + ("%9s" % text.money(line[0][2] * line[1]))) ^0
    ("-" * 44) ^0
    cart.subtotal(lines) => s
    row("Subtotal", text.money(s)) ^0
    cart.choices(lines, code) => found
    cart.best(found) => b
    0 => off
    if b >= 0:
        found[b][1] => off
        row("Discount (" + found[b][0][1] + ")", "-" + text.money(off)) ^0
        row("After discount", text.money(s - off)) ^0
    cart.half_up((s - off) * catalog.tax_per_myriad, 10000) => tax
    row("Tax " + text.money(catalog.tax_per_myriad) + "%", text.money(tax)) ^0
    row("Total", text.money(s - off + tax)) ^0
    if code != None and cart.amount_off(code, lines) == 0:
        ("Code " + code[0] + " is not used: it needs a subtotal of " + text.money(code[5]) + ".") ^0
    for i in [0:len(found) - 1]:
        if i != b:
            ("Not added: " + found[i][0][1] + " (" + text.money(found[i][1]) + ") - discounts do not stack.") ^0
```

Python projection of receipt.eml:

```python
import catalog
import cart
import text

def show_catalog():
    print("")
    print("-- catalog --")
    for p in catalog.products:
        print("  " + str(p[0]) + ") " + "%-20s" % p[1] + "%8s" % text.money(p[2]))
    print("")
    print("Offers - only the largest one that applies is taken:")
    for d in catalog.offers:
        print("  " + d[2])
    for d in catalog.codes:
        print("  code " + d[0] + ": " + d[2])

def row(label, amount):
    return "%-34s" % label + "%10s" % amount

def show(title, lines, code):
    print(title)
    print("%3s" % "Qty" + "  " + "%-20s" % "Item" + " " + "%8s" % "Price" + " " + "%9s" % "Total")
    for line in lines:
        print("%3d" % line[1] + "  " + "%-20s" % line[0][1] + " " + "%8s" % text.money(line[0][2]) + " " + "%9s" % text.money(line[0][2] * line[1]))
    print("-" * 44)
    s = cart.subtotal(lines)
    print(row("Subtotal", text.money(s)))
    found = cart.choices(lines, code)
    b = cart.best(found)
    off = 0
    if b >= 0:
        off = found[b][1]
        print(row("Discount (" + found[b][0][1] + ")", "-" + text.money(off)))
        print(row("After discount", text.money(s - off)))
    tax = cart.half_up((s - off) * catalog.tax_per_myriad, 10000)
    print(row("Tax " + text.money(catalog.tax_per_myriad) + "%", text.money(tax)))
    print(row("Total", text.money(s - off + tax)))
    if code != None and cart.amount_off(code, lines) == 0:
        print("Code " + code[0] + " is not used: it needs a subtotal of " + text.money(code[5]) + ".")
    for i in range(0, len(found)):
        if i != b:
            print("Not added: " + found[i][0][1] + " (" + text.money(found[i][1]) + ") - discounts do not stack.")
```

### text.eml

```eml
# P011 shopping cart - reading what is typed and writing money. 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 upper(s):
    # s with a-z turned into A-Z; everything else as it was.
    "abcdefghijklmnopqrstuvwxyz" => lower_letters
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ" => upper_letters
    "" => out
    for c in s:
        c => d
        for i in [0:25]:
            if lower_letters[i] == c:
                upper_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 money(c):
    # Whole cents as 1,234.50, built from the digits of the integer.
    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 upper(s):
    lower_letters = "abcdefghijklmnopqrstuvwxyz"
    upper_letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    out = ""
    for c in s:
        d = c
        for i in range(0, 26):
            if lower_letters[i] == c:
                d = upper_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 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

# P011 - Shopping cart

A small shop: pick products from a catalog of six, change quantities, enter a
discount code, look at the cart, and check out with a receipt. The receipt
lists every line, then the subtotal, the discount, what is left after it, the
tax and the total. Orders are numbered while the program runs.

- `main.eml` - the menu and its questions, with their checks
- `catalog.eml` - the products, the tax rate, the two offers that count by
  themselves and the two codes that count once entered
- `cart.eml` - the cart's lines and sums: the subtotal, what each discount
  takes off, and which discount wins
- `receipt.eml` - what the screen shows: the catalog with its offers, and the
  receipt with its notes
- `text.eml` - trimming, capitals, reading a whole number, and money

Discounts never stack. Every discount that applies to the cart - the offers
(5% off from 50.00; notebooks 3 for the price of 2) and the code, if one has
been entered - is worked out, and only the largest is taken; on a tie the code
comes first. The receipt names the others as not added, with what each would
have given, and says when an entered code is not used because the subtotal is
below its minimum. One code counts per order: a new code replaces the old one.

All money is whole cents. A percentage off and the tax (8.25% of what is left
after the discount) are rounded half up to the cent, in whole numbers.

What is checked: an item number is one in the catalog, and a quantity is a
whole number from 1 to 99 (0 to 99 when changing one, where 0 removes the
line); anything else asks again, and nothing cancels. One line holds at most
99. A code is one of the shop's codes, in any case; `-` removes it.

Sessions: `sessions/basic.in` shows the catalog, adds notebooks and coffee -
the 3-for-2 offer wins - enters a code that beats it, adds a lamp and replaces
the code with one that beats the 5% offer, changes a quantity, and checks out;
`sessions/bad-input.in` uses the cart, a change and checkout while the cart is
empty, types item numbers and quantities that are out of range, negative,
words or empty, goes over 99 of one item, enters an empty, a missing and an
unknown code, the same code twice, a code below its minimum, then a cart where
the 5% offer beats the code, and removes everything.

Built on the verified corpus cases `shopping-cart-total` (lines of price times
quantity summed into a total), `receipt-formatter` (a receipt with aligned
columns, a subtotal, tax and a total) and `the-two-discounts-were-added` (two
discounts do not add up to their sum).
