diff --git a/control_flow.ipynb b/control_flow.ipynb index b8112267..8760162a 100644 --- a/control_flow.ipynb +++ b/control_flow.ipynb @@ -13,33 +13,48 @@ "id": "1", "metadata": {}, "source": [ - "# Table of contents\n", + "## Table of Contents\n", "- [References](#References)\n", "- [Conditionals](#Conditionals)\n", + " - [Standard `if-elif-else` structure](#Standard-if-elif-else-structure)\n", + " - [Ternary operator (one-line `if-else`)](#Ternary-operator-(one-line-if-else))\n", + " - [When to use one-line `if-else`?](#When-to-use-one-line-if-else?)\n", + " - [Some practical examples](#Some-practical-examples)\n", "- [Quiz on conditionals](#Quiz-on-conditionals)\n", "- [The `while` loop](#The-while-loop)\n", "- [The `for` loop](#The-for-loop)\n", " - [The `enumerate` built-in](#The-enumerate-built-in)\n", " - [The `range` built-in](#The-range-built-in)\n", - "- [Warm-up exercises 👈 **solve these first**](#Warm-up-exercises)\n", - "- [Nested loops](#Nested-loops)\n", + "- [Warm-up Exercises](#Warm-up-Exercises)\n", + " 1. [Write a Python program that returns the characters in a string and their indexes](#1.-Write-a-Python-program-that-returns-the-characters-in-a-string-and-their-indexes)\n", + " 2. [Write a Python program that returns all the numbers in a given range, __including__ the first and the last elements](#2.-Write-a-Python-program-that-returns-all-the-numbers-in-a-given-range,-including-the-first-and-the-last-elements)\n", + " 3. [Write a Python program that takes a list of integers and returns the square root of each of them](#3.-Write-a-Python-program-that-takes-a-list-of-integers-and-returns-the-square-root-of-each-of-them)\n", + " 4. [Write a Python program that takes an integer and divides it by 2 until the result is no longer an even number](#4.-Write-a-Python-program-that-takes-an-integer-and-divides-it-by-2-until-the-result-is-no-longer-an-even-number)\n", "- [Altering loops](#Altering-loops)\n", - " - [`if` statement inside `for`/`while`](#if-statement-inside-for/while)\n", + " - [`if` statement inside `for`/`while`](#if-statement-inside-for/while)\n", + " - [Exercise: conditionals inside loops](#Exercise:-conditionals-inside-loops)\n", + " - [`break` and `continue`](#break-and-continue)\n", " - [The `break` keyword](#The-break-keyword)\n", + " - [Examples of using `break`](#Examples-of-using-break)\n", " - [The `continue` keyword](#The-continue-keyword)\n", - " - [`else` after a `for`/`while`](#else-after-a-for/while)\n", + " - [Examples of `continue`](#Examples-of-continue)\n", + " - [Which one should I use?](#Which-one-should-I-use?)\n", + " - [Common patterns](#Common-patterns)\n", + " - [Exercise: breaking out of loops](#Exercise:-breaking-out-of-loops)\n", + " - [`else` after a `for`/`while`](#else-after-a-for/while)\n", + " - [Exercise: using `else` in loops](#Exercise:-using-else-in-loops)\n", + "- [Nested loops](#Nested-loops)\n", + " - [Understanding the performance impact](#Understanding-the-performance-impact)\n", + " - [Tips to improve performance](#Tips-to-improve-performance)\n", "- [Quiz on loops](#Quiz-on-loops)\n", "- [Exceptions](#Exceptions)\n", " - [The `try-except` block](#The-try-except-block)\n", + " - [The `raise` statement](#The-raise-statement)\n", "- [Exercises](#Exercises)\n", - " - [Find the factors 🌶️](#Find-the-factors-🌶️)\n", - " - [Find the pair 🌶️](#Find-the-pair-🌶️)\n", - " - [Part 1](#Part-1)\n", - " - [Part 2](#Part-2)\n", - " - [Cats with hats 🌶️🌶️](#Cats-with-hats-🌶️🌶️)\n", - " - [Toboggan trajectory 🌶️🌶️🌶️](#Toboggan-trajectory-🌶️🌶️🌶️)\n", - " - [Part 1 🌶️](#Part-1-🌶️)\n", - " - [Part 2 🌶️🌶️](#Part-2-🌶️🌶️)\n" + " - [Exercise 1: Find the factors](#Exercise-1:-Find-the-factors)\n", + " - [Exercise 2: Find the pair](#Exercise-2:-Find-the-pair)\n", + " - [Exercise 3: Cats with hats](#Exercise-3:-Cats-with-hats)\n", + " - [Exercise 4: Base converter](#Exercise-4:-Base-converter)" ] }, { @@ -76,9 +91,10 @@ "id": "5", "metadata": {}, "source": [ - "Python [supports](./basic_datatypes.ipynb#Comparison-operators) different comparison expressions. They return either `True` or `False`.\n", + "Python [supports](./basic_datatypes.ipynb#Comparison-operators) different comparison expressions.\n", + "They are called **logical expressions** as they evaluate to either `True` or `False`.\n", "\n", - "We use these results to evaluate **conditional statements**, and have our program behave differently, based on the result." + "We can use these results in **conditional statements**, and have our program behave differently based on the result." ] }, { @@ -86,6 +102,8 @@ "id": "6", "metadata": {}, "source": [ + "### Standard `if-elif-else` structure\n", + "\n", "The main conditional is the `if-elif-else` block:\n", "\n", "```python\n", @@ -106,10 +124,118 @@ "- You can nest conditionals, but try to avoid too many branches for both readability and performance\n" ] }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "### Ternary operator (one-line `if-else`)\n", + "\n", + "Python offers a concise way to write simple `if-else` statements in one line:\n", + "\n", + "```python\n", + "# Standard if-else\n", + "if condition:\n", + " x = 1\n", + "else:\n", + " x = 2\n", + "\n", + "# Equivalent ternary operator\n", + "x = 1 if condition else 2\n", + "```\n", + "\n", + "The syntax is: `value_if_true if condition else value_if_false`\n", + "\n", + "#### When to use one-line `if-else`?\n", + "\n", + "##### Good Use Cases:\n", + "```python\n", + "# Simple value assignment\n", + "age_category = \"adult\" if age >= 18 else \"minor\"\n", + "\n", + "# Simple function calls\n", + "result = max(x, y) if x > 0 else min(x, y)\n", + "```\n", + "\n", + "##### Bad Use Cases:\n", + "```python\n", + "# DON'T: Nested conditionals in one line (hard to read)\n", + "result = 1 if x > 0 else 2 if x < 0 else 3 # Avoid this\n", + "\n", + "# BETTER: Write it clearly\n", + "if x > 0:\n", + " result = 1\n", + "elif x < 0:\n", + " result = 2\n", + "else:\n", + " result = 3\n", + "\n", + "# DON'T: Complex expressions in ternary\n", + "value = (complex_function(x) if condition else another_complex_function(y)) # Avoid this\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "#### Some practical examples\n", + "\n", + "##### 1. Input Validation\n", + "```python\n", + "def validate_age(age):\n", + " status = \"valid\" if 0 <= age <= 120 else \"invalid\"\n", + " return status\n", + "\n", + "# Example usage\n", + "print(validate_age(25)) # Output: \"valid\"\n", + "print(validate_age(-5)) # Output: \"invalid\"\n", + "```\n", + "\n", + "##### 2. User Authentication\n", + "```python\n", + "def check_access(user_role, is_authenticated):\n", + " if not is_authenticated:\n", + " return \"Please log in\"\n", + " elif user_role == \"admin\":\n", + " return \"Full access granted\"\n", + " elif user_role == \"user\":\n", + " return \"Limited access granted\"\n", + " else:\n", + " return \"Access denied\"\n", + "```\n", + "\n", + "##### 3. Grade Calculator\n", + "```python\n", + "def get_grade(score):\n", + " if score >= 90:\n", + " return 'A'\n", + " elif score >= 80:\n", + " return 'B'\n", + " elif score >= 70:\n", + " return 'C'\n", + " elif score >= 60:\n", + " return 'D'\n", + " else:\n", + " return 'F'\n", + "\n", + "# Alternative using ternary (only for simple cases)\n", + "def pass_fail(score):\n", + " return \"Pass\" if score >= 60 else \"Fail\"\n", + "```\n", + "\n", + "Remember:\n", + "- Use ternary operators only for simple, clear conditions\n", + "- Avoid nested ternary operators\n", + "- Prioritize readability and clarity over conciseness\n", + "- If a conditional structure becomes complex, break it down into separate functions or use standard `if-elif-else` blocks" + ] + }, { "cell_type": "code", "execution_count": null, - "id": "7", + "id": "9", "metadata": { "tags": [] }, @@ -142,7 +268,7 @@ }, { "cell_type": "markdown", - "id": "8", + "id": "10", "metadata": {}, "source": [ "## Quiz on conditionals" @@ -151,7 +277,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9", + "id": "11", "metadata": {}, "outputs": [], "source": [ @@ -161,7 +287,7 @@ }, { "cell_type": "markdown", - "id": "10", + "id": "12", "metadata": { "tags": [] }, @@ -171,7 +297,7 @@ }, { "cell_type": "markdown", - "id": "11", + "id": "13", "metadata": {}, "source": [ "`while` loops repeat a block of code for as long as some condition remains true.\n", @@ -191,7 +317,7 @@ }, { "cell_type": "markdown", - "id": "12", + "id": "14", "metadata": {}, "source": [ "A very simple example:" @@ -200,7 +326,7 @@ { "cell_type": "code", "execution_count": null, - "id": "13", + "id": "15", "metadata": { "tags": [] }, @@ -216,7 +342,7 @@ }, { "cell_type": "markdown", - "id": "14", + "id": "16", "metadata": {}, "source": [ "First, `n` is initialized to 1.\n", @@ -229,13 +355,11 @@ }, { "cell_type": "markdown", - "id": "15", + "id": "17", "metadata": {}, "source": [ - "\n", - "\n", "If you are not careful, you can create an **infinite loop**: it happens when your condition is always `True`.\n", - "The above example can be easily turn into an infinite loop:\n", + "The above example can be easily (and mistakenly) turned into an infinite loop:\n", "\n", "```python\n", "n = 1\n", @@ -258,7 +382,7 @@ }, { "cell_type": "markdown", - "id": "16", + "id": "18", "metadata": { "tags": [] }, @@ -268,7 +392,7 @@ }, { "cell_type": "markdown", - "id": "17", + "id": "19", "metadata": {}, "source": [ "In Python, an **iterable** is an **object** capable of returning its members one at a time.\n", @@ -284,7 +408,7 @@ }, { "cell_type": "markdown", - "id": "18", + "id": "20", "metadata": {}, "source": [ "In other languages (C++, Java or JavaScript), a `for` loop is more similar to `while` in Python. For example, the C++ loop\n", @@ -327,7 +451,7 @@ { "cell_type": "code", "execution_count": null, - "id": "19", + "id": "21", "metadata": { "tags": [] }, @@ -339,17 +463,17 @@ }, { "cell_type": "markdown", - "id": "20", + "id": "22", "metadata": {}, "source": [ "The loops runs until there are letters in the string `'Python'`, as strings are iterable.\n", - "Doing the same with a `while` loop would be just more complex:" + "Doing the same with a `while` loop would be more complex without a reason:" ] }, { "cell_type": "code", "execution_count": null, - "id": "21", + "id": "23", "metadata": { "tags": [] }, @@ -365,7 +489,7 @@ }, { "cell_type": "markdown", - "id": "22", + "id": "24", "metadata": {}, "source": [ "Why writing six lines of code when we can do the same with just two?" @@ -373,7 +497,7 @@ }, { "cell_type": "markdown", - "id": "23", + "id": "25", "metadata": {}, "source": [ "### The `enumerate` built-in\n", @@ -386,7 +510,7 @@ { "cell_type": "code", "execution_count": null, - "id": "24", + "id": "26", "metadata": {}, "outputs": [], "source": [ @@ -396,7 +520,18 @@ }, { "cell_type": "markdown", - "id": "25", + "id": "27", + "metadata": {}, + "source": [ + "The full syntax is `enumerate(iterable, start=0)`.\n", + "By default, enumeration starts at `0` (since Python uses 0-based indexing), but you can optionally set `start=1` (or any other integer).\n", + "\n", + "**Note:** Setting `start=n` with $n \\neq 0$ **does not** change the indexing of the original iterable." + ] + }, + { + "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ "### The `range` built-in\n", @@ -416,20 +551,20 @@ }, { "cell_type": "markdown", - "id": "26", + "id": "29", "metadata": { "tags": [] }, "source": [ - "## Warm-up exercises\n", + "## Warm-up Exercises\n", "\n", - "Here are a few exercises to practice the concepts seen above.\n", + "Here are a few exercises to practice the concepts we have seen so far.\n", "\n", "
\n", "

Important

\n", " \n", "
" ] @@ -437,7 +572,7 @@ { "cell_type": "code", "execution_count": null, - "id": "27", + "id": "30", "metadata": { "tags": [] }, @@ -448,7 +583,7 @@ }, { "cell_type": "markdown", - "id": "28", + "id": "31", "metadata": {}, "source": [ "#### 1. Write a Python program that returns the characters in a string and their indexes\n", @@ -465,7 +600,7 @@ { "cell_type": "code", "execution_count": null, - "id": "29", + "id": "32", "metadata": { "tags": [] }, @@ -474,20 +609,27 @@ "%%ipytest\n", "\n", "def solution_indexed_string(string: str) -> list[tuple]:\n", - " \"\"\"\n", - " Write your solution here\n", + " \"\"\"A function that returns a list of tuples containing each character in the input string paired with its index.\n", + "\n", + " Args:\n", + " string: The input string to be processed\n", + "\n", + " Returns:\n", + " - A list of tuples where each tuple contains:\n", + " - index (int): The position of the character in the string\n", + " - char (str): The character at that position\n", " \"\"\"\n", " return" ] }, { "cell_type": "markdown", - "id": "30", + "id": "33", "metadata": { "tags": [] }, "source": [ - "#### 2. Write a Python program that returns all the numbers in a given range, __including__ the first and the last elements\n", + "#### 2. Write a Python program that returns all the numbers in a given range, including the first and the last elements\n", "\n", "
\n", "

Note

\n", @@ -498,7 +640,7 @@ { "cell_type": "code", "execution_count": null, - "id": "31", + "id": "34", "metadata": { "tags": [] }, @@ -507,15 +649,23 @@ "%%ipytest\n", "\n", "def solution_range_of_nums(start: int, end: int) -> list[int]:\n", - " \"\"\"\n", - " Write your solution here\n", + " \"\"\"Creates a list of consecutive integers from start to end, inclusive of both boundaries.\n", + " \n", + " The sequence can be either increasing or decreasing depending on the input values.\n", + "\n", + " Args:\n", + " start: The first number in the range\n", + " end: The last number in the range\n", + "\n", + " Returns:\n", + " - A list of integers containing all numbers from start to end (inclusive), in the correct order\n", " \"\"\"\n", " return" ] }, { "cell_type": "markdown", - "id": "32", + "id": "35", "metadata": { "tags": [] }, @@ -534,7 +684,7 @@ { "cell_type": "code", "execution_count": null, - "id": "33", + "id": "36", "metadata": { "tags": [] }, @@ -544,16 +694,24 @@ "\n", "import math\n", "\n", - "def solution_sqrt_of_nums(numbers: list) -> list:\n", - " \"\"\"\n", - " Write your solution here\n", + "def solution_sqrt_of_nums(numbers: list[int]) -> list[int]:\n", + " \"\"\"Calculates the square root of each number in the input list.\n", + "\n", + " Uses `math.sqrt` to compute square roots. Numbers that don't have a real square root\n", + " (negative numbers) are skipped in the output.\n", + "\n", + " Args:\n", + " numbers: A list of integers to process\n", + "\n", + " Returns:\n", + " - A list of floats containing the square root of each valid number from the input list\n", " \"\"\"\n", " return" ] }, { "cell_type": "markdown", - "id": "34", + "id": "37", "metadata": {}, "source": [ "#### 4. Write a Python program that takes an integer and divides it by 2 until the result is no longer an even number" @@ -561,7 +719,7 @@ }, { "cell_type": "markdown", - "id": "35", + "id": "38", "metadata": {}, "source": [ "
\n", @@ -573,7 +731,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "39", "metadata": { "tags": [] }, @@ -581,16 +739,24 @@ "source": [ "%%ipytest\n", "\n", - "def solution_divide_until(num: int) -> int:\n", - " \"\"\"\n", - " Write your solution here\n", + "def solution_divide_until(number: int) -> int:\n", + " \"\"\"Repeatedly divides a number by 2 until the result becomes odd.\n", + "\n", + " Starting with the input number, performs integer division by 2 repeatedly\n", + " until reaching a number that cannot be evenly divided by 2 (an odd number).\n", + "\n", + " Args:\n", + " num: The starting integer to be divided\n", + "\n", + " Returns:\n", + " - The first odd number encountered in the division sequence\n", " \"\"\"\n", " return" ] }, { "cell_type": "markdown", - "id": "37", + "id": "40", "metadata": {}, "source": [ "---" @@ -598,42 +764,7 @@ }, { "cell_type": "markdown", - "id": "38", - "metadata": { - "tags": [] - }, - "source": [ - "## Nested loops" - ] - }, - { - "cell_type": "markdown", - "id": "39", - "metadata": { - "jp-MarkdownHeadingCollapsed": true, - "tags": [] - }, - "source": [ - "You can put loops inside of other loops (of any kind). Just be careful to respect the indentation:\n", - "\n", - "```python\n", - "for n in range(1, 4):\n", - " for m in range(4, 7):\n", - " print(\"n = \", n, \" and j = \", m)\n", - "```\n", - "\n", - "The outer loop over `n` goes from 1 to 3. For each iteration, a new inner loop over `m` is started from 4 to 6. You will get **9 lines of output**, as the two range objects contain exactly 3 elements each.\n", - "\n", - "
\n", - "

Important

\n", - " Nesting loops can have dramatic consequences on your program's performance.
\n", - " The body of the loop above repeats $n \\times m$ times. If $n$, $m$, or both are large numbers, your program might take a while to finish.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "40", + "id": "41", "metadata": { "tags": [] }, @@ -643,22 +774,21 @@ }, { "cell_type": "markdown", - "id": "41", + "id": "42", "metadata": {}, "source": [ - "There are **4 ways** in which you can alter the normal execution of a loop:\n", + "There are **3 ways** in which you can alter the normal execution of a loop:\n", "\n", "1. With an `if` statement inside a `for`/`while` loop\n", - "2. With the `break` keyword: the loop stops **immediately**\n", - "3. With the `continue` keyword: the statements **after** the keyword are skipped and the next iteration is started\n", - "4. With the `else` clause after a `for`/`while` body: the `else` statement(s) are run **only** if no `break` statement is encountered in the loop body\n", + "2. With the `break` or `continue` keywords\n", + "3. With the `else` clause after a `for`/`while` body: the `else` statement(s) are run **only** if no `break` statement is encountered in the loop body\n", "\n", - "Let's see an example for each of these" + "Let's see each of these in more detail." ] }, { "cell_type": "markdown", - "id": "42", + "id": "43", "metadata": {}, "source": [ "### `if` statement inside `for`/`while`\n", @@ -669,7 +799,7 @@ { "cell_type": "code", "execution_count": null, - "id": "43", + "id": "44", "metadata": { "tags": [] }, @@ -688,94 +818,355 @@ }, { "cell_type": "markdown", - "id": "44", + "id": "45", "metadata": {}, "source": [ - "### The `break` keyword\n", + "#### Exercise: conditionals inside loops\n", + "\n", + "Complete the function `solution_filter_by_position` below that filters a list of integers, keeping only numbers larger than their position in the list (**1-based index**).\n", + "\n", + "The filtered numbers should:\n", + "\n", + "- **Not** contain duplicates\n", + "- Be in **ascending** order\n", "\n", - "In this example, the `while` loop will continue until `i` is equal to 5. At that point, the `break` statement is executed, causing the loop to terminate prematurely." + "You must use an `if-else` inside a loop.\n", + "\n", + "Example: `[1, 3, 0, 2]` should return `[3]` because:\n", + "- 1 is not greater than position 1\n", + "- 3 is greater than position 2\n", + "- 0 is not greater than position 3\n", + "- 2 is not greater than position 4" ] }, { "cell_type": "code", "execution_count": null, - "id": "45", - "metadata": { - "tags": [] - }, + "id": "46", + "metadata": {}, "outputs": [], "source": [ - "i = 0\n", - "while i < 10:\n", - " i += 1\n", - " if i == 5:\n", - " break\n", - " print(i)\n", + "%%ipytest\n", "\n", - "print(\"Loop terminated with break\")" + "def solution_filter_by_position(numbers: list[int]) -> list[int]:\n", + " \"\"\"Filters numbers that are larger than their position in the list.\n", + "\n", + " Args:\n", + " numbers: List of integers to filter\n", + "\n", + " Returns:\n", + " - A new list containing only the numbers that are greater than\n", + " their 1-based position in the input list\n", + " \"\"\"\n", + " return" ] }, { "cell_type": "markdown", - "id": "46", + "id": "47", "metadata": {}, "source": [ - "### The `continue` keyword\n", - "\n", - "In this example, the `for` loop will iterate over the numbers from 0 to 9. However, when `i` is **even**, the `continue` statement is executed, causing the loop to skip the rest of the statements in the loop and move on to the next iteration" + "---" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "47", - "metadata": { - "tags": [] - }, - "outputs": [], + "cell_type": "markdown", + "id": "48", + "metadata": {}, "source": [ - "for i in range(10):\n", - " if i % 2 == 0:\n", - " continue\n", - " print(i)\n", + "### `break` and `continue`\n", "\n", - "print(\"Loop terminated normally\")" + "Python provides these two special keywords to alter the normal flow of loops in two ways that might sound very similar.\n", + "However, they serve very different purposes." ] }, { "cell_type": "markdown", - "id": "48", + "id": "49", "metadata": {}, "source": [ - "### `else` after a `for`/`while`\n", + "#### The `break` keyword\n", "\n", - "Here the `for` loop iterates over a list of numbers. If the current `num` is equal to 4, a print statement and then `break` are executed.\n", + "The `break` statement immediately terminates the loop it's in, skipping any remaining iterations.\n", + "It's particularly useful when:\n", "\n", - "If we complete the loop without finding 4, then the `else` block is executed and a message is printed indicating that 4 was not found in the list." + "- You've found what you're looking for\n", + "- You've encountered an error condition\n", + "- You want to exit early based on some condition\n", + "\n", + "##### Examples of using `break`\n", + "\n", + "1. **Finding an element in a list:**\n", + "```python\n", + "numbers = [4, 7, 2, 9, 1, 5]\n", + "target = 9\n", + "\n", + "for num in numbers:\n", + " if num == target:\n", + " print(f\"Found {target}!\")\n", + " break\n", + " print(f\"Checking {num}...\")\n", + "```\n", + "\n", + "2. **Input validation with `while` loop:**\n", + "```python\n", + "while True:\n", + " password = input(\"Enter your password: \")\n", + " if len(password) >= 8:\n", + " print(\"Password accepted!\")\n", + " break\n", + " print(\"Password must be at least 8 characters.\")\n", + "```\n", + "\n", + "3. **Early exit from nested loops:**\n", + "```python\n", + "matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]\n", + "target = 5\n", + "\n", + "found = False\n", + "for i, row in enumerate(matrix):\n", + " for j, value in enumerate(row):\n", + " if value == target:\n", + " print(f\"Found {target} at position ({i}, {j})\")\n", + " found = True\n", + " break\n", + " if found: # Break outer loop\n", + " break\n", + "```\n", + "\n", + "Here's an example you can play with:" ] }, { "cell_type": "code", "execution_count": null, - "id": "49", + "id": "50", "metadata": { "tags": [] }, "outputs": [], "source": [ - "numbers = [1, 3, 5, 7, 9]\n", - "\n", - "for num in numbers:\n", - " if num == 4:\n", - " print(\"Found 4 - breaking loop\")\n", + "i = 0\n", + "while i < 10:\n", + " i += 1\n", + " if i == 5:\n", " break\n", - "else:\n", + " print(i)\n", + "\n", + "print(\"Loop terminated with break\")" + ] + }, + { + "cell_type": "markdown", + "id": "51", + "metadata": {}, + "source": [ + "#### The `continue` keyword\n", + "\n", + "The `continue` statement skips the rest of the current iteration and moves to the next one.\n", + "It's useful when:\n", + "\n", + "- You want to skip certain elements\n", + "- You want to avoid nested `if` statements\n", + "- You have some cases you **don't want** to process\n", + "\n", + "\n", + "##### Examples of `continue`\n", + "\n", + "1. **Processing only specific items:**\n", + "```python\n", + "numbers = [1, -2, 3, -4, 5, -6]\n", + "\n", + "for num in numbers:\n", + " if num < 0:\n", + " continue\n", + " print(f\"Processing positive number: {num}\")\n", + "```\n", + "\n", + "2. **Skipping empty or invalid items:**\n", + "```python\n", + "data = [\"apple\", \"\", \"banana\", None, \"cherry\"]\n", + "\n", + "for item in data:\n", + " if not item: # Skip empty or None values\n", + " continue\n", + " print(f\"Processing {item.upper()}\")\n", + "```\n", + "\n", + "3. **Complex filtering conditions:**\n", + "```python\n", + "def process_user_better(user):\n", + " if user['age'] < 18:\n", + " continue\n", + " if not user['email']:\n", + " continue\n", + " if user['status'] != 'active':\n", + " continue\n", + " \n", + " print(f\"Processing user: {user['name']}\")\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "for i in range(10):\n", + " if i % 2 == 0:\n", + " continue\n", + " print(i)\n", + "\n", + "print(\"Loop terminated normally\")" + ] + }, + { + "cell_type": "markdown", + "id": "53", + "metadata": {}, + "source": [ + "#### Which one should I use?\n", + "\n", + "Use `break` when you:\n", + " - Want to completely exit a loop\n", + " - Have found what you're looking for\n", + " - Need to handle error conditions\n", + "\n", + "Use `continue` when you:\n", + " - Want to skip certain elements\n", + " - Need to avoid nested conditional code\n", + " - Have cases you want to ignore\n", + "\n", + "
\n", + "💡 Hint: Both break and continue can make code harder to read if overused. Sometimes a simple if statement or restructuring your loop might be clearer. Use these statements when they genuinely simplify your code.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "54", + "metadata": {}, + "source": [ + "#### Common patterns\n", + "\n", + "##### Pattern 1: Loop with early exit\n", + "\n", + "```python\n", + "for item in items:\n", + " if not validate(item):\n", + " break\n", + " process(item)\n", + "```\n", + "\n", + "##### Pattern 2: Skip invalid items\n", + "\n", + "```python\n", + "for item in items:\n", + " if not validate(item):\n", + " continue\n", + " process(item)\n", + "```\n", + "\n", + "##### Pattern 3: Process until condition\n", + "\n", + "```python\n", + "while True:\n", + " data = get_data()\n", + " if not data:\n", + " break\n", + " process(data)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "55", + "metadata": {}, + "source": [ + "
\n", + "Remember: Both break and continue only affect the innermost loop they appear in.\n", + "For nested loops, you might need additional variables or logic to control outer loops.\n", + "
\n" + ] + }, + { + "cell_type": "markdown", + "id": "56", + "metadata": {}, + "source": [ + "#### Exercise: breaking out of loops\n", + "\n", + "Complete the function `solution_find_even_multiple_three` below that searches through a list of numbers and returns the first even number that's also a multiple of 3.\n", + "If no such number exists, return `None`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57", + "metadata": {}, + "outputs": [], + "source": [ + "%%ipytest\n", + "\n", + "def solution_find_even_multiple_three(numbers: list[int]) -> int:\n", + " \"\"\"Finds the first even number that's also a multiple of 3.\n", + "\n", + " Args:\n", + " numbers: List of integers to search through\n", + "\n", + " Returns:\n", + " - The first number that is both even and divisible by 3, or None if no such number exists\n", + " \"\"\"\n", + " return" + ] + }, + { + "cell_type": "markdown", + "id": "58", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "id": "59", + "metadata": {}, + "source": [ + "### `else` after a `for`/`while`\n", + "\n", + "Here the `for` loop iterates over a list of numbers. If the current `num` is equal to 4, a print statement and then `break` are executed.\n", + "\n", + "If we complete the loop without finding 4, then the `else` block is executed and a message is printed indicating that 4 was not found in the list." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "60", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "numbers = [1, 3, 5, 7, 9]\n", + "\n", + "for num in numbers:\n", + " if num == 4:\n", + " print(\"Found 4 - breaking loop\")\n", + " break\n", + "else:\n", " print(\"4 not found in list\")" ] }, { "cell_type": "markdown", - "id": "50", + "id": "61", "metadata": {}, "source": [ "Let's see what happens if we change the list to `[1, 2, 3, 4]`:" @@ -784,7 +1175,7 @@ { "cell_type": "code", "execution_count": null, - "id": "51", + "id": "62", "metadata": { "tags": [] }, @@ -802,7 +1193,122 @@ }, { "cell_type": "markdown", - "id": "52", + "id": "63", + "metadata": {}, + "source": [ + "#### Exercise: using `else` in loops\n", + "\n", + "Complete the function `solution_is_pure_number` below that checks if a string is a \"pure\" number (that is, it contains **only** digits).\n", + "The function should return `True` if the string is pure digits, `False` otherwise.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64", + "metadata": {}, + "outputs": [], + "source": [ + "%%ipytest\n", + "\n", + "def solution_is_pure_number(text: str) -> bool:\n", + " \"\"\"Checks if a string contains only digits.\n", + "\n", + " Uses a for-else structure to verify that every character is a digit.\n", + "\n", + " Args:\n", + " text: String to check\n", + "\n", + " Returns:\n", + " - A boolean value: True if the string contains only digits, False otherwise. An empty string returns True.\n", + " \"\"\"\n", + " return" + ] + }, + { + "cell_type": "markdown", + "id": "65", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "id": "66", + "metadata": {}, + "source": [ + "## Nested loops" + ] + }, + { + "cell_type": "markdown", + "id": "67", + "metadata": {}, + "source": [ + "You can put loops inside of other loops (of any kind). Just be careful to respect the indentation:\n", + "\n", + "```python\n", + "for n in range(1, 4):\n", + " for m in range(4, 7):\n", + " print(\"n = \", n, \" and j = \", m)\n", + "```\n", + "\n", + "The outer loop over `n` goes from 1 to 3. For each iteration, a new inner loop over `m` is started from 4 to 6. You will get **9 lines of output**, as the two range objects contain exactly 3 elements each.\n", + "\n", + "
\n", + "

Important: Performance Considerations

\n", + " Nesting loops can have dramatic consequences on your program's performance.
\n", + " The body of the loop above repeats $n \\times m$ times. If $n$, $m$, or both are large numbers, your program might take a while to finish.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "68", + "metadata": {}, + "source": [ + "### Understanding the performance impact\n", + "\n", + "How much time do you (roughly) need if you are nesting multiple loops?\n", + "\n", + "- **Single loop**: the time required is proportional to the number of elements ($n$) we are iterating over.\n", + "- **Two nested loops**: the time required is proportional to $n^2$.\n", + "- **Three nested loops**: the time requires is proportional to $n^3$.\n", + "\n", + "For example, with $n=1000$:\n", + "\n", + "- Single loop: 1,000 iterations\n", + "- Two nested loops: 1,000,000 iterations\n", + "- Three nested loops: 1,000,000,000 iterations\n", + "\n", + "In the last case, with a billion iterations, even the fastest computer will show some slowdown.\n", + "In these cases, a slowdown due to multiply-nested loops is almost always undesirable, and we should try a better way." + ] + }, + { + "cell_type": "markdown", + "id": "69", + "metadata": {}, + "source": [ + "### Tips to improve performance\n", + "\n", + "These tips are most likely out of scope for an introductory tutorial, but for the sake of completeness, you can investigate if any of these is applicable to your case:\n", + "\n", + "1. Avoid unnecessary nesting\n", + "\n", + "2. Pre-compute values (if some computation is expensive)\n", + "\n", + "3. Think about using a different data structure (e.g. a `set()` instead of a `list()`)\n", + "\n", + "4. Use built-in functions (or libraries) instead of writing a solution from scratch\n", + "\n", + "5. Stop iterating with `break` if possible, thereby reducing the number of iterations to be performed" + ] + }, + { + "cell_type": "markdown", + "id": "70", "metadata": {}, "source": [ "## Quiz on loops" @@ -811,7 +1317,7 @@ { "cell_type": "code", "execution_count": null, - "id": "53", + "id": "71", "metadata": {}, "outputs": [], "source": [ @@ -821,7 +1327,7 @@ }, { "cell_type": "markdown", - "id": "54", + "id": "72", "metadata": { "tags": [] }, @@ -831,7 +1337,7 @@ }, { "cell_type": "markdown", - "id": "55", + "id": "73", "metadata": {}, "source": [ "Another way of controlling the flow of a program is by **catching exceptions**.\n", @@ -852,17 +1358,17 @@ }, { "cell_type": "markdown", - "id": "56", + "id": "74", "metadata": { "tags": [] }, "source": [ - "## The `try-except` block" + "### The `try-except` block" ] }, { "cell_type": "markdown", - "id": "57", + "id": "75", "metadata": {}, "source": [ "When you can predict if a certain exception might occur, it's **always a good programming practice** to write what the program should do in that case\n", @@ -873,7 +1379,7 @@ { "cell_type": "code", "execution_count": null, - "id": "58", + "id": "76", "metadata": { "tags": [] }, @@ -891,7 +1397,7 @@ }, { "cell_type": "markdown", - "id": "59", + "id": "77", "metadata": {}, "source": [ "You can handle **multiple exceptions** at the same time" @@ -900,7 +1406,7 @@ { "cell_type": "code", "execution_count": null, - "id": "60", + "id": "78", "metadata": { "tags": [] }, @@ -917,7 +1423,7 @@ }, { "cell_type": "markdown", - "id": "61", + "id": "79", "metadata": { "tags": [] }, @@ -941,7 +1447,7 @@ { "cell_type": "code", "execution_count": null, - "id": "62", + "id": "80", "metadata": { "tags": [] }, @@ -960,7 +1466,7 @@ }, { "cell_type": "markdown", - "id": "63", + "id": "81", "metadata": {}, "source": [ "- We try to open a file called `README.txt`. A `FileNotFoundError` will be raised if it's not found\n", @@ -972,7 +1478,116 @@ }, { "cell_type": "markdown", - "id": "64", + "id": "82", + "metadata": {}, + "source": [ + "### The `raise` statement" + ] + }, + { + "cell_type": "markdown", + "id": "83", + "metadata": {}, + "source": [ + "What if you want to *explicitly* raise an exception?\n", + "The preliminary question is \"why should I need raising an exception?\" There are several situations where this is appropriate.\n", + "Let's see a few of them.\n", + "\n", + "1. **Triggering a custom behavior**\n", + "\n", + "```python\n", + "def divide_numbers(a, b):\n", + " if b == 0:\n", + " raise ValueError(\"Division by zero is not allowed\")\n", + " return a / b\n", + "\n", + "try:\n", + " result = divide_numbers(10, 0)\n", + "except ValueError as e:\n", + " print(f\"Error: {e}\") # Will print: \"Error: Division by zero is not allowed\"\n", + "```\n", + "\n", + "In this example, we explicitly raise a `ValueError` when we detect an invalid operation, instead of relying on Python's default behavior, which would raise a `ZeroDivisionError`.\n", + "The main reason to do this is to provide a more meaningful error message, or perform some other actions if a particular exception occurs.\n", + "\n", + "2. **Raising custom exceptions**\n", + "\n", + "We can create custom exceptions by defining a class.\n", + "We'll see how to work with classes and objects on the last day of the tutorial, but now just remember that's another\n", + "Here's an example of how to define a custom exception:\n", + "\n", + "```python\n", + "class InsufficientFundsError(Exception):\n", + " \"\"\"Custom exception for banking operations\"\"\"\n", + " pass\n", + "```\n", + "\n", + "Don't worry about the syntax: you will learn everything you need when we'll be dealing with object-oriented programming.\n", + "\n", + "3. **Chaining exceptions**\n", + "\n", + "Say the we are dealing with some configuration file that we want to open.\n", + "We surely need to verify that the file exists, for example:\n", + "\n", + "```python\n", + "def open_file(filename):\n", + " try:\n", + " # do something like trying to access the file\n", + " except FileNotFoundError as error:\n", + " raise ValueError(f\"File {} was not found\") from error\n", + "\n", + "try:\n", + " config_file = open_file(\"config.json\")\n", + "except ValueError:\n", + " print(f\"Error: {e}\")\n", + " print(f\"Original error: {e.__cause__}\")\n", + "```\n", + "\n", + "Here we are using a variation of `raise` that is `raise from`.\n", + "What are the benefits?\n", + "\n", + "- It preserves the original exception as the cause (`__cause__`)\n", + "- It creates a clear chain of exceptions, showing how one error led to another\n", + "- It helps in debugging by maintaining the full error context\n", + "\n", + "4. **Re-raising exceptions**\n", + "\n", + "Sometimes we do not need custom exceptions, but it might be useful to perform some actions *before* Python raises an uncaught exception.\n", + "For example, logging the error somewhere if our program is not running interactively (because maybe it's processing our data overnight).\n", + "\n", + "```python\n", + "def process_data(data):\n", + " try:\n", + " # Some processing ...\n", + " result = data['key'] / 0\n", + " except Exception as e:\n", + " print(\"Logging error...\")\n", + " raise # Re-raises the same exception\n", + "```\n", + "\n", + "Simply using `raise` without arguments is useful when:\n", + "\n", + "- We want to preserve the original traceback (i.e., the full *error context*)\n", + "- We want to do something if the error occurs, but we still want it to propagate up and be caught by the interpreter (or another `try-except` block) " + ] + }, + { + "cell_type": "markdown", + "id": "84", + "metadata": {}, + "source": [ + "A few advice to keep in mind when using `raise`:\n", + "\n", + "1. Always raise specific exceptions rather than generic ones\n", + "2. Provide clear and detailed error messages\n", + "3. Use custom exceptions for specific errors\n", + "4. Use `raise from` when converting between exception types to maintain context\n", + "5. Only catch exceptions you can handle meaningfully" + ] + }, + { + "cell_type": "markdown", + "id": "85", "metadata": { "tags": [] }, @@ -983,38 +1598,38 @@ { "cell_type": "code", "execution_count": null, - "id": "65", + "id": "86", "metadata": { "tags": [] }, "outputs": [], "source": [ - "%reload_ext tutorial.tests.testsuite\n", - "\n", - "import pathlib" + "%reload_ext tutorial.tests.testsuite" ] }, { "cell_type": "markdown", - "id": "66", + "id": "87", "metadata": { "jp-MarkdownHeadingCollapsed": true, "tags": [] }, "source": [ - "## Find the factors 🌶️" + "## Exercise 1: Find the factors\n", + "\n", + "**Difficulty:** 🌶️" ] }, { "cell_type": "markdown", - "id": "67", + "id": "88", "metadata": {}, "source": [ "A factor of a positive integer `n` is any positive integer less than or equal to `n` that divides `n` with no remainder.\n", "\n", "
\n", "

Question

\n", - " Complete the Python code below. Given an integer n, return the list of all integers m <= n that are factors of n.\n", + " Given an integer n, return the list of all integers m <= n that are factors of n.\n", "
\n", "\n", "
\n", @@ -1026,7 +1641,7 @@ { "cell_type": "code", "execution_count": null, - "id": "68", + "id": "89", "metadata": { "tags": [] }, @@ -1035,34 +1650,42 @@ "%%ipytest\n", "\n", "def solution_find_factors(n: int) -> list[int]:\n", + " \"\"\"Finds all positive factors of a given number.\n", + "\n", + " A factor is any positive integer that divides n without leaving a remainder.\n", + " The function checks all integers from 1 to n (inclusive) and collects those\n", + " that are factors of n.\n", + "\n", + " Args:\n", + " n: A positive integer whose factors are to be found\n", + "\n", + " Returns:\n", + " - A sorted list of all positive integers that divide n without remainder.\n", + " For n <= 0, returns an empty list since factors are defined only for positive integers.\n", " \"\"\"\n", - " Write your solution here\n", - " \"\"\"\n", - " pass" + " return" ] }, { "cell_type": "markdown", - "id": "69", + "id": "90", "metadata": { "jp-MarkdownHeadingCollapsed": true, "tags": [] }, "source": [ - "## Find the pair 🌶️" + "## Exercise 2: Find the pair\n", + "\n", + "**Difficulty:** 🌶️" ] }, { "cell_type": "markdown", - "id": "70", + "id": "91", "metadata": {}, "source": [ - "Given a list of integers, your task is to complete the code below that finds the pair of numbers in the list that add up to `2020`. The list of numbers is already available as the variable `nums`.\n", - "\n", - "
\n", - "

Question

\n", - " What do you get if you multiply them together?\n", - "
\n", + "Given a list of integers, write a function that finds the **first** pair of numbers in the list that add up to `2020` and return **their product**.\n", + "The list of numbers is already available as the variable `nums`.\n", "\n", "
\n", "

Hint

\n", @@ -1072,7 +1695,7 @@ }, { "cell_type": "markdown", - "id": "71", + "id": "92", "metadata": { "tags": [] }, @@ -1083,7 +1706,7 @@ { "cell_type": "code", "execution_count": null, - "id": "72", + "id": "93", "metadata": { "tags": [] }, @@ -1092,15 +1715,25 @@ "%%ipytest\n", "\n", "def solution_find_pair(nums: list[int]) -> int:\n", + " \"\"\"Finds the product of two numbers from the input list that sum to 2020.\n", + "\n", + " Searches through all possible pairs of numbers in the input list to find\n", + " two different numbers that add up to 2020. When found, returns their product.\n", + "\n", + " Args:\n", + " nums: A list of integers to search through\n", + "\n", + " Returns:\n", + " - The product of the two numbers that sum to 2020.\n", + " If no such pair exists, returns None.\n", + " If multiple pairs exist, returns the product of the first pair found.\n", " \"\"\"\n", - " Write your solution here\n", - " \"\"\"\n", - " pass" + " return" ] }, { "cell_type": "markdown", - "id": "73", + "id": "94", "metadata": { "tags": [] }, @@ -1110,7 +1743,7 @@ }, { "cell_type": "markdown", - "id": "74", + "id": "95", "metadata": {}, "source": [ "
\n", @@ -1120,14 +1753,14 @@ "\n", "
\n", "

Hint

\n", - " Too many nested loops can worsen a lot your code's performance\n", + " Too many nested loops can worsen significantly your code's performance.\n", "
" ] }, { "cell_type": "code", "execution_count": null, - "id": "75", + "id": "96", "metadata": { "tags": [] }, @@ -1136,26 +1769,39 @@ "%%ipytest\n", "\n", "def solution_find_triplet(nums: list[int]) -> int:\n", + " \"\"\"Finds the product of three numbers from the input list that sum to 2020.\n", + "\n", + " Searches through all possible combinations of three different numbers in the \n", + " input list to find three numbers that add up to 2020. When found, returns \n", + " their product.\n", + "\n", + " Args:\n", + " nums: A list of integers to search through\n", + "\n", + " Returns:\n", + " - The product of the three numbers that sum to 2020.\n", + " If no such triplet exists, returns None.\n", + " If multiple triplets exist, returns the product of the first triplet found.\n", " \"\"\"\n", - " Write your solution here\n", - " \"\"\"\n", - " pass" + " return" ] }, { "cell_type": "markdown", - "id": "76", + "id": "97", "metadata": { "jp-MarkdownHeadingCollapsed": true, "tags": [] }, "source": [ - "## Cats with hats 🌶️🌶️" + "## Exercise 3: Cats with hats\n", + "\n", + "**Difficulty:** 🌶️🌶️" ] }, { "cell_type": "markdown", - "id": "77", + "id": "98", "metadata": {}, "source": [ "You have 100 cats.\n", @@ -1175,14 +1821,14 @@ "\n", "
\n", "

Hint

\n", - " You can approach this problem with either lists ([]) or dictionaries (key-value pairs).\n", + " You can approach this problem with either lists or dictionaries.\n", "
" ] }, { "cell_type": "code", "execution_count": null, - "id": "78", + "id": "99", "metadata": { "tags": [] }, @@ -1191,196 +1837,83 @@ "%%ipytest\n", "\n", "def solution_cats_with_hats() -> int:\n", + " \"\"\"Simulates putting hats on cats in a circular arrangement through multiple rounds.\n", + "\n", + " Simulates 100 rounds where each round visits cats at different intervals:\n", + " - Round 1: visits every cat (1, 2, 3, ...)\n", + " - Round 2: visits every 2nd cat (2, 4, 6, ...)\n", + " - Round 3: visits every 3rd cat (3, 6, 9, ...)\n", + " And so on until round 100.\n", + " At each visit, the hat status of the cat is toggled (if no hat, put one on; if has hat, take it off).\n", + "\n", + " Args:\n", + " None: The function works with a fixed setup of 100 cats and 100 rounds\n", + "\n", + " Returns:\n", + " - An integer representing the number of cats wearing hats after all 100 rounds are complete\n", " \"\"\"\n", - " Write your solution here\n", - " \"\"\"\n", - " pass" + " return" ] }, { "cell_type": "markdown", - "id": "79", + "id": "100", "metadata": { - "jp-MarkdownHeadingCollapsed": true, - "tags": [] + "jp-MarkdownHeadingCollapsed": true }, "source": [ - "## Toboggan trajectory 🌶️🌶️🌶️" + "## Exercise 4: Base converter\n", + "\n", + "**Difficulty:** 🌶️🌶️🌶️" ] }, { "cell_type": "markdown", - "id": "80", + "id": "101", "metadata": {}, "source": [ - "During a winter holidays break, your friends propose to hold a [toboggan](https://en.wikipedia.org/wiki/Toboggan) race. While inspecting the map of the place where you decided to hold the race, you realize that it could be rather dangerous as there are many trees along the slope.\n", + "Write a function that converts numbers between bases 2-16.\n", + "Your function must:\n", "\n", - "The following is an example of a map:\n", + "##### (1) Process the input number digit by digit (don't use `int()` on the whole number)\n", + " \n", + "##### (2) Handle these specific requirements\n", + "- Accept negative numbers (starting with `-`)\n", + "- Skip spaces in the input (e.g., `1010 1111` is valid)\n", + "- Accept letters `A` through `F` (or `a` through `f`) for non-decimal bases\n", "\n", - "```\n", - "..##.......\n", - "#...#...#..\n", - ".#....#..#.\n", - "..#.#...#.#\n", - ".#...##..#.\n", - "..#.##.....\n", - ".#.#.#....#\n", - ".#........#\n", - "#.##...#...\n", - "#...##....#\n", - ".#..#...#.#\n", - "```\n", + "##### (3) Implement proper validation\n", + "Your function should raise a `ValueError` exception if any of the following rule is **not** respected:\n", "\n", - "A `#` character indicates the position of a tree. These aren't the only trees though, because the map extends **on the right** many times. Your toboggan is an old model which can only follow certain paths with fixed steps **down** and **right**.\n", - "\n", - "You start at the top-left and check the position that is **right 3 and down 1**. Then, check the position that is right 3 and down 1 from there, and so on until you go past the bottom of the map." - ] - }, - { - "cell_type": "markdown", - "id": "81", - "metadata": {}, - "source": [ - "#### Part 1 🌶️" - ] - }, - { - "cell_type": "markdown", - "id": "82", - "metadata": {}, - "source": [ - "
\n", - "

Question

\n", - " How many trees would you encounter during your slope?\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "83", - "metadata": {}, - "source": [ - "
\n", - "

Hint

\n", - " Read the trees map as a nested list where each # corresponds to 1 and each empty site is 0\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "84", - "metadata": {}, - "source": [ - "
\n", - "

Hint

\n", - " In the solution_ function, define 4 variables:\n", - "
    \n", - "
  1. the position (starting at [0, 0])
  2. \n", - "
  3. the number of trees encountered
  4. \n", - "
  5. the depth of the map
  6. \n", - "
  7. the width of the map
  8. \n", - "
\n", - "
\n" + "- Bases must be between 2 and 16 (inclusive)\n", + "- Each digit must be valid for the source base\n", + "- Input number string must not be empty" ] }, { "cell_type": "code", "execution_count": null, - "id": "85", + "id": "102", "metadata": {}, "outputs": [], "source": [ "%%ipytest\n", "\n", - "trees_map_str = pathlib.Path(\"tutorial/tests/data/trees_1.txt\").read_text() # do NOT change this line\n", + "def solution_base_converter(number: str, from_base: int, to_base: int) -> str:\n", + " \"\"\"Converts a number from one base to another.\n", "\n", - "trees_map = []\n", + " Args:\n", + " number: String representation of the number to convert\n", + " from_base: Base of the input number (2-16)\n", + " to_base: Base to convert to (2-16)\n", "\n", - "for line in trees_map_str.splitlines():\n", - " row = []\n", - " for position in line:\n", - " # TODO\n", - " # For each position, add 1 to `row` if you meet a '#', 0 otherwise\n", - " # TODO\n", - " # add `row` to the `trees_map` list\n", + " Returns:\n", + " - String representation of the number in the target base\n", "\n", - "\n", - "def solution_toboggan_p1(trees_map, right=3, down=1):\n", + " Raises:\n", + " ValueError: If bases are invalid or if input contains invalid digits\n", " \"\"\"\n", - " Complete your solution with the given hints\n", - " \"\"\" \n", - " pos = [0, 0]\n", - " trees = # TODO\n", - " depth = len(trees_map)\n", - " width = # TODO\n", - "\n", - " # Hints:\n", - " # - write a loop until you reach the bottom of the map\n", - " # - if the current location is a tree, add 1 to `trees`\n", - " # - update `pos` by moving 3 right and 1 down\n", - " \n", - " return trees" - ] - }, - { - "cell_type": "markdown", - "id": "86", - "metadata": { - "tags": [] - }, - "source": [ - "#### Part 2 🌶️🌶️" - ] - }, - { - "cell_type": "markdown", - "id": "87", - "metadata": {}, - "source": [ - "You check other possible slopes to see if you chose the safest one. These are all the possible slopes according to your map:\n", - "\n", - "- Right 1, down 1\n", - "- Right 3, down 1 (**just checked**)\n", - "- Right 5, down 1\n", - "- Right 7, down 1\n", - "- Right 1, down 2\n", - "\n", - "
\n", - "

Question

\n", - " What do you get if you multiply together the number of trees encountered on each of the above slopes?\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "88", - "metadata": {}, - "source": [ - "
\n", - "

Hint

\n", - " Define a variable slopes as a list (or tuple) containing lists (or tuples) of the slopes' steps above\n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "89", - "metadata": {}, - "outputs": [], - "source": [ - "%%ipytest\n", - "\n", - "slopes = ((3, 1), ) # TODO\n", - "\n", - "def solution_toboggan_p2(trees_map, slopes):\n", - " total = 1\n", - " for right, down in slopes:\n", - " # TODO\n", - " # use your solution of Part 1 to calculate the trees for a given slope\n", - " # accumulate the product in `total`\n", - " \n", - " return total" + " return" ] } ], @@ -1400,7 +1933,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.10.15" }, "toc-autonumbering": false, "toc-showmarkdowntxt": false, diff --git a/tutorial/tests/data/trees_1.txt b/tutorial/tests/data/trees_1.txt deleted file mode 100644 index 8985593f..00000000 --- a/tutorial/tests/data/trees_1.txt +++ /dev/null @@ -1,323 +0,0 @@ -..#..#......###.#...#......#..# -...#.....#...#...#..........#.. -....#.#...............#.#.#.... -.........#.......##............ -#.#....#.#####.##.#........#..# -.....#...##.#..#.##...#.#..#... -#.#..###.#........#....##...#.# -..###.....#..###.....##........ -#.#.#...........#.....#.#....## -...#.#.##.##.#.#......#...##.#. -.....##.#..#....#..#...##...### -...#.....#..#..#...#.#....##... -.#...##.#.........#...#.#...... -....#...#.....#......#...#..... -.#...#.....#....#......#...#... -#...#......####..##...###...... -....#..#......##.##.....#..#... -....#....#.......#..#...#....#. -...##..#.##..#.#...#..##....... -##.#..#.....#.##.#....#..##.... -#....#....#.....#..#.#.#.....#. -##...#.###.....#....#..#.#.#... -#..#.......#...#.#...#.#.....#. -....#.#.......#.....###..#..#.# -......####...#.#..#..#.#.#.#... -#...##.....#...#.#.........#.#. -......#...##.#..#.#........#... -..#.#...........#..##...###.##. -#......#.#......#.....#.....#.# -.#...............###.#.###..... -...#...........##..#...##..##.# -#......#.##.#............#.##.# -.#.#....#....###........#..#... -...##.#.#..#.##.#..##..#.##..## -.....#...#.#.#...#....#......#. -..............#...##........... -..............##........#..###. -.#.##.......#.....##.#......#.. -..#......#..#.#####..#.#....... -#.#..#...#.#..#....#..#.##..#.. -...##.......#.#............#... -...#....#..#.##.###.......#.### -..###..#....#..#.....##...#..#. -..#.###.##......###....#....##. -...#...##...###....##.....###.# -.....#.....#.#.#.........#..### -#.#......#.#..#.####..#........ -#....#.##.......##............. -..##...........#....#.....##..# -..#...#...........#....#...#... -...#...#...#.....#..#....#....# -#......##.........#.#...##...#. -.##..#...#.....#....#.##.####.# -#..##.##.#......#.............# -.#.....#..##.###.#.#.#......... -.###....###..#....#..#.#.#..##. -....#........#..#....##..#.#.#. -.....#..........#..........#... -.#.##..#..#...#..#.##.#.##..... -.#....#...#......#.#..##.##..#. -.###.#...#.#.##....#.....#..##. -......##.......#..#.......#.#.# -.##.#.#.#......#.......#....... -#..#...##......#.......#......# -...#..#...##.#...#..##......... -.....#..###...##...#..#.#...#.# -..#.#.#....##..#.#.#.#...#..... -.....#.#.#..#..#.#.#...#....... -#.#.#...#.#.....#.#.#.##.###... -.....#.#.....####..#........... -..#.#.#...........##..#.#....#. -.#..#......#..#...........###.. -..#...###.##......#..###...#..# -#.#..#.....#..#.##.#..#.#.....# -.....................#.#..#.... -...##..##...#.#..#..##.#....#.. -.#..#.#....#...#.#.##.......... -....##.....#..#..##.........##. -..##...##........#.#....#...### -.#...#............#.#.#.#...... -#...#........#..#..#...#.#..... -..#..........#.......###.##.... -#...........###..#....##..#.##. -##...#..#.##.....#...........#. -.#..##.....#..#.#.....##.#..#.# -..#..#.##....#.........#.#.#... -#..#...#...#..#...........##... -.....#.......#.#......#.#.#...# -..#.#..#..#.#.#.......#.#...#.. -......#.....##.....#.....##.##. -#.#..#......#......#.####.##... -.####...#####.#....#.#..##..... -............#....#....#....##.. -###.........#............#.#... -...#...#....#.##..#...#......## -...##.#.#.##.##.#.....#...#.#.. -...#.....#...#..##......#.#.##. -.##.#......##................## -......#.....#..##.............# -#.#...##..#..#..#.##.....#..#.. -#......###.....#....##...##...# -....#..#.....#.......####...##. -#.#...#.#...#..........#..##..# -....#..#....#................## -.####..#........#..#.#...#..... -##.###...#.##........#..##..... -..###..##...#...#..#...##.....# -......#..##.................... -.#...#......#.#.##..#........#. -..#...#####.....##.....#...#... -.#..#....#..#....##.#....#..##. -.#.....##..###.#.....#.#.#.##.. -#..##.....##...#.....#..#.#.... -#.##......#.#......#..........# -#####........#.............#... -.#..#..##..#....#.....#..####.. -...#..##.##...####....#.##...## -..........#....#...........##.# -#...##...#...##....#.....#..... -.......#..#.....#.#.#.#.#.....# -...#..##..####..#..##.#.##....# -#...#...#...........#.#.....#.# -..#.....##...###.........#..##. -.......##..#.......#.......##.. -#.#....#....#.###............#. -...#......#.#.............#.#.. -......#..#....#....#....#..#... -.....##..#...........##...#.##. -..#....#.##.#......#........... -#...#....#.#.#.#.#..#.......... -.#..#..........#..#.#.....#.... -.....##......##....#.#.....#.#. -.....#..#..........#....#.....# -....#..#..#.#...#.#..#..#..##.# -.#..##.#..##...###.#..........# -..###..#......#...##...#.#..... -..#...#...#.....#.......#....#. -#...##..#.##.#....##.....#..... -..#.#.....#...#...#............ -.......#.#.#..#.....###.#...##. -....##.......#####...##..##..#. -#...#.##.....#.#...##.........# -..#.##..........#..###.#....#.. -#......#.##...#...#.....###.... -................#.##........... -##.###.#.#.#.##......##..#....# -..#.#........##..#..##......... -###....#..#....#..##....#.....# -#......#..#...........#.#...##. -...###.......#...#......##..#.# -.......#...##.#.#...#.##......# -......##..#...##.#.#...##....#. -..#...#...#...#.#.....#..##..#. -..##...#.....#.....#..##....... -....#........#.#.##.......#.#.. -#...#..##..#..##..#...#......#. -...#..#.#.#..#..#..####...#.... -#..#..#......#......#..#.###### -#..#..#..#........#..#.#....### -#..##..#.#.##.....#..#......#.# -##.......##.#..#.............#. -..........#.#..#..#............ -....#.#.#.#...#......#......#.. -###.#.#.........#.......#...##. -#.............####..#...#.##... -....##.......#................# -###...#..#......##....#.####.#. -..##.##.#.#.#.#...#.......#...# -.....#.##......#.......##..#.#. -.#...#.##..#.......#.#....#.#.# -##...##..#....#..#...#....#.... -..........#...##.#..##.......## -#.#...#....#......#.#.......### -......#...#.##....#....##.#.##. -..#..#.......#.......#....##... -##..##.......##............#.#. -.#.#...#..#.#.###......#....... -#...#..##....#...###..#.#.....# -.#.....#........#..##.#.#.#.... -..#.##....#..#...........#...#. -.....#.#...#.##..###...#...#... -#....####.......#..#.#...#..... -....#.....#....##..#.##.....### -........#.#.....###....#.#..... -...#.....#.##.....#......#..... -.....#...####......###..#...##. -#.#......#..........#..##.#..#. -..##......###...#...#.......#.. -#...#.#...#.#.........#........ -....#..#.##.#.##.###..#.....#.. -.#.#.#......#.#........#.....#. -.....#.#..#....#...#.....#.#.## -##.............#..#.....#.#.... -#............#..#....##......## -#....#......#......#....##..#.. -.#....#............#......##..# -..#.#.#..#.#....##.#.......#.## -#.##.....#...#......#...#...... -.......#...........#..#.##..#.# -##.....##.#.....####..........# -...#.......#.#.............#..# -...##........##..#..#.#........ -.#.##...#.....##.#......#....#. -.#................#.#...#..#... -#....#.#.#......#.#.#.##....#.. -..#......#............#...#.... -###..#.##........#....##.#...#. -.#..#..#......##............... -....##.............#....##...## -..#.#..#.#####....##.......###. -......#...#..#.#....#.#..#...#. -.........#..##.##...#....##..## -.............#.##....###.#..... -..#................#..#.#..#... -...#........#......#..###...... -.#.#.#....#.........#...###.### -.........#..#.#......##.....#.. -#...##..#.#.###..###........... -...#.#.#..#......#..##.#.##.... -.....##.......#................ -.##....#.#.#.##.....#.##......# -...#........#...##.#.##..##...# -..#..........#.#......####..##. -............#.#.#.#.....#...... -..##.####.#..#....#..#..##..... -......#........#...#..#.#..###. -#.#..............#..#...#..#... -....#............#...#..#...##. -..##....#...##.##.#..........## -..#..#.........#..#.....#.#.... -#.....#.###...##...##...##..... -#.#...#..#####.#...#..#.....#.. -..#.....###...#.........#.#...# -....#.##.........#.#.....#.#.#. -..........##...#....#.#.#.....# -...#...........#.....###....... -#....#..#...#.....#.......#.... -.#.#.....#..##..##..#........#. -.#.#.....#....#...#.#.##....... -....###...#...###.##....#...... -...#.#.##....#...##......#...#. -#....#...##.....#.##.#.....#.## -.#.#.....##.##.##..###...#..... -.#.#......#..#..#........#.#..# -........#...##........##...#... -.#..#.#.#..#.....#....#...#.#.. -#......#...#.#...#..#.#..#..... -.#......#.....#.........###.#.. -#..#..........##..###.......#.. -#..#..#....#......#......#..... -......#.....##.........##....#. -#..#.#...#...#.##.#..#..##..... -....#.#....###..#.....#...##.#. -..##.....##.#..#..##..#.#...... -.........#..#....###...#.#....# -.........#...#...#...#......##. -.......#..#.....#.#.#...#...#.. -............#.....###......#..# -#....##..###.......#...##....## -..#.##..#####..##.#...#......#. -#.#..#...###.............#.#... -##...#..#..#.#....#.#.......#.. -.....#....##.....###.##..#..... -......##..##..#.#..####.#...... -..#...#.#....#...#.#.........#. -##.....#.#....#..#..##........# -...........#..#........##..#... -..##.#...#.#.#..##..#..#..#..## -..........#.###.....#..#.....#. -......#............###..##.##.. -.#.......#..#...........#.###.# -#...#..##............##.......# -.###..#...#.#....#....#......#. -..##.........##............#.#. -.##.......##....#.#.#....#..#.# -#.##........#.....#.##...#.#... -#......#....#.#......##....#..# -#.##..##..#...#.###......#..... -..........#.#....###.#.....##.. -#..##...#.###..#.............#. -.#.#......#.##.#...#....#.....# -.##...#..##...#...........#.##. -.##..#.#.#..#.....#.....###.... -.#...#.#.#..#..#....##...#..#.. -#.#.#....#.....#..#..##..#.#... -......#..#...####..#.........#. -.#.#..#......#...#..####.....#. -...#.#...#...#....##..#.#.#.##. -...#........##.............#.#. -...#...#...#.......#..#.#.#..## -.####.#...##......#.##.##.#.#.. -#..###...........#..#.#...#.#.# -###...#.#..#...#.#...#.#..#.#.# -#....#.....##...#.#...#..#.#... -.#........##.##....##..#..#.... -.#.#.#..#........#...#..#.#.#.# -#.##.....#.#...#....##...#..#.# -..#.......##.#.###............# -##....###..##.........##..#.#.. -...##...#...#..###.#.....##..#. -###.................#.#..#..... -....#......#.....#..###......## -.......#...##..#............... -.#.....#..#.....#...##...#...## -.....##....#.#..#.##.....#...#. -#..####.#....#..#.....#....#..# -..#..##.#.##......#..#.#....#.. -..#.#.#.#.....#...#...#..#..... -.#........#.#...#.#..#...##.... -.#...#.#...#..#.#...###...#.#.. -#.....#...##..#.....#...#.#..#. -...#....#................#.#... -......##.#.#..........#...#.... -.##..#.#.#...#..#...####.#..... -#......#....#..#.......#....... -.#........#.#.#....###.#..##... -....##......#.....##...#...#... -..#..#.#.#...#..#.####.##...... -...#........#.#.##.#..#.##.#... -.#..##...#...#...##.......##.#. -#...#.#......#................. -..#..#.....#....##...#..###.... -.#...#.........#.#.##.#........ diff --git a/tutorial/tests/data/trees_2.txt b/tutorial/tests/data/trees_2.txt deleted file mode 100644 index e1bd0f65..00000000 --- a/tutorial/tests/data/trees_2.txt +++ /dev/null @@ -1,323 +0,0 @@ -.#.......#...........#......... -..##.......#.#.#.....##...#.... -.......#..#.....#...#.......... -...#..........###...#........## -#.#..#.#.##.#........#.#.....#. -#..#....#..#....#.............. -#..#........#.................. -..#.#...#.#...#....#.#.#..#.... -..............#..#............. -.##....#...................#... -........#..........#......#...# -.##..#..#...##..........#...#.. -.#...#....#.........#...#.....# -.#........##............#.#.... -...........#..............##... -.#..#......#..#..............#. -..#.#.#...........#........#... -..###..........#....#.#......#. -.......#...##..........#....... -........#...#.................. -....#....#..#.......#........#. -.......##.#......#.....#...##.. -..#.#........................#. -.#.....#.##..............#.#... -..#.#...#.#..#....#....#....... -.#....##.....#....#........#... -..#...........#.##....#...#.... -..#.##...#....#.#.....##...#... -.......#...####...#...#.......# -.#...#......................... -.......................#....... -.....#.#.........#..........#.# -#.........#............###..#.. -.....#.#.............###....... -...#..#........#.#.......#..... -...................#....#...... -...#..#...#............#..##... -...#.....#....#.......##......# -.....#....#...##..#..#...#...#. -..........#...........#.#.#.... -..#.......#...#.....#......#... -.........#.......##......#..#.# -..#.....#..#.###...#.#......#.. -#....#...#..#...#.....#........ -..#......#..#.......#.#.....#.. -#......#...#......#.....##.#... -........##.......#.......#..... -.#.#...............#........... -..............#...#.#....#..... -....#......#.#..#......#....... -...##....#....#...#............ -.#...............#...........#. -.#.#...#.#.....#.....#...#.#... -...##...........#.....#..#...#. -.#.#...##.#.#......#......#.... -.##.....#.......##....#.#.#.... -.......#...........#....#....#. -....#...........#......#.####.. -......#....#...#...##.......#.. -......................#.#####.. -..#...#.#...#..#..#......#..... -....#........##.......##....#.. -#.#......##.........##.#..#...# -.#.#....#...#..#...#...##....#. -.....##...#....#....#.#........ -......#..#....#.#...#.......... -.........#...................#. -............#.###....#.#....... -...#.#.....#......#....#.#..#.. -..............#..#.#.#.#....... -#..##...................##..... -..#.......#..#.........##..#... -.........##...#......#........# -..#.........#........##.###.#.. -...........#.#....#.....###.... -..#....##.#..#.##....#.....##.. -..#.....#.##..................# -#....#......................... -..............#..#...#.#....... -......#..#.#.##....#..........# -..#.........#.####.....#....... -......#..#.#..........#...#.... -......#.................#..#.#. -.....#..........#.............. -....#.....#............#....##. -.....#.....#........#.......... -............#.....#...#........ -........#....#.#............... -#.....#.........#......#..#.#.. -...#..#......#......#.......#.. -.....#......#.#....#..#...#...# -......................#..##.... -.............#.........###....# -#..............#.#..........##. -...#.#.................##...... -...........#.#.....#........... -.........#.................#.#. -........#........#...#..##...#. -........#......##.......###.... -..............#.#.#............ -.#.....###...##.#......#.....#. -.............#......#.#.#...#.# -..#.........#.......#.....#.... -......#........#...##......#... -.##..........##......#.#.....#. -..#.##....#....#............... -......#...#..#.....#.....#...#. -.......##..##..#............##. -..............#...##........#.. -#....#................#..#..... -........#.......#.#.#...#...... -......#.......#..............#. -#.#..#...#........#....#..####. -..#........#...........#.....#. -.##...........................# -.............#...........#..... -.#.....#.#...#.........#....... -..........#...#....#....#...... -.#..#........##....#........... -.......###......##...#......... -..........#.#.#..#.#....#...... -........##..#.........#....#... -........#.#......#.#...#.#..#.. -....#....................#.##.. -##....#..#...........#.....#.#. -...#..............##...##..#.#. -......#.##.#.......#..#...#.... -....#..#..##.....#.....#.#....# -.......#....##.##.............. -#..##....#.....#.#............. -..................#......#..#.. -..#......#...#..#.......#...#.. -...........#....#.#.....#...... -#..#...##.........###..#......# -.......#......................# -#.......#....................#. -..#..#..........#..#..#....#... -.##..#..#.....#.#..##.......... -#..###.......#..##..#...#..#.#. -.....##......###.....#.#.##...# -..............#...#....#.#..... -#...........#.................. -..............#....#..##..#..#. -.........#.............#....... -.#.#....#....#...............## -.##.##.#.....###.....#......... -....#..............##......#... -....#........##................ -....#.....#....#....##....##... -.#........#......#......#...... -....#..........#............... -##..........#......#.....#..... -........#.#..#.#..#.....##..... -..##......#.#.......#.#..#..... -.#.......#......#...........#.. -..#.#..#.#..................#.. -...#...#...#...##......#....... -.#...##....#...#...#...#....... -.......#.#.......#............. -.#.##.#.....#...........#.##.#. -.#.##.#........#...##.......... -.#.....#.....#....#..#......... -...##.............##........... -.#........##.....#.......#...#. -...........#..#..##........##.. -.....#..#...................... -..#.......#.................... -.....#......#....#....#.......# -........#..#.#.....#......#.... -..........#..#.....#......#.... -..........#####.....#........#. -........#..#...#.#....#......#. -.........#...#....#.#.......... -......#....##..........#...#... -#..............###.#.#......... -.#.#............##......#.#..#. -......#........................ -...#..#......#.......#....#...# -.......#....##.....#.#......##. -...........#..........#..#..... -...........#..#.....###......#. -.......#....#..##......#....... -.........#.#.#.......#..#...#.. -.......#.......##.....##...#... -..............#....#.....#..... -...#....#.....#.#..........##.. -###.........#.............#.... -...##......#.#........#....#..# -#....###.......#...#.#......##. -....#...##.......#......#.....# -.....#......#.................. -#........##....#....#.#........ -........#.......###...#........ -........#..#.......###......... -..............#......#..#...... -#......#.....#....#.#.......... -.#......##.#.#.....#...#.#....# -.##...........#..#.##.....#.... -.....#.....................#... -.#..#...#...##.#...#........... -.......#.......##..#.#..#...... -.......##.....#.....#.......... -.................#............. -#........#..#.......##......... -#...#..###.#..#....#.#.###..... -..#.......#.......#.......#.... -..............#............##.. -.#...#..#...##.........#....#.. -#...........#...#.............. -.......#.....#......#..#.....#. -..........#......#............. -##.........###..##.#....#..#.#. -..............###.............. -#..##.............##.....#..... -....##...................#..#.. -....#.....#..............#..#.# -........#........##...#.....##. -#...........#.##..........##... -#......##.....#...............# -..##..#....#.................#. -#.......##..................... -...............#.##..##......#. -..#.##..#.#....#.......##...... -......##....#............##.... -.#..#..##.....#.##....#........ -#.........#..........#...#....# -...#.......#.............#.#.#. -..##............#...##........# -.......#.#.#........#.......... -.....#.............#.....#..... -.........#.........#.........#. -#.....#....#.......#........... -.........#....#.............#.# -.##..#.......#...#......#...... -....#....#....#........#....#.. -............#.......#..#......# -.#............#.##........##... -..#...##...#....#...#.#...#..#. -#...#..........#..##.........#. -..#.........................#.# -...........#.........#..#.##... -.#..................#..#....... -......#......#...........#..#.. -...##.....#.....#..#.......#... -.........#.#.......#......#.... -...........#................#.. -.....#...#..#............##.... -.#.......#..#....#..........#.. -#.....#..#.....#..##.......##.. -...#.......#...#....#...#.#..## -...#...##......#....#....#..... -.......###.#..#.......#......#. -........#.#...#..#..#...#....#. -....#.........##.#.....#....... -....#.........#..##........#... -..#...........#......#....#.##. -.....................#......... -...................##......#..# -......#.#.....##..##..........# -..#.##........#.#.#..........#. -.#.......#...##.#....#....#.... -#.#......#..#..#.......#....... -.............#........#.......# -....#...#.....#........#...#... -..#..............##..#......... -..#.................#..#...##.. -....#..#...#................... -......#.........##.#..#..#...## -........#..#....#.......#.#.##. -.#...#...........#..........#.. -##.....#...#............##...#. -.##.....#...#.................. -.#.......####.#..##.##.#......# -.............#...#..#..#....... -...#.##.........#.#....#....... -...........##...##....#....##.. -........#......#...#........... -...........#..#...#....#.##.... -..##....#..........#....#...#.. -#....#.#.#.......#.#........... -......#............##.......... -#.#.###..#....#.......#...#.... -.#......##..#..#.#.........#..# -..#.........#........#....#.... -......##.#.......##....#..#..## -.............#...#............# -......#......#...#.#.#.##.#.... -#.#...#.##.....#..............# -..........#.............##.##.. -#......#....#...#.#.#.#..#....# -........#........#...#.#......# -.....#...........#............. -...........#....#..........#... -....####...#..##....#.#........ -.#......#...#..#...........#... -#......###..#.##.###........... -..#...........#.........#....#. -................#.#....#..#.##. -...................#......#.... -....#.#.....#.......#...###.##. -.#........#.#....#...#..#...#.. -....#..###.................#..# -.....#.#..#........#......#..#. -....#.....#...............#...# -............##.#.........#..#.. -.......#..#..##.#.#...##....... -..#..........##..#..#........#. -..............#..#...#......... -......#.#....#........##....... -.#.....##....#..#...#.......##. -..............#.##............. -#..#..#...##....##.#.....##.#.. -..#...###..#.........##........ -........##......#.....#..###... -.....#......##.###............. -....#.....#.#..#.#..#.......... -....#..#.......#...........#... -.#.............#..#......##.... -..#.#......#.#................. -.......#.#.#............#..#... -......###....##............#..# -.........#....#......#......... -..........#...............#..#. diff --git a/tutorial/tests/test_control_flow.py b/tutorial/tests/test_control_flow.py index 56cfd2b8..8e8c434d 100644 --- a/tutorial/tests/test_control_flow.py +++ b/tutorial/tests/test_control_flow.py @@ -1,18 +1,14 @@ +import contextlib import pathlib -import sys -from collections import Counter from math import isclose, sqrt -from typing import List, Tuple +from typing import Any, List, Optional, Tuple import pytest def read_data(name: str, data_dir: str = "data") -> pathlib.Path: """Read input data""" - current_module = sys.modules[__name__] - return ( - pathlib.Path(current_module.__file__).parent / f"{data_dir}/{name}" - ).resolve() + return (pathlib.Path(__file__).parent / f"{data_dir}/{name}").resolve() # @@ -20,9 +16,12 @@ def read_data(name: str, data_dir: str = "data") -> pathlib.Path: # -def reference_indexed_string(string: str) -> list[tuple[str, int]]: +def reference_indexed_string(string: str) -> List[Tuple[str, int]]: """Reference solution warm-up 1""" - return [(char, index) for index, char in enumerate(string)] + result = [] + for i, char in enumerate(string): + result.append((char, i)) + return result @pytest.mark.parametrize( @@ -46,7 +45,7 @@ def test_indexed_string(string: str, function_to_test) -> None: assert reference_indexed_string(string) == result -def reference_range_of_nums(start: int, end: int) -> list[int]: +def reference_range_of_nums(start: int, end: int) -> List[int]: """Reference solution warm-up 2""" step = 1 if start < end else -1 return list(range(start, end + step, step)) @@ -68,9 +67,13 @@ def test_range_of_nums(start: int, end: int, function_to_test) -> None: ), "The function returned an empty range" -def reference_sqrt_of_nums(nums: list[int]) -> list[float]: +def reference_sqrt_of_nums(numbers: List[int]) -> List[float]: """Reference solution warm-up 3""" - return [sqrt(num) for num in nums if num >= 0] + result = [] + for num in numbers: + if num >= 0: + result.append(sqrt(num)) + return result @pytest.mark.parametrize( @@ -106,11 +109,11 @@ def test_sqrt_of_nums(nums: list[int], function_to_test) -> None: ), "The function should return the square root of each number" -def reference_divide_until(num: int) -> int: +def reference_divide_until(number: int) -> int: """Reference solution warm-up 4""" - while num % 2 == 0: - num //= 2 - return num + while number % 2 == 0: + number //= 2 + return number @pytest.mark.parametrize( @@ -120,6 +123,111 @@ def test_divide_until(num: int, function_to_test) -> None: assert reference_divide_until(num) == function_to_test(num) +# +# Exercise: conditionals inside loops +# + + +def reference_filter_by_position(numbers: List[int]) -> List[int]: + result = set() + for pos, number in enumerate(numbers, start=1): + if number > pos: + result.add(number) + return sorted(result) + + +@pytest.mark.parametrize( + "numbers", + [ + [0, 3, 1, 2], # Basic case from example = {3} + [5, 4, 3, 2, 1], # Decreasing numbers = {4, 5} + [1, 3, 5, 7, 9], # All odd numbers = {3, 5, 7, 9} + [], # Empty list = {} + [0, 0, 0], # Same numbers with none valid = {} + [2, 2, 2, 2], # Same number with one valid = {2} + [4, 4, 4, 4], # Same number, three are valid but they are duplicates = {4} + [10, 20, 1, 2, 3], # Mixed large and small numbers = {10, 20} + ], +) +def test_filter_by_position(numbers: List[int], function_to_test) -> None: + """Test filtering numbers by position.""" + assert function_to_test(numbers) == reference_filter_by_position(numbers) + + +# +# Exercise: breaking out of loops +# + + +def reference_find_even_multiple_three(numbers: List[int]) -> Optional[int]: + result = None + for number in numbers: + if number % 2 == 0 and number % 3 == 0: + result = number + break + return result + + +@pytest.mark.parametrize( + "numbers", + [ + [1, 2, 3, 4, 6, 8], # 6 is first even multiple of 3 = 6 + [1, 3, 5, 7, 9], # No even numbers = None + [12, 18, 24], # All are valid, should return the first = 12 + [], # Empty list = None + [2, 4, 6, 8, 10], # Even numbers but no multiples of 3 = None + [1, 3, 5, 7, 12], # Valid number at the end = 12 + ], +) +def test_find_even_multiple_three(numbers: List[int], function_to_test) -> None: + """Test finding first even multiple of 3.""" + assert function_to_test(numbers) == reference_find_even_multiple_three(numbers) + + +# +# Exercise: using else in loops +# + + +def reference_is_pure_number(text: str) -> bool: + for char in text: + if char not in "1234567890": + return False + else: + return True + + +def is_for_else_used(function) -> bool: + import ast + import inspect + + tree = ast.parse(inspect.getsource(function)) + for node in ast.walk(tree): + if isinstance(node, ast.For) and node.orelse: + return True + return False + + +@pytest.mark.parametrize( + "text", + [ + "123456", # OK + "0987654321", # OK + "", # Empty + "abc123", # Mixed characters + "0000", # All zeros + "12.34", # With decimal point + " ", # All spaces + "-123", # With negative sign + "١٢٣", # Non-ASCII digits (should return False) + ], +) +def test_is_pure_number(text: str, function_to_test) -> None: + """Test checking for pure number strings.""" + assert is_for_else_used(function_to_test), "You must use a for-else construct" + assert function_to_test(text) == reference_is_pure_number(text) + + # # Exercise 1: Find the factors # @@ -127,7 +235,11 @@ def test_divide_until(num: int, function_to_test) -> None: def reference_find_factors(num: int) -> List[int]: """Reference solution to find the factors of an integer""" - return [m for m in range(1, num + 1) if num % m == 0] + factors = [] + for m in range(1, num + 1): + if num % m == 0: + factors.append(m) + return factors @pytest.mark.parametrize("num", [350, 487, 965, 816, 598, 443, 13, 17, 211]) @@ -145,7 +257,30 @@ def test_find_factors(num: int, function_to_test) -> None: ) -def reference_find_pair(nums: List[int]) -> int: +def reference_find_pair(nums: List[int]): + """ + Reference solutions: + - A solution with two nested loops + - A solution using a dictionary and a single loop + """ + + def find_pair_with_double_loop(nums: List[int]) -> Optional[int]: + """Two nested loops""" + for i in nums: + for j in nums: + if i + j == 2020: + return i * j + + def find_pair_with_sets(nums: List[int]) -> Optional[int]: + """Using a dictionary and a single loop""" + complements = {} + for num in nums: + if num in complements: + return num * complements[num] + complements[2020 - num] = num + + +def __reference_find_pair(nums: List[int]) -> Optional[int]: """Reference solution (part 1)""" complements = {} for num in nums: @@ -156,20 +291,39 @@ def reference_find_pair(nums: List[int]) -> int: @pytest.mark.parametrize("nums", [nums_1, nums_2]) def test_find_pair(nums: List[int], function_to_test) -> None: - assert function_to_test(nums) == reference_find_pair(nums) - - -def reference_find_triplet_slow(nums: List[int]) -> int: - """Reference solution (part 2), O(n^3)""" - n = len(nums) - for i in range(n - 2): - for j in range(i + 1, n - 1): - for k in range(j + 1, n): - if nums[i] + nums[j] + nums[k] == 2020: - return nums[i] * nums_2[j] * nums[k] - - -def reference_find_triplet(nums: List[int]) -> int: + assert function_to_test(nums) == __reference_find_pair(nums) + + +def reference_find_triplet(nums: List[int]): + """ + Reference solutions: + - A slow solution with three nested loops + - A fast solution using only two loops + """ + + def find_triplet_slow(nums: List[int]) -> Optional[int]: + """Slow solution with a triple loop""" + n = len(nums) + for i in range(n - 2): + for j in range(i + 1, n - 1): + for k in range(j + 1, n): + if nums[i] + nums[j] + nums[k] == 2020: + return nums[i] * nums[j] * nums[k] + + def find_triplet_best(nums: List[int]) -> Optional[int]: + """Fast solution with two loops""" + n = len(nums) + for i in range(n - 1): + s = set() + target_sum = 2020 - nums[i] + for j in range(i + 1, n): + last_num = target_sum - nums[j] + if last_num in s: + return nums[i] * nums[j] * last_num + s.add(nums[j]) + + +def __reference_find_triplet(nums: List[int]) -> Optional[int]: """Reference solution (part 2), O(n^2)""" n = len(nums) for i in range(n - 1): @@ -184,7 +338,7 @@ def reference_find_triplet(nums: List[int]) -> int: @pytest.mark.parametrize("nums", [nums_1, nums_2]) def test_find_triplet(nums: List[int], function_to_test) -> None: - assert function_to_test(nums) == reference_find_triplet(nums) + assert function_to_test(nums) == __reference_find_triplet(nums) # @@ -201,7 +355,7 @@ def reference_cats_with_hats() -> int: if cat % loop == 0: cats[cat] = not has_hat - return Counter(cats.values())[True] + return sum(cats.values()) def test_cats_with_hats(function_to_test) -> None: @@ -209,69 +363,158 @@ def test_cats_with_hats(function_to_test) -> None: # -# Exercise 4: Toboggan trajectory +# Exercise 4: Base converter # -def parse_data(filename: str) -> List[List[int]]: - """Parse a map of trees""" - input_data = read_data(filename).read_text() - return [ - [1 if pos == "#" else 0 for pos in line] for line in input_data.splitlines() - ] +def reference_base_converter(number: str, from_base: int, to_base: int) -> str: + """Reference solution to convert a number from one base to another""" + # Validate bases + if not (2 <= from_base <= 16 and 2 <= to_base <= 16): + err = "Bases must be between 2 and 16" + raise ValueError(err) + # Handle empty input + if not number or number.strip() in ("", "-"): + err = "Invalid empty input" + raise ValueError(err) -trees_1, trees_2 = (parse_data(f"trees_{num}.txt") for num in (1, 2)) + # Same to and from bases + if from_base == to_base: + return number + # Handle negative numbers + is_negative = number.strip().startswith("-") + number = number.strip().removeprefix("-") -def reference_toboggan_p1(trees_map: List[List[int]], right: int, down: int) -> int: - """Reference solution (part 1)""" - start, trees, depth, width = [0, 0], 0, len(trees_map), len(trees_map[0]) - while start[0] < depth: - trees += trees_map[start[0]][start[1]] - start = [start[0] + down, (start[1] + right) % width] - return trees + # Remove spaces and convert to uppercase for consistency + number = number.replace(" ", "").upper() + + # Validate digits + valid_digits = "0123456789ABCDEF" + for digit in number: + if digit not in valid_digits[:from_base]: + err = f"Invalid digit '{digit}' for base {from_base}" + raise ValueError(err) + + # Convert to base 10 + decimal = 0 + for digit in number: + decimal = decimal * from_base + valid_digits.index(digit) + + # Handle 0 as a special case + if decimal == 0: + return "0" + + if to_base == 10: + return str(decimal) + + # Convert to target base + result = "" + while decimal > 0: + digit = decimal % to_base + result += valid_digits[digit] + decimal //= to_base + + return f"-{result}" if is_negative else result + + +# We need a way to "disable" the use of `int()`, otherwise it's too easy +# Solution: replace `int()` with a function that raises an exception using a context manager +@contextlib.contextmanager +def block_int(): + """Context manager to block int() usage""" + original_int = int + + class IntReplacement: + def __call__(self, *args, **kwargs): + import inspect + + frame = inspect.currentframe() + while frame: + if frame.f_code.co_name == "solution_base_converter": + raise AssertionError("Using int() is not allowed.") # noqa: TRY003 + frame = frame.f_back + return original_int(*args, **kwargs) + + def __instancecheck__(self, instance: Any, /) -> bool: + return isinstance(instance, original_int) + + import builtins + + builtins.int = IntReplacement() + + try: + yield + finally: + builtins.int = original_int @pytest.mark.parametrize( - "trees_map, right, down", - [ - (trees_1, 3, 1), - (trees_2, 3, 1), - ], + "number,from_base,to_base", [("42", 10, 2), ("1A", 16, 2), ("1010", 2, 16)] +) +def test_base_converter_basics(number, from_base, to_base, function_to_test): + with block_int(): + expected = reference_base_converter(number, from_base, to_base) + assert function_to_test(number, from_base, to_base) == expected + + +@pytest.mark.parametrize( + "number,from_base,to_base", [("10 10", 2, 10), ("FF FF", 16, 2)] +) +def test_base_converter_with_spaces(number, from_base, to_base, function_to_test): + with block_int(): + expected = reference_base_converter(number, from_base, to_base) + assert function_to_test(number, from_base, to_base) == expected + + +@pytest.mark.parametrize("number,from_base,to_base", [("-42", 10, 2), ("-FF", 16, 10)]) +def test_base_converter_negative_numbers(number, from_base, to_base, function_to_test): + with block_int(): + expected = reference_base_converter(number, from_base, to_base) + assert function_to_test(number, from_base, to_base) == expected + + +@pytest.mark.parametrize( + "number,from_base,to_base", [("ff", 16, 10), ("FF", 16, 10), ("Ff", 16, 10)] ) -def test_toboggan_p1( - trees_map: List[List[int]], right: int, down: int, function_to_test -) -> None: - assert function_to_test(trees_map, right, down) == reference_toboggan_p1( - trees_map, right, down - ) +def test_base_converter_case_insensitive(number, from_base, to_base, function_to_test): + with block_int(): + expected = reference_base_converter(number, from_base, to_base) + assert function_to_test(number, from_base, to_base) == expected -def reference_toboggan_p2(trees_map: List[List[int]], slopes: Tuple[Tuple[int]]) -> int: - """Reference solution (part 2)""" - total = 1 - for right, down in slopes: - total *= reference_toboggan_p1(trees_map, right, down) - return total +@pytest.mark.parametrize( + "number,from_base,to_base", [("42", 1, 10), ("42", 10, 17), ("42", 0, 0)] +) +def test_base_converter_invalid_bases(number, from_base, to_base, function_to_test): + with block_int(): + with pytest.raises(ValueError): + reference_base_converter(number, from_base, to_base) + with pytest.raises(ValueError): + function_to_test(number, from_base, to_base) @pytest.mark.parametrize( - "trees_map, slopes", + "number,from_base,to_base", [ - ( - trees_1, - ((1, 1), (3, 1), (5, 1), (7, 1), (1, 2)), - ), # 9354744432 - ( - trees_2, - ((1, 1), (3, 1), (5, 1), (7, 1), (1, 2)), - ), # 1574890240 + ("2", 2, 10), # 2 not valid in base 2 + ("G", 16, 2), # G not valid in base 16 + ("9", 8, 2), # 9 not valid in base 8 ], ) -def test_toboggan_p2( - trees_map: List[List[int]], slopes: Tuple[Tuple[int]], function_to_test -) -> None: - assert function_to_test(trees_map, slopes) == reference_toboggan_p2( - trees_map, slopes - ) +def test_base_converter_invalid_digits(number, from_base, to_base, function_to_test): + with block_int(): + with pytest.raises(ValueError): + function_to_test(number, from_base, to_base) + + +@pytest.mark.parametrize( + "number,from_base,to_base", [("", 2, 2), (" ", 2, 2), ("-", 2, 2)] +) +def test_base_converter_empty_or_invalid_input( + number, from_base, to_base, function_to_test +): + with block_int(): + with pytest.raises(ValueError): + function_to_test(number, from_base, to_base) diff --git a/tutorial/tests/testsuite/helpers.py b/tutorial/tests/testsuite/helpers.py index b30d05e7..5c7f17c5 100644 --- a/tutorial/tests/testsuite/helpers.py +++ b/tutorial/tests/testsuite/helpers.py @@ -766,6 +766,7 @@ def pytest_exception_interact( outcome = ( TestOutcome.FAIL if exc.errisinstance(AssertionError) + or exc.errisinstance(pytest.fail.Exception) else TestOutcome.TEST_ERROR ) self.tests[report.nodeid] = TestCaseResult(