Project 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.

5 modules · 2 recorded sessionstext-menu UI in the terminalupdated 2026-10-03

Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.

About

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

Recorded sessions

What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.

bad-input

interpreter: byte-equal

== 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.
What was typed (53 lines)
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

basic

interpreter: byte-equal

== 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.
What was typed (23 lines)
1
2
1
3
2
3
2
4
5
take5
4
2
5
1
5
SAVE10
4
3
1
2
6
4
7

Modules

The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.

main.eml(entry)

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 (main.py)
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 (catalog.py)
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 (cart.py)
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 (receipt.py)
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 (text.py)
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

Built on these corpus cases