{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "5cbf1deb",
   "metadata": {},
   "source": [
    "# 3.4 — Breaking Code into Functions\n",
    "\n",
    "**Goal:** move the logic out of the menu into functions that take data in and return data out, so you can test each one without typing.\n",
    "\n",
    "**How to use this notebook**\n",
    "\n",
    "- Run cells **top to bottom**. Click a cell and press **Shift + Enter**.\n",
    "- Cells marked **TODO** are yours. Replace the `None`, `pass` or empty list with your code.\n",
    "- Under each TODO is a **check cell**. It stays quiet when your code is right. When it's wrong, it tells you what to look at. Read that message before you ask for help.\n",
    "- Each part starts with a worked example, then gives you practice in different contexts (library, gradebook, bike shop, budget and so on). Notice the *pattern* repeating across them.\n",
    "- Finished? Run **Part: Tidy up** so your practice files don't pile up."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "842b155c",
   "metadata": {},
   "outputs": [],
   "source": [
    "import io, contextlib\n",
    "\n",
    "def check_case(label, got, expected):\n",
    "    \"\"\"Print PASS or FAIL for one test case, without stopping the program.\"\"\"\n",
    "    if got == expected:\n",
    "        print(\"PASS: \" + label)\n",
    "    else:\n",
    "        print(\"FAIL: \" + label + \". Expected \" + repr(expected) + \", got \" + repr(got))\n",
    "\n",
    "def printed_pass(test_function, *args):\n",
    "    \"\"\"Run a test and return True if it printed PASS.\"\"\"\n",
    "    buf = io.StringIO()\n",
    "    with contextlib.redirect_stdout(buf):\n",
    "        test_function(*args)\n",
    "    return \"PASS\" in buf.getvalue()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "452bd6d9",
   "metadata": {},
   "source": [
    "## Part 1 — Data in, data out\n",
    "\n",
    "These two functions don't call `input()` or `print()`. They take data in and return data out. The menu does the talking. That's what makes them easy to test and easy to reuse."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "53ede439",
   "metadata": {},
   "outputs": [],
   "source": [
    "def add_item(inventory, name, qty, price):\n",
    "    \"\"\"Return the inventory with this item added, or updated if it's already there.\"\"\"\n",
    "    inventory[name] = {\"qty\": qty, \"price\": price}\n",
    "    return inventory\n",
    "\n",
    "def total_value(inventory):\n",
    "    \"\"\"Return the total value of everything in stock.\"\"\"\n",
    "    total = 0\n",
    "    for details in inventory.values():\n",
    "        total += details[\"qty\"] * details[\"price\"]\n",
    "    return total\n",
    "\n",
    "stock = {}\n",
    "stock = add_item(stock, \"apple\", 4, 0.50)\n",
    "stock = add_item(stock, \"bread\", 1, 2.25)\n",
    "print(total_value(stock))   # 4 * 0.50 + 1 * 2.25 = 4.25"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ad7c5246",
   "metadata": {},
   "source": [
    "## Part 2 — Printing instead of returning\n",
    "\n",
    "This is the most common bug in this lesson. Run it and read what comes back."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "751be168",
   "metadata": {},
   "outputs": [],
   "source": [
    "def count_items_buggy(inventory):\n",
    "    total = 0\n",
    "    for details in inventory.values():\n",
    "        total += details[\"qty\"]\n",
    "    print(total)          # prints, but doesn't return\n",
    "\n",
    "result = count_items_buggy(stock)\n",
    "print(\"The function gave back:\", result)     # None"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "74b77bb7",
   "metadata": {},
   "source": [
    "The function printed the number, then gave back `None`. Whoever called it gets nothing to use. In Practice F you'll fix this."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "628944d7",
   "metadata": {},
   "source": [
    "## Practice A: low stock (inventory)\n",
    "\n",
    "Write `low_stock(inventory, limit)`. Return a list of the item names whose `qty` is **less than** `limit`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e15d3c09",
   "metadata": {},
   "outputs": [],
   "source": [
    "def low_stock(inventory, limit):\n",
    "    \"\"\"Return a list of item names whose qty is less than limit.\"\"\"\n",
    "    # TODO: write this function\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1475966f",
   "metadata": {},
   "outputs": [],
   "source": [
    "assert sorted(low_stock(stock, 2)) == [\"bread\"], \"only bread has fewer than 2\"\n",
    "assert sorted(low_stock(stock, 5)) == [\"apple\", \"bread\"], \"both are under 5\"\n",
    "assert low_stock({}, 5) == [], \"an empty inventory has no low stock\"\n",
    "print(\"✅ Practice A done\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5418a2a1",
   "metadata": {},
   "source": [
    "## Practice B: a student's average (gradebook)\n",
    "\n",
    "`subjects` is a dictionary of subject to a list of marks. Write `student_average(subjects)` that returns the average of **all** the marks, across every subject."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "655b29f8",
   "metadata": {},
   "outputs": [],
   "source": [
    "def student_average(subjects):\n",
    "    \"\"\"subjects is like {\"math\": [88, 92], \"science\": [75]}. Return the average of ALL marks.\"\"\"\n",
    "    # TODO: write this function\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a1b97e28",
   "metadata": {},
   "outputs": [],
   "source": [
    "assert abs(student_average({\"math\": [80, 100], \"science\": [60]}) - 80) < 0.001, \"(80 + 100 + 60) / 3 is 80\"\n",
    "print(\"✅ Practice B done\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6a1bda30",
   "metadata": {},
   "source": [
    "## Practice C: a library that checks books out (two functions that work together)\n",
    "\n",
    "Write two functions:\n",
    "\n",
    "- `is_available(library, title)`: `True` if the book is in the library and **not** checked out.\n",
    "- `check_out(library, title)`: if the book is available, mark it checked out. Return the library either way. Use `is_available` inside it.\n",
    "\n",
    "Each book is a dictionary like `{\"out\": False}`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6f0eb106",
   "metadata": {},
   "outputs": [],
   "source": [
    "def is_available(library, title):\n",
    "    \"\"\"True if the book is in the library and isn't checked out.\"\"\"\n",
    "    # TODO\n",
    "    pass\n",
    "\n",
    "def check_out(library, title):\n",
    "    \"\"\"Mark the book as checked out, if it's available. Return the library.\"\"\"\n",
    "    # TODO: use is_available inside this function\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "beac2215",
   "metadata": {},
   "outputs": [],
   "source": [
    "lib = {\"Dune\": {\"out\": False}, \"Emma\": {\"out\": True}}\n",
    "assert is_available(lib, \"Dune\") is True, \"Dune is on the shelf\"\n",
    "assert is_available(lib, \"Emma\") is False, \"Emma is checked out\"\n",
    "assert is_available(lib, \"Nope\") is False, \"a book that isn't there is not available\"\n",
    "check_out(lib, \"Dune\")\n",
    "assert lib[\"Dune\"][\"out\"] is True, \"Dune should now be checked out\"\n",
    "check_out(lib, \"Emma\")\n",
    "assert lib[\"Emma\"][\"out\"] is True, \"Emma should still be checked out\"\n",
    "print(\"✅ Practice C done\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a80d1d42",
   "metadata": {},
   "source": [
    "## Practice D: a budget by category\n",
    "\n",
    "Each transaction is a dictionary with a `category` and an `amount`. Write `category_total(transactions, category)` that adds up the amounts for one category."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d5e35fcd",
   "metadata": {},
   "outputs": [],
   "source": [
    "def category_total(transactions, category):\n",
    "    \"\"\"Add up the amounts for one category, for example 'food'. Return the total.\"\"\"\n",
    "    # TODO: write this function\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "92bb0a9f",
   "metadata": {},
   "outputs": [],
   "source": [
    "transactions = [\n",
    "    {\"category\": \"food\", \"amount\": 12.5},\n",
    "    {\"category\": \"bus\", \"amount\": 3},\n",
    "    {\"category\": \"food\", \"amount\": 7.5},\n",
    "]\n",
    "assert category_total(transactions, \"food\") == 20, \"12.5 + 7.5 is 20\"\n",
    "assert category_total(transactions, \"games\") == 0, \"no games transactions means 0\"\n",
    "print(\"✅ Practice D done\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ae3003ff",
   "metadata": {},
   "source": [
    "## Practice E: the top scores (game leaderboard)\n",
    "\n",
    "`scores` is a dictionary of name to score. Write `top_n(scores, n)` that returns a list of `(name, score)` pairs for the **top n**, highest first.\n",
    "\n",
    "Hint: `sorted()` takes `key=` to say what to sort by, and `reverse=True` for highest first."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "98344c37",
   "metadata": {},
   "outputs": [],
   "source": [
    "def top_n(scores, n):\n",
    "    \"\"\"scores is a dict of name: score. Return (name, score) pairs for the top n, highest first.\"\"\"\n",
    "    # TODO: write this function\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "eda08413",
   "metadata": {},
   "outputs": [],
   "source": [
    "assert top_n({\"Ana\": 50, \"Ben\": 90, \"Cara\": 70}, 2) == [(\"Ben\", 90), (\"Cara\", 70)], \"top 2 are Ben and Cara\"\n",
    "assert top_n({\"Ana\": 50}, 5) == [(\"Ana\", 50)], \"asking for more than there are should just return them all\"\n",
    "print(\"✅ Practice E done\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0d3bf6d7",
   "metadata": {},
   "source": [
    "## Practice F: fix the printing function\n",
    "\n",
    "Rewrite `count_items` so it **returns** the total quantity of all items. (Compare with `count_items_buggy` from Part 2.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7129e9a1",
   "metadata": {},
   "outputs": [],
   "source": [
    "def count_items(inventory):\n",
    "    \"\"\"Return the total quantity of all items.\"\"\"\n",
    "    # TODO: write this function. Return the total, don't print it.\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f70a0bb4",
   "metadata": {},
   "outputs": [],
   "source": [
    "assert count_items(stock) == 5, \"apple 4 + bread 1 is 5\"\n",
    "assert count_items({}) == 0, \"an empty inventory has 0 items\"\n",
    "print(\"✅ Practice F done\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c8b005c8",
   "metadata": {},
   "source": [
    "## Practice G: write a test for a function\n",
    "\n",
    "Use the pattern from 3.3. Write `test_top_n` that checks `top_n({\"Ana\": 50, \"Ben\": 90}, 1)` returns `[(\"Ben\", 90)]`. Print PASS if it does."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3a3b9af8",
   "metadata": {},
   "outputs": [],
   "source": [
    "def test_top_n():\n",
    "    # TODO: assert top_n({\"Ana\": 50, \"Ben\": 90}, 1) == [(\"Ben\", 90)], then print \"PASS: top_n\"\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b34acbe3",
   "metadata": {},
   "outputs": [],
   "source": [
    "assert printed_pass(test_top_n), \"your test should print PASS when top_n is right\"\n",
    "print(\"✅ Practice G done\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2080eb0c",
   "metadata": {},
   "source": [
    "## Part 3 — Which one?\n",
    "\n",
    "Replace the `None` with `\"A\"`, `\"B\"` or `\"C\"`.\n",
    "\n",
    "**Which function is doing too much?** A) `total_value`  B) `add_and_save`  C) `count_items`\n",
    "\n",
    "A name with \"and\" in it is a warning sign."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ffb7e59b",
   "metadata": {},
   "outputs": [],
   "source": [
    "q1 = None   # \"A\", \"B\" or \"C\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b352d93b",
   "metadata": {},
   "outputs": [],
   "source": [
    "assert q1 == \"B\", \"add_and_save does two jobs: change the data and save it. Split it into two functions.\"\n",
    "print(\"✅ Part 3 done\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c51281e2",
   "metadata": {},
   "source": [
    "## You're done with this lesson when…\n",
    "\n",
    "- [ ] My functions take data in and return data out\n",
    "- [ ] I never forget `return`\n",
    "- [ ] I can call a function on its own, with test data, and see the result\n",
    "- [ ] I can write a test for one of my functions\n",
    "- [ ] I can spot a function that's doing too much"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
