{
  "id": "P011",
  "slug": "shopping-cart",
  "key": "P011-shopping-cart",
  "title": "Shopping cart",
  "summary": "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.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P011 - Shopping cart\n\nA small shop: pick products from a catalog of six, change quantities, enter a\ndiscount code, look at the cart, and check out with a receipt. The receipt\nlists every line, then the subtotal, the discount, what is left after it, the\ntax and the total. Orders are numbered while the program runs.\n\n- `main.eml` - the menu and its questions, with their checks\n- `catalog.eml` - the products, the tax rate, the two offers that count by\n  themselves and the two codes that count once entered\n- `cart.eml` - the cart's lines and sums: the subtotal, what each discount\n  takes off, and which discount wins\n- `receipt.eml` - what the screen shows: the catalog with its offers, and the\n  receipt with its notes\n- `text.eml` - trimming, capitals, reading a whole number, and money\n\nDiscounts never stack. Every discount that applies to the cart - the offers\n(5% off from 50.00; notebooks 3 for the price of 2) and the code, if one has\nbeen entered - is worked out, and only the largest is taken; on a tie the code\ncomes first. The receipt names the others as not added, with what each would\nhave given, and says when an entered code is not used because the subtotal is\nbelow its minimum. One code counts per order: a new code replaces the old one.\n\nAll money is whole cents. A percentage off and the tax (8.25% of what is left\nafter the discount) are rounded half up to the cent, in whole numbers.\n\nWhat is checked: an item number is one in the catalog, and a quantity is a\nwhole number from 1 to 99 (0 to 99 when changing one, where 0 removes the\nline); anything else asks again, and nothing cancels. One line holds at most\n99. A code is one of the shop's codes, in any case; `-` removes it.\n\nSessions: `sessions/basic.in` shows the catalog, adds notebooks and coffee -\nthe 3-for-2 offer wins - enters a code that beats it, adds a lamp and replaces\nthe code with one that beats the 5% offer, changes a quantity, and checks out;\n`sessions/bad-input.in` uses the cart, a change and checkout while the cart is\nempty, types item numbers and quantities that are out of range, negative,\nwords or empty, goes over 99 of one item, enters an empty, a missing and an\nunknown code, the same code twice, a code below its minimum, then a cart where\nthe 5% offer beats the code, and removes everything.\n\nBuilt on the verified corpus cases `shopping-cart-total` (lines of price times\nquantity summed into a total), `receipt-formatter` (a receipt with aligned\ncolumns, a subtotal, tax and a total) and `the-two-discounts-were-added` (two\ndiscounts do not add up to their sum).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P011 shopping cart: fill a cart from the catalog, change quantities, enter\n# a discount code, and check out with a receipt. Only one discount is ever\n# taken - the largest that applies; discounts are never added together.\nimport catalog\nimport cart\nimport receipt\nimport text\n\ndef ask_number(prompt, low, high, message):\n    # A whole number from low to high, asked again until one is typed; -1 if\n    # the answer is empty, which cancels.\n    while True:\n        text.trim(input(prompt)) => answer\n        if answer == \"\":\n            return 0 - 1\n        text.number(answer) => n\n        if n >= low and n <= high:\n            return n\n        message ^0\n\n(\"Type an item number from 1 to \" + str(len(catalog.products)) + \", or nothing to cancel.\") => item_message\n[] => lines\nNone => code\n1 => order\nTrue => running\nwhile running:\n    \"\" ^0\n    \"== Shop ==\" ^0\n    \"1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\" ^0\n    text.trim(input(\"choice> \")) => choice\n    if choice == \"1\":\n        receipt.show_catalog()\n    elif choice == \"2\":\n        ask_number(\"item number> \", 1, len(catalog.products), item_message) => n\n        if n == 0 - 1:\n            \"Cancelled.\" ^0\n        else:\n            cart.product(n) => p\n            ask_number(\"quantity> \", 1, 99, \"Type a quantity from 1 to 99, or nothing to cancel.\") => q\n            cart.line_for(lines, n) => line\n            if q == 0 - 1:\n                \"Cancelled.\" ^0\n            elif line == None:\n                lines + [[p, q]] => lines\n                (\"Added \" + str(q) + \" x \" + p[1] + \".\") ^0\n            elif line[1] + q > 99:\n                (\"At most 99 of one item; the cart has \" + str(line[1]) + \".\") ^0\n            else:\n                line[1] + q => line[1]\n                (\"Added \" + str(q) + \" x \" + p[1] + \" (now \" + str(line[1]) + \").\") ^0\n    elif choice == \"3\":\n        if len(lines) == 0:\n            \"The cart is empty.\" ^0\n        else:\n            ask_number(\"item number> \", 1, len(catalog.products), item_message) => n\n            if n == 0 - 1:\n                \"Cancelled.\" ^0\n            elif cart.line_for(lines, n) == None:\n                (cart.product(n)[1] + \" is not in the cart.\") ^0\n            else:\n                cart.line_for(lines, n) => line\n                ask_number(\"new quantity (0 removes)> \", 0, 99, \"Type a quantity from 0 to 99, or nothing to cancel.\") => q\n                if q == 0 - 1:\n                    \"Cancelled.\" ^0\n                elif q == 0:\n                    cart.without(lines, n) => lines\n                    (\"Removed \" + line[0][1] + \".\") ^0\n                else:\n                    q => line[1]\n                    (line[0][1] + \": now \" + str(q) + \".\") ^0\n    elif choice == \"4\":\n        if len(lines) == 0:\n            \"The cart is empty.\" ^0\n        else:\n            \"\" ^0\n            receipt.show(\"Your cart\", lines, code)\n    elif choice == \"5\":\n        text.upper(text.trim(input(\"code (- removes it)> \"))) => typed\n        if typed == \"\":\n            \"No code entered.\" ^0\n        elif typed == \"-\":\n            if code == None:\n                \"No code to remove.\" ^0\n            else:\n                (\"Code \" + code[0] + \" removed.\") ^0\n                None => code\n        elif catalog.find_code(typed) == None:\n            (\"There is no code \" + typed + \".\") ^0\n        elif code != None and code[0] == typed:\n            (\"Code \" + typed + \" is already entered.\") ^0\n        elif code != None:\n            (\"Code \" + typed + \" replaces \" + code[0] + \" - one code per order.\") ^0\n            catalog.find_code(typed) => code\n        else:\n            catalog.find_code(typed) => code\n            (\"Code \" + typed + \" entered: \" + code[2] + \".\") ^0\n    elif choice == \"6\":\n        if len(lines) == 0:\n            \"The cart is empty.\" ^0\n        else:\n            \"\" ^0\n            receipt.show(\"Order \" + str(order), lines, code)\n            (\"Paid - thank you. Order \" + str(order) + \" is done.\") ^0\n            [] => lines\n            None => code\n            order + 1 => order\n    elif choice == \"7\":\n        False => running\n    else:\n        \"Pick a number from 1 to 7.\" ^0\n\"Bye.\" ^0\n",
      "python": "import catalog\nimport cart\nimport receipt\nimport text\n\ndef ask_number(prompt, low, high, message):\n    while True:\n        answer = text.trim(input(prompt))\n        if answer == \"\":\n            return 0 - 1\n        n = text.number(answer)\n        if n >= low and n <= high:\n            return n\n        print(message)\n\nitem_message = \"Type an item number from 1 to \" + str(len(catalog.products)) + \", or nothing to cancel.\"\nlines = []\ncode = None\norder = 1\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"== Shop ==\")\n    print(\"1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\")\n    choice = text.trim(input(\"choice> \"))\n    if choice == \"1\":\n        receipt.show_catalog()\n    elif choice == \"2\":\n        n = ask_number(\"item number> \", 1, len(catalog.products), item_message)\n        if n == 0 - 1:\n            print(\"Cancelled.\")\n        else:\n            p = cart.product(n)\n            q = ask_number(\"quantity> \", 1, 99, \"Type a quantity from 1 to 99, or nothing to cancel.\")\n            line = cart.line_for(lines, n)\n            if q == 0 - 1:\n                print(\"Cancelled.\")\n            elif line == None:\n                lines = lines + [[p, q]]\n                print(\"Added \" + str(q) + \" x \" + p[1] + \".\")\n            elif line[1] + q > 99:\n                print(\"At most 99 of one item; the cart has \" + str(line[1]) + \".\")\n            else:\n                line[1] = line[1] + q\n                print(\"Added \" + str(q) + \" x \" + p[1] + \" (now \" + str(line[1]) + \").\")\n    elif choice == \"3\":\n        if len(lines) == 0:\n            print(\"The cart is empty.\")\n        else:\n            n = ask_number(\"item number> \", 1, len(catalog.products), item_message)\n            if n == 0 - 1:\n                print(\"Cancelled.\")\n            elif cart.line_for(lines, n) == None:\n                print(cart.product(n)[1] + \" is not in the cart.\")\n            else:\n                line = cart.line_for(lines, n)\n                q = ask_number(\"new quantity (0 removes)> \", 0, 99, \"Type a quantity from 0 to 99, or nothing to cancel.\")\n                if q == 0 - 1:\n                    print(\"Cancelled.\")\n                elif q == 0:\n                    lines = cart.without(lines, n)\n                    print(\"Removed \" + line[0][1] + \".\")\n                else:\n                    line[1] = q\n                    print(line[0][1] + \": now \" + str(q) + \".\")\n    elif choice == \"4\":\n        if len(lines) == 0:\n            print(\"The cart is empty.\")\n        else:\n            print(\"\")\n            receipt.show(\"Your cart\", lines, code)\n    elif choice == \"5\":\n        typed = text.upper(text.trim(input(\"code (- removes it)> \")))\n        if typed == \"\":\n            print(\"No code entered.\")\n        elif typed == \"-\":\n            if code == None:\n                print(\"No code to remove.\")\n            else:\n                print(\"Code \" + code[0] + \" removed.\")\n                code = None\n        elif catalog.find_code(typed) == None:\n            print(\"There is no code \" + typed + \".\")\n        elif code != None and code[0] == typed:\n            print(\"Code \" + typed + \" is already entered.\")\n        elif code != None:\n            print(\"Code \" + typed + \" replaces \" + code[0] + \" - one code per order.\")\n            code = catalog.find_code(typed)\n        else:\n            code = catalog.find_code(typed)\n            print(\"Code \" + typed + \" entered: \" + code[2] + \".\")\n    elif choice == \"6\":\n        if len(lines) == 0:\n            print(\"The cart is empty.\")\n        else:\n            print(\"\")\n            receipt.show(\"Order \" + str(order), lines, code)\n            print(\"Paid - thank you. Order \" + str(order) + \" is done.\")\n            lines = []\n            code = None\n            order = order + 1\n    elif choice == \"7\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 7.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "catalog.eml",
      "eml": "# P011 shopping cart - what the shop sells and the discounts it offers. A\n# product is [number, name, price]; every price is whole cents.\n\n[\n    [1, \"Notebook\", 350],\n    [2, \"Pens, pack of 3\", 225],\n    [3, \"Coffee beans 1kg\", 1299],\n    [4, \"Green tea, 50 bags\", 640],\n    [5, \"Desk lamp\", 2400],\n    [6, \"USB cable\", 795],\n] => products\n\n# Tax on what is left after the discount: 8.25%, in parts per ten thousand.\n825 => tax_per_myriad\n\n# A discount is [code, short name, description, kind, value, minimum, product].\n# Kind \"percent\" takes value percent off the subtotal and kind \"amount\" takes\n# value cents off it, either only from a subtotal of minimum cents; kind\n# \"3for2\" makes every third one of product free. Offers count by themselves;\n# a code counts only once it has been entered, and one code at a time.\n[\n    [\"\", \"5% off from 50.00\", \"5% off orders of 50.00 or more\", \"percent\", 5, 5000, 0],\n    [\"\", \"notebooks 3 for 2\", \"Notebooks: 3 for the price of 2\", \"3for2\", 0, 0, 1],\n] => offers\n\n[\n    [\"SAVE10\", \"SAVE10, 10% off\", \"10% off\", \"percent\", 10, 0, 0],\n    [\"TAKE5\", \"TAKE5, 5.00 off\", \"5.00 off orders of 30.00 or more\", \"amount\", 500, 3000, 0],\n] => codes\n\ndef find_code(typed):\n    # The code called typed (already in capitals), or None.\n    for d in codes:\n        if d[0] == typed:\n            return d\n    return None\n",
      "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]]\ntax_per_myriad = 825\noffers = [[\"\", \"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]]\ncodes = [[\"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]]\n\ndef find_code(typed):\n    for d in codes:\n        if d[0] == typed:\n            return d\n    return None\n"
    },
    {
      "name": "cart.eml",
      "eml": "# P011 shopping cart - the cart and its sums. The cart is a list of lines\n# [product, quantity] in the order they were added. Only one discount is ever\n# taken: every discount that applies is worked out, and the largest wins.\nimport catalog\n\ndef product(n):\n    # The product numbered n, or None.\n    for p in catalog.products:\n        if p[0] == n:\n            return p\n    return None\n\ndef line_for(lines, n):\n    # The cart line for product n, or None.\n    for line in lines:\n        if line[0][0] == n:\n            return line\n    return None\n\ndef without(lines, n):\n    # The cart lines without the one for product n.\n    [] => kept\n    for line in lines:\n        if line[0][0] != n:\n            kept + [line] => kept\n    return kept\n\ndef subtotal(lines):\n    0 => s\n    for line in lines:\n        s + line[0][2] * line[1] => s\n    return s\n\ndef half_up(n, d):\n    # n / d rounded half up to a whole number, for n >= 0 and d > 0.\n    return int((2 * n + d - (2 * n + d) % (2 * d)) / (2 * d))\n\ndef amount_off(d, lines):\n    # What discount d takes off these lines, in cents; 0 if it does not apply.\n    if d[3] == \"3for2\":\n        line_for(lines, d[6]) => line\n        if line == None:\n            return 0\n        return int((line[1] - line[1] % 3) / 3) * line[0][2]\n    subtotal(lines) => s\n    if s < d[5]:\n        return 0\n    if d[3] == \"percent\":\n        return half_up(s * d[4], 100)\n    if d[4] > s:\n        return s\n    return d[4]\n\ndef choices(lines, code):\n    # Every discount that takes something off, as [discount, cents]: the code\n    # first if one has been entered, then the offers.\n    [] => found\n    if code != None:\n        amount_off(code, lines) => c\n        if c > 0:\n            found + [[code, c]] => found\n    for d in catalog.offers:\n        amount_off(d, lines) => c\n        if c > 0:\n            found + [[d, c]] => found\n    return found\n\ndef best(found):\n    # Where the largest choice is in found (the earlier one on a tie), or -1.\n    0 - 1 => b\n    for i in [0:len(found) - 1]:\n        if b == 0 - 1 or found[i][1] > found[b][1]:\n            i => b\n    return b\n",
      "python": "import catalog\n\ndef product(n):\n    for p in catalog.products:\n        if p[0] == n:\n            return p\n    return None\n\ndef line_for(lines, n):\n    for line in lines:\n        if line[0][0] == n:\n            return line\n    return None\n\ndef without(lines, n):\n    kept = []\n    for line in lines:\n        if line[0][0] != n:\n            kept = kept + [line]\n    return kept\n\ndef subtotal(lines):\n    s = 0\n    for line in lines:\n        s = s + line[0][2] * line[1]\n    return s\n\ndef half_up(n, d):\n    return int((2 * n + d - (2 * n + d) % (2 * d)) / (2 * d))\n\ndef amount_off(d, lines):\n    if d[3] == \"3for2\":\n        line = line_for(lines, d[6])\n        if line == None:\n            return 0\n        return int((line[1] - line[1] % 3) / 3) * line[0][2]\n    s = subtotal(lines)\n    if s < d[5]:\n        return 0\n    if d[3] == \"percent\":\n        return half_up(s * d[4], 100)\n    if d[4] > s:\n        return s\n    return d[4]\n\ndef choices(lines, code):\n    found = []\n    if code != None:\n        c = amount_off(code, lines)\n        if c > 0:\n            found = found + [[code, c]]\n    for d in catalog.offers:\n        c = amount_off(d, lines)\n        if c > 0:\n            found = found + [[d, c]]\n    return found\n\ndef best(found):\n    b = 0 - 1\n    for i in range(0, len(found)):\n        if b == 0 - 1 or found[i][1] > found[b][1]:\n            b = i\n    return b\n"
    },
    {
      "name": "receipt.eml",
      "eml": "# P011 shopping cart - what the screen shows: the catalog with its offers, and\n# the receipt - the lines, the subtotal, the one discount taken, the tax and\n# the total, then a note on every discount that was left out.\nimport catalog\nimport cart\nimport text\n\ndef show_catalog():\n    \"\" ^0\n    \"-- catalog --\" ^0\n    for p in catalog.products:\n        (\"  \" + str(p[0]) + \") \" + (\"%-20s\" % p[1]) + (\"%8s\" % text.money(p[2]))) ^0\n    \"\" ^0\n    \"Offers - only the largest one that applies is taken:\" ^0\n    for d in catalog.offers:\n        (\"  \" + d[2]) ^0\n    for d in catalog.codes:\n        (\"  code \" + d[0] + \": \" + d[2]) ^0\n\ndef row(label, amount):\n    return (\"%-34s\" % label) + (\"%10s\" % amount)\n\ndef show(title, lines, code):\n    title ^0\n    ((\"%3s\" % \"Qty\") + \"  \" + (\"%-20s\" % \"Item\") + \" \" + (\"%8s\" % \"Price\") + \" \" + (\"%9s\" % \"Total\")) ^0\n    for line in lines:\n        ((\"%3d\" % line[1]) + \"  \" + (\"%-20s\" % line[0][1]) + \" \" + (\"%8s\" % text.money(line[0][2])) + \" \" + (\"%9s\" % text.money(line[0][2] * line[1]))) ^0\n    (\"-\" * 44) ^0\n    cart.subtotal(lines) => s\n    row(\"Subtotal\", text.money(s)) ^0\n    cart.choices(lines, code) => found\n    cart.best(found) => b\n    0 => off\n    if b >= 0:\n        found[b][1] => off\n        row(\"Discount (\" + found[b][0][1] + \")\", \"-\" + text.money(off)) ^0\n        row(\"After discount\", text.money(s - off)) ^0\n    cart.half_up((s - off) * catalog.tax_per_myriad, 10000) => tax\n    row(\"Tax \" + text.money(catalog.tax_per_myriad) + \"%\", text.money(tax)) ^0\n    row(\"Total\", text.money(s - off + tax)) ^0\n    if code != None and cart.amount_off(code, lines) == 0:\n        (\"Code \" + code[0] + \" is not used: it needs a subtotal of \" + text.money(code[5]) + \".\") ^0\n    for i in [0:len(found) - 1]:\n        if i != b:\n            (\"Not added: \" + found[i][0][1] + \" (\" + text.money(found[i][1]) + \") - discounts do not stack.\") ^0\n",
      "python": "import catalog\nimport cart\nimport text\n\ndef show_catalog():\n    print(\"\")\n    print(\"-- catalog --\")\n    for p in catalog.products:\n        print(\"  \" + str(p[0]) + \") \" + \"%-20s\" % p[1] + \"%8s\" % text.money(p[2]))\n    print(\"\")\n    print(\"Offers - only the largest one that applies is taken:\")\n    for d in catalog.offers:\n        print(\"  \" + d[2])\n    for d in catalog.codes:\n        print(\"  code \" + d[0] + \": \" + d[2])\n\ndef row(label, amount):\n    return \"%-34s\" % label + \"%10s\" % amount\n\ndef show(title, lines, code):\n    print(title)\n    print(\"%3s\" % \"Qty\" + \"  \" + \"%-20s\" % \"Item\" + \" \" + \"%8s\" % \"Price\" + \" \" + \"%9s\" % \"Total\")\n    for line in lines:\n        print(\"%3d\" % line[1] + \"  \" + \"%-20s\" % line[0][1] + \" \" + \"%8s\" % text.money(line[0][2]) + \" \" + \"%9s\" % text.money(line[0][2] * line[1]))\n    print(\"-\" * 44)\n    s = cart.subtotal(lines)\n    print(row(\"Subtotal\", text.money(s)))\n    found = cart.choices(lines, code)\n    b = cart.best(found)\n    off = 0\n    if b >= 0:\n        off = found[b][1]\n        print(row(\"Discount (\" + found[b][0][1] + \")\", \"-\" + text.money(off)))\n        print(row(\"After discount\", text.money(s - off)))\n    tax = cart.half_up((s - off) * catalog.tax_per_myriad, 10000)\n    print(row(\"Tax \" + text.money(catalog.tax_per_myriad) + \"%\", text.money(tax)))\n    print(row(\"Total\", text.money(s - off + tax)))\n    if code != None and cart.amount_off(code, lines) == 0:\n        print(\"Code \" + code[0] + \" is not used: it needs a subtotal of \" + text.money(code[5]) + \".\")\n    for i in range(0, len(found)):\n        if i != b:\n            print(\"Not added: \" + found[i][0][1] + \" (\" + text.money(found[i][1]) + \") - discounts do not stack.\")\n"
    },
    {
      "name": "text.eml",
      "eml": "# P011 shopping cart - reading what is typed and writing money. The\n# interpreter that checks every session does not run string methods yet, so\n# the text handling is written out here.\n\ndef trim(s):\n    # s without the spaces at either end.\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef upper(s):\n    # s with a-z turned into A-Z; everything else as it was.\n    \"abcdefghijklmnopqrstuvwxyz\" => lower_letters\n    \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\" => upper_letters\n    \"\" => out\n    for c in s:\n        c => d\n        for i in [0:25]:\n            if lower_letters[i] == c:\n                upper_letters[i] => d\n        out + d => out\n    return out\n\ndef number(s):\n    # The value of s if it is digits only (at least one), otherwise -1.\n    if s == \"\":\n        return 0 - 1\n    0 => n\n    for c in s:\n        if not (c in \"0123456789\"):\n            return 0 - 1\n        n * 10 + int(c) => n\n    return n\n\ndef money(c):\n    # Whole cents as 1,234.50, built from the digits of the integer.\n    str(c) => s\n    while len(s) < 3:\n        \"0\" + s => s\n    s[0:len(s) - 2] => whole\n    s[len(s) - 2:len(s)] => cents\n    \"\" => grouped\n    len(whole) => i\n    while i > 3:\n        \",\" + whole[i - 3:i] + grouped => grouped\n        i - 3 => i\n    return whole[0:i] + grouped + \".\" + cents\n",
      "python": "def trim(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef upper(s):\n    lower_letters = \"abcdefghijklmnopqrstuvwxyz\"\n    upper_letters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n    out = \"\"\n    for c in s:\n        d = c\n        for i in range(0, 26):\n            if lower_letters[i] == c:\n                d = upper_letters[i]\n        out = out + d\n    return out\n\ndef number(s):\n    if s == \"\":\n        return 0 - 1\n    n = 0\n    for c in s:\n        if not c in \"0123456789\":\n            return 0 - 1\n        n = n * 10 + int(c)\n    return n\n\ndef money(c):\n    s = str(c)\n    while len(s) < 3:\n        s = \"0\" + s\n    whole = s[0:len(s) - 2]\n    cents = s[len(s) - 2:len(s)]\n    grouped = \"\"\n    i = len(whole)\n    while i > 3:\n        grouped = \",\" + whole[i - 3:i] + grouped\n        i = i - 3\n    return whole[0:i] + grouped + \".\" + cents\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "8\n4\n3\n6\n2\n0\nx\n7\n\n2\n2\n0\n100\n-1\ntwo\n4\n2\n2\n96\n2\n2\n\n5\n\n5\n-\n5\nhalf\n5\n take5\n5\nTAKE5\n4\n3\n4\n3\n2\nabc\n\n2\n5\n5\n4\n5\n-\n3\n2\n0\n3\n5\n0\n4\n7\n",
      "screen": "\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 8\nPick a number from 1 to 7.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 4\nThe cart is empty.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 3\nThe cart is empty.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 6\nThe cart is empty.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 2\nitem number> 0\nType an item number from 1 to 6, or nothing to cancel.\nitem number> x\nType an item number from 1 to 6, or nothing to cancel.\nitem number> 7\nType an item number from 1 to 6, or nothing to cancel.\nitem number> \nCancelled.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 2\nitem number> 2\nquantity> 0\nType a quantity from 1 to 99, or nothing to cancel.\nquantity> 100\nType a quantity from 1 to 99, or nothing to cancel.\nquantity> -1\nType a quantity from 1 to 99, or nothing to cancel.\nquantity> two\nType a quantity from 1 to 99, or nothing to cancel.\nquantity> 4\nAdded 4 x Pens, pack of 3.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 2\nitem number> 2\nquantity> 96\nAt most 99 of one item; the cart has 4.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 2\nitem number> 2\nquantity> \nCancelled.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 5\ncode (- removes it)> \nNo code entered.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 5\ncode (- removes it)> -\nNo code to remove.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 5\ncode (- removes it)> half\nThere is no code HALF.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 5\ncode (- removes it)>  take5\nCode TAKE5 entered: 5.00 off orders of 30.00 or more.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 5\ncode (- removes it)> TAKE5\nCode TAKE5 is already entered.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 4\n\nYour cart\nQty  Item                    Price     Total\n  4  Pens, pack of 3          2.25      9.00\n--------------------------------------------\nSubtotal                                9.00\nTax 8.25%                               0.74\nTotal                                   9.74\nCode TAKE5 is not used: it needs a subtotal of 30.00.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 3\nitem number> 4\nGreen tea, 50 bags is not in the cart.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 3\nitem number> 2\nnew quantity (0 removes)> abc\nType a quantity from 0 to 99, or nothing to cancel.\nnew quantity (0 removes)> \nCancelled.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 2\nitem number> 5\nquantity> 5\nAdded 5 x Desk lamp.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 4\n\nYour cart\nQty  Item                    Price     Total\n  4  Pens, pack of 3          2.25      9.00\n  5  Desk lamp               24.00    120.00\n--------------------------------------------\nSubtotal                              129.00\nDiscount (5% off from 50.00)           -6.45\nAfter discount                        122.55\nTax 8.25%                              10.11\nTotal                                 132.66\nNot added: TAKE5, 5.00 off (5.00) - discounts do not stack.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 5\ncode (- removes it)> -\nCode TAKE5 removed.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 3\nitem number> 2\nnew quantity (0 removes)> 0\nRemoved Pens, pack of 3.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 3\nitem number> 5\nnew quantity (0 removes)> 0\nRemoved Desk lamp.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 4\nThe cart is empty.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 7\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\n1\n3\n2\n3\n2\n4\n5\ntake5\n4\n2\n5\n1\n5\nSAVE10\n4\n3\n1\n2\n6\n4\n7\n",
      "screen": "\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 1\n\n-- catalog --\n  1) Notebook                3.50\n  2) Pens, pack of 3         2.25\n  3) Coffee beans 1kg       12.99\n  4) Green tea, 50 bags      6.40\n  5) Desk lamp              24.00\n  6) USB cable               7.95\n\nOffers - only the largest one that applies is taken:\n  5% off orders of 50.00 or more\n  Notebooks: 3 for the price of 2\n  code SAVE10: 10% off\n  code TAKE5: 5.00 off orders of 30.00 or more\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 2\nitem number> 1\nquantity> 3\nAdded 3 x Notebook.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 2\nitem number> 3\nquantity> 2\nAdded 2 x Coffee beans 1kg.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 4\n\nYour cart\nQty  Item                    Price     Total\n  3  Notebook                 3.50     10.50\n  2  Coffee beans 1kg        12.99     25.98\n--------------------------------------------\nSubtotal                               36.48\nDiscount (notebooks 3 for 2)           -3.50\nAfter discount                         32.98\nTax 8.25%                               2.72\nTotal                                  35.70\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 5\ncode (- removes it)> take5\nCode TAKE5 entered: 5.00 off orders of 30.00 or more.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 4\n\nYour cart\nQty  Item                    Price     Total\n  3  Notebook                 3.50     10.50\n  2  Coffee beans 1kg        12.99     25.98\n--------------------------------------------\nSubtotal                               36.48\nDiscount (TAKE5, 5.00 off)             -5.00\nAfter discount                         31.48\nTax 8.25%                               2.60\nTotal                                  34.08\nNot added: notebooks 3 for 2 (3.50) - discounts do not stack.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 2\nitem number> 5\nquantity> 1\nAdded 1 x Desk lamp.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 5\ncode (- removes it)> SAVE10\nCode SAVE10 replaces TAKE5 - one code per order.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 4\n\nYour cart\nQty  Item                    Price     Total\n  3  Notebook                 3.50     10.50\n  2  Coffee beans 1kg        12.99     25.98\n  1  Desk lamp               24.00     24.00\n--------------------------------------------\nSubtotal                               60.48\nDiscount (SAVE10, 10% off)             -6.05\nAfter discount                         54.43\nTax 8.25%                               4.49\nTotal                                  58.92\nNot added: 5% off from 50.00 (3.02) - discounts do not stack.\nNot added: notebooks 3 for 2 (3.50) - discounts do not stack.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 3\nitem number> 1\nnew quantity (0 removes)> 2\nNotebook: now 2.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 6\n\nOrder 1\nQty  Item                    Price     Total\n  2  Notebook                 3.50      7.00\n  2  Coffee beans 1kg        12.99     25.98\n  1  Desk lamp               24.00     24.00\n--------------------------------------------\nSubtotal                               56.98\nDiscount (SAVE10, 10% off)             -5.70\nAfter discount                         51.28\nTax 8.25%                               4.23\nTotal                                  55.51\nNot added: 5% off from 50.00 (2.85) - discounts do not stack.\nPaid - thank you. Order 1 is done.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 4\nThe cart is empty.\n\n== Shop ==\n1) catalog  2) add  3) change quantity  4) cart  5) code  6) checkout  7) quit\nchoice> 7\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "shopping-cart-total",
      "caseId": "038-shopping-cart-total",
      "title": "Shopping cart total"
    },
    {
      "slug": "receipt-formatter",
      "caseId": "147-receipt-formatter",
      "title": "A receipt, and what `%` rounding actually does"
    },
    {
      "slug": "the-two-discounts-were-added",
      "caseId": "805-the-two-discounts-were-added",
      "title": "The two discounts were added"
    }
  ],
  "updated": "2026-10-03"
}
