{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "l09-c000",
   "metadata": {},
   "source": [
    "# Lecture 9 - Code quality 1: Testing and documentation\n",
    "\n",
    "**Authors:** Fiona McNeill, Matteo Degiacomi, Rayo Verweij, and Nigel Topham\n",
    "\n",
    "## Introduction\n",
    "\n",
    "Over the course of Part 1, we've assembled a basic toolkit for building programs. But writing code is not the entire story: we need to test and verify that our code works correctly, too. This notebook introduces best practices and techniques for writing good code and ensuring its quality.\n",
    "\n",
    "### Learning outcomes\n",
    "\n",
    "In this notebook, we'll discuss:\n",
    "\n",
    "- applying decomposition and DRY to give functions clear, reusable jobs;\n",
    "- explaining test-driven development and the red → green → refactor cycle; and\n",
    "- writing `assert` checks to automatically verify program functionality.\n",
    "\n",
    "### Using the notebook\n",
    "\n",
    "Remember, this notebook is _yours_! You can edit each cell, for example by adding new examples or changing values to see what happens. Feel free to experiment!\n",
    "\n",
    "A common approach for learning how to read and write code in a structured way is called **PRIMM**:\n",
    "1. **Predict**: before running the code, look at it - do you understand each line? What do you think will happen?\n",
    "2. **Run**: now run the code and see what output you get.\n",
    "3. **Investigate**: did it go as you thought it would? If not, can you figure out why?\n",
    "4. **Modify**: now change the code and once again try to predict the output. If you got it wrong the first time, did you get it right now?\n",
    "5. **Make**: finally, try to write your own program from scratch using the skills you learned.\n",
    "\n",
    "You can apply steps 1-4 to each code cell in the notebook and many will have prediction prompts to help you get started. In addition, there will be challenges that ask you to put everything together yourself and execute step 5. There will often be multiple ways to solve these, but each challenge has a potential answer listed.\n",
    "\n",
    "### Navigating the notebook\n",
    "\n",
    "A Jupyter notebook can be operated with your keyboard. Here is a **cheat sheet**:\n",
    "\n",
    "- To run the selected cell and move to the next cell, press <kbd>&#x21E7; Shift</kbd> + <kbd>&#x23ce; Enter</kbd>.\n",
    "- To run the selected cell and keep focus there, press <kbd>⌃ Ctrl</kbd> + <kbd>&#x23ce; Enter</kbd>.\n",
    "- To create a cell under the one currently highlighted, press <kbd>B</kbd>;\n",
    "- To delete the currently highlighted cell, press <kbd>D</kbd> twice (be careful with this one!) or press <kbd>X</kbd> to cut it;\n",
    "- To get help for a specific function, place the cursor within the function's brackets, hold <kbd>&#x21E7; Shift</kbd>, and press <kbd>&#x21E5; Tab</kbd>.\n",
    "\n",
    "Watch out: **code cells remember what happened in cells before**. So, especially in the more complicated notebooks later down the line, make sure to always run **every** cell from top to bottom, as one might rely on a piece of code that came before it! If your results become confusing, use **Kernel > Restart Kernel** and then rerun all the completed cells in order.\n",
    "\n",
    "### Further reading and practice\n",
    "\n",
    "- Official Python documentation: [The `assert` statement](https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement).\n",
    "- Python's [style guide](https://peps.python.org/pep-0008/#naming-conventions): see the naming conventions for classes, functions, and variables.\n",
    "- W3Schools: [`assert` keyword](https://www.w3schools.com/python/ref_keyword_assert.asp).\n",
    "- Helsinki Python MOOC: [Common error messages](https://programming-26.mooc.fi/error_messages).\n",
    "- Martin Fowler: [Test Driven Development](https://martinfowler.com/bliki/TestDrivenDevelopment.html) - a quick article that introduces the philosophy."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l09-c001",
   "metadata": {},
   "source": [
    "## 1. Coding best practices\n",
    "\n",
    "Good code helps its reader see what it is trying to do, and makes mistakes easier to isolate. Especially with the rise of generative AI tools, the amount of **technical debt** the world is accumulating is immense, and following best practices from the start will make your life, and that of your future collaborators, a lot easier.\n",
    "\n",
    "We started our discussion of best practices as early as Lecture 1, and have been building on that ever since:\n",
    "\n",
    "- **Plan** the steps of your algorithm before coding using **pseudocode**\n",
    "- **Decompose** problems into smaller, individual chunks\n",
    "- Give your variables clear, **descriptive names**\n",
    "  - Use capital letters for class names to distinguish them easily\n",
    "- Add **comments** to your code to explain parts that are not immediately obvious\n",
    "  - Use triple-quoted docstrings to add documentation to functions and methods\n",
    "- Use **consistent indentation**, with either tabs or four spaces\n",
    "- When choosing **collections**, use lists for sequences, sets for unique items, and dictionaries for lookups\n",
    "\n",
    "### On decomposition\n",
    "\n",
    "It's worth briefly revisiting **decomposition**, now that you know how to create your own functions. Breaking problems into smaller, individual chunks has multiple benefits:\n",
    "\n",
    "- *Easier planning:* difficult tasks become more manageable when you tackle the subproblems one at a time;\n",
    "- *Clearer code:* individual chunks are easier to read than long walls of text;\n",
    "- *Simpler testing:* individual functions are easier to test than functions that do many things at once; and\n",
    "- *Increased reusability:* for when later code requires some functionality that you've already written.\n",
    "\n",
    "Take a look at the following program:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l09-c002",
   "metadata": {},
   "outputs": [],
   "source": [
    "def mean(numbers):\n",
    "    return sum(numbers) / len(numbers)\n",
    "\n",
    "def format_report(numbers):\n",
    "    return f\"Count: {len(numbers)}; mean: {mean(numbers):.1f}\"\n",
    "\n",
    "def show_report(numbers):\n",
    "    print(format_report(numbers))\n",
    "\n",
    "marks = [55, 63, 71, 80]\n",
    "show_report(marks)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l09-c003",
   "metadata": {},
   "source": [
    "You instantly get a sense of what each part of the program does, each function is easy to test on its own, and reusing only certain parts, such as calculating the mean, has become easy-peasy.\n",
    "\n",
    "### DRY: don't repeat yourself\n",
    "\n",
    "On a related note, a phrase you'll hear programmers repeat a lot is “Don't Repeat Yourself” or **DRY**.\n",
    "\n",
    "Why is this important?\n",
    "\n",
    "**Predict:** A shop currently gives 10% off both items. If it changes to 15%, which lines need editing? What happens if we change only one?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l09-c004",
   "metadata": {},
   "outputs": [],
   "source": [
    "tea_price = 20\n",
    "coffee_price = 35\n",
    "tea_offer = tea_price * (1 - 0.10)\n",
    "coffee_offer = coffee_price * (1 - 0.10)\n",
    "print(tea_offer, coffee_offer)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l09-c005",
   "metadata": {},
   "source": [
    "If we have certain rules that we repeat multiple times in our code, a change in the rule means we have to remember every place we used it and update it. That's easy to forget!\n",
    "\n",
    "**Refactoring** is the process of improving the structure of code without changing its functionality. We can refactor the discount rule in the piece of code above into a function, keeping the functionality in one place:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l09-c006",
   "metadata": {},
   "outputs": [],
   "source": [
    "def discounted_price(price, discount_rate=0.10):\n",
    "    return price * (1 - discount_rate)\n",
    "\n",
    "print(discounted_price(20), discounted_price(35))\n",
    "print(discounted_price(20, discount_rate=0.15))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l09-c007",
   "metadata": {},
   "source": [
    "Now, we are easily able to apply our default discount, update the default discount if necessary, and even apply different discounts all with one function.\n",
    "\n",
    "Decomposition and DRY work together neatly: decomposition is about dividing responsibilities and DRY is about avoiding multiple copies of the same knowledge. When writing functions, together they point towards the same adage: **give your functions one clear focus**.\n",
    "\n",
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: refactor a temperature report</b>\n",
    "\n",
    "The code below converts temperatures, formats them, and displays a report, all in one function!\n",
    "\n",
    "Run it first and note its output. Then use decomposition and DRY to refactor it into several smaller functions, and check that your refactored version displays exactly the same report for `[0, 20, 100]`.\n",
    "</div>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l09-c008",
   "metadata": {},
   "outputs": [],
   "source": [
    "def show_temperature_report(celsius_readings):\n",
    "    for celsius in celsius_readings:\n",
    "        fahrenheit = celsius * 9 / 5 + 32\n",
    "        print(f\"{celsius:.1f}°C = {fahrenheit:.1f}°F\")\n",
    "\n",
    "    mean_celsius = sum(celsius_readings) / len(celsius_readings)\n",
    "    mean_fahrenheit = mean_celsius * 9 / 5 + 32\n",
    "    print(f\"Mean: {mean_celsius:.1f}°C = {mean_fahrenheit:.1f}°F\")\n",
    "\n",
    "\n",
    "show_temperature_report([0, 20, 100])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l09-c009",
   "metadata": {},
   "source": [
    "<details><summary><b>Click for a possible solution</b></summary>\n",
    "\n",
    "```python\n",
    "def mean(numbers):\n",
    "    return sum(numbers) / len(numbers)\n",
    "\n",
    "def celsius_to_fahrenheit(celsius):\n",
    "    return celsius * 9 / 5 + 32\n",
    "\n",
    "def format_temperature(celsius):\n",
    "    fahrenheit = celsius_to_fahrenheit(celsius)\n",
    "    return f\"{celsius:.1f}°C = {fahrenheit:.1f}°F\"\n",
    "\n",
    "def show_temperature_report(celsius_readings):\n",
    "    for celsius in celsius_readings:\n",
    "        print(format_temperature(celsius))\n",
    "    mean_celsius = mean(celsius_readings)\n",
    "    print(\"Mean:\", format_temperature(mean_celsius))\n",
    "\n",
    "\n",
    "show_temperature_report([0, 20, 100])\n",
    "```\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l09-c010",
   "metadata": {},
   "source": [
    "## 2. Automated testing\n",
    "\n",
    "How do we know whether code does what we intend it to do? First and foremost, `print()` is your friend: you can print anything and everything to check that code works as you expect it to.\n",
    "\n",
    "For larger programs, however, you might not want to have dozens (or even hundreds) of print statements clogging up your console. Thankfully, we can write **automated tests**, in which you describe in your code what output you expect for a particular input, and your program automatically checks it.\n",
    "\n",
    "### Writing test cases with `assert`\n",
    "\n",
    "Writing `assert` asks Python to check a Boolean condition. When it is `True`, the execution of your program continues silently. However, when it is `False`, Python stops the program with an **`AssertionError`**. An optional message after a comma explains the expectation.\n",
    "\n",
    "**Predict:** How many lines will the following program print?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l09-c011",
   "metadata": {},
   "outputs": [],
   "source": [
    "assert 2 + 3 == 5\n",
    "print(\"One assertion has passed\")\n",
    "\n",
    "assert \"  Ada  \".strip() == \"Ada\"\n",
    "print(\"Two assertions have passed\")\n",
    "\n",
    "assert 2 + 3 == 6\n",
    "print(\"Three assertions have passed\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e6ebf38b",
   "metadata": {},
   "source": [
    "Optionally, you can add a comma and a string after the Boolean expression in the `assert` statement to provide context to the `AssertionError`. The message should describe what you expected, so that whoever reads the error knows what went wrong.\n",
    "\n",
    "**Predict:** The code below is meant to convert a string to uppercase. Can you spot the mistake before running it?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e6a50aac",
   "metadata": {},
   "outputs": [],
   "source": [
    "shouted = \"Informatics\".lower()\n",
    "assert shouted == \"INFORMATICS\", \"The text should be converted to uppercase\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l09-c012",
   "metadata": {},
   "source": [
    "Note: `assert` is provided as a way to aid development, but it is possible for Python to disable assertion checking! Don't make the logic of your code depend on `assert` checks. Only use them to test that things work as expected.\n",
    "\n",
    "### Test-driven development\n",
    "\n",
    "**Test-driven development (TDD)** is a coding philosophy that prioritises writing automated tests. When following TDD, you decompose your task into small pieces of behaviour and describe each of them with a test *before* implementing it. Writing the test, then, makes you decide how to call the function and what it should return.\n",
    "\n",
    "TDD typically follows a three-step cycle:\n",
    "\n",
    "1. **Red:** write and run a test that fails because the required behaviour is still missing or incorrect.\n",
    "2. **Green:** write enough code to make that test pass, then rerun all existing tests.\n",
    "3. **Refactor:** if needed, improve your code's structure while preserving its behaviour, and rerun the tests.\n",
    "\n",
    "You would repeat this cycle for each small requirement in the code. Let's see what this looks like in practice.\n",
    "\n",
    "We want a function `clamp_score(score)` that returns `0` below `0`, `100` above `100`, and the score itself otherwise. \n",
    "\n",
    "We'll put a placeholder function in and, following TDD, start with a test. Let's start with one ordinary score, `50`.\n",
    "\n",
    "**Red:** The placeholder returns `None`, so the test will fail."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l09-c013",
   "metadata": {},
   "outputs": [],
   "source": [
    "def clamp_score(score):\n",
    "    pass\n",
    "\n",
    "assert clamp_score(50) == 50, \"A number between 0 and 100 should stay unchanged\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l09-c014",
   "metadata": {},
   "source": [
    "**Green:** Returning the score is enough to pass this first requirement."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l09-c015",
   "metadata": {},
   "outputs": [],
   "source": [
    "def clamp_score(score):\n",
    "    return score\n",
    "\n",
    "assert clamp_score(50) == 50, \"A number between 0 and 100 should stay unchanged\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l09-c016",
   "metadata": {},
   "source": [
    "Now let's choose the next requirement: scores below zero should become zero and scores above 100 should become 100.\n",
    "\n",
    "**Red:** Remember that programs stop at the first assertion that fails, so after running the code below once, comment out the assertion for `-1` to see the assertion for `101` fail as well."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l09-c017",
   "metadata": {},
   "outputs": [],
   "source": [
    "def clamp_score(score):\n",
    "    return score\n",
    "\n",
    "assert clamp_score(50) == 50, \"A number between 0 and 100 should stay unchanged\"\n",
    "assert clamp_score(-1) == 0, \"Negative scores should become zero\"\n",
    "assert clamp_score(101) == 100, \"Scores above 100 should become 100\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l09-c018",
   "metadata": {},
   "source": [
    "**Green:** Let's add the missing branches. Remember from Lecture 3 that we can test branches in `if` statements with **boundary tests**, so let's automate them and add checks exactly at `0` and `100`, too."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l09-c019",
   "metadata": {},
   "outputs": [],
   "source": [
    "def clamp_score(score):\n",
    "    if score < 0:\n",
    "        result = 0\n",
    "    else:\n",
    "        if score > 100:\n",
    "            result = 100\n",
    "        else:\n",
    "            result = score\n",
    "    return result\n",
    "\n",
    "assert clamp_score(50) == 50, \"A number between 0 and 100 should stay unchanged\"\n",
    "assert clamp_score(-1) == 0, \"Negative scores should become zero\"\n",
    "assert clamp_score(101) == 100, \"Scores above 100 should become 100\"\n",
    "assert clamp_score(0) == 0, \"A number between 0 and 100 should stay unchanged\"\n",
    "assert clamp_score(100) == 100, \"A number between 0 and 100 should stay unchanged\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l09-c020",
   "metadata": {},
   "source": [
    "**Refactor:** The tests pass, but we can still clean up the `if` statement somewhat to reduce the nesting. We should also add a docstring that describes the function for us.\n",
    "\n",
    "Rerun the tests to check that the behaviour stays the same."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l09-c021",
   "metadata": {},
   "outputs": [],
   "source": [
    "def clamp_score(score):\n",
    "    \"\"\"Returns 0 below 0, 100 above 100, and the score itself otherwise.\"\"\"\n",
    "    if score < 0:\n",
    "        return 0\n",
    "    if score > 100:\n",
    "        return 100\n",
    "    return score\n",
    "\n",
    "assert clamp_score(50) == 50, \"A number between 0 and 100 should stay unchanged\"\n",
    "assert clamp_score(-1) == 0, \"Negative scores should become zero\"\n",
    "assert clamp_score(101) == 100, \"Scores above 100 should become 100\"\n",
    "assert clamp_score(0) == 0, \"A number between 0 and 100 should stay unchanged\"\n",
    "assert clamp_score(100) == 100, \"A number between 0 and 100 should stay unchanged\"\n",
    "print(\"All five score checks passed!\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l09-c022",
   "metadata": {},
   "source": [
    "### Choosing useful tests\n",
    "\n",
    "The above example was a tad excessive - you don't have to write a test for every single piece of code you write. Do write them for pieces of code that are unintuitive to you, however. For example, writing tests for mutable collections can be valuable so you do not accidentally update any you don't mean to.\n",
    "\n",
    "It's also good practice to keep a test for every bug you fix. These are called **regression tests**, and can reveal if a later edit accidentally brought the bug back.\n",
    "\n",
    "**Predict:** What do the assertions below check that a single returned-number test would miss?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l09-c023",
   "metadata": {},
   "outputs": [],
   "source": [
    "class PracticeLog:\n",
    "    def __init__(self):\n",
    "        self.minutes = []\n",
    "\n",
    "    def add_session(self, minutes):\n",
    "        self.minutes.append(minutes)\n",
    "\n",
    "\n",
    "first_log = PracticeLog()\n",
    "second_log = PracticeLog()\n",
    "first_log.add_session(20)\n",
    "first_log.add_session(10)\n",
    "assert first_log.minutes == [20, 10]\n",
    "assert second_log.minutes == []"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l09-c024",
   "metadata": {},
   "source": [
    "These tests check accumulation and independence, as each instance of `PracticeLog` needs its own list. Had `minutes = []` been a class attribute, all logs would have shared the same list, and these tests would have caught that.\n",
    "\n",
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: test first</b>\n",
    "\n",
    "Write `temperature_band(value)`: return `\"cold\"` below 5°C, `\"mild\"` from 5°C up to but excluding 20°C, and `\"warm\"` from 20°C.\n",
    "\n",
    "Start with a placeholder and an assertion for `4`. Observe red, implement that case, and observe green. Add requirements one at a time until assertions for `4`, `5`, `6`, `19`, `20`, and `21` all pass. Finally, refactor and document your code.\n",
    "</div>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l09-c025",
   "metadata": {},
   "outputs": [],
   "source": [
    "def temperature_band(value):\n",
    "    pass\n",
    "\n",
    "\n",
    "# Write your first assertion before replacing the placeholder!\n",
    "# Add the remaining checks as you work through the TDD cycle\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l09-c026",
   "metadata": {},
   "source": [
    "<details><summary><b>Click for a possible final implementation</b></summary>\n",
    "\n",
    "```python\n",
    "def temperature_band(value):\n",
    "    \"\"\"Returns 'cold' for value < 5, 'mild' for 5 <= value < 20, and 'warm' otherwise\"\"\"\n",
    "    if value < 5:\n",
    "        return \"cold\"\n",
    "    if value < 20:\n",
    "        return \"mild\"\n",
    "    return \"warm\"\n",
    "\n",
    "assert temperature_band(4) == \"cold\"\n",
    "assert temperature_band(5) == \"mild\"\n",
    "assert temperature_band(6) == \"mild\"\n",
    "assert temperature_band(19) == \"mild\"\n",
    "assert temperature_band(20) == \"warm\"\n",
    "assert temperature_band(21) == \"warm\"\n",
    "```\n",
    "\n",
    "The boundary tests at `5` and `20` distinguish `<` from `<=`.\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l09-c032",
   "metadata": {},
   "source": [
    "## Consolidation challenge\n",
    "\n",
    "You've done it. Well done! You've now seen all content that's required to (just barely) pass the course. Let's put that to the test _properly_, shall we?\n",
    "\n",
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: rescue the study-session tracker</b>\n",
    "\n",
    "The following program is intended to track students' study sessions. It contains **four logic mistakes**, unhelpful names, and a bunch of repeated formatting. Your task is to untangle, test, and fix it; improve its structure and names; and document the resulting code using comments and docstrings.\n",
    "\n",
    "This is what the code is _supposed_ to do:\n",
    "\n",
    "- Each student has an independent study tracker.\n",
    "- Outer whitespace should be stripped from students' names.\n",
    "- There are three activities: `reading`, `coding`, and `quiz`, earning **1**, **2**, and **3** points per minute respectively. Activity names should ignore case and outer whitespace.\n",
    "- Duration text represents a valid whole number of minutes. Ignore unknown activities and durations of zero or less, returning `False` without changing the tracker. An accepted session returns `True`.\n",
    "- Keep the accepted activities in a list, including repeats, and the distinct activities in a set. Accumulate minutes and points across **all** accepted sessions.\n",
    "- The status is `\"target met\"` at **60 minutes or more**, otherwise `\"keep going\"`.\n",
    "- The builder processes every paired activity and duration without changing the supplied lists. Assume the two lists have equal lengths.\n",
    "- The group summary totals minutes, points, and accepted sessions, and gives the set of activities across the students. An empty group has zero totals and an empty set.\n",
    "\n",
    "Yes, it looks like a lot. But take a breath, find a friend if you'd like, and you will be able to do it :)\n",
    "</div>\n",
    "\n",
    "For the example data, the expected results are:\n",
    "\n",
    "| Student | Accepted sessions | Minutes | Points | Status |\n",
    "| --- | --- | --- | --- | --- |\n",
    "| Aisha | 3 | 60 | 110 | target met |\n",
    "| Ben | 2 | 20 | 45 | keep going |\n",
    "| Group | 5 | 80 | 155 | — |\n",
    "\n",
    "Aisha's line should be `Aisha: 3 sessions, 60 minutes, 110 points (target met)`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l09-c033",
   "metadata": {},
   "outputs": [],
   "source": [
    "class X:\n",
    "    def __init__(self, a):\n",
    "        self.a = a\n",
    "        self.a.strip()\n",
    "        self.b = []\n",
    "        self.c = set()\n",
    "        self.d = 0\n",
    "        self.e = 0\n",
    "\n",
    "    def f(self, g, h):\n",
    "        z = {\"reading\": 1, \"coding\": 2, \"quiz\": 3}\n",
    "        g = g.strip().lower()\n",
    "        h = int(h)\n",
    "        if g not in z or h <= 0:\n",
    "            return False\n",
    "        self.b.append(g)\n",
    "        self.c.add(g)\n",
    "        self.d += h\n",
    "        self.e = h * z[g]\n",
    "        return True\n",
    "\n",
    "    def j(self):\n",
    "        if self.d > 60:\n",
    "            return \"target met\"\n",
    "        return \"keep going\"\n",
    "\n",
    "    def k(self):\n",
    "        if self.j() == \"target met\":\n",
    "            return f\"{self.a}: {len(self.b)} sessions, {self.d} minutes, {self.e} points (target met)\"\n",
    "        return f\"{self.a}: {len(self.b)} sessions, {self.d} minutes, {self.e} points (keep going)\"\n",
    "\n",
    "    def __str__(self):\n",
    "        return self.k()\n",
    "\n",
    "def m(a, b, c):\n",
    "    q = X(a)\n",
    "    for i in range(len(b) - 1):\n",
    "        q.f(b[i], c[i])\n",
    "    return q\n",
    "\n",
    "def n(a):\n",
    "    b = 0\n",
    "    c = 0\n",
    "    d = 0\n",
    "    e = set()\n",
    "    for f in a:\n",
    "        b += f.d\n",
    "        c += f.e\n",
    "        d += len(f.b)\n",
    "        for g in f.c:\n",
    "            e.add(g)\n",
    "    return {\"minutes\": b, \"points\": c, \"sessions\": d, \"activities\": e}\n",
    "\n",
    "def p(a):\n",
    "    for b in a:\n",
    "        print(b)\n",
    "    c = n(a)\n",
    "    print(f\"Group: {c['sessions']} sessions, {c['minutes']} minutes, {c['points']} points\")\n",
    "    print(\"Activities:\", c[\"activities\"])\n",
    "\n",
    "r = [\"reading\", \" CODING \", \"quiz\"]\n",
    "s = [\"20\", \"30\", \"10\"]\n",
    "t = [\"coding\", \"unknown\", \"reading\", \"quiz\"]\n",
    "u = [\"15\", \"8\", \"0\", \"5\"]\n",
    "v = m(\"  Aisha  \", r, s)\n",
    "w = m(\"Ben\", t, u)\n",
    "y = [v, w]\n",
    "p(y)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l09-c036",
   "metadata": {},
   "source": [
    "<details><summary><b>Click for a possible repair, refactoring, and test set</b></summary>\n",
    "\n",
    "The four logic errors are ignoring the string returned by `strip()`, replacing the points total on each session, excluding exactly 60 minutes from the target, and omitting the last session in the builder.\n",
    "\n",
    "```python\n",
    "class StudyLog:\n",
    "    \"\"\"Track a student's accepted activities, minutes, and points.\"\"\"\n",
    "\n",
    "    def __init__(self, name):\n",
    "        \"\"\"Store the stripped name and start with no accepted sessions.\"\"\"\n",
    "        self.name = name.strip()\n",
    "        self.activities = []\n",
    "        self.unique_activities = set()\n",
    "        self.total_minutes = 0\n",
    "        self.total_points = 0\n",
    "\n",
    "    def add_session(self, activity, minutes_text):\n",
    "        \"\"\"Add a session and return whether it was accepted or not.\n",
    "        Activity names ignore case and outer whitespace. minutes_text must\n",
    "        represent a whole number.\n",
    "        \"\"\"\n",
    "        points_per_minute = {\"reading\": 1, \"coding\": 2, \"quiz\": 3}\n",
    "        activity = activity.strip().lower()\n",
    "        minutes = int(minutes_text)\n",
    "        if activity not in points_per_minute or minutes <= 0:\n",
    "            return False\n",
    "        self.activities.append(activity)\n",
    "        self.unique_activities.add(activity)\n",
    "        self.total_minutes += minutes\n",
    "        self.total_points += minutes * points_per_minute[activity]\n",
    "        return True\n",
    "\n",
    "    def target_status(self):\n",
    "        \"\"\"Return the status for a 60-minute study target.\"\"\"\n",
    "        if self.total_minutes >= 60:\n",
    "            return \"target met\"\n",
    "        return \"keep going\"\n",
    "\n",
    "    def format_report(self):\n",
    "        \"\"\"Return a one-line report.\"\"\"\n",
    "        report = f\"{self.name}: {len(self.activities)} sessions, \"\n",
    "        report += f\"{self.total_minutes} minutes, {self.total_points} points \"\n",
    "        return report + f\"({self.target_status()})\"\n",
    "\n",
    "    def __str__(self):\n",
    "        \"\"\"Return the same report when this object is printed.\"\"\"\n",
    "        return self.format_report()\n",
    "\n",
    "\n",
    "def build_log(name, activities, durations):\n",
    "    \"\"\"Return a new log from activity and duration lists.\n",
    "    Assume both lists have the same length.\"\"\"\n",
    "    log = StudyLog(name)\n",
    "    for index in range(len(activities)):\n",
    "        log.add_session(activities[index], durations[index])\n",
    "    return log\n",
    "\n",
    "\n",
    "def summarise_logs(logs):\n",
    "    \"\"\"Return group totals and distinct activities without changing the logs.\"\"\"\n",
    "    minutes = 0\n",
    "    points = 0\n",
    "    sessions = 0\n",
    "    activities = set()\n",
    "    for log in logs:\n",
    "        minutes += log.total_minutes\n",
    "        points += log.total_points\n",
    "        sessions += len(log.activities)\n",
    "        for activity in log.unique_activities:\n",
    "            activities.add(activity)\n",
    "    return {\"minutes\": minutes, \"points\": points,\n",
    "            \"sessions\": sessions, \"activities\": activities}\n",
    "\n",
    "\n",
    "def show_logs(logs):\n",
    "    \"\"\"Display each student's report and the group summary.\"\"\"\n",
    "    for log in logs:\n",
    "        print(log)\n",
    "    summary = summarise_logs(logs)\n",
    "    print(f\"Group: {summary['sessions']} sessions, {summary['minutes']} minutes, {summary['points']} points\")\n",
    "    print(\"Activities:\", summary[\"activities\"])\n",
    "\n",
    "\n",
    "activities = [\"reading\", \" CODING \", \"quiz\"]\n",
    "durations = [\"20\", \"30\", \"10\"]\n",
    "activities_before = activities.copy()\n",
    "durations_before = durations.copy()\n",
    "aisha = build_log(\"  Aisha  \", activities, durations)\n",
    "ben = build_log(\"Ben\", [\"coding\", \"unknown\", \"reading\", \"quiz\"],\n",
    "                [\"15\", \"8\", \"0\", \"5\"])\n",
    "assert activities == activities_before\n",
    "assert durations == durations_before\n",
    "assert aisha.name == \"Aisha\"\n",
    "assert aisha.activities == [\"reading\", \"coding\", \"quiz\"]\n",
    "assert aisha.unique_activities == {\"reading\", \"coding\", \"quiz\"}\n",
    "assert aisha.total_minutes == 60\n",
    "assert aisha.total_points == 110\n",
    "assert aisha.target_status() == \"target met\"\n",
    "assert aisha.format_report() == \"Aisha: 3 sessions, 60 minutes, 110 points (target met)\"\n",
    "assert str(aisha) == aisha.format_report()\n",
    "assert ben.activities == [\"coding\", \"quiz\"]\n",
    "assert ben.total_minutes == 20\n",
    "assert ben.total_points == 45\n",
    "assert ben.target_status() == \"keep going\"\n",
    "assert summarise_logs([aisha, ben]) == {\n",
    "    \"minutes\": 80, \"points\": 155, \"sessions\": 5,\n",
    "    \"activities\": {\"reading\", \"coding\", \"quiz\"}\n",
    "}\n",
    "\n",
    "# Using two calls to test accumulation, repeated activities, and independent instances\n",
    "first = StudyLog(\"First\")\n",
    "second = StudyLog(\"Second\")\n",
    "assert first.add_session(\"coding\", \"10\")\n",
    "assert first.add_session(\" coding \", \"5\")\n",
    "assert first.activities == [\"coding\", \"coding\"]\n",
    "assert first.unique_activities == {\"coding\"}\n",
    "assert first.total_minutes == 15\n",
    "assert first.total_points == 30\n",
    "assert second.activities == []\n",
    "assert second.unique_activities == set()\n",
    "assert second.total_minutes == 0\n",
    "assert second.total_points == 0\n",
    "\n",
    "for session in [[\"unknown\", \"10\"], [\"reading\", \"0\"], [\"quiz\", \"-2\"]]:\n",
    "    assert not first.add_session(session[0], session[1])\n",
    "    assert first.activities == [\"coding\", \"coding\"]\n",
    "    assert first.unique_activities == {\"coding\"}\n",
    "    assert first.total_minutes == 15\n",
    "    assert first.total_points == 30\n",
    "\n",
    "for case in [[59, \"keep going\"], [60, \"target met\"], [61, \"target met\"]]:\n",
    "    log = build_log(\"Boundary\", [\"reading\"], [str(case[0])])\n",
    "    assert log.total_minutes == case[0]\n",
    "    assert log.total_points == case[0]\n",
    "    assert log.target_status() == case[1]\n",
    "\n",
    "empty = build_log(\"Empty\", [], [])\n",
    "assert empty.activities == []\n",
    "assert empty.unique_activities == set()\n",
    "assert empty.target_status() == \"keep going\"\n",
    "assert summarise_logs([]) == {\n",
    "    \"minutes\": 0, \"points\": 0, \"sessions\": 0, \"activities\": set()\n",
    "}\n",
    "assert summarise_logs([empty]) == summarise_logs([])\n",
    "show_logs([aisha, ben])\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l09-c037",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "You have reached the end of Part 1, well done! Here is the final list of key terms and new syntax introduced in this lecture.\n",
    "\n",
    "### Key terms\n",
    "\n",
    "<dl>\n",
    "\t<dt><b>Decomposition</b></dt>\n",
    "\t<dd>Breaking a problem into smaller parts with clear responsibilities.</dd>\n",
    "\t<dt><b>DRY</b></dt>\n",
    "\t<dd>“Don't Repeat Yourself”: keeping a shared rule in one place.</dd>\n",
    "\t<dt><b>Refactoring</b></dt>\n",
    "\t<dd>Improving code's structure while preserving its intended behaviour.</dd>\n",
    "\t<dt><b>Assertion</b></dt>\n",
    "\t<dd>A condition that is checked during execution. A false condition causes an <code>AssertionError</code>.</dd>\n",
    "\t<dt><b>Test case</b></dt>\n",
    "\t<dd>An input together with its expected result or behaviour.</dd>\n",
    "\t<dt><b>Test-driven development</b></dt>\n",
    "\t<dd>An approach to coding in which you write a failing test for a small requirement, implement it, and then improve structure while keeping the tests passing.</dd>\n",
    "\t<dt><b>Regression test</b></dt>\n",
    "\t<dd>A retained check that can catch the return of a repaired bug.</dd>\n",
    "</dl>\n",
    "\n",
    "### New syntax\n",
    "\n",
    "<dl>\n",
    "\t<dt><code>assert condition</code></dt>\n",
    "\t<dd>Continues silently if <code>condition</code> evaluates to <code>True</code> or stops the program with <code>AssertionError</code> if not.</dd>\n",
    "\t<dt><code>assert condition, \"message\"</code></dt>\n",
    "\t<dd>Includes a message with the <code>AssertionError</code> if the assertion fails.</dd>\n",
    "\t<dt><code>AssertionError</code></dt>\n",
    "\t<dd>A type of error raised when an assertion fails.</dd>\n",
    "</dl>\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
