From 2ebcfe8656acad21011febdb1b94c0668563f7a6 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Mon, 30 Jun 2025 07:39:15 -0700 Subject: [PATCH 01/40] [Mecha Munch Management] Fixed typos in Instructions, Introduction, & Stub File (#3937) * Fixed typos in instructions, introduction, and stub file. * Changed stub docstring to iterable from dict. --- .../.docs/instructions.md | 32 +++++++++++-------- .../.docs/introduction.md | 10 +++--- .../mecha-munch-management/dict_methods.py | 2 +- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/exercises/concept/mecha-munch-management/.docs/instructions.md b/exercises/concept/mecha-munch-management/.docs/instructions.md index 7681547069f..e679db79742 100644 --- a/exercises/concept/mecha-munch-management/.docs/instructions.md +++ b/exercises/concept/mecha-munch-management/.docs/instructions.md @@ -57,24 +57,28 @@ Create the function `update_recipes(, )` that takes an "i The function should return the new/updated "ideas" dictionary. ```python ->>> update_recipes({'Banana Bread' : {'Banana': 1, 'Apple': 1, 'Walnuts': 1, 'Flour': 1, 'Eggs': 2, 'Butter': 1}, - 'Raspberry Pie' : {'Raspberry': 1, 'Orange': 1, 'Pie Crust': 1, 'Cream Custard': 1}}, -(('Banana Bread', {'Banana': 4, 'Walnuts': 2, 'Flour': 1, 'Butter': 1, 'Milk': 2, 'Eggs': 3}),)) +>>>update_recipes( + {'Banana Bread' : {'Banana': 1, 'Apple': 1, 'Walnuts': 1, 'Flour': 1, 'Eggs': 2, 'Butter': 1}, + 'Raspberry Pie' : {'Raspberry': 1, 'Orange': 1, 'Pie Crust': 1, 'Cream Custard': 1}}, + (('Banana Bread', {'Banana': 4, 'Walnuts': 2, 'Flour': 1, 'Butter': 1, 'Milk': 2, 'Eggs': 3}),) + ) ... -{'Banana Bread' : {'Banana': 4, 'Apple': 1, 'Walnuts': 2, 'Flour': 1, 'Butter': 1, 'Milk': 2, 'Eggs': 3}, - 'Raspberry Pie' : {'Raspberry': 1, 'Orange': 1, 'Pie Crust': 1, 'Cream Custard': 1}} - ->>> update_recipes({'Banana Bread' : {'Banana': 1, 'Apple': 1, 'Walnuts': 1, 'Flour': 1, 'Eggs': 2, 'Butter': 1}, - 'Raspberry Pie' : {'Raspberry': 1, 'Orange': 1, 'Pie Crust': 1, 'Cream Custard': 1}, - 'Pasta Primavera': {'Eggs': 1, 'Carrots': 1, 'Spinach': 2, 'Tomatoes': 3, 'Parmesan': 2, 'Milk': 1, 'Onion': 1}}, -[('Raspberry Pie', {'Raspberry': 3, 'Orange': 1, 'Pie Crust': 1, 'Cream Custard': 1, 'Whipped Cream': 2}), -('Pasta Primavera', {'Eggs': 1, 'Mixed Veggies': 2, 'Parmesan': 2, 'Milk': 1, 'Spinach': 1, 'Bread Crumbs': 1}), -('Blueberry Crumble', {'Blueberries': 2, 'Whipped Creme': 2, 'Granola Topping': 2, 'Yogurt': 3})]) +{'Banana Bread': {'Banana': 4, 'Walnuts': 2, 'Flour': 1, 'Butter': 1, 'Milk': 2, 'Eggs': 3}, + 'Raspberry Pie': {'Raspberry': 1, 'Orange': 1, 'Pie Crust': 1, 'Cream Custard': 1}} + +>>> update_recipes( + {'Banana Bread' : {'Banana': 1, 'Apple': 1, 'Walnuts': 1, 'Flour': 1, 'Eggs': 2, 'Butter': 1}, + 'Raspberry Pie' : {'Raspberry': 1, 'Orange': 1, 'Pie Crust': 1, 'Cream Custard': 1}, + 'Pasta Primavera': {'Eggs': 1, 'Carrots': 1, 'Spinach': 2, 'Tomatoes': 3, 'Parmesan': 2, 'Milk': 1, 'Onion': 1}}, + [('Raspberry Pie', {'Raspberry': 3, 'Orange': 1, 'Pie Crust': 1, 'Cream Custard': 1, 'Whipped Cream': 2}), + ('Pasta Primavera', {'Eggs': 1, 'Mixed Veggies': 2, 'Parmesan': 2, 'Milk': 1, 'Spinach': 1, 'Bread Crumbs': 1}), + ('Blueberry Crumble', {'Blueberries': 2, 'Whipped Creme': 2, 'Granola Topping': 2, 'Yogurt': 3})] + ) ... -{'Banana Bread': {'Banana': 1, 'Apple': 1, 'Walnuts': 1, 'Flour': 1, 'Eggs': 2, 'Butter': 1}, - 'Raspberry Pie': {'Raspberry': 3, 'Orange': 1, 'Pie Crust': 1, 'Cream Custard': 1, 'Whipped Cream': 2}, +{'Banana Bread': {'Banana': 1, 'Apple': 1, 'Walnuts': 1, 'Flour': 1, 'Eggs': 2, 'Butter': 1}, + 'Raspberry Pie': {'Raspberry': 3, 'Orange': 1, 'Pie Crust': 1, 'Cream Custard': 1, 'Whipped Cream': 2}, 'Pasta Primavera': {'Eggs': 1, 'Mixed Veggies': 2, 'Parmesan': 2, 'Milk': 1, 'Spinach': 1, 'Bread Crumbs': 1}, 'Blueberry Crumble': {'Blueberries': 2, 'Whipped Creme': 2, 'Granola Topping': 2, 'Yogurt': 3}} ``` diff --git a/exercises/concept/mecha-munch-management/.docs/introduction.md b/exercises/concept/mecha-munch-management/.docs/introduction.md index 17d67487715..b2938b8c216 100644 --- a/exercises/concept/mecha-munch-management/.docs/introduction.md +++ b/exercises/concept/mecha-munch-management/.docs/introduction.md @@ -87,7 +87,7 @@ This allows keys, values, or (`key`, `value`) pairs to be iterated over in Last- ```python >>> palette_II = {'Factory Stone Purple': '#7c677f', 'Green Treeline': '#478559', 'Purple baseline': '#161748'} -# Iterating in insertion order +# Iterating in insertion order (First in, first out) >>> for item in palette_II.items(): ... print(item) ... @@ -96,7 +96,7 @@ This allows keys, values, or (`key`, `value`) pairs to be iterated over in Last- ('Purple baseline', '#161748') -# Iterating in the reverse direction. +# Iterating in the reverse direction. (Last in, first out) >>> for item in reversed(palette_II.items()): ... print (item) ... @@ -108,12 +108,12 @@ This allows keys, values, or (`key`, `value`) pairs to be iterated over in Last- ## Sorting a Dictionary Dictionaries do not have a built-in sorting method. -However, it is possible to sort a `dict` _view_ using the built-in function `sorted()` with `.items()`. +However, it is possible to sort a `dict` _view_ using the built-in function `sorted()` with `dict.items()`. The sorted view can then be used to create a new dictionary. -Like iteration, the default sort is over dictionary `keys`. +Like iteration, the default sort is over the dictionary `keys`. ```python -# Default ordering for a dictionary is last in, first out (LIFO). +# Default ordering for a dictionary is insertion order (First in, first out). >>> color_palette = {'Grassy Green': '#9bc400', 'Purple Mountains Majesty': '#8076a3', 'Misty Mountain Pink': '#f9c5bd', diff --git a/exercises/concept/mecha-munch-management/dict_methods.py b/exercises/concept/mecha-munch-management/dict_methods.py index f502fe00ab9..92bfd7325f9 100644 --- a/exercises/concept/mecha-munch-management/dict_methods.py +++ b/exercises/concept/mecha-munch-management/dict_methods.py @@ -26,7 +26,7 @@ def update_recipes(ideas, recipe_updates): """Update the recipe ideas dictionary. :param ideas: dict - The "recipe ideas" dict. - :param recipe_updates: dict - dictionary with updates for the ideas section. + :param recipe_updates: iterable - with updates for the ideas section. :return: dict - updated "recipe ideas" dict. """ From a06cf488b95f441dd91479eaae1c37796fdc8af8 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Mon, 30 Jun 2025 08:40:39 -0700 Subject: [PATCH 02/40] Moved CI run to ubuntu-24. (#3938) --- .github/workflows/ci-workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 8764211cbe2..7bd6f0da199 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -12,7 +12,7 @@ on: jobs: housekeeping: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 From eedf04a724a32e4b344102c2279fa35d33d1d021 Mon Sep 17 00:00:00 2001 From: keiravillekode Date: Wed, 16 Jul 2025 11:25:42 +1000 Subject: [PATCH 03/40] 3 workflows use ubuntu-24.04 (#3942) --- .github/workflows/issue-commenter.yml | 2 +- .github/workflows/pr-commenter.yml | 2 +- .github/workflows/stale.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/issue-commenter.yml b/.github/workflows/issue-commenter.yml index 46319fa611c..0ec90aee053 100644 --- a/.github/workflows/issue-commenter.yml +++ b/.github/workflows/issue-commenter.yml @@ -5,7 +5,7 @@ on: jobs: comment-on-new-issue: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 name: Comments for every NEW issue. steps: - name: Checkout diff --git a/.github/workflows/pr-commenter.yml b/.github/workflows/pr-commenter.yml index 3b2592cda13..f12714aec38 100644 --- a/.github/workflows/pr-commenter.yml +++ b/.github/workflows/pr-commenter.yml @@ -4,7 +4,7 @@ on: jobs: pr-comment: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: exercism/pr-commenter-action@085ef62d2a541a112c3ade1d24deea83665ea186 with: diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 03023bc033a..b10b6011d19 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -6,7 +6,7 @@ on: jobs: stale: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 with: From a51a10e66ca838419822411438513470aa5ac7b4 Mon Sep 17 00:00:00 2001 From: Juergen H <52623305+jmh-git@users.noreply.github.com> Date: Sun, 27 Jul 2025 21:48:23 +0200 Subject: [PATCH 04/40] Update instructions.append.md (#3947) Instruction does not match the unit test. The exception message tested in the unit test is more precise. Hence, I've changed the instruction. --- .../largest-series-product/.docs/instructions.append.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/largest-series-product/.docs/instructions.append.md b/exercises/practice/largest-series-product/.docs/instructions.append.md index 5a0f9b92064..b0aa9dce025 100644 --- a/exercises/practice/largest-series-product/.docs/instructions.append.md +++ b/exercises/practice/largest-series-product/.docs/instructions.append.md @@ -10,7 +10,7 @@ To raise a `ValueError` with a message, write the message as an argument to the ```python # span of numbers is longer than number series -raise ValueError("span must be smaller than string length") +raise ValueError("span must not exceed string length") # span of number is negative raise ValueError("span must not be negative") From 0f0c01e0776b1ec94d3a96e7cace7f0e92ecf954 Mon Sep 17 00:00:00 2001 From: Sam Chukwuzube <68931805+princemuel@users.noreply.github.com> Date: Wed, 6 Aug 2025 20:49:43 +0100 Subject: [PATCH 05/40] [Pangram]: fix redundant double iteration in dig deeper solution (#3948) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue: Current implementation creates an intermediate list from a set, which is redundant and inefficient. **Before:** ```python return len([ltr for ltr in set(sentence.lower()) if ltr.isalpha()]) == 26 ``` **After:** ```python return len(set(ltr for ltr in sentence.lower() if ltr.isalpha())) == 26 ``` Why this is better: - Eliminates double iteration: Old version iterates once for set(), again for list comprehension - Removes unnecessary list creation: No need to convert set → list just to count - Better memory usage: Generator expression feeds directly into set constructor - Same time complexity but more efficient constant factors Reviewed-by: bethanyg * docs(pangram): improve approach's content and links * docs(pangram): improve approach's content and links * docs(pangram): improve performance content and links * Added princemuel as contributor --------- Co-authored-by: BethanyG --- .../practice/pangram/.approaches/config.json | 6 ++++-- .../practice/pangram/.approaches/introduction.md | 4 +--- .../pangram/.approaches/set-len/content.md | 16 +++++++--------- .../pangram/.approaches/set-len/snippet.txt | 3 +-- exercises/practice/pangram/.articles/config.json | 3 ++- .../.articles/performance/code/Benchmark.py | 2 +- .../pangram/.articles/performance/content.md | 16 ++++++++-------- .../pangram/.articles/performance/snippet.md | 10 +++++----- 8 files changed, 29 insertions(+), 31 deletions(-) diff --git a/exercises/practice/pangram/.approaches/config.json b/exercises/practice/pangram/.approaches/config.json index 550a3b5e11a..19a3b9f9735 100644 --- a/exercises/practice/pangram/.approaches/config.json +++ b/exercises/practice/pangram/.approaches/config.json @@ -1,6 +1,7 @@ { "introduction": { - "authors": ["bobahop"] + "authors": ["bobahop"], + "contributors": ["princemuel"] }, "approaches": [ { @@ -22,7 +23,8 @@ "slug": "set-len", "title": "set with len", "blurb": "Use set with len.", - "authors": ["bobahop"] + "authors": ["bobahop"], + "contributors": ["princemuel"] }, { "uuid": "0a6d1bbf-6d60-4489-b8d9-b8375894628b", diff --git a/exercises/practice/pangram/.approaches/introduction.md b/exercises/practice/pangram/.approaches/introduction.md index cf5538c0158..247348feae3 100644 --- a/exercises/practice/pangram/.approaches/introduction.md +++ b/exercises/practice/pangram/.approaches/introduction.md @@ -42,9 +42,7 @@ For more information, check the [`set` with `issubset()` approach][approach-set- ```python def is_pangram(sentence): - return len([ltr for ltr in set(sentence.lower()) if ltr.isalpha()]) \ - == 26 - + return len(set(ltr for ltr in sentence.lower() if ltr.isalpha())) == 26 ``` For more information, check the [`set` with `len()` approach][approach-set-len]. diff --git a/exercises/practice/pangram/.approaches/set-len/content.md b/exercises/practice/pangram/.approaches/set-len/content.md index b647a01d495..6c0347d5c06 100644 --- a/exercises/practice/pangram/.approaches/set-len/content.md +++ b/exercises/practice/pangram/.approaches/set-len/content.md @@ -2,20 +2,18 @@ ```python def is_pangram(sentence): - return len([ltr for ltr in set(sentence.lower()) if ltr.isalpha()]) \ - == 26 - + return len(set(ltr for ltr in sentence.lower() if ltr.isalpha())) == 26 ``` - This approach first makes a [set][set] from the [`lower`][lower]cased characters of the `sentence`. -- The characters in the `set`are then iterated in a [list comprehension][list-comprehension]. -- The characters are filtered by an `if` [`isalpha()`][isalpha] statement, so that only alphabetic characters make it into the list. -- The function returns whether the [`len()`][len] of the [`list`][list] is `26`. -If the number of unique letters in the `set` is equal to the `26` letters in the alphabet, then the function will return `True`. +- The characters are filtered using a [set comprehension][set-comprehension] with an `if` [`isalpha()`][isalpha] statement, so that only alphabetic characters make it into the set. +- The function returns whether the [`len()`][len] of the [`set`][set] is `26`. + If the number of unique [ASCII][ascii] (American Standard Code for Information Interchange) letters in the `set` is equal to the `26` letters in the [ASCII][ascii] alphabet, then the function will return `True`. +- This approach is efficient because it uses a set to eliminate duplicates and directly checks the length, which is a constant time operation. [lower]: https://docs.python.org/3/library/stdtypes.html?#str.lower [set]: https://docs.python.org/3/library/stdtypes.html?#set -[list-comprehension]: https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions +[set-comprehension]: https://realpython.com/python-set-comprehension/#introducing-set-comprehensions [isalpha]: https://docs.python.org/3/library/stdtypes.html?highlight=isalpha#str.isalpha [len]: https://docs.python.org/3/library/functions.html?#len -[list]: https://docs.python.org/3/library/stdtypes.html?#list +[ascii]: https://en.wikipedia.org/wiki/ASCII diff --git a/exercises/practice/pangram/.approaches/set-len/snippet.txt b/exercises/practice/pangram/.approaches/set-len/snippet.txt index 9a6a6d537bf..16c2ce6806a 100644 --- a/exercises/practice/pangram/.approaches/set-len/snippet.txt +++ b/exercises/practice/pangram/.approaches/set-len/snippet.txt @@ -1,3 +1,2 @@ def is_pangram(sentence): - return len([ltr for ltr in set(sentence.lower()) if ltr.isalpha()]) \ - == 26 + return len(set(ltr for ltr in sentence.lower() if ltr.isalpha())) == 26 diff --git a/exercises/practice/pangram/.articles/config.json b/exercises/practice/pangram/.articles/config.json index b7de79a678c..ec053d3d0f3 100644 --- a/exercises/practice/pangram/.articles/config.json +++ b/exercises/practice/pangram/.articles/config.json @@ -5,7 +5,8 @@ "slug": "performance", "title": "Performance deep dive", "blurb": "Deep dive to find out the most performant approach to determining a pangram.", - "authors": ["bobahop"] + "authors": ["bobahop"], + "contributors": ["princemuel"] } ] } diff --git a/exercises/practice/pangram/.articles/performance/code/Benchmark.py b/exercises/practice/pangram/.articles/performance/code/Benchmark.py index 1b423744479..6abefe1beed 100644 --- a/exercises/practice/pangram/.articles/performance/code/Benchmark.py +++ b/exercises/practice/pangram/.articles/performance/code/Benchmark.py @@ -28,7 +28,7 @@ def is_pangram(sentence): val = timeit.timeit("""is_pangram("Victor jagt zwölf_(12) Boxkämpfer quer über den großen Sylter Deich.")""", """ def is_pangram(sentence): - return len([ltr for ltr in set(sentence.lower()) if ltr.isalpha()]) == 26 + return len(set(ltr for ltr in sentence.lower() if ltr.isalpha())) == 26 """, number=loops) / loops diff --git a/exercises/practice/pangram/.articles/performance/content.md b/exercises/practice/pangram/.articles/performance/content.md index c5546e948ba..32f7fe24d5e 100644 --- a/exercises/practice/pangram/.articles/performance/content.md +++ b/exercises/practice/pangram/.articles/performance/content.md @@ -15,18 +15,18 @@ For our performance investigation, we'll also include a fourth approach that [us To benchmark the approaches, we wrote a [small benchmark application][benchmark-application] using the [`timeit`][timeit] library. ``` -all: 1.505466179997893e-05 -all: 1.6063886400021147e-05 // with sentence.casefold() -set: 1.950172399985604e-06 -len: 3.7158977999933994e-06 -bit: 8.75982620002469e-06 +all: 1.8692991019000146e-05 +all: 1.686682232399926e-05 // with sentence.casefold() +set: 2.5181135439997888e-06 +len: 5.848111433000668e-06 +bit: 1.2118699087000096e-05 ``` - The `set` `len()` approach is not as fast as the `set` `issubset()` approach. -- The `all()` approach is slower than either `set` approach. -Using `casefold` was slower than using `lower`. +- The `all()` approach is significantly slower than either `set` approach (approximately 6-8x slower). + Using `casefold()` versus `lower()` showed variable performance, with each being faster in different runs. - Although the bit field approach may be faster in other languages, it is significantly slower in Python. -It is faster than the `all()` approach, but much slower than either `set` approach. + It is faster than the `all()` approach, but much slower than either `set` approach. [benchmark-application]: https://github.com/exercism/python/blob/main/exercises/practice/pangram/.articles/performance/code/Benchmark.py [timeit]: https://docs.python.org/3/library/timeit.html diff --git a/exercises/practice/pangram/.articles/performance/snippet.md b/exercises/practice/pangram/.articles/performance/snippet.md index 0509fbee539..8542eba9fc4 100644 --- a/exercises/practice/pangram/.articles/performance/snippet.md +++ b/exercises/practice/pangram/.articles/performance/snippet.md @@ -1,7 +1,7 @@ ``` -all: 1.505466179997893e-05 -all: 1.6063886400021147e-05 // with sentence.casefold() -set: 1.950172399985604e-06 -len: 3.7158977999933994e-06 -bit: 8.75982620002469e-06 +all: 1.8692991019000146e-05 +all: 1.686682232399926e-05 // with sentence.casefold() +set: 2.5181135439997888e-06 +len: 5.848111433000668e-06 +bit: 1.2118699087000096e-05 ``` From cda2f92fd5fe3a38542b82c8466929ab16307885 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Sun, 24 Aug 2025 06:16:32 -0700 Subject: [PATCH 06/40] De-indented error checking scenarios and added variables. (#3966) --- .../practice/linked-list/.meta/template.j2 | 16 ++++-- .../practice/linked-list/linked_list_test.py | 50 ++++++++++++++----- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/exercises/practice/linked-list/.meta/template.j2 b/exercises/practice/linked-list/.meta/template.j2 index 604c5f5163c..95681821966 100644 --- a/exercises/practice/linked-list/.meta/template.j2 +++ b/exercises/practice/linked-list/.meta/template.j2 @@ -39,13 +39,23 @@ {%- if error_operation == "pop" or error_operation == "shift" %} with self.assertRaises(IndexError) as err: lst.{{ error_operation }}() - self.assertEqual(type(err.exception), IndexError) + + to_validate = err.exception + to_validate_msg = err.exception.args[0] + + self.assertEqual(type(to_validate), IndexError) + self.assertEqual(to_validate_msg, "{{ error_msg }}") + {%- elif error_operation == "delete" %} with self.assertRaises(ValueError) as err: lst.{{ error_operation }}({{ value if value else 0 }}) - self.assertEqual(type(err.exception), ValueError) + + to_validate = err.exception + to_validate_msg = err.exception.args[0] + + self.assertEqual(type(to_validate), ValueError) + self.assertEqual(to_validate_msg, "{{ error_msg }}") {%- endif %} - self.assertEqual(err.exception.args[0], "{{ error_msg }}") {%- endif %} {%- endmacro %} diff --git a/exercises/practice/linked-list/linked_list_test.py b/exercises/practice/linked-list/linked_list_test.py index 6724a1ebcef..c2c0d74e1a0 100644 --- a/exercises/practice/linked-list/linked_list_test.py +++ b/exercises/practice/linked-list/linked_list_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/linked-list/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2025-08-24 import unittest @@ -168,8 +168,12 @@ def test_using_pop_raises_an_error_if_the_list_is_empty(self): lst = LinkedList() with self.assertRaises(IndexError) as err: lst.pop() - self.assertEqual(type(err.exception), IndexError) - self.assertEqual(err.exception.args[0], "List is empty") + + to_validate = err.exception + to_validate_msg = err.exception.args[0] + + self.assertEqual(type(to_validate), IndexError) + self.assertEqual(to_validate_msg, "List is empty") def test_can_return_with_pop_and_then_raise_an_error_if_empty(self): lst = LinkedList() @@ -179,15 +183,23 @@ def test_can_return_with_pop_and_then_raise_an_error_if_empty(self): self.assertEqual(lst.pop(), 5) with self.assertRaises(IndexError) as err: lst.pop() - self.assertEqual(type(err.exception), IndexError) - self.assertEqual(err.exception.args[0], "List is empty") + + to_validate = err.exception + to_validate_msg = err.exception.args[0] + + self.assertEqual(type(to_validate), IndexError) + self.assertEqual(to_validate_msg, "List is empty") def test_using_shift_raises_an_error_if_the_list_is_empty(self): lst = LinkedList() with self.assertRaises(IndexError) as err: lst.shift() - self.assertEqual(type(err.exception), IndexError) - self.assertEqual(err.exception.args[0], "List is empty") + + to_validate = err.exception + to_validate_msg = err.exception.args[0] + + self.assertEqual(type(to_validate), IndexError) + self.assertEqual(to_validate_msg, "List is empty") def test_can_return_with_shift_and_then_raise_an_error_if_empty(self): lst = LinkedList() @@ -197,15 +209,23 @@ def test_can_return_with_shift_and_then_raise_an_error_if_empty(self): self.assertEqual(lst.shift(), 5) with self.assertRaises(IndexError) as err: lst.shift() - self.assertEqual(type(err.exception), IndexError) - self.assertEqual(err.exception.args[0], "List is empty") + + to_validate = err.exception + to_validate_msg = err.exception.args[0] + + self.assertEqual(type(to_validate), IndexError) + self.assertEqual(to_validate_msg, "List is empty") def test_using_delete_raises_an_error_if_the_list_is_empty(self): lst = LinkedList() with self.assertRaises(ValueError) as err: lst.delete(0) - self.assertEqual(type(err.exception), ValueError) - self.assertEqual(err.exception.args[0], "Value not found") + + to_validate = err.exception + to_validate_msg = err.exception.args[0] + + self.assertEqual(type(to_validate), ValueError) + self.assertEqual(to_validate_msg, "Value not found") def test_using_delete_raises_an_error_if_the_value_is_not_found(self): lst = LinkedList() @@ -214,5 +234,9 @@ def test_using_delete_raises_an_error_if_the_value_is_not_found(self): self.assertEqual(lst.pop(), 7) with self.assertRaises(ValueError) as err: lst.delete(0) - self.assertEqual(type(err.exception), ValueError) - self.assertEqual(err.exception.args[0], "Value not found") + + to_validate = err.exception + to_validate_msg = err.exception.args[0] + + self.assertEqual(type(to_validate), ValueError) + self.assertEqual(to_validate_msg, "Value not found") From aa0aa58c09584a1be80f6282bc7dd6f7f092aa26 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Wed, 27 Aug 2025 22:37:27 -0700 Subject: [PATCH 07/40] August sync of docs to problem specs. (#3968) --- exercises/practice/luhn/.docs/instructions.md | 2 +- exercises/practice/say/.docs/instructions.md | 52 +++---------------- exercises/practice/say/.docs/introduction.md | 6 +++ .../practice/triangle/.docs/instructions.md | 5 ++ 4 files changed, 20 insertions(+), 45 deletions(-) create mode 100644 exercises/practice/say/.docs/introduction.md diff --git a/exercises/practice/luhn/.docs/instructions.md b/exercises/practice/luhn/.docs/instructions.md index df2e304a39b..7702c6bbb5f 100644 --- a/exercises/practice/luhn/.docs/instructions.md +++ b/exercises/practice/luhn/.docs/instructions.md @@ -41,7 +41,7 @@ If the sum is evenly divisible by 10, the original number is valid. ### Invalid Canadian SIN -The number to be checked is `066 123 468`. +The number to be checked is `066 123 478`. We start at the end of the number and double every second digit, beginning with the second digit from the right and moving left. diff --git a/exercises/practice/say/.docs/instructions.md b/exercises/practice/say/.docs/instructions.md index ad3d347782e..3251c519ace 100644 --- a/exercises/practice/say/.docs/instructions.md +++ b/exercises/practice/say/.docs/instructions.md @@ -1,48 +1,12 @@ # Instructions -Given a number from 0 to 999,999,999,999, spell out that number in English. +Given a number, your task is to express it in English words exactly as your friend should say it out loud. +Yaʻqūb expects to use numbers from 0 up to 999,999,999,999. -## Step 1 +Examples: -Handle the basic case of 0 through 99. - -If the input to the program is `22`, then the output should be `'twenty-two'`. - -Your program should complain loudly if given a number outside the blessed range. - -Some good test cases for this program are: - -- 0 -- 14 -- 50 -- 98 -- -1 -- 100 - -### Extension - -If you're on a Mac, shell out to Mac OS X's `say` program to talk out loud. -If you're on Linux or Windows, eSpeakNG may be available with the command `espeak`. - -## Step 2 - -Implement breaking a number up into chunks of thousands. - -So `1234567890` should yield a list like 1, 234, 567, and 890, while the far simpler `1000` should yield just 1 and 0. - -## Step 3 - -Now handle inserting the appropriate scale word between those chunks. - -So `1234567890` should yield `'1 billion 234 million 567 thousand 890'` - -The program must also report any values that are out of range. -It's fine to stop at "trillion". - -## Step 4 - -Put it all together to get nothing but plain English. - -`12345` should give `twelve thousand three hundred forty-five`. - -The program must also report any values that are out of range. +- 0 → zero +- 1 → one +- 12 → twelve +- 123 → one hundred twenty-three +- 1,234 → one thousand two hundred thirty-four diff --git a/exercises/practice/say/.docs/introduction.md b/exercises/practice/say/.docs/introduction.md new file mode 100644 index 00000000000..abd22851ef7 --- /dev/null +++ b/exercises/practice/say/.docs/introduction.md @@ -0,0 +1,6 @@ +# Introduction + +Your friend Yaʻqūb works the counter at the busiest deli in town, slicing, weighing, and wrapping orders for a never-ending line of hungry customers. +To keep things moving, each customer takes a numbered ticket when they arrive. + +When it’s time to call the next person, Yaʻqūb reads their number out loud, always in full English words to make sure everyone hears it clearly. diff --git a/exercises/practice/triangle/.docs/instructions.md b/exercises/practice/triangle/.docs/instructions.md index ac39008726d..755cb8d19d3 100644 --- a/exercises/practice/triangle/.docs/instructions.md +++ b/exercises/practice/triangle/.docs/instructions.md @@ -13,6 +13,11 @@ A _scalene_ triangle has all sides of different lengths. For a shape to be a triangle at all, all sides have to be of length > 0, and the sum of the lengths of any two sides must be greater than or equal to the length of the third side. +~~~~exercism/note +We opted to not include tests for degenerate triangles (triangles that violate these rules) to keep things simpler. +You may handle those situations if you wish to do so, or safely ignore them. +~~~~ + In equations: Let `a`, `b`, and `c` be sides of the triangle. From 91eccfbf0a947d16f0ef74be181fe5095c143e73 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Wed, 27 Aug 2025 22:59:35 -0700 Subject: [PATCH 08/40] [Variable Length Quantity]: Updated tests.toml & Regenerated Test Cases (#3969) * Updated tests.toml and regenerated test cases. * Regenerated tests under Python 3.11.5, since 3.13.2 was causing CI failure. --- .../variable-length-quantity/.meta/tests.toml | 80 ++++++++++++------- .../variable_length_quantity_test.py | 17 +++- 2 files changed, 67 insertions(+), 30 deletions(-) diff --git a/exercises/practice/variable-length-quantity/.meta/tests.toml b/exercises/practice/variable-length-quantity/.meta/tests.toml index 923fa0c1aae..53be789a382 100644 --- a/exercises/practice/variable-length-quantity/.meta/tests.toml +++ b/exercises/practice/variable-length-quantity/.meta/tests.toml @@ -1,81 +1,103 @@ -# This is an auto-generated file. Regular comments will be removed when this -# file is regenerated. Regenerating will not touch any manually added keys, -# so comments can be added in a "comment" key. +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. [35c9db2e-f781-4c52-b73b-8e76427defd0] -description = "zero" +description = "Encode a series of integers, producing a series of bytes. -> zero" [be44d299-a151-4604-a10e-d4b867f41540] -description = "arbitrary single byte" +description = "Encode a series of integers, producing a series of bytes. -> arbitrary single byte" + +[890bc344-cb80-45af-b316-6806a6971e81] +description = "Encode a series of integers, producing a series of bytes. -> asymmetric single byte" [ea399615-d274-4af6-bbef-a1c23c9e1346] -description = "largest single byte" +description = "Encode a series of integers, producing a series of bytes. -> largest single byte" [77b07086-bd3f-4882-8476-8dcafee79b1c] -description = "smallest double byte" +description = "Encode a series of integers, producing a series of bytes. -> smallest double byte" [63955a49-2690-4e22-a556-0040648d6b2d] -description = "arbitrary double byte" +description = "Encode a series of integers, producing a series of bytes. -> arbitrary double byte" + +[4977d113-251b-4d10-a3ad-2f5a7756bb58] +description = "Encode a series of integers, producing a series of bytes. -> asymmetric double byte" [29da7031-0067-43d3-83a7-4f14b29ed97a] -description = "largest double byte" +description = "Encode a series of integers, producing a series of bytes. -> largest double byte" [3345d2e3-79a9-4999-869e-d4856e3a8e01] -description = "smallest triple byte" +description = "Encode a series of integers, producing a series of bytes. -> smallest triple byte" [5df0bc2d-2a57-4300-a653-a75ee4bd0bee] -description = "arbitrary triple byte" +description = "Encode a series of integers, producing a series of bytes. -> arbitrary triple byte" + +[6731045f-1e00-4192-b5ae-98b22e17e9f7] +description = "Encode a series of integers, producing a series of bytes. -> asymmetric triple byte" [f51d8539-312d-4db1-945c-250222c6aa22] -description = "largest triple byte" +description = "Encode a series of integers, producing a series of bytes. -> largest triple byte" [da78228b-544f-47b7-8bfe-d16b35bbe570] -description = "smallest quadruple byte" +description = "Encode a series of integers, producing a series of bytes. -> smallest quadruple byte" [11ed3469-a933-46f1-996f-2231e05d7bb6] -description = "arbitrary quadruple byte" +description = "Encode a series of integers, producing a series of bytes. -> arbitrary quadruple byte" + +[b45ef770-cbba-48c2-bd3c-c6362679516e] +description = "Encode a series of integers, producing a series of bytes. -> asymmetric quadruple byte" [d5f3f3c3-e0f1-4e7f-aad0-18a44f223d1c] -description = "largest quadruple byte" +description = "Encode a series of integers, producing a series of bytes. -> largest quadruple byte" [91a18b33-24e7-4bfb-bbca-eca78ff4fc47] -description = "smallest quintuple byte" +description = "Encode a series of integers, producing a series of bytes. -> smallest quintuple byte" [5f34ff12-2952-4669-95fe-2d11b693d331] -description = "arbitrary quintuple byte" +description = "Encode a series of integers, producing a series of bytes. -> arbitrary quintuple byte" + +[9be46731-7cd5-415c-b960-48061cbc1154] +description = "Encode a series of integers, producing a series of bytes. -> asymmetric quintuple byte" [7489694b-88c3-4078-9864-6fe802411009] -description = "maximum 32-bit integer input" +description = "Encode a series of integers, producing a series of bytes. -> maximum 32-bit integer input" [f9b91821-cada-4a73-9421-3c81d6ff3661] -description = "two single-byte values" +description = "Encode a series of integers, producing a series of bytes. -> two single-byte values" [68694449-25d2-4974-ba75-fa7bb36db212] -description = "two multi-byte values" +description = "Encode a series of integers, producing a series of bytes. -> two multi-byte values" [51a06b5c-de1b-4487-9a50-9db1b8930d85] -description = "many multi-byte values" +description = "Encode a series of integers, producing a series of bytes. -> many multi-byte values" [baa73993-4514-4915-bac0-f7f585e0e59a] -description = "one byte" +description = "Decode a series of bytes, producing a series of integers. -> one byte" [72e94369-29f9-46f2-8c95-6c5b7a595aee] -description = "two bytes" +description = "Decode a series of bytes, producing a series of integers. -> two bytes" [df5a44c4-56f7-464e-a997-1db5f63ce691] -description = "three bytes" +description = "Decode a series of bytes, producing a series of integers. -> three bytes" [1bb58684-f2dc-450a-8406-1f3452aa1947] -description = "four bytes" +description = "Decode a series of bytes, producing a series of integers. -> four bytes" [cecd5233-49f1-4dd1-a41a-9840a40f09cd] -description = "maximum 32-bit integer" +description = "Decode a series of bytes, producing a series of integers. -> maximum 32-bit integer" [e7d74ba3-8b8e-4bcb-858d-d08302e15695] -description = "incomplete sequence causes error" +description = "Decode a series of bytes, producing a series of integers. -> incomplete sequence causes error" [aa378291-9043-4724-bc53-aca1b4a3fcb6] -description = "incomplete sequence causes error, even if value is zero" +description = "Decode a series of bytes, producing a series of integers. -> incomplete sequence causes error, even if value is zero" [a91e6f5a-c64a-48e3-8a75-ce1a81e0ebee] -description = "multiple values" +description = "Decode a series of bytes, producing a series of integers. -> multiple values" diff --git a/exercises/practice/variable-length-quantity/variable_length_quantity_test.py b/exercises/practice/variable-length-quantity/variable_length_quantity_test.py index baeb2365430..e059f82ee3f 100644 --- a/exercises/practice/variable-length-quantity/variable_length_quantity_test.py +++ b/exercises/practice/variable-length-quantity/variable_length_quantity_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/variable-length-quantity/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2025-08-28 import unittest @@ -17,6 +17,9 @@ def test_zero(self): def test_arbitrary_single_byte(self): self.assertEqual(encode([0x40]), [0x40]) + def test_asymmetric_single_byte(self): + self.assertEqual(encode([0x53]), [0x53]) + def test_largest_single_byte(self): self.assertEqual(encode([0x7F]), [0x7F]) @@ -26,6 +29,9 @@ def test_smallest_double_byte(self): def test_arbitrary_double_byte(self): self.assertEqual(encode([0x2000]), [0xC0, 0x0]) + def test_asymmetric_double_byte(self): + self.assertEqual(encode([0xAD]), [0x81, 0x2D]) + def test_largest_double_byte(self): self.assertEqual(encode([0x3FFF]), [0xFF, 0x7F]) @@ -35,6 +41,9 @@ def test_smallest_triple_byte(self): def test_arbitrary_triple_byte(self): self.assertEqual(encode([0x100000]), [0xC0, 0x80, 0x0]) + def test_asymmetric_triple_byte(self): + self.assertEqual(encode([0x1D59C]), [0x87, 0xAB, 0x1C]) + def test_largest_triple_byte(self): self.assertEqual(encode([0x1FFFFF]), [0xFF, 0xFF, 0x7F]) @@ -44,6 +53,9 @@ def test_smallest_quadruple_byte(self): def test_arbitrary_quadruple_byte(self): self.assertEqual(encode([0x8000000]), [0xC0, 0x80, 0x80, 0x0]) + def test_asymmetric_quadruple_byte(self): + self.assertEqual(encode([0x357704]), [0x81, 0xD5, 0xEE, 0x4]) + def test_largest_quadruple_byte(self): self.assertEqual(encode([0xFFFFFFF]), [0xFF, 0xFF, 0xFF, 0x7F]) @@ -53,6 +65,9 @@ def test_smallest_quintuple_byte(self): def test_arbitrary_quintuple_byte(self): self.assertEqual(encode([0xFF000000]), [0x8F, 0xF8, 0x80, 0x80, 0x0]) + def test_asymmetric_quintuple_byte(self): + self.assertEqual(encode([0x86656105]), [0x88, 0xB3, 0x95, 0xC2, 0x5]) + def test_maximum_32_bit_integer_input(self): self.assertEqual(encode([0xFFFFFFFF]), [0x8F, 0xFF, 0xFF, 0xFF, 0x7F]) From 3297093c98982acebf21549b3f3d4b1be4add029 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 00:16:42 -0700 Subject: [PATCH 09/40] Bump actions/checkout from 4.2.2 to 5.0.0 (#3975) Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.2 to 5.0.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/11bd71901bbe5b1630ceea73d27597364c9af683...08c6903cd8c0fde910a37f88322edcfb5dd907a8) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 5.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-workflow.yml | 4 ++-- .github/workflows/issue-commenter.yml | 2 +- .github/workflows/test-runner.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 7bd6f0da199..1279566e79b 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -14,7 +14,7 @@ jobs: housekeeping: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 - name: Set up Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 @@ -55,7 +55,7 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9, 3.10.6, 3.11.2] steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 with: diff --git a/.github/workflows/issue-commenter.yml b/.github/workflows/issue-commenter.yml index 0ec90aee053..5472e7d95e7 100644 --- a/.github/workflows/issue-commenter.yml +++ b/.github/workflows/issue-commenter.yml @@ -9,7 +9,7 @@ jobs: name: Comments for every NEW issue. steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 - name: Read issue-comment.md id: issue-comment diff --git a/.github/workflows/test-runner.yml b/.github/workflows/test-runner.yml index 88c348a3662..428be225caa 100644 --- a/.github/workflows/test-runner.yml +++ b/.github/workflows/test-runner.yml @@ -10,6 +10,6 @@ jobs: test-runner: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 - name: Run test-runner run: docker compose run test-runner From 64ce10de654150a21ff13b1ee94984a0284e44de Mon Sep 17 00:00:00 2001 From: BethanyG Date: Mon, 22 Sep 2025 14:16:45 -0700 Subject: [PATCH 10/40] [Cater Waiter & Sets Concept]: Better Differentiation of Emoji Examples in Introduction & About Files (#3991) * Changed emoji examples in indtroduction to be more differentiated. * Differentiated emoji examples in about.md doc as well. * Change one more example to differentiate. --- concepts/sets/about.md | 32 ++++++++++--------- .../cater-waiter/.docs/introduction.md | 20 ++++++------ 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/concepts/sets/about.md b/concepts/sets/about.md index 058be5c7def..204df380577 100644 --- a/concepts/sets/about.md +++ b/concepts/sets/about.md @@ -34,12 +34,13 @@ While sets can be created in many different ways, the most straightforward const A `set` can be directly entered as a _set literal_ with curly `{}` brackets and commas between elements. Duplicates are silently omitted: + ```python ->>> one_element = {'😀'} -{'😀'} +>>> one_element = {'➕'} +{'➕'} ->>> multiple_elements = {'😀', '😃', '😄', '😁'} -{'😀', '😃', '😄', '😁'} +>>> multiple_elements = {'➕', '🔻', '🔹', '🔆'} +{'➕', '🔻', '🔹', '🔆'} >>> multiple_duplicates = {'Hello!', 'Hello!', 'Hello!', '¡Hola!','Привіт!', 'こんにちは!', @@ -108,9 +109,9 @@ Remember: sets can hold different datatypes and _nested_ datatypes, but all `set ```python # Attempting to use a list for a set member throws a TypeError ->>> lists_as_elements = {['😅','🤣'], - ['😂','🙂','🙃'], - ['😜', '🤪', '😝']} +>>> lists_as_elements = {['🌈','💦'], + ['☁️','⭐️','🌍'], + ['⛵️', '🚲', '🚀']} Traceback (most recent call last): File "", line 1, in @@ -118,9 +119,9 @@ TypeError: unhashable type: 'list' # Standard sets are mutable, so they cannot be hashed. ->>> sets_as_elements = {{'😅','🤣'}, - {'😂','🙂','🙃'}, - {'😜', '🤪', '😝'}} +>>> sets_as_elements = {{'🌈','💦'}, + {'☁️','⭐️','🌍'}, + {'⛵️', '🚲', '🚀'}} Traceback (most recent call last): File "", line 1, in @@ -131,14 +132,15 @@ However, a `set` of `sets` can be created via type `frozenset()`: ```python # Frozensets don't have a literal form. ->>> set_1 = frozenset({'😜', '😝', '🤪'}) ->>> set_2 = frozenset({'😅', '🤣'}) ->>> set_3 = frozenset({'😂', '🙂', '🙃'}) +>>> set_1 = frozenset({'🌈','💦'}) +>>> set_2 = frozenset({'☁️','⭐️','🌍'}) +>>> set_3 = frozenset({'⛵️', '🚲', '🚀'}) >>> frozen_sets_as_elements = {set_1, set_2, set_3} >>> frozen_sets_as_elements -{frozenset({'😜', '😝', '🤪'}), frozenset({'😅', '🤣'}), -frozenset({'😂', '🙂', '🙃'})} +{frozenset({'⛵️', '🚀', '🚲'}), + frozenset({'🌈', '💦'}), + frozenset({'☁️', '⭐️', '🌍'})} ``` diff --git a/exercises/concept/cater-waiter/.docs/introduction.md b/exercises/concept/cater-waiter/.docs/introduction.md index 926aea3d906..0993c4f0aa2 100644 --- a/exercises/concept/cater-waiter/.docs/introduction.md +++ b/exercises/concept/cater-waiter/.docs/introduction.md @@ -32,11 +32,11 @@ A `set` can be directly entered as a _set literal_ with curly `{}` brackets and Duplicates are silently omitted: ```python ->>> one_element = {'😀'} -{'😀'} +>>> one_element = {'➕'} +{'➕'} ->>> multiple_elements = {'😀', '😃', '😄', '😁'} -{'😀', '😃', '😄', '😁'} +>>> multiple_elements = {'➕', '🔻', '🔹', '🔆'} +{'➕', '🔻', '🔹', '🔆'} >>> multiple_duplicates = {'Hello!', 'Hello!', 'Hello!', '¡Hola!','Привіт!', 'こんにちは!', @@ -91,9 +91,9 @@ Sets can hold different datatypes and _nested_ datatypes, but all `set` elements ```python # Attempting to use a list for a set member throws a TypeError ->>> lists_as_elements = {['😅','🤣'], - ['😂','🙂','🙃'], - ['😜', '🤪', '😝']} +>>> lists_as_elements = {['🌈','💦'], + ['☁️','⭐️','🌍'], + ['⛵️', '🚲', '🚀']} Traceback (most recent call last): File "", line 1, in @@ -101,9 +101,9 @@ TypeError: unhashable type: 'list' # Standard sets are mutable, so they cannot be hashed. ->>> sets_as_elements = {{'😅','🤣'}, - {'😂','🙂','🙃'}, - {'😜', '🤪', '😝'}} +>>> sets_as_elements = {{'🌈','💦'}, + {'☁️','⭐️','🌍'}, + {'⛵️', '🚲', '🚀'}} Traceback (most recent call last): File "", line 1, in From 67d865b48e310810f0ca8e282ae7f6269d0b088b Mon Sep 17 00:00:00 2001 From: BethanyG Date: Mon, 22 Sep 2025 14:44:15 -0700 Subject: [PATCH 11/40] Fixed grammatical error in paragraph describing * with zip(). (#3992) --- concepts/unpacking-and-multiple-assignment/about.md | 5 +++-- exercises/concept/locomotive-engineer/.docs/introduction.md | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/concepts/unpacking-and-multiple-assignment/about.md b/concepts/unpacking-and-multiple-assignment/about.md index a24e2c6d1a5..d4b9168ad13 100644 --- a/concepts/unpacking-and-multiple-assignment/about.md +++ b/concepts/unpacking-and-multiple-assignment/about.md @@ -364,8 +364,8 @@ numbers = [1, 2, 3] 1 ``` -Using `*` unpacking with the `zip()` function is another common use case. -Since `zip()` takes multiple iterables and returns a `list` of `tuples` with the values from each `iterable` grouped: +Using `*` unpacking with the [`zip()` built-in][zip] is another common use case. +The `zip()` function takes multiple iterables and returns a `list` of `tuples` with the values from each `iterable` grouped: ```python >>> values = (['x', 'y', 'z'], [1, 2, 3], [True, False, True]) @@ -383,3 +383,4 @@ Since `zip()` takes multiple iterables and returns a `list` of `tuples` with the [sorting algorithms]: https://realpython.com/sorting-algorithms-python/ [unpacking]: https://www.geeksforgeeks.org/unpacking-arguments-in-python/?ref=rp [view-objects]: https://docs.python.org/3/library/stdtypes.html#dict-views +[zip]: https://docs.python.org/3/library/functions.html#zip diff --git a/exercises/concept/locomotive-engineer/.docs/introduction.md b/exercises/concept/locomotive-engineer/.docs/introduction.md index 39ba5b49090..b10fff1217f 100644 --- a/exercises/concept/locomotive-engineer/.docs/introduction.md +++ b/exercises/concept/locomotive-engineer/.docs/introduction.md @@ -351,8 +351,8 @@ numbers = [1, 2, 3] 1 ``` -Using `*` unpacking with the `zip()` function is another common use case. -Since `zip()` takes multiple iterables and returns a `list` of `tuples` with the values from each `iterable` grouped: +Using `*` unpacking with the [`zip()` built-in][zip] is another common use case. +The `zip()` function takes multiple iterables and returns a `list` of `tuples` with the values from each `iterable` grouped: ```python >>> values = (['x', 'y', 'z'], [1, 2, 3], [True, False, True]) @@ -369,3 +369,4 @@ Since `zip()` takes multiple iterables and returns a `list` of `tuples` with the [sorting algorithms]: https://realpython.com/sorting-algorithms-python/ [unpacking]: https://www.geeksforgeeks.org/unpacking-arguments-in-python/?ref=rp [view-objects]: https://docs.python.org/3/library/stdtypes.html#dict-views +[zip]: https://docs.python.org/3/library/functions.html#zip From ab7852e88ed0ba9cab5b1123b6d66132fa6ee674 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Mon, 22 Sep 2025 15:20:20 -0700 Subject: [PATCH 12/40] [Meltdown Mitigation]: Corrected "critical" to "balanced in criticality" on line 15 of Instructions (#3993) * Corrected critical to balanced in criticaltiy on line 15 of intructions. * Corrected docstring to refer to balanced in criticality. --- exercises/concept/meltdown-mitigation/.docs/instructions.md | 4 ++-- exercises/concept/meltdown-mitigation/conditionals.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/exercises/concept/meltdown-mitigation/.docs/instructions.md b/exercises/concept/meltdown-mitigation/.docs/instructions.md index 3d6d96d0cdb..cd8995de8a1 100644 --- a/exercises/concept/meltdown-mitigation/.docs/instructions.md +++ b/exercises/concept/meltdown-mitigation/.docs/instructions.md @@ -11,8 +11,8 @@ The following three tasks are all related to writing code for maintaining ideal ## 1. Check for criticality -The first thing a control system has to do is check if the reactor is balanced in criticality. -A reactor is said to be critical if it satisfies the following conditions: +The first thing a control system has to do is check if the reactor is _balanced in criticality_. +A reactor is said to be balanced in criticality if it satisfies the following conditions: - The temperature is less than 800 K. - The number of neutrons emitted per second is greater than 500. diff --git a/exercises/concept/meltdown-mitigation/conditionals.py b/exercises/concept/meltdown-mitigation/conditionals.py index 1eb0a571ff5..ff5769d8352 100644 --- a/exercises/concept/meltdown-mitigation/conditionals.py +++ b/exercises/concept/meltdown-mitigation/conditionals.py @@ -8,7 +8,7 @@ def is_criticality_balanced(temperature, neutrons_emitted): :param neutrons_emitted: int or float - number of neutrons emitted per second. :return: bool - is criticality balanced? - A reactor is said to be critical if it satisfies the following conditions: + A reactor is said to be balanced in criticality if it satisfies the following conditions: - The temperature is less than 800 K. - The number of neutrons emitted per second is greater than 500. - The product of temperature and neutrons emitted per second is less than 500000. From fbc67692497ab85f0b7678cb7cba35c69d026e86 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Mon, 22 Sep 2025 17:32:05 -0700 Subject: [PATCH 13/40] September exercise sync of two-bucket tests. (#3994) New tests were added and solutions need to be re-run. --- exercises/practice/two-bucket/.meta/tests.toml | 6 ++++++ exercises/practice/two-bucket/two_bucket_test.py | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/exercises/practice/two-bucket/.meta/tests.toml b/exercises/practice/two-bucket/.meta/tests.toml index d6ff02f53e5..a3fe533ece6 100644 --- a/exercises/practice/two-bucket/.meta/tests.toml +++ b/exercises/practice/two-bucket/.meta/tests.toml @@ -27,6 +27,12 @@ description = "Measure one step using bucket one of size 1 and bucket two of siz [eb329c63-5540-4735-b30b-97f7f4df0f84] description = "Measure using bucket one of size 2 and bucket two of size 3 - start with bucket one and end with bucket two" +[58d70152-bf2b-46bb-ad54-be58ebe94c03] +description = "Measure using bucket one much bigger than bucket two" + +[9dbe6499-caa5-4a58-b5ce-c988d71b8981] +description = "Measure using bucket one much smaller than bucket two" + [449be72d-b10a-4f4b-a959-ca741e333b72] description = "Not possible to reach the goal" diff --git a/exercises/practice/two-bucket/two_bucket_test.py b/exercises/practice/two-bucket/two_bucket_test.py index b7d1cc01953..d097866e5b3 100644 --- a/exercises/practice/two-bucket/two_bucket_test.py +++ b/exercises/practice/two-bucket/two_bucket_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/two-bucket/canonical-data.json -# File last updated on 2023-07-21 +# File last updated on 2025-09-23 import unittest @@ -40,6 +40,12 @@ def test_measure_using_bucket_one_of_size_2_and_bucket_two_of_size_3_start_with_ ): self.assertEqual(measure(2, 3, 3, "one"), (2, "two", 2)) + def test_measure_using_bucket_one_much_bigger_than_bucket_two(self): + self.assertEqual(measure(5, 1, 2, "one"), (6, "one", 1)) + + def test_measure_using_bucket_one_much_smaller_than_bucket_two(self): + self.assertEqual(measure(3, 15, 9, "one"), (6, "two", 0)) + def test_not_possible_to_reach_the_goal(self): with self.assertRaisesWithMessage(ValueError): measure(6, 15, 5, "one") From cb300edc5c48ee76bac397293fa7aa0311505c88 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Fri, 26 Sep 2025 12:36:46 -0700 Subject: [PATCH 14/40] [Little Sister's Vocab]: Fixed up Code Examples for `str.join()` & Added an Additional Hint. (#3995) * Fixed up code examples for join and added an additional hint. * Touched up hints phrasing, added no loop directive to instructions, and added additional examples to concept about. * Typo correction. * Corrected separator misspelling. * Cleaned up in-line comments per PR review. * Fixed capitalization on inline comments in last join example. --- concepts/strings/about.md | 38 +++++++++++++++++-- .../little-sisters-vocab/.docs/hints.md | 7 +++- .../.docs/instructions.md | 3 ++ .../.docs/introduction.md | 32 ++++++++++++++-- 4 files changed, 71 insertions(+), 9 deletions(-) diff --git a/concepts/strings/about.md b/concepts/strings/about.md index 0107f6e70f0..064c4c11bcb 100644 --- a/concepts/strings/about.md +++ b/concepts/strings/about.md @@ -9,7 +9,7 @@ The Python docs also provide a very detailed [unicode HOWTO][unicode how-to] tha Strings implement all [common sequence operations][common sequence operations] and can be iterated through using `for item in ` or `for index, item in enumerate()` syntax. Individual code points (_strings of length 1_) can be referenced by `0-based index` number from the left, or `-1-based index` number from the right. -Strings can be concatenated with `+`, or via `.join()`, split via `.split()`, and offer multiple formatting and assembly options. +Strings can be concatenated with `+`, or via `.join()`, split via `.split()`, and offer multiple formatting, assembly, and templating options. A `str` literal can be declared via single `'` or double `"` quotes. The escape `\` character is available as needed. @@ -168,12 +168,12 @@ sentence = word + " " + "means" + " " + number + " in " + language + "." "дев'ять means nine in Ukrainian." ``` -If a `list`, `tuple`, `set` or other collection of individual strings needs to be combined into a single `str`, [`.join()`][str-join], is a better option: +If a `list`, `tuple`, `set` or other collection of individual strings needs to be combined into a single `str`, [`.join()`][str-join] is a better option: ```python # str.join() makes a new string from the iterables elements. ->>> chickens = ["hen", "egg", "rooster"] +>>> chickens = ["hen", "egg", "rooster"] # Lists are iterable. >>> ' '.join(chickens) 'hen egg rooster' @@ -183,6 +183,34 @@ If a `list`, `tuple`, `set` or other collection of individual strings needs to b >>> ' 🌿 '.join(chickens) 'hen 🌿 egg 🌿 rooster' + + +# Any iterable can be used as input. +>>> flowers = ("rose", "daisy", "carnation") # Tuples are iterable. +>>> '*-*'.join(flowers) +'rose*-*daisy*-*carnation' + +>>> flowers = {"rose", "daisy", "carnation"} # Sets are iterable, but output order is not guaranteed. +>>> '*-*'.join(flowers) +'rose*-*carnation*-*daisy' + +>>> phrase = "This is my string" # Strings are iterable, but be careful! +>>> '..'.join(phrase) +'T..h..i..s.. ..i..s.. ..m..y.. ..s..t..r..i..n..g' + + +# Separators are inserted **between** elements, but can be any string (including spaces). +# This can be exploited for interesting effects. +>>> under_words = ['under', 'current', 'sea', 'pin', 'dog', 'lay'] +>>> separator = ' ⤴️ under' # Note the leading space, but no trailing space. +>>> separator.join(under_words) +'under ⤴️ undercurrent ⤴️ undersea ⤴️ underpin ⤴️ underdog ⤴️ underlay' + +# The separator can be composed different ways, as long as the result is a string. +>>> upper_words = ['upper', 'crust', 'case', 'classmen', 'most', 'cut'] +>>> separator = ' 🌟 ' + upper_words[0] # This becomes one string, similar to ' ⤴️ under'. +>>> separator.join(upper_words) + 'upper 🌟 uppercrust 🌟 uppercase 🌟 upperclassmen 🌟 uppermost 🌟 uppercut' ``` Strings support all [common sequence operations][common sequence operations]. @@ -194,7 +222,9 @@ Indexes _with_ items can be iterated through in a loop via `for index, item in e >>> exercise = 'လေ့ကျင့်' -# Note that there are more code points than perceived glyphs or characters +# Note that there are more code points than perceived glyphs or characters. +# Care should be used when iterating over languages that use +# combining characters, or when dealing with emoji. >>> for code_point in exercise: ... print(code_point) ... diff --git a/exercises/concept/little-sisters-vocab/.docs/hints.md b/exercises/concept/little-sisters-vocab/.docs/hints.md index 2e5540805c4..0be143a7f66 100644 --- a/exercises/concept/little-sisters-vocab/.docs/hints.md +++ b/exercises/concept/little-sisters-vocab/.docs/hints.md @@ -14,8 +14,11 @@ There's four activities in the assignment, each with a set of text or words to w ## 2. Add prefixes to word groups -- Believe it or not, [`str.join()`][str-join] is all you need here. -- Like [`str.split()`][str-split]`, `str.join()` can take an arbitrary-length string, made up of any unicode code points. +- Believe it or not, [`str.join()`][str-join] is all you need here. **A loop is not required**. +- The tests will be feeding your function a `list`. There will be no need to alter this `list` if you can figure out a good delimiter string. +- Remember that delimiter strings go between elements and "glue" them together into a single string. Delimiters are inserted _without_ space, although you can include space characters within them. +- Like [`str.split()`][str-split], `str.join()` can process an arbitrary-length string, made up of any unicode code points. _Unlike_ `str.split()`, it can also process arbitrary-length iterables like `list`, `tuple`, and `set`. + ## 3. Remove a suffix from a word diff --git a/exercises/concept/little-sisters-vocab/.docs/instructions.md b/exercises/concept/little-sisters-vocab/.docs/instructions.md index 2658bb980a4..991845a7043 100644 --- a/exercises/concept/little-sisters-vocab/.docs/instructions.md +++ b/exercises/concept/little-sisters-vocab/.docs/instructions.md @@ -40,6 +40,9 @@ Implement the `make_word_groups()` function that takes a `vocab_wor `[, , .... ]`, and returns a string with the prefix applied to each word that looks like: `' :: :: :: '`. +Creating a `for` or `while` loop to process the input is not needed here. +Think carefully about which string methods (and delimiters) you could use instead. + ```python >>> make_word_groups(['en', 'close', 'joy', 'lighten']) diff --git a/exercises/concept/little-sisters-vocab/.docs/introduction.md b/exercises/concept/little-sisters-vocab/.docs/introduction.md index 7aaea474ee2..3b7ee76b275 100644 --- a/exercises/concept/little-sisters-vocab/.docs/introduction.md +++ b/exercises/concept/little-sisters-vocab/.docs/introduction.md @@ -50,7 +50,7 @@ If a `list`, `tuple`, `set` or other collection of individual strings needs to b ```python # str.join() makes a new string from the iterables elements. ->>> chickens = ["hen", "egg", "rooster"] +>>> chickens = ["hen", "egg", "rooster"] # Lists are iterable. >>> ' '.join(chickens) 'hen egg rooster' @@ -60,6 +60,34 @@ If a `list`, `tuple`, `set` or other collection of individual strings needs to b >>> ' 🌿 '.join(chickens) 'hen 🌿 egg 🌿 rooster' + + +# Any iterable can be used as input. +>>> flowers = ("rose", "daisy", "carnation") # Tuples are iterable. +>>> '*-*'.join(flowers) +'rose*-*daisy*-*carnation' + +>>> flowers = {"rose", "daisy", "carnation"} # Sets are iterable, but output order is not guaranteed. +>>> '*-*'.join(flowers) +'rose*-*carnation*-*daisy' + +>>> phrase = "This is my string" # Strings are iterable, but be careful! +>>> '..'.join(phrase) +'T..h..i..s.. ..i..s.. ..m..y.. ..s..t..r..i..n..g' + + +# Separators are inserted **between** elements, but can be any string (including spaces). +# This can be exploited for interesting effects. +>>> under_words = ['under', 'current', 'sea', 'pin', 'dog', 'lay'] +>>> separator = ' ⤴️ under' +>>> separator.join(under_words) +'under ⤴️ undercurrent ⤴️ undersea ⤴️ underpin ⤴️ underdog ⤴️ underlay' + +# The separator can be composed different ways, as long as the result is a string. +>>> upper_words = ['upper', 'crust', 'case', 'classmen', 'most', 'cut'] +>>> separator = ' 🌟 ' + upper_words[0] +>>> separator.join(upper_words) + 'upper 🌟 uppercrust 🌟 uppercase 🌟 upperclassmen 🌟 uppermost 🌟 uppercut' ``` Code points within a `str` can be referenced by `0-based index` number from the left: @@ -95,7 +123,6 @@ creative = '창의적인' ``` - There is no separate “character” or "rune" type in Python, so indexing a string produces a new `str` of length 1: @@ -169,7 +196,6 @@ Strings can also be broken into smaller strings via [`.split()`] ['feline', 'four-footed', 'ferocious', 'furry'] ``` - Separators for `.split()` can be more than one character. The **whole string** is used for split matching. From 8c285ac4c2ac068d20670463eb344a5f1b6ac730 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 11:17:34 -0700 Subject: [PATCH 15/40] Bump actions/setup-python from 5.6.0 to 6.0.0 (#4000) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5.6.0 to 6.0.0. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/a26af69be951a213d495a4c3e4e4022e16d87065...e797f83bcb11b83ae66e0230d6156d7c80228e7c) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-workflow.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 1279566e79b..3a80387e3a5 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c with: python-version: 3.11.2 @@ -57,7 +57,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c with: python-version: ${{ matrix.python-version }} From 61dbb3a46b802b2263c4f6c41a28a5706b85eaaf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 11:17:56 -0700 Subject: [PATCH 16/40] Bump actions/stale from 9.1.0 to 10.0.0 (#3999) Bumps [actions/stale](https://github.com/actions/stale) from 9.1.0 to 10.0.0. - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/stale/compare/5bef64f19d7facfb25b37b414482c7164d639639...3a9db7e6a41a89f618792c92c0e97cc736e1b13f) --- updated-dependencies: - dependency-name: actions/stale dependency-version: 10.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index b10b6011d19..1c4ddca6a9a 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -8,7 +8,7 @@ jobs: stale: runs-on: ubuntu-24.04 steps: - - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 + - uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 21 From b3a9d9a35fc96802e8b5c04948a696aad59a1d6e Mon Sep 17 00:00:00 2001 From: Mark Rosemaker <48681726+MarkRosemaker@users.noreply.github.com> Date: Sun, 12 Oct 2025 18:56:04 +0200 Subject: [PATCH 17/40] [Tree Building Exercise & Class Inheritance Concept] Spelling Fixes (#4009) * Fix possessive 'its' spelling in documentation and tests * Corrected 'possibly' to 'possible * Revert "Corrected 'possibly' to 'possible" This reverts commit 9a42041c455913d8c97f98f8e45eca3cd64219e3. * revert: changes in reference/ folder [no important files changed] --- concepts/class-inheritance/about.md | 2 +- exercises/practice/tree-building/.meta/example.py | 2 +- exercises/practice/tree-building/tree_building_test.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/concepts/class-inheritance/about.md b/concepts/class-inheritance/about.md index 5db7909e2c7..9f1bdf30cd9 100644 --- a/concepts/class-inheritance/about.md +++ b/concepts/class-inheritance/about.md @@ -7,7 +7,7 @@ In situations where only a small amount of functionality needs to be customized `Inheritance` describes `is a kind of` relationship between two or more classes, abstracting common details into super (_base_ or _parent_) class and storing specific ones in the subclass (_derived class_ or _child class_). -To create a child class, specify the parent class name inside the pair of parenthesis, followed by it's name. +To create a child class, specify the parent class name inside the pair of parenthesis, followed by its name. Example ```python class Child(Parent): diff --git a/exercises/practice/tree-building/.meta/example.py b/exercises/practice/tree-building/.meta/example.py index e3929ea031c..7cf8a6ea908 100644 --- a/exercises/practice/tree-building/.meta/example.py +++ b/exercises/practice/tree-building/.meta/example.py @@ -18,7 +18,7 @@ def validate_record(record): raise ValueError('Only root should have equal record and parent id.') if not record.equal_id() and record.parent_id >= record.record_id: - raise ValueError("Node parent_id should be smaller than it's record_id.") + raise ValueError("Node parent_id should be smaller than its record_id.") def BuildTree(records): diff --git a/exercises/practice/tree-building/tree_building_test.py b/exercises/practice/tree-building/tree_building_test.py index 426ed2b95b3..a405aa1ac80 100644 --- a/exercises/practice/tree-building/tree_building_test.py +++ b/exercises/practice/tree-building/tree_building_test.py @@ -111,7 +111,7 @@ def test_root_node_has_parent(self): with self.assertRaises(ValueError) as err: BuildTree(records) self.assertEqual(type(err.exception), ValueError) - self.assertEqual(err.exception.args[0], "Node parent_id should be smaller than it's record_id.") + self.assertEqual(err.exception.args[0], "Node parent_id should be smaller than its record_id.") def test_no_root_node(self): records = [ @@ -167,7 +167,7 @@ def test_cycle_indirectly(self): with self.assertRaises(ValueError) as err: BuildTree(records) self.assertEqual(type(err.exception), ValueError) - self.assertEqual(err.exception.args[0], "Node parent_id should be smaller than it's record_id.") + self.assertEqual(err.exception.args[0], "Node parent_id should be smaller than its record_id.") def test_higher_id_parent_of_lower_id(self): records = [ @@ -179,7 +179,7 @@ def test_higher_id_parent_of_lower_id(self): with self.assertRaises(ValueError) as err: BuildTree(records) self.assertEqual(type(err.exception), ValueError) - self.assertEqual(err.exception.args[0], "Node parent_id should be smaller than it's record_id.") + self.assertEqual(err.exception.args[0], "Node parent_id should be smaller than its record_id.") def assert_node_is_branch(self, node, node_id, children_count): self.assertEqual(node.node_id, node_id) From e1566bdff11f722ddcb63e93e466aa822cef05a7 Mon Sep 17 00:00:00 2001 From: Abdelrahman Ali <110213862+abdelrahman495@users.noreply.github.com> Date: Sun, 12 Oct 2025 20:12:13 +0300 Subject: [PATCH 18/40] [Inventory Management & Dicts Concept] Correct Spelling Errors (#4010) * Update introduction.md Fixed a typo for better readability. * Updated Dicts concept about.md Made same spelling change to the concept about.md, which uses the same code examples. --------- Co-authored-by: BethanyG --- concepts/dicts/about.md | 2 +- exercises/concept/inventory-management/.docs/introduction.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/concepts/dicts/about.md b/concepts/dicts/about.md index c34160b2ef6..72ea9079c6d 100644 --- a/concepts/dicts/about.md +++ b/concepts/dicts/about.md @@ -172,7 +172,7 @@ You can change an entry `value` by assigning to its _key_: New `key`:`value` pairs can be _added_ in the same fashion: ```python -# Adding an new "color" key with a new "tawney" value. +# Adding a new "color" key with a new "tawney" value. >>> bear["color"] = 'tawney' {'name': 'Grizzly Bear', 'speed': 40, 'land_animal': True, 'color': 'tawney'} diff --git a/exercises/concept/inventory-management/.docs/introduction.md b/exercises/concept/inventory-management/.docs/introduction.md index 738f36ef754..161b1d0e7cc 100644 --- a/exercises/concept/inventory-management/.docs/introduction.md +++ b/exercises/concept/inventory-management/.docs/introduction.md @@ -84,7 +84,7 @@ You can change an entry `value` by assigning to its _key_: New `key`:`value` pairs can be _added_ in the same fashion: ```python -# Adding an new "color" key with a new "tawney" value. +# Adding a new "color" key with a new "tawney" value. >>> bear["color"] = 'tawney' {'name': 'Grizzly Bear', 'speed': 40, 'land_animal': True, 'color': 'tawney'} From 62ac923ed48dfa11e7aa0479f6a5a4e39987b9d4 Mon Sep 17 00:00:00 2001 From: Vidar <01914rifat@tuta.io> Date: Wed, 22 Oct 2025 18:47:00 +0600 Subject: [PATCH 19/40] Fix a typo (#4016) This commit will: - Fix the typo "lean" to "learn" --- docs/LEARNING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/LEARNING.md b/docs/LEARNING.md index 50a3259eed7..d71a95455cc 100644 --- a/docs/LEARNING.md +++ b/docs/LEARNING.md @@ -6,7 +6,7 @@ Python is (_as [Wikipedia says][wikipython]_), a *general-purpose and high-level It is especially good at 'gluing' different systems and programs together. -And we think the best way to lean is to _play_ and to _practice_ with coding projects big and small - or with small problems like the ones here on exercism! +And we think the best way to learn is to _play_ and to _practice_ with coding projects big and small - or with small problems like the ones here on exercism! Below you will find some additional jumping-off places to start your learning journey, recommended by our community. From 4742d3ec895689cc9a3ef29d9cdcd47b24e75220 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 21:00:38 -0800 Subject: [PATCH 20/40] Bump actions/checkout from 5.0.0 to 6.0.0 (#4034) Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.0 to 6.0.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/08c6903cd8c0fde910a37f88322edcfb5dd907a8...1af3b93b6815bc44a9784bd300feb67ff0d1eeb3) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-workflow.yml | 4 ++-- .github/workflows/issue-commenter.yml | 2 +- .github/workflows/test-runner.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 3a80387e3a5..0ad6f1f3139 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -14,7 +14,7 @@ jobs: housekeeping: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c @@ -55,7 +55,7 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9, 3.10.6, 3.11.2] steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c with: diff --git a/.github/workflows/issue-commenter.yml b/.github/workflows/issue-commenter.yml index 5472e7d95e7..4f6bff60471 100644 --- a/.github/workflows/issue-commenter.yml +++ b/.github/workflows/issue-commenter.yml @@ -9,7 +9,7 @@ jobs: name: Comments for every NEW issue. steps: - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 - name: Read issue-comment.md id: issue-comment diff --git a/.github/workflows/test-runner.yml b/.github/workflows/test-runner.yml index 428be225caa..97fcf6e5be3 100644 --- a/.github/workflows/test-runner.yml +++ b/.github/workflows/test-runner.yml @@ -10,6 +10,6 @@ jobs: test-runner: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 - name: Run test-runner run: docker compose run test-runner From f5ee289c41aadd39db56ffcbcee9fd4fba9243cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 21:01:15 -0800 Subject: [PATCH 21/40] Bump actions/setup-python from 6.0.0 to 6.1.0 (#4035) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.0.0 to 6.1.0. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/e797f83bcb11b83ae66e0230d6156d7c80228e7c...83679a892e2d95755f2dac6acb0bfd1e9ac5d548) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-workflow.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 0ad6f1f3139..e853469c6d0 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 - name: Set up Python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 with: python-version: 3.11.2 @@ -57,7 +57,7 @@ jobs: steps: - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 - - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c + - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 with: python-version: ${{ matrix.python-version }} From dcbfdef0aa7a2fbd5e514347c23636e1c08ab660 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 20 Dec 2025 08:44:26 -0800 Subject: [PATCH 22/40] Bump actions/stale from 10.0.0 to 10.1.0 (#4020) Bumps [actions/stale](https://github.com/actions/stale) from 10.0.0 to 10.1.0. - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/stale/compare/3a9db7e6a41a89f618792c92c0e97cc736e1b13f...5f858e3efba33a5ca4407a664cc011ad407f2008) --- updated-dependencies: - dependency-name: actions/stale dependency-version: 10.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 1c4ddca6a9a..4a5a9a772f1 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -8,7 +8,7 @@ jobs: stale: runs-on: ubuntu-24.04 steps: - - uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f + - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 21 From d069b5db6e9bd791264946c3683bbbfffab49e28 Mon Sep 17 00:00:00 2001 From: PetreM Date: Sun, 21 Dec 2025 23:25:30 +0200 Subject: [PATCH 23/40] robot-name approach: fix typos, minor rephrasing/improvements (#4051) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * robot-name approach: fix typos, minor rephasing/improvements - Fixed a few typos. - Rephrased here and there where I subjectively thought it useful. Please let me know if they're undesirable. - Expanded some contractions - this might help non-native speakers. - When another approach is mentioned, added link to it - hopefully this is useful rather than an distraction - I thought the explanation about the alternative to using the walrus operator was not clear, so rephrased and added a code snippet. I hope this is useful. * Update exercises/practice/robot-name/.approaches/mass-name-generation/content.md Co-authored-by: BethanyG * Update exercises/practice/robot-name/.approaches/mass-name-generation/content.md Co-authored-by: BethanyG * Update exercises/practice/robot-name/.approaches/mass-name-generation/content.md Co-authored-by: BethanyG * Update exercises/practice/robot-name/.approaches/mass-name-generation/content.md Co-authored-by: BethanyG * Update exercises/practice/robot-name/.approaches/name-on-the-fly/content.md Co-authored-by: BethanyG * Update exercises/practice/robot-name/.approaches/name-on-the-fly/content.md Co-authored-by: BethanyG * Update exercises/practice/robot-name/.approaches/mass-name-generation/content.md Co-authored-by: András B Nagy <20251272+BNAndras@users.noreply.github.com> * blank lines after headers and remove trailing whitespace * add blank lines around code snippets and expand one more contraction --------- Co-authored-by: BethanyG Co-authored-by: András B Nagy <20251272+BNAndras@users.noreply.github.com> --- .../robot-name/.approaches/introduction.md | 12 ++++-- .../mass-name-generation/content.md | 21 +++++----- .../.approaches/name-on-the-fly/content.md | 39 +++++++++++++------ 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/exercises/practice/robot-name/.approaches/introduction.md b/exercises/practice/robot-name/.approaches/introduction.md index c4b67383801..d0140e65348 100644 --- a/exercises/practice/robot-name/.approaches/introduction.md +++ b/exercises/practice/robot-name/.approaches/introduction.md @@ -1,12 +1,15 @@ # Introduction -Robot Name in Python is an interesting exercise for practising randomness. + +Robot Name in Python is an interesting exercise for practicing randomness. ## General Guidance -Two ways immedietely come to mind: generate all the possible names and then return them sequentially, or generate a random name and ensure that it's not been previously used. + +Two ways immediately come to mind: generate all the possible names and then return them sequentially, or generate a random name and ensure that it has not been previously used. Randomness can be a little, well, random, so **it's very easy to have an incorrect solution and still pass the tests**. It's strongly recommended to submit your solution for Code Review. ## Approach: mass name generation + We'd first have to generate all the possible names, shuffle them, and then use `next` (the simplest way) or maintain a `current_index` and get the name. Here's a possible way to do it: @@ -26,14 +29,17 @@ class Robot(object): def reset(self): self.name = next(NAMES) ``` + Note that selecting randomly from the list of all names would be incorrect, as there's a possibility of the name being repeated. For more detail and explanation of the code, [read here][approach-mass-name-generation]. ## Approach: name on the fly -Another approach is to generate the name on the fly and add it to a cache or a store, and checking if the generated name hasn't been used previously. + +Another approach is to generate the name on the fly and add it to a cache or a store, checking if the generated name hasn't been used previously. A possible way to implement this: + ```python from string import ascii_uppercase, digits from random import choices diff --git a/exercises/practice/robot-name/.approaches/mass-name-generation/content.md b/exercises/practice/robot-name/.approaches/mass-name-generation/content.md index 392a34ca197..a245195fa50 100644 --- a/exercises/practice/robot-name/.approaches/mass-name-generation/content.md +++ b/exercises/practice/robot-name/.approaches/mass-name-generation/content.md @@ -1,8 +1,9 @@ # Mass Name Generation -We'd first have to generate all the possible names, shuffle them, and then use `next` (the simplest way) or maintain a `current_index` and get the name. -Note that selecting randomly from the list of all names would be incorrect, as there's a possibility of the name being repeated. -Here's a possible way to do it: +We first generate all the possible names, shuffle them, and then either use `next` (the simplest way) or maintain a `current_index` to get the name. +Note that selecting randomly from the list of all names would be incorrect, as there is a possibility of the name being repeated. + +One possible way to do it: ```python from itertools import product @@ -25,25 +26,27 @@ class Robot(object): The first few lines of the mass name generation uses [`itertools.product`][itertools-product]. The resultant code is a simplification of: + ```python letter_pairs = (''.join((l1, l2)) for l1 in ascii_uppercase for l2 in ascii_uppercase) numbers = (str(i).zfill(3) for i in range(1000)) names = [l + n for l in letter_pairs for n in numbers] ``` -After the name generation, the names are shuffled - using the [default `seed`][random-seed] in the `random` module (the current timestamp). +After the name generation, the names are shuffled - using the [default `seed`][random-seed] in the `random` module (the current timestamp). When the tests reseed `random`, this has no effect as the names were shuffled before that. -We then set `NAMES` to the iterable of names, and in `reset`, set the robot's name to the `next(name)`. -If you'd like, read more on [`iter` and `next`][iter-and-next]. +We then set `NAMES` to the iterable of names, and in `reset`, set the robot's name to the `next(name)`. +If you are interested, you can read more on [`iter` and `next`][iter-and-next]. -Unlike the on the fly approach, this has a relatively short "generation" time, because we're merely giving the `next` name instead of generating it. -However, this has a huge startup memory and time cost, as 676,000 strings have to be calculated and stored. +Unlike the [on the fly approach][approach-name-on-the-fly], this has a relatively short "generation" time, because we are merely giving the `next` name instead of generating it. +However, this has a huge startup memory and time cost, as 676,000 strings have to be calculated and stored. For an approximate calculation, 676,000 strings * 5 characters / string * 1 byte / character gives 3380000 bytes or 3.38 MB of RAM - and that's just the memory aspect of it. -Sounds small, but it's relatively very expensive at the beginning. +Sounds small, but this might be a relatively significant startup cost. Thus, this approach is inefficient in cases where only a small number of names are needed _and_ the time to set/reset the robot isn't crucial. [random-seed]: https://docs.python.org/3/library/random.html#random.seed [iter-and-next]: https://www.programiz.com/python-programming/methods/built-in/iter [itertools-product]: https://www.hackerrank.com/challenges/itertools-product/problem +[approach-name-on-the-fly]: https://exercism.org/tracks/python/exercises/robot-name/approaches/name-on-the-fly diff --git a/exercises/practice/robot-name/.approaches/name-on-the-fly/content.md b/exercises/practice/robot-name/.approaches/name-on-the-fly/content.md index 0aa9f9a3fab..494b32b2d10 100644 --- a/exercises/practice/robot-name/.approaches/name-on-the-fly/content.md +++ b/exercises/practice/robot-name/.approaches/name-on-the-fly/content.md @@ -1,7 +1,9 @@ # Find name on the fly -We generate the name on the fly and add it to a cache or a store, and checking if the generated name hasn't been used previously. + +We generate the name on the fly and add it to a cache or a store, checking to make sure that the generated name has not been used previously. A possible way to implement this: + ```python from string import ascii_uppercase, digits from random import choices @@ -10,7 +12,7 @@ cache = set() class Robot: - def __get_name(self): + def __get_name(self): return ''.join(choices(ascii_uppercase, k=2) + choices(digits, k=3)) def reset(self): @@ -19,18 +21,30 @@ class Robot: cache.add(name) self.name = name - def __init__(self): + def __init__(self): self.reset() ``` -We use a `set` for the cache as it has a low access time, and we don't need the preservation of order or the ability to be indexed. -This way is merely one of the many to generate the name. +We use a `set` for the cache as it has a low access time, and because we do not need the preservation of order or the ability to access by index. + +Using `choices` is one of the many ways to generate the name. Another way might be to use `randrange` along with `zfill` for the number part, and a double `random.choice` / `random.choice` on `itertools.product` to generate the letter part. -This is the shortest way, and best utilizes the Python standard library. +The first is shorter, and best utilizes the Python standard library. + +As we are using a `while` loop to check for the name generation, it is convenient to store the local `name` using the [walrus operator][walrus-operator]. +It's also possible to find the name once before the loop, and then find it again inside the loop, but that would be an unnecessary repetition: + +```python +def reset(self): + name = self.__get_name() + while name in cache: + name = self.__get_name() + cache.add(name) + self.name = name +``` -As we're using a `while` loop to check for the name generation, it's convenient to store the local `name` using the [walrus operator][walrus-operator]. -It's also possible to find the name before the loop and find it again inside the loop, but that would unnecessary repetition. A helper method ([private][private-helper-methods] in this case) makes your code cleaner, but it's equally valid to have the code in the loop itself: + ```python def reset(self): while (name := ''.join(choices(ascii_uppercase, k=2) + choices(digits, k=3))) in cache: @@ -39,14 +53,15 @@ def reset(self): self.name = name ``` -We call `reset` from `__init__` - it's syntactically valid to do it the other way round, but it's not considered good practice to call [dunder methods][dunder-methods] directly. +We call `reset` from `__init__` - it is syntactically valid to do it the other way around, but it is not considered good practice to call [dunder methods][dunder-methods] directly. This has almost no startup time and memory, apart from declaring an empty `set`. -Note that the _generation_ time is the same as the mass generation approach, as a similar method is used. +Note that the _generation_ time is the same as the [mass generation approach][approach-mass-name-generation], as a similar method is used. However, as the name is generated at the time of setting/resetting, the method time itself is higher. -In the long run, if many names are generated, this is inefficient, since collisions will start being generated more often than unique names. +In the long run, if many names are generated, this is inefficient, since collisions will start being generated more often than unique names. [walrus-operator]: https://realpython.com/python-walrus-operator/ [private-helper-methods]: https://www.geeksforgeeks.org/private-methods-in-python/ -[dunder-methods]: https://dbader.org/blog/python-dunder-methods \ No newline at end of file +[dunder-methods]: https://dbader.org/blog/python-dunder-methods +[approach-mass-name-generation]: https://exercism.org/tracks/python/exercises/robot-name/approaches/mass-name-generation From 6865784f3f7abda58aa593668cb312ebd4921cc0 Mon Sep 17 00:00:00 2001 From: PetreM Date: Thu, 25 Dec 2025 00:37:50 +0200 Subject: [PATCH 24/40] rna-transcription approach: a few improvements (#4052) * rna-transcription approach: a few improvements - Renamed `chr` to `char` in the snippets because `chr` is a built-in function and although shadowing it in this case may not be a problem, it is still a bad practice when it can be avoided. - The dictionary-join approach mentions list comprehensions, but instead it uses a generator expression. Replaced this in the explanation and expanded to give the list comprehension based implementation along with a brief comparison. - The overview mentions one approach is four times faster. In a brief comparison, it varies from 2.5x for a very short string and up to 60x faster for a 10^6 long one. Probably not worth going into the details, but 4x is just innacurate. * convert code snippets to single quotes for consistency * several updates following discussions - Replaced `char` with `nucleotide` as this is terminology from the domain. - Rephrased a "see also" link to be more screen reader friendly. - A note about the exercise not requiring tests for invalid characters is preset in one of the approaches. Copied it over to the other approach, for uniformity. - Rephrased mention about performance and speedup. - Replaced mention of ASCII with Unicode adding a brief explanation and links. * move note regarding testing for erroneous inputs to `introduction.md` ... because it applies to the exercise in general, not a particular approach. Re-applying missed commits from prior cherry-pick. * Re-applied the commits from below via cherry-pick. convert code snippets to single quotes for consistency --- .../.approaches/dictionary-join/content.md | 32 ++++++++++++++++--- .../.approaches/dictionary-join/snippet.txt | 4 +-- .../.approaches/introduction.md | 18 +++++++---- .../translate-maketrans/content.md | 17 +++++----- .../translate-maketrans/snippet.txt | 2 +- 5 files changed, 51 insertions(+), 22 deletions(-) diff --git a/exercises/practice/rna-transcription/.approaches/dictionary-join/content.md b/exercises/practice/rna-transcription/.approaches/dictionary-join/content.md index f3ec1f755fb..fcf0c58953a 100644 --- a/exercises/practice/rna-transcription/.approaches/dictionary-join/content.md +++ b/exercises/practice/rna-transcription/.approaches/dictionary-join/content.md @@ -1,11 +1,11 @@ # dictionary look-up with `join` ```python -LOOKUP = {"G": "C", "C": "G", "T": "A", "A": "U"} +LOOKUP = {'G': 'C', 'C': 'G', 'T': 'A', 'A': 'U'} def to_rna(dna_strand): - return ''.join(LOOKUP[chr] for chr in dna_strand) + return ''.join(LOOKUP[nucleotide] for nucleotide in dna_strand) ``` @@ -16,15 +16,37 @@ but the `LOOKUP` dictionary is defined with all uppercase letters, which is the It indicates that the value is not intended to be changed. In the `to_rna()` function, the [`join()`][join] method is called on an empty string, -and is passed the list created from a [list comprehension][list-comprehension]. +and is passed the list created from a [generator expression][generator-expression]. -The list comprehension iterates each character in the input, +The generator expression iterates each character in the input, looks up the DNA character in the look-up dictionary, and outputs its matching RNA character as an element in the list. -The `join()` method collects the list of RNA characters back into a string. +The `join()` method collects the RNA characters back into a string. Since an empty string is the separator for the `join()`, there are no spaces between the RNA characters in the string. +A generator expression is similar to a [list comprehension][list-comprehension], but instead of creating a list, it returns a generator, and iterating that generator yields the elements on the fly. + +A variant that uses a list comprehension is almost identical, but note the additional square brackets inside the `join()`: + +```python +LOOKUP = {'G': 'C', 'C': 'G', 'T': 'A', 'A': 'U'} + +def to_rna(dna_strand): + return ''.join([LOOKUP[nucleotide] for nucleotide in dna_strand]) +``` + + +For a relatively small number of elements, using lists is fine and may be faster, but as the number of elements increases, the memory consumption increases and performance decreases. +You can read more about [when to choose generators over list comprehensions][list-comprehension-choose-generator-expression] to dig deeper into the topic. + + +~~~~exercism/note +As of this writing, no invalid DNA characters are in the argument to `to_rna()`, so there is no error handling required for invalid input. +~~~~ + [dictionaries]: https://docs.python.org/3/tutorial/datastructures.html?#dictionaries [const]: https://realpython.com/python-constants/ [join]: https://docs.python.org/3/library/stdtypes.html?#str.join [list-comprehension]: https://realpython.com/list-comprehension-python/#using-list-comprehensions +[list-comprehension-choose-generator-expression]: https://realpython.com/list-comprehension-python/#choose-generators-for-large-datasets +[generator-expression]: https://realpython.com/introduction-to-python-generators/#building-generators-with-generator-expressions diff --git a/exercises/practice/rna-transcription/.approaches/dictionary-join/snippet.txt b/exercises/practice/rna-transcription/.approaches/dictionary-join/snippet.txt index 558bf981408..398f2dfb07f 100644 --- a/exercises/practice/rna-transcription/.approaches/dictionary-join/snippet.txt +++ b/exercises/practice/rna-transcription/.approaches/dictionary-join/snippet.txt @@ -1,5 +1,5 @@ -LOOKUP = {"G": "C", "C": "G", "T": "A", "A": "U"} +LOOKUP = {'G': 'C', 'C': 'G', 'T': 'A', 'A': 'U'} def to_rna(dna_strand): - return ''.join(LOOKUP[chr] for chr in dna_strand) + return ''.join(LOOKUP[nucleotide] for nucleotide in dna_strand) diff --git a/exercises/practice/rna-transcription/.approaches/introduction.md b/exercises/practice/rna-transcription/.approaches/introduction.md index ca2d74a1090..54b4c1f7d30 100644 --- a/exercises/practice/rna-transcription/.approaches/introduction.md +++ b/exercises/practice/rna-transcription/.approaches/introduction.md @@ -7,13 +7,13 @@ Another approach is to do a dictionary lookup on each character and join the res ## General guidance Whichever approach is used needs to return the RNA complement for each DNA value. -The `translate()` method with `maketrans()` transcribes using the [ASCII][ASCII] values of the characters. +The `translate()` method with `maketrans()` transcribes using the [Unicode][Unicode] code points of the characters. Using a dictionary look-up with `join()` transcribes using the string values of the characters. ## Approach: `translate()` with `maketrans()` ```python -LOOKUP = str.maketrans("GCTA", "CGAU") +LOOKUP = str.maketrans('GCTA', 'CGAU') def to_rna(dna_strand): @@ -26,11 +26,11 @@ For more information, check the [`translate()` with `maketrans()` approach][appr ## Approach: dictionary look-up with `join()` ```python -LOOKUP = {"G": "C", "C": "G", "T": "A", "A": "U"} +LOOKUP = {'G': 'C', 'C': 'G', 'T': 'A', 'A': 'U'} def to_rna(dna_strand): - return ''.join(LOOKUP[chr] for chr in dna_strand) + return ''.join(LOOKUP[nucleotide] for nucleotide in dna_strand) ``` @@ -38,8 +38,14 @@ For more information, check the [dictionary look-up with `join()` approach][appr ## Which approach to use? -The `translate()` with `maketrans()` approach benchmarked over four times faster than the dictionary look-up with `join()` approach. +If performance matters, consider using the [`translate()` with `maketrans()` approach][approach-translate-maketrans]. +How an implementation behaves in terms of performance may depend on the actual data being processed, on hardware, and other factors. -[ASCII]: https://www.asciitable.com/ + +~~~~exercism/note +As of this writing, no invalid DNA characters are in the argument to `to_rna()`, so there is no error handling required for invalid input. +~~~~ + +[Unicode]: https://en.wikipedia.org/wiki/Unicode [approach-translate-maketrans]: https://exercism.org/tracks/python/exercises/rna-transcription/approaches/translate-maketrans [approach-dictionary-join]: https://exercism.org/tracks/python/exercises/rna-transcription/approaches/dictionary-join diff --git a/exercises/practice/rna-transcription/.approaches/translate-maketrans/content.md b/exercises/practice/rna-transcription/.approaches/translate-maketrans/content.md index 9b484e3cb55..9373cf12b26 100644 --- a/exercises/practice/rna-transcription/.approaches/translate-maketrans/content.md +++ b/exercises/practice/rna-transcription/.approaches/translate-maketrans/content.md @@ -1,7 +1,7 @@ # `translate()` with `maketrans()` ```python -LOOKUP = str.maketrans("GCTA", "CGAU") +LOOKUP = str.maketrans('GCTA', 'CGAU') def to_rna(dna_strand): @@ -15,20 +15,21 @@ Python doesn't _enforce_ having real constant values, but the `LOOKUP` translation table is defined with all uppercase letters, which is the naming convention for a Python [constant][const]. It indicates that the value is not intended to be changed. -The translation table that is created uses the [ASCII][ASCII] values (also called the ordinal values) for each letter in the two strings. -The ASCII value for "G" in the first string is the key for the ASCII value of "C" in the second string, and so on. +The translation table that is created uses the [Unicode][Unicode] _code points_ (sometimes called the ordinal values) for each letter in the two strings. +As Unicode was designed to be backwards compatible with [ASCII][ASCII] and because the exercise uses Latin letters, the code points in the translation table can be interpreted as ASCII. +However, the functions can deal with any Unicode character. +You can learn more by reading about [strings and their representation in the Exercism Python syllabus][concept-string]. + +The Unicode value for "G" in the first string is the key for the Unicode value of "C" in the second string, and so on. In the `to_rna()` function, the [`translate()`][translate] method is called on the input, and is passed the translation table. The output of `translate()` is a string where all of the input DNA characters have been replaced by their RNA complement in the translation table. - -~~~~exercism/note -As of this writing, no invalid DNA characters are in the argument to `to_rna()`, so there is no error handling required for invalid input. -~~~~ - [dictionaries]: https://docs.python.org/3/tutorial/datastructures.html?#dictionaries [maketrans]: https://docs.python.org/3/library/stdtypes.html?#str.maketrans [const]: https://realpython.com/python-constants/ [translate]: https://docs.python.org/3/library/stdtypes.html?#str.translate [ASCII]: https://www.asciitable.com/ +[Unicode]: https://en.wikipedia.org/wiki/Unicode +[concept-strings]: https://exercism.org/tracks/python/concepts/strings diff --git a/exercises/practice/rna-transcription/.approaches/translate-maketrans/snippet.txt b/exercises/practice/rna-transcription/.approaches/translate-maketrans/snippet.txt index 2d00b83be6b..db15d868f19 100644 --- a/exercises/practice/rna-transcription/.approaches/translate-maketrans/snippet.txt +++ b/exercises/practice/rna-transcription/.approaches/translate-maketrans/snippet.txt @@ -1,4 +1,4 @@ -LOOKUP = str.maketrans("GCTA", "CGAU") +LOOKUP = str.maketrans('GCTA', 'CGAU') def to_rna(dna_strand): From 0410dcc8527539be0f5707f2079bd888f2e0888c Mon Sep 17 00:00:00 2001 From: BethanyG Date: Tue, 30 Dec 2025 13:55:45 -0800 Subject: [PATCH 25/40] December 2025 practice exercise docs sync. (#4058) Pulled in latest exercise introductions and instructions from problem-specifications. --- .../.docs/instructions.md | 2 +- .../ocr-numbers/.docs/instructions.md | 80 ++++++------------- .../ocr-numbers/.docs/introduction.md | 6 ++ .../practice/triangle/.docs/instructions.md | 3 +- 4 files changed, 33 insertions(+), 58 deletions(-) create mode 100644 exercises/practice/ocr-numbers/.docs/introduction.md diff --git a/exercises/practice/killer-sudoku-helper/.docs/instructions.md b/exercises/practice/killer-sudoku-helper/.docs/instructions.md index fdafdca8fbe..9f5cb1368ff 100644 --- a/exercises/practice/killer-sudoku-helper/.docs/instructions.md +++ b/exercises/practice/killer-sudoku-helper/.docs/instructions.md @@ -74,7 +74,7 @@ You can also find Killer Sudokus in varying difficulty in numerous newspapers, a ## Credit -The screenshots above have been generated using [F-Puzzles.com](https://www.f-puzzles.com/), a Puzzle Setting Tool by Eric Fox. +The screenshots above have been generated using F-Puzzles.com, a Puzzle Setting Tool by Eric Fox. [sudoku-rules]: https://masteringsudoku.com/sudoku-rules-beginners/ [killer-guide]: https://masteringsudoku.com/killer-sudoku/ diff --git a/exercises/practice/ocr-numbers/.docs/instructions.md b/exercises/practice/ocr-numbers/.docs/instructions.md index 7beb2577957..8a391ce4f6e 100644 --- a/exercises/practice/ocr-numbers/.docs/instructions.md +++ b/exercises/practice/ocr-numbers/.docs/instructions.md @@ -1,79 +1,47 @@ # Instructions -Given a 3 x 4 grid of pipes, underscores, and spaces, determine which number is represented, or whether it is garbled. +Optical Character Recognition or OCR is software that converts images of text into machine-readable text. +Given a grid of characters representing some digits, convert the grid to a string of digits. +If the grid has multiple rows of cells, the rows should be separated in the output with a `","`. -## Step One +- The grid is made of one of more lines of cells. +- Each line of the grid is made of one or more cells. +- Each cell is three columns wide and four rows high (3x4) and represents one digit. +- Digits are drawn using pipes (`"|"`), underscores (`"_"`), and spaces (`" "`). -To begin with, convert a simple binary font to a string containing 0 or 1. +## Edge cases -The binary font uses pipes and underscores, four rows high and three columns wide. +- If the input is not a valid size, your program should indicate there is an error. +- If the input is the correct size, but a cell is not recognizable, your program should output a `"?"` for that character. -```text - _ # - | | # zero. - |_| # - # the fourth row is always blank -``` +## Examples -Is converted to "0" - -```text - # - | # one. - | # - # (blank fourth row) -``` - -Is converted to "1" - -If the input is the correct size, but not recognizable, your program should return '?' - -If the input is the incorrect size, your program should return an error. - -## Step Two - -Update your program to recognize multi-character binary strings, replacing garbled numbers with ? - -## Step Three - -Update your program to recognize all numbers 0 through 9, both individually and as part of a larger string. - -```text - _ - _| -|_ - -``` - -Is converted to "2" +The following input (without the comments) is converted to `"1234567890"`. ```text _ _ _ _ _ _ _ _ # - | _| _||_||_ |_ ||_||_|| | # decimal numbers. + | _| _||_||_ |_ ||_||_|| | # Decimal numbers. ||_ _| | _||_| ||_| _||_| # - # fourth line is always blank + # The fourth line is always blank, ``` -Is converted to "1234567890" - -## Step Four +The following input is converted to `"123,456,789"`. -Update your program to handle multiple numbers, one per line. -When converting several lines, join the lines with commas. + ```text - _ _ + _ _ | _| _| ||_ _| - - _ _ -|_||_ |_ + + _ _ +|_||_ |_ | _||_| - - _ _ _ + + _ _ _ ||_||_| ||_| _| - + ``` -Is converted to "123,456,789". + diff --git a/exercises/practice/ocr-numbers/.docs/introduction.md b/exercises/practice/ocr-numbers/.docs/introduction.md new file mode 100644 index 00000000000..366d76062c7 --- /dev/null +++ b/exercises/practice/ocr-numbers/.docs/introduction.md @@ -0,0 +1,6 @@ +# Introduction + +Your best friend Marta recently landed their dream job working with a local history museum's collections. +Knowing of your interests in programming, they confide in you about an issue at work for an upcoming exhibit on computing history. +A local university's math department had donated several boxes of historical printouts, but given the poor condition of the documents, the decision has been made to digitize the text. +However, the university's old printer had some quirks in how text was represented, and your friend could use your help to extract the data successfully. diff --git a/exercises/practice/triangle/.docs/instructions.md b/exercises/practice/triangle/.docs/instructions.md index 755cb8d19d3..e9b053dcd34 100644 --- a/exercises/practice/triangle/.docs/instructions.md +++ b/exercises/practice/triangle/.docs/instructions.md @@ -14,7 +14,8 @@ A _scalene_ triangle has all sides of different lengths. For a shape to be a triangle at all, all sides have to be of length > 0, and the sum of the lengths of any two sides must be greater than or equal to the length of the third side. ~~~~exercism/note -We opted to not include tests for degenerate triangles (triangles that violate these rules) to keep things simpler. +_Degenerate triangles_ are triangles where the sum of the length of two sides is **equal** to the length of the third side, e.g. `1, 1, 2`. +We opted to not include tests for degenerate triangles in this exercise. You may handle those situations if you wish to do so, or safely ignore them. ~~~~ From a0c1db908af44cadf8771973124bd0063a1eca00 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Tue, 30 Dec 2025 13:56:58 -0800 Subject: [PATCH 26/40] Synced tests to problem-specifications and regenerated test cases. (#4059) Added new test cases from problem-specifications and regenerated test file. --- exercises/practice/flower-field/.meta/tests.toml | 3 +++ exercises/practice/flower-field/flower_field_test.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/exercises/practice/flower-field/.meta/tests.toml b/exercises/practice/flower-field/.meta/tests.toml index c2b24fdaf5c..965ba8fd4d7 100644 --- a/exercises/practice/flower-field/.meta/tests.toml +++ b/exercises/practice/flower-field/.meta/tests.toml @@ -44,3 +44,6 @@ description = "cross" [dd9d4ca8-9e68-4f78-a677-a2a70fd7a7b8] description = "large garden" + +[6e4ac13a-3e43-4728-a2e3-3551d4b1a996] +description = "multiple adjacent flowers" diff --git a/exercises/practice/flower-field/flower_field_test.py b/exercises/practice/flower-field/flower_field_test.py index 019f7357fdb..d0f1334cbfc 100644 --- a/exercises/practice/flower-field/flower_field_test.py +++ b/exercises/practice/flower-field/flower_field_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/flower-field/canonical-data.json -# File last updated on 2025-06-25 +# File last updated on 2025-12-30 import unittest @@ -52,6 +52,9 @@ def test_large_garden(self): ["1*22*1", "12*322", " 123*2", "112*4*", "1*22*2", "111111"], ) + def test_multiple_adjacent_flowers(self): + self.assertEqual(annotate([" ** "]), ["1**1"]) + # Additional tests for this track def test_annotate_9(self): self.assertEqual( From e14ea7fb90db29b14eb321f067fe012a0c55c84b Mon Sep 17 00:00:00 2001 From: BethanyG Date: Tue, 30 Dec 2025 13:57:59 -0800 Subject: [PATCH 27/40] Updated tests.toml and regenerated isbn-verifier test cases. (#4060) Added two new test cases from problem-specifications. --- exercises/practice/isbn-verifier/.meta/tests.toml | 6 ++++++ exercises/practice/isbn-verifier/isbn_verifier_test.py | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/exercises/practice/isbn-verifier/.meta/tests.toml b/exercises/practice/isbn-verifier/.meta/tests.toml index 6d5a8459907..17e18d47ac5 100644 --- a/exercises/practice/isbn-verifier/.meta/tests.toml +++ b/exercises/practice/isbn-verifier/.meta/tests.toml @@ -30,6 +30,12 @@ description = "invalid character in isbn is not treated as zero" [28025280-2c39-4092-9719-f3234b89c627] description = "X is only valid as a check digit" +[8005b57f-f194-44ee-88d2-a77ac4142591] +description = "only one check digit is allowed" + +[fdb14c99-4cf8-43c5-b06d-eb1638eff343] +description = "X is not substituted by the value 10" + [f6294e61-7e79-46b3-977b-f48789a4945b] description = "valid isbn without separating dashes" diff --git a/exercises/practice/isbn-verifier/isbn_verifier_test.py b/exercises/practice/isbn-verifier/isbn_verifier_test.py index dbcddf19d48..5c9bf6f755a 100644 --- a/exercises/practice/isbn-verifier/isbn_verifier_test.py +++ b/exercises/practice/isbn-verifier/isbn_verifier_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/isbn-verifier/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2025-12-30 import unittest @@ -31,6 +31,12 @@ def test_invalid_character_in_isbn_is_not_treated_as_zero(self): def test_x_is_only_valid_as_a_check_digit(self): self.assertIs(is_valid("3-598-2X507-9"), False) + def test_only_one_check_digit_is_allowed(self): + self.assertIs(is_valid("3-598-21508-96"), False) + + def test_x_is_not_substituted_by_the_value_10(self): + self.assertIs(is_valid("3-598-2X507-5"), False) + def test_valid_isbn_without_separating_dashes(self): self.assertIs(is_valid("3598215088"), True) From 0792f3e139854149767b6b54894a403e9b167063 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Tue, 30 Dec 2025 13:59:14 -0800 Subject: [PATCH 28/40] Synced tests.toml and regenerated test cases. (#4061) Added three new test cases from problem-specifications. --- exercises/practice/satellite/.meta/tests.toml | 22 +++++++- .../practice/satellite/satellite_test.py | 55 ++++++++++++++++++- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/exercises/practice/satellite/.meta/tests.toml b/exercises/practice/satellite/.meta/tests.toml index 8314daa436f..d0ed5b6ac5a 100644 --- a/exercises/practice/satellite/.meta/tests.toml +++ b/exercises/practice/satellite/.meta/tests.toml @@ -1,6 +1,13 @@ -# This is an auto-generated file. Regular comments will be removed when this -# file is regenerated. Regenerating will not touch any manually added keys, -# so comments can be added in a "comment" key. +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. [8df3fa26-811a-4165-9286-ff9ac0850d19] description = "Empty tree" @@ -19,3 +26,12 @@ description = "Reject inconsistent traversals of same length" [d86a3d72-76a9-43b5-9d3a-e64cb1216035] description = "Reject traversals with repeated items" + +[af31ae02-7e5b-4452-a990-bccb3fca9148] +description = "A degenerate binary tree" + +[ee54463d-a719-4aae-ade4-190d30ce7320] +description = "Another degenerate binary tree" + +[87123c08-c155-4486-90a4-e2f75b0f3e8f] +description = "Tree with many more items" diff --git a/exercises/practice/satellite/satellite_test.py b/exercises/practice/satellite/satellite_test.py index f44a5384798..6b960de73e3 100644 --- a/exercises/practice/satellite/satellite_test.py +++ b/exercises/practice/satellite/satellite_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/satellite/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2025-12-30 import unittest @@ -67,3 +67,56 @@ def test_reject_traversals_with_repeated_items(self): tree_from_traversals(preorder, inorder) self.assertEqual(type(err.exception), ValueError) self.assertEqual(err.exception.args[0], "traversals must contain unique items") + + def test_a_degenerate_binary_tree(self): + preorder = ["a", "b", "c", "d"] + inorder = ["d", "c", "b", "a"] + + expected = { + "v": "a", + "l": { + "v": "b", + "l": {"v": "c", "l": {"v": "d", "l": {}, "r": {}}, "r": {}}, + "r": {}, + }, + "r": {}, + } + self.assertEqual(tree_from_traversals(preorder, inorder), expected) + + def test_another_degenerate_binary_tree(self): + preorder = ["a", "b", "c", "d"] + inorder = ["a", "b", "c", "d"] + + expected = { + "v": "a", + "l": {}, + "r": { + "v": "b", + "l": {}, + "r": {"v": "c", "l": {}, "r": {"v": "d", "l": {}, "r": {}}}, + }, + } + self.assertEqual(tree_from_traversals(preorder, inorder), expected) + + def test_tree_with_many_more_items(self): + preorder = ["a", "b", "d", "g", "h", "c", "e", "f", "i"] + inorder = ["g", "d", "h", "b", "a", "e", "c", "i", "f"] + + expected = { + "v": "a", + "l": { + "v": "b", + "l": { + "v": "d", + "l": {"v": "g", "l": {}, "r": {}}, + "r": {"v": "h", "l": {}, "r": {}}, + }, + "r": {}, + }, + "r": { + "v": "c", + "l": {"v": "e", "l": {}, "r": {}}, + "r": {"v": "f", "l": {"v": "i", "l": {}, "r": {}}, "r": {}}, + }, + } + self.assertEqual(tree_from_traversals(preorder, inorder), expected) From 44a9bb75db4f419f1dde7163f3d9788f8009a165 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Jan 2026 15:31:41 -0800 Subject: [PATCH 29/40] Bump actions/checkout from 6.0.0 to 6.0.1 (#4063) Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.0 to 6.0.1. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/1af3b93b6815bc44a9784bd300feb67ff0d1eeb3...8e8c483db84b4bee98b60c0593521ed34d9990e8) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-workflow.yml | 4 ++-- .github/workflows/issue-commenter.yml | 2 +- .github/workflows/test-runner.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index e853469c6d0..33f47f541e4 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -14,7 +14,7 @@ jobs: housekeeping: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - name: Set up Python uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 @@ -55,7 +55,7 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9, 3.10.6, 3.11.2] steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 with: diff --git a/.github/workflows/issue-commenter.yml b/.github/workflows/issue-commenter.yml index 4f6bff60471..9e3b678f66e 100644 --- a/.github/workflows/issue-commenter.yml +++ b/.github/workflows/issue-commenter.yml @@ -9,7 +9,7 @@ jobs: name: Comments for every NEW issue. steps: - name: Checkout - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - name: Read issue-comment.md id: issue-comment diff --git a/.github/workflows/test-runner.yml b/.github/workflows/test-runner.yml index 97fcf6e5be3..f32c41b958a 100644 --- a/.github/workflows/test-runner.yml +++ b/.github/workflows/test-runner.yml @@ -10,6 +10,6 @@ jobs: test-runner: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - name: Run test-runner run: docker compose run test-runner From b5d1682ad2cfd6e4f6b1d89a8bb40e6db2d083a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Jan 2026 15:32:06 -0800 Subject: [PATCH 30/40] Bump actions/stale from 10.1.0 to 10.1.1 (#4062) Bumps [actions/stale](https://github.com/actions/stale) from 10.1.0 to 10.1.1. - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/stale/compare/5f858e3efba33a5ca4407a664cc011ad407f2008...997185467fa4f803885201cee163a9f38240193d) --- updated-dependencies: - dependency-name: actions/stale dependency-version: 10.1.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 4a5a9a772f1..f40e8010052 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -8,7 +8,7 @@ jobs: stale: runs-on: ubuntu-24.04 steps: - - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 + - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 21 From 608b96371f7c3744002268ef38d68cdf3911cf65 Mon Sep 17 00:00:00 2001 From: Colin Leach Date: Fri, 2 Jan 2026 13:52:27 -0700 Subject: [PATCH 31/40] Flower field approaches (#3935) * [Flower Field] draft approaches doc * minor edits * Suggestions and edits for flower-field approaches intro doc. * Add guidance on approach selection for 2D processing Added a section discussing the choice of approach for processing a 2-dimensional board, emphasizing the trade-offs between readability and performance. Honestly, I'm not sure what's best here, so please don't feel inhibited in hacking it about! --------- Co-authored-by: Colin Leach Co-authored-by: BethanyG --- .../flower-field/.approaches/config.json | 8 + .../flower-field/.approaches/introduction.md | 272 ++++++++++++++++++ 2 files changed, 280 insertions(+) create mode 100644 exercises/practice/flower-field/.approaches/config.json create mode 100644 exercises/practice/flower-field/.approaches/introduction.md diff --git a/exercises/practice/flower-field/.approaches/config.json b/exercises/practice/flower-field/.approaches/config.json new file mode 100644 index 00000000000..cf5b9a7b872 --- /dev/null +++ b/exercises/practice/flower-field/.approaches/config.json @@ -0,0 +1,8 @@ +{ + "introduction": { + "authors": [ + "colinleach", + "BethanyG" + ] + } +} diff --git a/exercises/practice/flower-field/.approaches/introduction.md b/exercises/practice/flower-field/.approaches/introduction.md new file mode 100644 index 00000000000..ab5e24840d2 --- /dev/null +++ b/exercises/practice/flower-field/.approaches/introduction.md @@ -0,0 +1,272 @@ + +# Introduction + +The Flower Field exercise is designed to practice iteration, boolean logic and raising errors with error messages. +It also provides ample opportunities for working with `lists`, `list-indexing`, `comprehensions`, `tuples`, and `generator-expressions`. + + +## General considerations and guidance for the exercise + +It is possible (_and potentially easier_) to break the problem down into a series of sub-tasks, with plenty of scope to mix and match strategies within these sections: + +- Is the board valid? +- Is the current square a flower? +- What are the valid neighboring squares, and how many of them contain flowers? + +Core Python does not support matrices nor N-dimensional arrays, though these are at the heart of many third-party packages such as NumPy. +Due to this limitation, the input board and final result for this exercise are implemented in the tests as a `list` of strings; one string per "row" of the board. + + +Intermediate processing for the problem is likely to use lists of lists with a final `''.join()` for each "row" in the returned single `list`, although other strategies could be employed. +Helpfully, Python considers both [lists][ordered-sequences] and [strings][text-sequences] as [sequence types][common-sequence-operations], and can iterate over/index into both in the same fashion. + + +## Validating boards + +The "board" or "field" must be rectangular: essentially, all rows must be the same length as the first row. +This means that any board can be invalidated using the built-ins `all()` or `any()` to check for equal lengths of the strings in the `list` (_see an example below_). + +Perhaps surprisingly, both row and column lengths **can be zero/empty**, so an apparently "non-existent board or field" is considered valid and needs special handling: + + +```python + rows = len(garden) + if rows > 0: + cols = len(garden[0]) + else: + return [] + + if any([len(row) != cols for row in garden]): + raise ValueError('The board is invalid with current input.') +``` + +Additionally, the only valid entries for the board/field are a space `' '` (_position empty_) or an asterisk `'*'` (_flower in position_). + All other characters are _invalid_ and should `raise` an error with an appropriate error message. + The exercise [tests][flower-field-tests] check for specific error messages including punctuation, so should be read or copied carefully. + +Some solutions use regular expressions for these checks, but there are simpler (_and more performant_) options: + + +```python + if garden[row][col] not in (' ', '*'): + # raise error +``` + +Depending on how the code is structured, it may be possible to combine the checks for row length with the checks for valid characters. +More commonly, board/field dimensions are checked at the beginning. +Invalid characters are then detected while iterating through the rows of the board/field. + + +## Processing squares and finding occupied neighbors + +Squares containing a flower are straightforward: you can copy `'*'` to the corresponding square in the results `list`. + +Empty squares present a challenge: count how many flowers are in all the squares _adjacent_ to it. +But *How many squares are adjacent to the current position?* +In the middle of a reasonably large board there will be 8 adjacent squares, but this is reduced for squares at edges or corners. + + +### Some square processing methods + +Note that we only want a _count_ of nearby flowers. +Their precise _location_ is irrelevant. + + +1. Nested `if..elif` statements + + This can be made to work, but can quickly become very verbose or confusing if not thought out carefully: + + ```python + for index_i, _ in enumerate(flowerfield): + temp_row = "" + for index_j in range(column_count): + if flowerfield[index_i][index_j].isspace(): + temp_row += count_flowers(flowerfield, index_i, index_j) + elif flowerfield[index_i][index_j] == "*": + temp_row += "*" + else: + raise ValueError("The board is invalid with current input.") + flowerfield[index_i] = temp_row + ``` + +2. Explicit coordinates + + List all the possibilities then filter out any squares that fall outside the board: + + ```python + def count_adjacent(row, col): + adj_squares = ( + (row-1, col-1), (row-1, col), (row-1, col+1), + (row, col-1), (row, col+1), + (row+1, col-1), (row+1, col), (row+1, col+1), + ) + + # which are on the board? + neighbors = [garden[row][col] for row, col in adj_squares + if 0 <= row < rows and 0 <= col < cols] + # how many contain flowers? + return len([adj for adj in neighbors if adj == '*']) + ``` + +3. Using a comprehension or generator expression + + ```python + # Using a list comprehension + squares = [(row + row_diff, col + col_diff) + for row_diff in (-1, 0, 1) + for col_diff in (-1, 0, 1)] + + # Using a generator expression + squares = ((row + row_diff, col + col_diff) + for row_diff in (-1, 0, 1) + for col_diff in (-1, 0, 1)) + ``` + + A key insight here is that we can work on a 3x3 block of cells: we already ensured that the central cell does *not* contain a flower that would affect our count. + We can then filter and count as in the `count_adjacent` function in the previous code. + +4. Using complex numbers + + ```python + def neighbors(cell): + """Yield all eight neighboring cells.""" + for x in (-1, 0, 1): + for y in (-1, 0, 1): + if offset := x + y * 1j: + yield cell + offset + ``` + + A particularly elegant solution is to treat the board/field as a portion of the complex plane. + In Python, [complex numbers][complex-numbers] are a standard numeric type, alongside integers and floats. + *This is less widely known than it deserves to be.* + + The constructor for a complex number is `complex(x, y)` or (as here) `x + y * 1j`, where `x` and `y` are the real and imaginary parts, respectively. + + There are two properties of complex numbers that help us in this case: + - The real and imaginary parts act independently under addition. + - The value `complex(0, 0)` is the complex zero, which like integer zero is treated as False in Python conditionals. + + A tuple of integers would not work as a substitute, because `+` behaves as the concatenation operator for tuples: + + ```python + >>> complex(1, 2) + complex(3, 4) + (4+6j) + >>> (1, 2) + (3, 4) + (1, 2, 3, 4) + ``` + + Note also the use of the ["walrus" operator][walrus-operator] `:=` in the definition of `offset` above. + This relatively recent addition to Python simplifies variable assignment within the limited scope of an if statement or a comprehension. + + +## Ways of putting it all together + +The example below takes an object-oriented approach using complex numbers, included because it is a particularly clear illustration of the various topics discussed above. + +All validation checks are done in the object constructor. + +```python +"""Flower Field.""" + +def neighbors(cell): + """Yield all eight neighboring cells.""" + for x in (-1, 0, 1): + for y in (-1, 0, 1): + if offset := x + y * 1j: + yield cell + offset + + +class Garden: + """garden helper.""" + + def __init__(self, data): + """Initialize.""" + self.height = len(data) + self.width = len(data[0]) if data else 0 + + if not all(len(row) == self.width for row in data): + raise ValueError("The board is invalid with current input.") + + self.data = {} + for y, line in enumerate(data): + for x, val in enumerate(line): + self.data[x + y * 1j] = val + if not all(v in (" ", "*") for v in self.data.values()): + raise ValueError("The board is invalid with current input.") + + def val(self, x, y): + """Return the value for one square.""" + cur = x + y * 1j + if self.data[cur] == "*": + return "*" + count = sum(self.data.get(neighbor, "") == "*" for neighbor in neighbors(cur)) + return str(count) if count else " " + + def convert(self): + """Convert the garden.""" + return ["".join(self.val(x, y) + for x in range(self.width)) + for y in range(self.height)] + + +def annotate(garden): + """Annotate a garden.""" + return Garden(garden).convert() +``` + +The example below takes an opposite strategy, using a single function, `list comprehensions`, and nested `if-elif` statements": + +```python +def annotate(garden): + grid = [[0 for _ in row] for row in garden] + positions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)] + + for col, row in enumerate(garden): + # Checking that the board/field is rectangular up front. + if len(row) != len(grid[0]): + raise ValueError("The board is invalid with current input.") + + # Validating square content. + for index, square in enumerate(row): + if square == " ": + continue + elif square != "*": + raise ValueError("The board is invalid with current input.") + grid[col][index] = "*" + + for dr, dc in positions: + dr += col + if dr < 0 or dr >= len(grid): + continue + + dc += index + if dc < 0 or dc >= len(grid[dr]): + continue + + if grid[dr][dc] != "*": + grid[dr][dc] += 1 + + return ["".join(" " if square == 0 else str(square) for square in row) for row in grid] +``` + +## Which approach to use? + +Processing a 2-dimensional board inevitably means using some form of nested loops, which is likely to dominate performance. + +Using comprehensions and/or generators instead of explicit loops may offer a slight speed-up, as well as more concise code. +However, performance differences are probably small, and the concise syntax _may_ be less easy to read. + +In this case, readability is probably more important than aggressive optimization. +So, we need to understand the target audience, and how they perceive "readability". + +Python experts find comprehensions very idiomatic (and generators, which have similar syntax), but programmers with a different language background can get confused. + +Complex numbers are a more extreme case: wonderfully clear and elegant for people with a suitable mathematical background, potentially mystifying for the wider population. +Tastes differ! + +[common-sequence-operations]: https://docs.python.org/3.13/library/stdtypes.html#common-sequence-operations +[complex-numbers]: https://exercism.org/tracks/python/concepts/complex-numbers +[flower-field-tests]: https://github.com/exercism/python/blob/main/exercises/practice/flower-field/flower_field_test.py +[ordered-sequences]: https://docs.python.org/3.13/library/stdtypes.html#sequence-types-list-tuple-range +[text-sequences]: https://docs.python.org/3.13/library/stdtypes.html#text-sequence-type-str +[walrus-operator]: https://peps.python.org/pep-0572/ From 1416f46046d24d17203fe1cb94c8e0609559bfd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20B=20Nagy?= <20251272+BNAndras@users.noreply.github.com> Date: Tue, 20 Jan 2026 03:08:13 -0800 Subject: [PATCH 32/40] Clarify guidance in Ghost Gobble Arcade Game (#4070) --- .../concept/ghost-gobble-arcade-game/.docs/instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/concept/ghost-gobble-arcade-game/.docs/instructions.md b/exercises/concept/ghost-gobble-arcade-game/.docs/instructions.md index 04b0b51a423..d79e6ed1bbf 100644 --- a/exercises/concept/ghost-gobble-arcade-game/.docs/instructions.md +++ b/exercises/concept/ghost-gobble-arcade-game/.docs/instructions.md @@ -42,7 +42,7 @@ True ## 4. Define if Pac-Man wins Define the `win()` function that takes three parameters (_if Pac-Man has eaten all of the dots_, _if Pac-Man has a power pellet active_, and _if Pac-Man is touching a ghost_) and returns a Boolean value if Pac-Man wins. - The function should return `True` if Pac-Man has eaten all of the dots and has not lost based on the parameters defined in part 3. + The function should return `True` if Pac-Man has eaten all of the dots and has not lost based on the rules defined in part 3. ```python >>> win(False, True, False) From 39e917c3702ab0ce1592d9927e78f22dfffc81ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20B=20Nagy?= <20251272+BNAndras@users.noreply.github.com> Date: Thu, 22 Jan 2026 22:52:51 -0800 Subject: [PATCH 33/40] Add `line-up` exercise (#4074) --- config.json | 8 ++ .../practice/line-up/.docs/instructions.md | 19 +++ .../practice/line-up/.docs/introduction.md | 7 + exercises/practice/line-up/.meta/config.json | 19 +++ exercises/practice/line-up/.meta/example.py | 19 +++ exercises/practice/line-up/.meta/template.j2 | 10 ++ exercises/practice/line-up/.meta/tests.toml | 67 ++++++++++ exercises/practice/line-up/line_up.py | 2 + exercises/practice/line-up/line_up_test.py | 125 ++++++++++++++++++ 9 files changed, 276 insertions(+) create mode 100644 exercises/practice/line-up/.docs/instructions.md create mode 100644 exercises/practice/line-up/.docs/introduction.md create mode 100644 exercises/practice/line-up/.meta/config.json create mode 100644 exercises/practice/line-up/.meta/example.py create mode 100644 exercises/practice/line-up/.meta/template.j2 create mode 100644 exercises/practice/line-up/.meta/tests.toml create mode 100644 exercises/practice/line-up/line_up.py create mode 100644 exercises/practice/line-up/line_up_test.py diff --git a/config.json b/config.json index ff5fad2c81e..2bbf7486ad5 100644 --- a/config.json +++ b/config.json @@ -511,6 +511,14 @@ ], "difficulty": 1 }, + { + "slug": "line-up", + "name": "Line Up", + "uuid": "e09d877e-489b-4dbd-89c5-f6b15e867b67", + "practices": ["string-formatting"], + "prerequisites": ["basics", "strings"], + "difficulty": 1 + }, { "slug": "difference-of-squares", "name": "Difference of Squares", diff --git a/exercises/practice/line-up/.docs/instructions.md b/exercises/practice/line-up/.docs/instructions.md new file mode 100644 index 00000000000..9e686ecbffb --- /dev/null +++ b/exercises/practice/line-up/.docs/instructions.md @@ -0,0 +1,19 @@ +# Instructions + +Given a name and a number, your task is to produce a sentence using that name and that number as an [ordinal numeral][ordinal-numeral]. +Yaʻqūb expects to use numbers from 1 up to 999. + +Rules: + +- Numbers ending in 1 (unless ending in 11) → `"st"` +- Numbers ending in 2 (unless ending in 12) → `"nd"` +- Numbers ending in 3 (unless ending in 13) → `"rd"` +- All other numbers → `"th"` + +Examples: + +- `"Mary", 1` → `"Mary, you are the 1st customer we serve today. Thank you!"` +- `"John", 12` → `"John, you are the 12th customer we serve today. Thank you!"` +- `"Dahir", 162` → `"Dahir, you are the 162nd customer we serve today. Thank you!"` + +[ordinal-numeral]: https://en.wikipedia.org/wiki/Ordinal_numeral diff --git a/exercises/practice/line-up/.docs/introduction.md b/exercises/practice/line-up/.docs/introduction.md new file mode 100644 index 00000000000..ea07268ae3b --- /dev/null +++ b/exercises/practice/line-up/.docs/introduction.md @@ -0,0 +1,7 @@ +# Introduction + +Your friend Yaʻqūb works the counter at a deli in town, slicing, weighing, and wrapping orders for a line of hungry customers that gets longer every day. +Waiting customers are starting to lose track of who is next, so he wants numbered tickets they can use to track the order in which they arrive. + +To make the customers feel special, he does not want the ticket to have only a number on it. +They shall get a proper English sentence with their name and number on it. diff --git a/exercises/practice/line-up/.meta/config.json b/exercises/practice/line-up/.meta/config.json new file mode 100644 index 00000000000..c7417123792 --- /dev/null +++ b/exercises/practice/line-up/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "BNAndras" + ], + "files": { + "solution": [ + "line_up.py" + ], + "test": [ + "line_up_test.py" + ], + "example": [ + ".meta/example.py" + ] + }, + "blurb": "Help lining up customers at Yaʻqūb's Deli.", + "source": "mk-mxp, based on previous work from Exercism contributors codedge and neenjaw", + "source_url": "https://forum.exercism.org/t/new-exercise-ordinal-numbers/19147" +} diff --git a/exercises/practice/line-up/.meta/example.py b/exercises/practice/line-up/.meta/example.py new file mode 100644 index 00000000000..a847e80c99c --- /dev/null +++ b/exercises/practice/line-up/.meta/example.py @@ -0,0 +1,19 @@ +def line_up(name, number): + suffix = get_suffix(number) + return f"{name}, you are the {number}{suffix} customer we serve today. Thank you!" + + +def get_suffix(number): + if 11 <= number % 100 <= 13: + return "th" + + mod_10 = number % 10 + + if mod_10 == 1: + return "st" + if mod_10 == 2: + return "nd" + if mod_10 == 3: + return "rd" + + return "th" diff --git a/exercises/practice/line-up/.meta/template.j2 b/exercises/practice/line-up/.meta/template.j2 new file mode 100644 index 00000000000..df7283398b5 --- /dev/null +++ b/exercises/practice/line-up/.meta/template.j2 @@ -0,0 +1,10 @@ +{%- import "generator_macros.j2" as macros with context -%} +{{ macros.canonical_ref() }} + +{{ macros.header(imports=['line_up']) }} + +class {{ exercise | camel_case }}Test(unittest.TestCase): + {% for case in cases -%} + def test_{{ case["description"] | to_snake }}(self): + self.assertEqual(line_up("{{ case["input"]["name"] }}", {{ case["input"]["number"] }}), "{{ case["expected"] }}") + {% endfor -%} diff --git a/exercises/practice/line-up/.meta/tests.toml b/exercises/practice/line-up/.meta/tests.toml new file mode 100644 index 00000000000..36fdf1d0cd3 --- /dev/null +++ b/exercises/practice/line-up/.meta/tests.toml @@ -0,0 +1,67 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[7760d1b8-4864-4db4-953b-0fa7c047dbc0] +description = "format smallest non-exceptional ordinal numeral 4" + +[e8b7c715-6baa-4f7b-8fb3-2fa48044ab7a] +description = "format greatest single digit non-exceptional ordinal numeral 9" + +[f370aae9-7ae7-4247-90ce-e8ff8c6934df] +description = "format non-exceptional ordinal numeral 5" + +[37f10dea-42a2-49de-bb92-0b690b677908] +description = "format non-exceptional ordinal numeral 6" + +[d8dfb9a2-3a1f-4fee-9dae-01af3600054e] +description = "format non-exceptional ordinal numeral 7" + +[505ec372-1803-42b1-9377-6934890fd055] +description = "format non-exceptional ordinal numeral 8" + +[8267072d-be1f-4f70-b34a-76b7557a47b9] +description = "format exceptional ordinal numeral 1" + +[4d8753cb-0364-4b29-84b8-4374a4fa2e3f] +description = "format exceptional ordinal numeral 2" + +[8d44c223-3a7e-4f48-a0ca-78e67bf98aa7] +description = "format exceptional ordinal numeral 3" + +[6c4f6c88-b306-4f40-bc78-97cdd583c21a] +description = "format smallest two digit non-exceptional ordinal numeral 10" + +[e257a43f-d2b1-457a-97df-25f0923fc62a] +description = "format non-exceptional ordinal numeral 11" + +[bb1db695-4d64-457f-81b8-4f5a2107e3f4] +description = "format non-exceptional ordinal numeral 12" + +[60a3187c-9403-4835-97de-4f10ebfd63e2] +description = "format non-exceptional ordinal numeral 13" + +[2bdcebc5-c029-4874-b6cc-e9bec80d603a] +description = "format exceptional ordinal numeral 21" + +[74ee2317-0295-49d2-baf0-d56bcefa14e3] +description = "format exceptional ordinal numeral 62" + +[b37c332d-7f68-40e3-8503-e43cbd67a0c4] +description = "format exceptional ordinal numeral 100" + +[0375f250-ce92-4195-9555-00e28ccc4d99] +description = "format exceptional ordinal numeral 101" + +[0d8a4974-9a8a-45a4-aca7-a9fb473c9836] +description = "format non-exceptional ordinal numeral 112" + +[06b62efe-199e-4ce7-970d-4bf73945713f] +description = "format exceptional ordinal numeral 123" diff --git a/exercises/practice/line-up/line_up.py b/exercises/practice/line-up/line_up.py new file mode 100644 index 00000000000..c6d20c4e0a7 --- /dev/null +++ b/exercises/practice/line-up/line_up.py @@ -0,0 +1,2 @@ +def line_up(name, number): + pass diff --git a/exercises/practice/line-up/line_up_test.py b/exercises/practice/line-up/line_up_test.py new file mode 100644 index 00000000000..1b593c4ddb6 --- /dev/null +++ b/exercises/practice/line-up/line_up_test.py @@ -0,0 +1,125 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/line-up/canonical-data.json +# File last updated on 2026-01-23 + +import unittest + +from line_up import ( + line_up, +) + + +class LineUpTest(unittest.TestCase): + def test_format_smallest_non_exceptional_ordinal_numeral_4(self): + self.assertEqual( + line_up("Gianna", 4), + "Gianna, you are the 4th customer we serve today. Thank you!", + ) + + def test_format_greatest_single_digit_non_exceptional_ordinal_numeral_9(self): + self.assertEqual( + line_up("Maarten", 9), + "Maarten, you are the 9th customer we serve today. Thank you!", + ) + + def test_format_non_exceptional_ordinal_numeral_5(self): + self.assertEqual( + line_up("Petronila", 5), + "Petronila, you are the 5th customer we serve today. Thank you!", + ) + + def test_format_non_exceptional_ordinal_numeral_6(self): + self.assertEqual( + line_up("Attakullakulla", 6), + "Attakullakulla, you are the 6th customer we serve today. Thank you!", + ) + + def test_format_non_exceptional_ordinal_numeral_7(self): + self.assertEqual( + line_up("Kate", 7), + "Kate, you are the 7th customer we serve today. Thank you!", + ) + + def test_format_non_exceptional_ordinal_numeral_8(self): + self.assertEqual( + line_up("Maximiliano", 8), + "Maximiliano, you are the 8th customer we serve today. Thank you!", + ) + + def test_format_exceptional_ordinal_numeral_1(self): + self.assertEqual( + line_up("Mary", 1), + "Mary, you are the 1st customer we serve today. Thank you!", + ) + + def test_format_exceptional_ordinal_numeral_2(self): + self.assertEqual( + line_up("Haruto", 2), + "Haruto, you are the 2nd customer we serve today. Thank you!", + ) + + def test_format_exceptional_ordinal_numeral_3(self): + self.assertEqual( + line_up("Henriette", 3), + "Henriette, you are the 3rd customer we serve today. Thank you!", + ) + + def test_format_smallest_two_digit_non_exceptional_ordinal_numeral_10(self): + self.assertEqual( + line_up("Alvarez", 10), + "Alvarez, you are the 10th customer we serve today. Thank you!", + ) + + def test_format_non_exceptional_ordinal_numeral_11(self): + self.assertEqual( + line_up("Jacqueline", 11), + "Jacqueline, you are the 11th customer we serve today. Thank you!", + ) + + def test_format_non_exceptional_ordinal_numeral_12(self): + self.assertEqual( + line_up("Juan", 12), + "Juan, you are the 12th customer we serve today. Thank you!", + ) + + def test_format_non_exceptional_ordinal_numeral_13(self): + self.assertEqual( + line_up("Patricia", 13), + "Patricia, you are the 13th customer we serve today. Thank you!", + ) + + def test_format_exceptional_ordinal_numeral_21(self): + self.assertEqual( + line_up("Washi", 21), + "Washi, you are the 21st customer we serve today. Thank you!", + ) + + def test_format_exceptional_ordinal_numeral_62(self): + self.assertEqual( + line_up("Nayra", 62), + "Nayra, you are the 62nd customer we serve today. Thank you!", + ) + + def test_format_exceptional_ordinal_numeral_100(self): + self.assertEqual( + line_up("John", 100), + "John, you are the 100th customer we serve today. Thank you!", + ) + + def test_format_exceptional_ordinal_numeral_101(self): + self.assertEqual( + line_up("Zeinab", 101), + "Zeinab, you are the 101st customer we serve today. Thank you!", + ) + + def test_format_non_exceptional_ordinal_numeral_112(self): + self.assertEqual( + line_up("Knud", 112), + "Knud, you are the 112th customer we serve today. Thank you!", + ) + + def test_format_exceptional_ordinal_numeral_123(self): + self.assertEqual( + line_up("Yma", 123), + "Yma, you are the 123rd customer we serve today. Thank you!", + ) From 9443eb16209946077665c3aeb8a182bf2041b7d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20B=20Nagy?= <20251272+BNAndras@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:39:51 -0800 Subject: [PATCH 34/40] Add `camicia` (#4075) * add `camicia` * Conditionally format long inputs * Add prerequisites --- config.json | 8 + .../practice/camicia/.docs/instructions.md | 84 ++++++ .../practice/camicia/.docs/introduction.md | 24 ++ exercises/practice/camicia/.meta/config.json | 19 ++ exercises/practice/camicia/.meta/example.py | 70 +++++ exercises/practice/camicia/.meta/template.j2 | 19 ++ exercises/practice/camicia/.meta/tests.toml | 94 ++++++ exercises/practice/camicia/camicia.py | 2 + exercises/practice/camicia/camicia_test.py | 267 ++++++++++++++++++ 9 files changed, 587 insertions(+) create mode 100644 exercises/practice/camicia/.docs/instructions.md create mode 100644 exercises/practice/camicia/.docs/introduction.md create mode 100644 exercises/practice/camicia/.meta/config.json create mode 100644 exercises/practice/camicia/.meta/example.py create mode 100644 exercises/practice/camicia/.meta/template.j2 create mode 100644 exercises/practice/camicia/.meta/tests.toml create mode 100644 exercises/practice/camicia/camicia.py create mode 100644 exercises/practice/camicia/camicia_test.py diff --git a/config.json b/config.json index 2bbf7486ad5..a436014ce71 100644 --- a/config.json +++ b/config.json @@ -1738,6 +1738,14 @@ ], "difficulty": 5 }, + { + "slug": "camicia", + "name": "Camicia", + "uuid": "ec314b34-2ee3-4eec-9a97-aece9e5fd6c2", + "practices": [], + "prerequisites": ["lists", "sets", "strings"], + "difficulty": 5 + }, { "slug": "rational-numbers", "name": "Rational Numbers", diff --git a/exercises/practice/camicia/.docs/instructions.md b/exercises/practice/camicia/.docs/instructions.md new file mode 100644 index 00000000000..db62fcef27d --- /dev/null +++ b/exercises/practice/camicia/.docs/instructions.md @@ -0,0 +1,84 @@ +# Instructions + +In this exercise, you will simulate a game very similar to the classic card game **Camicia**. +Your program will receive the initial configuration of two players' decks and must simulate the game until it ends (or detect that it will never end). + +## Rules + +- The deck is split between **two players**. + The player's cards are read from left to right, where the leftmost card is the top of the deck. +- A round consists of both players playing at least one card. +- Players take turns placing the **top card** of their deck onto a central pile. +- If the card is a **number card** (2-10), play simply passes to the other player. +- If the card is a **payment card**, a penalty must be paid: + - **J** → opponent must pay 1 card + - **Q** → opponent must pay 2 cards + - **K** → opponent must pay 3 cards + - **A** → opponent must pay 4 cards +- If the player paying a penalty reveals another payment card, that player stops paying the penalty. + The other player must then pay a penalty based on the new payment card. +- If the penalty is fully paid without interruption, the player who placed the **last payment card** collects the central pile and places it at the bottom of their deck. + That player then starts the next round. +- If a player runs out of cards and is unable to play a card (either while paying a penalty or when it is their turn), the other player collects the central pile. +- The moment when a player collects cards from the central pile is called a **trick**. +- If a player has all the cards in their possession after a trick, the game **ends**. +- The game **enters a loop** as soon as the decks are identical to what they were earlier during the game, **not** counting number cards! + +## Examples + +A small example of a match that ends. + +| Round | Player A | Player B | Pile | Penalty Due | +| :---- | :----------- | :------------------------- | :------------------------- | :---------- | +| 1 | 2 A 7 8 Q 10 | 3 4 5 6 K 9 J | | - | +| 1 | A 7 8 Q 10 | 3 4 5 6 K 9 J | 2 | - | +| 1 | A 7 8 Q 10 | 4 5 6 K 9 J | 2 3 | - | +| 1 | 7 8 Q 10 | 4 5 6 K 9 J | 2 3 A | Player B: 4 | +| 1 | 7 8 Q 10 | 5 6 K 9 J | 2 3 A 4 | Player B: 3 | +| 1 | 7 8 Q 10 | 6 K 9 J | 2 3 A 4 5 | Player B: 2 | +| 1 | 7 8 Q 10 | K 9 J | 2 3 A 4 5 6 | Player B: 1 | +| 1 | 7 8 Q 10 | 9 J | 2 3 A 4 5 6 K | Player A: 3 | +| 1 | 8 Q 10 | 9 J | 2 3 A 4 5 6 K 7 | Player A: 2 | +| 1 | Q 10 | 9 J | 2 3 A 4 5 6 K 7 8 | Player A: 1 | +| 1 | 10 | 9 J | 2 3 A 4 5 6 K 7 8 Q | Player B: 2 | +| 1 | 10 | J | 2 3 A 4 5 6 K 7 8 Q 9 | Player B: 1 | +| 1 | 10 | - | 2 3 A 4 5 6 K 7 8 Q 9 J | Player A: 1 | +| 1 | - | - | 2 3 A 4 5 6 K 7 8 Q 9 J 10 | - | +| 2 | - | 2 3 A 4 5 6 K 7 8 Q 9 J 10 | - | - | + +status: `"finished"`, cards: 13, tricks: 1 + +This is a small example of a match that loops. + +| Round | Player A | Player B | Pile | Penalty Due | +| :---- | :------- | :------- | :---- | :---------- | +| 1 | J 2 3 | 4 J 5 | - | - | +| 1 | 2 3 | 4 J 5 | J | Player B: 1 | +| 1 | 2 3 | J 5 | J 4 | - | +| 2 | 2 3 J 4 | J 5 | - | - | +| 2 | 3 J 4 | J 5 | 2 | - | +| 2 | 3 J 4 | 5 | 2 J | Player A: 1 | +| 2 | J 4 | 5 | 2 J 3 | - | +| 3 | J 4 | 5 2 J 3 | - | - | +| 3 | J 4 | 2 J 3 | 5 | - | +| 3 | 4 | 2 J 3 | 5 J | Player B: 1 | +| 3 | 4 | J 3 | 5 J 2 | - | +| 4 | 4 5 J 2 | J 3 | - | - | + +The start of round 4 matches the start of round 2. +Recall, the value of the number cards does not matter. + +status: `"loop"`, cards: 8, tricks: 3 + +## Your Task + +- Using the input, simulate the game following the rules above. +- Determine the following information regarding the game: + - **Status**: `"finished"` or `"loop"` + - **Cards**: total number of cards played throughout the game + - **Tricks**: number of times the central pile was collected + +~~~~exercism/advanced +For those who want to take on a more exciting challenge, the hunt for other records for the longest game with an end is still open. +There are 653,534,134,886,878,245,000 (approximately 654 quintillion) possibilities, and we haven't calculated them all yet! +~~~~ diff --git a/exercises/practice/camicia/.docs/introduction.md b/exercises/practice/camicia/.docs/introduction.md new file mode 100644 index 00000000000..761d8a82c50 --- /dev/null +++ b/exercises/practice/camicia/.docs/introduction.md @@ -0,0 +1,24 @@ +# Introduction + +One rainy afternoon, you sit at the kitchen table playing cards with your grandmother. +The game is her take on [Camicia][bmn]. + +At first it feels like just another friendly match: cards slapped down, laughter across the table, the occasional victorious grin from Nonna. +But as the game stretches on, something strange happens. +The same cards keep cycling back. +You play card after card, yet the end never seems to come. + +You start to wonder. +_Will this game ever finish? +Or could we keep playing forever?_ + +Later, driven by curiosity, you search online and to your surprise you discover that what happened wasn't just bad luck. +You and your grandmother may have stumbled upon one of the longest possible sequences! +Suddenly, you're hooked. +What began as a casual game has turned into a quest: _how long can such a game really last?_ +_Can you find a sequence even longer than the one you played at the kitchen table?_ +_Perhaps even long enough to set a new world record?_ + +And so, armed with nothing but a deck of cards and some algorithmic ingenuity, you decide to investigate... + +[bmn]: https://en.wikipedia.org/wiki/Beggar-my-neighbour diff --git a/exercises/practice/camicia/.meta/config.json b/exercises/practice/camicia/.meta/config.json new file mode 100644 index 00000000000..9956e52178b --- /dev/null +++ b/exercises/practice/camicia/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "BNAndras" + ], + "files": { + "solution": [ + "camicia.py" + ], + "test": [ + "camicia_test.py" + ], + "example": [ + ".meta/example.py" + ] + }, + "blurb": "Simulate the card game and determine whether the match ends or enters an infinite loop.", + "source": "Beggar-My-Neighbour", + "source_url": "https://www.richardpmann.com/beggar-my-neighbour-records.html" +} diff --git a/exercises/practice/camicia/.meta/example.py b/exercises/practice/camicia/.meta/example.py new file mode 100644 index 00000000000..7451490d956 --- /dev/null +++ b/exercises/practice/camicia/.meta/example.py @@ -0,0 +1,70 @@ +def simulate_game(player_a, player_b): + hand_a = list(map(get_value, player_a)) + hand_b = list(map(get_value, player_b)) + turn = "A" + pile = [] + seen = set() + total_tricks = 0 + cards_played = 0 + current_debt = 0 + + while True: + if not pile: + state = (tuple(hand_a), tuple(hand_b), turn) + if state in seen: + return { + "status": "loop", + "tricks": total_tricks, + "cards": cards_played + } + seen.add(state) + + active_hand = hand_a if turn == "A" else hand_b + other_hand = hand_b if turn == "A" else hand_a + + if not active_hand: + extra_trick = 0 if not pile else 1 + return { + "status": "finished", + "tricks": total_tricks + extra_trick, + "cards": cards_played, + } + + card_val = active_hand.pop(0) + pile.append(card_val) + cards_played += 1 + + if card_val > 0: + current_debt = card_val + turn = "B" if turn == "A" else "A" + else: + if current_debt > 0: + current_debt -= 1 + if not current_debt: + other_hand.extend(pile) + pile = [] + total_tricks += 1 + current_debt = 0 + + if not hand_a or not hand_b: + return { + "status": "finished", + "tricks": total_tricks, + "cards": cards_played + } + + turn = "B" if turn == "A" else "A" + else: + turn = "B" if turn == "A" else "A" + + +def get_value(card): + if card == "J": + return 1 + if card == "Q": + return 2 + if card == "K": + return 3 + if card == "A": + return 4 + return 0 diff --git a/exercises/practice/camicia/.meta/template.j2 b/exercises/practice/camicia/.meta/template.j2 new file mode 100644 index 00000000000..f7e443d6510 --- /dev/null +++ b/exercises/practice/camicia/.meta/template.j2 @@ -0,0 +1,19 @@ +{%- import "generator_macros.j2" as macros with context -%} +{{ macros.canonical_ref() }} + +{{ macros.header() }} + +class {{ exercise | camel_case }}Test(unittest.TestCase): + {% for case in cases -%} + def test_{{ case["description"] | to_snake }}(self): + {%- if case["input"]["playerA"]|length > 10 or case["input"]["playerB"]|length > 10 %} + # fmt: off + player_a = {{ case["input"]["playerA"] }} + player_b = {{ case["input"]["playerB"] }} + # fmt: on + {%- else %} + player_a = {{ case["input"]["playerA"] }} + player_b = {{ case["input"]["playerB"] }} + {%- endif %} + self.assertEqual(simulate_game(player_a, player_b), {{ case["expected"] }}) + {% endfor -%} diff --git a/exercises/practice/camicia/.meta/tests.toml b/exercises/practice/camicia/.meta/tests.toml new file mode 100644 index 00000000000..18d3fdd99f7 --- /dev/null +++ b/exercises/practice/camicia/.meta/tests.toml @@ -0,0 +1,94 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[0b7f737c-3ecd-4a55-b34d-e65c62a85c28] +description = "two cards, one trick" + +[27c19d75-53a5-48e5-b33b-232c3884d4f3] +description = "three cards, one trick" + +[9b02dd49-efaf-4b71-adca-a05c18a7c5b0] +description = "four cards, one trick" + +[fa3f4479-466a-4734-a001-ab79bfe27260] +description = "the ace reigns supreme" + +[07629689-f589-4f54-a6d1-8ce22776ce72] +description = "the king beats ace" + +[54d4a1c5-76fb-4d1e-8358-0e0296ac0601] +description = "the queen seduces the king" + +[c875500c-ff3d-47a4-bd1e-b60b90da80aa] +description = "the jack betrays the queen" + +[436875da-96ca-4149-be22-0b78173b8125] +description = "the 10 just wants to put on a show" + +[5be39bb6-1b34-4ce6-a1cd-0fcc142bb272] +description = "simple loop with decks of 3 cards" + +[2795dc21-0a2a-4c38-87c2-5a42e1ff15eb] +description = "the story is starting to get a bit complicated" + +[6999dfac-3fdc-41e2-b64b-38f4be228712] +description = "two tricks" + +[83dcd4f3-e089-4d54-855a-73f5346543a3] +description = "more tricks" + +[3107985a-f43e-486a-9ce8-db51547a9941] +description = "simple loop with decks of 4 cards" + +[dca32c31-11ed-49f6-b078-79ab912c1f7b] +description = "easy card combination" + +[1f8488d0-48d3-45ae-b819-59cedad0a5f4] +description = "easy card combination, inverted decks" + +[98878d35-623a-4d05-b81a-7bdc569eb88d] +description = "mirrored decks" + +[3e0ba597-ca10-484b-87a3-31a7df7d6da3] +description = "opposite decks" + +[92334ddb-aaa7-47fa-ab36-e928a8a6a67c] +description = "random decks #1" + +[30477523-9651-4860-84a3-e1ac461bb7fa] +description = "random decks #2" + +[20967de8-9e94-4e0e-9010-14bc1c157432] +description = "Kleber 1999" + +[9f2fdfe8-27f3-4323-816d-6bce98a9c6f7] +description = "Collins 2006" + +[c90b6f8d-7013-49f3-b5cb-14ea006cca1d] +description = "Mann and Wu 2007" + +[a3f1fbc5-1d0b-499a-92a5-22932dfc6bc8] +description = "Nessler 2012" + +[9cefb1ba-e6d1-4ab7-9d8f-76d8e0976d5f] +description = "Anderson 2013" + +[d37c0318-5be6-48d0-ab72-a7aaaff86179] +description = "Rucklidge 2014" + +[4305e479-ba87-432f-8a29-cd2bd75d2f05] +description = "Nessler 2021" + +[252f5cc3-b86d-4251-87ce-f920b7a6a559] +description = "Nessler 2022" + +[b9efcfa4-842f-4542-8112-8389c714d958] +description = "Casella 2024, first infinite game found" diff --git a/exercises/practice/camicia/camicia.py b/exercises/practice/camicia/camicia.py new file mode 100644 index 00000000000..61ac6ed552d --- /dev/null +++ b/exercises/practice/camicia/camicia.py @@ -0,0 +1,2 @@ +def simulate_game(player_a, player_b): + pass diff --git a/exercises/practice/camicia/camicia_test.py b/exercises/practice/camicia/camicia_test.py new file mode 100644 index 00000000000..6ad8db8a9f1 --- /dev/null +++ b/exercises/practice/camicia/camicia_test.py @@ -0,0 +1,267 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/camicia/canonical-data.json +# File last updated on 2026-01-24 + +import unittest + +from camicia import ( + simulate_game, +) + + +class CamiciaTest(unittest.TestCase): + def test_two_cards_one_trick(self): + player_a = ["2"] + player_b = ["3"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 2, "tricks": 1}, + ) + + def test_three_cards_one_trick(self): + player_a = ["2", "4"] + player_b = ["3"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 3, "tricks": 1}, + ) + + def test_four_cards_one_trick(self): + player_a = ["2", "4"] + player_b = ["3", "5", "6"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 4, "tricks": 1}, + ) + + def test_the_ace_reigns_supreme(self): + player_a = ["2", "A"] + player_b = ["3", "4", "5", "6", "7"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 7, "tricks": 1}, + ) + + def test_the_king_beats_ace(self): + player_a = ["2", "A"] + player_b = ["3", "4", "5", "6", "K"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 7, "tricks": 1}, + ) + + def test_the_queen_seduces_the_king(self): + player_a = ["2", "A", "7", "8", "Q"] + player_b = ["3", "4", "5", "6", "K"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 10, "tricks": 1}, + ) + + def test_the_jack_betrays_the_queen(self): + player_a = ["2", "A", "7", "8", "Q"] + player_b = ["3", "4", "5", "6", "K", "9", "J"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 12, "tricks": 1}, + ) + + def test_the_10_just_wants_to_put_on_a_show(self): + player_a = ["2", "A", "7", "8", "Q", "10"] + player_b = ["3", "4", "5", "6", "K", "9", "J"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 13, "tricks": 1}, + ) + + def test_simple_loop_with_decks_of_3_cards(self): + player_a = ["J", "2", "3"] + player_b = ["4", "J", "5"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "loop", "cards": 8, "tricks": 3}, + ) + + def test_the_story_is_starting_to_get_a_bit_complicated(self): + # fmt: off + player_a = ['2', '6', '6', 'J', '4', 'K', 'Q', '10', 'K', 'J', 'Q', '2', '3', 'K', '5', '6', 'Q', 'Q', 'A', 'A', '6', '9', 'K', 'A', '8', 'K', '2', 'A', '9', 'A', 'Q', '4', 'K', 'K', 'K', '3', '5', 'K', '8', 'Q', '3', 'Q', '7', 'J', 'K', 'J', '9', 'J', '3', '3', 'K', 'K', 'Q', 'A', 'K', '7', '10', 'A', 'Q', '7', '10', 'J', '4', '5', 'J', '9', '10', 'Q', 'J', 'J', 'K', '6', '10', 'J', '6', 'Q', 'J', '5', 'J', 'Q', 'Q', '8', '3', '8', 'A', '2', '6', '9', 'K', '7', 'J', 'K', 'K', '8', 'K', 'Q', '6', '10', 'J', '10', 'J', 'Q', 'J', '10', '3', '8', 'K', 'A', '6', '9', 'K', '2', 'A', 'A', '10', 'J', '6', 'A', '4', 'J', 'A', 'J', 'J', '6', '2', 'J', '3', 'K', '2', '5', '9', 'J', '9', '6', 'K', 'A', '5', 'Q', 'J', '2', 'Q', 'K', 'A', '3', 'K', 'J', 'K', '2', '5', '6', 'Q', 'J', 'Q', 'Q', 'J', '2', 'J', '9', 'Q', '7', '7', 'A', 'Q', '7', 'Q', 'J', 'K', 'J', 'A', '7', '7', '8', 'Q', '10', 'J', '10', 'J', 'J', '9', '2', 'A', '2'] + player_b = ['7', '2', '10', 'K', '8', '2', 'J', '9', 'A', '5', '6', 'J', 'Q', '6', 'K', '6', '5', 'A', '4', 'Q', '7', 'J', '7', '10', '2', 'Q', '8', '2', '2', 'K', 'J', 'A', '5', '5', 'A', '4', 'Q', '6', 'Q', 'K', '10', '8', 'Q', '2', '10', 'J', 'A', 'Q', '8', 'Q', 'Q', 'J', 'J', 'A', 'A', '9', '10', 'J', 'K', '4', 'Q', '10', '10', 'J', 'K', '10', '2', 'J', '7', 'A', 'K', 'K', 'J', 'A', 'J', '10', '8', 'K', 'A', '7', 'Q', 'Q', 'J', '3', 'Q', '4', 'A', '3', 'A', 'Q', 'Q', 'Q', '5', '4', 'K', 'J', '10', 'A', 'Q', 'J', '6', 'J', 'A', '10', 'A', '5', '8', '3', 'K', '5', '9', 'Q', '8', '7', '7', 'J', '7', 'Q', 'Q', 'Q', 'A', '7', '8', '9', 'A', 'Q', 'A', 'K', '8', 'A', 'A', 'J', '8', '4', '8', 'K', 'J', 'A', '10', 'Q', '8', 'J', '8', '6', '10', 'Q', 'J', 'J', 'A', 'A', 'J', '5', 'Q', '6', 'J', 'K', 'Q', '8', 'K', '4', 'Q', 'Q', '6', 'J', 'K', '4', '7', 'J', 'J', '9', '9', 'A', 'Q', 'Q', 'K', 'A', '6', '5', 'K'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 361, "tricks": 1}, + ) + + def test_two_tricks(self): + player_a = ["J"] + player_b = ["3", "J"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 5, "tricks": 2}, + ) + + def test_more_tricks(self): + player_a = ["J", "2", "4"] + player_b = ["3", "J", "A"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 12, "tricks": 4}, + ) + + def test_simple_loop_with_decks_of_4_cards(self): + player_a = ["2", "3", "J", "6"] + player_b = ["K", "5", "J", "7"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "loop", "cards": 16, "tricks": 4}, + ) + + def test_easy_card_combination(self): + # fmt: off + player_a = ['4', '8', '7', '5', '4', '10', '3', '9', '7', '3', '10', '10', '6', '8', '2', '8', '5', '4', '5', '9', '6', '5', '2', '8', '10', '9'] + player_b = ['6', '9', '4', '7', '2', '2', '3', '6', '7', '3', 'A', 'A', 'A', 'A', 'K', 'K', 'K', 'K', 'Q', 'Q', 'Q', 'Q', 'J', 'J', 'J', 'J'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 40, "tricks": 4}, + ) + + def test_easy_card_combination_inverted_decks(self): + # fmt: off + player_a = ['3', '3', '5', '7', '3', '2', '10', '7', '6', '7', 'A', 'A', 'A', 'A', 'K', 'K', 'K', 'K', 'Q', 'Q', 'Q', 'Q', 'J', 'J', 'J', 'J'] + player_b = ['5', '10', '8', '2', '6', '7', '2', '4', '9', '2', '6', '10', '10', '5', '4', '8', '4', '8', '6', '9', '8', '5', '9', '3', '4', '9'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 40, "tricks": 4}, + ) + + def test_mirrored_decks(self): + # fmt: off + player_a = ['2', 'A', '3', 'A', '3', 'K', '4', 'K', '2', 'Q', '2', 'Q', '10', 'J', '5', 'J', '6', '10', '2', '9', '10', '7', '3', '9', '6', '9'] + player_b = ['6', 'A', '4', 'A', '7', 'K', '4', 'K', '7', 'Q', '7', 'Q', '5', 'J', '8', 'J', '4', '5', '8', '9', '10', '6', '8', '3', '8', '5'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 59, "tricks": 4}, + ) + + def test_opposite_decks(self): + # fmt: off + player_a = ['4', 'A', '9', 'A', '4', 'K', '9', 'K', '6', 'Q', '8', 'Q', '8', 'J', '10', 'J', '9', '8', '4', '6', '3', '6', '5', '2', '4', '3'] + player_b = ['10', '7', '3', '2', '9', '2', '7', '8', '7', '5', 'J', '7', 'J', '10', 'Q', '10', 'Q', '3', 'K', '5', 'K', '6', 'A', '2', 'A', '5'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 151, "tricks": 21}, + ) + + def test_random_decks_1(self): + # fmt: off + player_a = ['K', '10', '9', '8', 'J', '8', '6', '9', '7', 'A', 'K', '5', '4', '4', 'J', '5', 'J', '4', '3', '5', '8', '6', '7', '7', '4', '9'] + player_b = ['6', '3', 'K', 'A', 'Q', '10', 'A', '2', 'Q', '8', '2', '10', '10', '2', 'Q', '3', 'K', '9', '7', 'A', '3', 'Q', '5', 'J', '2', '6'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 542, "tricks": 76}, + ) + + def test_random_decks_2(self): + # fmt: off + player_a = ['8', 'A', '4', '8', '5', 'Q', 'J', '2', '6', '2', '9', '7', 'K', 'A', '8', '10', 'K', '8', '10', '9', 'K', '6', '7', '3', 'K', '9'] + player_b = ['10', '5', '2', '6', 'Q', 'J', 'A', '9', '5', '5', '3', '7', '3', 'J', 'A', '2', 'Q', '3', 'J', 'Q', '4', '10', '4', '7', '4', '6'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 327, "tricks": 42}, + ) + + def test_kleber_1999(self): + # fmt: off + player_a = ['4', '8', '9', 'J', 'Q', '8', '5', '5', 'K', '2', 'A', '9', '8', '5', '10', 'A', '4', 'J', '3', 'K', '6', '9', '2', 'Q', 'K', '7'] + player_b = ['10', 'J', '3', '2', '4', '10', '4', '7', '5', '3', '6', '6', '7', 'A', 'J', 'Q', 'A', '7', '2', '10', '3', 'K', '9', '6', '8', 'Q'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 5790, "tricks": 805}, + ) + + def test_collins_2006(self): + # fmt: off + player_a = ['A', '8', 'Q', 'K', '9', '10', '3', '7', '4', '2', 'Q', '3', '2', '10', '9', 'K', 'A', '8', '7', '7', '4', '5', 'J', '9', '2', '10'] + player_b = ['4', 'J', 'A', 'K', '8', '5', '6', '6', 'A', '6', '5', 'Q', '4', '6', '10', '8', 'J', '2', '5', '7', 'Q', 'J', '3', '3', 'K', '9'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 6913, "tricks": 960}, + ) + + def test_mann_and_wu_2007(self): + # fmt: off + player_a = ['K', '2', 'K', 'K', '3', '3', '6', '10', 'K', '6', 'A', '2', '5', '5', '7', '9', 'J', 'A', 'A', '3', '4', 'Q', '4', '8', 'J', '6'] + player_b = ['4', '5', '2', 'Q', '7', '9', '9', 'Q', '7', 'J', '9', '8', '10', '3', '10', 'J', '4', '10', '8', '6', '8', '7', 'A', 'Q', '5', '2'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 7157, "tricks": 1007}, + ) + + def test_nessler_2012(self): + # fmt: off + player_a = ['10', '3', '6', '7', 'Q', '2', '9', '8', '2', '8', '4', 'A', '10', '6', 'K', '2', '10', 'A', '5', 'A', '2', '4', 'Q', 'J', 'K', '4'] + player_b = ['10', 'Q', '4', '6', 'J', '9', '3', 'J', '9', '3', '3', 'Q', 'K', '5', '9', '5', 'K', '6', '5', '7', '8', 'J', 'A', '7', '8', '7'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 7207, "tricks": 1015}, + ) + + def test_anderson_2013(self): + # fmt: off + player_a = ['6', '7', 'A', '3', 'Q', '3', '5', 'J', '3', '2', 'J', '7', '4', '5', 'Q', '10', '5', 'A', 'J', '2', 'K', '8', '9', '9', 'K', '3'] + player_b = ['4', 'J', '6', '9', '8', '5', '10', '7', '9', 'Q', '2', '7', '10', '8', '4', '10', 'A', '6', '4', 'A', '6', '8', 'Q', 'K', 'K', '2'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 7225, "tricks": 1016}, + ) + + def test_rucklidge_2014(self): + # fmt: off + player_a = ['8', 'J', '2', '9', '4', '4', '5', '8', 'Q', '3', '9', '3', '6', '2', '8', 'A', 'A', 'A', '9', '4', '7', '2', '5', 'Q', 'Q', '3'] + player_b = ['K', '7', '10', '6', '3', 'J', 'A', '7', '6', '5', '5', '8', '10', '9', '10', '4', '2', '7', 'K', 'Q', '10', 'K', '6', 'J', 'J', 'K'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 7959, "tricks": 1122}, + ) + + def test_nessler_2021(self): + # fmt: off + player_a = ['7', '2', '3', '4', 'K', '9', '6', '10', 'A', '8', '9', 'Q', '7', 'A', '4', '8', 'J', 'J', 'A', '4', '3', '2', '5', '6', '6', 'J'] + player_b = ['3', '10', '8', '9', '8', 'K', 'K', '2', '5', '5', '7', '6', '4', '3', '5', '7', 'A', '9', 'J', 'K', '2', 'Q', '10', 'Q', '10', 'Q'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 7972, "tricks": 1106}, + ) + + def test_nessler_2022(self): + # fmt: off + player_a = ['2', '10', '10', 'A', 'J', '3', '8', 'Q', '2', '5', '5', '5', '9', '2', '4', '3', '10', 'Q', 'A', 'K', 'Q', 'J', 'J', '9', 'Q', 'K'] + player_b = ['10', '7', '6', '3', '6', 'A', '8', '9', '4', '3', 'K', 'J', '6', 'K', '4', '9', '7', '8', '5', '7', '8', '2', 'A', '7', '4', '6'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 8344, "tricks": 1164}, + ) + + def test_casella_2024_first_infinite_game_found(self): + # fmt: off + player_a = ['2', '8', '4', 'K', '5', '2', '3', 'Q', '6', 'K', 'Q', 'A', 'J', '3', '5', '9', '8', '3', 'A', 'A', 'J', '4', '4', 'J', '7', '5'] + player_b = ['7', '7', '8', '6', '10', '10', '6', '10', '7', '2', 'Q', '6', '3', '2', '4', 'K', 'Q', '10', 'J', '5', '9', '8', '9', '9', 'K', 'A'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "loop", "cards": 474, "tricks": 66}, + ) From 7dcd00766d1c6562a242f864d51f2f94d2cf9414 Mon Sep 17 00:00:00 2001 From: Hassan El anabi Date: Mon, 26 Jan 2026 19:22:31 +0100 Subject: [PATCH 35/40] Fix minor inconsistencies in introduction.md of guidos-gorgeous-lasagna (#4073) * Fix minor inconsistencies in introduction.md - Update output to be consistent with the Python interactive-interpreter (e.g. print() prints its argument string without quotes). - Replace "Python terminal" with "Python shell" for accuracy. While informally acceptable, they are technically different (a shell runs inside a terminal). - Change parenthesis to plural form: parentheses - Replace "return expression" with "return statement" for accuracy. * Refine basic concepts after forum discussions * Clarify comment on the call to str.upper() --- concepts/basics/about.md | 77 +++++++++++-------- concepts/basics/introduction.md | 36 ++++++--- .../.docs/introduction.md | 74 ++++++++++-------- 3 files changed, 115 insertions(+), 72 deletions(-) diff --git a/concepts/basics/about.md b/concepts/basics/about.md index ef873ce418f..4a1a93e0e80 100644 --- a/concepts/basics/about.md +++ b/concepts/basics/about.md @@ -68,8 +68,9 @@ For example, `my_first_variable` can be re-assigned many times using `=`, and ca >>> print(type(my_first_variable)) +>>> my_first_variable = 'You can call me "str".' # Strings can be declared using single or double quote marks. >>> print(my_first_variable) -"Now, I'm a string." # Strings can be declared using single or double quote marks. +You can call me "str". import collections >>> my_first_variable = collections.Counter([1,1,2,3,3,3,4,5,6,7]) # Now my_first_variable has been re-bound to a Counter object. @@ -77,7 +78,7 @@ import collections >>> print(my_first_variable) ->>> Counter({3: 3, 1: 2, 2: 1, 4: 1, 5: 1, 6: 1, 7: 1}) +Counter({3: 3, 1: 2, 2: 1, 4: 1, 5: 1, 6: 1, 7: 1}) ``` @@ -108,7 +109,7 @@ When functions are bound to a [class][classes] name, they're referred to as [met Related functions and classes (_with their methods_) can be grouped together in the same file or module, and imported in part or in whole for use in other programs. The `def` keyword begins a [function definition][function definition]. -Each function can have zero or more formal [parameters][parameters] in `()` parenthesis, followed by a `:` colon. +Each function can have zero or more formal [parameters][parameters] in `()` parentheses, followed by a `:` colon. Statements for the _body_ of the function begin on the line following `def` and must be _indented in a block_: @@ -144,7 +145,7 @@ def add_two_numbers(number_one, number_two): return number_one + number_two -# Calling the function in the Python terminal returns the sum of the numbers. +# Calling the function in the Python shell returns the sum of the numbers. >>> add_two_numbers(3, 4) 7 @@ -155,28 +156,42 @@ def add_two_numbers(number_one, number_two): 11 ``` -Functions that do not have an _explicit_ `return` expression will _implicitly_ return the [`None`][none] object. +Functions that do not have an _explicit_ expression following a `return` will _implicitly_ return the [`None`][none] object. The details of `None` will be covered in a later exercise. For the purposes of this exercise and explanation, `None` is a placeholder that represents nothing, or null: ```python -# This function does not have an explicit return. -def add_two_numbers(number_one, number_two): - result = number_one + number_two +# This function will return `None` +def square_a_number(number): + square = number * number + return # <-- note that this return is not followed by an expression -# Calling the function in the Python terminal appears +# Calling the function in the Python shell appears # to not return anything at all. ->>> add_two_numbers(5, 7) +>>> square_a_number(2) >>> # Using print() with the function call shows that # the function is actually returning the **None** object. ->>> print(add_two_numbers(5, 7)) +>>> print(square_a_number(2)) None +``` + +Functions that omit `return` will also _implicitly_ return the [`None`][none] object. +This means that if you do not use `return` in a function, Python will return the `None` object for you. + +```python + +# This function omits a return keyword altogether +def add_two_numbers(number_one, number_two): + result = number_one + number_two +>>> add_two_numbers(5, 7) +>>> print(add_two_numbers(5, 7)) +None # Assigning the function call to a variable and printing # the variable will also show None. @@ -192,32 +207,32 @@ Functions are [_called_][calls] or invoked using their name followed by `()`. Dot (`.`) notation is used for calling functions defined inside a class or module. ```python ->>> def number_to_the_power_of(number_one, number_two): - return number_one ** number_two +>>> def raise_to_power(number, power): + return number ** power ... ->>> number_to_the_power_of(3,3) # Invoking the function with the arguments 3 and 3. +>>> raise_to_power(3,3) # Invoking the function with the arguments 3 and 3. 27 # A mis-match between the number of parameters and the number of arguments will raise an error. ->>> number_to_the_power_of(4,) +>>> raise_to_power(4,) ... Traceback (most recent call last): File "", line 1, in -TypeError: number_to_the_power_of() missing 1 required positional argument: 'number_two' +TypeError: raise_to_power() missing 1 required positional argument: 'power' # Calling methods or functions in classes and modules. >>> start_text = "my silly sentence for examples." ->>> str.upper(start_text) # Calling the upper() method for the built-in str class. -"MY SILLY SENTENCE FOR EXAMPLES." +>>> str.upper(start_text) # Calling the upper() method from the built-in str class on start_text. +'MY SILLY SENTENCE FOR EXAMPLES.' # Importing the math module import math >>> math.pow(2,4) # Calling the pow() function from the math module ->>> 16.0 +16.0 ``` @@ -273,32 +288,32 @@ Testing and `doctest` will be covered in a later concept. ```python # An example on a user-defined function. ->>> def number_to_the_power_of(number_one, number_two): +>>> def raise_to_power(number, power): """Raise a number to an arbitrary power. - :param number_one: int the base number. - :param number_two: int the power to raise the base number to. - :return: int - number raised to power of second number + :param number: int the base number. + :param power: int the power to raise the base number to. + :return: int - number raised to the specified power. - Takes number_one and raises it to the power of number_two, returning the result. + Takes a number and raises it to the specified power, returning the result. """ - return number_one ** number_two + return number ** power ... # Calling the .__doc__ attribute of the function and printing the result. ->>> print(number_to_the_power_of.__doc__) +>>> print(raise_to_power.__doc__) Raise a number to an arbitrary power. - :param number_one: int the base number. - :param number_two: int the power to raise the base number to. - :return: int - number raised to power of second number + :param number: int the base number. + :param power: int the power to raise the base number to. + :return: int - number raised to the specified power. - Takes number_one and raises it to the power of number_two, returning the result. + Takes a number and raises it to the specified power, returning the result. -# Printing the __doc__ attribute for the built-in type: str. +# Printing the __doc__ attribute of the built-in type: str. >>> print(str.__doc__) str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str diff --git a/concepts/basics/introduction.md b/concepts/basics/introduction.md index 818dd47deac..d44e6c9fd07 100644 --- a/concepts/basics/introduction.md +++ b/concepts/basics/introduction.md @@ -38,8 +38,9 @@ A name can be reassigned (or re-bound) to different values (different object typ >>> print(type(my_first_variable)) +>>> my_first_variable = 'You can call me "str".' #<-- Strings can be declared using single or double quote marks. >>> print(my_first_variable) -"Now, I'm a string." # Strings can be declared using single or double quote marks. +You can call me "str". ``` @@ -54,7 +55,7 @@ Constants should be defined at a [module][module] (file) level, and are typicall ## Functions The `def` keyword begins a [function definition][function definition]. -Each function can have zero or more formal [parameters][parameters] in `()` parenthesis, followed by a `:` colon. +Each function can have zero or more formal [parameters][parameters] in `()` parentheses, followed by a `:` colon. Statements for the _body_ of the function begin on the line following `def` and must be _indented in a block_. @@ -90,7 +91,7 @@ def add_two_numbers(number_one, number_two): return number_one + number_two -# Calling the function in the Python terminal returns the sum of the numbers. +# Calling the function in the Python shell returns the sum of the numbers. >>> add_two_numbers(3, 4) 7 @@ -102,29 +103,42 @@ def add_two_numbers(number_one, number_two): ``` -Functions that do not have an _explicit_ `return` expression will _implicitly_ return the [`None`][none] object. -This means that if you do not use `return` in a function, Python will return the `None` object for you. +Functions that do not have an _explicit_ expression following a `return` will _implicitly_ return the [`None`][none] object. The details of `None` will be covered in a later exercise. For the purposes of this exercise and explanation, `None` is a placeholder that represents nothing, or null: ```python -# This function does not have an explicit return. -def add_two_numbers(number_one, number_two): - result = number_one + number_two +# This function will return `None` +def square_a_number(number): + square = number * number + return # <-- note that this return is not followed by an expression -# Calling the function in the Python terminal appears +# Calling the function in the Python shell appears # to not return anything at all. ->>> add_two_numbers(5, 7) +>>> square_a_number(2) >>> # Using print() with the function call shows that # the function is actually returning the **None** object. ->>> print(add_two_numbers(5, 7)) +>>> print(square_a_number(2)) None +``` + +Functions that omit `return` will also _implicitly_ return the [`None`][none] object. +This means that if you do not use `return` in a function, Python will return the `None` object for you. +```python + +# This function omits a return keyword altogether +def add_two_numbers(number_one, number_two): + result = number_one + number_two + +>>> add_two_numbers(5, 7) +>>> print(add_two_numbers(5, 7)) +None # Assigning the function call to a variable and printing # the variable will also show None. diff --git a/exercises/concept/guidos-gorgeous-lasagna/.docs/introduction.md b/exercises/concept/guidos-gorgeous-lasagna/.docs/introduction.md index 20321da5303..b4bd6de8533 100644 --- a/exercises/concept/guidos-gorgeous-lasagna/.docs/introduction.md +++ b/exercises/concept/guidos-gorgeous-lasagna/.docs/introduction.md @@ -48,8 +48,9 @@ A name can be reassigned (or re-bound) to different values (different object typ >>> print(type(my_first_variable)) +>>> my_first_variable = 'You can call me "str".' #<-- Strings can be declared using single or double quote marks. >>> print(my_first_variable) -"Now, I'm a string." #<-- Strings can be declared using single or double quote marks. +You can call me "str". ``` @@ -63,7 +64,7 @@ Using `SCREAMING_SNAKE_CASE` signals that the name should not be re-assigned, or ## Functions The `def` keyword begins a [function definition][function definition]. -Each function can have zero or more formal [parameters][parameters] in `()` parenthesis, followed by a `:` colon. +Each function can have zero or more formal [parameters][parameters] in `()` parentheses, followed by a `:` colon. Statements for the _body_ of the function begin on the line following `def` and must be _indented in a block_. @@ -99,7 +100,7 @@ Functions _explicitly_ return a value or object via the [`return`][return] keywo return number_one + number_two -# Calling the function in the Python terminal returns the sum of the numbers. +# Calling the function in the Python shell returns the sum of the numbers. >>> add_two_numbers(3, 4) 7 @@ -111,29 +112,42 @@ Functions _explicitly_ return a value or object via the [`return`][return] keywo ``` -Functions that do not have an _explicit_ `return` expression will _implicitly_ return the [`None`][none] object. -This means that if you do not use `return` in a function, Python will return the `None` object for you. +Functions that do not have an _explicit_ expression following a `return` will _implicitly_ return the [`None`][none] object. The details of `None` will be covered in a later exercise. For the purposes of this exercise and explanation, `None` is a placeholder that represents nothing, or null: ```python -# This function does not have an explicit return. -def add_two_numbers(number_one, number_two): - result = number_one + number_two +# This function will return `None` +def square_a_number(number): + square = number * number + return # <-- note that this return is not followed by an expression -# Calling the function in the Python terminal appears +# Calling the function in the Python shell appears # to not return anything at all. ->>> add_two_numbers(5, 7) +>>> square_a_number(2) >>> # Using print() with the function call shows that # the function is actually returning the **None** object. ->>> print(add_two_numbers(5, 7)) +>>> print(square_a_number(2)) None +``` + +Functions that omit `return` will also _implicitly_ return the [`None`][none] object. +This means that if you do not use `return` in a function, Python will return the `None` object for you. +```python + +# This function omits a return keyword altogether +def add_two_numbers(number_one, number_two): + result = number_one + number_two + +>>> add_two_numbers(5, 7) +>>> print(add_two_numbers(5, 7)) +None # Assigning the function call to a variable and printing # the variable will also show None. @@ -149,32 +163,32 @@ Functions are [_called_][calls] or invoked using their name followed by `()`. Dot (`.`) notation is used for calling functions defined inside a class or module. ```python ->>> def number_to_the_power_of(number_one, number_two): - return number_one ** number_two +>>> def raise_to_power(number, power): + return number ** power ... ->>> number_to_the_power_of(3,3) # Invoking the function with the arguments 3 and 3. +>>> raise_to_power(3,3) # Invoking the function with the arguments 3 and 3. 27 # A mismatch between the number of parameters and the number of arguments will raise an error. ->>> number_to_the_power_of(4,) +>>> raise_to_power(4,) ... Traceback (most recent call last): File "", line 1, in -TypeError: number_to_the_power_of() missing 1 required positional argument: 'number_two' +TypeError: raise_to_power() missing 1 required positional argument: 'power' # Calling methods or functions in classes and modules. >>> start_text = "my silly sentence for examples." ->>> str.upper(start_text) # Calling the upper() method for the built-in str class. -"MY SILLY SENTENCE FOR EXAMPLES." +>>> str.upper(start_text) # Calling the upper() method from the built-in str class on start_text. +'MY SILLY SENTENCE FOR EXAMPLES.' # Importing the math module import math >>> math.pow(2,4) # Calling the pow() function from the math module ->>> 16.0 +16.0 ``` @@ -216,28 +230,28 @@ Docstrings can also function as [lightweight unit tests][doctests], which will b ```python # An example on a user-defined function. ->>> def number_to_the_power_of(number_one, number_two): +>>> def raise_to_power(number, power): """Raise a number to an arbitrary power. - :param number_one: int the base number. - :param number_two: int the power to raise the base number to. - :return: int - number raised to power of second number + :param number: int the base number. + :param power: int the power to raise the base number to. + :return: int - number raised to the specified power. - Takes number_one and raises it to the power of number_two, returning the result. + Takes a number and raises it to the specified power, returning the result. """ - return number_one ** number_two + return number ** power ... # Calling the .__doc__ attribute of the function and printing the result. ->>> print(number_to_the_power_of.__doc__) +>>> print(raise_to_power.__doc__) Raise a number to an arbitrary power. - :param number_one: int the base number. - :param number_two: int the power to raise the base number to. - :return: int - number raised to power of second number + :param number: int the base number. + :param power: int the power to raise the base number to. + :return: int - number raised to the specified power. - Takes number_one and raises it to the power of number_two, returning the result. + Takes a number and raises it to the specified power, returning the result. ``` [calls]: https://docs.python.org/3/reference/expressions.html#calls From 159a40351b612aaca06082c9a5e0f383d74ecc0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20B=20Nagy?= <20251272+BNAndras@users.noreply.github.com> Date: Tue, 27 Jan 2026 10:46:27 -0800 Subject: [PATCH 36/40] Add `game-of-life` (#4077) * Add `game-of-life` * Fix test case formatting --- config.json | 10 ++ .../game-of-life/.docs/instructions.md | 11 ++ .../game-of-life/.docs/introduction.md | 9 ++ .../practice/game-of-life/.meta/config.json | 19 +++ .../practice/game-of-life/.meta/example.py | 34 +++++ .../practice/game-of-life/.meta/template.j2 | 22 ++++ .../practice/game-of-life/.meta/tests.toml | 34 +++++ .../practice/game-of-life/game_of_life.py | 2 + .../game-of-life/game_of_life_test.py | 117 ++++++++++++++++++ 9 files changed, 258 insertions(+) create mode 100644 exercises/practice/game-of-life/.docs/instructions.md create mode 100644 exercises/practice/game-of-life/.docs/introduction.md create mode 100644 exercises/practice/game-of-life/.meta/config.json create mode 100644 exercises/practice/game-of-life/.meta/example.py create mode 100644 exercises/practice/game-of-life/.meta/template.j2 create mode 100644 exercises/practice/game-of-life/.meta/tests.toml create mode 100644 exercises/practice/game-of-life/game_of_life.py create mode 100644 exercises/practice/game-of-life/game_of_life_test.py diff --git a/config.json b/config.json index a436014ce71..707dae2ce33 100644 --- a/config.json +++ b/config.json @@ -959,6 +959,16 @@ "prerequisites": ["basics", "bools", "numbers", "classes"], "difficulty": 2 }, + { + "slug": "game-of-life", + "name": "Conway's Game of Life", + "uuid": "1675a497-d3b2-4772-bbee-4edae5a44e91", + "practices": [], + "prerequisites": [ + "lists" + ], + "difficulty": 3 + }, { "slug": "eliuds-eggs", "name": "Eliud's Eggs", diff --git a/exercises/practice/game-of-life/.docs/instructions.md b/exercises/practice/game-of-life/.docs/instructions.md new file mode 100644 index 00000000000..49531406489 --- /dev/null +++ b/exercises/practice/game-of-life/.docs/instructions.md @@ -0,0 +1,11 @@ +# Instructions + +After each generation, the cells interact with their eight neighbors, which are cells adjacent horizontally, vertically, or diagonally. + +The following rules are applied to each cell: + +- Any live cell with two or three live neighbors lives on. +- Any dead cell with exactly three live neighbors becomes a live cell. +- All other cells die or stay dead. + +Given a matrix of 1s and 0s (corresponding to live and dead cells), apply the rules to each cell, and return the next generation. diff --git a/exercises/practice/game-of-life/.docs/introduction.md b/exercises/practice/game-of-life/.docs/introduction.md new file mode 100644 index 00000000000..2347b936e44 --- /dev/null +++ b/exercises/practice/game-of-life/.docs/introduction.md @@ -0,0 +1,9 @@ +# Introduction + +[Conway's Game of Life][game-of-life] is a fascinating cellular automaton created by the British mathematician John Horton Conway in 1970. + +The game consists of a two-dimensional grid of cells that can either be "alive" or "dead." + +After each generation, the cells interact with their eight neighbors via a set of rules, which define the new generation. + +[game-of-life]: https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life diff --git a/exercises/practice/game-of-life/.meta/config.json b/exercises/practice/game-of-life/.meta/config.json new file mode 100644 index 00000000000..f89d6569022 --- /dev/null +++ b/exercises/practice/game-of-life/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "BNAndras" + ], + "files": { + "solution": [ + "game_of_life.py" + ], + "test": [ + "game_of_life_test.py" + ], + "example": [ + ".meta/example.py" + ] + }, + "blurb": "Implement Conway's Game of Life.", + "source": "Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life" +} diff --git a/exercises/practice/game-of-life/.meta/example.py b/exercises/practice/game-of-life/.meta/example.py new file mode 100644 index 00000000000..d4a430a2a3e --- /dev/null +++ b/exercises/practice/game-of-life/.meta/example.py @@ -0,0 +1,34 @@ +def tick(matrix): + def count_neighbors(r, c): + count = 0 + for dr, dc in [ + (-1, -1), (-1, 0), (-1, 1), + (0, -1), (0, 1), + (1, -1), (1, 0), (1, 1), + ]: + nr, nc = r + dr, c + dc + if 0 <= nr < rows and 0 <= nc < cols: + count += matrix[nr][nc] + return count + + if not matrix: + return [] + + rows = len(matrix) + cols = len(matrix[0]) + + new_matrix = [] + for r in range(rows): + new_row = [] + for c in range(cols): + neighbors = count_neighbors(r, c) + current = matrix[r][c] + state = 0 + if current == 1 and (neighbors == 2 or neighbors == 3): + state = 1 + elif current == 0 and neighbors == 3: + state = 1 + new_row.append(state) + new_matrix.append(new_row) + + return new_matrix diff --git a/exercises/practice/game-of-life/.meta/template.j2 b/exercises/practice/game-of-life/.meta/template.j2 new file mode 100644 index 00000000000..324b1519baa --- /dev/null +++ b/exercises/practice/game-of-life/.meta/template.j2 @@ -0,0 +1,22 @@ +{% import "generator_macros.j2" as macros with context %} +{{ macros.canonical_ref() }} + +{{ macros.header() }} + +class {{ exercise | camel_case }}Test(unittest.TestCase): + {% for case in cases %} + def test_{{ case.description | to_snake }}(self): + matrix = [ + {% for row in case.input.matrix %} + {{ row }}, + {% endfor %} + ] + expected = [ + {% for row in case.expected %} + {{ row }}, + {% endfor %} + ] + self.assertEqual( + tick(matrix), expected + ) + {% endfor %} diff --git a/exercises/practice/game-of-life/.meta/tests.toml b/exercises/practice/game-of-life/.meta/tests.toml new file mode 100644 index 00000000000..398cd4546e5 --- /dev/null +++ b/exercises/practice/game-of-life/.meta/tests.toml @@ -0,0 +1,34 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[ae86ea7d-bd07-4357-90b3-ac7d256bd5c5] +description = "empty matrix" + +[4ea5ccb7-7b73-4281-954a-bed1b0f139a5] +description = "live cells with zero live neighbors die" + +[df245adc-14ff-4f9c-b2ae-f465ef5321b2] +description = "live cells with only one live neighbor die" + +[2a713b56-283c-48c8-adae-1d21306c80ae] +description = "live cells with two live neighbors stay alive" + +[86d5c5a5-ab7b-41a1-8907-c9b3fc5e9dae] +description = "live cells with three live neighbors stay alive" + +[015f60ac-39d8-4c6c-8328-57f334fc9f89] +description = "dead cells with three live neighbors become alive" + +[2ee69c00-9d41-4b8b-89da-5832e735ccf1] +description = "live cells with four or more neighbors die" + +[a79b42be-ed6c-4e27-9206-43da08697ef6] +description = "bigger matrix" diff --git a/exercises/practice/game-of-life/game_of_life.py b/exercises/practice/game-of-life/game_of_life.py new file mode 100644 index 00000000000..70694804ddd --- /dev/null +++ b/exercises/practice/game-of-life/game_of_life.py @@ -0,0 +1,2 @@ +def tick(matrix): + pass diff --git a/exercises/practice/game-of-life/game_of_life_test.py b/exercises/practice/game-of-life/game_of_life_test.py new file mode 100644 index 00000000000..b62b74407cb --- /dev/null +++ b/exercises/practice/game-of-life/game_of_life_test.py @@ -0,0 +1,117 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/game-of-life/canonical-data.json +# File last updated on 2026-01-26 + +import unittest + +from game_of_life import ( + tick, +) + + +class GameOfLifeTest(unittest.TestCase): + def test_empty_matrix(self): + matrix = [] + expected = [] + self.assertEqual(tick(matrix), expected) + + def test_live_cells_with_zero_live_neighbors_die(self): + matrix = [ + [0, 0, 0], + [0, 1, 0], + [0, 0, 0], + ] + expected = [ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + ] + self.assertEqual(tick(matrix), expected) + + def test_live_cells_with_only_one_live_neighbor_die(self): + matrix = [ + [0, 0, 0], + [0, 1, 0], + [0, 1, 0], + ] + expected = [ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + ] + self.assertEqual(tick(matrix), expected) + + def test_live_cells_with_two_live_neighbors_stay_alive(self): + matrix = [ + [1, 0, 1], + [1, 0, 1], + [1, 0, 1], + ] + expected = [ + [0, 0, 0], + [1, 0, 1], + [0, 0, 0], + ] + self.assertEqual(tick(matrix), expected) + + def test_live_cells_with_three_live_neighbors_stay_alive(self): + matrix = [ + [0, 1, 0], + [1, 0, 0], + [1, 1, 0], + ] + expected = [ + [0, 0, 0], + [1, 0, 0], + [1, 1, 0], + ] + self.assertEqual(tick(matrix), expected) + + def test_dead_cells_with_three_live_neighbors_become_alive(self): + matrix = [ + [1, 1, 0], + [0, 0, 0], + [1, 0, 0], + ] + expected = [ + [0, 0, 0], + [1, 1, 0], + [0, 0, 0], + ] + self.assertEqual(tick(matrix), expected) + + def test_live_cells_with_four_or_more_neighbors_die(self): + matrix = [ + [1, 1, 1], + [1, 1, 1], + [1, 1, 1], + ] + expected = [ + [1, 0, 1], + [0, 0, 0], + [1, 0, 1], + ] + self.assertEqual(tick(matrix), expected) + + def test_bigger_matrix(self): + matrix = [ + [1, 1, 0, 1, 1, 0, 0, 0], + [1, 0, 1, 1, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 1, 1, 0], + [1, 0, 0, 0, 1, 1, 0, 0], + [1, 1, 0, 0, 0, 1, 1, 1], + [0, 0, 1, 0, 1, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 1, 1], + ] + expected = [ + [1, 1, 0, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 0], + [1, 0, 1, 1, 1, 1, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 1], + [1, 1, 0, 0, 1, 0, 0, 1], + [1, 1, 0, 1, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 1], + ] + self.assertEqual(tick(matrix), expected) From fcba46f0af3c2533b143c8503e166b0c05f72689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20B=20Nagy?= <20251272+BNAndras@users.noreply.github.com> Date: Fri, 30 Jan 2026 11:06:23 -0800 Subject: [PATCH 37/40] Add `state-of-tic-tac-toe` (#4080) * Add `state-of-tic-tac-toe` * Update config.json Reduced difficulty of `eluids-eggs` and re-ordered exercises. --------- Co-authored-by: BethanyG --- config.json | 30 ++- .../.docs/instructions.md | 101 ++++++++ .../state-of-tic-tac-toe/.meta/config.json | 19 ++ .../state-of-tic-tac-toe/.meta/example.py | 49 ++++ .../state-of-tic-tac-toe/.meta/template.j2 | 28 ++ .../state-of-tic-tac-toe/.meta/tests.toml | 101 ++++++++ .../state_of_tic_tac_toe.py | 2 + .../state_of_tic_tac_toe_test.py | 245 ++++++++++++++++++ 8 files changed, 566 insertions(+), 9 deletions(-) create mode 100644 exercises/practice/state-of-tic-tac-toe/.docs/instructions.md create mode 100644 exercises/practice/state-of-tic-tac-toe/.meta/config.json create mode 100644 exercises/practice/state-of-tic-tac-toe/.meta/example.py create mode 100644 exercises/practice/state-of-tic-tac-toe/.meta/template.j2 create mode 100644 exercises/practice/state-of-tic-tac-toe/.meta/tests.toml create mode 100644 exercises/practice/state-of-tic-tac-toe/state_of_tic_tac_toe.py create mode 100644 exercises/practice/state-of-tic-tac-toe/state_of_tic_tac_toe_test.py diff --git a/config.json b/config.json index 707dae2ce33..13345d628b1 100644 --- a/config.json +++ b/config.json @@ -668,6 +668,21 @@ ], "difficulty": 2 }, + { + "slug": "eliuds-eggs", + "name": "Eliud's Eggs", + "uuid": "356e2d29-7efc-4fa3-bec7-8b61c3e967da", + "practices": ["loops"], + "prerequisites": [ + "basics", + "lists", + "list-methods", + "loops", + "strings", + "string-methods" + ], + "difficulty": 2 + }, { "slug": "protein-translation", "name": "Protein Translation", @@ -970,17 +985,14 @@ "difficulty": 3 }, { - "slug": "eliuds-eggs", - "name": "Eliud's Eggs", - "uuid": "356e2d29-7efc-4fa3-bec7-8b61c3e967da", + "slug": "state-of-tic-tac-toe", + "name": "State of Tic-Tac-Toe", + "uuid": "cb40b36f-f7dc-4018-aad5-38976defb352", "practices": ["loops"], "prerequisites": [ - "basics", - "lists", - "list-methods", - "loops", - "strings", - "string-methods" + "conditionals", + "lists", + "loops" ], "difficulty": 3 }, diff --git a/exercises/practice/state-of-tic-tac-toe/.docs/instructions.md b/exercises/practice/state-of-tic-tac-toe/.docs/instructions.md new file mode 100644 index 00000000000..1a03ebb6cb4 --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.docs/instructions.md @@ -0,0 +1,101 @@ +# Instructions + +In this exercise, you're going to implement a program that determines the state of a [tic-tac-toe][] game. +(_You may also know the game as "noughts and crosses" or "Xs and Os"._) + +The game is played on a 3×3 grid. +Players take turns to place `X`s and `O`s on the grid. +The game ends when one player has won by placing three of marks in a row, column, or along a diagonal of the grid, or when the entire grid is filled up. + +In this exercise, we will assume that `X` starts. + +It's your job to determine which state a given game is in. + +There are 3 potential game states: + +- The game is **ongoing**. +- The game ended in a **draw**. +- The game ended in a **win**. + +If the given board is invalid, throw an appropriate error. + +If a board meets the following conditions, it is invalid: + +- The given board cannot be reached when turns are taken in the correct order (remember that `X` starts). +- The game was played after it already ended. + +## Examples + +### Ongoing game + +```text + | | + X | | +___|___|___ + | | + | X | O +___|___|___ + | | + O | X | + | | +``` + +### Draw + +```text + | | + X | O | X +___|___|___ + | | + X | X | O +___|___|___ + | | + O | X | O + | | +``` + +### Win + +```text + | | + X | X | X +___|___|___ + | | + | O | O +___|___|___ + | | + | | + | | +``` + +### Invalid + +#### Wrong turn order + +```text + | | + O | O | X +___|___|___ + | | + | | +___|___|___ + | | + | | + | | +``` + +#### Continued playing after win + +```text + | | + X | X | X +___|___|___ + | | + O | O | O +___|___|___ + | | + | | + | | +``` + +[tic-tac-toe]: https://en.wikipedia.org/wiki/Tic-tac-toe diff --git a/exercises/practice/state-of-tic-tac-toe/.meta/config.json b/exercises/practice/state-of-tic-tac-toe/.meta/config.json new file mode 100644 index 00000000000..737a603c6e4 --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "BNAndras" + ], + "files": { + "solution": [ + "state_of_tic_tac_toe.py" + ], + "test": [ + "state_of_tic_tac_toe_test.py" + ], + "example": [ + ".meta/example.py" + ] + }, + "blurb": "Determine the game state of a match of Tic-Tac-Toe.", + "source": "Created by Sascha Mann for the Julia track of the Exercism Research Experiment.", + "source_url": "https://github.com/exercism/research_experiment_1/tree/julia-dev/exercises/julia-1-a" +} diff --git a/exercises/practice/state-of-tic-tac-toe/.meta/example.py b/exercises/practice/state-of-tic-tac-toe/.meta/example.py new file mode 100644 index 00000000000..5f0bf70291a --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.meta/example.py @@ -0,0 +1,49 @@ +def gamestate(board): + def check_if_won(player): + # Rows + for row in board: + if all(cell == player for cell in row): + return True + + # Cols + for col in range(3): + if all(board[row][col] == player for row in range(3)): + return True + + # top left to bottom right + if all(board[i][i] == player for i in range(3)): + return True + + # top right to bottom left + if all(board[i][2-i] == player for i in range(3)): + return True + return False + + x_count = sum(row.count("X") for row in board) + o_count = sum(row.count("O") for row in board) + + if o_count > x_count: + raise ValueError("Wrong turn order: O started") + if x_count > o_count + 1: + raise ValueError("Wrong turn order: X went twice") + + x_won = check_if_won("X") + o_won = check_if_won("O") + + if x_won and o_won: + raise ValueError("Impossible board: game should have ended after the game was won") + + if x_won: + if x_count == o_count: + raise ValueError("Impossible board: game should have ended after the game was won") + return "win" + + if o_won: + if x_count > o_count: + raise ValueError("Impossible board: game should have ended after the game was won") + return "win" + + if x_count + o_count == 9: + return "draw" + + return "ongoing" diff --git a/exercises/practice/state-of-tic-tac-toe/.meta/template.j2 b/exercises/practice/state-of-tic-tac-toe/.meta/template.j2 new file mode 100644 index 00000000000..826f2703b41 --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.meta/template.j2 @@ -0,0 +1,28 @@ +{% import "generator_macros.j2" as macros with context %} +{{ macros.canonical_ref() }} + +{{ macros.header() }} + +{% macro test_case(case) -%} + def test_{{ case["description"]|to_snake }}(self): + board = [ + {% for row in case["input"]["board"] -%} + "{{ row }}", + {% endfor -%} + ] + {%- if case is error_case %} + with self.assertRaises(ValueError) as err: + {{ case["property"]|to_snake }}(board) + self.assertEqual(type(err.exception), ValueError) + self.assertEqual(err.exception.args[0], "{{ case["expected"]["error"] }}") + {%- else %} + self.assertEqual({{ case["property"]|to_snake }}(board), "{{ case["expected"] }}") + {%- endif %} +{%- endmacro %} + +class {{ exercise|camel_case }}Test(unittest.TestCase): + {%- for category in cases %} + {%- for case in category["cases"] %} + {{ test_case(case) }} + {%- endfor %} + {%- endfor %} diff --git a/exercises/practice/state-of-tic-tac-toe/.meta/tests.toml b/exercises/practice/state-of-tic-tac-toe/.meta/tests.toml new file mode 100644 index 00000000000..8fc25e2118d --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.meta/tests.toml @@ -0,0 +1,101 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[fe8e9fa9-37af-4d7e-aa24-2f4b8517161a] +description = "Won games -> Finished game where X won via left column victory" + +[96c30df5-ae23-4cf6-bf09-5ef056dddea1] +description = "Won games -> Finished game where X won via middle column victory" + +[0d7a4b0a-2afd-4a75-8389-5fb88ab05eda] +description = "Won games -> Finished game where X won via right column victory" + +[bd1007c0-ec5d-4c60-bb9f-1a4f22177d51] +description = "Won games -> Finished game where O won via left column victory" + +[c032f800-5735-4354-b1b9-46f14d4ee955] +description = "Won games -> Finished game where O won via middle column victory" + +[662c8902-c94a-4c4c-9d9c-e8ca513db2b4] +description = "Won games -> Finished game where O won via right column victory" + +[2d62121f-7e3a-44a0-9032-0d73e3494941] +description = "Won games -> Finished game where X won via top row victory" + +[108a5e82-cc61-409f-aece-d7a18c1beceb] +description = "Won games -> Finished game where X won via middle row victory" +include = false + +[346527db-4db9-4a96-b262-d7023dc022b0] +description = "Won games -> Finished game where X won via middle row victory" +reimplements = "108a5e82-cc61-409f-aece-d7a18c1beceb" + +[a013c583-75f8-4ab2-8d68-57688ff04574] +description = "Won games -> Finished game where X won via bottom row victory" + +[2c08e7d7-7d00-487f-9442-e7398c8f1727] +description = "Won games -> Finished game where O won via top row victory" + +[bb1d6c62-3e3f-4d1a-9766-f8803c8ed70f] +description = "Won games -> Finished game where O won via middle row victory" + +[6ef641e9-12ec-44f5-a21c-660ea93907af] +description = "Won games -> Finished game where O won via bottom row victory" + +[ab145b7b-26a7-426c-ab71-bf418cd07f81] +description = "Won games -> Finished game where X won via falling diagonal victory" + +[7450caab-08f5-4f03-a74b-99b98c4b7a4b] +description = "Won games -> Finished game where X won via rising diagonal victory" + +[c2a652ee-2f93-48aa-a710-a70cd2edce61] +description = "Won games -> Finished game where O won via falling diagonal victory" + +[5b20ceea-494d-4f0c-a986-b99efc163bcf] +description = "Won games -> Finished game where O won via rising diagonal victory" + +[035a49b9-dc35-47d3-9d7c-de197161b9d4] +description = "Won games -> Finished game where X won via a row and a column victory" + +[e5dfdeb0-d2bf-4b5a-b307-e673f69d4a53] +description = "Won games -> Finished game where X won via two diagonal victories" + +[b42ed767-194c-4364-b36e-efbfb3de8788] +description = "Drawn games -> Draw" + +[227a76b2-0fef-4e16-a4bd-8f9d7e4c3b13] +description = "Drawn games -> Another draw" + +[4d93f15c-0c40-43d6-b966-418b040012a9] +description = "Ongoing games -> Ongoing game: one move in" + +[c407ae32-4c44-4989-b124-2890cf531f19] +description = "Ongoing games -> Ongoing game: two moves in" + +[199b7a8d-e2b6-4526-a85e-78b416e7a8a9] +description = "Ongoing games -> Ongoing game: five moves in" + +[1670145b-1e3d-4269-a7eb-53cd327b302e] +description = "Invalid boards -> Invalid board: X went twice" + +[47c048e8-b404-4bcf-9e51-8acbb3253f3b] +description = "Invalid boards -> Invalid board: O started" + +[b1dc8b13-46c4-47db-a96d-aa90eedc4e8d] +description = "Invalid boards -> Invalid board" +include = false + +[6c1920f2-ab5c-4648-a0c9-997414dda5eb] +description = "Invalid boards -> Invalid board: X won and O kept playing" +reimplements = "b1dc8b13-46c4-47db-a96d-aa90eedc4e8d" + +[4801cda2-f5b7-4c36-8317-3cdd167ac22c] +description = "Invalid boards -> Invalid board: players kept playing after a win" diff --git a/exercises/practice/state-of-tic-tac-toe/state_of_tic_tac_toe.py b/exercises/practice/state-of-tic-tac-toe/state_of_tic_tac_toe.py new file mode 100644 index 00000000000..b41c78f383f --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/state_of_tic_tac_toe.py @@ -0,0 +1,2 @@ +def gamestate(board): + pass diff --git a/exercises/practice/state-of-tic-tac-toe/state_of_tic_tac_toe_test.py b/exercises/practice/state-of-tic-tac-toe/state_of_tic_tac_toe_test.py new file mode 100644 index 00000000000..173ecf887a0 --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/state_of_tic_tac_toe_test.py @@ -0,0 +1,245 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/state-of-tic-tac-toe/canonical-data.json +# File last updated on 2026-01-30 + +import unittest + +from state_of_tic_tac_toe import ( + gamestate, +) + + +class StateOfTicTacToeTest(unittest.TestCase): + def test_finished_game_where_x_won_via_left_column_victory(self): + board = [ + "XOO", + "X ", + "X ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_middle_column_victory(self): + board = [ + "OXO", + " X ", + " X ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_right_column_victory(self): + board = [ + "OOX", + " X", + " X", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_left_column_victory(self): + board = [ + "OXX", + "OX ", + "O ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_middle_column_victory(self): + board = [ + "XOX", + " OX", + " O ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_right_column_victory(self): + board = [ + "XXO", + " XO", + " O", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_top_row_victory(self): + board = [ + "XXX", + "XOO", + "O ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_middle_row_victory(self): + board = [ + "O ", + "XXX", + " O ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_bottom_row_victory(self): + board = [ + " OO", + "O X", + "XXX", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_top_row_victory(self): + board = [ + "OOO", + "XXO", + "XX ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_middle_row_victory(self): + board = [ + "XX ", + "OOO", + "X ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_bottom_row_victory(self): + board = [ + "XOX", + " XX", + "OOO", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_falling_diagonal_victory(self): + board = [ + "XOO", + " X ", + " X", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_rising_diagonal_victory(self): + board = [ + "O X", + "OX ", + "X ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_falling_diagonal_victory(self): + board = [ + "OXX", + "OOX", + "X O", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_rising_diagonal_victory(self): + board = [ + " O", + " OX", + "OXX", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_a_row_and_a_column_victory(self): + board = [ + "XXX", + "XOO", + "XOO", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_two_diagonal_victories(self): + board = [ + "XOX", + "OXO", + "XOX", + ] + self.assertEqual(gamestate(board), "win") + + def test_draw(self): + board = [ + "XOX", + "XXO", + "OXO", + ] + self.assertEqual(gamestate(board), "draw") + + def test_another_draw(self): + board = [ + "XXO", + "OXX", + "XOO", + ] + self.assertEqual(gamestate(board), "draw") + + def test_ongoing_game_one_move_in(self): + board = [ + " ", + "X ", + " ", + ] + self.assertEqual(gamestate(board), "ongoing") + + def test_ongoing_game_two_moves_in(self): + board = [ + "O ", + " X ", + " ", + ] + self.assertEqual(gamestate(board), "ongoing") + + def test_ongoing_game_five_moves_in(self): + board = [ + "X ", + " XO", + "OX ", + ] + self.assertEqual(gamestate(board), "ongoing") + + def test_invalid_board_x_went_twice(self): + board = [ + "XX ", + " ", + " ", + ] + with self.assertRaises(ValueError) as err: + gamestate(board) + self.assertEqual(type(err.exception), ValueError) + self.assertEqual(err.exception.args[0], "Wrong turn order: X went twice") + + def test_invalid_board_o_started(self): + board = [ + "OOX", + " ", + " ", + ] + with self.assertRaises(ValueError) as err: + gamestate(board) + self.assertEqual(type(err.exception), ValueError) + self.assertEqual(err.exception.args[0], "Wrong turn order: O started") + + def test_invalid_board_x_won_and_o_kept_playing(self): + board = [ + "XXX", + "OOO", + " ", + ] + with self.assertRaises(ValueError) as err: + gamestate(board) + self.assertEqual(type(err.exception), ValueError) + self.assertEqual( + err.exception.args[0], + "Impossible board: game should have ended after the game was won", + ) + + def test_invalid_board_players_kept_playing_after_a_win(self): + board = [ + "XXX", + "OOO", + "XOX", + ] + with self.assertRaises(ValueError) as err: + gamestate(board) + self.assertEqual(type(err.exception), ValueError) + self.assertEqual( + err.exception.args[0], + "Impossible board: game should have ended after the game was won", + ) From eb4b767878ce8851bd08844caf401b09a09defa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20B=20Nagy?= <20251272+BNAndras@users.noreply.github.com> Date: Fri, 30 Jan 2026 11:21:59 -0800 Subject: [PATCH 38/40] Add `relative-distance` (#4079) * Add `relative-distance` * Apply code review --- config.json | 11 + .../.docs/instructions.append.md | 28 ++ .../relative-distance/.docs/instructions.md | 39 +++ .../relative-distance/.docs/introduction.md | 12 + .../.meta/additional_tests.json | 41 +++ .../relative-distance/.meta/config.json | 19 ++ .../relative-distance/.meta/example.py | 36 +++ .../relative-distance/.meta/template.j2 | 44 ++++ .../relative-distance/.meta/tests.toml | 33 +++ .../relative-distance/relative_distance.py | 6 + .../relative_distance_test.py | 246 ++++++++++++++++++ 11 files changed, 515 insertions(+) create mode 100644 exercises/practice/relative-distance/.docs/instructions.append.md create mode 100644 exercises/practice/relative-distance/.docs/instructions.md create mode 100644 exercises/practice/relative-distance/.docs/introduction.md create mode 100644 exercises/practice/relative-distance/.meta/additional_tests.json create mode 100644 exercises/practice/relative-distance/.meta/config.json create mode 100644 exercises/practice/relative-distance/.meta/example.py create mode 100644 exercises/practice/relative-distance/.meta/template.j2 create mode 100644 exercises/practice/relative-distance/.meta/tests.toml create mode 100644 exercises/practice/relative-distance/relative_distance.py create mode 100644 exercises/practice/relative-distance/relative_distance_test.py diff --git a/config.json b/config.json index 13345d628b1..b9601afc8be 100644 --- a/config.json +++ b/config.json @@ -1821,6 +1821,17 @@ ], "difficulty": 5 }, + { + "slug": "relative-distance", + "name": "Relative Distance", + "uuid": "d590865c-ef30-424a-8cfb-7f31f04dee1b", + "practices": [], + "prerequisites": [ + "lists", + "dicts" + ], + "difficulty": 5 + }, { "slug": "dot-dsl", "name": "DOT DSL", diff --git a/exercises/practice/relative-distance/.docs/instructions.append.md b/exercises/practice/relative-distance/.docs/instructions.append.md new file mode 100644 index 00000000000..241ff7d310e --- /dev/null +++ b/exercises/practice/relative-distance/.docs/instructions.append.md @@ -0,0 +1,28 @@ +# Instructions append + +## Class-based solution + +The tests for this exercise expect your solution to be implemented as a `RelativeDistance` class in Python. +Your `RelativeDistance` class should be initialized using `family_tree`, a dictionary where the keys are individuals and the values are lists of that individual's children. +You will also need to implement a `degree_of_separation` method which will return the degree of separation between `person_a` and `person_b` who are individuals in the family tree. + +If you are unfamiliar with classes in Python, here is a brief overview of how to implement the `RelativeDistance` class: + +A class is a blueprint for creating objects, bundling attributes (data) and methods (functionality) together. +In this exercise, you are given stubbed implementations for the `__init__` special method used to create an instance of the `RelativeDistance` class as well as the `degree_of_separation` method. +To access the `family_tree` data from within the `degree_of_separation` method, you will need to first assign it within the `__init__` method to an appropriate attribute on `self`, which represents the current instance of the `RelativeDistance` class. +Then you can add your logic to the `degree_of_separation` method to calculate the degree of separation between `person_a` and `person_b`. + +## Exception messages + +Sometimes it is necessary to [raise an exception](https://docs.python.org/3/tutorial/errors.html#raising-exceptions). +When you do this, you should always include a **meaningful error message** to indicate what the source of the error is. +This makes your code more readable and helps significantly with debugging. +For situations where you know that the error source will be a certain type, you can choose to raise one of the [built in error types](https://docs.python.org/3/library/exceptions.html#base-classes), but should still include a meaningful message. + +This particular exercise requires that you use the [raise statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement) to "throw" multiple `ValueError`s. +In the first scenario, you will need to raise a `ValueError` when either one or both of the people passed to the `RelativeDistance.degree_of_separation` method are not present in the family tree. +If both people are present in the family tree, you will need to raise a `ValueError` when there is no valid connection between them as defined by the rules. +The tests will only pass if you both `raise` the expected `exception` type and include the expected message with it. + +Please check the tests and their expected results carefully. diff --git a/exercises/practice/relative-distance/.docs/instructions.md b/exercises/practice/relative-distance/.docs/instructions.md new file mode 100644 index 00000000000..9046aee7c8f --- /dev/null +++ b/exercises/practice/relative-distance/.docs/instructions.md @@ -0,0 +1,39 @@ +# Instructions + +Your task is to determine the degree of separation between two individuals in a family tree. +This is similar to the pop culture idea that every Hollywood actor is [within six degrees of Kevin Bacon][six-bacons]. + +- You will be given an input, with all parent names and their children. +- Each name is unique, a child _can_ have one or two parents. +- The degree of separation is defined as the shortest number of connections from one person to another. +- If two individuals are not connected, return a value that represents "no known relationship." + Please see the test cases for the actual implementation. + +## Example + +Given the following family tree: + +```text + ┌──────────┐ ┌──────────┐ ┌───────────┐ + │ Helena │ │ Erdős ├─────┤ Shusaku │ + └───┬───┬──┘ └─────┬────┘ └────┬──────┘ + ┌───┘ └───────┐ └───────┬───────┘ +┌─────┴────┐ ┌────┴───┐ ┌─────┴────┐ +│ Isla ├─────┤ Tariq │ │ Kevin │ +└────┬─────┘ └────┬───┘ └──────────┘ + │ │ +┌────┴────┐ ┌────┴───┐ +│ Uma │ │ Morphy │ +└─────────┘ └────────┘ +``` + +The degree of separation between Tariq and Uma is 2 (Tariq → Isla → Uma). +There's no known relationship between Isla and Kevin, as there is no connection in the given data. +The degree of separation between Uma and Isla is 1. + +~~~~exercism/note +Isla and Tariq are siblings and have a separation of 1. +Similarly, this implementation would report a separation of 2 from you to your father's brother. +~~~~ + +[six-bacons]: https://en.m.wikipedia.org/wiki/Six_Degrees_of_Kevin_Bacon diff --git a/exercises/practice/relative-distance/.docs/introduction.md b/exercises/practice/relative-distance/.docs/introduction.md new file mode 100644 index 00000000000..34073b40ac6 --- /dev/null +++ b/exercises/practice/relative-distance/.docs/introduction.md @@ -0,0 +1,12 @@ +# Introduction + +You've been hired to develop **Noble Knots**, the hottest new dating app for nobility! +With centuries of royal intermarriage, things have gotten… _complicated_. +To avoid any _oops-we're-twins_ situations, your job is to build a system that checks how closely two people are related. + +Noble Knots is inspired by Iceland's "[Islendinga-App][islendiga-app]," which is backed up by a database that traces all known family connections between Icelanders from the time of the settlement of Iceland. +Your algorithm will determine the **degree of separation** between two individuals in the royal family tree. + +Will your app help crown a perfect match? + +[islendiga-app]: https://web.archive.org/web/20250816223614/http://www.islendingaapp.is/information-in-english/ diff --git a/exercises/practice/relative-distance/.meta/additional_tests.json b/exercises/practice/relative-distance/.meta/additional_tests.json new file mode 100644 index 00000000000..0b42263ce85 --- /dev/null +++ b/exercises/practice/relative-distance/.meta/additional_tests.json @@ -0,0 +1,41 @@ +{ + "cases": [ + { + "description": "person A not in tree", + "property": "degreeOfSeparation", + "input": { + "family_tree": { + "Priya": ["Rami"] + }, + "person_a": "Kaito", + "person_b": "Priya" + }, + "expected": {"error": "Person A not in family tree."} + }, + { + "description": "person B not in tree", + "property": "degreeOfSeparation", + "input": { + "family_tree": { + "Priya": ["Rami"] + }, + "person_a": "Priya", + "person_b": "Kaito" + }, + "expected": {"error": "Person B not in family tree."} + }, + { + "description": "no connection between individuals", + "property": "degreeOfSeparation", + "input": { + "family_tree": { + "Priya": ["Rami"], + "Kaito": ["Elif"] + }, + "person_a": "Priya", + "person_b": "Kaito" + }, + "expected": {"error": "No connection between person A and person B."} + } + ] +} diff --git a/exercises/practice/relative-distance/.meta/config.json b/exercises/practice/relative-distance/.meta/config.json new file mode 100644 index 00000000000..97abf1e0337 --- /dev/null +++ b/exercises/practice/relative-distance/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "BNAndras" + ], + "files": { + "solution": [ + "relative_distance.py" + ], + "test": [ + "relative_distance_test.py" + ], + "example": [ + ".meta/example.py" + ] + }, + "blurb": "Given a family tree, calculate the degree of separation.", + "source": "vaeng", + "source_url": "https://github.com/exercism/problem-specifications/pull/2537" +} diff --git a/exercises/practice/relative-distance/.meta/example.py b/exercises/practice/relative-distance/.meta/example.py new file mode 100644 index 00000000000..2de98263b71 --- /dev/null +++ b/exercises/practice/relative-distance/.meta/example.py @@ -0,0 +1,36 @@ +import collections +import itertools + +class RelativeDistance: + def __init__(self, family_tree): + self.neighbors = collections.defaultdict(set) + for parent, children in family_tree.items(): + for child in children: + self.neighbors[parent].add(child) + self.neighbors[child].add(parent) + + for child1, child2 in itertools.combinations(children, 2): + self.neighbors[child1].add(child2) + self.neighbors[child2].add(child1) + + def degree_of_separation(self, person_a, person_b): + if person_a not in self.neighbors: + raise ValueError("Person A not in family tree.") + if person_b not in self.neighbors: + raise ValueError("Person B not in family tree.") + + queue = collections.deque([(person_a, 0)]) + visited = {person_a} + + while queue: + current, degree = queue.popleft() + + if current == person_b: + return degree + + for neighbor in self.neighbors.get(current, []): + if neighbor not in visited: + visited.add(neighbor) + queue.append((neighbor, degree + 1)) + + raise ValueError("No connection between person A and person B.") diff --git a/exercises/practice/relative-distance/.meta/template.j2 b/exercises/practice/relative-distance/.meta/template.j2 new file mode 100644 index 00000000000..99d8ed42e7e --- /dev/null +++ b/exercises/practice/relative-distance/.meta/template.j2 @@ -0,0 +1,44 @@ +{%- import "generator_macros.j2" as macros with context -%} +{{ macros.canonical_ref() }} + +{{ macros.header(imports=['RelativeDistance']) }} + +class {{ exercise | camel_case }}Test(unittest.TestCase): + {% for case in cases -%} + def test_{{ case["description"] | to_snake }}(self): + family_tree = { + {%- for person, relatives in case["input"]["familyTree"].items() %} + "{{ person }}": {{ relatives }}, + {%- endfor %} + } + {%- if case["expected"] is none %} + with self.assertRaises(ValueError): + RelativeDistance(family_tree).degree_of_separation("{{ case["input"]["personA"] }}", "{{ case["input"]["personB"] }}") + {%- else %} + self.assertEqual( + RelativeDistance(family_tree).degree_of_separation("{{ case["input"]["personA"] }}", "{{ case["input"]["personB"] }}"), + {{ case["expected"] }} + ) + {%- endif %} + {% endfor -%} + + # Additional track-specific tests + {% for case in additional_cases -%} + def test_{{ case["description"] | to_snake }}(self): + family_tree = { + {%- for person, relatives in case["input"]["family_tree"].items() %} + "{{ person }}": {{ relatives }}, + {%- endfor %} + } + {%- if case["expected"]["error"] %} + with self.assertRaises(ValueError) as err: + RelativeDistance(family_tree).degree_of_separation("{{ case["input"]["person_a"] }}", "{{ case["input"]["person_b"] }}") + self.assertEqual(type(err.exception), ValueError) + self.assertEqual(err.exception.args[0], "{{ case["expected"]["error"] }}") + {%- else %} + self.assertEqual( + RelativeDistance(family_tree).degree_of_separation("{{ case["input"]["person_a"] }}", "{{ case["input"]["person_b"] }}"), + {{ case["expected"] }} + ) + {%- endif %} + {% endfor -%} diff --git a/exercises/practice/relative-distance/.meta/tests.toml b/exercises/practice/relative-distance/.meta/tests.toml new file mode 100644 index 00000000000..25560343fa2 --- /dev/null +++ b/exercises/practice/relative-distance/.meta/tests.toml @@ -0,0 +1,33 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[4a1ded74-5d32-47fb-8ae5-321f51d06b5b] +description = "Direct parent-child relation" + +[30d17269-83e9-4f82-a0d7-8ef9656d8dce] +description = "Sibling relationship" + +[8dffa27d-a8ab-496d-80b3-2f21c77648b5] +description = "Two degrees of separation, grandchild" + +[34e56ec1-d528-4a42-908e-020a4606ee60] +description = "Unrelated individuals" +comment = "skipped in favor of test-specific tests" +include = false + +[93ffe989-bad2-48c4-878f-3acb1ce2611b] +description = "Complex graph, cousins" + +[2cc2e76b-013a-433c-9486-1dbe29bf06e5] +description = "Complex graph, no shortcut, far removed nephew" + +[46c9fbcb-e464-455f-a718-049ea3c7400a] +description = "Complex graph, some shortcuts, cross-down and cross-up, cousins several times removed, with unrelated family tree" diff --git a/exercises/practice/relative-distance/relative_distance.py b/exercises/practice/relative-distance/relative_distance.py new file mode 100644 index 00000000000..21f09f2f74f --- /dev/null +++ b/exercises/practice/relative-distance/relative_distance.py @@ -0,0 +1,6 @@ +class RelativeDistance: + def __init__(self, family_tree): + pass + + def degree_of_separation(self, person_a, person_b): + pass diff --git a/exercises/practice/relative-distance/relative_distance_test.py b/exercises/practice/relative-distance/relative_distance_test.py new file mode 100644 index 00000000000..bedc24f873f --- /dev/null +++ b/exercises/practice/relative-distance/relative_distance_test.py @@ -0,0 +1,246 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/relative-distance/canonical-data.json +# File last updated on 2026-01-30 + +import unittest + +from relative_distance import ( + RelativeDistance, +) + + +class RelativeDistanceTest(unittest.TestCase): + def test_direct_parent_child_relation(self): + family_tree = { + "Vera": ["Tomoko"], + "Tomoko": ["Aditi"], + } + self.assertEqual( + RelativeDistance(family_tree).degree_of_separation("Vera", "Tomoko"), 1 + ) + + def test_sibling_relationship(self): + family_tree = { + "Dalia": ["Olga", "Yassin"], + } + self.assertEqual( + RelativeDistance(family_tree).degree_of_separation("Olga", "Yassin"), 1 + ) + + def test_two_degrees_of_separation_grandchild(self): + family_tree = { + "Khadija": ["Mateo"], + "Mateo": ["Rami"], + } + self.assertEqual( + RelativeDistance(family_tree).degree_of_separation("Khadija", "Rami"), 2 + ) + + def test_complex_graph_cousins(self): + family_tree = { + "Aiko": ["Bao", "Carlos"], + "Bao": ["Dalia", "Elias"], + "Carlos": ["Fatima", "Gustavo"], + "Dalia": ["Hassan", "Isla"], + "Elias": ["Javier"], + "Fatima": ["Khadija", "Liam"], + "Gustavo": ["Mina"], + "Hassan": ["Noah", "Olga"], + "Isla": ["Pedro"], + "Javier": ["Quynh", "Ravi"], + "Khadija": ["Sofia"], + "Liam": ["Tariq", "Uma"], + "Mina": ["Viktor", "Wang"], + "Noah": ["Xiomara"], + "Olga": ["Yuki"], + "Pedro": ["Zane", "Aditi"], + "Quynh": ["Boris"], + "Ravi": ["Celine"], + "Sofia": ["Diego", "Elif"], + "Tariq": ["Farah"], + "Uma": ["Giorgio"], + "Viktor": ["Hana", "Ian"], + "Wang": ["Jing"], + "Xiomara": ["Kaito"], + "Yuki": ["Leila"], + "Zane": ["Mateo"], + "Aditi": ["Nia"], + "Boris": ["Oscar"], + "Celine": ["Priya"], + "Diego": ["Qi"], + "Elif": ["Rami"], + "Farah": ["Sven"], + "Giorgio": ["Tomoko"], + "Hana": ["Umar"], + "Ian": ["Vera"], + "Jing": ["Wyatt"], + "Kaito": ["Xia"], + "Leila": ["Yassin"], + "Mateo": ["Zara"], + "Nia": ["Antonio"], + "Oscar": ["Bianca"], + "Priya": ["Cai"], + "Qi": ["Dimitri"], + "Rami": ["Ewa"], + "Sven": ["Fabio"], + "Tomoko": ["Gabriela"], + "Umar": ["Helena"], + "Vera": ["Igor"], + "Wyatt": ["Jun"], + "Xia": ["Kim"], + "Yassin": ["Lucia"], + "Zara": ["Mohammed"], + } + self.assertEqual( + RelativeDistance(family_tree).degree_of_separation("Dimitri", "Fabio"), 9 + ) + + def test_complex_graph_no_shortcut_far_removed_nephew(self): + family_tree = { + "Aiko": ["Bao", "Carlos"], + "Bao": ["Dalia", "Elias"], + "Carlos": ["Fatima", "Gustavo"], + "Dalia": ["Hassan", "Isla"], + "Elias": ["Javier"], + "Fatima": ["Khadija", "Liam"], + "Gustavo": ["Mina"], + "Hassan": ["Noah", "Olga"], + "Isla": ["Pedro"], + "Javier": ["Quynh", "Ravi"], + "Khadija": ["Sofia"], + "Liam": ["Tariq", "Uma"], + "Mina": ["Viktor", "Wang"], + "Noah": ["Xiomara"], + "Olga": ["Yuki"], + "Pedro": ["Zane", "Aditi"], + "Quynh": ["Boris"], + "Ravi": ["Celine"], + "Sofia": ["Diego", "Elif"], + "Tariq": ["Farah"], + "Uma": ["Giorgio"], + "Viktor": ["Hana", "Ian"], + "Wang": ["Jing"], + "Xiomara": ["Kaito"], + "Yuki": ["Leila"], + "Zane": ["Mateo"], + "Aditi": ["Nia"], + "Boris": ["Oscar"], + "Celine": ["Priya"], + "Diego": ["Qi"], + "Elif": ["Rami"], + "Farah": ["Sven"], + "Giorgio": ["Tomoko"], + "Hana": ["Umar"], + "Ian": ["Vera"], + "Jing": ["Wyatt"], + "Kaito": ["Xia"], + "Leila": ["Yassin"], + "Mateo": ["Zara"], + "Nia": ["Antonio"], + "Oscar": ["Bianca"], + "Priya": ["Cai"], + "Qi": ["Dimitri"], + "Rami": ["Ewa"], + "Sven": ["Fabio"], + "Tomoko": ["Gabriela"], + "Umar": ["Helena"], + "Vera": ["Igor"], + "Wyatt": ["Jun"], + "Xia": ["Kim"], + "Yassin": ["Lucia"], + "Zara": ["Mohammed"], + } + self.assertEqual( + RelativeDistance(family_tree).degree_of_separation("Lucia", "Jun"), 14 + ) + + def test_complex_graph_some_shortcuts_cross_down_and_cross_up_cousins_several_times_removed_with_unrelated_family_tree( + self, + ): + family_tree = { + "Aiko": ["Bao", "Carlos"], + "Bao": ["Dalia"], + "Carlos": ["Fatima", "Gustavo"], + "Dalia": ["Hassan", "Isla"], + "Fatima": ["Khadija", "Liam"], + "Gustavo": ["Mina"], + "Hassan": ["Noah", "Olga"], + "Isla": ["Pedro"], + "Javier": ["Quynh", "Ravi"], + "Khadija": ["Sofia"], + "Liam": ["Tariq", "Uma"], + "Mina": ["Viktor", "Wang"], + "Noah": ["Xiomara"], + "Olga": ["Yuki"], + "Pedro": ["Zane", "Aditi"], + "Quynh": ["Boris"], + "Ravi": ["Celine"], + "Sofia": ["Diego", "Elif"], + "Tariq": ["Farah"], + "Uma": ["Giorgio"], + "Viktor": ["Hana", "Ian"], + "Wang": ["Jing"], + "Xiomara": ["Kaito"], + "Yuki": ["Leila"], + "Zane": ["Mateo"], + "Aditi": ["Nia"], + "Boris": ["Oscar"], + "Celine": ["Priya"], + "Diego": ["Qi"], + "Elif": ["Rami"], + "Farah": ["Sven"], + "Giorgio": ["Tomoko"], + "Hana": ["Umar"], + "Ian": ["Vera"], + "Jing": ["Wyatt"], + "Kaito": ["Xia"], + "Leila": ["Yassin"], + "Mateo": ["Zara"], + "Nia": ["Antonio"], + "Oscar": ["Bianca"], + "Priya": ["Cai"], + "Qi": ["Dimitri"], + "Rami": ["Ewa"], + "Sven": ["Fabio"], + "Tomoko": ["Gabriela"], + "Umar": ["Helena"], + "Vera": ["Igor"], + "Wyatt": ["Jun"], + "Xia": ["Kim"], + "Yassin": ["Lucia"], + "Zara": ["Mohammed"], + } + self.assertEqual( + RelativeDistance(family_tree).degree_of_separation("Wyatt", "Xia"), 12 + ) + + # Additional track-specific tests + def test_person_a_not_in_tree(self): + family_tree = { + "Priya": ["Rami"], + } + with self.assertRaises(ValueError) as err: + RelativeDistance(family_tree).degree_of_separation("Kaito", "Priya") + self.assertEqual(type(err.exception), ValueError) + self.assertEqual(err.exception.args[0], "Person A not in family tree.") + + def test_person_b_not_in_tree(self): + family_tree = { + "Priya": ["Rami"], + } + with self.assertRaises(ValueError) as err: + RelativeDistance(family_tree).degree_of_separation("Priya", "Kaito") + self.assertEqual(type(err.exception), ValueError) + self.assertEqual(err.exception.args[0], "Person B not in family tree.") + + def test_no_connection_between_individuals(self): + family_tree = { + "Priya": ["Rami"], + "Kaito": ["Elif"], + } + with self.assertRaises(ValueError) as err: + RelativeDistance(family_tree).degree_of_separation("Priya", "Kaito") + self.assertEqual(type(err.exception), ValueError) + self.assertEqual( + err.exception.args[0], "No connection between person A and person B." + ) From 800b3e83075e9220886f0de20ae090c3070d65d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 17:08:29 -0800 Subject: [PATCH 39/40] Bump actions/checkout from 6.0.1 to 6.0.2 (#4082) Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.1 to 6.0.2. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/8e8c483db84b4bee98b60c0593521ed34d9990e8...de0fac2e4500dabe0009e67214ff5f5447ce83dd) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-workflow.yml | 4 ++-- .github/workflows/issue-commenter.yml | 2 +- .github/workflows/test-runner.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 33f47f541e4..b70dd7b9d8d 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -14,7 +14,7 @@ jobs: housekeeping: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Set up Python uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 @@ -55,7 +55,7 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9, 3.10.6, 3.11.2] steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 with: diff --git a/.github/workflows/issue-commenter.yml b/.github/workflows/issue-commenter.yml index 9e3b678f66e..ca615dd43a0 100644 --- a/.github/workflows/issue-commenter.yml +++ b/.github/workflows/issue-commenter.yml @@ -9,7 +9,7 @@ jobs: name: Comments for every NEW issue. steps: - name: Checkout - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Read issue-comment.md id: issue-comment diff --git a/.github/workflows/test-runner.yml b/.github/workflows/test-runner.yml index f32c41b958a..65a3d75c562 100644 --- a/.github/workflows/test-runner.yml +++ b/.github/workflows/test-runner.yml @@ -10,6 +10,6 @@ jobs: test-runner: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Run test-runner run: docker compose run test-runner From bc67d58aa0ed48b1dc41fd2da62ee83325123d2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 17:09:08 -0800 Subject: [PATCH 40/40] Bump actions/setup-python from 6.1.0 to 6.2.0 (#4081) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.1.0 to 6.2.0. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/83679a892e2d95755f2dac6acb0bfd1e9ac5d548...a309ff8b426b58ec0e2a45f0f869d46889d02405) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: 6.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-workflow.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index b70dd7b9d8d..afc5323f3ec 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Set up Python - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 with: python-version: 3.11.2 @@ -57,7 +57,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 with: python-version: ${{ matrix.python-version }}