diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index c895b2ae2a2..33f47f541e4 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -12,12 +12,12 @@ on: jobs: housekeeping: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - name: Set up Python - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 with: python-version: 3.11.2 @@ -55,9 +55,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9, 3.10.6, 3.11.2] steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/issue-commenter.yml b/.github/workflows/issue-commenter.yml index d4434c808b0..9e3b678f66e 100644 --- a/.github/workflows/issue-commenter.yml +++ b/.github/workflows/issue-commenter.yml @@ -5,11 +5,11 @@ 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 - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - name: Read issue-comment.md id: issue-comment 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 f526390311f..f40e8010052 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -6,9 +6,9 @@ on: jobs: stale: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e + - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 21 diff --git a/.github/workflows/test-runner.yml b/.github/workflows/test-runner.yml index 9faae798e25..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@d632683dd7b4114ad314bca15554477dd762a938 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - name: Run test-runner run: docker compose run test-runner diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bc278b027e5..d9c30d85e0a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,31 +4,35 @@

Contributing

                          [![Discourse topics](https://img.shields.io/discourse/topics?color=8A08E6&label=Connect%20&labelColor=FFDF58&logo=Discourse&logoColor=8A08E6&server=https%3A%2F%2Fforum.exercism.org&style=social)](https://forum.exercism.org) -  [![Exercism_II](https://img.shields.io/badge/Exercism--Built-9101FF?logo=python&logoColor=FFDF58&labelColor=3D7AAB&label=Python%203.10%20Powered)](https://exercism.org) +  [![Exercism_II](https://img.shields.io/badge/Exercism--Built-9101FF?logo=python&logoColor=FFDF58&labelColor=3D7AAB&label=Python%203.11%20Powered)](https://exercism.org)   [![Exercism_III](https://img.shields.io/badge/PAUSED-C73D4E?labelColor=3D454D&label=Contributions)](https://exercism.org/blog/freeing-our-maintainers)   [![Build Status](https://github.com/exercism/python/workflows/Exercises%20check/badge.svg)](https://github.com/exercism/python/actions?query=workflow%3A%22Exercises+check%22) -
- -Hi.  πŸ‘‹πŸ½  πŸ‘‹  **We are happy you are here.**  πŸŽ‰ πŸŒŸ - - -
- +> [!IMPORTANT] +>

We are not accepting community contributions at this time.

+>
+> +> +> +> We love our community. We're grateful you are interested in improving the Python track. +> But our maintainers are **not accepting community contributions at this time.** +> If you would like to discuss possible future changes, please open a [thread on the forum](https://forum.exercism.org/). +> +> This [community blog post](https://exercism.org/blog/freeing-our-maintainers) contains more details. +> +> +>
-We πŸ’› πŸ’™   our community. -**`But our maintainers are not accepting community contributions at this time.`** -Please read this [community blog post](https://exercism.org/blog/freeing-our-maintainers) for details. - -
+Hi.  πŸ‘‹πŸ½  πŸ‘‹  **We are happy you are here.**  πŸŽ‰ πŸŒŸ + **`exercism/Python`** is one of many programming language tracks on [exercism(dot)org][exercism-website]. This repo holds all the instructions, tests, code, & support files for Python _exercises_ currently under development or implemented & available for students. -🌟   Track exercises support Python `3.7` - `3.11.2`. +🌟   Track exercises support Python `3.7` - `3.11.5`. Exceptions to this support are noted where they occur. 🌟   Track tooling (_test-runner, representer, analyzer, and Continuous Integration_) runs on Python `3.11.2`. @@ -43,20 +47,25 @@ Practice exercises are open-ended, and can be used to practice concepts learned, It is not uncommon to discover typos, confusing directions, or incorrect implementations of certain tests or code examples. Or you might have a great suggestion for a hint to aid students ( πŸ’™  ), see optimizations for exemplar or test code, find missing test cases to add, or want to correct factual and/or logical errors. Or maybe you have a great idea πŸ’‘ for an exercise or feature ( πŸ’™ ). _Our track is always a work in progress!_ 🌟🌟 -While contributions are paused, we ask that you [`open a thread in our community forum`](https://forum.exercism.org) to let us know what you have found/suggest. +While contributions are paused, we ask that you [**open a thread in our community forum**](https://forum.exercism.org) to let us know what you have found/suggest.
## 🚧 **Did you write a patch that fixes a bug?** -**`Our maintainers are not accepting community contributions at this time.`** -Please read this [community blog post](https://exercism.org/blog/freeing-our-maintainers) for details. +Our maintainers are not accepting community contributions at this time. +
+Until the pause on contributions ends, all PRs from the larger community will be **automatically closed** with a note. +We ask that you [**open a thread in our community forum**](https://forum.exercism.org) to discuss any potential changes. Changes may or may not be approved, depending on the forum discussion. -Once the pause ends, we will **happily** consider your PR. -Until that time, all PRs from the larger community will be **automatically closed** with a note. +Please read this [community blog post](https://exercism.org/blog/freeing-our-maintainers) for additional details. +
-We're leaving the general contributing docs below for our long-term collaborators and maintainers. +We're leaving the track contributing docs below for our long-term collaborators and maintainers. +
+
+ Python Track Contributing Docs

In General

@@ -369,45 +378,32 @@ configlet generate --spec-path path/to/problem/specifications -- configlet generate --spec-path path/to/problem/specifications ``` +
+
[.flake8]: https://github.com/exercism/python/blob/main/.flake8 [.style.yapf]: https://github.com/exercism/python/blob/main/.style.yapf [american-english]: https://github.com/exercism/docs/blob/main/building/markdown/style-guide.md -[being-a-good-community-member]: https://github.com/exercism/docs/tree/main/community/good-member [card-games-testfile]: https://github.com/exercism/python/blob/main/exercises/concept/card-games/lists_test.py [cater-waiter]: https://github.com/exercism/python/tree/main/exercises/concept/cater-waiter [concept-exercise-anatomy]: https://github.com/exercism/docs/blob/main/building/tracks/concept-exercises.md -[concept-exercises]: https://github.com/exercism/docs/blob/main/building/tracks/concept-exercises.md [config-json]: https://github.com/exercism/javascript/blob/main/config.json -[config-json]: https://github.com/exercism/python/blob/main/config.json -[configlet-general]: https://github.com/exercism/configlet [configlet-lint]: https://github.com/exercism/configlet#configlet-lint [configlet]: https://github.com/exercism/docs/blob/main/building/configlet/generating-documents.md [distinguishing-test-iterations]: https://docs.python.org/3/library/unittest.html#distinguishing-test-iterations-using-subtests [enumerate]: https://docs.python.org/3/library/functions.html#enumerate [eol]: https://en.wikipedia.org/wiki/Newline [exercise-config-json]: https://github.com/exercism/docs/blob/main/building/tracks/concept-exercises.md#full-example -[exercise-presentation]: https://github.com/exercism/docs/blob/main/building/tracks/presentation.md -[exercism-admins]: https://github.com/exercism/docs/blob/main/community/administrators.md -[exercism-code-of-conduct]: https://exercism.org/docs/using/legal/code-of-conduct -[exercism-concepts]: https://github.com/exercism/docs/blob/main/building/tracks/concepts.md -[exercism-contributors]: https://github.com/exercism/docs/blob/main/community/contributors.md [exercism-internal-linking]: https://github.com/exercism/docs/blob/main/building/markdown/internal-linking.md [exercism-markdown-specification]: https://github.com/exercism/docs/blob/main/building/markdown/markdown.md [exercism-markdown-widgets]: https://github.com/exercism/docs/blob/main/building/markdown/widgets.md -[exercism-mentors]: https://github.com/exercism/docs/tree/main/mentoring -[exercism-tasks]: https://exercism.org/docs/building/product/tasks -[exercism-track-maintainers]: https://github.com/exercism/docs/blob/main/community/maintainers.md -[exercism-track-structure]: https://github.com/exercism/docs/tree/main/building/tracks [exercism-website]: https://exercism.org/ -[exercism-writing-style]: https://github.com/exercism/docs/blob/main/building/markdown/style-guide.md [flake8-noqa]: https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html#in-line-ignoring-errors [flake8]: http://flake8.pycqa.org/ [google-coding-style]: https://google.github.io/styleguide/pyguide.html [guidos-gorgeous-lasagna-testfile]: https://github.com/exercism/python/blob/main/exercises/concept/guidos-gorgeous-lasagna/lasagna_test.py -[help-wanted]: https://github.com/exercism/python/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22 [implicit-line-joining]: https://google.github.io/styleguide/pyguide.html#32-line-length [markdown-language]: https://guides.github.com/pdfs/markdown-cheatsheet-online.pdf [open-an-issue]: https://github.com/exercism/python/issues/new/choose @@ -429,5 +425,4 @@ configlet generate --spec-path path/to/problem/specifications [the-words-that-we-use]: https://github.com/exercism/docs/blob/main/community/good-member/words.md [unittest]: https://docs.python.org/3/library/unittest.html#unittest.TestCase [version-tagged-language-features]: https://docs.python.org/3/library/stdtypes.html#dict.popitem -[website-contributing-section]: https://exercism.org/docs/building [yapf]: https://github.com/google/yapf diff --git a/README.md b/README.md index d8ec6644585..f3d083aab42 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,27 @@
-Hi.  πŸ‘‹πŸ½  πŸ‘‹  **We are happy you are here.**  πŸŽ‰ πŸŒŸ +> [!IMPORTANT] +>

We are not accepting community contributions at this time.

+> +> +> +> +> We love our community. We're grateful you are interested in improving the Python track. +> But our maintainers are **not accepting community contributions at this time.** +> If you would like to suggest a change / discuss an issue, please open a [thread on the forum](https://forum.exercism.org/). +> +> This [community blog post](https://exercism.org/blog/freeing-our-maintainers) contains more details. +> +> +>

+Hi.  πŸ‘‹πŸ½  πŸ‘‹  **We are happy you are here.**  πŸŽ‰ πŸŒŸ + +

+ **`exercism/Python`** is one of many programming language tracks on [exercism(dot)org][exercism-website]. This repo holds all the instructions, tests, code, & support files for Python _exercises_ currently under development or implemented & available for students. @@ -43,15 +60,15 @@ It might also be helpful to look at [Being a Good Community Member][being-a-good
-We πŸ’› πŸ’™   our community. -**`But our maintainers are not accepting community contributions at this time.`** +We πŸ’› πŸ’™ our community. +**But our maintainers are not accepting community contributions at this time.** Please read this [community blog post][freeing-maintainers] for details.
Here to suggest a new feature or new exercise?? **Hooray!**  πŸŽ‰   -We'd love if you did that via our [Exercism Community Forum](https://forum.exercism.org/). +We'd love if you did that via our [Community Forum](https://forum.exercism.org/). Please read [Suggesting Exercise Improvements][suggesting-improvements] & [Chesterton's Fence][chestertons-fence]. _Thoughtful suggestions will likely result in faster & more enthusiastic responses from volunteers._ @@ -67,7 +84,7 @@ _Thoughtful suggestions will likely result in faster & more enthusiastic respons ## Python Software and Documentation -**Copyright Β© 2001-2023 Python Software Foundation. All rights reserved.** +**Copyright Β© 2001-2025 Python Software Foundation. All rights reserved.** Python software and documentation are licensed under the [PSF License Agreement][psf-license]. @@ -99,7 +116,6 @@ This repository uses the [MIT License](/LICENSE). [exercism-writing-style]: https://github.com/exercism/docs/blob/main/building/markdown/style-guide.md [freeing-maintainers]: https://exercism.org/blog/freeing-our-maintainers [practice-exercises]: https://github.com/exercism/docs/blob/main/building/tracks/practice-exercises.md -[prs]: https://github.com/exercism/docs/blob/main/community/good-member/pull-requests.md [psf-license]: https://docs.python.org/3/license.html#psf-license [python-syllabus]: https://exercism.org/tracks/python/concepts [suggesting-improvements]: https://github.com/exercism/docs/blob/main/community/good-member/suggesting-exercise-improvements.md diff --git a/concepts/bitwise-operators/about.md b/concepts/bitwise-operators/about.md index a4ddb509c11..a68e5378f12 100644 --- a/concepts/bitwise-operators/about.md +++ b/concepts/bitwise-operators/about.md @@ -117,7 +117,7 @@ This means that all bits are inverted and a number is _**interpreted as negative Positive numbers have an MSB of `0`. This representation has the advantage of only having one version of zero, so that the programmer doesn't have to manage `-0` and `+0`. -This way of representing negative and positive numbers adds a complication for Python: there are no finite-integer concepts like `int32` or `int64` internally in the core langauge. +This way of representing negative and positive numbers adds a complication for Python: there are no finite-integer concepts like `int32` or `int64` internally in the core language. In 'modern' Python, `int`s are of unlimited size (_limited only by hardware capacity_), and a negative or bit-inverted number has a (_theoretically_) infinite number of `1`'s to the left, just as a positive number has unlimited `0`'s. This makes it difficult to give a useful example of `bitwise not`: 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/concepts/classes/about.md b/concepts/classes/about.md index f88ce892f3b..11b03643543 100644 --- a/concepts/classes/about.md +++ b/concepts/classes/about.md @@ -118,7 +118,7 @@ class MyClass: def __init__(self, location): # This is an instance or object property, attribute, or variable. - # Note that we are unpacking the tuple argument into two seperate instance variables. + # Note that we are unpacking the tuple argument into two separate instance variables. self.location_x = location[0] self.location_y = location[1] diff --git a/concepts/conditionals/about.md b/concepts/conditionals/about.md index d0b91f26ce6..2060905b335 100644 --- a/concepts/conditionals/about.md +++ b/concepts/conditionals/about.md @@ -141,8 +141,8 @@ def just_the_buzz(number): >>> just_the_buzz(15) 'Buzz!' ->>> just_the_buzz(10) -'10' +>>> just_the_buzz(7) +'7' ``` ## Truthy and Falsy 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/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/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/concepts/unpacking-and-multiple-assignment/about.md b/concepts/unpacking-and-multiple-assignment/about.md index 787e2ef08e8..d4b9168ad13 100644 --- a/concepts/unpacking-and-multiple-assignment/about.md +++ b/concepts/unpacking-and-multiple-assignment/about.md @@ -222,12 +222,20 @@ This will pack all the values into a `list`/`tuple`. >>> combined_fruits ("apple", "banana", "cherry", "orange", "kiwi", "melon", "mango") -# If the * operator is used on the left side of "=" the result is a list +# If the * operator is used on the left side of "=" the result is a list. +# Note the trailing comma. >>> *combined_fruits_too, = *fruits, *more_fruits >>> combined_fruits_too ['apple', 'banana', 'cherry', 'orange', 'kiwi', 'melon', 'mango'] + +# A list literal can be used instead, but might not be as readable. +>>> [*combined_fruits_too] = *fruits, *more_fruits +>>> combined_fruits_too +['apple', 'banana', 'cherry', 'orange', 'kiwi', 'melon', 'mango'] ``` +For more details on the use of `*` and `**`, check out [PEP 3132][pep-3132] and [PEP 448][pep-448]. + ### Packing a dictionary with `**` Packing a dictionary is done by using the `**` operator. @@ -356,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]) @@ -370,6 +378,9 @@ Since `zip()` takes multiple iterables and returns a `list` of `tuples` with the [items]: https://docs.python.org/3/library/stdtypes.html#dict.items [multiple assignment]: https://www.geeksforgeeks.org/assigning-multiple-variables-in-one-line-in-python/ [packing and unpacking]: https://www.geeksforgeeks.org/packing-and-unpacking-arguments-in-python/ +[pep-448]: https://peps.python.org/pep-0448/ +[pep-3132]: https://peps.python.org/pep-3132/ [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/config.json b/config.json index 09bd642c985..ff5fad2c81e 100644 --- a/config.json +++ b/config.json @@ -752,7 +752,7 @@ "slug": "nth-prime", "name": "Nth Prime", "uuid": "a20924d2-fe6d-4714-879f-3239feb9d2f2", - "practices": [], + "practices": ["generators"], "prerequisites": [ "basics", "bools", @@ -1328,17 +1328,19 @@ "difficulty": 4 }, { - "slug": "minesweeper", - "name": "Minesweeper", - "uuid": "7e768b54-4591-4a30-9ddb-66ca13400ca3", - "practices": ["lists"], - "prerequisites": [ - "basics", + "slug": "swift-scheduling", + "name": "Swift Scheduling", + "uuid": "ebddfc37-a3fc-4524-bd62-9c70f979713c", + "practices": [], + "prerequisites": ["basics", "bools", "conditionals", "lists", + "list-methods", "loops", - "numbers" + "numbers", + "strings", + "string-methods" ], "difficulty": 4 }, @@ -1410,6 +1412,22 @@ ], "difficulty": 4 }, + { + "slug": "flower-field", + "name": "Flower Field", + "uuid": "0c2751c1-5d2f-499a-81b8-226e5092ea88", + "practices": ["lists"], + "prerequisites": [ + "conditionals", + "lists", + "list-methods", + "loops", + "numbers", + "strings", + "string-methods" + ], + "difficulty": 4 + }, { "slug": "rail-fence-cipher", "name": "Rail Fence Cipher", @@ -1485,7 +1503,7 @@ "slug": "scale-generator", "name": "Scale Generator", "uuid": "8cd58325-61fc-46fd-85f9-425b4c41f3de", - "practices": ["generators"], + "practices": [], "prerequisites": [ "basics", "bools", @@ -1503,12 +1521,7 @@ "slug": "largest-series-product", "name": "Largest Series Product", "uuid": "21624a3e-6e43-4c0e-94b0-dee5cdaaf2aa", - "practices": [ - "functions", - "higher-order-functions", - "functional-tools", - "anonymous-functions" - ], + "practices": ["generators"], "prerequisites": [ "basics", "conditionals", @@ -2223,6 +2236,15 @@ "prerequisites": [], "difficulty": 4, "status": "deprecated" + }, + { + "slug": "minesweeper", + "name": "Minesweeper", + "uuid": "7e768b54-4591-4a30-9ddb-66ca13400ca3", + "practices": [], + "prerequisites": [], + "difficulty": 4, + "status": "deprecated" } ], "foregone": ["lens-person", "nucleotide-count", "parallel-letter-frequency"] 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. diff --git a/docs/TESTS.md b/docs/TESTS.md index 54a10b5782a..8c01c524816 100644 --- a/docs/TESTS.md +++ b/docs/TESTS.md @@ -28,14 +28,14 @@ Otherwise, the `pytest` installation will be global. ```powershell PS C:\Users\foobar> py -m pip install pytest pytest-cache pytest-subtests pytest-pylint -Successfully installed pytest-7.2.2 ... +Successfully installed pytest-8.3.3 ... ``` #### Linux / MacOS ```bash $ python3 -m pip install pytest pytest-cache pytest-subtests pytest-pylint -Successfully installed pytest-7.2.2 ... +Successfully installed pytest-8.3.3 ... ``` @@ -43,23 +43,36 @@ To check if installation was successful: ```bash $ python3 -m pytest --version -pytest 7.2.2 +pytest 8.3.3 ``` ## Running the tests -To run the tests, go to the folder where the exercise is stored using `cd` in your terminal (_replace `{exercise-folder-location}` below with your path_). +To run the tests, go to the folder where the exercise is stored using `cd` in your terminal (_replace `` below with your path_). ```bash -$ cd {exercise-folder-location} +$ cd ``` +
+ +~~~~exercism/note + `` or most things inside angle brackets denote a **_placeholder value_**. +A normal path or file name should be written _without_ any brackets. + + +For example: `/Users/janedoe/exercism/python/exercises/concept/chaitanas-colossal-coaster` (on *nix systems), `C:\Users\janedoe\exercism\python\exercises\practice\hello-world\` (on Windows), `myFolder` or `my_file.py`. +~~~~ + +
+ + The file you will want to run usually ends in `_test.py`. This file contains the tests for the exercise solution, and are the same tests that run on the website when a solution is uploaded. -Next, run the following command in your terminal, replacing `{exercise_test.py}` with the location/name of the test file: +Next, run the following command in your terminal, replacing `` with the location/name of the test file: ```bash -$ python3 -m pytest -o markers=task {exercise_test.py} +$ python3 -m pytest -o markers=task ==================== 7 passed in 0.08s ==================== ``` @@ -88,15 +101,15 @@ When tests fail, `pytest` prints the text of each failed test, along with the ex Below is an generic example of a failed test: ```bash -$(my_venv) python3 -m pytest -o markers=task {exercise_test.py} +$(my_venv) python3 -m pytest -o markers=task =================== FAILURES ==================== ______________ name_of_failed_test ______________ -# Test code inside of {exercise_test.py} that failed. +# Test code inside of that failed. ... E TypeOfError: ReturnedValue != ExpectedValue -exercise_test.py:{line_of_failed_test}: TypeOfError +exercise_test.py:: TypeOfError ============ short test summary info ============ FAILED exercise_test.py::ExerciseTest::name_of_failed_test ========== 1 failed, 2 passed in 0.13s ========== @@ -216,10 +229,10 @@ If you do not know where you have installed Python, run the following command in ```bash $ python3 -c "import os, sys; print(os.path.dirname(sys.executable))" -{python_directory} + ``` -The _returned_ directory is where your current active Python version is installed, in this section it is referred to as `{python_directory}`. +The _returned_ directory is where your current active Python version is installed, in this section it is referred to as ``. #### Windows @@ -232,7 +245,7 @@ Then find the `Path` variable in your _User variables_, select it, and click `Ed ![Selecting the path variable](https://raw.githubusercontent.com/exercism/python/main/docs/img/Windows-EnvironmentVariables.png) -Then add a new line, as shown in the picture, replacing `{python_directory}` with your Python installation's directory: +Then add a new line, as shown in the picture, replacing `` with your Python installation's directory: ![Add python to path](https://raw.githubusercontent.com/exercism/python/main/docs/img/Windows-AddPythonPath.png) @@ -240,17 +253,17 @@ Then add a new line, as shown in the picture, replacing `{python_directory}` wit The below should work for most Linux and MacOS flavors with a `bash` shell. Commands may vary by Linux distro, and whether a `fish` or `zsh` shell is used. -Replace `{python_directory}` with the output of `python3 -c "import os, sys; print(os.path.dirname(sys.executable))"` +Replace `` with the output of `python3 -c "import os, sys; print(os.path.dirname(sys.executable))"` ```bash -export PATH=”$PATH:{python_directory}}” +export PATH="$PATH:" ``` [Code Quality: Tools and Best Practices]: https://realpython.com/python-code-quality/ [Getting Started Guide]: https://docs.pytest.org/en/latest/getting-started.html [configuration file formats]: https://docs.pytest.org/en/6.2.x/customize.html#configuration-file-formats [marking test functions with attributes]: https://docs.pytest.org/en/6.2.x/mark.html#raising-errors-on-unknown-marks -[pdb]: https://docs.python.org/3.9/library/pdb.html +[pdb]: https://docs.python.org/3.11/library/pdb.html [pip]: https://pip.pypa.io/en/stable/getting-started/ [psf-installer]: https://www.python.org/downloads/ [pylint]: https://pylint.pycqa.org/en/latest/user_guide/ diff --git a/docs/TOOLS.md b/docs/TOOLS.md index 20ce04ded09..bacb8626aaa 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -30,7 +30,10 @@ If you have an editor, IDE, tool, or plugin recommendation, we encourage you to Before you start exploring, make sure that you have a recent version of Python installed. -The Exercism platform currently supports `Python 3.7 - 3.11.2` (_exercises and tests_) and `Python 3.11.2` (_tooling_). +The Exercism web platform currently supports `Python 3.7 - 3.11.5` (_exercises and tests_) and `Python 3.11.5` (_tooling_). +Our online test runner currently uses `pytest 7.2.2` and `pytest-subtests 0.11.0`. +Our online analyzer uses `pylint 2.17.7`. +Using different versions of `Python`, `pytest`, or `pylint` locally might give you different results than the website. For more information, please refer to [Installing Python locally][Installing Python locally].
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 diff --git a/exercises/concept/chaitanas-colossal-coaster/.docs/instructions.md b/exercises/concept/chaitanas-colossal-coaster/.docs/instructions.md index 40280179c8e..0a5bf25ff0d 100644 --- a/exercises/concept/chaitanas-colossal-coaster/.docs/instructions.md +++ b/exercises/concept/chaitanas-colossal-coaster/.docs/instructions.md @@ -12,6 +12,8 @@ There are two queues for this ride, each represented as a `list`: You have been asked to write some code to better manage the guests at the park. You need to implement the following functions as soon as possible before the guests (and your boss, Chaitana!) get cranky. + Make sure you read carefully. + Some tasks ask that you change or update the existing queue, while others ask you to make a copy of it. ## 1. Add me to the queue diff --git a/exercises/concept/chaitanas-colossal-coaster/list_methods_test.py b/exercises/concept/chaitanas-colossal-coaster/list_methods_test.py index 5df2af327b7..7a754b78e12 100644 --- a/exercises/concept/chaitanas-colossal-coaster/list_methods_test.py +++ b/exercises/concept/chaitanas-colossal-coaster/list_methods_test.py @@ -1,6 +1,7 @@ import unittest -import pytest from copy import deepcopy +import pytest + from list_methods import ( add_me_to_the_queue, @@ -16,108 +17,215 @@ class ListMethodsTest(unittest.TestCase): @pytest.mark.task(taskno=1) def test_add_me_to_the_queue(self): - test_data = [(['Tony', 'Bruce'], ['RobotGuy', 'WW'], 0, 'HawkEye'), - (['Tony', 'Bruce'], ['RobotGuy', 'WW'], 1, 'RichieRich')] - result_data = [['RobotGuy', 'WW', 'HawkEye'], - ['Tony', 'Bruce', 'RichieRich']] + test_data = [ + ((['Tony', 'Bruce'], ['RobotGuy', 'WW'], 0, 'HawkEye'), ['RobotGuy', 'WW', 'HawkEye']), + ((['Tony', 'Bruce'], ['RobotGuy', 'WW'], 1, 'RichieRich'), ['Tony', 'Bruce', 'RichieRich']), + ((['Agatha', 'Pepper', 'Valkyrie'], ['Drax', 'Nebula'], 1, 'Okoye'), ['Agatha', 'Pepper', 'Valkyrie', 'Okoye']), + ((['Agatha', 'Pepper', 'Valkyrie'], ['Drax', 'Nebula'], 0, 'Gamora'), ['Drax', 'Nebula', 'Gamora']), + ] - for variant, (params, expected) in enumerate(zip(test_data, result_data), start=1): + for variant, (params, expected) in enumerate(test_data, start=1): + # Deepcopy() is needed here because the task expects the input lists to be mutated. + # That mutation wrecks havoc with the verification and error messaging. + express_queue, normal_queue, ticket_type, person_name = deepcopy(params) + with self.subTest(f'variation #{variant}', params=params, expected=expected): + actual_result = add_me_to_the_queue(*params) + + error_message = ( + f'\nCalled add_me_to_the_queue{express_queue, normal_queue, ticket_type, person_name}.\n' + f'The function returned {actual_result},\n' + f' but the tests expected {expected} after {person_name} was added.') + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=1) + def test_add_me_to_the_queue_validate_queue(self): + test_data = [ + ((['Tony', 'Bruce'], ['RobotGuy', 'WW'], 0, 'HawkEye'), ['RobotGuy', 'WW', 'HawkEye']), + ((['Tony', 'Bruce'], ['RobotGuy', 'WW'], 1, 'RichieRich'), ['Tony', 'Bruce', 'RichieRich']), + ((['Agatha', 'Pepper', 'Valkyrie'], ['Drax', 'Nebula'], 1, 'Okoye'), ['Agatha', 'Pepper', 'Valkyrie', 'Okoye']), + ((['Agatha', 'Pepper', 'Valkyrie'], ['Drax', 'Nebula'], 0, 'Gamora'), ['Drax', 'Nebula', 'Gamora']), + ] + + for variant, (params, expected) in enumerate(test_data, start=1): # Deepcopy() is needed here because the task expects the input lists to be mutated. # That mutation wrecks havoc with the verification and error messaging. express_queue, normal_queue, ticket_type, person_name = deepcopy(params) + express, normal, ticket, name = params with self.subTest(f'variation #{variant}', - express_queue=express_queue, - normal_queue=normal_queue, - ticket_type=ticket_type, - person_name=person_name, - expected=expected): + express=express, normal=normal, + ticket=ticket, name=name, expected=expected): - actual_result = add_me_to_the_queue(*params) - error_message = (f'Called add_me_to_the_queue{express_queue, normal_queue, ticket_type, person_name}. ' - f'The function returned {actual_result}, but ' - f'The tests expected {expected} when adding ' - f'{person_name} to the queue.') + actual_result = add_me_to_the_queue(express, normal, ticket, name) + + if type == 1: + error_message = ( + f'\nCalled add_me_to_the_queue{express_queue, normal_queue, ticket_type, person_name}.\n' + f'The queue == {express}, but the tests expected\n' + f'queue == {expected} after {person_name} was added.' + ) - self.assertListEqual(actual_result, expected, msg=error_message) + self.assertIs(actual_result, express, msg=error_message) + + if type == 0: + error_message = ( + f'\nCalled add_me_to_the_queue{express_queue, normal_queue, ticket_type, person_name}.\n' + f'The queue == {normal}, but the tests expected \n' + f'queue == {expected} after {person_name} was added.' + ) + + self.assertIs(actual_result, normal, msg=error_message) @pytest.mark.task(taskno=2) def test_find_my_friend(self): - test_data = [(['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 'Natasha'), - (['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 'Steve'), - (['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 'Rocket')] + test_data = [ + (['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 'Natasha'), + (['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 'Steve'), + (['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 'Rocket'), + ] + result_data = (0,1,4) for variant, (params, expected) in enumerate(zip(test_data, result_data), start=1): with self.subTest(f'variation #{variant}', params=params, expected=expected): actual_result = find_my_friend(*params) - error_message = (f'Called find_my_friend{params}. ' - f'The function returned {actual_result}, but ' - f'The tests expected {expected} when looking for ' - f'{params[-1]} in the queue.') + error_message = ( + f'\nCalled find_my_friend{params}.\n' + f'The function returned {actual_result}, but\n' + f'the tests expected {expected} when looking for\n' + f'{params[-1]} in the queue.' + ) self.assertIs(actual_result, expected, msg=error_message) @pytest.mark.task(taskno=3) def test_add_me_with_my_friends(self): - test_data = [(['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 0, 'Bucky'), + test_data = [ + (['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 0, 'Bucky'), (['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 1, 'Bucky'), - (['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 5, 'Bucky')] + (['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 5, 'Bucky'), + ] - result_data = [['Bucky', 'Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], - ['Natasha', 'Bucky', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], - ['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket', 'Bucky']] + result_data = [ + ['Bucky', 'Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], + ['Natasha', 'Bucky', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], + ['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket', 'Bucky'], + ] for variant, (params, expected) in enumerate(zip(test_data, result_data), start=1): - # Deepcopy() is needed here because the task expects the input lists to be mutated. # That mutation wrecks havoc with the verification and error messaging. queue, index, person_name = deepcopy(params) - with self.subTest(f'variation #{variant}', - queue=queue, - index=index, - person_name=person_name, - expected=expected): + with self.subTest(f'variation #{variant}', params=params, expected=expected): actual_result = add_me_with_my_friends(*params) - error_message = (f'Called add_me_with_my_friends{queue, index, person_name}. ' - f'The function returned {actual_result}, but ' - f'The tests expected {expected} when adding ' - f'{person_name} to position {index} in the queue.') + error_message = ( + f'\nCalled add_me_with_my_friends{queue, index, person_name}.\n' + f'The function returned {actual_result}, but\n' + f'the tests expected {expected} when adding\n' + f'{person_name} to position {index} in the queue.' + ) + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=3) + def test_add_me_with_my_friends_validate_queue(self): + test_data = [ + (['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 0, 'Bucky'), + (['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 1, 'Bucky'), + (['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 5, 'Bucky'), + ] + + result_data = [ + ['Bucky', 'Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], + ['Natasha', 'Bucky', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], + ['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket', 'Bucky'], + ] - self.assertListEqual(actual_result, expected, msg=error_message) + for variant, (params, expected) in enumerate(zip(test_data, result_data), start=1): + # Deepcopy() is needed here because the task expects the input lists to be mutated. + # That mutation wrecks havoc with the verification and error messaging. + start_queue, add_index, person_name = deepcopy(params) + queue, _, _ = params + + with self.subTest(f'variation #{variant}', params=params, expected=expected): + actual_result = add_me_with_my_friends(*params) + error_message = ( + f'\nCalled add_me_with_my_friends{start_queue, add_index, person_name}.\n' + f'The function returned {actual_result},\n' + 'but the original queue was unmodified. The tests expected the \n' + f'*original* queue to be modified by adding "{person_name}".' + ) + + self.assertIs(actual_result, queue, msg=error_message) @pytest.mark.task(taskno=4) def test_remove_the_mean_person(self): - test_data = [(['Natasha', 'Steve', 'Ultron', 'Wanda', 'Rocket'], 'Ultron'), - (['Natasha', 'Steve', 'Wanda', 'Rocket', 'Ultron'], 'Ultron'), - (['Ultron', 'Natasha', 'Steve', 'Wanda', 'Rocket'], 'Ultron'), + test_data = [ + (['Natasha', 'Steve', 'Ultron', 'Wanda', 'Rocket'], 'Ultron'), + (['Natasha', 'Steve', 'Wanda', 'Rocket', 'Ultron'], 'Rocket'), + (['Ultron', 'Natasha', 'Steve', 'Wanda', 'Rocket'], 'Steve'), + ] + result_data = [ + ['Natasha', 'Steve', 'Wanda', 'Rocket'], + ['Natasha', 'Steve', 'Wanda', 'Ultron'], + ['Ultron', 'Natasha', 'Wanda', 'Rocket'], ] - result_data = [['Natasha', 'Steve', 'Wanda', 'Rocket'], - ['Natasha', 'Steve', 'Wanda', 'Rocket'], - ['Natasha', 'Steve', 'Wanda', 'Rocket']] for variant, (params, expected) in enumerate(zip(test_data, result_data), start=1): # Deepcopy() is needed here because the task expects the input lists to be mutated. # That mutation wrecks havoc with the verification and error messaging. - queue, person_name = deepcopy(params) + start_queue, person_name = deepcopy(params) - with self.subTest(f'variation #{variant}', - queue=queue, - person_name=person_name, - expected=expected): + with self.subTest(f'variation #{variant}', params=params, expected=expected): + actual_result = remove_the_mean_person(*params) + error_message = ( + f'\nCalled remove_the_mean_person{start_queue, person_name}.\n' + f'The function returned {actual_result}, but\n' + f'the tests expected {expected} when removing\n' + f'{person_name} from the queue.' + ) + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=4) + def test_remove_the_mean_person_validate_queue(self): + test_data = [ + (['Natasha', 'Steve', 'Ultron', 'Wanda', 'Rocket'], 'Ultron'), + (['Natasha', 'Steve', 'Wanda', 'Rocket', 'Ultron'], 'Rocket'), + (['Ultron', 'Natasha', 'Steve', 'Wanda', 'Rocket'], 'Steve'), + ] + + result_data = [ + ['Natasha', 'Steve', 'Wanda', 'Rocket'], + ['Natasha', 'Steve', 'Wanda', 'Ultron'], + ['Ultron', 'Natasha', 'Wanda', 'Rocket'], + ] + + + for variant, (params, expected) in enumerate(zip(test_data, result_data), start=1): + + # Deepcopy() is needed here because the task expects the input lists to be mutated. + # That mutation wrecks havoc with the verification and error messaging. + start_queue, person_name = deepcopy(params) + queue, _ = params + with self.subTest(f'variation #{variant}', params=params, expected=expected): actual_result = remove_the_mean_person(*params) - error_message = (f'Called remove_the_mean_person{queue, person_name}. ' - f'The function returned {actual_result}, but ' - f'The tests expected {expected} when removing ' - f'{person_name} from the queue.') + error_message = ( + f'\nCalled remove_the_mean_person{start_queue, person_name}.\n' + f'The function returned {actual_result}, queue == {queue}.\n' + f'But the tests expected queue == {expected} when removing\n' + f'{person_name}.' + ) + + self.assertIs(actual_result, queue, msg=error_message) - self.assertListEqual(actual_result, expected, msg=error_message) @pytest.mark.task(taskno=5) def test_how_many_namefellows(self): @@ -141,46 +249,68 @@ def test_how_many_namefellows(self): @pytest.mark.task(taskno=6) def test_remove_the_last_person(self): test_data = [ - (['Natasha', 'Steve', 'Ultron', 'Natasha', 'Rocket'], 'Rocket'), - (['Wanda', 'Natasha', 'Steve', 'Rocket', 'Ultron'], 'Ultron'), - (['Steve', 'Wanda', 'Rocket', 'Ultron', 'Natasha'], 'Natasha') + (['Natasha', 'Steve', 'Ultron', 'Natasha', 'Rocket'], ['Natasha', 'Steve', 'Ultron', 'Natasha'], 'Rocket'), + (['Wanda', 'Natasha', 'Steve', 'Rocket', 'Ultron'], ['Wanda', 'Natasha', 'Steve', 'Rocket'], 'Ultron'), + (['Steve', 'Wanda', 'Rocket', 'Ultron', 'Natasha'], ['Steve', 'Wanda', 'Rocket', 'Ultron'], 'Natasha') ] - for variant, (input, expected) in enumerate(test_data, start=1): - with self.subTest(f'variation #{variant}', input=input, expected=expected): - actual_result = remove_the_last_person(input) + for variant, (queue, modified, expected) in enumerate(test_data, start=1): + with self.subTest(f'variation #{variant}', queue=queue, modified=modified, expected=expected): + + # Deepcopy() is needed here because the task expects the input lists to be mutated. + # That mutation wrecks havoc with the verification and error messaging. + unmodified_queue = deepcopy(queue) expected_result = expected + actual_result = remove_the_last_person(queue) + expected_queue = modified - error_message = (f'Called remove_the_last_person({input}).' - f'The function returned {actual_result}, but the tests expected {expected_result}.') + error_message = (f'\nCalled remove_the_last_person({unmodified_queue}).\n' + f'The function was expected to remove and return the name "{expected_result}" ' + f'and change the queue to {expected_queue},\n' + f'but the name "{actual_result}" was returned and the queue == {queue}.') - self.assertEqual(actual_result, expected_result, msg=error_message) + self.assertEqual((actual_result, queue), (expected_result, expected_queue), msg=error_message) - @pytest.mark.task(taskno=6) - def test_remove_the_last_person_validate_queue(self): - test_data = [ - (['Natasha', 'Steve', 'Ultron', 'Natasha', 'Rocket'], ['Natasha', 'Steve', 'Ultron', 'Natasha']), - (['Wanda', 'Natasha', 'Steve', 'Rocket', 'Ultron'], ['Wanda', 'Natasha', 'Steve', 'Rocket']), - (['Steve', 'Wanda', 'Rocket', 'Ultron', 'Natasha'], ['Steve', 'Wanda', 'Rocket', 'Ultron']) - ] - for variant, (input, modified) in enumerate(test_data, start=1): - with self.subTest(f'variation #{variant}', input=input, modified=modified): - unmodified = deepcopy(input) - actual_result = remove_the_last_person(input) - expected_queue = modified - error_message = (f'\nCalled remove_the_last_person({unmodified}).\n' - f'The function was expected to change the queue to {expected_queue},\n' - f'but the queue looks like {input} instead.') + @pytest.mark.task(taskno=7) + def test_sorted_names(self): + test_data =( + (['Steve', 'Ultron', 'Natasha', 'Rocket'], ['Natasha', 'Rocket', 'Steve', 'Ultron']), + (['Agatha', 'Pepper', 'Valkyrie', 'Drax', 'Nebula'], ['Agatha', 'Drax', 'Nebula', 'Pepper', 'Valkyrie']), + (['Gamora', 'Loki', 'Tony', 'Peggy', 'Okoye'], ['Gamora', 'Loki', 'Okoye', 'Peggy', 'Tony']), + ) + + for variant, (queue, expected) in enumerate(test_data, start=1): + with self.subTest(f'variation #{variant}', queue=queue, expected=expected): + actual_result = sorted_names(queue) + expected_result = expected + + error_message = (f'\nCalled sorted_names({queue}).\n' + f'The function returned {actual_result}, but \n' + f'the tests expect {expected_result}.') - self.assertEqual(input, expected_queue, msg=error_message) + self.assertEqual(actual_result, expected_result, msg=error_message) @pytest.mark.task(taskno=7) - def test_sorted_names(self): - test_data = ['Steve', 'Ultron', 'Natasha', 'Rocket'] - expected = ['Natasha', 'Rocket', 'Steve', 'Ultron'] - actual_result = sorted_names(test_data) - error_message = (f'Called sorted_names({test_data}).' - f'The function returned {actual_result}, but the tests ' - f'expected {expected}.') - - self.assertListEqual(actual_result, expected, msg=error_message) + def test_sorted_names_validate_queue(self): + test_data = ( + (['Steve', 'Ultron', 'Natasha', 'Rocket'], ['Natasha', 'Rocket', 'Steve', 'Ultron']), + (['Agatha', 'Pepper', 'Valkyrie', 'Drax', 'Nebula'], ['Agatha', 'Drax', 'Nebula', 'Pepper', 'Valkyrie']), + (['Gamora', 'Loki', 'Tony', 'Peggy', 'Okoye'], ['Gamora', 'Loki', 'Okoye', 'Peggy', 'Tony']), + ) + + for variant, (queue, expected) in enumerate(test_data, start=1): + with self.subTest(f'variation #{variant}', queue=queue, expected=expected): + + # Deepcopy() is needed here because the input lists might be mutated. + # That mutation wrecks havoc with the verification and error messaging. + original_queue = deepcopy(queue) + actual_result = sorted_names(queue) + expected_result = expected + + error_message = (f'\nCalled sorted_names({original_queue}).\n' + f'The function returned {actual_result}, \n' + f'with a queue == {queue}.\n' + f'The tests expect {expected_result}, \n' + f'with a queue == {original_queue}.') + + self.assertIsNot(actual_result, queue, msg=error_message) diff --git a/exercises/concept/guidos-gorgeous-lasagna/.docs/hints.md b/exercises/concept/guidos-gorgeous-lasagna/.docs/hints.md index f2c07d50b6e..96668ffbd0b 100644 --- a/exercises/concept/guidos-gorgeous-lasagna/.docs/hints.md +++ b/exercises/concept/guidos-gorgeous-lasagna/.docs/hints.md @@ -24,7 +24,8 @@ - You need to define a [function][defining functions] with a single parameter representing the number of layers. - Use the [mathematical operator for multiplication][numbers] to multiply values. -- You could define an extra _constant_ for the time in minutes per layer rather than using a "magic number" in your code. +- You can define a PREPARATION_TIME _constant_ for the time in minutes per layer rather than using a ["magic + number"][magic-numbers] in your code. - This function should [return a value][return]. ## 4. Calculate total elapsed cooking time (prep + bake) in minutes @@ -43,6 +44,7 @@ [constants]: https://stackoverflow.com/a/2682752 [defining functions]: https://docs.python.org/3/tutorial/controlflow.html#defining-functions [docstrings]: https://docs.python.org/3/tutorial/controlflow.html#tut-docstrings +[magic-numbers]: https://en.wikipedia.org/wiki/Magic_number_(programming) [naming]: https://realpython.com/python-variables/ [numbers]: https://docs.python.org/3/tutorial/introduction.html#numbers [pep257]: https://www.python.org/dev/peps/pep-0257/ diff --git a/exercises/concept/guidos-gorgeous-lasagna/.docs/instructions.md b/exercises/concept/guidos-gorgeous-lasagna/.docs/instructions.md index fce16c46795..1991721c38b 100644 --- a/exercises/concept/guidos-gorgeous-lasagna/.docs/instructions.md +++ b/exercises/concept/guidos-gorgeous-lasagna/.docs/instructions.md @@ -50,10 +50,16 @@ Assume each layer takes 2 minutes to prepare. ``` -## 4. Calculate total elapsed cooking time (prep + bake) in minutes +## 4. Calculate total elapsed time (prepping + baking) in minutes + +Define the `elapsed_time_in_minutes()` function that takes two parameters as arguments: + +- `number_of_layers` (_the number of layers added to the lasagna_) +- `elapsed_bake_time` (_the number of minutes the lasagna has spent baking in the oven already_). + +This function should return the total minutes you have been in the kitchen cooking β€” your preparation time layering + +the time the lasagna has spent baking in the oven. -Define the `elapsed_time_in_minutes()` function that takes two parameters as arguments: `number_of_layers` (_the number of layers added to the lasagna_) and `elapsed_bake_time` (_the number of minutes the lasagna has been baking in the oven_). -This function should return the total number of minutes you have been cooking, or the sum of your preparation time and the time the lasagna has already spent baking in the oven. ```python >>> def elapsed_time_in_minutes(number_of_layers, elapsed_bake_time): diff --git a/exercises/concept/guidos-gorgeous-lasagna/lasagna.py b/exercises/concept/guidos-gorgeous-lasagna/lasagna.py index a3df0ab2da0..0e1a50d571e 100644 --- a/exercises/concept/guidos-gorgeous-lasagna/lasagna.py +++ b/exercises/concept/guidos-gorgeous-lasagna/lasagna.py @@ -8,7 +8,7 @@ """ -#TODO: define the 'EXPECTED_BAKE_TIME' constant below. +#TODO: define your EXPECTED_BAKE_TIME (required) and PREPARATION_TIME (optional) constants below. #TODO: Remove 'pass' and complete the 'bake_time_remaining()' function below. @@ -27,9 +27,9 @@ def bake_time_remaining(): #TODO: Define the 'preparation_time_in_minutes()' function below. -# You might also consider defining a 'PREPARATION_TIME' constant. +# To avoid the use of magic numbers (see: https://en.wikipedia.org/wiki/Magic_number_(programming)), you should define a PREPARATION_TIME constant. # You can do that on the line below the 'EXPECTED_BAKE_TIME' constant. -# This will make it easier to do calculations. +# This will make it easier to do calculations, and make changes to your code. 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'} 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. diff --git a/exercises/concept/locomotive-engineer/.docs/introduction.md b/exercises/concept/locomotive-engineer/.docs/introduction.md index 798a334aeb9..b10fff1217f 100644 --- a/exercises/concept/locomotive-engineer/.docs/introduction.md +++ b/exercises/concept/locomotive-engineer/.docs/introduction.md @@ -213,12 +213,16 @@ This will pack all the values into a `list`/`tuple`. >>> combined_fruits ("apple", "banana", "cherry", "orange", "kiwi", "melon", "mango") -# If the * operator is used on the left side of "=" the result is a list +# If the * operator is used on the left side of "=" the result is a list. +# Note the trailing comma. >>> *combined_fruits_too, = *fruits, *more_fruits >>> combined_fruits_too ['apple', 'banana', 'cherry', 'orange', 'kiwi', 'melon', 'mango'] ``` +For more background on using `*` on the left-hand side, see [PEP 3132][pep-3132]. + + ### Packing a dictionary with `**` Packing a dictionary is done by using the `**` operator. @@ -347,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]) @@ -361,6 +365,8 @@ Since `zip()` takes multiple iterables and returns a `list` of `tuples` with the [items]: https://docs.python.org/3/library/stdtypes.html#dict.items [multiple assignment]: https://www.geeksforgeeks.org/assigning-multiple-variables-in-one-line-in-python/ [packing and unpacking]: https://www.geeksforgeeks.org/packing-and-unpacking-arguments-in-python/ +[pep-3132]: https://peps.python.org/pep-3132/ [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/mecha-munch-management/.docs/hints.md b/exercises/concept/mecha-munch-management/.docs/hints.md index 3287768ff5f..2d2f49e2cc8 100644 --- a/exercises/concept/mecha-munch-management/.docs/hints.md +++ b/exercises/concept/mecha-munch-management/.docs/hints.md @@ -50,6 +50,7 @@ The dictionary section of the [official tutorial][dicts-docs] and the mapping ty [dicts-docs]: https://docs.python.org/3/tutorial/datastructures.html#dictionaries [fromkeys]: https://docs.python.org/3/library/stdtypes.html#dict.fromkeys [items]: https://docs.python.org/3/library/stdtypes.html#dict.items +[keys]: https://docs.python.org/3/library/stdtypes.html#dict.keys [mapping-types-dict]: https://docs.python.org/3/library/stdtypes.html#mapping-types-dict [mvp]: https://en.wikipedia.org/wiki/Minimum_viable_product [set-default]: https://docs.python.org/3/library/stdtypes.html#dict.setdefault diff --git a/exercises/concept/mecha-munch-management/.docs/instructions.md b/exercises/concept/mecha-munch-management/.docs/instructions.md index 802366aab45..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, '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/.meta/config.json b/exercises/concept/mecha-munch-management/.meta/config.json index f09d0f29537..b75803ad5a8 100644 --- a/exercises/concept/mecha-munch-management/.meta/config.json +++ b/exercises/concept/mecha-munch-management/.meta/config.json @@ -14,6 +14,9 @@ ], "exemplar": [ ".meta/exemplar.py" + ], + "editor": [ + "dict_methods_test_data.py" ] }, "icon": "gross-store", 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. """ diff --git a/exercises/concept/mecha-munch-management/dict_methods_test.py b/exercises/concept/mecha-munch-management/dict_methods_test.py index 63fc1874a05..4d8dab865a1 100644 --- a/exercises/concept/mecha-munch-management/dict_methods_test.py +++ b/exercises/concept/mecha-munch-management/dict_methods_test.py @@ -1,29 +1,29 @@ import unittest import pytest from collections import OrderedDict -from dict_methods import (add_item, - read_notes, - update_recipes, - sort_entries, - send_to_store, - update_store_inventory) - +from dict_methods import ( + add_item, + read_notes, + update_recipes, + sort_entries, + send_to_store, + update_store_inventory, +) + +from dict_methods_test_data import ( + add_item_data, + read_notes_data, + update_recipes_data, + sort_entries_data, + send_to_store_data, + update_store_inventory_data, +) class MechaMunchManagementTest(unittest.TestCase): @pytest.mark.task(taskno=1) def test_add_item(self): - input_data = [ - ({'Apple': 1, 'Banana': 4 }, ('Apple', 'Banana', 'Orange')), - ({'Orange': 1, 'Raspberry': 1, 'Blueberries': 10}, ['Raspberry', 'Blueberries', 'Raspberry']), - ({'Broccoli': 1, 'Banana': 1}, ('Broccoli', 'Kiwi', 'Kiwi', 'Kiwi', 'Melon', 'Apple', 'Banana', 'Banana')) - ] - - output_data = [{'Apple': 2, 'Banana': 5, 'Orange': 1}, - {'Orange': 1, 'Raspberry': 3, 'Blueberries': 11}, - {'Broccoli': 2, 'Banana': 3, 'Kiwi': 3, 'Melon': 1, 'Apple': 1}] - - for variant, (input_data, expected) in enumerate(zip(input_data, output_data), start=1): + for variant, (input_data, expected) in enumerate(add_item_data, start=1): with self.subTest(f'variation #{variant}', input_data=input_data, expected=expected): actual_result = add_item(input_data[0], input_data[1]) error_msg= (f'Called add_item({input_data[0]}, {input_data[1]}). ' @@ -34,13 +34,7 @@ def test_add_item(self): @pytest.mark.task(taskno=2) def test_read_notes(self): - input_data = [('Apple', "Banana"), ('Orange', 'Raspberry', 'Blueberries'), - ['Broccoli', 'Kiwi', 'Melon', 'Apple', 'Banana']] - - output_data = [{'Apple': 1, 'Banana': 1}, {'Orange': 1, 'Raspberry': 1, 'Blueberries': 1}, - {'Broccoli': 1, 'Kiwi': 1, 'Melon': 1, 'Apple': 1, 'Banana': 1}] - - for variant, (input_data, expected) in enumerate(zip(input_data, output_data), start=1): + for variant, (input_data, expected) in enumerate(read_notes_data, start=1): with self.subTest(f'variation #{variant}', input_data=input_data, expected=expected): actual_result = read_notes(input_data) error_msg = (f'Called read_notes({input_data}). ' @@ -51,36 +45,7 @@ def test_read_notes(self): @pytest.mark.task(taskno=3) def test_update_recipes(self): - input_data = [ - ({'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}),)), - - ({'Apple Pie': {'Apple': 1, 'Pie Crust': 1, 'Cream Custard': 1}, - 'Blueberry Pie': {'Blueberries': 1, 'Pie Crust': 1, 'Cream Custard': 1}}, - (('Blueberry Pie', {'Blueberries': 2, 'Pie Crust': 1, 'Cream Custard': 1}), - ('Apple Pie', {'Apple': 1, 'Pie Crust': 1, 'Cream Custard': 1}))), - - ({'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}))) - ] - - output_data = [ - {'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}}, - {'Apple Pie': {'Apple': 1, 'Pie Crust': 1, 'Cream Custard': 1}, - 'Blueberry Pie': {'Blueberries': 2, 'Pie Crust': 1, 'Cream Custard': 1}}, - {'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}} - ] - - for variant, (input_data, expected) in enumerate(zip(input_data, output_data), start=1): + for variant, (input_data, expected) in enumerate(update_recipes_data, start=1): with self.subTest(f'variation #{variant}', input_data=input_data, expected=expected): actual_result = update_recipes(input_data[0], input_data[1]) error_msg = (f'Called update_recipes({input_data[0]}, {input_data[1]}). ' @@ -91,21 +56,7 @@ def test_update_recipes(self): @pytest.mark.task(taskno=4) def test_sort_entries(self): - input_data = [ - {'Banana': 4, 'Apple': 2, 'Orange': 1, 'Pear': 12}, - {'Apple': 3, 'Orange': 5, 'Banana': 1, 'Avocado': 2}, - {'Orange': 3, 'Banana': 2, 'Apple': 1}, - {'Apple': 2, 'Raspberry': 2, 'Blueberries': 5, 'Broccoli' : 2, 'Kiwi': 1, 'Melon': 4} - ] - - output_data = [ - {'Apple': 2, 'Banana': 4, 'Orange': 1, 'Pear': 12}, - {'Apple': 3, 'Avocado': 2, 'Banana': 1, 'Orange': 5}, - {'Apple': 1, 'Banana': 2, 'Orange': 3}, - {'Apple' : 2, 'Blueberries': 5, 'Broccoli': 2, 'Kiwi': 1, 'Melon': 4, 'Raspberry': 2} - ] - - for variant, (input_data, expected) in enumerate(zip(input_data, output_data), start=1): + for variant, (input_data, expected) in enumerate(sort_entries_data, start=1): with self.subTest(f'variation #{variant}', input_data=input_data, expecred=expected): actual_result = sort_entries(input_data) error_msg = (f'Called sort_entries({input_data}). ' @@ -119,49 +70,7 @@ def test_sort_entries(self): @pytest.mark.task(taskno=5) def test_send_to_store(self): - input_data = [ - ({'Banana': 3, 'Apple': 2, 'Orange': 1, 'Milk': 2}, - {'Banana': ['Aisle 5', False], 'Apple': ['Aisle 4', False], - 'Orange': ['Aisle 4', False], 'Milk': ['Aisle 2', True]}), - - ({'Kiwi': 3, 'Juice': 5, 'Yoghurt': 2, 'Milk': 5}, - {'Kiwi': ['Aisle 6', False], 'Juice': ['Aisle 5', False], - 'Yoghurt': ['Aisle 2', True], 'Milk': ['Aisle 2', True]}), - - ({'Apple': 2, 'Raspberry': 2, 'Blueberries': 5, - 'Broccoli': 2, 'Kiwi': 1, 'Melon': 4}, - - {'Apple': ['Aisle 1', False], 'Raspberry': ['Aisle 6', False], - 'Blueberries': ['Aisle 6', False], 'Broccoli': ['Aisle 3', False], - 'Kiwi': ['Aisle 6', False], 'Melon': ['Aisle 6', False]}), - - ({'Orange': 1}, - {'Banana': ['Aisle 5', False], 'Apple': ['Aisle 4', False], - 'Orange': ['Aisle 4', False], 'Milk': ['Aisle 2', True]}), - - ({'Banana': 3, 'Apple': 2, 'Orange': 1}, - {'Banana': ['Aisle 5', False], 'Apple': ['Aisle 4', False], - 'Orange': ['Aisle 4', False], 'Milk': ['Aisle 2', True]}), - ] - - output_data = [ - {'Orange': [1, 'Aisle 4', False], 'Milk': [2, 'Aisle 2', True], - 'Banana': [3, 'Aisle 5', False], 'Apple': [2, 'Aisle 4', False]}, - - {'Yoghurt': [2, 'Aisle 2', True], 'Milk': [5, 'Aisle 2', True], - 'Kiwi': [3, 'Aisle 6', False], 'Juice': [5, 'Aisle 5', False]}, - - {'Raspberry': [2, 'Aisle 6', False], 'Melon': [4, 'Aisle 6', False], - 'Kiwi': [1, 'Aisle 6', False], 'Broccoli': [2, 'Aisle 3', False], - 'Blueberries': [5, 'Aisle 6', False], 'Apple': [2, 'Aisle 1', False]}, - - {'Orange': [1, 'Aisle 4', False]}, - - {'Orange': [1, 'Aisle 4', False], 'Banana': [3, 'Aisle 5', False], - 'Apple': [2, 'Aisle 4', False]}, - ] - - for variant, (input_data, expected) in enumerate(zip(input_data, output_data), start=1): + for variant, (input_data, expected) in enumerate(send_to_store_data, start=1): with self.subTest(f'variation #{variant}', input_data=input_data, expected=expected): actual_result = send_to_store(input_data[0], input_data[1]) error_msg = (f'Called send_to_store({input_data[0]}, {input_data[1]}). ' @@ -175,36 +84,7 @@ def test_send_to_store(self): @pytest.mark.task(taskno=6) def test_update_store_inventory(self): - input_data = [ - ({'Orange': [1, 'Aisle 4', False], 'Milk': [2, 'Aisle 2', True], - 'Banana': [3, 'Aisle 5', False], 'Apple': [2, 'Aisle 4', False]}, - {'Banana': [15, 'Aisle 5', False], 'Apple': [12, 'Aisle 4', False], - 'Orange': [1, 'Aisle 4', False], 'Milk': [4, 'Aisle 2', True]}), - - ({'Kiwi': [3, 'Aisle 6', False]},{'Kiwi': [3, 'Aisle 6', False], 'Juice': [5, 'Aisle 5', False], - 'Yoghurt': [2, 'Aisle 2', True], 'Milk': [5, 'Aisle 2', True]}), - - ({'Kiwi': [1, 'Aisle 6', False], 'Melon': [4, 'Aisle 6', False], 'Apple': [2, 'Aisle 1', False], - 'Raspberry': [2, 'Aisle 6', False], 'Blueberries': [5, 'Aisle 6', False], - 'Broccoli': [1, 'Aisle 3', False]}, - {'Apple': [2, 'Aisle 1', False], 'Raspberry': [5, 'Aisle 6', False], - 'Blueberries': [10, 'Aisle 6', False], 'Broccoli': [4, 'Aisle 3', False], - 'Kiwi': [1, 'Aisle 6', False], 'Melon': [8, 'Aisle 6', False]}) - ] - - output_data = [ - {'Banana': [12, 'Aisle 5', False], 'Apple': [10, 'Aisle 4', False], - 'Orange': ['Out of Stock', 'Aisle 4', False], 'Milk': [2, 'Aisle 2', True]}, - - {'Juice': [5, 'Aisle 5', False], 'Yoghurt': [2, 'Aisle 2', True], - 'Milk': [5, 'Aisle 2', True], 'Kiwi': ["Out of Stock", 'Aisle 6', False]}, - - {'Kiwi': ['Out of Stock', 'Aisle 6', False], 'Melon': [4, 'Aisle 6', False], - 'Apple': ['Out of Stock', 'Aisle 1', False], 'Raspberry': [3, 'Aisle 6', False], - 'Blueberries': [5, 'Aisle 6', False], 'Broccoli': [3, 'Aisle 3', False]} - ] - - for variant, (input_data, expected) in enumerate(zip(input_data, output_data), start=1): + for variant, (input_data, expected) in enumerate(update_store_inventory_data, start=1): with self.subTest(f'variation #{variant}', input_data=input_data, expected=expected): actual_result = update_store_inventory(input_data[0], input_data[1]) error_msg = (f'Called update_store_inventory({input_data[0]}, {input_data[1]}). ' diff --git a/exercises/concept/mecha-munch-management/dict_methods_test_data.py b/exercises/concept/mecha-munch-management/dict_methods_test_data.py new file mode 100644 index 00000000000..eea18cf541a --- /dev/null +++ b/exercises/concept/mecha-munch-management/dict_methods_test_data.py @@ -0,0 +1,393 @@ +##add_item test cases## +add_item_inputs = [ + ({"Apple": 1, "Banana": 4}, ("Apple", "Banana", "Orange")), + ( + {"Orange": 1, "Raspberry": 1, "Blueberries": 10}, + ["Raspberry", "Blueberries", "Raspberry"], + ), + ( + {"Broccoli": 1, "Banana": 1}, + ("Broccoli", "Kiwi", "Kiwi", "Kiwi", "Melon", "Apple", "Banana", "Banana"), + ), +] + +add_item_outputs = [ + {"Apple": 2, "Banana": 5, "Orange": 1}, + {"Orange": 1, "Raspberry": 3, "Blueberries": 11}, + {"Broccoli": 2, "Banana": 3, "Kiwi": 3, "Melon": 1, "Apple": 1}, +] + +add_item_data = zip(add_item_inputs, add_item_outputs) + + +##read_notes test cases## +read_notes_inputs = [ + ("Apple", "Banana"), + ("Orange", "Raspberry", "Blueberries"), + ["Broccoli", "Kiwi", "Melon", "Apple", "Banana"], +] + +read_notes_outputs = [ + {"Apple": 1, "Banana": 1}, + {"Orange": 1, "Raspberry": 1, "Blueberries": 1}, + {"Broccoli": 1, "Kiwi": 1, "Melon": 1, "Apple": 1, "Banana": 1}, +] + +read_notes_data = zip(read_notes_inputs, read_notes_outputs) + + +##update_recipes test cases## +update_recipes_inputs = [ + ( + { + "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, + }, + ), + ), + ), + ( + { + "Apple Pie": {"Apple": 1, "Pie Crust": 1, "Cream Custard": 1}, + "Blueberry Pie": {"Blueberries": 1, "Pie Crust": 1, "Cream Custard": 1}, + }, + ( + ("Blueberry Pie", {"Blueberries": 2, "Pie Crust": 1, "Cream Custard": 1}), + ("Apple Pie", {"Apple": 1, "Pie Crust": 1, "Cream Custard": 1}), + ), + ), + ( + { + "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, + }, + ), + ), + ), +] + +update_recipes_outputs = [ + { + "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, + }, + }, + { + "Apple Pie": {"Apple": 1, "Pie Crust": 1, "Cream Custard": 1}, + "Blueberry Pie": {"Blueberries": 2, "Pie Crust": 1, "Cream Custard": 1}, + }, + { + "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, + }, + }, +] + +update_recipes_data = zip(update_recipes_inputs, update_recipes_outputs) + + +##sort_entries test cases## +sort_entries_inputs = [ + {"Banana": 4, "Apple": 2, "Orange": 1, "Pear": 12}, + {"Apple": 3, "Orange": 5, "Banana": 1, "Avocado": 2}, + {"Orange": 3, "Banana": 2, "Apple": 1}, + { + "Apple": 2, + "Raspberry": 2, + "Blueberries": 5, + "Broccoli": 2, + "Kiwi": 1, + "Melon": 4, + }, +] + +sort_entries_outputs = [ + {"Apple": 2, "Banana": 4, "Orange": 1, "Pear": 12}, + {"Apple": 3, "Avocado": 2, "Banana": 1, "Orange": 5}, + {"Apple": 1, "Banana": 2, "Orange": 3}, + { + "Apple": 2, + "Blueberries": 5, + "Broccoli": 2, + "Kiwi": 1, + "Melon": 4, + "Raspberry": 2, + }, +] + + +sort_entries_data = zip(sort_entries_inputs, sort_entries_outputs) + + +##send_to_store test cases## +send_to_store_inputs = [ + ( + {"Banana": 3, "Apple": 2, "Orange": 1, "Milk": 2}, + { + "Banana": ["Aisle 5", False], + "Apple": ["Aisle 4", False], + "Orange": ["Aisle 4", False], + "Milk": ["Aisle 2", True], + }, + ), + ( + {"Kiwi": 3, "Juice": 5, "Yoghurt": 2, "Milk": 5}, + { + "Kiwi": ["Aisle 6", False], + "Juice": ["Aisle 5", False], + "Yoghurt": ["Aisle 2", True], + "Milk": ["Aisle 2", True], + }, + ), + ( + { + "Apple": 2, + "Raspberry": 2, + "Blueberries": 5, + "Broccoli": 2, + "Kiwi": 1, + "Melon": 4, + }, + { + "Apple": ["Aisle 1", False], + "Raspberry": ["Aisle 6", False], + "Blueberries": ["Aisle 6", False], + "Broccoli": ["Aisle 3", False], + "Kiwi": ["Aisle 6", False], + "Melon": ["Aisle 6", False], + }, + ), + ( + {"Orange": 1}, + { + "Banana": ["Aisle 5", False], + "Apple": ["Aisle 4", False], + "Orange": ["Aisle 4", False], + "Milk": ["Aisle 2", True], + }, + ), + ( + {"Banana": 3, "Apple": 2, "Orange": 1}, + { + "Banana": ["Aisle 5", False], + "Apple": ["Aisle 4", False], + "Orange": ["Aisle 4", False], + "Milk": ["Aisle 2", True], + }, + ), +] + +send_to_store_outputs = [ + { + "Orange": [1, "Aisle 4", False], + "Milk": [2, "Aisle 2", True], + "Banana": [3, "Aisle 5", False], + "Apple": [2, "Aisle 4", False], + }, + { + "Yoghurt": [2, "Aisle 2", True], + "Milk": [5, "Aisle 2", True], + "Kiwi": [3, "Aisle 6", False], + "Juice": [5, "Aisle 5", False], + }, + { + "Raspberry": [2, "Aisle 6", False], + "Melon": [4, "Aisle 6", False], + "Kiwi": [1, "Aisle 6", False], + "Broccoli": [2, "Aisle 3", False], + "Blueberries": [5, "Aisle 6", False], + "Apple": [2, "Aisle 1", False], + }, + {"Orange": [1, "Aisle 4", False]}, + { + "Orange": [1, "Aisle 4", False], + "Banana": [3, "Aisle 5", False], + "Apple": [2, "Aisle 4", False], + }, +] + +send_to_store_data = zip(send_to_store_inputs, send_to_store_outputs) + + +##update_store_inventory test cases## +update_store_inventory_inputs = [ + ( + { + "Orange": [1, "Aisle 4", False], + "Milk": [2, "Aisle 2", True], + "Banana": [3, "Aisle 5", False], + "Apple": [2, "Aisle 4", False], + }, + { + "Banana": [15, "Aisle 5", False], + "Apple": [12, "Aisle 4", False], + "Orange": [1, "Aisle 4", False], + "Milk": [4, "Aisle 2", True], + }, + ), + ( + {"Kiwi": [3, "Aisle 6", False]}, + { + "Kiwi": [3, "Aisle 6", False], + "Juice": [5, "Aisle 5", False], + "Yoghurt": [2, "Aisle 2", True], + "Milk": [5, "Aisle 2", True], + }, + ), + ( + { + "Kiwi": [1, "Aisle 6", False], + "Melon": [4, "Aisle 6", False], + "Apple": [2, "Aisle 1", False], + "Raspberry": [2, "Aisle 6", False], + "Blueberries": [5, "Aisle 6", False], + "Broccoli": [1, "Aisle 3", False], + }, + { + "Apple": [2, "Aisle 1", False], + "Raspberry": [5, "Aisle 6", False], + "Blueberries": [10, "Aisle 6", False], + "Broccoli": [4, "Aisle 3", False], + "Kiwi": [1, "Aisle 6", False], + "Melon": [8, "Aisle 6", False], + }, + ), +] + +update_store_inventory_outputs = [ + { + "Banana": [12, "Aisle 5", False], + "Apple": [10, "Aisle 4", False], + "Orange": ["Out of Stock", "Aisle 4", False], + "Milk": [2, "Aisle 2", True], + }, + { + "Juice": [5, "Aisle 5", False], + "Yoghurt": [2, "Aisle 2", True], + "Milk": [5, "Aisle 2", True], + "Kiwi": ["Out of Stock", "Aisle 6", False], + }, + { + "Kiwi": ["Out of Stock", "Aisle 6", False], + "Melon": [4, "Aisle 6", False], + "Apple": ["Out of Stock", "Aisle 1", False], + "Raspberry": [3, "Aisle 6", False], + "Blueberries": [5, "Aisle 6", False], + "Broccoli": [3, "Aisle 3", False], + }, +] + +update_store_inventory_data = zip( + update_store_inventory_inputs, update_store_inventory_outputs +) 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. diff --git a/exercises/practice/acronym/.approaches/generator-expression/content.md b/exercises/practice/acronym/.approaches/generator-expression/content.md index f5b590ccaa6..47ec9aa8f89 100644 --- a/exercises/practice/acronym/.approaches/generator-expression/content.md +++ b/exercises/practice/acronym/.approaches/generator-expression/content.md @@ -30,7 +30,7 @@ A [`generator-expression`][generator-expression] is then used to iterate through Generator expressions are short-form [generators][generators] - lazy iterators that produce their values _on demand_, instead of saving them to memory. This generator expression is consumed by [`str.join()`][str-join], which joins the generated letters together using an empty string. -Other "seperator" strings can be used with `str.join()` - see [concept:python/string-methods]() for some additional examples. +Other "separator" strings can be used with `str.join()` - see [concept:python/string-methods]() for some additional examples. Since the generator expression and `join()` are fairly succinct, they are put directly on the `return` line rather than assigning and returning an intermediate variable for the acronym. diff --git a/exercises/practice/acronym/.approaches/introduction.md b/exercises/practice/acronym/.approaches/introduction.md index 38b606b4a26..9aaac23d6fa 100644 --- a/exercises/practice/acronym/.approaches/introduction.md +++ b/exercises/practice/acronym/.approaches/introduction.md @@ -157,4 +157,4 @@ To compare performance of the approaches, take a look at the [Performance articl [approach-map-function]: https://exercism.org/tracks/python/exercises/acronym/approaches/map-function [approach-regex-join]: https://exercism.org/tracks/python/exercises/acronym/approaches/regex-join [approach-regex-sub]: https://exercism.org/tracks/python/exercises/acronym/approaches/regex-sub -[article-performance]: https://exercism.org/tracks/python/exercises/isogram/articles/performance +[article-performance]: https://exercism.org/tracks/python/exercises/acronym/articles/performance diff --git a/exercises/practice/acronym/.approaches/list-comprehension/content.md b/exercises/practice/acronym/.approaches/list-comprehension/content.md index 3a7b0cd40fb..7e98f45c74f 100644 --- a/exercises/practice/acronym/.approaches/list-comprehension/content.md +++ b/exercises/practice/acronym/.approaches/list-comprehension/content.md @@ -25,7 +25,7 @@ As of this writing, both of these methods benchmark slower than using `str.repla A [`list comprehension`][list comprehension] is then used to iterate through the phrase and select the first letters of each word via [`bracket notation`][subscript notation]. This comprehension is passed into [`str.join()`][str-join], which unpacks the `list` of first letters and joins them together using an empty string - the acronym. -Other "seperator" strings besides an empty string can be used with `str.join()` - see [concept:python/string-methods]() for some additional examples. +Other "separator" strings besides an empty string can be used with `str.join()` - see [concept:python/string-methods]() for some additional examples. Since the comprehension and `join()` are fairly succinct, they are put directly on the `return` line rather than assigning and returning an intermediate variable for the acronym. diff --git a/exercises/practice/acronym/.approaches/regex-join/content.md b/exercises/practice/acronym/.approaches/regex-join/content.md index f6ca2f4844d..227ba06d5ea 100644 --- a/exercises/practice/acronym/.approaches/regex-join/content.md +++ b/exercises/practice/acronym/.approaches/regex-join/content.md @@ -74,7 +74,7 @@ Note that when using `finditer()`, the `Match object` has to be unpacked via `ma Generator expressions are short-form [generators][generators] - lazy iterators that produce their values _on demand_, instead of saving them to memory. This generator expression is consumed by [`str.join()`][str-join], which joins the generated letters together using an empty string. -Other "seperator" strings can be used with `str.join()` - see [concept:python/string-methods]() for some additional examples. +Other "separator" strings can be used with `str.join()` - see [concept:python/string-methods]() for some additional examples. Finally, the result of `.join()` is capitalized using the [chained][chaining] [`.upper()`][str-upper]. diff --git a/exercises/practice/affine-cipher/.docs/instructions.md b/exercises/practice/affine-cipher/.docs/instructions.md index 4eff918de78..1603dbbce91 100644 --- a/exercises/practice/affine-cipher/.docs/instructions.md +++ b/exercises/practice/affine-cipher/.docs/instructions.md @@ -4,7 +4,7 @@ Create an implementation of the affine cipher, an ancient encryption system crea The affine cipher is a type of monoalphabetic substitution cipher. Each character is mapped to its numeric equivalent, encrypted with a mathematical function and then converted to the letter relating to its new numeric value. -Although all monoalphabetic ciphers are weak, the affine cipher is much stronger than the atbash cipher, because it has many more keys. +Although all monoalphabetic ciphers are weak, the affine cipher is much stronger than the Atbash cipher, because it has many more keys. [//]: # " monoalphabetic as spelled by Merriam-Webster, compare to polyalphabetic " @@ -20,7 +20,7 @@ Where: - `i` is the letter's index from `0` to the length of the alphabet - 1. - `m` is the length of the alphabet. - For the Roman alphabet `m` is `26`. + For the Latin alphabet `m` is `26`. - `a` and `b` are integers which make up the encryption key. Values `a` and `m` must be _coprime_ (or, _relatively prime_) for automatic decryption to succeed, i.e., they have number `1` as their only common factor (more information can be found in the [Wikipedia article about coprime integers][coprime-integers]). diff --git a/exercises/practice/anagram/.docs/instructions.md b/exercises/practice/anagram/.docs/instructions.md index a7298485b3f..dca24f52627 100644 --- a/exercises/practice/anagram/.docs/instructions.md +++ b/exercises/practice/anagram/.docs/instructions.md @@ -1,13 +1,12 @@ # Instructions -Your task is to, given a target word and a set of candidate words, to find the subset of the candidates that are anagrams of the target. +Given a target word and one or more candidate words, your task is to find the candidates that are anagrams of the target. An anagram is a rearrangement of letters to form a new word: for example `"owns"` is an anagram of `"snow"`. A word is _not_ its own anagram: for example, `"stop"` is not an anagram of `"stop"`. -The target and candidates are words of one or more ASCII alphabetic characters (`A`-`Z` and `a`-`z`). -Lowercase and uppercase characters are equivalent: for example, `"PoTS"` is an anagram of `"sTOp"`, but `StoP` is not an anagram of `sTOp`. -The anagram set is the subset of the candidate set that are anagrams of the target (in any order). -Words in the anagram set should have the same letter case as in the candidate set. +The target word and candidate words are made up of one or more ASCII alphabetic characters (`A`-`Z` and `a`-`z`). +Lowercase and uppercase characters are equivalent: for example, `"PoTS"` is an anagram of `"sTOp"`, but `"StoP"` is not an anagram of `"sTOp"`. +The words you need to find should be taken from the candidate words, using the same letter case. -Given the target `"stone"` and candidates `"stone"`, `"tones"`, `"banana"`, `"tons"`, `"notes"`, `"Seton"`, the anagram set is `"tones"`, `"notes"`, `"Seton"`. +Given the target `"stone"` and the candidate words `"stone"`, `"tones"`, `"banana"`, `"tons"`, `"notes"`, and `"Seton"`, the anagram words you need to find are `"tones"`, `"notes"`, and `"Seton"`. diff --git a/exercises/practice/atbash-cipher/.docs/instructions.md b/exercises/practice/atbash-cipher/.docs/instructions.md index 21ca2ce0aa8..1e7627b1e59 100644 --- a/exercises/practice/atbash-cipher/.docs/instructions.md +++ b/exercises/practice/atbash-cipher/.docs/instructions.md @@ -1,6 +1,6 @@ # Instructions -Create an implementation of the atbash cipher, an ancient encryption system created in the Middle East. +Create an implementation of the Atbash cipher, an ancient encryption system created in the Middle East. The Atbash cipher is a simple substitution cipher that relies on transposing all the letters in the alphabet such that the resulting alphabet is backwards. The first letter is replaced with the last letter, the second with the second-last, and so on. diff --git a/exercises/practice/atbash-cipher/.meta/config.json b/exercises/practice/atbash-cipher/.meta/config.json index 9f678c6f203..5df506281a0 100644 --- a/exercises/practice/atbash-cipher/.meta/config.json +++ b/exercises/practice/atbash-cipher/.meta/config.json @@ -27,7 +27,7 @@ ".meta/example.py" ] }, - "blurb": "Create an implementation of the atbash cipher, an ancient encryption system created in the Middle East.", + "blurb": "Create an implementation of the Atbash cipher, an ancient encryption system created in the Middle East.", "source": "Wikipedia", "source_url": "https://en.wikipedia.org/wiki/Atbash" } diff --git a/exercises/practice/bank-account/.docs/instructions.append.md b/exercises/practice/bank-account/.docs/instructions.append.md index 6204bee7ddc..0f71c081eb0 100644 --- a/exercises/practice/bank-account/.docs/instructions.append.md +++ b/exercises/practice/bank-account/.docs/instructions.append.md @@ -1,5 +1,22 @@ # Instructions append +~~~~exercism/note +Python doesn't support "true" concurrency due to the [Global Interpreter Lock][GIL]. +While work is ongoing to create support for [free-threading in Python][free-threading], it is still experimental. +Current standard library solutions such as [multiprocessing][multiprocessing-module] and [threading][threading-module] are difficult to implement with the current track tooling. + + +As a result, the concurrency requirement has been set aside for this exercise. +Account operations are sequential on a single thread, and no concurrency or "race condition" tests are run. + +[GIL]: https://realpython.com/python-gil/ +[free-threading]: https://docs.python.org/3/howto/free-threading-python.html +[threading-module]: https://docs.python.org/3/library/threading.html#module-threading +[multiprocessing-module]: https://docs.python.org/3/library/multiprocessing.html#sharing-state-between-processes +~~~~ + +
+ ## 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. @@ -21,4 +38,4 @@ raise ValueError('amount must be greater than 0') # withdrawal is too big raise ValueError('amount must be less than balance') -``` \ No newline at end of file +``` diff --git a/exercises/practice/binary-search-tree/.docs/instructions.md b/exercises/practice/binary-search-tree/.docs/instructions.md index c9bbba5b96d..7625220e9a0 100644 --- a/exercises/practice/binary-search-tree/.docs/instructions.md +++ b/exercises/practice/binary-search-tree/.docs/instructions.md @@ -19,29 +19,52 @@ All data in the left subtree is less than or equal to the current node's data, a For example, if we had a node containing the data 4, and we added the data 2, our tree would look like this: +![A graph with root node 4 and a single child node 2.](https://assets.exercism.org/images/exercises/binary-search-tree/tree-4-2.svg) + +```text 4 / 2 +``` If we then added 6, it would look like this: +![A graph with root node 4 and two child nodes 2 and 6.](https://assets.exercism.org/images/exercises/binary-search-tree/tree-4-2-6.svg) + +```text 4 / \ 2 6 +``` If we then added 3, it would look like this +![A graph with root node 4, two child nodes 2 and 6, and a grandchild node 3.](https://assets.exercism.org/images/exercises/binary-search-tree/tree-4-2-6-3.svg) + +```text 4 / \ 2 6 \ 3 +``` And if we then added 1, 5, and 7, it would look like this +![A graph with root node 4, two child nodes 2 and 6, and four grandchild nodes 1, 3, 5 and 7.](https://assets.exercism.org/images/exercises/binary-search-tree/tree-4-2-6-1-3-5-7.svg) + +```text 4 / \ / \ 2 6 / \ / \ 1 3 5 7 +``` + +## Credit + +The images were created by [habere-et-dispertire][habere-et-dispertire] using [PGF/TikZ][pgf-tikz] by Till Tantau. + +[habere-et-dispertire]: https://exercism.org/profiles/habere-et-dispertire +[pgf-tikz]: https://en.wikipedia.org/wiki/PGF/TikZ diff --git a/exercises/practice/bob/.approaches/introduction.md b/exercises/practice/bob/.approaches/introduction.md index 07d68d1a1e7..b9a54b9f570 100644 --- a/exercises/practice/bob/.approaches/introduction.md +++ b/exercises/practice/bob/.approaches/introduction.md @@ -13,7 +13,7 @@ Regardless of the approach used, some things you could look out for include - Use the [`endswith`][endswith] method instead of checking the last character by index for `?`. -- Don't copy/paste the logic for determining a shout and for determing a question into determing a shouted question. +- Don't copy/paste the logic for determining a shout and for determining a question into determining a shouted question. Combine the two determinations instead of copying them. Not duplicating the code will keep the code [DRY][dry]. diff --git a/exercises/practice/bob/.meta/tests.toml b/exercises/practice/bob/.meta/tests.toml index 6304855792d..5299e2895fc 100644 --- a/exercises/practice/bob/.meta/tests.toml +++ b/exercises/practice/bob/.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. [e162fead-606f-437a-a166-d051915cea8e] description = "stating something" @@ -64,6 +71,7 @@ description = "alternate silence" [66953780-165b-4e7e-8ce3-4bcb80b6385a] description = "multiple line question" +include = false [5371ef75-d9ea-4103-bcfa-2da973ddec1b] description = "starting with whitespace" @@ -76,3 +84,7 @@ description = "other whitespace" [12983553-8601-46a8-92fa-fcaa3bc4a2a0] description = "non-question ending with whitespace" + +[2c7278ac-f955-4eb4-bf8f-e33eb4116a15] +description = "multiple line question" +reimplements = "66953780-165b-4e7e-8ce3-4bcb80b6385a" diff --git a/exercises/practice/bob/bob_test.py b/exercises/practice/bob/bob_test.py index faba5f9612e..755d5c935e4 100644 --- a/exercises/practice/bob/bob_test.py +++ b/exercises/practice/bob/bob_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/bob/canonical-data.json -# File last updated on 2023-07-20 +# File last updated on 2025-01-10 import unittest @@ -79,12 +79,6 @@ def test_prolonged_silence(self): def test_alternate_silence(self): self.assertEqual(response("\t\t\t\t\t\t\t\t\t\t"), "Fine. Be that way!") - def test_multiple_line_question(self): - self.assertEqual( - response("\nDoes this cryogenic chamber make me look fat?\nNo."), - "Whatever.", - ) - def test_starting_with_whitespace(self): self.assertEqual(response(" hmmmmmmm..."), "Whatever.") @@ -100,3 +94,8 @@ def test_non_question_ending_with_whitespace(self): self.assertEqual( response("This is a statement ending with whitespace "), "Whatever." ) + + def test_multiple_line_question(self): + self.assertEqual( + response("\nDoes this cryogenic chamber make\n me look fat?"), "Sure." + ) diff --git a/exercises/practice/change/.docs/instructions.md b/exercises/practice/change/.docs/instructions.md index 30fa567750e..5887f4cb693 100644 --- a/exercises/practice/change/.docs/instructions.md +++ b/exercises/practice/change/.docs/instructions.md @@ -1,14 +1,8 @@ # Instructions -Correctly determine the fewest number of coins to be given to a customer such that the sum of the coins' value would equal the correct amount of change. +Determine the fewest number of coins to give a customer so that the sum of their values equals the correct amount of change. -## For example +## Examples -- An input of 15 with [1, 5, 10, 25, 100] should return one nickel (5) and one dime (10) or [5, 10] -- An input of 40 with [1, 5, 10, 25, 100] should return one nickel (5) and one dime (10) and one quarter (25) or [5, 10, 25] - -## Edge cases - -- Does your algorithm work for any given set of coins? -- Can you ask for negative change? -- Can you ask for a change value smaller than the smallest coin value? +- An amount of 15 with available coin values [1, 5, 10, 25, 100] should return one coin of value 5 and one coin of value 10, or [5, 10]. +- An amount of 40 with available coin values [1, 5, 10, 25, 100] should return one coin of value 5, one coin of value 10, and one coin of value 25, or [5, 10, 25]. diff --git a/exercises/practice/change/.docs/introduction.md b/exercises/practice/change/.docs/introduction.md new file mode 100644 index 00000000000..b4f8308a1b1 --- /dev/null +++ b/exercises/practice/change/.docs/introduction.md @@ -0,0 +1,26 @@ +# Introduction + +In the mystical village of Coinholt, you stand behind the counter of your bakery, arranging a fresh batch of pastries. +The door creaks open, and in walks Denara, a skilled merchant with a keen eye for quality goods. +After a quick meal, she slides a shimmering coin across the counter, representing a value of 100 units. + +You smile, taking the coin, and glance at the total cost of the meal: 88 units. +That means you need to return 12 units in change. + +Denara holds out her hand expectantly. +"Just give me the fewest coins," she says with a smile. +"My pouch is already full, and I don't want to risk losing them on the road." + +You know you have a few options. +"We have Lumis (worth 10 units), Viras (worth 5 units), and Zenth (worth 2 units) available for change." + +You quickly calculate the possibilities in your head: + +- one Lumis (1 Γ— 10 units) + one Zenth (1 Γ— 2 units) = 2 coins total +- two Viras (2 Γ— 5 units) + one Zenth (1 Γ— 2 units) = 3 coins total +- six Zenth (6 Γ— 2 units) = 6 coins total + +"The best choice is two coins: one Lumis and one Zenth," you say, handing her the change. + +Denara smiles, clearly impressed. +"As always, you've got it right." diff --git a/exercises/practice/collatz-conjecture/.docs/instructions.md b/exercises/practice/collatz-conjecture/.docs/instructions.md index ba060483e4d..af332a810f0 100644 --- a/exercises/practice/collatz-conjecture/.docs/instructions.md +++ b/exercises/practice/collatz-conjecture/.docs/instructions.md @@ -1,29 +1,3 @@ # Instructions -The Collatz Conjecture or 3x+1 problem can be summarized as follows: - -Take any positive integer n. -If n is even, divide n by 2 to get n / 2. -If n is odd, multiply n by 3 and add 1 to get 3n + 1. -Repeat the process indefinitely. -The conjecture states that no matter which number you start with, you will always reach 1 eventually. - -Given a number n, return the number of steps required to reach 1. - -## Examples - -Starting with n = 12, the steps would be as follows: - -0. 12 -1. 6 -2. 3 -3. 10 -4. 5 -5. 16 -6. 8 -7. 4 -8. 2 -9. 1 - -Resulting in 9 steps. -So for input n = 12, the return value would be 9. +Given a positive integer, return the number of steps it takes to reach 1 according to the rules of the Collatz Conjecture. diff --git a/exercises/practice/collatz-conjecture/.docs/introduction.md b/exercises/practice/collatz-conjecture/.docs/introduction.md new file mode 100644 index 00000000000..c35bdeb67dc --- /dev/null +++ b/exercises/practice/collatz-conjecture/.docs/introduction.md @@ -0,0 +1,28 @@ +# Introduction + +One evening, you stumbled upon an old notebook filled with cryptic scribbles, as though someone had been obsessively chasing an idea. +On one page, a single question stood out: **Can every number find its way to 1?** +It was tied to something called the **Collatz Conjecture**, a puzzle that has baffled thinkers for decades. + +The rules were deceptively simple. +Pick any positive integer. + +- If it's even, divide it by 2. +- If it's odd, multiply it by 3 and add 1. + +Then, repeat these steps with the result, continuing indefinitely. + +Curious, you picked number 12 to test and began the journey: + +12 ➜ 6 ➜ 3 ➜ 10 ➜ 5 ➜ 16 ➜ 8 ➜ 4 ➜ 2 ➜ 1 + +Counting from the second number (6), it took 9 steps to reach 1, and each time the rules repeated, the number kept changing. +At first, the sequence seemed unpredictable β€” jumping up, down, and all over. +Yet, the conjecture claims that no matter the starting number, we'll always end at 1. + +It was fascinating, but also puzzling. +Why does this always seem to work? +Could there be a number where the process breaks down, looping forever or escaping into infinity? +The notebook suggested solving this could reveal something profound β€” and with it, fame, [fortune][collatz-prize], and a place in history awaits whoever could unlock its secrets. + +[collatz-prize]: https://mathprize.net/posts/collatz-conjecture/ diff --git a/exercises/practice/collatz-conjecture/.meta/config.json b/exercises/practice/collatz-conjecture/.meta/config.json index e5eda73e1df..cfed91f3bdf 100644 --- a/exercises/practice/collatz-conjecture/.meta/config.json +++ b/exercises/practice/collatz-conjecture/.meta/config.json @@ -26,6 +26,6 @@ ] }, "blurb": "Calculate the number of steps to reach 1 using the Collatz conjecture.", - "source": "An unsolved problem in mathematics named after mathematician Lothar Collatz", - "source_url": "https://en.wikipedia.org/wiki/3x_%2B_1_problem" + "source": "Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Collatz_conjecture" } diff --git a/exercises/practice/complex-numbers/.docs/instructions.md b/exercises/practice/complex-numbers/.docs/instructions.md index 50b19aedff6..2b8a7a49d82 100644 --- a/exercises/practice/complex-numbers/.docs/instructions.md +++ b/exercises/practice/complex-numbers/.docs/instructions.md @@ -1,29 +1,100 @@ # Instructions -A complex number is a number in the form `a + b * i` where `a` and `b` are real and `i` satisfies `i^2 = -1`. +A **complex number** is expressed in the form `z = a + b * i`, where: -`a` is called the real part and `b` is called the imaginary part of `z`. -The conjugate of the number `a + b * i` is the number `a - b * i`. -The absolute value of a complex number `z = a + b * i` is a real number `|z| = sqrt(a^2 + b^2)`. The square of the absolute value `|z|^2` is the result of multiplication of `z` by its complex conjugate. +- `a` is the **real part** (a real number), -The sum/difference of two complex numbers involves adding/subtracting their real and imaginary parts separately: -`(a + i * b) + (c + i * d) = (a + c) + (b + d) * i`, -`(a + i * b) - (c + i * d) = (a - c) + (b - d) * i`. +- `b` is the **imaginary part** (also a real number), and -Multiplication result is by definition -`(a + i * b) * (c + i * d) = (a * c - b * d) + (b * c + a * d) * i`. +- `i` is the **imaginary unit** satisfying `i^2 = -1`. -The reciprocal of a non-zero complex number is -`1 / (a + i * b) = a/(a^2 + b^2) - b/(a^2 + b^2) * i`. +## Operations on Complex Numbers -Dividing a complex number `a + i * b` by another `c + i * d` gives: -`(a + i * b) / (c + i * d) = (a * c + b * d)/(c^2 + d^2) + (b * c - a * d)/(c^2 + d^2) * i`. +### Conjugate -Raising e to a complex exponent can be expressed as `e^(a + i * b) = e^a * e^(i * b)`, the last term of which is given by Euler's formula `e^(i * b) = cos(b) + i * sin(b)`. +The conjugate of the complex number `z = a + b * i` is given by: -Implement the following operations: +```text +zc = a - b * i +``` -- addition, subtraction, multiplication and division of two complex numbers, -- conjugate, absolute value, exponent of a given complex number. +### Absolute Value -Assume the programming language you are using does not have an implementation of complex numbers. +The absolute value (or modulus) of `z` is defined as: + +```text +|z| = sqrt(a^2 + b^2) +``` + +The square of the absolute value is computed as the product of `z` and its conjugate `zc`: + +```text +|z|^2 = z * zc = a^2 + b^2 +``` + +### Addition + +The sum of two complex numbers `z1 = a + b * i` and `z2 = c + d * i` is computed by adding their real and imaginary parts separately: + +```text +z1 + z2 = (a + b * i) + (c + d * i) + = (a + c) + (b + d) * i +``` + +### Subtraction + +The difference of two complex numbers is obtained by subtracting their respective parts: + +```text +z1 - z2 = (a + b * i) - (c + d * i) + = (a - c) + (b - d) * i +``` + +### Multiplication + +The product of two complex numbers is defined as: + +```text +z1 * z2 = (a + b * i) * (c + d * i) + = (a * c - b * d) + (b * c + a * d) * i +``` + +### Reciprocal + +The reciprocal of a non-zero complex number is given by: + +```text +1 / z = 1 / (a + b * i) + = a / (a^2 + b^2) - b / (a^2 + b^2) * i +``` + +### Division + +The division of one complex number by another is given by: + +```text +z1 / z2 = z1 * (1 / z2) + = (a + b * i) / (c + d * i) + = (a * c + b * d) / (c^2 + d^2) + (b * c - a * d) / (c^2 + d^2) * i +``` + +### Exponentiation + +Raising _e_ (the base of the natural logarithm) to a complex exponent can be expressed using Euler's formula: + +```text +e^(a + b * i) = e^a * e^(b * i) + = e^a * (cos(b) + i * sin(b)) +``` + +## Implementation Requirements + +Given that you should not use built-in support for complex numbers, implement the following operations: + +- **addition** of two complex numbers +- **subtraction** of two complex numbers +- **multiplication** of two complex numbers +- **division** of two complex numbers +- **conjugate** of a complex number +- **absolute value** of a complex number +- **exponentiation** of _e_ (the base of the natural logarithm) to a complex number diff --git a/exercises/practice/crypto-square/.meta/tests.toml b/exercises/practice/crypto-square/.meta/tests.toml index 085d142eadb..94ef0819fe8 100644 --- a/exercises/practice/crypto-square/.meta/tests.toml +++ b/exercises/practice/crypto-square/.meta/tests.toml @@ -32,3 +32,8 @@ description = "8 character plaintext results in 3 chunks, the last one with a tr [fbcb0c6d-4c39-4a31-83f6-c473baa6af80] description = "54 character plaintext results in 7 chunks, the last two with trailing spaces" +include = false + +[33fd914e-fa44-445b-8f38-ff8fbc9fe6e6] +description = "54 character plaintext results in 8 chunks, the last two with trailing spaces" +reimplements = "fbcb0c6d-4c39-4a31-83f6-c473baa6af80" diff --git a/exercises/practice/crypto-square/crypto_square_test.py b/exercises/practice/crypto-square/crypto_square_test.py index 97630a67501..5703ccd8193 100644 --- a/exercises/practice/crypto-square/crypto_square_test.py +++ b/exercises/practice/crypto-square/crypto_square_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/crypto-square/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2025-06-20 import unittest @@ -47,7 +47,7 @@ def test_8_character_plaintext_results_in_3_chunks_the_last_one_with_a_trailing_ expected = "clu hlt io " self.assertEqual(cipher_text(value), expected) - def test_54_character_plaintext_results_in_7_chunks_the_last_two_with_trailing_spaces( + def test_54_character_plaintext_results_in_8_chunks_the_last_two_with_trailing_spaces( self, ): value = "If man was meant to stay on the ground, god would have given us roots." diff --git a/exercises/practice/darts/.approaches/booleans-as-ints/content.md b/exercises/practice/darts/.approaches/booleans-as-ints/content.md new file mode 100644 index 00000000000..d3c1541d2a6 --- /dev/null +++ b/exercises/practice/darts/.approaches/booleans-as-ints/content.md @@ -0,0 +1,36 @@ +# Using Boolean Values as Integers + + +```python +def score(x_coord, y_coord): + radius = (x_coord**2 + y_coord**2) + return (radius<=1)*5 + (radius<=25)*4 + (radius<=100)*1 +``` + + +In Python, the [Boolean values `True` and `False` are _subclasses_ of `int`][bools-as-ints] and can be interpreted as `0` (False) and `1` (True) in a mathematical context. +This approach leverages that interpretation by checking which areas the throw falls into and multiplying each Boolean `int` by a scoring multiple. +For example, a throw that lands on the 25 (_or 5 if using `math.sqrt(x**2 + y**2)`_) circle should have a score of 5: + +```python +>>> (False)*5 + (True)*4 + (True)*1 +5 +``` + + +This makes for very compact code and has the added boost of not requiring any `loops` or additional data structures. +However, it is considered bad form to rely on Boolean interpretation. +Instead, the Python documentation recommends an explicit conversion to `int`: + + +```python +def score(x_coord, y_coord): + radius = (x_coord**2 + y_coord**2) + return int(radius<=1)*5 + int(radius<=25)*4 + int(radius<=100)*1 +``` + +Beyond that recommendation, the terseness of this approach might be harder to reason about or decode β€” especially if a programmer is coming from a programming langauge that does not treat Boolean values as `ints`. +Despite the "radius" variable name, it is also more difficult to relate the scoring "rings" of the Dartboard to the values being checked and calculated in the `return` statement. +If using this code in a larger program, it would be strongly recommended that a docstring be provided to explain the Dartboard rings, scoring rules, and the corresponding scores. + +[bools-as-ints]: https://docs.python.org/3/library/stdtypes.html#boolean-type-bool \ No newline at end of file diff --git a/exercises/practice/darts/.approaches/booleans-as-ints/snippet.txt b/exercises/practice/darts/.approaches/booleans-as-ints/snippet.txt new file mode 100644 index 00000000000..ec7dcfabbfc --- /dev/null +++ b/exercises/practice/darts/.approaches/booleans-as-ints/snippet.txt @@ -0,0 +1,3 @@ +def score(x_coord, y_coord): + radius = (x_coord**2 + y_coord**2) + return (radius<=1)*5 + (radius<=25)*4 +(radius<=100)*1 \ No newline at end of file diff --git a/exercises/practice/darts/.approaches/config.json b/exercises/practice/darts/.approaches/config.json new file mode 100644 index 00000000000..77f331bfce0 --- /dev/null +++ b/exercises/practice/darts/.approaches/config.json @@ -0,0 +1,50 @@ +{ + "introduction": { + "authors": ["bethanyg"], + "contributors": [] + }, + "approaches": [ + { + "uuid": "7d78f598-8b4c-4f7f-89e1-e8644e934a4c", + "slug": "if-statements", + "title": "Use If Statements", + "blurb": "Use if-statements to check scoring boundaries for a dart throw.", + "authors": ["bethanyg"] + }, + { + "uuid": "f8f5533a-09d2-4b7b-9dec-90f268bfc03b", + "slug": "tuple-and-loop", + "title": "Use a Tuple & Loop through Scores", + "blurb": "Score the Dart throw by looping through a tuple of scores.", + "authors": ["bethanyg"] + }, + { + "uuid": "a324f99e-15bb-43e0-9181-c1652094bc4f", + "slug": "match-case", + "title": "Use Structural Pattern Matching ('Match-Case')", + "blurb": "Use a Match-Case (Structural Pattern Matching) to score the dart throw.)", + "authors": ["bethanyg"] + }, + { + "uuid": "966bd2dd-c4fd-430b-ad77-3a304dedd82e", + "slug": "dict-and-generator", + "title": "Use a Dictionary with a Generator Expression", + "blurb": "Use a generator expression looping over a scoring dictionary, getting the max score for the dart throw.", + "authors": ["bethanyg"] + }, + { + "uuid": "5b087f50-31c5-4b84-9116-baafd3a30ed6", + "slug": "booleans-as-ints", + "title": "Use Boolean Values as Integers", + "blurb": "Use True and False as integer values to calculate the score of the dart throw.", + "authors": ["bethanyg"] + }, + { + "uuid": "0b2dbcd3-f0ac-45f7-af75-3451751fd21f", + "slug": "dict-and-dict-get", + "title": "Use a Dictionary with dict.get", + "blurb": "Loop over a dictionary and retrieve score via dct.get.", + "authors": ["bethanyg"] + } + ] +} \ No newline at end of file diff --git a/exercises/practice/darts/.approaches/dict-and-dict-get/content.md b/exercises/practice/darts/.approaches/dict-and-dict-get/content.md new file mode 100644 index 00000000000..a3c5bc2ac58 --- /dev/null +++ b/exercises/practice/darts/.approaches/dict-and-dict-get/content.md @@ -0,0 +1,72 @@ +# Using a Dictionary and `dict.get()` + + +```python +def score(x_coord, y_coord): + point = (x_coord**2 + y_coord**2) + scores = { + point <= 100: 1, + point <= 25: 5, + point <= 1: 10 + } + + return scores.get(True, 0) +``` + +At first glance, this approach looks similar to the [Booleans as Integers][approach-boolean-values-as-integers] approach, due to the Boolean evaluation used in the dictionary keys. +However, this approach is **not** interpreting Booleans as integers and is instead exploiting three key properties of [dictionaries][dicts]: + + +1. [Keys must be hashable][hashable-keys] β€” in other words, keys have to be _unique_. +2. Insertion order is preserved (_as of `Python 3.7`_), and evaluation/iteration happens in insertion order. +3. Duplicate keys _overwrite_ existing keys. + If the first key is `True` and the third key is `True`, the _value_ from the third key will overwrite the value from the first key. + +Finally, the `return` line uses [`dict.get()`][dict-get] to `return` a default value of 0 when a throw is outside the existing circle radii. +To see this in action, you can view this code on [Python Tutor][dict-get-python-tutor]. + + +Because of the listed dictionary qualities, **_order matters_**. +This approach depends on the outermost scoring circle containing all smaller circles and that +checks proceed from largest --> smallest circle. +Iterating in the opposite direction will not resolve to the correct score. +The following code variations do not pass the exercise tests: + + +```python + +def score(x_coord, y_coord): + point = (x_coord**2 + y_coord**2) + scores = { + point <= 1: 10, + point <= 25: 5, + point <= 100: 1, + } + + return scores.get(True, 0) + + #OR# + +def score(x_coord, y_coord): + point = (x_coord**2 + y_coord**2) + scores = { + point <= 25: 5, + point <= 1: 10, + point <= 100: 1, + } + + return scores.get(True, 0) + +``` + +While this approach is a _very clever_ use of dictionary properties, it is likely to be very hard to reason about for those who are not deeply knowledgeable. +Even those experienced in Python might take longer than usual to figure out what is happening in the code. +Extensibility could also be error-prone due to needing a strict order for the `dict` keys. + +This approach offers no space or speed advantages over using `if-statements` or other strategies, so is not recommended for use beyond a learning context. + +[approach-boolean-values-as-integers]: https://exercism.org/tracks/python/exercises/darts/approaches/boolean-values-as-integers +[dicts]: https://docs.python.org/3/library/stdtypes.html#mapping-types-dict +[dict-get]: https://docs.python.org/3/library/stdtypes.html#dict.get +[dict-get-python-tutor]: https://pythontutor.com/render.html#code=def%20score%28x_coord,%20y_coord%29%3A%0A%20%20%20%20point%20%3D%20%28x_coord**2%20%2B%20y_coord**2%29%0A%20%20%20%20scores%20%3D%20%7B%0A%20%20%20%20%20%20%20%20point%20%3C%3D%20100%3A%201,%0A%20%20%20%20%20%20%20%20point%20%3C%3D%2025%3A%205,%0A%20%20%20%20%20%20%20%20point%20%3C%3D%201%3A%2010%0A%20%20%20%20%7D%0A%20%20%20%20%0A%20%20%20%20return%20scores.get%28True,%200%29%0A%20%20%20%20%0Aprint%28score%281,3%29%29&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false +[hashable-keys]: https://www.pythonmorsels.com/what-are-hashable-objects/#dictionary-keys-must-be-hashable diff --git a/exercises/practice/darts/.approaches/dict-and-dict-get/snippet.txt b/exercises/practice/darts/.approaches/dict-and-dict-get/snippet.txt new file mode 100644 index 00000000000..6d496f54d33 --- /dev/null +++ b/exercises/practice/darts/.approaches/dict-and-dict-get/snippet.txt @@ -0,0 +1,5 @@ +def score(x_coord, y_coord): + point = (x_coord**2 + y_coord**2) + scores = {point <= 100: 1, point <= 25: 5, point <= 1: 10} + + return scores.get(True, 0) \ No newline at end of file diff --git a/exercises/practice/darts/.approaches/dict-and-generator/content.md b/exercises/practice/darts/.approaches/dict-and-generator/content.md new file mode 100644 index 00000000000..30ffeac1eb0 --- /dev/null +++ b/exercises/practice/darts/.approaches/dict-and-generator/content.md @@ -0,0 +1,69 @@ +# Use a Dictionary and a Generator Expression + +```python +def score(x_coord, y_coord): + throw = x_coord**2 + y_coord**2 + rules = {1: 10, 25: 5, 100: 1, 200: 0} + + return max(point for distance, point in + rules.items() if throw <= distance) +``` + + +This approach is very similar to the [tuple and loop][approach-tuple-and-loop] approach, but iterates over [`dict.items()`][dict-items] and writes the `loop` as a [`generator-expression`][generator-expression] inside `max()`. +In cases where the scoring circles overlap, `max()` will return the maximum score available for the throw. +The generator expression inside `max()` is the equivalent of using a `for-loop` and a variable to determine the max score: + + +```python +def score(x_coord, y_coord): + throw = x_coord**2 + y_coord**2 + rules = {1: 10, 25: 5, 100: 1} + max_score = 0 + + for distance, point in rules.items(): + if throw <= distance and point > max_score: + max_score = point + return max_score +``` + + +A `list` or `tuple` can also be used in place of `max()`, but then requires an index to return the max score: + +```python +def score(x_coord, y_coord): + throw = x_coord**2 + y_coord**2 + rules = {1: 10, 25: 5, 100: 1, 200: 0} + + return [point for distance, point in + rules.items() if throw <= distance][0] #<-- have to specify index 0. + +#OR# + +def score(x_coord, y_coord): + throw = x_coord**2 + y_coord**2 + rules = {1: 10, 25: 5, 100: 1, 200: 0} + + return tuple(point for distance, point in + rules.items() if throw <= distance)[0] +``` + + +This solution can even be reduced to a "one-liner". +However, this is not performant, and is difficult to read: + +```python +def score(x_coord, y_coord): + return max(point for distance, point in + {1: 10, 25: 5, 100: 1, 200: 0}.items() if + (x_coord**2 + y_coord**2) <= distance) +``` + +While all of these variations do pass the tests, they suffer from even more over-engineering/performance caution than the earlier tuple and loop approach (_although for the data in this problem, the performance hit is slight_). +Additionally, the dictionary will take much more space in memory than using a `tuple` of tuples to hold scoring values. +In some circumstances, these variations might also be harder to reason about for those not familiar with `generator-expressions` or `list comprehensions`. + + +[approach-tuple-and-loop]: https://exercism.org/tracks/python/exercises/darts/approaches/tuple-and-loop +[dict-items]: https://docs.python.org/3/library/stdtypes.html#dict.items +[generator-expression]: https://dbader.org/blog/python-generator-expressions diff --git a/exercises/practice/darts/.approaches/dict-and-generator/snippet.txt b/exercises/practice/darts/.approaches/dict-and-generator/snippet.txt new file mode 100644 index 00000000000..f6649cf3a92 --- /dev/null +++ b/exercises/practice/darts/.approaches/dict-and-generator/snippet.txt @@ -0,0 +1,8 @@ +def score(x_coord, y_coord): + length = x_coord**2 + y_coord**2 + rules = {1.0: 10, 25.0: 5, 100.0: 1, 200: 0} + score = max(point for + distance, point in + rules.items() if length <= distance) + + return score \ No newline at end of file diff --git a/exercises/practice/darts/.approaches/if-statements/content.md b/exercises/practice/darts/.approaches/if-statements/content.md new file mode 100644 index 00000000000..40e2886ddb5 --- /dev/null +++ b/exercises/practice/darts/.approaches/if-statements/content.md @@ -0,0 +1,73 @@ +# Use `if-statements` + + +```python +import math + +# Checks scores from the center --> edge. +def score(x_coord, y_coord): + distance = math.sqrt(x_coord**2 + y_coord**2) + + if distance <= 1: return 10 + if distance <= 5: return 5 + if distance <= 10: return 1 + + return 0 +``` + +This approach uses [concept:python/conditionals]() to check the boundaries for each scoring ring, returning the corresponding score. +Calculating the euclidian distance is assigned to the variable "distance" to avoid having to re-calculate it for every if check. +Because the `if-statements` are simple and readable, they're written on one line to shorten the function body. +Zero is returned if no other check is true. + + +To avoid importing the `math` module (_for a very very slight speedup_), (x**2 +y**2) can be calculated instead, and the scoring rings can be adjusted to 1, 25, and 100: + + +```python +# Checks scores from the center --> edge. +def score(x_coord, y_coord): + distance = x_coord**2 + y_coord**2 + + if distance <= 1: return 10 + if distance <= 25: return 5 + if distance <= 100: return 1 + + return 0 +``` + + +# Variation 1: Check from Edge to Center Using Upper and Lower Bounds + + +```python +import math + +# Checks scores from the edge --> center +def score(x_coord, y_coord): + distance = math.sqrt(x_coord**2 + y_coord**2) + + if distance > 10: return 0 + if 5 < distance <= 10: return 1 + if 1 < distance <= 5: return 5 + + return 10 +``` + +This variant checks from the edge moving inward, checking both a lower and upper bound due to the overlapping scoring circles in this direction. + +Scores for any of these solutions can also be assigned to a variable to avoid multiple `returns`, but this isn't really necessary: + +```python +# Checks scores from the edge --> center +def score(x_coord, y_coord): + distance = x_coord**2 + y_coord**2 + points = 10 + + if distance > 100: points = 0 + if 25 < distance <= 100: points = 1 + if 1 < distance <= 25: points = 5 + + return points +``` + diff --git a/exercises/practice/darts/.approaches/if-statements/snippet.txt b/exercises/practice/darts/.approaches/if-statements/snippet.txt new file mode 100644 index 00000000000..18537416e2f --- /dev/null +++ b/exercises/practice/darts/.approaches/if-statements/snippet.txt @@ -0,0 +1,8 @@ +import math + +def score(x_coord, y_coord): + distance = math.sqrt(x_coord**2 + y_coord**2) + if distance <= 1: return 10 + if distance <= 5: return 5 + if distance <= 10: return 1 + return 0 \ No newline at end of file diff --git a/exercises/practice/darts/.approaches/introduction.md b/exercises/practice/darts/.approaches/introduction.md new file mode 100644 index 00000000000..cf7c6a23dd5 --- /dev/null +++ b/exercises/practice/darts/.approaches/introduction.md @@ -0,0 +1,146 @@ +# Introduction + + +There are multiple Pythonic ways to solve the Darts exercise. +Among them are: + +- Using `if-statements` +- Using a `tuple` (or `list` or `dict`) and a `for-loop` +- Using a `dict` (or `tuple` or `list`) and a `generator-expression` +- Using `boolean` values as `ints` +- Using a `dict` and `dict.get()` +- Using `match/case` (_Python 3.10+ only_) + +
+ +## General guidance + +The goal of the Darts exercise is to score a single throw in a Darts game. +The scoring areas are _concentric circles_, so boundary values need to be checked in order to properly score a throw. +The key is to determine how far from the center the dart lands (_by calculating sqrt(x**2 + y**2), or a variation_) and then determine what scoring ring it falls into. + + +**_Order matters_** - each bigger target circle contains all the smaller circles, so the most straightforward solution is to check the smallest circle first. +Otherwise, you must box your scoring by checking both a _lower bound_ and an _upper bound_. + + +Darts that fall on a _boundary_ are scored based on the area below the line (_closer to center_), so checking `<=` or `>=` is advised. + + +## Approach: Using `if` statements + + +```python +import math + +# Checks scores from the center --> edge. +def score(x_coord, y_coord): + distance = math.sqrt(x_coord**2 + y_coord**2) + + if distance <= 1: return 10 + if distance <= 5: return 5 + if distance <= 10: return 1 + + return 0 +``` + + +This approach uses [concept:python/conditionals]() to check the boundaries for each scoring ring, returning the corresponding score. +For more details, see the [if statements][approach-if-statements] approach. + + +## Approach: Using a `tuple` and a `loop` + +```python +def score(x_coord, y_coord): + throw = x_coord**2 + y_coord**2 + rules = (1, 10), (25, 5), (100, 1), (200, 0) + + for distance, points in rules: + if throw <= distance: + return points +``` + + +This approach uses a loop to iterate through the _rules_ `tuple`, unpacking each `distance` and corresponding`score`. +For more details, see the [tuple and loop][approach-tuple-and-loop] approach. + + +## Approach: Using a `dict` with a `generator-expression` + +```python +def score(x_coord, y_coord): + throw = x_coord**2 + y_coord**2 + rules = {1: 10, 25: 5, 100: 1, 200: 0} + + return max(point for distance, point in + rules.items() if throw <= distance) +``` + +This approach is very similar to the [tuple and loop][approach-tuple-and-loop] approach, but iterates over [`dict.items()`][dict-items]. +For more information, see the [dict and generator][approach-dict-and-generator] approach. + + +## Approach: Using Boolean Values as Integers + +```python +def score(x_coord, y_coord): + radius = (x_coord**2 + y_coord**2) + return (radius<=1)*5 + (radius<=25)*4 +(radius<=100)*1 +``` + + +This approach exploits the fact that Boolean values are an integer subtype in Python. +For more information, see the [boolean values as integers][approach-boolean-values-as-integers] approach. + + +## Approach: Using a `Dictionary` and `dict.get()` + +```python +def score(x_coord, y_coord): + point = (x_coord**2 + y_coord**2) + scores = { + point <= 100: 1, + point <= 25: 5, + point <= 1: 10 + } + + return scores.get(True, 0) +``` + +This approach uses a dictionary to hold the distance --> scoring mappings and `dict.get()` to retrieve the correct points value. +For more details, read the [`Dictionary and dict.get()`][approach-dict-and-dict-get] approach. + + +## Approach: Using `match/case` (structural pattern matching) + +```python +from math import hypot, ceil + + +def score(x, y): + match ceil(hypot(x, y)): + case 0 | 1: return 10 + case 2 | 3 | 4 | 5: return 5 + case 6 | 7 | 8 | 9 | 10: return 1 + case _: return 0 +``` + + +This approach uses `Python 3.10`'s structural pattern matching with `return` values on the same line as `case`. +A fallthrough case (`_`) is used if the dart throw is outside the outer circle of the target (_greater than 10_). +For more details, see the [structural pattern matching][approach-struct-pattern-matching] approach. + + +## Which approach to use? + +Many of these approaches are a matter of personal preference - there are not significant memory or performance differences. +Although a strong argument could be made for simplicity and clarity β€” many listed solutions (_while interesting_) are harder to reason about or are over-engineered for the current scope of the exercise. + +[approach-boolean-values-as-integers]: https://exercism.org/tracks/python/exercises/darts/approaches/boolean-values-as-integers +[approach-dict-and-dict-get]: https://exercism.org/tracks/python/exercises/darts/approaches/dict-and-dict-get +[approach-dict-and-generator]: https://exercism.org/tracks/python/exercises/darts/approaches/dict-and-generator +[approach-if-statements ]: https://exercism.org/tracks/python/exercises/darts/approaches/if-statements +[approach-struct-pattern-matching]: https://exercism.org/tracks/python/exercises/darts/approaches/struct-pattern-matching +[approach-tuple-and-loop]: https://exercism.org/tracks/python/exercises/darts/approaches/tuple-and-loop +[dict-items]: https://docs.python.org/3/library/stdtypes.html#dict.items diff --git a/exercises/practice/darts/.approaches/match-case/content.md b/exercises/practice/darts/.approaches/match-case/content.md new file mode 100644 index 00000000000..04430a5dc52 --- /dev/null +++ b/exercises/practice/darts/.approaches/match-case/content.md @@ -0,0 +1,85 @@ +# Use `match/case` (Structural Pattern Matching) + + +```python +from math import hypot, ceil + + +def score(x, y): + throw = ceil(hypot(x, y)) + + match throw: + case 0 | 1: return 10 + case 2 | 3 | 4 | 5: return 5 + case 6 | 7 | 8 | 9 | 10: return 1 + case _: return 0 + +#OR# + +def score(x, y): + match ceil(hypot(x, y)): + case 0 | 1: return 10 + case 2 | 3 | 4 | 5: return 5 + case 6 | 7 | 8 | 9 | 10: return 1 + case _: return 0 +``` + +This approach uses `Python 3.10`'s [`structural pattern matching`][structural-pattern-matching] with `return` values on the same line as `case`. +Because the match is numeric, each case explicitly lists allowed values using the `|` (OR) operator. +A fallthrough case (`_`) is used if the dart throw is greater than 10 (_the outer circle radius of the target_). +This is equivalent to using `if-statements` to check throw values although some might argue it is clearer to read. +An `if-statement` equivalent would be: + +```python +from math import hypot, ceil + + +def score(x, y): + throw = ceil(hypot(x, y)) + + if throw in (0, 1): return 10 + if throw in (2, 3, 4, 5): return 5 + if throw in (6, 7, 8, 9, 10): return 1 + + return 0 +``` + +One can also use `<`, `>`, or `<=` and `>=` in structural pattern matching, although the syntax becomes almost identical to using them with `if-statements`, but more verbose: + + +```python +from math import hypot, ceil + + +def score(x, y): + throw = ceil(hypot(x, y)) + + match throw: + case throw if throw <= 1: return 10 + case throw if throw <= 5: return 5 + case throw if throw <= 10: return 1 + case _: return 0 +``` + + +Finally, one can use an [assignment expression][assignment-expression] or [walrus operator][walrus] to calculate the throw value rather than calculating and assigning a variable on a separate line. +This isn't necessary (_the first variations shows this clearly_) and might be harder to reason about/understand for some programmers: + + +```python +from math import hypot, ceil + +def score(x, y): + match throw := ceil(hypot(x, y)): + case throw if throw <= 1: return 10 + case throw if throw <=5: return 5 + case throw if throw <=10: return 1 + case _: return 0 +``` + +Using structural pattern matching for this exercise doesn't offer any clear performance advantages over the `if-statement`, but might be "cleaner", more "organized looking", or easier for others to scan/read. + + +[assignment-expression]: https://docs.python.org/3/reference/expressions.html#grammar-token-python-grammar-assignment_expression +[structural-pattern-matching]: https://peps.python.org/pep-0636/ +[walrus]: https://peps.python.org/pep-0572/ diff --git a/exercises/practice/darts/.approaches/match-case/snippet.txt b/exercises/practice/darts/.approaches/match-case/snippet.txt new file mode 100644 index 00000000000..e66b5382b21 --- /dev/null +++ b/exercises/practice/darts/.approaches/match-case/snippet.txt @@ -0,0 +1,8 @@ +from math import hypot, ceil + +def score(x, y): + match ceil(hypot(x, y)): + case 0 | 1: return 10 + case 2 | 3 | 4 | 5: return 5 + case 6 | 7 | 8 | 9 | 10: return 1 + case _: return 0 \ No newline at end of file diff --git a/exercises/practice/darts/.approaches/tuple-and-loop/content.md b/exercises/practice/darts/.approaches/tuple-and-loop/content.md new file mode 100644 index 00000000000..042b9e88ae9 --- /dev/null +++ b/exercises/practice/darts/.approaches/tuple-and-loop/content.md @@ -0,0 +1,55 @@ +# Use a tuple with a loop + +```python +def score(x_coord, y_coord): + throw = x_coord**2 + y_coord**2 + rules = (1, 10), (25, 5), (100, 1), (200, 0) + + for distance, points in rules: + if throw <= distance: + return points +``` + +This approach uses a loop to iterate through the _rules_ `tuple`, unpacking each (`distance`, `points`) pair (_For a little more on unpacking, see [Tuple Unpacking Improves Python Code Readability][tuple-unpacking]_). +If the calculated distance of the throw is less than or equal to a given distance, the score for that region is returned. +A `list` of `lists`, a `list` of `tuples`, or a dictionary could be used here to the same effect: + +```python +def score(x_coord, y_coord): + throw = x_coord**2 + y_coord**2 + rules = [[1, 10], [25, 5], [100, 1]] + + for distance, points in rules: + if throw <= distance: + return points + + return 0 + +#OR# + +def score(x_coord, y_coord): + throw = x_coord**2 + y_coord**2 + rules = [(1, 10), (25, 5), (100, 1), (200, 0)] + + for distance, points in rules: + if throw <= distance: + return points + +#OR# + +def score(x_coord, y_coord): + throw = x_coord**2 + y_coord**2 + rules = {1: 10, 25: 5, 100: 1} + + for distance, points in rules.items(): + if throw <= distance: + return points + + return 0 +``` + +This approach would work nicely in a scenario where you expect to be adding more scoring "rings", since it is cleaner to edit the data structure than to add additional `if-statements` as you would have to in the [`if-statement` approach][approach-if-statements ]. +For the three rings as defined by the current exercise, it is a bit over-engineered to use a data structure + `loop`, and results in a slight (_**very** slight_) slowdown over using `if-statements`. + +[tuple-unpacking]: https://treyhunner.com/2018/03/tuple-unpacking-improves-python-code-readability/#Unpacking_in_a_for_loop +[approach-if-statements ]: https://exercism.org/tracks/python/exercises/darts/approaches/if-statements diff --git a/exercises/practice/darts/.approaches/tuple-and-loop/snippet.txt b/exercises/practice/darts/.approaches/tuple-and-loop/snippet.txt new file mode 100644 index 00000000000..ad505005263 --- /dev/null +++ b/exercises/practice/darts/.approaches/tuple-and-loop/snippet.txt @@ -0,0 +1,7 @@ +def score(x_coord, y_coord): + distance = x_coord**2 + y_coord**2 + rules = (1.0, 10), (25.0, 5), (100.0, 1), (200.0, 0) + + for distance, point in rules: + if length <= distance: + return point \ No newline at end of file diff --git a/exercises/practice/dominoes/.docs/instructions.md b/exercises/practice/dominoes/.docs/instructions.md index 1ced9f6448f..75055b9e892 100644 --- a/exercises/practice/dominoes/.docs/instructions.md +++ b/exercises/practice/dominoes/.docs/instructions.md @@ -2,7 +2,9 @@ Make a chain of dominoes. -Compute a way to order a given set of dominoes in such a way that they form a correct domino chain (the dots on one half of a stone match the dots on the neighboring half of an adjacent stone) and that dots on the halves of the stones which don't have a neighbor (the first and last stone) match each other. +Compute a way to order a given set of domino stones so that they form a correct domino chain. +In the chain, the dots on one half of a stone must match the dots on the neighboring half of an adjacent stone. +Additionally, the dots on the halves of the stones without neighbors (the first and last stone) must match each other. For example given the stones `[2|1]`, `[2|3]` and `[1|3]` you should compute something like `[1|2] [2|3] [3|1]` or `[3|2] [2|1] [1|3]` or `[1|3] [3|2] [2|1]` etc, where the first and last numbers are the same. diff --git a/exercises/practice/dominoes/.docs/introduction.md b/exercises/practice/dominoes/.docs/introduction.md new file mode 100644 index 00000000000..df248c2116e --- /dev/null +++ b/exercises/practice/dominoes/.docs/introduction.md @@ -0,0 +1,13 @@ +# Introduction + +In Toyland, the trains are always busy delivering treasures across the city, from shiny marbles to rare building blocks. +The tracks they run on are made of colorful domino-shaped pieces, each marked with two numbers. +For the trains to move, the dominoes must form a perfect chain where the numbers match. + +Today, an urgent delivery of rare toys is on hold. +You've been handed a set of track pieces to inspect. +If they can form a continuous chain, the train will be on its way, bringing smiles across Toyland. +If not, the set will be discarded, and another will be tried. + +The toys are counting on you to solve this puzzle. +Will the dominoes connect the tracks and send the train rolling, or will the set be left behind? diff --git a/exercises/practice/dot-dsl/.docs/instructions.md b/exercises/practice/dot-dsl/.docs/instructions.md index b3a63996d82..5e65ebef943 100644 --- a/exercises/practice/dot-dsl/.docs/instructions.md +++ b/exercises/practice/dot-dsl/.docs/instructions.md @@ -22,7 +22,7 @@ Write a Domain Specific Language similar to the Graphviz dot language. Our DSL is similar to the Graphviz dot language in that our DSL will be used to create graph data structures. However, unlike the DOT Language, our DSL will be an internal DSL for use only in our language. -More information about the difference between internal and external DSLs can be found [here][fowler-dsl]. +[Learn more about the difference between internal and external DSLs][fowler-dsl]. [dsl]: https://en.wikipedia.org/wiki/Domain-specific_language [dot-language]: https://en.wikipedia.org/wiki/DOT_(graph_description_language) diff --git a/exercises/practice/eliuds-eggs/.docs/introduction.md b/exercises/practice/eliuds-eggs/.docs/introduction.md index 49eaffd8bc3..2b2e5c43d8b 100644 --- a/exercises/practice/eliuds-eggs/.docs/introduction.md +++ b/exercises/practice/eliuds-eggs/.docs/introduction.md @@ -12,36 +12,54 @@ The position information encoding is calculated as follows: 2. Convert the number from binary to decimal. 3. Show the result on the display. -Example 1: +## Example 1 + +![Seven individual nest boxes arranged in a row whose first, third, fourth and seventh nests each have a single egg.](https://assets.exercism.org/images/exercises/eliuds-eggs/example-1-coop.svg) ```text -Chicken Coop: _ _ _ _ _ _ _ |E| |E|E| | |E| +``` + +### Resulting Binary + +![1011001](https://assets.exercism.org/images/exercises/eliuds-eggs/example-1-binary.svg) + +```text + _ _ _ _ _ _ _ +|1|0|1|1|0|0|1| +``` -Resulting Binary: - 1 0 1 1 0 0 1 +### Decimal number on the display -Decimal number on the display: 89 -Actual eggs in the coop: +### Actual eggs in the coop + 4 + +## Example 2 + +![Seven individual nest boxes arranged in a row where only the fourth nest has an egg.](https://assets.exercism.org/images/exercises/eliuds-eggs/example-2-coop.svg) + +```text + _ _ _ _ _ _ _ +| | | |E| | | | ``` -Example 2: +### Resulting Binary + +![0001000](https://assets.exercism.org/images/exercises/eliuds-eggs/example-2-binary.svg) ```text -Chicken Coop: - _ _ _ _ _ _ _ _ -| | | |E| | | | | + _ _ _ _ _ _ _ +|0|0|0|1|0|0|0| +``` -Resulting Binary: - 0 0 0 1 0 0 0 0 +### Decimal number on the display -Decimal number on the display: -16 +8 + +### Actual eggs in the coop -Actual eggs in the coop: 1 -``` diff --git a/exercises/practice/flatten-array/.docs/instructions.md b/exercises/practice/flatten-array/.docs/instructions.md index 89dacfa327a..b5b82713d92 100644 --- a/exercises/practice/flatten-array/.docs/instructions.md +++ b/exercises/practice/flatten-array/.docs/instructions.md @@ -1,11 +1,16 @@ # Instructions -Take a nested list and return a single flattened list with all values except nil/null. +Take a nested array of any depth and return a fully flattened array. -The challenge is to take an arbitrarily-deep nested list-like structure and produce a flattened structure without any nil/null values. +Note that some language tracks may include null-like values in the input array, and the way these values are represented varies by track. +Such values should be excluded from the flattened array. -For example: +Additionally, the input may be of a different data type and contain different types, depending on the track. -input: [1,[2,3,null,4],[null],5] +Check the test suite for details. -output: [1,2,3,4,5] +## Example + +input: `[1, [2, 6, null], [[null, [4]], 5]]` + +output: `[1, 2, 6, 4, 5]` diff --git a/exercises/practice/flatten-array/.docs/introduction.md b/exercises/practice/flatten-array/.docs/introduction.md new file mode 100644 index 00000000000..a314857465e --- /dev/null +++ b/exercises/practice/flatten-array/.docs/introduction.md @@ -0,0 +1,7 @@ +# Introduction + +A shipment of emergency supplies has arrived, but there's a problem. +To protect from damage, the items β€” flashlights, first-aid kits, blankets β€” are packed inside boxes, and some of those boxes are nested several layers deep inside other boxes! + +To be prepared for an emergency, everything must be easily accessible in one box. +Can you unpack all the supplies and place them into a single box, so they're ready when needed most? diff --git a/exercises/practice/flatten-array/.meta/tests.toml b/exercises/practice/flatten-array/.meta/tests.toml index 6300219d716..44acf175d2a 100644 --- a/exercises/practice/flatten-array/.meta/tests.toml +++ b/exercises/practice/flatten-array/.meta/tests.toml @@ -32,12 +32,32 @@ description = "null values are omitted from the final result" [c6cf26de-8ccd-4410-84bd-b9efd88fd2bc] description = "consecutive null values at the front of the list are omitted from the final result" +include = false + +[bc72da10-5f55-4ada-baf3-50e4da02ec8e] +description = "consecutive null values at the front of the array are omitted from the final result" +reimplements = "c6cf26de-8ccd-4410-84bd-b9efd88fd2bc" [382c5242-587e-4577-b8ce-a5fb51e385a1] description = "consecutive null values in the middle of the list are omitted from the final result" +include = false + +[6991836d-0d9b-4703-80a0-3f1f23eb5981] +description = "consecutive null values in the middle of the array are omitted from the final result" +reimplements = "382c5242-587e-4577-b8ce-a5fb51e385a1" [ef1d4790-1b1e-4939-a179-51ace0829dbd] description = "6 level nest list with null values" +include = false + +[dc90a09c-5376-449c-a7b3-c2d20d540069] +description = "6 level nested array with null values" +reimplements = "ef1d4790-1b1e-4939-a179-51ace0829dbd" [85721643-705a-4150-93ab-7ae398e2942d] description = "all values in nested list are null" +include = false + +[51f5d9af-8f7f-4fb5-a156-69e8282cb275] +description = "all values in nested array are null" +reimplements = "85721643-705a-4150-93ab-7ae398e2942d" diff --git a/exercises/practice/flatten-array/flatten_array_test.py b/exercises/practice/flatten-array/flatten_array_test.py index cecb3c5633f..8cd077d9ad6 100644 --- a/exercises/practice/flatten-array/flatten_array_test.py +++ b/exercises/practice/flatten-array/flatten_array_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/flatten-array/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2025-03-22 import unittest @@ -45,26 +45,26 @@ def test_null_values_are_omitted_from_the_final_result(self): expected = [1, 2] self.assertEqual(flatten(inputs), expected) - def test_consecutive_null_values_at_the_front_of_the_list_are_omitted_from_the_final_result( + def test_consecutive_null_values_at_the_front_of_the_array_are_omitted_from_the_final_result( self, ): inputs = [None, None, 3] expected = [3] self.assertEqual(flatten(inputs), expected) - def test_consecutive_null_values_in_the_middle_of_the_list_are_omitted_from_the_final_result( + def test_consecutive_null_values_in_the_middle_of_the_array_are_omitted_from_the_final_result( self, ): inputs = [1, None, None, 4] expected = [1, 4] self.assertEqual(flatten(inputs), expected) - def test_6_level_nest_list_with_null_values(self): + def test_6_level_nested_array_with_null_values(self): inputs = [0, 2, [[2, 3], 8, [[100]], None, [[None]]], -2] expected = [0, 2, 2, 3, 8, 100, -2] self.assertEqual(flatten(inputs), expected) - def test_all_values_in_nested_list_are_null(self): + def test_all_values_in_nested_array_are_null(self): inputs = [None, [[[None]]], None, None, [[None, None], None], None] expected = [] self.assertEqual(flatten(inputs), expected) 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/ diff --git a/exercises/practice/flower-field/.docs/instructions.append.md b/exercises/practice/flower-field/.docs/instructions.append.md new file mode 100644 index 00000000000..2e20d976804 --- /dev/null +++ b/exercises/practice/flower-field/.docs/instructions.append.md @@ -0,0 +1,14 @@ +# Instructions append + +## 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" a `ValueError` when the `board()` function receives malformed input. The tests will only pass if you both `raise` the `exception` and include a message with it. + +To raise a `ValueError` with a message, write the message as an argument to the `exception` type: + +```python +# when the board receives malformed input +raise ValueError("The board is invalid with current input.") +``` diff --git a/exercises/practice/flower-field/.docs/instructions.md b/exercises/practice/flower-field/.docs/instructions.md new file mode 100644 index 00000000000..bbdae0c2cb1 --- /dev/null +++ b/exercises/practice/flower-field/.docs/instructions.md @@ -0,0 +1,26 @@ +# Instructions + +Your task is to add flower counts to empty squares in a completed Flower Field garden. +The garden itself is a rectangle board composed of squares that are either empty (`' '`) or a flower (`'*'`). + +For each empty square, count the number of flowers adjacent to it (horizontally, vertically, diagonally). +If the empty square has no adjacent flowers, leave it empty. +Otherwise replace it with the count of adjacent flowers. + +For example, you may receive a 5 x 4 board like this (empty spaces are represented here with the 'Β·' character for display on screen): + +```text +Β·*Β·*Β· +Β·Β·*Β·Β· +Β·Β·*Β·Β· +Β·Β·Β·Β·Β· +``` + +Which your code should transform into this: + +```text +1*3*1 +13*31 +Β·2*2Β· +Β·111Β· +``` diff --git a/exercises/practice/flower-field/.docs/introduction.md b/exercises/practice/flower-field/.docs/introduction.md new file mode 100644 index 00000000000..af9b6153617 --- /dev/null +++ b/exercises/practice/flower-field/.docs/introduction.md @@ -0,0 +1,7 @@ +# Introduction + +[Flower Field][history] is a compassionate reimagining of the popular game Minesweeper. +The object of the game is to find all the flowers in the garden using numeric hints that indicate how many flowers are directly adjacent (horizontally, vertically, diagonally) to a square. +"Flower Field" shipped in regional versions of Microsoft Windows in Italy, Germany, South Korea, Japan and Taiwan. + +[history]: https://web.archive.org/web/20020409051321fw_/http://rcm.usr.dsi.unimi.it/rcmweb/fnm/ diff --git a/exercises/practice/flower-field/.meta/additional_tests.json b/exercises/practice/flower-field/.meta/additional_tests.json new file mode 100644 index 00000000000..f55a3e2d483 --- /dev/null +++ b/exercises/practice/flower-field/.meta/additional_tests.json @@ -0,0 +1,53 @@ +{ + "exercise": "flower-field", + "version": "2.0", + "comments": [ + " The expected outputs are represented as arrays of strings to ", + " improve readability in this JSON file. ", + " Your track may choose whether to present the input as a single ", + " string (concatenating all the lines) or as the list. " + ], + "cases": [ + { + "description": "annotate 9", + "property": "annotate", + "input": { + "garden": [ + " ", + " * ", + " ", + " ", + " * " + ] + }, + "expected": [ + " 111", + " 1*1", + " 111", + "111 ", + "1*1 " + ] + }, + { + "description": "different len", + "property": "annotate", + "input": { + "garden": [ + " ", + "* ", + " " + ] + }, + "expected": {"error": "The board is invalid with current input."} + }, + { + "description": "invalid char", + "property": "annotate", + "input": { + "garden": ["X * "] + }, + "expected": {"error": "The board is invalid with current input."} + + } + ] +} diff --git a/exercises/practice/flower-field/.meta/config.json b/exercises/practice/flower-field/.meta/config.json new file mode 100644 index 00000000000..ef72b0341cc --- /dev/null +++ b/exercises/practice/flower-field/.meta/config.json @@ -0,0 +1,22 @@ +{ + "authors": [ + "habere-et-dispertire", + "bethanyg" + ], + "contributors": [ + "isaacg", + "kotp" + ], + "files": { + "solution": [ + "flower_field.py" + ], + "test": [ + "flower_field_test.py" + ], + "example": [ + ".meta/example.py" + ] + }, + "blurb": "Mark all the flowers in a garden." +} diff --git a/exercises/practice/flower-field/.meta/example.py b/exercises/practice/flower-field/.meta/example.py new file mode 100644 index 00000000000..e3ae009cc27 --- /dev/null +++ b/exercises/practice/flower-field/.meta/example.py @@ -0,0 +1,40 @@ +def annotate(garden): + if not garden: + return [] + verify_board(garden) + row_len = len(garden[0]) + col_len = len(garden) + board = [list(row) for row in garden] + + for index1 in range(col_len): + for index2 in range(row_len): + if board[index1][index2] != ' ': + continue + + low = max(index2 - 1, 0) + high = min(index2 + 2, row_len + 2) + counts = garden[index1][low:high].count('*') + + if index1 > 0: + counts += garden[index1 - 1][low:high].count('*') + if index1 < col_len - 1: + counts += garden[index1 + 1][low:high].count('*') + if counts == 0: + continue + + board[index1][index2] = str(counts) + return [''.join(row) for row in board] + + +def verify_board(garden): + # Rows with different lengths + row_len = len(garden[0]) + if not all(len(row) == row_len for row in garden): + raise ValueError('The board is invalid with current input.') + + # Unknown character in board + character_set = set() + for row in garden: + character_set.update(row) + if character_set - set(' *'): + raise ValueError('The board is invalid with current input.') diff --git a/exercises/practice/flower-field/.meta/template.j2 b/exercises/practice/flower-field/.meta/template.j2 new file mode 100644 index 00000000000..f71afb3bcdf --- /dev/null +++ b/exercises/practice/flower-field/.meta/template.j2 @@ -0,0 +1,27 @@ +{%- import "generator_macros.j2" as macros with context -%} +{{ macros.canonical_ref() }} + +{{ macros.header()}} + +{%- macro test_call(case) -%} + {{ case["property"] | to_snake }}({{ case["input"]["garden"] }}) +{%- endmacro %} + +class {{ exercise | camel_case }}Test(unittest.TestCase): + {% for case in cases -%} + def test_{{ case["description"] | to_snake }}(self): + self.assertEqual({{ test_call(case) }}, {{ case["expected"] }}) + {% endfor %} + + # Additional tests for this track + {% for case in additional_cases -%} + def test_{{ case["description"] | to_snake }}(self): + {%- if case is error_case %} + with self.assertRaises(ValueError) as err: + {{ test_call(case) }} + self.assertEqual(type(err.exception), ValueError) + self.assertEqual(err.exception.args[0], "{{ case["expected"]["error"] }}") + {%- else %} + self.assertEqual({{- test_call(case) }}, {{ case["expected"] }}) + {%- endif %} + {% endfor %} diff --git a/exercises/practice/flower-field/.meta/tests.toml b/exercises/practice/flower-field/.meta/tests.toml new file mode 100644 index 00000000000..965ba8fd4d7 --- /dev/null +++ b/exercises/practice/flower-field/.meta/tests.toml @@ -0,0 +1,49 @@ +# 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. + +[237ff487-467a-47e1-9b01-8a891844f86c] +description = "no rows" + +[4b4134ec-e20f-439c-a295-664c38950ba1] +description = "no columns" + +[d774d054-bbad-4867-88ae-069cbd1c4f92] +description = "no flowers" + +[225176a0-725e-43cd-aa13-9dced501f16e] +description = "garden full of flowers" + +[3f345495-f1a5-4132-8411-74bd7ca08c49] +description = "flower surrounded by spaces" + +[6cb04070-4199-4ef7-a6fa-92f68c660fca] +description = "space surrounded by flowers" + +[272d2306-9f62-44fe-8ab5-6b0f43a26338] +description = "horizontal line" + +[c6f0a4b2-58d0-4bf6-ad8d-ccf4144f1f8e] +description = "horizontal line, flowers at edges" + +[a54e84b7-3b25-44a8-b8cf-1753c8bb4cf5] +description = "vertical line" + +[b40f42f5-dec5-4abc-b167-3f08195189c1] +description = "vertical line, flowers at edges" + +[58674965-7b42-4818-b930-0215062d543c] +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.py b/exercises/practice/flower-field/flower_field.py new file mode 100644 index 00000000000..88793e3779b --- /dev/null +++ b/exercises/practice/flower-field/flower_field.py @@ -0,0 +1,3 @@ +def annotate(garden): + # Function body starts here + pass diff --git a/exercises/practice/flower-field/flower_field_test.py b/exercises/practice/flower-field/flower_field_test.py new file mode 100644 index 00000000000..d0f1334cbfc --- /dev/null +++ b/exercises/practice/flower-field/flower_field_test.py @@ -0,0 +1,79 @@ +# 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-12-30 + +import unittest + +from flower_field import ( + annotate, +) + + +class FlowerFieldTest(unittest.TestCase): + def test_no_rows(self): + self.assertEqual(annotate([]), []) + + def test_no_columns(self): + self.assertEqual(annotate([""]), [""]) + + def test_no_flowers(self): + self.assertEqual(annotate([" ", " ", " "]), [" ", " ", " "]) + + def test_garden_full_of_flowers(self): + self.assertEqual(annotate(["***", "***", "***"]), ["***", "***", "***"]) + + def test_flower_surrounded_by_spaces(self): + self.assertEqual(annotate([" ", " * ", " "]), ["111", "1*1", "111"]) + + def test_space_surrounded_by_flowers(self): + self.assertEqual(annotate(["***", "* *", "***"]), ["***", "*8*", "***"]) + + def test_horizontal_line(self): + self.assertEqual(annotate([" * * "]), ["1*2*1"]) + + def test_horizontal_line_flowers_at_edges(self): + self.assertEqual(annotate(["* *"]), ["*1 1*"]) + + def test_vertical_line(self): + self.assertEqual(annotate([" ", "*", " ", "*", " "]), ["1", "*", "2", "*", "1"]) + + def test_vertical_line_flowers_at_edges(self): + self.assertEqual(annotate(["*", " ", " ", " ", "*"]), ["*", "1", " ", "1", "*"]) + + def test_cross(self): + self.assertEqual( + annotate([" * ", " * ", "*****", " * ", " * "]), + [" 2*2 ", "25*52", "*****", "25*52", " 2*2 "], + ) + + def test_large_garden(self): + self.assertEqual( + annotate([" * * ", " * ", " * ", " * *", " * * ", " "]), + ["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( + annotate([" ", " * ", " ", " ", " * "]), + [" 111", " 1*1", " 111", "111 ", "1*1 "], + ) + + def test_different_len(self): + with self.assertRaises(ValueError) as err: + annotate([" ", "* ", " "]) + self.assertEqual(type(err.exception), ValueError) + self.assertEqual( + err.exception.args[0], "The board is invalid with current input." + ) + + def test_invalid_char(self): + with self.assertRaises(ValueError) as err: + annotate(["X * "]) + self.assertEqual(type(err.exception), ValueError) + self.assertEqual( + err.exception.args[0], "The board is invalid with current input." + ) diff --git a/exercises/practice/forth/.meta/tests.toml b/exercises/practice/forth/.meta/tests.toml index 16e0ffd9a6e..5b5c09e240f 100644 --- a/exercises/practice/forth/.meta/tests.toml +++ b/exercises/practice/forth/.meta/tests.toml @@ -24,6 +24,9 @@ description = "addition -> errors if there is nothing on the stack" [06efb9a4-817a-435e-b509-06166993c1b8] description = "addition -> errors if there is only one value on the stack" +[1e07a098-c5fa-4c66-97b2-3c81205dbc2f] +description = "addition -> more than two values on the stack" + [09687c99-7bbc-44af-8526-e402f997ccbf] description = "subtraction -> can subtract two numbers" @@ -33,6 +36,9 @@ description = "subtraction -> errors if there is nothing on the stack" [b3cee1b2-9159-418a-b00d-a1bb3765c23b] description = "subtraction -> errors if there is only one value on the stack" +[2c8cc5ed-da97-4cb1-8b98-fa7b526644f4] +description = "subtraction -> more than two values on the stack" + [5df0ceb5-922e-401f-974d-8287427dbf21] description = "multiplication -> can multiply two numbers" @@ -42,6 +48,9 @@ description = "multiplication -> errors if there is nothing on the stack" [8ba4b432-9f94-41e0-8fae-3b3712bd51b3] description = "multiplication -> errors if there is only one value on the stack" +[5cd085b5-deb1-43cc-9c17-6b1c38bc9970] +description = "multiplication -> more than two values on the stack" + [e74c2204-b057-4cff-9aa9-31c7c97a93f5] description = "division -> can divide two numbers" @@ -57,12 +66,21 @@ description = "division -> errors if there is nothing on the stack" [d5547f43-c2ff-4d5c-9cb0-2a4f6684c20d] description = "division -> errors if there is only one value on the stack" +[f224f3e0-b6b6-4864-81de-9769ecefa03f] +description = "division -> more than two values on the stack" + [ee28d729-6692-4a30-b9be-0d830c52a68c] description = "combined arithmetic -> addition and subtraction" [40b197da-fa4b-4aca-a50b-f000d19422c1] description = "combined arithmetic -> multiplication and division" +[f749b540-53aa-458e-87ec-a70797eddbcb] +description = "combined arithmetic -> multiplication and addition" + +[c8e5a4c2-f9bf-4805-9a35-3c3314e4989a] +description = "combined arithmetic -> addition and multiplication" + [c5758235-6eef-4bf6-ab62-c878e50b9957] description = "dup -> copies a value on the stack" diff --git a/exercises/practice/forth/forth_test.py b/exercises/practice/forth/forth_test.py index f8402bdd64c..1489bbd7df0 100644 --- a/exercises/practice/forth/forth_test.py +++ b/exercises/practice/forth/forth_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/forth/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2024-11-04 import unittest @@ -36,6 +36,9 @@ def test_addition_errors_if_there_is_only_one_value_on_the_stack(self): str(err.exception.args[0]), "Insufficient number of items in stack" ) + def test_addition_more_than_two_values_on_the_stack(self): + self.assertEqual(evaluate(["1 2 3 +"]), [1, 5]) + def test_subtraction_can_subtract_two_numbers(self): self.assertEqual(evaluate(["3 4 -"]), [-1]) @@ -55,6 +58,9 @@ def test_subtraction_errors_if_there_is_only_one_value_on_the_stack(self): str(err.exception.args[0]), "Insufficient number of items in stack" ) + def test_subtraction_more_than_two_values_on_the_stack(self): + self.assertEqual(evaluate(["1 12 3 -"]), [1, 9]) + def test_multiplication_can_multiply_two_numbers(self): self.assertEqual(evaluate(["2 4 *"]), [8]) @@ -74,6 +80,9 @@ def test_multiplication_errors_if_there_is_only_one_value_on_the_stack(self): str(err.exception.args[0]), "Insufficient number of items in stack" ) + def test_multiplication_more_than_two_values_on_the_stack(self): + self.assertEqual(evaluate(["1 2 3 *"]), [1, 6]) + def test_division_can_divide_two_numbers(self): self.assertEqual(evaluate(["12 3 /"]), [4]) @@ -103,12 +112,21 @@ def test_division_errors_if_there_is_only_one_value_on_the_stack(self): str(err.exception.args[0]), "Insufficient number of items in stack" ) + def test_division_more_than_two_values_on_the_stack(self): + self.assertEqual(evaluate(["1 12 3 /"]), [1, 4]) + def test_combined_arithmetic_addition_and_subtraction(self): self.assertEqual(evaluate(["1 2 + 4 -"]), [-1]) def test_combined_arithmetic_multiplication_and_division(self): self.assertEqual(evaluate(["2 4 * 3 /"]), [2]) + def test_combined_arithmetic_multiplication_and_addition(self): + self.assertEqual(evaluate(["1 3 4 * +"]), [13]) + + def test_combined_arithmetic_addition_and_multiplication(self): + self.assertEqual(evaluate(["1 3 4 + *"]), [7]) + def test_dup_copies_a_value_on_the_stack(self): self.assertEqual(evaluate(["1 dup"]), [1, 1]) diff --git a/exercises/practice/grade-school/.docs/instructions.md b/exercises/practice/grade-school/.docs/instructions.md index 9a63e398d85..3cb1b5d5f90 100644 --- a/exercises/practice/grade-school/.docs/instructions.md +++ b/exercises/practice/grade-school/.docs/instructions.md @@ -1,21 +1,21 @@ # Instructions -Given students' names along with the grade that they are in, create a roster for the school. +Given students' names along with the grade they are in, create a roster for the school. In the end, you should be able to: -- Add a student's name to the roster for a grade +- Add a student's name to the roster for a grade: - "Add Jim to grade 2." - "OK." -- Get a list of all students enrolled in a grade +- Get a list of all students enrolled in a grade: - "Which students are in grade 2?" - - "We've only got Jim just now." + - "We've only got Jim right now." - Get a sorted list of all students in all grades. - Grades should sort as 1, 2, 3, etc., and students within a grade should be sorted alphabetically by name. - - "Who all is enrolled in school right now?" + Grades should be sorted as 1, 2, 3, etc., and students within a grade should be sorted alphabetically by name. + - "Who is enrolled in school right now?" - "Let me think. - We have Anna, Barb, and Charlie in grade 1, Alex, Peter, and Zoe in grade 2 and Jim in grade 5. - So the answer is: Anna, Barb, Charlie, Alex, Peter, Zoe and Jim" + We have Anna, Barb, and Charlie in grade 1, Alex, Peter, and Zoe in grade 2, and Jim in grade 5. + So the answer is: Anna, Barb, Charlie, Alex, Peter, Zoe, and Jim." -Note that all our students only have one name (It's a small town, what do you want?) and each student cannot be added more than once to a grade or the roster. -In fact, when a test attempts to add the same student more than once, your implementation should indicate that this is incorrect. +Note that all our students only have one name (it's a small town, what do you want?), and each student cannot be added more than once to a grade or the roster. +If a test attempts to add the same student more than once, your implementation should indicate that this is incorrect. diff --git a/exercises/practice/grains/.articles/performance/content.md b/exercises/practice/grains/.articles/performance/content.md index b1150018314..7ab12a14781 100644 --- a/exercises/practice/grains/.articles/performance/content.md +++ b/exercises/practice/grains/.articles/performance/content.md @@ -37,7 +37,7 @@ pow square 64: 5.738279999932274e-07 Using `if number not in range(1, 65):` was over `125` nanoseconds longer than using `if number < 1 or number > 64:` for all approaches. [approaches]: https://exercism.org/tracks/python/exercises/grains/approaches -[approach-bit-shifting]: https://exercism.org/python/csharp/exercises/grains/approaches/bit-shifting +[approach-bit-shifting]: https://exercism.org/tracks/python/exercises/grains/approaches/bit-shifting [approach-pow]: https://exercism.org/tracks/python/exercises/grains/approaches/pow [approach-exponentiation]: https://exercism.org/tracks/python/exercises/grains/approaches/exponentiation [benchmark-application]: https://github.com/exercism/python/blob/main/exercises/practice/grains/.articles/performance/code/Benchmark.py diff --git a/exercises/practice/grains/.docs/instructions.md b/exercises/practice/grains/.docs/instructions.md index df479fc0a17..f5b752a8175 100644 --- a/exercises/practice/grains/.docs/instructions.md +++ b/exercises/practice/grains/.docs/instructions.md @@ -1,15 +1,11 @@ # Instructions -Calculate the number of grains of wheat on a chessboard given that the number on each square doubles. +Calculate the number of grains of wheat on a chessboard. -There once was a wise servant who saved the life of a prince. -The king promised to pay whatever the servant could dream up. -Knowing that the king loved chess, the servant told the king he would like to have grains of wheat. -One grain on the first square of a chess board, with the number of grains doubling on each successive square. +A chessboard has 64 squares. +Square 1 has one grain, square 2 has two grains, square 3 has four grains, and so on, doubling each time. -There are 64 squares on a chessboard (where square 1 has one grain, square 2 has two grains, and so on). +Write code that calculates: -Write code that shows: - -- how many grains were on a given square, and +- the number of grains on a given square - the total number of grains on the chessboard diff --git a/exercises/practice/grains/.docs/introduction.md b/exercises/practice/grains/.docs/introduction.md new file mode 100644 index 00000000000..0df4f46f726 --- /dev/null +++ b/exercises/practice/grains/.docs/introduction.md @@ -0,0 +1,6 @@ +# Introduction + +There once was a wise servant who saved the life of a prince. +The king promised to pay whatever the servant could dream up. +Knowing that the king loved chess, the servant told the king he would like to have grains of wheat. +One grain on the first square of a chessboard, with the number of grains doubling on each successive square. diff --git a/exercises/practice/grains/.meta/config.json b/exercises/practice/grains/.meta/config.json index 4e59df7431c..00ca9f18d1f 100644 --- a/exercises/practice/grains/.meta/config.json +++ b/exercises/practice/grains/.meta/config.json @@ -30,5 +30,5 @@ }, "blurb": "Calculate the number of grains of wheat on a chessboard given that the number on each square doubles.", "source": "The CodeRanch Cattle Drive, Assignment 6", - "source_url": "https://coderanch.com/wiki/718824/Grains" + "source_url": "https://web.archive.org/web/20240908084142/https://coderanch.com/wiki/718824/Grains" } diff --git a/exercises/practice/hamming/.docs/instructions.md b/exercises/practice/hamming/.docs/instructions.md index 020fdd02d4e..8f47a179e01 100644 --- a/exercises/practice/hamming/.docs/instructions.md +++ b/exercises/practice/hamming/.docs/instructions.md @@ -1,26 +1,15 @@ # Instructions -Calculate the Hamming Distance between two DNA strands. +Calculate the Hamming distance between two DNA strands. -Your body is made up of cells that contain DNA. -Those cells regularly wear out and need replacing, which they achieve by dividing into daughter cells. -In fact, the average human body experiences about 10 quadrillion cell divisions in a lifetime! - -When cells divide, their DNA replicates too. -Sometimes during this process mistakes happen and single pieces of DNA get encoded with the incorrect information. -If we compare two strands of DNA and count the differences between them we can see how many mistakes occurred. -This is known as the "Hamming Distance". - -We read DNA using the letters C,A,G and T. +We read DNA using the letters C, A, G and T. Two strands might look like this: GAGCCTACTAACGGGAT CATCGTAATGACGGCCT ^ ^ ^ ^ ^ ^^ -They have 7 differences, and therefore the Hamming Distance is 7. - -The Hamming Distance is useful for lots of things in science, not just biology, so it's a nice phrase to be familiar with :) +They have 7 differences, and therefore the Hamming distance is 7. ## Implementation notes diff --git a/exercises/practice/hamming/.docs/introduction.md b/exercises/practice/hamming/.docs/introduction.md new file mode 100644 index 00000000000..8419bf479e5 --- /dev/null +++ b/exercises/practice/hamming/.docs/introduction.md @@ -0,0 +1,12 @@ +# Introduction + +Your body is made up of cells that contain DNA. +Those cells regularly wear out and need replacing, which they achieve by dividing into daughter cells. +In fact, the average human body experiences about 10 quadrillion cell divisions in a lifetime! + +When cells divide, their DNA replicates too. +Sometimes during this process mistakes happen and single pieces of DNA get encoded with the incorrect information. +If we compare two strands of DNA and count the differences between them, we can see how many mistakes occurred. +This is known as the "Hamming distance". + +The Hamming distance is useful in many areas of science, not just biology, so it's a nice phrase to be familiar with :) diff --git a/exercises/practice/hamming/.meta/config.json b/exercises/practice/hamming/.meta/config.json index 1280ded9127..117a2d954d0 100644 --- a/exercises/practice/hamming/.meta/config.json +++ b/exercises/practice/hamming/.meta/config.json @@ -31,7 +31,7 @@ ".meta/example.py" ] }, - "blurb": "Calculate the Hamming difference between two DNA strands.", + "blurb": "Calculate the Hamming distance between two DNA strands.", "source": "The Calculating Point Mutations problem at Rosalind", "source_url": "https://rosalind.info/problems/hamm/" } 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) 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/knapsack/.docs/instructions.md b/exercises/practice/knapsack/.docs/instructions.md index 3411db9886a..0ebf7914c55 100644 --- a/exercises/practice/knapsack/.docs/instructions.md +++ b/exercises/practice/knapsack/.docs/instructions.md @@ -1,11 +1,11 @@ # Instructions -Your task is to determine which items to take so that the total value of his selection is maximized, taking into account the knapsack's carrying capacity. +Your task is to determine which items to take so that the total value of her selection is maximized, taking into account the knapsack's carrying capacity. Items will be represented as a list of items. Each item will have a weight and value. All values given will be strictly positive. -Bob can take only one of each item. +Lhakpa can take only one of each item. For example: @@ -21,5 +21,5 @@ Knapsack Maximum Weight: 10 ``` For the above, the first item has weight 5 and value 10, the second item has weight 4 and value 40, and so on. -In this example, Bob should take the second and fourth item to maximize his value, which, in this case, is 90. -He cannot get more than 90 as his knapsack has a weight limit of 10. +In this example, Lhakpa should take the second and fourth item to maximize her value, which, in this case, is 90. +She cannot get more than 90 as her knapsack has a weight limit of 10. diff --git a/exercises/practice/knapsack/.docs/introduction.md b/exercises/practice/knapsack/.docs/introduction.md index 9b2bed8b4e9..9ac9df596b6 100644 --- a/exercises/practice/knapsack/.docs/introduction.md +++ b/exercises/practice/knapsack/.docs/introduction.md @@ -1,8 +1,10 @@ # Introduction -Bob is a thief. -After months of careful planning, he finally manages to crack the security systems of a fancy store. +Lhakpa is a [Sherpa][sherpa] mountain guide and porter. +After months of careful planning, the expedition Lhakpa works for is about to leave. +She will be paid the value she carried to the base camp. -In front of him are many items, each with a value and weight. -Bob would gladly take all of the items, but his knapsack can only hold so much weight. -Bob has to carefully consider which items to take so that the total value of his selection is maximized. +In front of her are many items, each with a value and weight. +Lhakpa would gladly take all of the items, but her knapsack can only hold so much weight. + +[sherpa]: https://en.wikipedia.org/wiki/Sherpa_people#Mountaineering 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") diff --git a/exercises/practice/largest-series-product/.meta/example.py b/exercises/practice/largest-series-product/.meta/example.py index 0599e7417e6..f7cfc2310b8 100644 --- a/exercises/practice/largest-series-product/.meta/example.py +++ b/exercises/practice/largest-series-product/.meta/example.py @@ -5,7 +5,7 @@ def slices(series, size): if not size <= len(series): - raise ValueError('span must be smaller than string length') + raise ValueError('span must not exceed string length') elif not 0 < size: raise ValueError('span must not be negative') elif not all(item.isdigit() for item in series): @@ -20,4 +20,4 @@ def slices(series, size): def largest_product(series, size): if size == 0: return 1 - return max(reduce(mul, slice) for slice in slices(series, size)) + return max(reduce(mul, slice) for slice in slices(series, size)) \ No newline at end of file diff --git a/exercises/practice/largest-series-product/.meta/tests.toml b/exercises/practice/largest-series-product/.meta/tests.toml index 88316925977..982f517cc32 100644 --- a/exercises/practice/largest-series-product/.meta/tests.toml +++ b/exercises/practice/largest-series-product/.meta/tests.toml @@ -38,6 +38,11 @@ description = "reports zero if all spans include zero" [5d81aaf7-4f67-4125-bf33-11493cc7eab7] description = "rejects span longer than string length" +include = false + +[0ae1ce53-d9ba-41bb-827f-2fceb64f058b] +description = "rejects span longer than string length" +reimplements = "5d81aaf7-4f67-4125-bf33-11493cc7eab7" [06bc8b90-0c51-4c54-ac22-3ec3893a079e] description = "reports 1 for empty string and empty product (0 span)" @@ -49,6 +54,11 @@ include = false [6d96c691-4374-4404-80ee-2ea8f3613dd4] description = "rejects empty string and nonzero span" +include = false + +[6cf66098-a6af-4223-aab1-26aeeefc7402] +description = "rejects empty string and nonzero span" +reimplements = "6d96c691-4374-4404-80ee-2ea8f3613dd4" [7a38f2d6-3c35-45f6-8d6f-12e6e32d4d74] description = "rejects invalid character in digits" diff --git a/exercises/practice/largest-series-product/largest_series_product_test.py b/exercises/practice/largest-series-product/largest_series_product_test.py index e5056236736..494cd891389 100644 --- a/exercises/practice/largest-series-product/largest_series_product_test.py +++ b/exercises/practice/largest-series-product/largest_series_product_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/largest-series-product/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2025-06-20 import unittest @@ -44,17 +44,13 @@ def test_rejects_span_longer_than_string_length(self): with self.assertRaises(ValueError) as err: largest_product("123", 4) self.assertEqual(type(err.exception), ValueError) - self.assertEqual( - err.exception.args[0], "span must be smaller than string length" - ) + self.assertEqual(err.exception.args[0], "span must not exceed string length") def test_rejects_empty_string_and_nonzero_span(self): with self.assertRaises(ValueError) as err: largest_product("", 1) self.assertEqual(type(err.exception), ValueError) - self.assertEqual( - err.exception.args[0], "span must be smaller than string length" - ) + self.assertEqual(err.exception.args[0], "span must not exceed string length") def test_rejects_invalid_character_in_digits(self): with self.assertRaises(ValueError) as err: diff --git a/exercises/practice/leap/.meta/config.json b/exercises/practice/leap/.meta/config.json index 1c35f22be1f..2e838e97b44 100644 --- a/exercises/practice/leap/.meta/config.json +++ b/exercises/practice/leap/.meta/config.json @@ -34,5 +34,5 @@ }, "blurb": "Determine whether a given year is a leap year.", "source": "CodeRanch Cattle Drive, Assignment 3", - "source_url": "https://coderanch.com/t/718816/Leap" + "source_url": "https://web.archive.org/web/20240907033714/https://coderanch.com/t/718816/Leap" } diff --git a/exercises/practice/linked-list/.docs/instructions.append.md b/exercises/practice/linked-list/.docs/instructions.append.md index 00032862c2a..25f30a19934 100644 --- a/exercises/practice/linked-list/.docs/instructions.append.md +++ b/exercises/practice/linked-list/.docs/instructions.append.md @@ -5,7 +5,7 @@ While linked lists can be implemented in a variety of ways with a variety of underlying data structures, we ask here that you implement your linked list in an OOP fashion. In the stub file, you will see the start of a `Node` class, as well as a `LinkedList` class. -Your `Node` class should keep track of its value, as well as which other nodes preceed or follow. +Your `Node` class should keep track of its value, as well as which nodes precede or follow. Your `push`, `pop`, `shift`, `unshift`, and the special method for `len` should be implemented in the `LinkedList` class. You might also find it useful to implement a special `iter` method for iteration. @@ -17,7 +17,7 @@ If the value appears more than once, only the **first** occurrence should be rem
-## Exception messages +## Exception Messages Sometimes it is necessary to [raise an exception][raising]. 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. 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") diff --git a/exercises/practice/luhn/.approaches/recursion/content.md b/exercises/practice/luhn/.approaches/recursion/content.md index 6bb89e7932b..a84c5478510 100644 --- a/exercises/practice/luhn/.approaches/recursion/content.md +++ b/exercises/practice/luhn/.approaches/recursion/content.md @@ -29,7 +29,7 @@ class Luhn: ``` -The `Luhn` object is initialzed with the `card_num` value, which is the number to be validated with the Luhn algorithm. +The `Luhn` object is initialized with the `card_num` value, which is the number to be validated with the Luhn algorithm. The result of the validation is returned from `Luhn`'s `valid()` method. In this approach, a member variable is set to the result of running the Luhn algorithm. That variable is returned from the `valid()` method. diff --git a/exercises/practice/luhn/.approaches/replace-reverse-enumerate/content.md b/exercises/practice/luhn/.approaches/replace-reverse-enumerate/content.md index fd4e49b12c0..fe0b697b315 100644 --- a/exercises/practice/luhn/.approaches/replace-reverse-enumerate/content.md +++ b/exercises/practice/luhn/.approaches/replace-reverse-enumerate/content.md @@ -29,7 +29,7 @@ class Luhn: ``` -The `Luhn` object is initialzed with the `card_num` value, which is the number to be validated with the Luhn algorithm. +The `Luhn` object is initialized with the `card_num` value, which is the number to be validated with the Luhn algorithm. The result of the validation is returned from `Luhn`'s `valid()` method. In this approach, a member variable is set to the result of running the Luhn algorithm. That variable is returned from the `valid()` method. diff --git a/exercises/practice/luhn/.approaches/reversed-for/content.md b/exercises/practice/luhn/.approaches/reversed-for/content.md index abf5a591ca4..683fca30486 100644 --- a/exercises/practice/luhn/.approaches/reversed-for/content.md +++ b/exercises/practice/luhn/.approaches/reversed-for/content.md @@ -30,7 +30,7 @@ class Luhn: ``` -The `Luhn` object is initialzed with the `card_num` value, which is the number to be validated with the Luhn algorithm. +The `Luhn` object is initialized with the `card_num` value, which is the number to be validated with the Luhn algorithm. The result of the validation is returned from `Luhn`'s `valid()` method. In this approach, a member variable is set to the result of running the Luhn algorithm. That variable is returned from the `valid()` method. diff --git a/exercises/practice/luhn/.docs/instructions.md b/exercises/practice/luhn/.docs/instructions.md index 8cbe791fc23..7702c6bbb5f 100644 --- a/exercises/practice/luhn/.docs/instructions.md +++ b/exercises/practice/luhn/.docs/instructions.md @@ -1,64 +1,68 @@ # Instructions -Given a number determine whether or not it is valid per the Luhn formula. +Determine whether a number is valid according to the [Luhn formula][luhn]. -The [Luhn algorithm][luhn] is a simple checksum formula used to validate a variety of identification numbers, such as credit card numbers and Canadian Social Insurance Numbers. +The number will be provided as a string. -The task is to check if a given string is valid. - -## Validating a Number +## Validating a number Strings of length 1 or less are not valid. Spaces are allowed in the input, but they should be stripped before checking. All other non-digit characters are disallowed. -### Example 1: valid credit card number +## Examples -```text -4539 3195 0343 6467 -``` +### Valid credit card number -The first step of the Luhn algorithm is to double every second digit, starting from the right. -We will be doubling +The number to be checked is `4539 3195 0343 6467`. + +The first step of the Luhn algorithm is to start at the end of the number and double every second digit, beginning with the second digit from the right and moving left. ```text -4_3_ 3_9_ 0_4_ 6_6_ +4539 3195 0343 6467 +↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ (double these) ``` -If doubling the number results in a number greater than 9 then subtract 9 from the product. -The results of our doubling: +If the result of doubling a digit is greater than 9, we subtract 9 from that result. +We end up with: ```text 8569 6195 0383 3437 ``` -Then sum all of the digits: +Finally, we sum all digits. +If the sum is evenly divisible by 10, the original number is valid. ```text -8+5+6+9+6+1+9+5+0+3+8+3+3+4+3+7 = 80 +8 + 5 + 6 + 9 + 6 + 1 + 9 + 5 + 0 + 3 + 8 + 3 + 3 + 4 + 3 + 7 = 80 ``` -If the sum is evenly divisible by 10, then the number is valid. -This number is valid! +80 is evenly divisible by 10, so number `4539 3195 0343 6467` is valid! + +### Invalid Canadian SIN + +The number to be checked is `066 123 478`. -### Example 2: invalid credit card number +We start at the end of the number and double every second digit, beginning with the second digit from the right and moving left. ```text -8273 1232 7352 0569 +066 123 478 + ↑ ↑ ↑ ↑ (double these) ``` -Double the second digits, starting from the right +If the result of doubling a digit is greater than 9, we subtract 9 from that result. +We end up with: ```text -7253 2262 5312 0539 +036 226 458 ``` -Sum the digits +We sum the digits: ```text -7+2+5+3+2+2+6+2+5+3+1+2+0+5+3+9 = 57 +0 + 3 + 6 + 2 + 2 + 6 + 4 + 5 + 8 = 36 ``` -57 is not evenly divisible by 10, so this number is not valid. +36 is not evenly divisible by 10, so number `066 123 478` is not valid! [luhn]: https://en.wikipedia.org/wiki/Luhn_algorithm diff --git a/exercises/practice/luhn/.docs/introduction.md b/exercises/practice/luhn/.docs/introduction.md new file mode 100644 index 00000000000..dee48006edd --- /dev/null +++ b/exercises/practice/luhn/.docs/introduction.md @@ -0,0 +1,11 @@ +# Introduction + +At the Global Verification Authority, you've just been entrusted with a critical assignment. +Across the city, from online purchases to secure logins, countless operations rely on the accuracy of numerical identifiers like credit card numbers, bank account numbers, transaction codes, and tracking IDs. +The Luhn algorithm is a simple checksum formula used to help identify mistyped numbers. + +A batch of identifiers has just arrived on your desk. +All of them must pass the Luhn test to ensure they're legitimate. +If any fail, they'll be flagged as invalid, preventing mistakes such as incorrect transactions or failed account verifications. + +Can you ensure this is done right? The integrity of many services depends on you. diff --git a/exercises/practice/meetup/.docs/instructions.md b/exercises/practice/meetup/.docs/instructions.md index 000de2fd127..8b1bda5eb4c 100644 --- a/exercises/practice/meetup/.docs/instructions.md +++ b/exercises/practice/meetup/.docs/instructions.md @@ -2,7 +2,7 @@ Your task is to find the exact date of a meetup, given a month, year, weekday and week. -There are five week values to consider: `first`, `second`, `third`, `fourth`, `last`, `teenth`. +There are six week values to consider: `first`, `second`, `third`, `fourth`, `last`, `teenth`. For example, you might be asked to find the date for the meetup on the first Monday in January 2018 (January 1, 2018). diff --git a/exercises/practice/nth-prime/.approaches/generator-fun/content.md b/exercises/practice/nth-prime/.approaches/generator-fun/content.md index 39290335a04..b7fc867ab77 100644 --- a/exercises/practice/nth-prime/.approaches/generator-fun/content.md +++ b/exercises/practice/nth-prime/.approaches/generator-fun/content.md @@ -19,11 +19,13 @@ Using a lambda expression, we `filter` out any numbers above two that are prime. Doesn't this result in an infinite loop? No - `filter` _also_ returns a generator object (which are [evaluated lazily][generator]), so while it's too will produce values infinitely if evaluated, it doesn't hang to program at the time of instantiation. -`itertools.islice` takes in a generator object and an end count, returning a generator object which _only evalutes until that end count_. +`itertools.islice` takes in a generator object and an end count, returning a generator object which _only evaluates until that end count_. The next line exhausts all the values in the generator except the end, and we finally return the last element. -We can utilize the `functools.cache` decorator for greater speeds at higher values of `number`, so we take it out. The added bonus is that a very long line of code is cleant up. +We can utilize the `functools.cache` decorator for greater speeds at higher values of `number`, so we take it out. +The added bonus is that a very long line of code is cleaned up. + ```python from itertools import islice, count 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/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 ``` diff --git a/exercises/practice/pascals-triangle/.docs/introduction.md b/exercises/practice/pascals-triangle/.docs/introduction.md index 60b8ec30dc8..eab454e5a69 100644 --- a/exercises/practice/pascals-triangle/.docs/introduction.md +++ b/exercises/practice/pascals-triangle/.docs/introduction.md @@ -13,7 +13,7 @@ Over the next hour, your teacher reveals some amazing things hidden in this tria - It contains the Fibonacci sequence. - If you color odd and even numbers differently, you get a beautiful pattern called the [SierpiΕ„ski triangle][wikipedia-sierpinski-triangle]. -The teacher implores you and your classmates to lookup other uses, and assures you that there are lots more! +The teacher implores you and your classmates to look up other uses, and assures you that there are lots more! At that moment, the school bell rings. You realize that for the past hour, you were completely absorbed in learning about Pascal's triangle. You quickly grab your laptop from your bag and go outside, ready to enjoy both the sunshine _and_ the wonders of Pascal's triangle. diff --git a/exercises/practice/phone-number/.docs/instructions.md b/exercises/practice/phone-number/.docs/instructions.md index 62ba48e96fd..5d4d3739f45 100644 --- a/exercises/practice/phone-number/.docs/instructions.md +++ b/exercises/practice/phone-number/.docs/instructions.md @@ -1,6 +1,6 @@ # Instructions -Clean up user-entered phone numbers so that they can be sent SMS messages. +Clean up phone numbers so that they can be sent SMS messages. The **North American Numbering Plan (NANP)** is a telephone numbering system used by many countries in North America like the United States, Canada or Bermuda. All NANP-countries share the same international country code: `1`. diff --git a/exercises/practice/phone-number/.docs/introduction.md b/exercises/practice/phone-number/.docs/introduction.md new file mode 100644 index 00000000000..c4142c5af72 --- /dev/null +++ b/exercises/practice/phone-number/.docs/introduction.md @@ -0,0 +1,12 @@ +# Introduction + +You've joined LinkLine, a leading communications company working to ensure reliable connections for everyone. +The team faces a big challenge: users submit phone numbers in all sorts of formats β€” dashes, spaces, dots, parentheses, and even prefixes. +Some numbers are valid, while others are impossible to use. + +Your mission is to turn this chaos into order. +You'll clean up valid numbers, formatting them appropriately for use in the system. +At the same time, you'll identify and filter out any invalid entries. + +The success of LinkLine's operations depends on your ability to separate the useful from the unusable. +Are you ready to take on the challenge and keep the connections running smoothly? diff --git a/exercises/practice/pig-latin/.meta/tests.toml b/exercises/practice/pig-latin/.meta/tests.toml index 49ce6e110e8..d524305b45c 100644 --- a/exercises/practice/pig-latin/.meta/tests.toml +++ b/exercises/practice/pig-latin/.meta/tests.toml @@ -1,69 +1,79 @@ -# 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. [11567f84-e8c6-4918-aedb-435f0b73db57] -description = "word beginning with a" +description = "ay is added to words that start with vowels -> word beginning with a" [f623f581-bc59-4f45-9032-90c3ca9d2d90] -description = "word beginning with e" +description = "ay is added to words that start with vowels -> word beginning with e" [7dcb08b3-23a6-4e8a-b9aa-d4e859450d58] -description = "word beginning with i" +description = "ay is added to words that start with vowels -> word beginning with i" [0e5c3bff-266d-41c8-909f-364e4d16e09c] -description = "word beginning with o" +description = "ay is added to words that start with vowels -> word beginning with o" [614ba363-ca3c-4e96-ab09-c7320799723c] -description = "word beginning with u" +description = "ay is added to words that start with vowels -> word beginning with u" [bf2538c6-69eb-4fa7-a494-5a3fec911326] -description = "word beginning with a vowel and followed by a qu" +description = "ay is added to words that start with vowels -> word beginning with a vowel and followed by a qu" [e5be8a01-2d8a-45eb-abb4-3fcc9582a303] -description = "word beginning with p" +description = "first letter and ay are moved to the end of words that start with consonants -> word beginning with p" [d36d1e13-a7ed-464d-a282-8820cb2261ce] -description = "word beginning with k" +description = "first letter and ay are moved to the end of words that start with consonants -> word beginning with k" [d838b56f-0a89-4c90-b326-f16ff4e1dddc] -description = "word beginning with x" +description = "first letter and ay are moved to the end of words that start with consonants -> word beginning with x" [bce94a7a-a94e-4e2b-80f4-b2bb02e40f71] -description = "word beginning with q without a following u" +description = "first letter and ay are moved to the end of words that start with consonants -> word beginning with q without a following u" + +[e59dbbe8-ccee-4619-a8e9-ce017489bfc0] +description = "first letter and ay are moved to the end of words that start with consonants -> word beginning with consonant and vowel containing qu" [c01e049a-e3e2-451c-bf8e-e2abb7e438b8] -description = "word beginning with ch" +description = "some letter clusters are treated like a single consonant -> word beginning with ch" [9ba1669e-c43f-4b93-837a-cfc731fd1425] -description = "word beginning with qu" +description = "some letter clusters are treated like a single consonant -> word beginning with qu" [92e82277-d5e4-43d7-8dd3-3a3b316c41f7] -description = "word beginning with qu and a preceding consonant" +description = "some letter clusters are treated like a single consonant -> word beginning with qu and a preceding consonant" [79ae4248-3499-4d5b-af46-5cb05fa073ac] -description = "word beginning with th" +description = "some letter clusters are treated like a single consonant -> word beginning with th" [e0b3ae65-f508-4de3-8999-19c2f8e243e1] -description = "word beginning with thr" +description = "some letter clusters are treated like a single consonant -> word beginning with thr" [20bc19f9-5a35-4341-9d69-1627d6ee6b43] -description = "word beginning with sch" +description = "some letter clusters are treated like a single consonant -> word beginning with sch" [54b796cb-613d-4509-8c82-8fbf8fc0af9e] -description = "word beginning with yt" +description = "some letter clusters are treated like a single vowel -> word beginning with yt" [8c37c5e1-872e-4630-ba6e-d20a959b67f6] -description = "word beginning with xr" +description = "some letter clusters are treated like a single vowel -> word beginning with xr" [a4a36d33-96f3-422c-a233-d4021460ff00] -description = "y is treated like a consonant at the beginning of a word" +description = "position of y in a word determines if it is a consonant or a vowel -> y is treated like a consonant at the beginning of a word" [adc90017-1a12-4100-b595-e346105042c7] -description = "y is treated like a vowel at the end of a consonant cluster" +description = "position of y in a word determines if it is a consonant or a vowel -> y is treated like a vowel at the end of a consonant cluster" [29b4ca3d-efe5-4a95-9a54-8467f2e5e59a] -description = "y as second letter in two letter word" +description = "position of y in a word determines if it is a consonant or a vowel -> y as second letter in two letter word" [44616581-5ce3-4a81-82d0-40c7ab13d2cf] -description = "a whole phrase" +description = "phrases are translated -> a whole phrase" diff --git a/exercises/practice/pig-latin/pig_latin_test.py b/exercises/practice/pig-latin/pig_latin_test.py index e5a441eb6b9..1217d6883f9 100644 --- a/exercises/practice/pig-latin/pig_latin_test.py +++ b/exercises/practice/pig-latin/pig_latin_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/pig-latin/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2025-01-10 import unittest @@ -40,6 +40,9 @@ def test_word_beginning_with_x(self): def test_word_beginning_with_q_without_a_following_u(self): self.assertEqual(translate("qat"), "atqay") + def test_word_beginning_with_consonant_and_vowel_containing_qu(self): + self.assertEqual(translate("liquid"), "iquidlay") + def test_word_beginning_with_ch(self): self.assertEqual(translate("chair"), "airchay") diff --git a/exercises/practice/pov/.meta/config.json b/exercises/practice/pov/.meta/config.json index cdc96ff934a..44527ba940d 100644 --- a/exercises/practice/pov/.meta/config.json +++ b/exercises/practice/pov/.meta/config.json @@ -22,5 +22,5 @@ }, "blurb": "Reparent a graph on a selected node.", "source": "Adaptation of exercise from 4clojure", - "source_url": "https://www.4clojure.com/" + "source_url": "https://github.com/oxalorg/4ever-clojure" } diff --git a/exercises/practice/protein-translation/.docs/instructions.md b/exercises/practice/protein-translation/.docs/instructions.md index 7dc34d2edfd..35c953b11f9 100644 --- a/exercises/practice/protein-translation/.docs/instructions.md +++ b/exercises/practice/protein-translation/.docs/instructions.md @@ -1,36 +1,17 @@ # Instructions -Translate RNA sequences into proteins. +Your job is to translate RNA sequences into proteins. -RNA can be broken into three nucleotide sequences called codons, and then translated to a polypeptide like so: +RNA strands are made up of three-nucleotide sequences called **codons**. +Each codon translates to an **amino acid**. +When joined together, those amino acids make a protein. -RNA: `"AUGUUUUCU"` => translates to +In the real world, there are 64 codons, which in turn correspond to 20 amino acids. +However, for this exercise, you’ll only use a few of the possible 64. +They are listed below: -Codons: `"AUG", "UUU", "UCU"` -=> which become a polypeptide with the following sequence => - -Protein: `"Methionine", "Phenylalanine", "Serine"` - -There are 64 codons which in turn correspond to 20 amino acids; however, all of the codon sequences and resulting amino acids are not important in this exercise. -If it works for one codon, the program should work for all of them. -However, feel free to expand the list in the test suite to include them all. - -There are also three terminating codons (also known as 'STOP' codons); if any of these codons are encountered (by the ribosome), all translation ends and the protein is terminated. - -All subsequent codons after are ignored, like this: - -RNA: `"AUGUUUUCUUAAAUG"` => - -Codons: `"AUG", "UUU", "UCU", "UAA", "AUG"` => - -Protein: `"Methionine", "Phenylalanine", "Serine"` - -Note the stop codon `"UAA"` terminates the translation and the final methionine is not translated into the protein sequence. - -Below are the codons and resulting Amino Acids needed for the exercise. - -| Codon | Protein | -| :----------------- | :------------ | +| Codon | Amino Acid | +| ------------------ | ------------- | | AUG | Methionine | | UUU, UUC | Phenylalanine | | UUA, UUG | Leucine | @@ -40,6 +21,18 @@ Below are the codons and resulting Amino Acids needed for the exercise. | UGG | Tryptophan | | UAA, UAG, UGA | STOP | +For example, the RNA string β€œAUGUUUUCU” has three codons: β€œAUG”, β€œUUU” and β€œUCU”. +These map to Methionine, Phenylalanine, and Serine. + +## β€œSTOP” Codons + +You’ll note from the table above that there are three **β€œSTOP” codons**. +If you encounter any of these codons, ignore the rest of the sequence β€” the protein is complete. + +For example, β€œAUGUUUUCUUAAAUG” contains a STOP codon (β€œUAA”). +Once we reach that point, we stop processing. +We therefore only consider the part before it (i.e. β€œAUGUUUUCU”), not any further codons after it (i.e. β€œAUG”). + Learn more about [protein translation on Wikipedia][protein-translation]. [protein-translation]: https://en.wikipedia.org/wiki/Translation_(biology) diff --git a/exercises/practice/pythagorean-triplet/.docs/instructions.md b/exercises/practice/pythagorean-triplet/.docs/instructions.md index 1c1a8aea61c..ced833d7a5b 100644 --- a/exercises/practice/pythagorean-triplet/.docs/instructions.md +++ b/exercises/practice/pythagorean-triplet/.docs/instructions.md @@ -1,4 +1,4 @@ -# Instructions +# Description A Pythagorean triplet is a set of three natural numbers, {a, b, c}, for which, diff --git a/exercises/practice/pythagorean-triplet/.docs/introduction.md b/exercises/practice/pythagorean-triplet/.docs/introduction.md new file mode 100644 index 00000000000..3453c6ed48f --- /dev/null +++ b/exercises/practice/pythagorean-triplet/.docs/introduction.md @@ -0,0 +1,19 @@ +# Introduction + +You are an accomplished problem-solver, known for your ability to tackle the most challenging mathematical puzzles. +One evening, you receive an urgent letter from an inventor called the Triangle Tinkerer, who is working on a groundbreaking new project. +The letter reads: + +> Dear Mathematician, +> +> I need your help. +> I am designing a device that relies on the unique properties of Pythagorean triplets β€” sets of three integers that satisfy the equation aΒ² + bΒ² = cΒ². +> This device will revolutionize navigation, but for it to work, I must program it with every possible triplet where the sum of a, b, and c equals a specific number, N. +> Calculating these triplets by hand would take me years, but I hear you are more than up to the task. +> +> Time is of the essence. +> The future of my invention β€” and perhaps even the future of mathematical innovation β€” rests on your ability to solve this problem. + +Motivated by the importance of the task, you set out to find all Pythagorean triplets that satisfy the condition. +Your work could have far-reaching implications, unlocking new possibilities in science and engineering. +Can you rise to the challenge and make history? diff --git a/exercises/practice/pythagorean-triplet/.meta/config.json b/exercises/practice/pythagorean-triplet/.meta/config.json index a9bed96083f..040a45106fd 100644 --- a/exercises/practice/pythagorean-triplet/.meta/config.json +++ b/exercises/practice/pythagorean-triplet/.meta/config.json @@ -31,7 +31,7 @@ ".meta/example.py" ] }, - "blurb": "There exists exactly one Pythagorean triplet for which a + b + c = 1000. Find the triplet.", - "source": "Problem 9 at Project Euler", + "blurb": "Given an integer N, find all Pythagorean triplets for which a + b + c = N.", + "source": "A variation of Problem 9 from Project Euler", "source_url": "https://projecteuler.net/problem=9" } diff --git a/exercises/practice/raindrops/.approaches/itertools-compress/content.md b/exercises/practice/raindrops/.approaches/itertools-compress/content.md index b5da4768622..42e02c188e0 100644 --- a/exercises/practice/raindrops/.approaches/itertools-compress/content.md +++ b/exercises/practice/raindrops/.approaches/itertools-compress/content.md @@ -19,7 +19,7 @@ If the 'sounds' string is empty, a string version of the number is returned inst This is very succinct code that avoids string concatenation. However, it does require the overhead of importing `compress()` from the [itertools][itertools] module. The code is also harder to maintain should there be additional factors/sounds needed. -Because the factors and sounds are seperated, there is a chance mistakes could be made like forgetting a number or swapping which factor is paired with which sound. +Because the factors and sounds are separated, there is a chance mistakes could be made like forgetting a number or swapping which factor is paired with which sound. A better approach for maintenance might be to turn the 'sounds' `tuple` into a dictionary where the factors and sounds can be stored separate from the logic that does the calculations and string creation: diff --git a/exercises/practice/resistor-color-expert/.docs/instructions.md b/exercises/practice/resistor-color-expert/.docs/instructions.md index 7a110832c8f..96b98274462 100644 --- a/exercises/practice/resistor-color-expert/.docs/instructions.md +++ b/exercises/practice/resistor-color-expert/.docs/instructions.md @@ -1,36 +1,36 @@ # Instructions In this exercise, you are going to create a helpful program so that you don't have to remember the values of the bands. -The program will take 1, 4, or 5 colors as input, and outputs the correct value, in ohms. +The program will take 1, 4, or 5 colors as input and output the correct value in ohms. The color bands are encoded as follows: -- Black: 0 -- Brown: 1 -- Red: 2 -- Orange: 3 -- Yellow: 4 -- Green: 5 -- Blue: 6 -- Violet: 7 -- Grey: 8 -- White: 9 - -In `resistor-color trio` you decoded the first three colors. +- black: 0 +- brown: 1 +- red: 2 +- orange: 3 +- yellow: 4 +- green: 5 +- blue: 6 +- violet: 7 +- grey: 8 +- white: 9 + +In [`Resistor Color Trio`][resistor-color-trio-exercise] you decoded the first three color bands. For instance: orange-orange-brown translated to the main value `330`. In this exercise you will need to add _tolerance_ to the mix. Tolerance is the maximum amount that a value can be above or below the main value. -For example, if the last band is green, the maximum tolerance will be Β±0.5%. +For example, if the last band is green, the maximum tolerance will be `Β±0.5%`. The tolerance band will have one of these values: -- Grey - 0.05% -- Violet - 0.1% -- Blue - 0.25% -- Green - 0.5% -- Brown - 1% -- Red - 2% -- Gold - 5% -- Silver - 10% +- grey - 0.05% +- violet - 0.1% +- blue - 0.25% +- green - 0.5% +- brown - 1% +- red - 2% +- gold - 5% +- silver - 10% The four-band resistor is built up like this: @@ -38,10 +38,10 @@ The four-band resistor is built up like this: | ------- | ------- | ---------- | --------- | | Value_1 | Value_2 | Multiplier | Tolerance | -Meaning +Examples: -- orange-orange-brown-green would be 330 ohms with a Β±0.5% tolerance. -- orange-orange-red-grey would be 3300 ohms with Β±0.05% tolerance. +- orange-orange-brown-green would be `330` ohms with a `Β±0.5%` tolerance. +- orange-orange-red-grey would be `3300` ohms with `Β±0.05%` tolerance. The difference between a four and five-band resistor is that the five-band resistor has an extra band to indicate a more precise value. @@ -49,31 +49,34 @@ The difference between a four and five-band resistor is that the five-band resis | ------- | ------- | ------- | ---------- | --------- | | Value_1 | Value_2 | Value_3 | Multiplier | Tolerance | -Meaning +Examples: -- orange-orange-orange-black-green would be 333 ohms with a Β±0.5% tolerance. -- orange-red-orange-blue-violet would be 323M ohms with a Β±0.10 tolerance. +- orange-orange-orange-black-green would be `333` ohms with a `Β±0.5%` tolerance. +- orange-red-orange-blue-violet would be `323M` ohms with a `Β±0.10` tolerance. There are also one band resistors. One band resistors only have the color black with a value of 0. -This exercise is about translating the resistor band colors into a label: + +Your program should translate an input `list` of resistor band colors into a label: "... ohms ...%" -So an input of "orange", "orange", "black", "green" should return: +So an input `list` of `["orange", "orange", "black", "green"]` should return: "33 ohms Β±0.5%" When there are more than a thousand ohms, we say "kiloohms". That's similar to saying "kilometer" for 1000 meters, and "kilograms" for 1000 grams. -So an input of "orange", "orange", "orange", "grey" should return: +So an input `list` of `["orange", "orange", "orange", "grey"]` should return: "33 kiloohms Β±0.05%" When there are more than a million ohms, we say "megaohms". -So an input of "orange", "orange", "blue", "red" should return: +So an input `list` of `["orange", "orange", "blue", "red"]` should return: "33 megaohms Β±2%" + +[resistor-color-trio-exercise]: https://exercism.org/tracks/python/exercises/resistor-color-trio diff --git a/exercises/practice/resistor-color-expert/.docs/introduction.md b/exercises/practice/resistor-color-expert/.docs/introduction.md index fd9e05efc4d..868b03c534e 100644 --- a/exercises/practice/resistor-color-expert/.docs/introduction.md +++ b/exercises/practice/resistor-color-expert/.docs/introduction.md @@ -1,10 +1,14 @@ # Introduction If you want to build something using a Raspberry Pi, you'll probably use _resistors_. -Like the previous `Resistor Color Duo` and `Resistor Color Trio` exercises, you will be translating resistor color bands to human-readable labels. +Like the previous [`Resistor Color Duo`][resistor-color-duo-exercise] and [`Resistor Color Trio`][resistor-color-trio-exercise] exercises, you will be translating resistor color bands to human-readable labels. - Each resistor has a resistance value. - Resistors are small - so small in fact that if you printed the resistance value on them, it would be hard to read. To get around this problem, manufacturers print color-coded bands onto the resistors to denote their resistance values. - Each band acts as a digit of a number. For example, if they printed a brown band (value 1) followed by a green band (value 5), it would translate to the number 15. + + +[resistor-color-duo-exercise]: https://exercism.org/tracks/python/exercises/resistor-color-duo +[resistor-color-trio-exercise]: https://exercism.org/tracks/python/exercises/resistor-color-trio diff --git a/exercises/practice/reverse-string/.approaches/additional-approaches/content.md b/exercises/practice/reverse-string/.approaches/additional-approaches/content.md new file mode 100644 index 00000000000..1ff735608c4 --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/additional-approaches/content.md @@ -0,0 +1,159 @@ +# Additional Approaches that are Further Afield + +Below are some interesting strategies that are distinct from the canonical approaches that have already been discussed. +While they do not offer particular performance boosts over the canonical approaches (_and some offer very large penalties_), they do explore interesting corners of Python. + + +## Convert the Input to a UTF-8 bytearray and use a Sliding Window to Reverse + + +```python +def reverse(text): + + # Create bytearrays for input and output. + given, output = bytearray(text.encode("utf-8")), bytearray(len(text)) + index = 0 + LENGTH_MASK = 0xE0 # this is 0b11110000 (binary) or 224 (decimal) + + # Loop through the input bytearray. + while index < len(given): + + #Either the len is 1 or it is calculated by counting the bits after masking. + seq_len = (not(given[index] >> 7) or + (given[index] & LENGTH_MASK).bit_count()) + + #Calculate the index start. + location = index + seq_len +1 + + #Prepend the byte segment to the output bytearray + output[-location:-index or None] = given[index:index + seq_len] + + #Increment the index count or slide the 'window'. + index += seq_len + + #Decode output to UTF-8 string and return. + return output.decode("utf-8") + +``` + +This strategy encodes the string into a UTF-8 [`bytearray`][bytearray]. +It then uses a `while` loop to iterate through the text, calculating the length of a sequence (or 'window') to slice from 'given' and prepend to 'output'. + The 'index' counter is then incremented by the length of the 'window'. + Once the 'index' is greater than the length of 'given', the 'output' bytearray is decoded into a UTF-8 string and returned. + This is (_almost_) the same set of operations as described in the code below, but operating on bytes in a bytearray, as opposed to text/codepoints in a `list` - although this strategy does not use `list.pop()` (_bytearray objects do not have a pop method_). + + This uses `O(n)` space for the output array. +It incurs additional runtime overhead by _prepending_ to the output array, which is an expensive operation that forces many repeated shifts. +Encoding to bytes and decoding to codepoints further slow this approach. + + +## Convert the Input to a list and use a While Loop to Pop and Append to a Second List + + +```python +def reverse(text): + codepoints, stniopedoc = list(text), [] + + while codepoints: + stniopedoc.append(codepoints.pop()) + + return ''.join(stniopedoc) +``` + +This strategy uses two lists. +One `list` for the codepoints in the text, and one to hold the codepoints in reverse order. +First, the input text is turned into a the 'codepoints' `list`, and iterated over. +Each codepoint is `pop()`ped from 'codepoints' and appended to the 'stniopedoc' `list`. +Finally, 'stniopedoc' is joined via `str.join()` to create the reversed string. + +While this is a straightforward and readable approach, it creates both memory and performance overhead, due to the creation of the lists and the use of `join()`. +This is much faster than the bytearray strategy or using string concatenation, but is still almost slower than the slicing strategy. +It also takes up `O(n)` auxiliary space with the stniopedoc list. + + + +## Using Recursion Instead of a Loop + + +```python +def reverse(text): + if len(text) == 0: + return text + else: + return reverse(text[1:]) + text[0] +``` + +This strategy uses a slice to copy all but the leftmost part of the string, concatenating the codepoint at the first index to the end. +The function then calls itself with the (now shorter) text slice. +This slice + concatenation process continues until the `len()` is 0, and the reversed text is returned up the call stack. +This is the same as iterating over the string backward in a `loop`, appending each codepoint to a new string, and has identical time complexity. +It also uses O(n) space, with the space being successive calls on the call stack. + +Because each recursive call is placed on the stack and Python limits recursive calls to a max of 1000, this code produces a `maximum recursion depth exceeded` error for any string longer than ~999 characters. + + +## Using `map()` and `lambbda` with `Join()` Instead of a Loop + +```python +def reverse(text): + return "".join(list(map(lambda x: text[(-x-1)], range(len(text))))) +``` + +This variation uses the built-in `map()` and a `lambda` to iterate over the string backward, constructing a `list`. +The `list` is then fed to `str.join()`, which unpacks it and turns it into a string. +This is a very non-performant way to walk the string backwards, and also incurs extra overhead due to the unneeded construction of an intermediary `list`. + +`map()` can instead be directly fed to `join()`, which improves performance to `O(n)`: + +```python +def reverse(text): + return "".join(map(lambda x: text[(-x-1)], range(len(text)))) +``` + + +## Using a `lambda` that returns a Reverse Sequence Slice + + +```python +reverse = lambda text: text[::-1] +``` + + +This strategy assigns the name "reverse" to a `lambda` that produces a reverse slice of the string. +This looks quite clever and is shorter than a "traditional" function, but it is far from obvious that this line defines a callable named "reverse" that returns a reversed string. +While this code compiles to the same function definition as the first approach article, it is not clear to many programmers who might read through this code that they could call `reverse('some_string')` the way they could call other functions. + + +This has the added disadvantage of creating troubleshooting issues since any errors will be attributed to `lambda` in the stack trace and not associated with an explicit function named `reverse`. +Help calls and `__repr__` calls are similarly affected. +This is not the intended use of `lambdas` (_which are for unnamed or anonymous functions_), nor does it confer any sort of performance boost over other methods, but _does_ create readability issues with anyone unfamiliar with `lambda` syntax and compilation. + + +## Timings vs Reverse Slice + + +As a (very) rough comparison, below is a timing table for these functions vs the canonical reverse slice: + + +| **string lengths >>>>** | Str Len: 5 | Str Len: 11 | Str Len: 22 | Str Len: 52 | Str Len: 68 | Str Len: 86 | Str Len: 142 | Str Len: 1420 | Str Len: 14200 | Str Len: 142000 | +|------------------------- |------------ |------------- |------------- |------------- |------------- |------------- |-------------- |--------------- |---------------- |----------------- | +| reverse slice | 1.66e-07 | 1.75e-07 | 1.79e-07 | 2.03e-07 | 2.22e-07 | 2.38e-07 | 3.63e-07 | 1.44e-06 | 1.17e-05 | 1.16e-04 | +| reverse lambda | 1.68e-07 | 1.72e-07 | 1.85e-07 | 2.03e-07 | 2.44e-07 | 2.35e-07 | 3.65e-07 | 1.47e-06 | 1.25e-05 | 1.18e-04 | +| reverse dual lists | 9.17e-07 | 1.56e-06 | 2.70e-06 | 5.69e-06 | 8.30e-06 | 1.07e-05 | 1.80e-05 | 1.48e-04 | 1.50e-03 | 1.53e-02 | +| reverse recursive | 8.74e-07 | 1.90e-06 | 4.02e-06 | 8.97e-06 | 1.24e-05 | 1.47e-05 | 3.34e-05 | --- | --- | --- | +| reverse bytes | 1.92e-06 | 3.82e-06 | 7.36e-06 | 1.65e-05 | 2.17e-05 | 2.71e-05 | 4.47e-05 | 5.17e-04 | 6.10e-03 | 2.16e-01 | + + +As you can see, the reverse using two lists and the reverse using a bytearray are orders of magnitude slower than using a reverse slice. +For the largest inputs measured, the dual list solution was almost 55x slower, and the bytearray solution was almost 1800x slower. +Timings for strings over 142 characters could not be run for the recursive strategy, due to Python's 1000 call recursion limit. + + +Measurements were taken on a 3.1 GHz Quad-Core Intel Core i7 Mac running MacOS Ventura. +Tests used `timeit.Timer.autorange()`, repeated 3 times. +Time is reported in seconds taken per string after calculating the 'best of' time. +The [`timeit`][timeit] module docs have more details, and [note.nkmk.me][note_nkmk_me] has a nice summary of methods. + +[bytearray]: https://docs.python.org/3/library/stdtypes.html#bytearray +[timeit]: https://docs.python.org/3/library/timeit.html#python-interface +[note_nkmk_me]: https://note.nkmk.me/en/python-timeit-measure/ diff --git a/exercises/practice/reverse-string/.approaches/additional-approaches/snippet.txt b/exercises/practice/reverse-string/.approaches/additional-approaches/snippet.txt new file mode 100644 index 00000000000..9bb10135a0f --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/additional-approaches/snippet.txt @@ -0,0 +1,8 @@ + given, output = bytearray(text.encode("utf-8")), bytearray(len(given)) + index, LENGTH_MASK = 0, 0xE0 # 0b11110000 or 224 + while index < len(given): + seq_len = not(given[index] >> 7) or (given[index] & LENGTH_MASK).bit_count() + location = index + seq_len +1 + output[-location:-index or None] = given[index:index + seq_len] + index += seq_len + return output.decode("utf-8") \ No newline at end of file diff --git a/exercises/practice/reverse-string/.approaches/backward-iteration-with-range/content.md b/exercises/practice/reverse-string/.approaches/backward-iteration-with-range/content.md new file mode 100644 index 00000000000..7b1ddd5b773 --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/backward-iteration-with-range/content.md @@ -0,0 +1,78 @@ +## Backward Iteration with Range + + +```python +def reverse(text): + output = "" + for index in range(len(text) - 1, -1, -1): #For 'Robot', this is 4 (start) 0 (stop), iterating (4,3,2,1,0) + output += text[index] + return output +``` + + +These variations all use the built-in [`range()`][range] object to iterate over the input text from right --> left, adding each codepoint to the output string. +This is the same as iterating over the text backward using one or more `index` variables, but incurs slightly less overhead by substituting `range()` for them. +Note that the code above also avoids _prepending_ to the output string. + +For very long strings, this code will still degrade to `O(n**2)` performance, due to the use of string concatenation. +Using `''.join()` here can avoid heavy concatenation penalty as strings grow longer and the CPython string append optimization mentioned in the [iteration and concatenation][approach-iteration-and-concatenation] approach breaks down. + + +## Variation #1: Forward Iteration in Range, Negative Index + + +```python +def reverse(text): + output = '' + + for index in range(1, len(text) + 1): + output += text[-index] + return output +``` + + +This version iterates left --> right using a positive `range()` and then _prepends to the string_ by using a negative index for the codepoint. +This has the same faults as variation #1, with the added cost of prepending via concatenation. + + +## Variation #2: Feed Range and the Index into Join() + +```python +def reverse(text): + return "".join(text[index] for index in range(len(text)-1, -1, -1)) + ``` + + This version omits the intermediary output string, and uses `"".join()` directly in the return. + Within the `join()` call, `range()` is used with a negative step to iterate over the input text backward. + + This strategy avoids the penalties of string concatenation with an intermediary string. + It is still `O(n)` in time complexity, and is slower than reverse indexing due to the calls to `join()`, `len()` and `range()`, and the creation of the generator expression. + + Because of the aforementioned string append optimization in CPython, this approach will benchmark slower for strings under length 1000, but becomes more and more efficient as the length of the string grows. + Since the CPython optimization is not stable nor transferable to other versions of Python, using `join()` by default is recommended in any situation where the string concatenation is not strictly repetition and length constrained. + + +## Timings vs Reverse Slice + + + As a (very) rough comparison, below is a timing table for these functions vs the canonical reverse slice: + + + +| **string length >>>>** | 5 | 11 | 22 | 52 | 66 | 86 | 142 | 1420 | 14200 | 142000 | +|------------------------ |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: | +| reverse slice | 1.68e-07 | 1.74e-07 | 1.83e-07 | 2.07e-07 | 2.14e-07 | 2.29e-07 | 3.51e-07 | 1.50e-06 | 1.19e-05 | 1.17e-04 | +| reverse negative range | 5.89e-07 | 9.93e-07 | 1.78e-06 | 3.69e-06 | 4.71e-06 | 5.83e-06 | 9.61e-06 | 1.39e-04 | 1.46e-03 | 1.81e-02 | +| reverse positive range | 6.20e-07 | 1.14e-06 | 2.23e-06 | 4.54e-06 | 5.74e-06 | 7.38e-06 | 1.20e-05 | 1.70e-04 | 1.75e-03 | 2.07e-02 | +| reverse range and join | 8.90e-07 | 1.31e-06 | 2.14e-06 | 4.15e-06 | 5.22e-06 | 6.57e-06 | 1.06e-05 | 1.05e-04 | 1.04e-03 | 1.07e-02 | + + +Measurements were taken on a 3.1 GHz Quad-Core Intel Core i7 Mac running MacOS Ventura. +Tests used `timeit.Timer.autorange()`, repeated 3 times. +Time is reported in seconds taken per string after calculating the 'best of' time. +The [`timeit`][timeit] module docs have more details, and [note.nkmk.me][note_nkmk_me] has a nice summary of methods. + +[approach-iteration-and-concatenation]: https://exercism.org/tracks/python/exercises/reverse-string/.approaches/iteration-and-concatenation +[note_nkmk_me]: https://note.nkmk.me/en/python-timeit-measure/ +[range]: https://docs.python.org/3/library/stdtypes.html#range +[timeit]: https://docs.python.org/3/library/timeit.html#python-interface diff --git a/exercises/practice/reverse-string/.approaches/backward-iteration-with-range/snippet.txt b/exercises/practice/reverse-string/.approaches/backward-iteration-with-range/snippet.txt new file mode 100644 index 00000000000..cdb261d85aa --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/backward-iteration-with-range/snippet.txt @@ -0,0 +1,5 @@ +def reverse(text): + new_word = "" + for index in range(len(text) - 1, -1, -1): + new_word += text[index] + return new_word diff --git a/exercises/practice/reverse-string/.approaches/built-in-list-reverse/content.md b/exercises/practice/reverse-string/.approaches/built-in-list-reverse/content.md new file mode 100644 index 00000000000..b195d099a59 --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/built-in-list-reverse/content.md @@ -0,0 +1,83 @@ +# Make the Input Text a List and Use list.reverse() to Reverse In-Place + + +```python +def reverse(text): + output = list(text) + output.reverse() + + return ''.join(output) +``` + +These approaches start with turning the text into a `list` of codepoints. +Rather than use a loop + append to then reverse the text, the [`list.reverse()`][list-reverse-method] method is used to perform an in-place reversal. +`join()` is then used to turn the list into a string. + +This takes `O(n)` time complexity because `list.reverse()` & `join()` iterate through the entire `list`. +It uses `O(n)` space for the output `list`. + +`list.reverse()` cannot be fed to `join()` here because it returns `None` as opposed to returning the `list`. +Because `list.reverse()` **mutates the list**, it is not advisable in situations where you want to preserve the original `list` of codepoints. + + +## Variation #1: Keep a Copy of the Original Ordering of Codepoints + + +```python +def reverse(text): + codepoints, output = list(text), list(text) + output.reverse() + return ''.join(output) +``` + +This variation is essentially the same as the solution above, but makes a codepoints list to keep the original codepoint ordering of the input text. +This does add some time and space overhead. + + +## Variation #2: Iterate Through the String and Append to Create List Before Reversing + + +```python +def reverse(text): + output = [] + + for item in text: + output.append(item) + + output.reverse() + + return ''.join(output) +``` + +This variation declares output as an empty literal, then loops through the codepoints of the input text and appends them to output. +`list.reverse()` is then called to reverse output in place. + Finally, output is joined into a string via `str.join()`. +Using this method is the same as calling the `list` constructor directly on the input text (_`list(text)`_), which will iterate through it automatically. + Calling the constructor is also quite a bit faster than using a "written out" `for-loop`. + + +## Timings vs Reverse Slice + + +As a (very) rough comparison, below is a timing table for these functions vs the canonical reverse slice: + +As you can see, using `list.reverse()` after converting the input text to a list is much slower than using a reverse slice. +Iterating in a loop to create the output list also adds even more time. + + +| **string lengths >>>>** | 5 | 11 | 22 | 52 | 66 | 86 | 142 | 1420 | 14200 | 142000 | +|------------------------- |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: | +| reverse slice | 1.70e-07 | 1.74e-07 | 1.00e-07 | 2.06e-07 | 2.20e-07 | 2.39e-07 | 3.59e-07 | 1.47e-06 | 1.22e-05 | 1.20e-04 | +| reverse reverse method | 3.28e-07 | 2.00e-07 | 5.39e-07 | 8.96e-07 | 1.35e-06 | 1.55e-06 | 2.31e-06 | 2.01e-05 | 1.93e-04 | 1.94e-03 | +| reverse iterate list | 4.74e-07 | 7.60e-07 | 1.25e-06 | 2.75e-06 | 3.53e-06 | 4.52e-06 | 7.22e-06 | 6.07e-05 | 5.84e-04 | 6.28e-03 | + + +Measurements were taken on a 3.1 GHz Quad-Core Intel Core i7 Mac running MacOS Ventura. +Tests used `timeit.Timer.autorange()`, repeated 3 times. +Time is reported in seconds taken per string after calculating the 'best of' time. +The [`timeit`][timeit] module docs have more details, and [note.nkmk.me][note_nkmk_me] has a nice summary of methods. + + +[list-reverse-method]: https://docs.python.org/3/library/stdtypes.html#mutable-sequence-types +[note_nkmk_me]: https://note.nkmk.me/en/python-timeit-measure/ +[timeit]: https://docs.python.org/3/library/timeit.html#python-interface diff --git a/exercises/practice/reverse-string/.approaches/built-in-list-reverse/snippet.txt b/exercises/practice/reverse-string/.approaches/built-in-list-reverse/snippet.txt new file mode 100644 index 00000000000..8a999c3831e --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/built-in-list-reverse/snippet.txt @@ -0,0 +1,5 @@ +def reverse(text): + output = list(text) + output.reverse() + + return ''.join(output) \ No newline at end of file diff --git a/exercises/practice/reverse-string/.approaches/built-in-reversed/content.md b/exercises/practice/reverse-string/.approaches/built-in-reversed/content.md new file mode 100644 index 00000000000..62050382629 --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/built-in-reversed/content.md @@ -0,0 +1,54 @@ +# Use the built-in reversed() and Unpack with join() + + +```python +def reverse(text): + return (''.join(reversed(text))) +``` + +This approach calls the built-in `reversed()` function to return a [reverse iterator](https://docs.python.org/3/library/functions.html#reversed) that is then unpacked by `str.join()`. +This is equivalent to using a reverse slice, but incurs a bit of extra overhead due to the unpacking/iteration needed by `str.join()`. +This takes `O(n)` time and `O(n)` space for the reversed copy. + + +```python +def reverse(text): + output = '' + for index in reversed(range(len(text))): + output += text[index] + return output +``` + +This version uses `reversed()` to reverse a `range()` object rather than feed a start/stop/step to `range()` itself. +It then uses the reverse range to iterate over the input string and concatenate each code point to a new 'output' string. +This has over-complicated `reversed()`, as it can be called directly on the input string with almost no overhead. +This has also incurs the performance hit of repeated concatenation to the 'output' string. + +While this approach _looks_ as if it would be similar to the first, it is actually `O(n**2)` in time complexity due to string concatenation. +It was also the slowest in benchmarks. + + +## Timings vs Reverse Slice + + +As a (very) rough comparison, below is a timing table for these functions vs the canonical reverse slice: + +While `reversed()` is very fast, the call to `join()` to unpack slows things down compared to using a reverse slice. +For long strings, this slight overhead starts to become significant. +Using `reversed()` but concatenating to a string is non-performant in this context. + + +| **string length >>>>** | 5 | 11 | 22 | 52 | 66 | 86 | S142 | 1420 | 14200 | 142000 | +|------------------------ |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: | +| reverse slice | 1.70e-07 | 1.78e-07 | 1.89e-07 | 2.10e-07 | 2.25e-07 | 2.40e-07 | 3.56e-07 | 1.52e-06 | 1.22e-05 | 1.20e-04 | +| reverse reversed | 3.71e-07 | 4.77e-07 | 6.78e-07 | 1.20e-06 | 1.63e-06 | 1.01e-06 | 2.78e-06 | 2.47e-05 | 2.44e-04 | 2.40e-03 | +| reverse reversed range | 6.34e-07 | 1.05e-06 | 1.85e-06 | 3.85e-06 | 4.73e-06 | 6.10e-06 | 9.77e-06 | 1.44e-04 | 1.53e-03 | 1.89e-02 | + + +Measurements were taken on a 3.1 GHz Quad-Core Intel Core i7 Mac running MacOS Ventura. +Tests used `timeit.Timer.autorange()`, repeated 3 times. +Time is reported in seconds taken per string after calculating the 'best of' time. +The [`timeit`][timeit] module docs have more details, and [note.nkmk.me][note_nkmk_me] has a nice summary of methods. + +[note_nkmk_me]: https://note.nkmk.me/en/python-timeit-measure/ +[timeit]: https://docs.python.org/3/library/timeit.html#python-interface diff --git a/exercises/practice/reverse-string/.approaches/built-in-reversed/snippet.txt b/exercises/practice/reverse-string/.approaches/built-in-reversed/snippet.txt new file mode 100644 index 00000000000..a45b911005e --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/built-in-reversed/snippet.txt @@ -0,0 +1,2 @@ +def reverse(text): + return (''.join(reversed(text))) diff --git a/exercises/practice/reverse-string/.approaches/config.json b/exercises/practice/reverse-string/.approaches/config.json new file mode 100644 index 00000000000..6623bb52d90 --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/config.json @@ -0,0 +1,57 @@ +{ + "introduction": { + "authors": ["bethanyg", "colinleach"], + "contributors": [] + }, + "approaches": [ + { + "uuid": "e124fe69-dbef-4aaf-8910-706b5e3ce6bd", + "slug": "sequence-slicing", + "title": "Sequence Slicing", + "blurb": "Use a slice with a negative step to reverse the string.", + "authors": ["bethanyg"] + }, + { + "uuid": "cbe2766f-e02f-4160-8227-eead7b4ca9fb", + "slug": "iteration-and-concatenation", + "title": "Iteration and Concatenation", + "blurb": "Iterate through the codepoints and concatenate them to a new string.", + "authors": ["bethanyg"] + }, + { + "uuid": "894b1c9b-e256-471e-96f6-02453476ccc4", + "slug": "backward-iteration-with-range", + "title": "Backward iteration with Range", + "blurb": "Use a negative step with range() to iterate backward and append to a new string.", + "authors": ["bethanyg"] + }, + { + "uuid": "722e8d0e-a8d1-49a7-9b6f-38da0f7380e6", + "slug": "list-and-join", + "title": "Make a list and use join()", + "blurb": "Create a list from the string and use join to make a new string.", + "authors": ["bethanyg"] + }, + { + "uuid": "b2c8e7fa-8265-4221-b0be-c1cd13166925", + "slug": "built-in-list-reverse", + "title": "Use the built-in list.reverse() function.", + "blurb": "Create a list of codepoints, use list.reverse() to reverse in place, and join() to make a new string.", + "authors": ["bethanyg"] + }, + { + "uuid": "cbb4411a-4652-45d7-b73c-ca116ccd4f02", + "slug": "built-in-reversed", + "title": "Use the built-in reversed() function.", + "blurb": "Use reversed() and unpack it with join() to make a new string.", + "authors": ["bethanyg"] + }, + { + "uuid": "1267e48f-edda-44a7-a441-a36155a8fba2", + "slug": "additional-approaches", + "title": "Additional approaches that are further afield", + "blurb": "Additional interesting approaches.", + "authors": ["bethanyg"] + } + ] +} \ No newline at end of file diff --git a/exercises/practice/reverse-string/.approaches/introduction.md b/exercises/practice/reverse-string/.approaches/introduction.md new file mode 100644 index 00000000000..b20a312fdb7 --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/introduction.md @@ -0,0 +1,171 @@ +# Introduction + + +The goal of the Reverse String exercise is to output a given string in reverse order. +It can be solved in a lot of different ways in Python, with a near-endless amount of variation. + +However, not all strategies are efficient, concise, or small in memory. +Care must be taken to not inadvertently slow down the code by using methods that don't scale well. + +Additionally, most 'canonical' solutions for reversing a string using the Python standard library do not account for Unicode text beyond the ASCII (0-127) range. + + +In this introduction, we cover six general approaches and an additional group of 'interesting' takes, but there are many more techniques that could be used. + +1. Sequence Slice with Negative Step +2. Iteration with String Concatenation +3. Reverse Iteration with Range() +4. Make a list and Use str.join() +5. Make a list and use list.reverse() +6. Use the built-in reversed() +7. Other [interesting approaches][approach-additional-approaches] + +We encourage you to experiment and get creative with the techniques you use, and see how it changes the way you think about the problem and think about Python. + + +And while Unicode text is outside the core tests for this exercise (_there are optional tests in the test file you can enable for Unicode_), we encourage you to give reversing strings that have non ASCII text a try. + + +## Approach: Sequence Slice with a Negative Step + +```python +def reverse(text): + return text[::-1] +``` + +This is "THE" canonical solution, _provided_ you know what encoding and character sets you are dealing with. +For example, if you know all of your text is **always** going to be within the ASCII space, this is by far the most succinct and performant way to reverse a string in Python. + +For more details, see the [sequence slicing approach][approach-sequence-slicing] + + +## Approach: Iterate over the String; Concatenate to a New String + + +```python +def reverse(text): + output = '' + for codepoint in text: + output = codepoint + output + return output +``` + +This approach iterates over the string, concatenating each codepoint to a new string. +This approach and its variants avoid all use of built-ins such as `range()`, `reversed()`, and `list.reverse()`. +But for very long strings, this approach can degrade performance toward O(n**2). + +For more information and relative performance timings for this group, check out the [iteration and concatenation][approach-iteration-and-concatenation] approach. + + +## Approach: Use range() to Iterate Backwards over the String, Append to New String + + +```python +def reverse(text): + new_word = "" + + for index in range(len(text) - 1, -1, -1): #For 'Robot', this is 4 (start) 0 (stop), iterating (4,3,2,1,0) + new_word += text[index] + return new_word +``` + +This method uses the built-in [`range()`][range] object to iterate over text right-to-left, adding each codepoint to the 'new_word' string. +This is essentially the same technique as the approach above, but incurs slightly less overhead by avoiding the potential performance hit of _prepending_ to the 'new_word' string, or creating index or tracking variables. + +For very long strings, this approach will still degrade to `O(n**2)` performance, due to the use of string concatenation. +Using `''.join()` here can avoid the concatenation penalty. +For more information and relative performance timings for this group, check out the [backwards iteration with range][approach-backward-iteration-with-range] approach. + + +## Approach: Create a List and Use str.join() to make new String. + + +```python +def reverse(text): + output = [] + + for codepoint in text: + output.insert(0,codepoint) + return "".join(output) +``` + +This approach either breaks the string up into a list of codepoints to swap or creates an empty list as a "parking place" to insert or append codepoints. +It then iterates over the text, swapping, inserting, or appending each codepoint to the output list. +Finally, `str.join()` is used to re-assemble the `list` into a string. + +For more variations and relative performance timings for this group, check out the [list and join][approach-list-and-join] approach. + + +## Approach: Make the Input Text a List & Use list.reverse() to Reverse in Place + + +```python +def reverse(text): + output = list(text) + output.reverse() + + return ''.join(output) +``` + +This approach turns the string into a list of codepoints and then uses the `list.reverse()` method to re-arrange the list _in place_. +After the reversal of the list, `str.join()` is used to create the reversed string. + +For more details, see the [built in list.reverse()][approach-built-in-list-reverse] approach. + + +## Approach: Use the built-in reversed() Function & join() to Unpack + + +```python +def reverse(text): + return (''.join(reversed(text))) +``` + +This approach calls the built-in `reversed()` function to return a [reverse iterator](https://docs.python.org/3/library/functions.html#reversed) that is then unpacked by `str.join()`. +This is equivalent to using a reverse slice, but incurs a bit of extra overhead due to the unpacking/iteration needed by `str.join()`. + +For more details, see the [built-in reversed()][approach-built-in-reversed] approach. + + +```python +def reverse(text): + output = '' + for index in reversed(range(len(text))): + output += text[index] + return output +``` + +This version uses `reversed()` to reverse a `range()` object rather than feed a start/stop/step to `range()` itself. +It then uses the reverse range to iterate over the input string and concatenate each code point to a new 'output' string. +This has over-complicated `reversed()` a bit, as it can be called directly on the input string with almost no overhead. +This has also incurred the performance hit of repeated concatenation to the 'output' string. + +## Other Interesting Approaches + +These range from using recursion to converting text to bytes before processing. +Some even use `map()` and or a `lambda` + +Take a look at the [additional approaches][approach-additional-approaches] 'approach' for more details and timings. + + +## Which Approach to Use? + +The fastest and most canonical by far is the reverse slice. +Unless you are in an interview situation where you need to "show your work", or working with varied Unicode outside the ASCII range, a reverse slice is the easiest and most direct method of reversal. + +A reverse slice will also work well for varied Unicode that has been pre-processed to ensure that multibyte characters and combined letters with diacritical and accent marks ('extended graphemes') remain grouped. + + +For other scenarios, converting the intput text to a `list`, swapping or iterating, and then using `join()` is recommended. + +To compare performance of these approach groups, see the [Performance article][article-performance]. + +[approach-additional-approaches]: https://exercism.org/tracks/python/exercises/reverse-string/approaches/additional-approaches +[approach-backward-iteration-with-range]: https://exercism.org/tracks/python/exercises/reverse-string/approaches/backward-iteration-with-range +[approach-built-in-list-reverse]: https://exercism.org/tracks/python/exercises/reverse-string/approaches/built-in-list-reverse +[approach-built-in-reversed]: https://exercism.org/tracks/python/exercises/reverse-string/approaches/built-in-reversed +[approach-iteration-and-concatenation]: https://exercism.org/tracks/python/exercises/reverse-string/approaches/iteration-and-concatenation +[approach-list-and-join]: https://exercism.org/tracks/python/exercises/reverse-string/approaches/list-and-join +[approach-sequence-slicing]: https://exercism.org/tracks/python/exercises/reverse-string/approaches/sequence-slicing +[article-performance]: https://exercism.org/tracks/python/exercises/reverse-string/articles/performance +[range]: https://docs.python.org/3/library/stdtypes.html#range diff --git a/exercises/practice/reverse-string/.approaches/iteration-and-concatenation/content.md b/exercises/practice/reverse-string/.approaches/iteration-and-concatenation/content.md new file mode 100644 index 00000000000..7acc4d7c66c --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/iteration-and-concatenation/content.md @@ -0,0 +1,110 @@ +# Iteration and Concatenation + + +```python +def reverse(text): + output = '' + for codepoint in text: + output = codepoint + output + return output +``` + +The variations here all iterate over the string, concatenating each codepoint to a new string. +While this avoids all use of built-ins such as `range()`, `reversed()`, and `list.reverse()`, it incurs both a memory and speed penalty over using a reverse slice. + +Strings are immutable in Python. +Using concatenation via `+` or `+=` forces the re-creation of the 'output' string for _every codepoint added from the input string._ +That means the code has a minimum time complexity of `O(m + n)`, where `n` is the length of the text being iterated over, and `m` is the number of concatenations to the 'output' string. +For some more detail on `O(n + m)` vs `O(n)`, see this [Stack Overflow post][time-complexity-omn-vs-on]. +The code also uses `O(n)` space to store 'output'. + +As input strings grow longer, concatenation can become even more problematic, and performance can degrade to `O(n**2)`, as longer and longer shifts and reallocations occur in memory. +In fact, the "standard" way to describe the time complexity of this code is to say that is O(n**2), or quadratic. + +Interestingly, CPython includes an optimization that attempts to avoid the worst of the shift and reallocation behavior by reusing memory when it detects that a string append is happening. +Because the code above _prepends_ the codepoint to the left-hand side of 'output', this optimization cannot be used. +Even in cases where strings are appended to, this optimization cannot be relied upon to be stable and is not transferable to other implementations of Python. + +For some interesting reading on this topic, see these Stack Overflow posts: +- [Time Complexity of String Concatenation in Python][time-complexity-of-string-concatenation-in-python], +- [Time Complexity of Iterative String Append][time-complexity-of-iterative-string-append], +- [Most efficient String Concatenation Method in Python][most-efficient-string-concatenation-method-in-Python], +- [join() is faster than +, but what is wrong here?][join() is faster than +, but what is wrong here?], and +- [Is += bad practice in Python?][is += bad practice in Python?] + +To see the difference between reverse slicing and looping in terms of steps, check out [slicing verses iterating+concatenation][python-tutor] at the PythonTutor site. + + +## Variation #1: Using a While Loop and a Negative Index + + +```python +def reverse(text): + output = '' + index = -1 + + while index >= -len(text): + output += text[index] + index -= 1 + return output +``` + +This solution uses a while loop to "count down" the length of the string using a negative index. +Each number is used to index into the input string and concatenate the resulting codepoint to a new string. +Because each index is further from zero than the last, this has the effect of "iterating backward" over the input string. + +This approach incurs additional overhead for length checking the input string repeatedly in the loop, and setting/decrementing the index variable, both of which can be avoided by using the built-in `range()` object. +Overall, this was the slowest of the three variations when timed. + + +## Variation #2: Using a While Loop with a Positive Index + + +```python +def reverse(text): + result ='' + index = len(text)-1 + + while index >= 0: + result += text[index] + index -= 1 + return result +``` + +This solution uses a while loop to "count down" the length of the string until it reaches zero using a positive index. +Each number is used to index into the input string and concatenate the resulting codepoint to a new string. +Because each index is closer to zero than the last, this has the effect of also "iterating backward" over the input string. +Algorithmically, this takes as much tine and space as the code samples above, since it uses an intermediate string for the reversal and must loop through every codepoint in the input. + + +## Timings vs Reverse Slice + + +As seen in the table below, all of these approaches are slower than using a reverse slice. +Interestingly, iteration + prepending to the string is fastest in this group for strings under length 1420. +But keep in mind that in general, string concatenation and prepending should be avoided for any 'industrial strength' use cases. + + +| **string length >>>>** | 5 | 11 | 22 | 52 | 66 | 86 | 142 | 1420 | 14200 | 142000 | +|------------------------ |---------- |---------- |---------- |---------- |---------- |---------- |---------- |---------- |---------- |---------- | +| reverse slice | 1.66e-07 | 1.73e-07 | 1.88e-07 | 1.12e-07 | 2.15e-07 | 2.32e-07 | 3.46e-07 | 1.42e-06 | 1.18e-05 | 1.15e-04 | +| reverse string prepend | 4.28e-07 | 8.05e-07 | 1.52e-06 | 3.45e-06 | 4.82e-06 | 5.55e-06 | 9.83e-06 | 2.23e-04 | 2.96e-03 | 5.17e-01 | +| reverse positive index | 4.65e-07 | 8.85e-07 | 1.73e-06 | 3.70e-06 | 4.83e-06 | 6.55e-06 | 1.01e-05 | 1.54e-04 | 1.60e-03 | 2.61e-02 | +| reverse negative index | 5.65e-07 | 1.32e-06 | 2.61e-06 | 5.91e-06 | 7.62e-06 | 4.00e-06 | 1.62e-05 | 2.16e-04 | 2.19e-03 | 2.48e-02 | + + +Measurements were taken on a 3.1 GHz Quad-Core Intel Core i7 Mac running MacOS Ventura. +Tests used `timeit.Timer.autorange()`, repeated 3 times. +Time is reported in seconds taken per string after calculating the 'best of' time. +The [`timeit`][timeit] module docs have more details, and [note.nkmk.me][note_nkmk_me] has a nice summary of methods. + +[python-tutor]: https://pythontutor.com/render.html#code=def%20reverse_loop%28text%29%3A%0A%20%20%20%20output%20%3D%20''%0A%20%20%20%20for%20letter%20in%20text%3A%0A%20%20%20%20%20%20%20%20output%20%3D%20letter%20%2B%20output%0A%20%20%20%20return%20output%0A%20%20%20%20%0Adef%20reverse_slice%28text%29%3A%0A%20%20%20%20return%20text%5B%3A%3A-1%5D%0A%20%20%20%20%0A%0Aprint%28reverse_loop%28'Robot'%29%29%0Aprint%28reverse_slice%28'Robot'%29%29&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false + +[is += bad practice in Python?]: https://stackoverflow.com/questions/39675898/is-python-string-concatenation-bad-practice +[join() is faster than +, but what is wrong here?]: https://stackoverflow.com/a/1350289 +[most-efficient-string-concatenation-method-in-Python]: https://stackoverflow.com/questions/1316887/what-is-the-most-efficient-string-concatenation-method-in-python +[note_nkmk_me]: https://note.nkmk.me/en/python-timeit-measure/ +[time-complexity-of-iterative-string-append]: https://stackoverflow.com/questions/34008010/is-the-time-complexity-of-iterative-string-append-actually-on2-or-on +[time-complexity-of-string-concatenation-in-python]: https://stackoverflow.com/questions/37133547/time-complexity-of-string-concatenation-in-python +[time-complexity-omn-vs-on]: https://cs.stackexchange.com/questions/139307/time-complexity-omn-vs-on +[timeit]: https://docs.python.org/3/library/timeit.html#python-interface diff --git a/exercises/practice/reverse-string/.approaches/iteration-and-concatenation/snippet.txt b/exercises/practice/reverse-string/.approaches/iteration-and-concatenation/snippet.txt new file mode 100644 index 00000000000..d4758b06017 --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/iteration-and-concatenation/snippet.txt @@ -0,0 +1,5 @@ +def reverse(text): + output = '' + for codepoint in text: + output = codepoint + output + return output diff --git a/exercises/practice/reverse-string/.approaches/list-and-join/content.md b/exercises/practice/reverse-string/.approaches/list-and-join/content.md new file mode 100644 index 00000000000..07f7daa03f2 --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/list-and-join/content.md @@ -0,0 +1,148 @@ +# Create a List and Use str.join() to Make A New String + + +To avoid performance issues with concatenating to a string, this group of approaches uses one or more `list`s to perform swaps or reversals before joining the codepoints back into a string. +This avoids the `O(n**2)` danger of repeated shifting/reallocation when concatenating long strings. +However, the use of `join()` and other techniques still make all of these solutions `O(n)` - `O(n+m)` in time complexity. + + +```python +def reverse(text): + output = [] + + for codepoint in text: + output.insert(0,codepoint) + return "".join(output) +``` + +The code above iterates over the codepoints in the input text and uses `list.insert()` to insert each one into the output list. +Note that `list.insert(0, codepoint)` _prepends_, which is very inefficient for `lists`, while appending takes place in (amortized) O(1) time. +So this code incurs a time penalty because it forces repeated shifts of every element in the list with every insertion. +A small re-write using `range()` to change the iteration direction will boost performance: + + +## Variation #1: Use Range to Iterate Over the String Backward and list.append() to Output + + +```python +def reverse(text): + output = [] + length = len(text)-1 + + for index in range(length, -1, -1): + output.append(text[index]) + return "".join(output) +``` + +This code iterates backward over the string using `range()`, and can therefore use `list.append()` to append to the output list in (amortized) constant time. +However, the use of `join()` to unpack the list and create a string still makes this `O(n)`. +This also takes `O(n)` space for the output `list`. + + +## Variation #2: Convert Text to List and Use range() to Iterate over 1/2 the String, Swapping Values + + +```python +def reverse(text): + output = list(text) + length = len(text) // 2 #Cut the amount of iteration in half + + for index in range(length): + + #Swap values at given indexes + output[index], output[length - index - 1] = output[length - index - 1], output[index] + return ''.join(output) +``` + + +This variation calculates a median which is then used with `range()` in a `for loop` to iterate over _half_ the indexes in the 'output' list, swapping values into their reversed places. +`str.join()` is then used to create a new string. +This technique is quite speedy, and re-arranges the list of codepoints 'in place', avoiding expensive string concatenation. +It is still `O(n)` time complexity because `list()` and `join()` each force iteration over the entire length of the input string. + + +## Variation #3: Convert Text to List, Use Start and End Variables to Iterate and Swap Values + + +```python +def reverse(text): + output = list(text) + start = 0 + end = len(text) - 1 + + while start < end: + #Swap values in output until the indexes meet at the 'center' + output[start], output[end] = output[end], output[start] + start += 1 + end -= 1 + return "".join(output) +``` + + +This variation 'automatically' finds the midpoint by incrementing and decrementing 'start' and 'end' variables. +Otherwise, it is identical to variation 2. + + +## Variation #4: Convert Text to Bytearray, Iterate and Swap + + +```python +def reverse(text): + output = bytearray(text.encode("utf-8")) + length = len(output) + + for index in range(length//2): + output[index], output[length-1-index] = output[length-1-index], output[index] + return output.decode("utf-8") +``` + + +This variation is operationally the same as variations #2 & #3 above, except that it encodes the string to a `utf-8` [bytearray](https://docs.python.org/3/library/stdtypes.html#bytearray). + It then iterates over the bytearray to perform the swaps. +Finally, the bytearray is decoded into a `utf-8` string to return the reversed word. +This incurs overhead when encoding/decoding to and from the `bytearray`. +This also throws an ` UnicodeDecodeError: invalid start byte` when working with any multi-byte codepoints because no check was conducted to keep multibyte codepoints grouped together during the reversal. + +Because of this issue, no timings are available for this variation. +For code that keeps bytes together correctly, see the bytearray variation in the [additional approaches][approach-additional-approaches] approach. + + +## Variation #5: Use Generator Expression with Join to Iterate Backwards Over Codepoints List + +```python +def reverse(text): + codepoints = list(text) + length = len(text) - 1 + return "".join(codepoints[index] for index in range(length, -1, -1)) +``` + +This variation puts the for/while loop used in other strategies directly into `join()` using a generator expression. +The text is first converted to a list and the generator-expression "swaps" the codepoints over the whole `list`, using `range()` for the indexes. +Interestingly, because of the work to create and manage the generator, this variation is actually _slower_ than using an auxiliary `list` and `loop` to manage codepoints and then calling `join()` separately. + + +## Timings vs Reverse Slice + + +As a (very) rough comparison, below is a timing table for these functions vs the canonical reverse slice: + + +| **string lengths >>>>** | 5 | 11 | 22 | 52 | 66 | 86 | 142 | 1420 | 14200 | 142000 | +|------------------------- |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: | +| reverse slice | 1.67e-07 | 1.76e-07 | 1.85e-07 | 2.03e-07 | 2.12e-07 | 2.32e-07 | 3.52e-07 | 1.47e-06 | 1.20e-05 | 1.17e-04 | +| reverse auto half swap | 4.59e-07 | 7.53e-07 | 1.16e-06 | 2.25e-06 | 3.08e-06 | 3.80e-06 | 5.97e-06 | 7.08e-05 | 7.21e-04 | 7.18e-03 | +| reverse half swap | 6.34e-07 | 9.24e-07 | 1.51e-06 | 2.91e-06 | 3.71e-06 | 4.53e-06 | 7.52e-06 | 2.52e-04 | 1.01e-03 | 1.05e-02 | +| reverse append | 6.44e-07 | 1.00e-06 | 1.56e-06 | 3.28e-06 | 4.48e-06 | 5.54e-06 | 8.89e-06 | 2.20e-04 | 8.73e-04 | 9.10e-03 | +| reverse generator join | 1.02e-06 | 1.39e-06 | 2.16e-06 | 4.13e-06 | 5.31e-06 | 6.79e-06 | 1.11e-05 | 1.07e-04 | 1.07e-03 | 1.05e-02 | +| reverse insert | 5.29e-07 | 9.10e-07 | 1.64e-06 | 3.77e-06 | 4.90e-06 | 6.86e-06 | 1.14e-05 | 2.70e-04 | 2.35e-02 | 2.74e+00 | + + + +Measurements were taken on a 3.1 GHz Quad-Core Intel Core i7 Mac running MacOS Ventura. +Tests used `timeit.Timer.autorange()`, repeated 3 times. +Time is reported in seconds taken per string after calculating the 'best of' time. +The [`timeit`][timeit] module docs have more details, and [note.nkmk.me][note_nkmk_me] has a nice summary of methods. + +[note_nkmk_me]: https://note.nkmk.me/en/python-timeit-measure/ +[timeit]: https://docs.python.org/3/library/timeit.html#python-interface +[approach-additional-approaches]: https://exercism.org/tracks/python/exercises/reverse-string/.approaches/additional-approaches diff --git a/exercises/practice/reverse-string/.approaches/list-and-join/snippet.txt b/exercises/practice/reverse-string/.approaches/list-and-join/snippet.txt new file mode 100644 index 00000000000..4ecd5cb1819 --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/list-and-join/snippet.txt @@ -0,0 +1,8 @@ +def reverse(text): + output = list(text) + start, end = 0, len(text) - 1 + while start < end: + output[start], output[end] = output[end], output[start] + start += 1 + end -= 1 + return "".join(output) \ No newline at end of file diff --git a/exercises/practice/reverse-string/.approaches/sequence-slicing/content.md b/exercises/practice/reverse-string/.approaches/sequence-slicing/content.md new file mode 100644 index 00000000000..2c85dbf19cc --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/sequence-slicing/content.md @@ -0,0 +1,48 @@ +# Sequence Slice with Negative Step + + +```python +def reverse(text): + return text[::-1] +``` + +This approach uses Python's negative indexes and _[sequence slices][sequence slicing]_ to iterate over the string in reverse order, returning a reversed copy. + + + + + + +
index from left ⟹






+ +| 0
πŸ‘‡πŸΎ | 1
πŸ‘‡πŸΎ | 2
πŸ‘‡πŸΎ | 3
πŸ‘‡πŸΎ | 4
πŸ‘‡πŸΎ | 5
πŸ‘‡πŸΎ | +|:--------: |:--------: |:--------: |:--------: |:--------: |:--------: | +| P | y | t | h | o | n | +| πŸ‘†πŸΎ
-6 | πŸ‘†πŸΎ
-5 | πŸ‘†πŸΎ
-4 | πŸ‘†πŸΎ
-3 | πŸ‘†πŸΎ
-2 | πŸ‘†πŸΎ
-1 | +





⟸ index from right
+ +Slices use **`[ : : ]`** syntax. +The space before the first `:` indicates which index to start iterating from (_inclusive_), the space before the second `:` indicates which index to stop before (_exclusive_), and the final space after the second `:` indicates the direction of iteration and size of the 'step'. + A positive step moves left --> right and a negative step moves right --> left. + If start/stop indexes are omitted, Python assumes 'start of string' and 'end of string'. +Omitting the step defaults to a step of +1, but any size step can be used. +Slices return a _copy_ of the original object. +This same syntax works on `strings`, `bytearray`, `lists`, `tuples`, and `ranges`, which are all sequence types. + + +Reverse slicing has `O(n)` time complexity - the amount of time/work scales directly with the length of the string being iterated through and reversed. +And since slicing returns copy, the space for the copy also scales with the size of the input. + +Using a slice on a string is roughly equivalent to looping over the string from the right-hand side, appending each codepoint to a new string. +However, the code below takes `O(n + n)` best case and `O(n**2)` worst case due to the operations needed for string concatenation. + + +```python +def reverse(text): + output = '' + for index in range(-1, -(len(text)+1), -1): + output += text[index] + return output +``` + +[sequence slicing]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations diff --git a/exercises/practice/reverse-string/.approaches/sequence-slicing/snippet.txt b/exercises/practice/reverse-string/.approaches/sequence-slicing/snippet.txt new file mode 100644 index 00000000000..86e703117a0 --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/sequence-slicing/snippet.txt @@ -0,0 +1,2 @@ +def reverse(text): + return text[::-1] \ No newline at end of file diff --git a/exercises/practice/reverse-string/.articles/config.json b/exercises/practice/reverse-string/.articles/config.json new file mode 100644 index 00000000000..e9b09717516 --- /dev/null +++ b/exercises/practice/reverse-string/.articles/config.json @@ -0,0 +1,11 @@ +{ + "articles": [ + { + "uuid": "1d5866e9-6c74-411b-ab67-e986d154876e", + "slug": "performance", + "title": "Performance deep dive", + "blurb": "Deep dive to find out the most performant approach for reversing a string.", + "authors": ["bethanyg", "colinleach"] + } + ] +} \ No newline at end of file diff --git a/exercises/practice/reverse-string/.articles/performance/code/Benchmark.py b/exercises/practice/reverse-string/.articles/performance/code/Benchmark.py new file mode 100644 index 00000000000..7846a0e9fca --- /dev/null +++ b/exercises/practice/reverse-string/.articles/performance/code/Benchmark.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +"""Script for timing Reverse String Solutions. + +Creates timing table and timing graphs for +multiple approaches to reversing a string in Python. +Adapted from code written by colinleach. + +Created Jan 2024 +@author: bethanygarcia +""" + + +import timeit + +import pandas as pd +import numpy as np + + +# ------------ FUNCTIONS TO TIME ------------- # + + +def reverse_slice(text): + return text[::-1] + + +def reverse_iterate_and_prepend(text): + output = '' + for codepoint in text: + output = codepoint + output + return output + + +def reverse_range(text): + return "".join(text[index] for index in range(len(text) - 1, -1, -1)) + + +def reverse_half_swap(text): + output = list(text) + length = len(text) // 2 # Cut the amount of iteration in half. + + for index in range(length): + + # Swap values at given indexes in output list. + output[index], output[length - index - 1] = output[length - index - 1], output[index] + return ''.join(output) + + +def reverse_list_reverse(text): + output = list(text) + output.reverse() + + return ''.join(output) + + +def reverse_reversed(text): + return (''.join(reversed(text))) + + +def reverse_map(text): + return "".join(map(lambda x: text[(-x - 1)], range(len(text)))) + +## ---------END FUNCTIONS TO BE TIMED-------------------- ## + + + +## -------- Timing Code Starts Here ---------------------## +# Input Data Setup for ASCII Solutions + +long = 'SΓΌnnipΓ€evanΓ€dalalΓ΅pupeopΓ€rastlΓ΅unavΓ€simatus Pneumonoultramicroscopicsilicovolcanoconiosis Aequeosalinocalcalinoceraceoaluminosocupreovitriolic' + +words = [ + 'Ramen', + 'Euouae', + 'racecar', + 'Strengths', + "I'm hungry!", + 'Otorhinolaryngological', + 'Antidisestablishmentarianism', + 'Pseudopseudohypoparathyroidism', + 'Hippopotomonstrosesquippedaliophobia', + 'SΓΌnnipΓ€evanΓ€dalalΓ΅pupeopΓ€rastlΓ΅unavΓ€simatus', + 'Aequeosalinocalcalinoceraceoaluminosocupreovitriolic', + 'Lentokonesuihkuturbiinimoottoriapumekaanikkoaliupseerioppilas', + 'Miinibaashkiminasiganibiitoosijiganibadagwiingweshiganibakwezhigan', + 'RindfleischΒ­etikettierungsΒ­ΓΌberwachungsΒ­aufgabenΒ­ΓΌbertragungsΒ­gesetz', + 'Incomprehensibilities Otorhinolaryngological cyfrwngddarostyngedigaeth', + 'Antidisestablishmentarianism Spectrophotofluorometrically Antidisestablishmentarianism', + 'SΓΌnnipΓ€evanΓ€dalalΓ΅pupeopΓ€rastlΓ΅unavΓ€simatus Pneumonoultramicroscopicsilicovolcanoconiosis Aequeosalinocalcalinoceraceoaluminosocupreovitriolic', + long * 10, + long * 100, + long * 1000 +] + +# #Set up columns and rows for Pandas Data Frame +col_headers = [f'Str Len: {len(string)}' for string in words] +row_headers = ['reverse slice', 'iterate & prepend', 'iterate with range', 'list swap', 'list reverse', + 'reversed builtin', 'map and join'] +labels = row_headers + +# # empty dataframe will be filled in one cell at a time later +df = pd.DataFrame(np.nan, index=row_headers, columns=col_headers) + +# #Function List to Call When Timing +functions = [reverse_slice, reverse_iterate_and_prepend, reverse_range, reverse_half_swap, reverse_list_reverse, + reverse_reversed, reverse_map] + +# Run timings using timeit.autorange(). Run Each Set 3 Times. +for function, title in zip(functions, row_headers): + timings = [[ + timeit.Timer(lambda: function(data), globals=globals()).autorange()[1] / + timeit.Timer(lambda: function(data), globals=globals()).autorange()[0] + for data in words] for rounds in range(3)] + + # Only the fastest Cycle counts. + timing_result = min(timings) + + # timing_result = [round(min(timeit.repeat(lambda: function(data), repeat=3, number=1000000, globals=globals())), 6) for data in words_II] + print(f'{title}', f'Timings : {timing_result}') + + # Insert results into the dataframe + df.loc[title, 'Str Len: 5':'Str Len: 142000'] = timing_result + +# The next bit is useful for `introduction.md` +pd.options.display.float_format = '{:,.2e}'.format +print('\nDataframe in Markdown format:\n') +print(df.to_markdown(floatfmt=".2e")) diff --git a/exercises/practice/reverse-string/.articles/performance/content.md b/exercises/practice/reverse-string/.articles/performance/content.md new file mode 100644 index 00000000000..dee0b06d742 --- /dev/null +++ b/exercises/practice/reverse-string/.articles/performance/content.md @@ -0,0 +1,60 @@ +# Performance + +In this article, we'll find out how to most efficiently reverse a string in Python. + +The approaches [introduction][introduction] lists six groups of approaches: + +1. [Sequence Slice with Negative Step][approach-sequence-slicing] +2. [Iteration with String Concatenation][approach-iteration-and-concatenation] +3. [Reverse Iteration with Range()][approach-backward-iteration-with-range] +4. [Make a list and Use str.join()][approach-list-and-join] +5. [Make a list and use list.reverse()][approach-built-in-list-reverse] +6. [Use the built-in reversed()][approach-built-in-reversed] +7. Other [interesting approaches][approach-additional-approaches] + +For our performance investigations, we will compare the most performant from each group and a seventh approach using [`map()`][map in alternative approaches]. + +## Benchmarks + +To benchmark these functions, we wrote a small [benchmarking script][benchmark script] using the [timeit][timeit] module along with third-party libraries [numpy][numpy] and [pandas][pandas]. + + +The reverse slice is by far the most performant, followed by the built-ins `list.reverse()` and `reversed()`. +Iteration and concatenation is next, due to the CPython string optimization (_see the [iteration and concatenation][approach-iteration-and-concatenation] approach for all the details_), but this approach slows radically for strings longer than 142 characters. + + +With more than 142 characters, using a list, swapping positions, and joining via `join()` is the most performant method that doesn't use built-ins. +Using `map()` with `join()` was the least performant approach overall. + + + +| **string length >>>>** | 5 | 11 | 22 | 52 | 66 | 86 | 142 | 1420 | 14200 | 142000 | +|-------------------- |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: |:--------: | +| reverse slice | 1.71e-07 | 1.73e-07 | 1.86e-07 | 2.07e-07 | 2.19e-07 | 2.36e-07 | 3.49e-07 | 1.51e-06 | 1.19e-05 | 1.18e-04 | +| list reverse | 3.29e-07 | 4.28e-07 | 5.73e-07 | 8.92e-07 | 1.20e-06 | 1.51e-06 | 2.34e-06 | 1.94e-05 | 1.90e-04 | 1.91e-03 | +| reversed builtin | 3.68e-07 | 4.83e-07 | 6.98e-07 | 1.20e-06 | 1.62e-06 | 2.03e-06 | 2.71e-06 | 2.42e-05 | 2.35e-04 | 2.36e-03 | +| iterate & concatenate | 4.18e-07 | 8.10e-07 | 1.49e-06 | 3.49e-06 | 4.35e-06 | 6.18e-06 | 4.12e-06 | 2.03e-04 | 3.31e-03 | 4.61e-01 | +| list swap | 6.43e-07 | 4.00e-07 | 1.54e-06 | 3.01e-06 | 2.06e-06 | 4.71e-06 | 7.47e-06 | 8.97e-05 | 2.52e-03 | 1.02e-02 | +| iterate with range | 9.19e-07 | 1.35e-06 | 2.12e-06 | 4.15e-06 | 5.23e-06 | 6.60e-06 | 1.10e-05 | 1.05e-04 | 1.02e-03 | 1.07e-02 | +| map and join | 9.56e-07 | 1.72e-06 | 3.08e-06 | 6.27e-06 | 7.96e-06 | 1.03e-05 | 1.71e-05 | 1.70e-04 | 1.68e-03 | 1.70e-02 | + + +Measurements were taken on a 3.1 GHz Quad-Core Intel Core i7 Mac running MacOS Ventura. +Tests used `timeit.Timer.autorange()`, repeated 3 times. +Time is reported in seconds taken per string after calculating the 'best of' time. +The [`timeit`][timeit] module docs have more details, and [note.nkmk.me][note_nkmk_me] has a nice summary of methods. + +[approach-additional-approaches]: https://exercism.org/tracks/python/exercises/reverse-string/.approaches/additional-approaches +[approach-backward-iteration-with-range]: https://exercism.org/tracks/python/exercises/reverse-string/.approaches/backward-iteration-with-range +[approach-built-in-list-reverse]: https://exercism.org/tracks/python/exercises/reverse-string/.approaches/built-in-list-reverse +[approach-built-in-reversed]: https://exercism.org/tracks/python/exercises/reverse-string/.approaches/built-in-reversed +[approach-iteration-and-concatenation]: https://exercism.org/tracks/python/exercises/reverse-string/.approaches/iteration-and-concatenation +[approach-list-and-join]: https://exercism.org/tracks/python/exercises/reverse-string/.approaches/list-and-join +[approach-sequence-slicing]: https://exercism.org/tracks/python/exercises/reverse-string/.approaches/sequence-slicing +[introduction]: https://exercism.org/tracks/python/exercises/reverse-string/.approaches/introduction.md +[map in alternative approaches]: .org/tracks/python/exercises/reverse-string/.approaches/additional-approaches#Using-`map()`-and-`lambbda`-with-`Join()`-Instead-of-a-Loop +[numpy]: https://numpy.org/ +[pandas]: https://pandas.pydata.org/ +[note_nkmk_me]: https://note.nkmk.me/en/python-timeit-measure/ +[timeit]: https://docs.python.org/3/library/timeit.html#python-interface +[benchmark script]: https://exercism.org/tracks/python/exercises/reverse-string/.articles/code/Benchmark.py \ No newline at end of file diff --git a/exercises/practice/reverse-string/.articles/performance/snippet.md b/exercises/practice/reverse-string/.articles/performance/snippet.md new file mode 100644 index 00000000000..38645472093 --- /dev/null +++ b/exercises/practice/reverse-string/.articles/performance/snippet.md @@ -0,0 +1,8 @@ +| | 5 | 142000 | +| reverse slice | 1.71e-07 | 1.18e-04 | +| list reverse | 3.29e-07 | 1.91e-03 | +| reversed builtin | 3.68e-07 | 2.36e-03 | +| iterate & prepend | 4.18e-07 | 4.61e-01 | +| list swap | 6.43e-07 | 1.02e-02 | +| iterate with range | 9.19e-07 | 1.07e-02 | +| map and join | 9.56e-07 | 1.70e-02 | \ No newline at end of file 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): diff --git a/exercises/practice/rna-transcription/.docs/instructions.md b/exercises/practice/rna-transcription/.docs/instructions.md index 36da381f5a7..4dbfd3a2719 100644 --- a/exercises/practice/rna-transcription/.docs/instructions.md +++ b/exercises/practice/rna-transcription/.docs/instructions.md @@ -1,12 +1,12 @@ # Instructions -Your task is determine the RNA complement of a given DNA sequence. +Your task is to determine the RNA complement of a given DNA sequence. Both DNA and RNA strands are a sequence of nucleotides. -The four nucleotides found in DNA are adenine (**A**), cytosine (**C**), guanine (**G**) and thymine (**T**). +The four nucleotides found in DNA are adenine (**A**), cytosine (**C**), guanine (**G**), and thymine (**T**). -The four nucleotides found in RNA are adenine (**A**), cytosine (**C**), guanine (**G**) and uracil (**U**). +The four nucleotides found in RNA are adenine (**A**), cytosine (**C**), guanine (**G**), and uracil (**U**). Given a DNA strand, its transcribed RNA strand is formed by replacing each nucleotide with its complement: diff --git a/exercises/practice/rna-transcription/.meta/config.json b/exercises/practice/rna-transcription/.meta/config.json index 636aa7ed318..090e5781775 100644 --- a/exercises/practice/rna-transcription/.meta/config.json +++ b/exercises/practice/rna-transcription/.meta/config.json @@ -32,7 +32,7 @@ ".meta/example.py" ] }, - "blurb": "Given a DNA strand, return its RNA Complement Transcription.", + "blurb": "Given a DNA strand, return its RNA complement.", "source": "Hyperphysics", "source_url": "https://web.archive.org/web/20220408112140/http://hyperphysics.phy-astr.gsu.edu/hbase/Organic/transcription.html" } 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 diff --git a/exercises/practice/roman-numerals/.approaches/introduction.md b/exercises/practice/roman-numerals/.approaches/introduction.md index 35f73e3acee..3358c23f40e 100644 --- a/exercises/practice/roman-numerals/.approaches/introduction.md +++ b/exercises/practice/roman-numerals/.approaches/introduction.md @@ -186,11 +186,11 @@ As the textbooks say, further analysis of this approach is left as an exercise f ## Which approach to use? In production, it would make sense to use the `roman` package. -It is debugged and supports Roman-to-Arabic conversions in addtion to the Arabic-to-Roman approaches discussed here. +It is debugged and supports Roman-to-Arabic conversions in addition to the Arabic-to-Roman approaches discussed here. Most submissions, like the `roman` package implementation, use some variant of [`loop-over-romans`][loop-over-romans]. -Using a [2-D lookup table][table-lookup] takes a bit more initialization, but then everthing can be done in a list comprehension instead of nested loops. +Using a [2-D lookup table][table-lookup] takes a bit more initialization, but then everything can be done in a list comprehension instead of nested loops. Python is relatively unusual in supporting both tuples-of-tuples and relatively fast list comprehensions, so the approach seems a good fit for this language. No performance article is currently included for this exercise. @@ -200,6 +200,6 @@ The problem is inherently limited in scope by the design of Roman numerals, so a [if-else]: https://exercism.org/tracks/python/exercises/roman-numerals/approaches/if-else [table-lookup]: https://exercism.org/tracks/python/exercises/roman-numerals/approaches/table-lookup -[loop-over-romans]: https://exercism.org/tracks/python/exercises/roman-numerals/approaches/loop-over-roman +[loop-over-romans]: https://exercism.org/tracks/python/exercises/roman-numerals/approaches/loop-over-romans [recurse-match]: https://exercism.org/tracks/python/exercises/roman-numerals/approaches/recurse-match [roman-module]: https://github.com/zopefoundation/roman diff --git a/exercises/practice/saddle-points/.docs/instructions.md b/exercises/practice/saddle-points/.docs/instructions.md index c585568b462..f69cdab9584 100644 --- a/exercises/practice/saddle-points/.docs/instructions.md +++ b/exercises/practice/saddle-points/.docs/instructions.md @@ -13,11 +13,12 @@ Or it might have one, or even several. Here is a grid that has exactly one candidate tree. ```text - 1 2 3 4 - |----------- -1 | 9 8 7 8 -2 | 5 3 2 4 <--- potential tree house at row 2, column 1, for tree with height 5 -3 | 6 6 7 1 + ↓ + 1 2 3 4 + |----------- + 1 | 9 8 7 8 +β†’ 2 |[5] 3 2 4 + 3 | 6 6 7 1 ``` - Row 2 has values 5, 3, 2, and 4. The largest value is 5. 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) 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/say/.meta/config.json b/exercises/practice/say/.meta/config.json index 1090a04a475..ec2336bd985 100644 --- a/exercises/practice/say/.meta/config.json +++ b/exercises/practice/say/.meta/config.json @@ -30,5 +30,5 @@ }, "blurb": "Given a number from 0 to 999,999,999,999, spell out that number in English.", "source": "A variation on the JavaRanch CattleDrive, Assignment 4", - "source_url": "https://coderanch.com/wiki/718804" + "source_url": "https://web.archive.org/web/20240907035912/https://coderanch.com/wiki/718804" } diff --git a/exercises/practice/scrabble-score/.approaches/enum/content.md b/exercises/practice/scrabble-score/.approaches/enum/content.md index 5c2ad3a18ae..f5845a50918 100644 --- a/exercises/practice/scrabble-score/.approaches/enum/content.md +++ b/exercises/practice/scrabble-score/.approaches/enum/content.md @@ -48,6 +48,7 @@ The `score` function uses the same [generator expression][generator-expression] Instead of looking up the value in a _dictionary_, it looks up the `InEnum` class member value. [classes]: https://docs.python.org/3/tutorial/classes.html +[dictionary-approach]: https://exercism.org/tracks/python/exercises/scrabble-score/approaches/dictionary [enum]: https://docs.python.org/3/library/enum.html [generator-expression]: https://peps.python.org/pep-0289/ [int-enum]: https://docs.python.org/3/library/enum.html#enum.IntEnum diff --git a/exercises/practice/scrabble-score/.approaches/nested-tuple/content.md b/exercises/practice/scrabble-score/.approaches/nested-tuple/content.md index 70dc860a0a4..6bbd28a6bc2 100644 --- a/exercises/practice/scrabble-score/.approaches/nested-tuple/content.md +++ b/exercises/practice/scrabble-score/.approaches/nested-tuple/content.md @@ -30,6 +30,7 @@ You can read more about unpacking in the [concept:python/unpacking-and-multiple- Then the code checks if the character is in the unpacked letters and if it is we return its score. +[dictionary-approach]: https://exercism.org/tracks/python/exercises/scrabble-score/approaches/dictionary [generator-expression]: https://peps.python.org/pep-0289/ [for-loop]: https://realpython.com/python-for-loop/ [tuple]: https://docs.python.org/3/tutorial/datastructures.html#tuples-and-sequences diff --git a/exercises/practice/sieve/.approaches/comprehensions/content.md b/exercises/practice/sieve/.approaches/comprehensions/content.md index 664ea32c11a..2f0d778bd39 100644 --- a/exercises/practice/sieve/.approaches/comprehensions/content.md +++ b/exercises/practice/sieve/.approaches/comprehensions/content.md @@ -27,7 +27,8 @@ def primes(limit): if all(number % divisor != 0 for divisor in range(2, number))] ``` -This second example using a `list-comprehension` with `all()` is certainly concise and _relatively_ readable, but the performance is again quite poor. +This second example using a `list-comprehension` with `all()` is certainly concise and _relatively_ readable, but it uses **`%`** (_which the instructions ask you not to use_) and the performance is again quite poor. + This is not quite a fully nested loop (_there is a short-circuit when `all()` evaluates to `False`_), but it is by no means "performant". In this case, scaling with input size is intermediate between linear and quadratic, so not quite as bad as the first example. diff --git a/exercises/practice/sieve/.docs/instructions.md b/exercises/practice/sieve/.docs/instructions.md index 085c0a57d96..71292e1782d 100644 --- a/exercises/practice/sieve/.docs/instructions.md +++ b/exercises/practice/sieve/.docs/instructions.md @@ -6,37 +6,96 @@ A prime number is a number larger than 1 that is only divisible by 1 and itself. For example, 2, 3, 5, 7, 11, and 13 are prime numbers. By contrast, 6 is _not_ a prime number as it not only divisible by 1 and itself, but also by 2 and 3. -To use the Sieve of Eratosthenes, you first create a list of all the numbers between 2 and your given number. -Then you repeat the following steps: +To use the Sieve of Eratosthenes, first, write out all the numbers from 2 up to and including your given number. +Then, follow these steps: -1. Find the next unmarked number in your list (skipping over marked numbers). +1. Find the next unmarked number (skipping over marked numbers). This is a prime number. 2. Mark all the multiples of that prime number as **not** prime. -You keep repeating these steps until you've gone through every number in your list. +Repeat the steps until you've gone through every number. At the end, all the unmarked numbers are prime. ~~~~exercism/note -The tests don't check that you've implemented the algorithm, only that you've come up with the correct list of primes. -To check you are implementing the Sieve correctly, a good first test is to check that you do not use division or remainder operations. +The Sieve of Eratosthenes marks off multiples of each prime using addition (repeatedly adding the prime) or multiplication (directly computing its multiples), rather than checking each number for divisibility. + +The tests don't check that you've implemented the algorithm, only that you've come up with the correct primes. ~~~~ ## Example Let's say you're finding the primes less than or equal to 10. -- List out 2, 3, 4, 5, 6, 7, 8, 9, 10, leaving them all unmarked. +- Write out 2, 3, 4, 5, 6, 7, 8, 9, 10, leaving them all unmarked. + + ```text + 2 3 4 5 6 7 8 9 10 + ``` + - 2 is unmarked and is therefore a prime. Mark 4, 6, 8 and 10 as "not prime". + + ```text + 2 3 [4] 5 [6] 7 [8] 9 [10] + ↑ + ``` + - 3 is unmarked and is therefore a prime. Mark 6 and 9 as not prime _(marking 6 is optional - as it's already been marked)_. + + ```text + 2 3 [4] 5 [6] 7 [8] [9] [10] + ↑ + ``` + - 4 is marked as "not prime", so we skip over it. + + ```text + 2 3 [4] 5 [6] 7 [8] [9] [10] + ↑ + ``` + - 5 is unmarked and is therefore a prime. Mark 10 as not prime _(optional - as it's already been marked)_. + + ```text + 2 3 [4] 5 [6] 7 [8] [9] [10] + ↑ + ``` + - 6 is marked as "not prime", so we skip over it. + + ```text + 2 3 [4] 5 [6] 7 [8] [9] [10] + ↑ + ``` + - 7 is unmarked and is therefore a prime. + + ```text + 2 3 [4] 5 [6] 7 [8] [9] [10] + ↑ + ``` + - 8 is marked as "not prime", so we skip over it. + + ```text + 2 3 [4] 5 [6] 7 [8] [9] [10] + ↑ + ``` + - 9 is marked as "not prime", so we skip over it. + + ```text + 2 3 [4] 5 [6] 7 [8] [9] [10] + ↑ + ``` + - 10 is marked as "not prime", so we stop as there are no more numbers to check. -You've examined all numbers and found 2, 3, 5, and 7 are still unmarked, which means they're the primes less than or equal to 10. + ```text + 2 3 [4] 5 [6] 7 [8] [9] [10] + ↑ + ``` + +You've examined all the numbers and found that 2, 3, 5, and 7 are still unmarked, meaning they're the primes less than or equal to 10. diff --git a/exercises/practice/simple-cipher/.docs/instructions.md b/exercises/practice/simple-cipher/.docs/instructions.md index 475af61828f..afd0b57da93 100644 --- a/exercises/practice/simple-cipher/.docs/instructions.md +++ b/exercises/practice/simple-cipher/.docs/instructions.md @@ -1,66 +1,40 @@ # Instructions -Implement a simple shift cipher like Caesar and a more secure substitution cipher. +Create an implementation of the [VigenΓ¨re cipher][wiki]. +The VigenΓ¨re cipher is a simple substitution cipher. -## Step 1 +## Cipher terminology -"If he had anything confidential to say, he wrote it in cipher, that is, by so changing the order of the letters of the alphabet, that not a word could be made out. -If anyone wishes to decipher these, and get at their meaning, he must substitute the fourth letter of the alphabet, namely D, for A, and so with the others." -β€”Suetonius, Life of Julius Caesar +A cipher is an algorithm used to encrypt, or encode, a string. +The unencrypted string is called the _plaintext_ and the encrypted string is called the _ciphertext_. +Converting plaintext to ciphertext is called _encoding_ while the reverse is called _decoding_. -Ciphers are very straight-forward algorithms that allow us to render text less readable while still allowing easy deciphering. -They are vulnerable to many forms of cryptanalysis, but Caesar was lucky that his enemies were not cryptanalysts. +In a _substitution cipher_, each plaintext letter is replaced with a ciphertext letter which is computed with the help of a _key_. +(Note, it is possible for replacement letter to be the same as the original letter.) -The Caesar Cipher was used for some messages from Julius Caesar that were sent afield. -Now Caesar knew that the cipher wasn't very good, but he had one ally in that respect: almost nobody could read well. -So even being a couple letters off was sufficient so that people couldn't recognize the few words that they did know. +## Encoding details -Your task is to create a simple shift cipher like the Caesar Cipher. -This image is a great example of the Caesar Cipher: +In this cipher, the key is a series of lowercase letters, such as `"abcd"`. +Each letter of the plaintext is _shifted_ or _rotated_ by a distance based on a corresponding letter in the key. +An `"a"` in the key means a shift of 0 (that is, no shift). +A `"b"` in the key means a shift of 1. +A `"c"` in the key means a shift of 2, and so on. -![Caesar Cipher][img-caesar-cipher] +The first letter of the plaintext uses the first letter of the key, the second letter of the plaintext uses the second letter of the key and so on. +If you run out of letters in the key before you run out of letters in the plaintext, start over from the start of the key again. -For example: +If the key only contains one letter, such as `"dddddd"`, then all letters of the plaintext are shifted by the same amount (three in this example), which would make this the same as a rotational cipher or shift cipher (sometimes called a Caesar cipher). +For example, the plaintext `"iamapandabear"` would become `"ldpdsdqgdehdu"`. -Giving "iamapandabear" as input to the encode function returns the cipher "ldpdsdqgdehdu". -Obscure enough to keep our message secret in transit. +If the key only contains the letter `"a"` (one or more times), the shift distance is zero and the ciphertext is the same as the plaintext. -When "ldpdsdqgdehdu" is put into the decode function it would return the original "iamapandabear" letting your friend read your original message. +Usually the key is more complicated than that, though! +If the key is `"abcd"` then letters of the plaintext would be shifted by a distance of 0, 1, 2, and 3. +If the plaintext is `"hello"`, we need 5 shifts so the key would wrap around, giving shift distances of 0, 1, 2, 3, and 0. +Applying those shifts to the letters of `"hello"` we get `"hfnoo"`. -## Step 2 +## Random keys -Shift ciphers quickly cease to be useful when the opposition commander figures them out. -So instead, let's try using a substitution cipher. -Try amending the code to allow us to specify a key and use that for the shift distance. +If no key is provided, generate a key which consists of at least 100 random lowercase letters from the Latin alphabet. -Here's an example: - -Given the key "aaaaaaaaaaaaaaaaaa", encoding the string "iamapandabear" -would return the original "iamapandabear". - -Given the key "ddddddddddddddddd", encoding our string "iamapandabear" -would return the obscured "ldpdsdqgdehdu" - -In the example above, we've set a = 0 for the key value. -So when the plaintext is added to the key, we end up with the same message coming out. -So "aaaa" is not an ideal key. -But if we set the key to "dddd", we would get the same thing as the Caesar Cipher. - -## Step 3 - -The weakest link in any cipher is the human being. -Let's make your substitution cipher a little more fault tolerant by providing a source of randomness and ensuring that the key contains only lowercase letters. - -If someone doesn't submit a key at all, generate a truly random key of at least 100 lowercase characters in length. - -## Extensions - -Shift ciphers work by making the text slightly odd, but are vulnerable to frequency analysis. -Substitution ciphers help that, but are still very vulnerable when the key is short or if spaces are preserved. -Later on you'll see one solution to this problem in the exercise "crypto-square". - -If you want to go farther in this field, the questions begin to be about how we can exchange keys in a secure way. -Take a look at [Diffie-Hellman on Wikipedia][dh] for one of the first implementations of this scheme. - -[img-caesar-cipher]: https://upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Caesar_cipher_left_shift_of_3.svg/320px-Caesar_cipher_left_shift_of_3.svg.png -[dh]: https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange +[wiki]: https://en.wikipedia.org/wiki/Vigen%C3%A8re_cipher diff --git a/exercises/practice/simple-cipher/.meta/config.json b/exercises/practice/simple-cipher/.meta/config.json index 0dc1687acfe..ced62d99264 100644 --- a/exercises/practice/simple-cipher/.meta/config.json +++ b/exercises/practice/simple-cipher/.meta/config.json @@ -30,7 +30,7 @@ ".meta/example.py" ] }, - "blurb": "Implement a simple shift cipher like Caesar and a more secure substitution cipher.", + "blurb": "Implement the VigenΓ¨re cipher, a simple substitution cipher.", "source": "Substitution Cipher at Wikipedia", "source_url": "https://en.wikipedia.org/wiki/Substitution_cipher" } diff --git a/exercises/practice/simple-linked-list/.docs/hints.md b/exercises/practice/simple-linked-list/.docs/hints.md index da373540ad0..c017108a610 100644 --- a/exercises/practice/simple-linked-list/.docs/hints.md +++ b/exercises/practice/simple-linked-list/.docs/hints.md @@ -13,7 +13,7 @@ In order for _custom objects_ to support `len()`, the special method [`__len__`][__len__] needs to be defined. - Iteration in Python is supported for most sequence, container, or collection type objects. In order for a _custom_ object to support looping or re-ordering, the special method `__iter__` needs to be defined. -[Implementing an iterator for a class.][implementing iterators] can help show you how. +[Implementing an iterator for a class][implementing iterators] can help show you how. [Baeldung: The Stack Data Structure]: https://www.baeldung.com/cs/stack-data-structure [Koder Dojo Coding an ADS Stack in Python]: https://www.koderdojo.com/blog/coding-a-stack-abstract-data-structure-using-linked-list-in-python diff --git a/exercises/practice/simple-linked-list/.docs/instructions.append.md b/exercises/practice/simple-linked-list/.docs/instructions.append.md index 888e675a841..7f848fbaab1 100644 --- a/exercises/practice/simple-linked-list/.docs/instructions.append.md +++ b/exercises/practice/simple-linked-list/.docs/instructions.append.md @@ -46,7 +46,7 @@ For details on implementing special or "dunder" methods in Python, see [Python D ## Building an Iterator To support looping through or reversing your `LinkedList`, you will need to implement the `__iter__` special method. -See [implementing an iterator for a class.][custom iterators] for implementation details. +See [implementing an iterator for a class][custom iterators] for implementation details.
@@ -56,7 +56,7 @@ Sometimes it is necessary to both [customize][customize errors] and [`raise`][ra 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. -Custom exceptions can be created through new exception classes (see [`classes`][classes tutorial] for more detail.) that are typically subclasses of [`Exception`][exception base class]. +Custom exceptions can be created through new exception classes (see [`classes`][classes tutorial] for more detail) that are typically subclasses of [`Exception`][exception base class]. For situations where you know the error source will be a derivative of a certain exception _type_, you can choose to inherit from one of the [`built in error types`][built-in errors] under the _Exception_ class. When raising the error, you should still include a meaningful message. diff --git a/exercises/practice/simple-linked-list/.meta/config.json b/exercises/practice/simple-linked-list/.meta/config.json index 2fc136a325f..2134b492371 100644 --- a/exercises/practice/simple-linked-list/.meta/config.json +++ b/exercises/practice/simple-linked-list/.meta/config.json @@ -3,6 +3,7 @@ "cmccandless" ], "contributors": [ + "Bethanyg", "Dog", "N-Parsons", "rootulp", diff --git a/exercises/practice/simple-linked-list/simple_linked_list.py b/exercises/practice/simple-linked-list/simple_linked_list.py index cbf120e2fcb..dfb9e6c9798 100644 --- a/exercises/practice/simple-linked-list/simple_linked_list.py +++ b/exercises/practice/simple-linked-list/simple_linked_list.py @@ -1,3 +1,7 @@ +class EmptyListException(Exception): + pass + + class Node: def __init__(self, value): pass @@ -10,7 +14,10 @@ def next(self): class LinkedList: - def __init__(self, values=[]): + def __init__(self, values=None): + pass + + def __iter__(self): pass def __len__(self): @@ -27,7 +34,3 @@ def pop(self): def reversed(self): pass - - -class EmptyListException(Exception): - pass diff --git a/exercises/practice/square-root/.docs/instructions.append.md b/exercises/practice/square-root/.docs/instructions.append.md index 84b4cf8ee70..eab4f0ac659 100644 --- a/exercises/practice/square-root/.docs/instructions.append.md +++ b/exercises/practice/square-root/.docs/instructions.append.md @@ -3,10 +3,11 @@ ## How this Exercise is Structured in Python -Python offers a wealth of mathematical functions in the form of the [math module][math-module] and built-ins such as [`pow()`][pow] and [`sum()`][sum]. +Python offers a wealth of mathematical functions in the form of the [math module][math-module] and built-ins such as the exponentiation operator `**`, [`pow()`][pow] and [`sum()`][sum]. However, we'd like you to consider the challenge of solving this exercise without those built-ins or modules. -While there is a mathematical formula that will find the square root of _any_ number, we have gone the route of having only [natural numbers][nautral-number] (positive integers) as solutions. +While there is a mathematical formula that will find the square root of _any_ number, we have gone the route of having only [natural numbers][nautral-number] (positive integers) as solutions. +It is possible to compute the square root of any natural number using only natural numbers in the computation. [math-module]: https://docs.python.org/3/library/math.html diff --git a/exercises/practice/square-root/.docs/instructions.md b/exercises/practice/square-root/.docs/instructions.md index e9905e9d416..d258b86876e 100644 --- a/exercises/practice/square-root/.docs/instructions.md +++ b/exercises/practice/square-root/.docs/instructions.md @@ -1,13 +1,18 @@ # Instructions -Given a natural radicand, return its square root. +Your task is to calculate the square root of a given number. -Note that the term "radicand" refers to the number for which the root is to be determined. -That is, it is the number under the root symbol. +- Try to avoid using the pre-existing math libraries of your language. +- As input you'll be given a positive whole number, i.e. 1, 2, 3, 4… +- You are only required to handle cases where the result is a positive whole number. -Check out the Wikipedia pages on [square root][square-root] and [methods of computing square roots][computing-square-roots]. +Some potential approaches: -Recall also that natural numbers are positive real whole numbers (i.e. 1, 2, 3 and up). +- Linear or binary search for a number that gives the input number when squared. +- Successive approximation using Newton's or Heron's method. +- Calculating one digit at a time or one bit at a time. -[square-root]: https://en.wikipedia.org/wiki/Square_root +You can check out the Wikipedia pages on [integer square root][integer-square-root] and [methods of computing square roots][computing-square-roots] to help with choosing a method of calculation. + +[integer-square-root]: https://en.wikipedia.org/wiki/Integer_square_root [computing-square-roots]: https://en.wikipedia.org/wiki/Methods_of_computing_square_roots diff --git a/exercises/practice/square-root/.docs/introduction.md b/exercises/practice/square-root/.docs/introduction.md new file mode 100644 index 00000000000..1d692934f28 --- /dev/null +++ b/exercises/practice/square-root/.docs/introduction.md @@ -0,0 +1,10 @@ +# Introduction + +We are launching a deep space exploration rocket and we need a way to make sure the navigation system stays on target. + +As the first step in our calculation, we take a target number and find its square root (that is, the number that when multiplied by itself equals the target number). + +The journey will be very long. +To make the batteries last as long as possible, we had to make our rocket's onboard computer very power efficient. +Unfortunately that means that we can't rely on fancy math libraries and functions, as they use more power. +Instead we want to implement our own square root calculation. diff --git a/exercises/practice/sublist/.docs/instructions.md b/exercises/practice/sublist/.docs/instructions.md index 7535931afa8..8228edc6ce2 100644 --- a/exercises/practice/sublist/.docs/instructions.md +++ b/exercises/practice/sublist/.docs/instructions.md @@ -8,8 +8,8 @@ Given any two lists `A` and `B`, determine if: - None of the above is true, thus lists `A` and `B` are unequal Specifically, list `A` is equal to list `B` if both lists have the same values in the same order. -List `A` is a superlist of `B` if `A` contains a sub-sequence of values equal to `B`. -List `A` is a sublist of `B` if `B` contains a sub-sequence of values equal to `A`. +List `A` is a superlist of `B` if `A` contains a contiguous sub-sequence of values equal to `B`. +List `A` is a sublist of `B` if `B` contains a contiguous sub-sequence of values equal to `A`. Examples: diff --git a/exercises/practice/swift-scheduling/.docs/instructions.md b/exercises/practice/swift-scheduling/.docs/instructions.md new file mode 100644 index 00000000000..6423a1066b7 --- /dev/null +++ b/exercises/practice/swift-scheduling/.docs/instructions.md @@ -0,0 +1,50 @@ +# Instructions + +Your task is to convert delivery date descriptions to _actual_ delivery dates, based on when the meeting started. + +There are two types of delivery date descriptions: + +1. Fixed: a predefined set of words. +2. Variable: words that have a variable component, but follow a predefined set of patterns. + +## Fixed delivery date descriptions + +There are three fixed delivery date descriptions: + +- `"NOW"` +- `"ASAP"` (As Soon As Possible) +- `"EOW"` (End Of Week) + +The following table shows how to translate them: + +| Description | Meeting start | Delivery date | +| ----------- | ----------------------------- | ----------------------------------- | +| `"NOW"` | - | Two hours after the meeting started | +| `"ASAP"` | Before 13:00 | Today at 17:00 | +| `"ASAP"` | After or at 13:00 | Tomorrow at 13:00 | +| `"EOW"` | Monday, Tuesday, or Wednesday | Friday at 17:00 | +| `"EOW"` | Thursday or Friday | Sunday at 20:00 | + +## Variable delivery date descriptions + +There are two variable delivery date description patterns: + +- `"M"` (N-th month) +- `"Q"` (N-th quarter) + +| Description | Meeting start | Delivery date | +| ----------- | ------------------------- | --------------------------------------------------------- | +| `"M"` | Before N-th month | At 8:00 on the _first_ workday of this year's N-th month | +| `"M"` | After or in N-th month | At 8:00 on the _first_ workday of next year's N-th month | +| `"Q"` | Before or in N-th quarter | At 8:00 on the _last_ workday of this year's N-th quarter | +| `"Q"` | After N-th quarter | At 8:00 on the _last_ workday of next year's N-th quarter | + +~~~~exercism/note +A workday is a Monday, Tuesday, Wednesday, Thursday, or Friday. + +A year has four quarters, each with three months: +1. January/February/March +2. April/May/June +3. July/August/September +4. October/November/December. +~~~~ diff --git a/exercises/practice/swift-scheduling/.docs/introduction.md b/exercises/practice/swift-scheduling/.docs/introduction.md new file mode 100644 index 00000000000..2322f813fff --- /dev/null +++ b/exercises/practice/swift-scheduling/.docs/introduction.md @@ -0,0 +1,6 @@ +# Introduction + +This week, it is your turn to take notes in the department's planning meeting. +In this meeting, your boss will set delivery dates for all open work items. +Annoyingly, instead of specifying the _actual_ delivery dates, your boss will only _describe them_ in an abbreviated format. +As many of your colleagues won't be familiar with this corporate lingo, you'll need to convert these delivery date descriptions to actual delivery dates. diff --git a/exercises/practice/swift-scheduling/.meta/config.json b/exercises/practice/swift-scheduling/.meta/config.json new file mode 100644 index 00000000000..ba6f97353fa --- /dev/null +++ b/exercises/practice/swift-scheduling/.meta/config.json @@ -0,0 +1,21 @@ +{ + "authors": [ + "erikschierboom", + "bethanyg" + ], + "contributors": [], + "files": { + "solution": [ + "swift_scheduling.py" + ], + "test": [ + "swift_scheduling_test.py" + ], + "example": [ + ".meta/example.py" + ] + }, + "blurb": "Convert delivery date descriptions to actual delivery dates.", + "source": "Erik Schierboom", + "source_url": "https://github.com/exercism/problem-specifications/pull/2536" +} diff --git a/exercises/practice/swift-scheduling/.meta/example.py b/exercises/practice/swift-scheduling/.meta/example.py new file mode 100644 index 00000000000..9f411819abf --- /dev/null +++ b/exercises/practice/swift-scheduling/.meta/example.py @@ -0,0 +1,57 @@ +from datetime import datetime, timedelta + + +def delivery_date(start, description): + start_date = datetime.fromisoformat(start) + + + if description == 'NOW': + due_date = start_date + timedelta(hours=2) + + if description == 'ASAP': + if str(start_date.time()) < '13:00:00': + due_date = start_date.replace(hour=17, minute=0) + else: + due_date = ( + start_date.replace(hour=13, minute=0) + + timedelta(days=1) + ) + + if description =='EOW': + if start_date.isoweekday() < 4: + due_date = ( + start_date.replace(hour=17, minute=0) + + timedelta(days=5 - start_date.isoweekday()) + ) + else: + due_date = ( + start_date.replace(hour=20, minute=0) + + timedelta(days=7 - start_date.isoweekday()) + ) + + if description.endswith('M'): + month = int(description[:-1]) + target = datetime(start_date.year, month, 1, 8, 0, 0) + + if start_date.month >= target.month: + target = target.replace(year=target.year + 1) + if target.isoweekday() not in (6,7) and target.day in range(1, 8): + due_date = target + else: + if target.isoweekday() == 6: due_date = target + timedelta(days = 2) + if target.isoweekday() == 7: due_date = target + timedelta(days = 1) + + if description.startswith('Q'): + target = int(description[1:]) + current = ((start_date.month + 2) // 3) + month = {"Q1":4,"Q2": 7,"Q3": 10,"Q4": 1}[description] + rollover = 1 if (current > target or target == 4) else 0 + + due_date = start_date.replace( + start_date.year + rollover, month, 1, 8, 0, 0 + ) - timedelta(days=1) + + if due_date.isoweekday() == 6: due_date -= timedelta(days=1) + if due_date.isoweekday() == 7: due_date -= timedelta(days=2) + + return due_date.isoformat() diff --git a/exercises/practice/swift-scheduling/.meta/template.j2 b/exercises/practice/swift-scheduling/.meta/template.j2 new file mode 100644 index 00000000000..eef5a58991c --- /dev/null +++ b/exercises/practice/swift-scheduling/.meta/template.j2 @@ -0,0 +1,20 @@ +{%- import "generator_macros.j2" as macros with context -%} +{{ macros.canonical_ref() }} + +{{ macros.header(imports=imports, ignore=ignore) }} + + +{% macro test_case(case) -%} + {%- set input = case["input"] -%} + def test_{{ case["description"] | to_snake }}(self): + self.assertEqual( + {{ case["property"] | to_snake }}{{ case["input"]["meetingStart"], case["input"]["description"] }}, + "{{ case["expected"] }}" + ) +{%- endmacro %} + + +class {{ exercise | camel_case }}Test(unittest.TestCase): + {% for case in cases %} + {{ test_case(case) }} + {% endfor %} diff --git a/exercises/practice/swift-scheduling/.meta/tests.toml b/exercises/practice/swift-scheduling/.meta/tests.toml new file mode 100644 index 00000000000..ef2aa5166bc --- /dev/null +++ b/exercises/practice/swift-scheduling/.meta/tests.toml @@ -0,0 +1,51 @@ +# 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. + +[1d0e6e72-f370-408c-bc64-5dafa9c6da73] +description = "NOW translates to two hours later" + +[93325e7b-677d-4d96-b017-2582af879dc2] +description = "ASAP before one in the afternoon translates to today at five in the afternoon" + +[cb4252a3-c4c1-41f6-8b8c-e7269733cef8] +description = "ASAP at one in the afternoon translates to tomorrow at one in the afternoon" + +[6fddc1ea-2fe9-4c60-81f7-9220d2f45537] +description = "ASAP after one in the afternoon translates to tomorrow at one in the afternoon" + +[25f46bf9-6d2a-4e95-8edd-f62dd6bc8a6e] +description = "EOW on Monday translates to Friday at five in the afternoon" + +[0b375df5-d198-489e-acee-fd538a768616] +description = "EOW on Tuesday translates to Friday at five in the afternoon" + +[4afbb881-0b5c-46be-94e1-992cdc2a8ca4] +description = "EOW on Wednesday translates to Friday at five in the afternoon" + +[e1341c2b-5e1b-4702-a95c-a01e8e96e510] +description = "EOW on Thursday translates to Sunday at eight in the evening" + +[bbffccf7-97f7-4244-888d-bdd64348fa2e] +description = "EOW on Friday translates to Sunday at eight in the evening" + +[d651fcf4-290e-407c-8107-36b9076f39b2] +description = "EOW translates to leap day" + +[439bf09f-3a0e-44e7-bad5-b7b6d0c4505a] +description = "2M before the second month of this year translates to the first workday of the second month of this year" + +[86d82e83-c481-4fb4-9264-625de7521340] +description = "11M in the eleventh month translates to the first workday of the eleventh month of next year" + +[0d0b8f6a-1915-46f5-a630-1ff06af9da08] +description = "4M in the ninth month translates to the first workday of the fourth month of next year" + +[06d401e3-8461-438f-afae-8d26aa0289e0] +description = "Q1 in the first quarter translates to the last workday of the first quarter of this year" + +[eebd5f32-b16d-4ecd-91a0-584b0364b7ed] +description = "Q4 in the second quarter translates to the last workday of the fourth quarter of this year" + +[c920886c-44ad-4d34-a156-dc4176186581] +description = "Q3 in the fourth quarter translates to the last workday of the third quarter of next year" diff --git a/exercises/practice/swift-scheduling/swift_scheduling.py b/exercises/practice/swift-scheduling/swift_scheduling.py new file mode 100644 index 00000000000..99fb9053eb9 --- /dev/null +++ b/exercises/practice/swift-scheduling/swift_scheduling.py @@ -0,0 +1,2 @@ +def delivery_date(start, description): + pass diff --git a/exercises/practice/swift-scheduling/swift_scheduling_test.py b/exercises/practice/swift-scheduling/swift_scheduling_test.py new file mode 100644 index 00000000000..08ed2485b9c --- /dev/null +++ b/exercises/practice/swift-scheduling/swift_scheduling_test.py @@ -0,0 +1,109 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/swift-scheduling/canonical-data.json +# File last updated on 2025-06-25 + +import unittest + +from swift_scheduling import ( + delivery_date, +) + + +class SwiftSchedulingTest(unittest.TestCase): + def test_now_translates_to_two_hours_later(self): + self.assertEqual( + delivery_date("2012-02-13T09:00:00", "NOW"), "2012-02-13T11:00:00" + ) + + def test_asap_before_one_in_the_afternoon_translates_to_today_at_five_in_the_afternoon( + self, + ): + self.assertEqual( + delivery_date("1999-06-03T09:45:00", "ASAP"), "1999-06-03T17:00:00" + ) + + def test_asap_at_one_in_the_afternoon_translates_to_tomorrow_at_one_in_the_afternoon( + self, + ): + self.assertEqual( + delivery_date("2008-12-21T13:00:00", "ASAP"), "2008-12-22T13:00:00" + ) + + def test_asap_after_one_in_the_afternoon_translates_to_tomorrow_at_one_in_the_afternoon( + self, + ): + self.assertEqual( + delivery_date("2008-12-21T14:50:00", "ASAP"), "2008-12-22T13:00:00" + ) + + def test_eow_on_monday_translates_to_friday_at_five_in_the_afternoon(self): + self.assertEqual( + delivery_date("2025-02-03T16:00:00", "EOW"), "2025-02-07T17:00:00" + ) + + def test_eow_on_tuesday_translates_to_friday_at_five_in_the_afternoon(self): + self.assertEqual( + delivery_date("1997-04-29T10:50:00", "EOW"), "1997-05-02T17:00:00" + ) + + def test_eow_on_wednesday_translates_to_friday_at_five_in_the_afternoon(self): + self.assertEqual( + delivery_date("2005-09-14T11:00:00", "EOW"), "2005-09-16T17:00:00" + ) + + def test_eow_on_thursday_translates_to_sunday_at_eight_in_the_evening(self): + self.assertEqual( + delivery_date("2011-05-19T08:30:00", "EOW"), "2011-05-22T20:00:00" + ) + + def test_eow_on_friday_translates_to_sunday_at_eight_in_the_evening(self): + self.assertEqual( + delivery_date("2022-08-05T14:00:00", "EOW"), "2022-08-07T20:00:00" + ) + + def test_eow_translates_to_leap_day(self): + self.assertEqual( + delivery_date("2008-02-25T10:30:00", "EOW"), "2008-02-29T17:00:00" + ) + + def test_2_m_before_the_second_month_of_this_year_translates_to_the_first_workday_of_the_second_month_of_this_year( + self, + ): + self.assertEqual( + delivery_date("2007-01-02T14:15:00", "2M"), "2007-02-01T08:00:00" + ) + + def test_11_m_in_the_eleventh_month_translates_to_the_first_workday_of_the_eleventh_month_of_next_year( + self, + ): + self.assertEqual( + delivery_date("2013-11-21T15:30:00", "11M"), "2014-11-03T08:00:00" + ) + + def test_4_m_in_the_ninth_month_translates_to_the_first_workday_of_the_fourth_month_of_next_year( + self, + ): + self.assertEqual( + delivery_date("2019-11-18T15:15:00", "4M"), "2020-04-01T08:00:00" + ) + + def test_q1_in_the_first_quarter_translates_to_the_last_workday_of_the_first_quarter_of_this_year( + self, + ): + self.assertEqual( + delivery_date("2003-01-01T10:45:00", "Q1"), "2003-03-31T08:00:00" + ) + + def test_q4_in_the_second_quarter_translates_to_the_last_workday_of_the_fourth_quarter_of_this_year( + self, + ): + self.assertEqual( + delivery_date("2001-04-09T09:00:00", "Q4"), "2001-12-31T08:00:00" + ) + + def test_q3_in_the_fourth_quarter_translates_to_the_last_workday_of_the_third_quarter_of_next_year( + self, + ): + self.assertEqual( + delivery_date("2022-10-06T11:00:00", "Q3"), "2023-09-29T08:00:00" + ) 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) diff --git a/exercises/practice/triangle/.docs/instructions.md b/exercises/practice/triangle/.docs/instructions.md index ac39008726d..e9b053dcd34 100644 --- a/exercises/practice/triangle/.docs/instructions.md +++ b/exercises/practice/triangle/.docs/instructions.md @@ -13,6 +13,12 @@ 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 +_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. +~~~~ + In equations: Let `a`, `b`, and `c` be sides of the triangle. 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") 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]) diff --git a/exercises/practice/wordy/.approaches/dunder-getattribute/content.md b/exercises/practice/wordy/.approaches/dunder-getattribute/content.md index 3f82c0ee518..167460f2d3c 100644 --- a/exercises/practice/wordy/.approaches/dunder-getattribute/content.md +++ b/exercises/practice/wordy/.approaches/dunder-getattribute/content.md @@ -13,7 +13,11 @@ OPS = { def answer(question): question = question.removeprefix("What is").removesuffix("?").strip() if not question: raise ValueError("syntax error") - if question.isdigit(): return int(question) + + if question.startswith("-") and question[1:].isdigit(): + return -int(question[1:]) + elif question.isdigit(): + return int(question) found_op = False for name, op in OPS.items(): @@ -56,8 +60,9 @@ The method calls are [chained][method-chaining], so that the output from one cal If the input has no characters left, it uses the [falsiness][falsiness] of an empty string with the [`not`][not] operator to return a `ValueError("syntax error")`. -Next, the [`isdigit`][isdigit] method is used to see if the remaining characters in the input are digits. -If so, it uses the [`int()`][int-constructor] constructor to return the string as an integer. +Next, the [`str.startswith()`][startswith] and [`isdigit`][isdigit] methods are used to see if the remaining characters in the input are either negative or positive digits. +Because "-" is used to denote negative numbers, `str.startswith("-")` is used in the first condition and `question[1:].isdigit()` is then used for the remaining string. +If the `str.isdigit()` checks pass, the [`int()`][int-constructor] constructor is used to return the string as an integer with the proper sign. Next, the elements in the `OPS` dictionary are iterated over. If the key name is in the input, then the [`str.replace`][replace] method is used to replace the name in the input with the `dunder-method` value. @@ -94,5 +99,6 @@ When the loop exhausts, the first element of the list is selected as the functio [replace]: https://docs.python.org/3/library/stdtypes.html?#str.replace [split]: https://docs.python.org/3/library/stdtypes.html?#str.split [strip]: https://docs.python.org/3/library/stdtypes.html#str.strip +[startswith]: https://docs.python.org/3/library/stdtypes.html#str.startswith [unpacking]: https://treyhunner.com/2018/10/asterisks-in-python-what-they-are-and-how-to-use-them/ [unpacking-and-multiple-assignment]: https://exercism.org/tracks/python/concepts/unpacking-and-multiple-assignment diff --git a/exercises/practice/wordy/.approaches/functools-reduce/content.md b/exercises/practice/wordy/.approaches/functools-reduce/content.md index 8bb7de25f30..8bc42449fa0 100644 --- a/exercises/practice/wordy/.approaches/functools-reduce/content.md +++ b/exercises/practice/wordy/.approaches/functools-reduce/content.md @@ -16,16 +16,17 @@ def answer(question): raise ValueError("unknown operation") # Using the built-in filter() to clean & split the question.. - list(filter(lambda x: - x not in ("What", "is", "by"), - question.strip("?").split())) + question = list(filter(lambda x: + x not in ("What", "is", "by"), + question.strip("?").split())) # Separate candidate operators and numbers into two lists. operations = question[1::2] # Convert candidate elements to int(), checking for "-". # All other values are replaced with None. - digits = [int(element) if (element.isdigit() or element[1:].isdigit()) + digits = [int(element) if + (element.isdigit() or element[1:].isdigit()) else None for element in question[::2]] # If there is a mis-match between operators and numbers, toss error. @@ -106,7 +107,8 @@ def answer(question): # Convert candidate elements to int(), checking for "-". # All other values are replaced with None. - digits = [int(element) if (element.isdigit() or element[1:].isdigit()) + digits = [int(element) if + (element.isdigit() or element[1:].isdigit()) else None for element in question[::2]] # If there is a mis-match between operators and numbers, toss error. diff --git a/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md b/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md index 9f57c1c9c6b..9fdf3e20e09 100644 --- a/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md +++ b/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md @@ -5,22 +5,24 @@ from operator import add, mul, sub from operator import floordiv as div + OPERATIONS = {"plus": add, "minus": sub, "multiplied": mul, "divided": div} + def answer(question): if not question.startswith("What is") or "cubed" in question: raise ValueError("unknown operation") - + question = question.removeprefix("What is").removesuffix("?").strip() - if question.isdigit(): - return int(question) - - if not question: + if not question: raise ValueError("syntax error") - + + if (question.startswith("-") and question[1:].isdigit()) or question.isdigit(): + return int(question) + equation = [word for word in question.split() if word != 'by'] - + while len(equation) > 1: try: x_value, operation, y_value, *rest = equation @@ -28,7 +30,7 @@ def answer(question): *rest] except: raise ValueError("syntax error") - + return equation[0] ``` diff --git a/exercises/practice/wordy/.approaches/introduction.md b/exercises/practice/wordy/.approaches/introduction.md index 135af33b59e..821b1228425 100644 --- a/exercises/practice/wordy/.approaches/introduction.md +++ b/exercises/practice/wordy/.approaches/introduction.md @@ -31,12 +31,12 @@ However, solutions all follow the same general steps: 1. Remove the parts of the question string that do not apply to calculating the answer. 2. Iterate over the question, determining which words are numbers, and which are meant to be mathematical operations. - -- _Converting the question string into a `list` of words is hugely helpful here._ + β€” _Converting the question string into a `list` of words is hugely helpful here._ 3. **_Starting from the left_**, take the first three elements and convert number strings to `int` and operations words to the mathematical operations +, -, *, and /. 4. Apply the operation to the numbers, which should result in a single number. - -- _Employing a `try-except` block can trap any errors thrown and make the code both "safer" and less complex._ + β€” _Employing a `try-except` block can trap any errors thrown and make the code both "safer" and less complex._ 5. Use the calculated number from step 4 as the start for the next "trio" (_number, operation, number_) in the question. The calculated number + the remainder of the question becomes the question being worked on in the next iteration. - -- _Using a `while-loop` with a test on the length of the question to do calculation is a very common strategy._ + β€” _Using a `while-loop` with a test on the length of the question to do calculation is a very common strategy._ 6. Once the question is calculated down to a single number, that is the answer. Anything else that happens in the loop/iteration or within the accumulated result is a `ValueError("syntax error")`. ~~~~ @@ -54,8 +54,11 @@ For question cleaning, [`str.removeprefix`][removeprefix] and 'Supercalifragilistic' -#The two methods can be chained to remove both a suffix and prefix in one line. ->>> 'Supercalifragilisticexpialidocious'.removesuffix('expialidocious').removeprefix('Super') +#The two methods can be chained to remove both a suffix and prefix in "one line". +#The line has been broken up here for better display. +>>> ('Supercalifragilisticexpialidocious' + .removesuffix('expialidocious') + .removeprefix('Super')) 'califragilistic' ``` @@ -179,7 +182,7 @@ def answer(question): question = question.removeprefix("What is").removesuffix("?").strip() - if question.isdigit(): + if (question.startswith("-") and question[1:].isdigit()) or question.isdigit(): return int(question) if not question: @@ -283,7 +286,7 @@ def answer(question): question = question.removeprefix("What is").removesuffix("?").strip() - if question.isdigit(): + if (question.startswith("-") and question[1:].isdigit()) or question.isdigit(): return int(question) if not question: @@ -419,7 +422,11 @@ OPS = { def answer(question): question = question.removeprefix("What is").removesuffix("?").strip() if not question: raise ValueError("syntax error") - if question.isdigit(): return int(question) + + if question.startswith("-") and question[1:].isdigit(): + return -int(question[1:]) + elif question.isdigit(): + return int(question) found_op = False for name, op in OPS.items(): diff --git a/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/content.md b/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/content.md index df06d3b1716..d78f3e7db83 100644 --- a/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/content.md +++ b/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/content.md @@ -16,7 +16,7 @@ def answer(question): question = question.removeprefix("What is").removesuffix("?").strip() - if question.isdigit(): + if (question.startswith("-") and question[1:].isdigit()) or question.isdigit(): return int(question) if not question: @@ -67,7 +67,7 @@ def answer(question): question = question.removeprefix("What is").removesuffix("?").strip() - if question.isdigit(): + if (question.startswith("-") and question[1:].isdigit()) or question.isdigit(): return int(question) if not question: diff --git a/exercises/practice/wordy/.approaches/recursion/content.md b/exercises/practice/wordy/.approaches/recursion/content.md index 0ba69fca502..794f1b41c19 100644 --- a/exercises/practice/wordy/.approaches/recursion/content.md +++ b/exercises/practice/wordy/.approaches/recursion/content.md @@ -246,9 +246,9 @@ For example: 1. "1 plus -10 multiplied by 13 divided by 2" would match on "1 plus -10" (_group x_) **multiplied by** "13 divided by 2" (_group y_). 2. This is re-arranged to `mul(calculate("1 plus -10"), calculate("13 divided by 2"))` -3. At this point the loop would pause as the two recursive calls to `calculate()` spawn -4. The loops then run again β€” and so would the calls to `calculate()`, until there wasn't any match that caused a split of the question or an error. -5. One at a time, the numbers would then be returned, until the main `mul(calculate("1 plus -10"), calculate("13 divided by 2"))` could be solved, at which point the answer is returned. +3. At this point, the loop pauses as the two recursive calls to `calculate()` spawn +4. The loop runs again β€” and so do the calls to `calculate()` β€” until there isn't any match that splits the question or any errors. +5. One at a time, the numbers are returned from the `calculate()` calls on the stack, until the main `mul(calculate("1 plus -10"), calculate("13 divided by 2"))` is solved, at which point the answer is returned. For a more visual picture, you can step through the code on [pythontutor.com][recursion-in-loop-pythontutor]. diff --git a/exercises/practice/wordy/.meta/tests.toml b/exercises/practice/wordy/.meta/tests.toml index 912d5760097..52bdf919cc4 100644 --- a/exercises/practice/wordy/.meta/tests.toml +++ b/exercises/practice/wordy/.meta/tests.toml @@ -1,13 +1,32 @@ -# 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. [88bf4b28-0de3-4883-93c7-db1b14aa806e] description = "just a number" +[18983214-1dfc-4ebd-ac77-c110dde699ce] +description = "just a zero" + +[607c08ee-2241-4288-916d-dae5455c87e6] +description = "just a negative number" + [bb8c655c-cf42-4dfc-90e0-152fcfd8d4e0] description = "addition" +[bb9f2082-171c-46ad-ad4e-c3f72087c1b5] +description = "addition with a left hand zero" + +[6fa05f17-405a-4742-80ae-5d1a8edb0d5d] +description = "addition with a right hand zero" + [79e49e06-c5ae-40aa-a352-7a3a01f70015] description = "more addition" @@ -52,6 +71,7 @@ description = "unknown operation" [8a7e85a8-9e7b-4d46-868f-6d759f4648f8] description = "Non math question" +include = false [42d78b5f-dbd7-4cdb-8b30-00f794bb24cf] description = "reject problem missing an operand" diff --git a/exercises/practice/wordy/wordy_test.py b/exercises/practice/wordy/wordy_test.py index ffcaf49aed4..c34ed27cda7 100644 --- a/exercises/practice/wordy/wordy_test.py +++ b/exercises/practice/wordy/wordy_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/wordy/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2025-06-20 import unittest @@ -13,9 +13,21 @@ class WordyTest(unittest.TestCase): def test_just_a_number(self): self.assertEqual(answer("What is 5?"), 5) + def test_just_a_zero(self): + self.assertEqual(answer("What is 0?"), 0) + + def test_just_a_negative_number(self): + self.assertEqual(answer("What is -123?"), -123) + def test_addition(self): self.assertEqual(answer("What is 1 plus 1?"), 2) + def test_addition_with_a_left_hand_zero(self): + self.assertEqual(answer("What is 0 plus 2?"), 2) + + def test_addition_with_a_right_hand_zero(self): + self.assertEqual(answer("What is 3 plus 0?"), 3) + def test_more_addition(self): self.assertEqual(answer("What is 53 plus 2?"), 55) @@ -61,12 +73,6 @@ def test_unknown_operation(self): self.assertEqual(type(err.exception), ValueError) self.assertEqual(err.exception.args[0], "unknown operation") - def test_non_math_question(self): - with self.assertRaises(ValueError) as err: - answer("Who is the President of the United States?") - self.assertEqual(type(err.exception), ValueError) - self.assertEqual(err.exception.args[0], "unknown operation") - def test_reject_problem_missing_an_operand(self): with self.assertRaises(ValueError) as err: answer("What is 1 plus?") diff --git a/reference/concepts/boolean_values.md b/reference/concepts/boolean_values.md index 40bce78e7cb..a3ec6c0b28a 100644 --- a/reference/concepts/boolean_values.md +++ b/reference/concepts/boolean_values.md @@ -3,4 +3,4 @@ TODO: ADD MORE - this solution uses Boolean values (`True` / `False`) [hamming](../exercise-concepts/hamming.md) -- True and False of type `bopl`. The example solution uses `True` and `False` as return values from functions that test membership in a list of values. [markdown](../exercise-concepts/markdown.md) +- True and False of type `bool`. The example solution uses `True` and `False` as return values from functions that test membership in a list of values. [markdown](../exercise-concepts/markdown.md) diff --git a/reference/concepts/builtin_types/dict.md b/reference/concepts/builtin_types/dict.md index 90e7ec62d5e..f636cb27c2e 100644 --- a/reference/concepts/builtin_types/dict.md +++ b/reference/concepts/builtin_types/dict.md @@ -1,6 +1,6 @@ # `dict` -Python's primary [mapping type][docs-mapping-type] that associatess keys with values in a [hash map][hash-map]. +Python's primary [mapping type][docs-mapping-type] that associates keys with values in a [hash map][hash-map]. See examples of usage in [markdown][markdown], [rna-transcription][rna-transcription], and [robot-simulator][robot-simulator]. diff --git a/reference/concepts/builtin_types/frozenset.md b/reference/concepts/builtin_types/frozenset.md index 03d582e3833..021a657eaa7 100644 --- a/reference/concepts/builtin_types/frozenset.md +++ b/reference/concepts/builtin_types/frozenset.md @@ -2,7 +2,7 @@ TODO: ADD MORE DETAIL -See the Python documentation entries for the [`set`][docs-set] collection, the [immutable][set type][docs-set-type]; essentially a [hash map][hash-map] in which only the key is relevant, and which disallows [mutaion][mutation] of keys after intialization. +See the Python documentation entries for the [`set`][docs-set] collection, the [immutable][set type][docs-set-type]; essentially a [hash map][hash-map] in which only the key is relevant, and which disallows [mutation][mutation] of keys after initialization. [immutable]: https://github.com/exercism/v3/blob/main/reference/concepts/immutability.md [mutation]: https://github.com/exercism/v3/blob/main/reference/concepts/mutation.md diff --git a/reference/concepts/builtin_types/list.md b/reference/concepts/builtin_types/list.md index 364df7b9f5b..c7d3231a0fd 100644 --- a/reference/concepts/builtin_types/list.md +++ b/reference/concepts/builtin_types/list.md @@ -8,7 +8,7 @@ A multi-dimensional list-with-a-list is used as a simple (but not very efficient TODO: ADD MORE DETAIL -See the Python documentation entries for the [mutable][mutation] [`list` type][docs-list-type] and it's [constructor][list-as-function]. This is Python's most commonly used [sequential collection][docs-sequence-types], and as it allows _heterogenous data_ it's quite different from the [fixed array][general-concept-array] and [singly-linked list][general-concept-list] types you may have encountered in other, less flexible, languages. +See the Python documentation entries for the [mutable][mutation] [`list` type][docs-list-type] and it's [constructor][list-as-function]. This is Python's most commonly used [sequential collection][docs-sequence-types], and as it allows _heterogeneous data_ it's quite different from the [fixed array][general-concept-array] and [singly-linked list][general-concept-list] types you may have encountered in other, less flexible, languages. [variable-length-quantity]: ../../exercise-concepts/variable-length-quantity.md [markdown]: ../../exercise-concepts/markdown.md diff --git a/reference/concepts/constructor.md b/reference/concepts/constructor.md index 292e436fa1e..b3d14c459c5 100644 --- a/reference/concepts/constructor.md +++ b/reference/concepts/constructor.md @@ -3,4 +3,4 @@ TODO: ADD MORE - student needs to know how to build an object using its constructor [binary-search-tree](../exercise-concepts/binary-search-tree.md) -- customizing object initalization with actions and persisting data. The example uses a constructor to process the passed in data into a list of lists assigned to an instance property [matrix](../exercise-concepts/matrix.md) +- customizing object initialization with actions and persisting data. The example uses a constructor to process the passed in data into a list of lists assigned to an instance property [matrix](../exercise-concepts/matrix.md) diff --git a/reference/concepts/default_arguments.md b/reference/concepts/default_arguments.md index 987eaee82fb..0131934be40 100644 --- a/reference/concepts/default_arguments.md +++ b/reference/concepts/default_arguments.md @@ -2,4 +2,4 @@ TODO: ADD MORE -- pre-setting function arguments to protect against them not being passed by a caller. The example uses `direction = NORTH` and `x=0, y=0` to ensure those values for a `robot` even if they are not initally passed. [robot-simulator](../exercise-concepts/robot-simulator.md) +- pre-setting function arguments to protect against them not being passed by a caller. The example uses `direction = NORTH` and `x=0, y=0` to ensure those values for a `robot` even if they are not initially passed. [robot-simulator](../exercise-concepts/robot-simulator.md) diff --git a/reference/concepts/dunder_methods.md b/reference/concepts/dunder_methods.md index 751eebea35c..c52459ac4c4 100644 --- a/reference/concepts/dunder_methods.md +++ b/reference/concepts/dunder_methods.md @@ -7,4 +7,4 @@ TODO: ADD MORE - "dunder" -> "double under", referring to the names of these methods being prefixed with two underscores, e.g. `__init__`. There is no formal privacy in Python, but conventionally a single underscore indicates a private method, or one that the programmer should assume may change at any time; methods without an underscore are considered part of an object's public API. Double underscores are even more special - they are used by Python's builtin functions like `len()`, for example, to allow objects to implement various interfaces and functionality. They can also be used for operator overloading. If you have a custom class that you would like to be able to compare to other instances of the same class, implementing `__lt__`, `__gt__`, `__eq__` etc. allow programmers to use the `>`, `<`, `=` operators. Dunder methods allow programmers to build useful objects with simple interfaces, i.e. you can add two instances together using `+` instead of writing something like `instance1.add(instance2)`. [hamming](../exercise-concepts/hamming.md) - the example uses the `__init__` magic method as its constructor for the class [matrix](../exercise-concepts/matrix.md) - User defined classes can (and generally do) overload the `__init__` method, whose first argument is `self`, because the result of `__init__` is a class _instance_. [phone-number](../exercise-concepts/phone-number.md) -- The example uses `__init__` as a constructor for the class, which also calls `__new__`. In addition, the example uses `__call__()` via the appending of `()` to instance method names, and `__eq__()` (_rich compairison_) via the use of `==` [robot-simulator](../exercise-concepts/robot-simulator.md) +- The example uses `__init__` as a constructor for the class, which also calls `__new__`. In addition, the example uses `__call__()` via the appending of `()` to instance method names, and `__eq__()` (_rich_comparison_) via the use of `==` [robot-simulator](../exercise-concepts/robot-simulator.md) diff --git a/reference/concepts/initialization.md b/reference/concepts/initialization.md index 20979b99265..c0880200d53 100644 --- a/reference/concepts/initialization.md +++ b/reference/concepts/initialization.md @@ -2,4 +2,4 @@ TODO: ADD MORE -- customizing object instatiation with actions and persisting data. The example uses `__init__` to persist a `compass` object and x, y coordinates assigned to instance attributes. [robot-simulator](../exercise-concepts/robot-simulator.md) +- customizing object instantiation with actions and persisting data. The example uses `__init__` to persist a `compass` object and x, y coordinates assigned to instance attributes. [robot-simulator](../exercise-concepts/robot-simulator.md) diff --git a/reference/concepts/instance_attributes.md b/reference/concepts/instance_attributes.md index 1ee02d76d99..7251ef962f1 100644 --- a/reference/concepts/instance_attributes.md +++ b/reference/concepts/instance_attributes.md @@ -2,4 +2,4 @@ TODO: ADD MORE -- this exercise rquires one or more instance attributes to persist passed in data. [robot-simulator](../exercise-concepts/robot-simulator.md) +- this exercise requires one or more instance attributes to persist passed in data. [robot-simulator](../exercise-concepts/robot-simulator.md) diff --git a/reference/concepts/instance_methods.md b/reference/concepts/instance_methods.md index 5b61f32c57c..25ec8caa1c7 100644 --- a/reference/concepts/instance_methods.md +++ b/reference/concepts/instance_methods.md @@ -4,6 +4,6 @@ TODO: ADD MORE - the exercise relies on the `def` statement to create an instance method [allergies](../exercise-concepts/allergies.md) - use of `def` to define a class's methods [clock](../exercise-concepts/clock.md) -- classes can have instance _methods_ which are called from an instance of the class (as opposed to class methods, called from the Class itself). The first parameter of an instance method is always `self`, which is provided when calling from the instance (i.e. the programmer does not need to pass it as an argument explicitly). Static methods are methods called from the class itself, and are not connected to an instance of the class. They have access to class attributes (those defined on the class, not connected to the `self`), and do not require an instance of the class to exist. Classes can also define a `property` by using the `@property` decorator (not shown here); a `property` can be "lazily evaluated" to avoid uneeded computation [phone-number](../exercise-concepts/phone-number.md) +- classes can have instance _methods_ which are called from an instance of the class (as opposed to class methods, called from the Class itself). The first parameter of an instance method is always `self`, which is provided when calling from the instance (i.e. the programmer does not need to pass it as an argument explicitly). Static methods are methods called from the class itself, and are not connected to an instance of the class. They have access to class attributes (those defined on the class, not connected to the `self`), and do not require an instance of the class to exist. Classes can also define a `property` by using the `@property` decorator (not shown here); a `property` can be "lazily evaluated" to avoid unneeded computation [phone-number](../exercise-concepts/phone-number.md) - tests for this exercises require one or more instance methods that will return a specified row or column list of the `matrix`. [matrix](../exercise-concepts/matrix.md) - tests for this exercises require one or more instance methods that will take in a set of starting coordinates and a bearing and then accept a series of instructions that "move" the instance to a new set of coordinates and bearing. [robot-simulator](../exercise-concepts/robot-simulator.md) diff --git a/reference/concepts/instance_properties.md b/reference/concepts/instance_properties.md index f4a05ee9c93..39c49a98e23 100644 --- a/reference/concepts/instance_properties.md +++ b/reference/concepts/instance_properties.md @@ -2,4 +2,4 @@ TODO: ADD MORE -- this exercise rquires one or more instance properties to persist passed in data. [matrix](../exercise-concepts/matrix.md) +- this exercise requires one or more instance properties to persist passed in data. [matrix](../exercise-concepts/matrix.md) diff --git a/reference/concepts/regular_expressions.md b/reference/concepts/regular_expressions.md index 821210074c0..8721003a248 100644 --- a/reference/concepts/regular_expressions.md +++ b/reference/concepts/regular_expressions.md @@ -4,7 +4,7 @@ TODO: ADD MORE - the `re.sub()` function of the `re` module that replaces a `regular expression` match with a new value. The example solutions use this function in various places to substitute _markdown_ syntax for _HTML_ syntax in the passed in markdown text. [markdown](../exercise-concepts/markdown.md) - Both the original code to be refactored for this exercise and the example solution import and use the `re` module for Regular Expressions in python. [markdown](../exercise-concepts/markdown.md) -- the `re.match()` function from the `re` module returns a `match` object with any matched values from a specified Regular Expression or pre-compliled Regular Expression. The example uses `re.match()` in multiple places to search for text patterns that need re-formatting or subsitituting. [markdown](../exercise-concepts/markdown.md) +- the `re.match()` function from the `re` module returns a `match` object with any matched values from a specified Regular Expression or pre-compiled Regular Expression. The example uses `re.match()` in multiple places to search for text patterns that need re-formatting or substituting. [markdown](../exercise-concepts/markdown.md) - Various functions in the re module return a `re.Match` _instance_ which in turn has a `Match.group` method. `Match.group` exists even if there are no groups specified in the pattern. See the [Match.group docs](https://docs.python.org/3/library/re.html#re.Match.group) for more detail. [markdown](../exercise-concepts/markdown.md) - regular expressions is a language of sorts that can detect substrings and extract groups from a string, as well as replace them with something else [phone-number](../exercise-concepts/phone-number.md) - A Domain Specific Language (DSL) for text processing. Like many other programming languages in use, python supports a quasi-dialect of PCRE (_Perl compatible regular expressions_). `Regular expressions` can be used via the core python `re` module, or the third-party `regex` module. Both the original code to be refactored for this exercise and the example solutions use the core `re` module to access `regular expressions` functionality. [markdown](../exercise-concepts/markdown.md) diff --git a/reference/concepts/return_value.md b/reference/concepts/return_value.md index 5cd84d303eb..e2074391c51 100644 --- a/reference/concepts/return_value.md +++ b/reference/concepts/return_value.md @@ -7,7 +7,7 @@ TODO: ADD MORE - Most of the functions in the example solution specify a _return_ value using the `return` keyword. [markdown](../exercise-concepts/markdown.md) - the exercise must use a `return` statement to return a value to the caller [leap](../exercise-concepts/leap.md) - this function return a string by this line: `return text[::-1]` [reverse-string](../exercise-concepts/reverse-string.md) -- the `return` keyword is used in a _return statement_ at the end of a function. Exits a function and may or may not pass data or an expression back to calling code. Functions in python without an expicit `return` keyword and statment will return (pass back) the singleton object `none`. The example code _returns_ a copy of the passed-in argument (assumed to be a string) that has been mapped through `str.translate()`, using the table made from `str.maketrans()` [rna-transcription](../exercise-concepts/rna-transcription.md) -- knowing that functions need not have _explicit_ return statements or values but will return `None` if `return` is not specified. Except for the two `@property`-decorated functions, all of the functions in the example omit an explicit `return` statment and all return `None`. [robot-simulator](../exercise-concepts/robot-simulator.md) +- the `return` keyword is used in a _return statement_ at the end of a function. Exits a function and may or may not pass data or an expression back to calling code. Functions in python without an explicit `return` keyword and statement will return (pass back) the singleton object `none`. The example code _returns_ a copy of the passed-in argument (assumed to be a string) that has been mapped through `str.translate()`, using the table made from `str.maketrans()` [rna-transcription](../exercise-concepts/rna-transcription.md) +- knowing that functions need not have _explicit_ return statements or values but will return `None` if `return` is not specified. Except for the two `@property`-decorated functions, all functions in the example omit an explicit `return` statement and all return `None`. [robot-simulator](../exercise-concepts/robot-simulator.md) - the knowledge of `return` statement could be a useful concept in this exercise [variable-length-quantity](../exercise-concepts/variable-length-quantity.md) - "row" and "column" list values are expected from defined instance method(s) [matrix](../exercise-concepts/matrix.md) diff --git a/reference/concepts/slicing.md b/reference/concepts/slicing.md index dbfe4915619..2694063c0f6 100644 --- a/reference/concepts/slicing.md +++ b/reference/concepts/slicing.md @@ -4,4 +4,4 @@ TODO: ADD MORE - the extended solution to this exercise can employ a slice (returns a copy) instead of calling `.copy()`. [matrix](../exercise-concepts/matrix.md) - a slice within an iterable, i.e. the slice of items from `[x]` to `[y]`, can be accessed via `[x:y]` notation; a third parameter allows "skipping" by `z`, i.e. `stringname[x:y:z]` [phone-number](../exercise-concepts/phone-number.md) -- becase `str` in Python is a sequence type, [slicing](https://docs.python.org/3/reference/expressions.html#slicings) syntax can be used here. Specifically: for syntax `string[start:stop:stride]`: [reverse-string](../exercise-concepts/reverse-string.md) +- because `str` in Python is a sequence type, [slicing](https://docs.python.org/3/reference/expressions.html#slicings) syntax can be used here. Specifically: for syntax `string[start:stop:stride]`: [reverse-string](../exercise-concepts/reverse-string.md) diff --git a/reference/concepts/string_splitting.md b/reference/concepts/string_splitting.md index 778ca9ad7ed..bb6f502c166 100644 --- a/reference/concepts/string_splitting.md +++ b/reference/concepts/string_splitting.md @@ -3,4 +3,4 @@ TODO: ADD MORE - The example solution uses `str.split()` to break the passed in markdown string into a list of lines broken up by the `\n` character. The alternate Python example solution uses `str.splitlines()` for the same effect across all line end characters. [markdown](../exercise-concepts/markdown.md) -- the example uses `str.split` with and without seperators to break the passed in string into "rows" and then "elements" [matrix](../exercise-concepts/matrix.md) +- the example uses `str.split` with and without separators to break the passed in string into "rows" and then "elements" [matrix](../exercise-concepts/matrix.md) diff --git a/reference/concepts/string_translation.md b/reference/concepts/string_translation.md index 2b0c5e9fdcc..456b21abde0 100644 --- a/reference/concepts/string_translation.md +++ b/reference/concepts/string_translation.md @@ -2,4 +2,4 @@ TODO: ADD MORE -- the `str.translate()` _instance method_ is called on an object from the `str` class (e.g. ``.translate()). Returns a copy of the inital string with each character re-mapped through the given _translation table_. The _translation table_ is typically a mapping or sequence type that implements indexing via the magic method `__getitem__()`. [rna-transcription](../exercise-concepts/rna-transcription.md) +- the `str.translate()` _instance method_ is called on an object from the `str` class (e.g. ``.translate()). Returns a copy of the initial string with each character re-mapped through the given _translation table_. The _translation table_ is typically a mapping or sequence type that implements indexing via the magic method `__getitem__()`. [rna-transcription](../exercise-concepts/rna-transcription.md) diff --git a/reference/concepts/type_hinting.md b/reference/concepts/type_hinting.md index cc18920d4ea..18cf3b11a67 100644 --- a/reference/concepts/type_hinting.md +++ b/reference/concepts/type_hinting.md @@ -2,4 +2,4 @@ TODO: ADD MORE -- In modern Python it's possibly to type hint annotations to parameters and variables, see [typing](https://docs.python.org/3/library/typing.html#module-typing). While not neccessary in Python such annotations can help your code be easier to read, understand, and check automatically using tools like `mypy`. [reverse-string](../exercise-concepts/reverse-string.md) +- In modern Python it's possibly to type hint annotations to parameters and variables, see [typing](https://docs.python.org/3/library/typing.html#module-typing). While not necessary in Python such annotations can help your code be easier to read, understand, and check automatically using tools like `mypy`. [reverse-string](../exercise-concepts/reverse-string.md) diff --git a/reference/exercise-concepts/binary-search-tree.md b/reference/exercise-concepts/binary-search-tree.md index a44e11c99de..ae047fc9d56 100644 --- a/reference/exercise-concepts/binary-search-tree.md +++ b/reference/exercise-concepts/binary-search-tree.md @@ -60,7 +60,7 @@ class BinarySearchTree: ## Concepts -- [class][class]: a general comprehension of class concept and and how it works is required, `class` statement +- [class][class]: a general comprehension of class concept and how it works is required, `class` statement - [Implied Argument][implied-argument]: student needs to know how to use statement `self` in a class - [class members][class-members]: student must know how members of a class work - [class methods][class-methods]: student must know how methods of a class work inside and outside the class, the use and meaning of `def` statement @@ -78,5 +78,5 @@ class BinarySearchTree: - [Integer comparison][integer-comparison]: concept required to solve the exercise - [Recursion][recursion]: recursion is a core concept in this exercise - [Lists][lists]: knowledge of lists and iteration on lists is required for this exercise -- [Conditional structures][conditional-structures]: knowledge of conditional conceptis and `if...else` statements are required +- [Conditional structures][conditional-structures]: knowledge of conditional concepts and `if...else` statements are required - [Methods of list][methods-of-list]: the use of methods of list could be useful in this exercise. Methods like `append`, `pop`... diff --git a/reference/exercise-concepts/leap.md b/reference/exercise-concepts/leap.md index deb14475f95..6846d3b097f 100644 --- a/reference/exercise-concepts/leap.md +++ b/reference/exercise-concepts/leap.md @@ -18,7 +18,7 @@ def leap(year): - [Modular Division][modular-division]: the exercise relies on the `%` operator to check if one number is evenly divisible by another - [Boolean Operators][boolean-operators]: the exercise relies on `and`, `or`, and (optionally) `not` to form Boolean predicates - [Boolean Logic][boolean-logic]: the exercise relies on `and` and `or` to combine Boolean predicates into a single logical answer -- [Comparision][comparision]: the exercise relies on the `==` and `!=` operators to make binary comparisons between values +- [Comparison][comparison]: the exercise relies on the `==` and `!=` operators to make binary comparisons between values - [Equivalence][equivalence]: the exercise relies on the `==` and `!=` operators to check that two values are equivalent (or not) - [Order of Evaluation][order-of-evaluation]: the exercise relies on parentheses to explicitly modify the normal order of evaluation of an expression - [Operator Precedence][operator-precedence]: the exercise is most simply stated when the student understands the operator precedence binding rules of Python diff --git a/reference/exercise-concepts/markdown.md b/reference/exercise-concepts/markdown.md index 34c896b883c..137f1a81799 100644 --- a/reference/exercise-concepts/markdown.md +++ b/reference/exercise-concepts/markdown.md @@ -161,14 +161,14 @@ def parse(markdown: str) -> str: - [Regular Expressions][regular-expressions]: Both the original code to be refactored for this exercise and the example solution import and use the `re` module for Regular Expressions in python. - [Importing][importing]: Both the original code to be refactored for the exercise and the example solution use the `import` keyword to import the `re` module in support of Regular Expressions in python. - [String Splitting][string-splitting]: The example solution uses `str.split()` to break the passed in markdown string into a list of lines broken up by the `\n` character. The alternate Python example solution uses `str.splitlines()` for the same effect across all line end characters. -- [Regular Expressions][regular-expressions]: the `re.match()` function from the `re` module returns a `match` object with any matched values from a specified Regular Expression or pre-compliled Regular Expression. The example uses `re.match()` in multiple places to search for text patterns that need re-formatting or subsitituting. +- [Regular Expressions][regular-expressions]: the `re.match()` function from the `re` module returns a `match` object with any matched values from a specified Regular Expression or pre-compiled Regular Expression. The example uses `re.match()` in multiple places to search for text patterns that need re-formatting or substituting. - [Regular expressions][regular-expressions]: A Domain Specific Language (DSL) for text processing. Like many other programming languages in use, python supports a quasi-dialect of PCRE (_Perl compatible regular expressions_). `Regular expressions` can be used via the core python `re` module, or the third-party `regex` module. Both the original code to be refactored for this exercise and the example solutions use the core `re` module to access `regular expressions` functionality. - [Return value][return-value]: Most of the functions in the example solution specify a _return_ value using the `return` keyword. - [None][none]: Pythons null type, referred to when a null or "placeholder" is needed. It is in and of itself a singleton in any given python program. -- [Booleans][booleans]: True and False of type `bopl`. The example solution uses `True` and `False` as return values from functions that test membership in a list of values. +- [Booleans][booleans]: True and False of type `bool`. The example solution uses `True` and `False` as return values from functions that test membership in a list of values. - [Assignment][assignment]: The example solution uses assignment for variables and other values. - [Regular Expressions][regular-expression]: the `re.sub()` function of the `re` module that replaces a `regular expression` match with a new value. The example solutions use this function in various places to substitute _markdown_ syntax for _HTML_ syntax in the passed in markdown text. -- [Dictionaries][dictionaries]: Mapping type. The example solution employes a dictionary to return values from the `parse_line()` function. +- [Dictionaries][dictionaries]: Mapping type. The example solution employs a dictionary to return values from the `parse_line()` function. - [For loops][for-loops]: The example solution uses `for` loops to iterate over various function inputs. - [Iteration][iterable]: The example solution uses the `for _ in _` syntax to iterate over a list of lines. This is possible because a list is an `iterable`. - [Conditionals][conditionals]: The example solution uses `if` to check for pattern matching and membership conditions in different functions for processing different markdown patterns. diff --git a/reference/exercise-concepts/matrix.md b/reference/exercise-concepts/matrix.md index 1b5b2da6c94..37fe76175d2 100644 --- a/reference/exercise-concepts/matrix.md +++ b/reference/exercise-concepts/matrix.md @@ -53,17 +53,17 @@ class Matrix(object): - [Classes][classes]: the exercise objective is to define a `matrix` type. Tested methods are linked to a `matrix` class - [Objects][objects]: creating different instances with different data representing different `matrices` is tested -- [Constructor][constructor]: customizing object initalization with actions and persisting data. The example uses a constructor to process the passed in data into a list of lists assigned to an instance property +- [Constructor][constructor]: customizing object initialization with actions and persisting data. The example uses a constructor to process the passed in data into a list of lists assigned to an instance property - [Dunder Methods][dunder-methods]: the example uses the `__init__` magic method as its constructor for the class - [Return Values][return-value]: "row" and "column" list values are expected from defined instance method(s) - [Implicit Argument][implicit-argument]: the example uses the `self` implicit argument for methods and properties linked to a specific instance of the class - [Namespaces][namespaces]: knowing to use `self`.`` for instance properties and `self` as first argument to instance methods in a class - [Instance Methods][instance-methods]: tests for this exercises require one or more instance methods that will return a specified row or column list of the `matrix`. -- [Instance Properties][instance-properties]: this exercise rquires one or more instance properties to persist passed in data. +- [Instance Properties][instance-properties]: this exercise requires one or more instance properties to persist passed in data. - [Mutability][mutability]: in the extended example, knowing there are no protected or private properties in python and adjusting coding patterns - [Assignment][assignment]: instance properties need to be assigned passed in data - [Method Arguments][method-arguments]: the methods returning "row" and "column" need to take both `self` and an integer as arguments -- [Lists][lists]: this exercise requires "row" or "column" be returnd as a `list`. A `list` of `lists` is also the reccommended way to process and store the passed-in data. +- [Lists][lists]: this exercise requires "row" or "column" be returned as a `list`. A `list` of `lists` is also the recommended way to process and store the passed-in data. - [Indexing][indexing]: the "rows" and "columns" of this exercise need to be retrieved from a list of lists via index - [Bracket Notation][bracket-notation]: knowing that `[]` should be used to refer to a value at a specific index in a list - [Slicing][slicing]: the extended solution to this exercise can employ a slice (returns a copy) instead of calling `.copy()`. @@ -71,9 +71,9 @@ class Matrix(object): - [Iterables][iterables]: understanding that strings, lists, and other data structures can be iterated over in the same fashion - [Iterators][iterators]: the example solution for this exercise uses `zip()`, which returns an _iterator_. - [For Loop][for-loop]: iterating over the passed in `matrix` string using a `for` loop to extract "rows" and "columns" that are appended to a list -- [Comprehension Syntax][comprehension-syntax]: knowing that this is equivelent to a `for loop` - putting the row or column creation code _inside_ the list literal instead of using loop + append. -- [Zip][zip]: the example solution for this exercise uses this function to aggregage the column-wise elements of each rown list to form the matrix "columns". +- [Comprehension Syntax][comprehension-syntax]: knowing that this is equivalent to a `for loop` - putting the row or column creation code _inside_ the list literal instead of using loop + append. +- [Zip][zip]: the example solution for this exercise uses this function to aggregate the column-wise elements of each row list to form the matrix "columns". - [Argument Unpacking][argument unpacking]: the example solution for this exercise uses `splat` (`*`) to unpack rows for the `zip()` function. -- [String Splitting][string-splitting]: the example uses `str.split` with and without seperators to break the passed in string into "rows" and then "elements" +- [String Splitting][string-splitting]: the example uses `str.split` with and without separators to break the passed in string into "rows" and then "elements" - [Type Conversion][type-conversion]: the passed in data is in `str` format but the output is expected as a list of type `int`. - [Int][int]: the example converts the parsed `str` elements into `int` diff --git a/reference/exercise-concepts/phone-number.md b/reference/exercise-concepts/phone-number.md index b7b631f4ab0..d56201ccdcf 100644 --- a/reference/exercise-concepts/phone-number.md +++ b/reference/exercise-concepts/phone-number.md @@ -40,7 +40,7 @@ class PhoneNumber: - [Class][class]: classes are defined with the `class :` syntax - [Dunder Methods][dunder-methods]: User defined classes can (and generally do) overload the `__init__` method, whose first argument is `self`, because the result of `__init__` is a class _instance_. - [Inheritance][inheritance]: The default `__str___` method is inherited from `Object`, which every class in Python inherits from. (See: inheritance) -- [Methods][methods]: classes can have instance _methods_ which are called from an instance of the class (as opposed to class methods, called from the Class itself). The first parameter of an instance method is always `self`, which is provided when calling from the instance (i.e. the programmer does not need to pass it as an argument explicitly). Static methods are methods called from the class itself, and are not connected to an instance of the class. They have access to class attributes (those defined on the class, not connected to the `self`), and do not require an instance of the class to exist. Classes can also define a `property` by using the `@property` decorator (not shown here); a `property` can be "lazily evaluated" to avoid uneeded computation +- [Methods][methods]: classes can have instance _methods_ which are called from an instance of the class (as opposed to class methods, called from the Class itself). The first parameter of an instance method is always `self`, which is provided when calling from the instance (i.e. the programmer does not need to pass it as an argument explicitly). Static methods are methods called from the class itself, and are not connected to an instance of the class. They have access to class attributes (those defined on the class, not connected to the `self`), and do not require an instance of the class to exist. Classes can also define a `property` by using the `@property` decorator (not shown here); a `property` can be "lazily evaluated" to avoid unneeded computation - [Non-Public Methods][non-public-methods]: Methods or attributes (including those of an imported module) prefixed with an underscore, `_`, are conventionally treated as "non-public" methods. Python does not support data privacy in the way a language like Java does. Instead convention dictates that methods and attributes that are not prefixed with a single underscore can be expected to remain stable along with semver, i.e. a public method will be backwards compatible with minor version updates, and can change with major version updates. Generally, importing non-public functions or using non-public methods is discouraged, though Python will not explicitly stop the programmer from doing so. - [Implied Argument][implied-argument]: within the class definition, methods and properties can be accessed via the `self.` notation - [Inheritance][inheritance]: a "subclass" will inherit all methods, attributes from it's parent class, and can then override methods as needed. Overriding means the logic in the parent class is not used. The `super` builtin function (not shown here) exists to allow the programmer to defer logic up the inheritance chain to the parent class when needed. diff --git a/reference/exercise-concepts/reverse-string.md b/reference/exercise-concepts/reverse-string.md index 52b5fd4e2f5..b9a9944e9ec 100644 --- a/reference/exercise-concepts/reverse-string.md +++ b/reference/exercise-concepts/reverse-string.md @@ -16,7 +16,7 @@ def reverse(text: str = "") -> str: - [Immutability][immutability]: `text` str in Python is [immutable](https://docs.python.org/3/library/stdtypes.html#text-sequence-type-str). In this exercise, you return a new string, the old string `text` is not changed. - [Return Value][return-value]: this function return a string by this line: `return text[::-1]` -- [Slicing][slicing]: becase `str` in Python is a sequence type, [slicing](https://docs.python.org/3/reference/expressions.html#slicings) syntax can be used here. Specifically: for syntax `string[start:stop:stride]`: +- [Slicing][slicing]: because `str` in Python is a sequence type, [slicing](https://docs.python.org/3/reference/expressions.html#slicings) syntax can be used here. Specifically: for syntax `string[start:stop:stride]`: - `start`: 0-index of the start position, `start=0` by default (i.e., not specified) (start from the beginning) - `stop`: 0-index of the stop position, `stop=-1` by default (i.e., not specified) (stop at the end) @@ -31,4 +31,4 @@ def reverse(text: str = "") -> str: [Extra material for string slicing.](https://www.digitalocean.com/community/tutorials/how-to-index-and-slice-strings-in-python-3) - [Docstrings][docstrings]: used to document the function, normally situated right below `def func():` -- [Type hinting][type-hinting]: In modern Python it's possibly to type hint annotations to parameters and variables, see [typing](https://docs.python.org/3/library/typing.html#module-typing). While not neccessary in Python such annotations can help your code be easier to read, understand, and check automatically using tools like `mypy`. +- [Type hinting][type-hinting]: In modern Python it's possibly to type hint annotations to parameters and variables, see [typing](https://docs.python.org/3/library/typing.html#module-typing). While not necessary in Python such annotations can help your code be easier to read, understand, and check automatically using tools like `mypy`. diff --git a/reference/exercise-concepts/rna-transcription.md b/reference/exercise-concepts/rna-transcription.md index 004ded95110..3eb5f63b6f6 100644 --- a/reference/exercise-concepts/rna-transcription.md +++ b/reference/exercise-concepts/rna-transcription.md @@ -16,7 +16,7 @@ def to_rna(dna_strand): - [Static Methods][static-methods]: Distinct from built-in functions, instance methods, and class methods, these are methods that are bound to a class, rather than an instance, and called _without_ explicitly or implicitly passing in an object of the class. The example solution for this exercise uses the `static` `str` method `maketrans`. - [String Methods][string-methods]: this exercise uses `str.maketrans()` (a static method of `str` that returns a dictionary to create a _translation table_ as required by the `str.translate()` instance method. This method is unusual in that it takes either a single dictionary or two strings of equal length. The example solution for this exercise uses `str.maketrans()` with a two-string argument. - [Dictionary][dictionary]: mapping type that has key-value pairs. Returned by `str.maketrans` in the example code. Also one of the argument types accepted by `str.maketrans()`. -- [String Translation][string-translation]: the `str.translate()` _instance method_ is called on an object from the `str` class (e.g. ``.translate()). Returns a copy of the inital string with each character re-mapped through the given _translation table_. The _translation table_ is typically a mapping or sequence type that implements indexing via the magic method `__getitem__()`. +- [String Translation][string-translation]: the `str.translate()` _instance method_ is called on an object from the `str` class (e.g. ``.translate()). Returns a copy of the initial string with each character re-mapped through the given _translation table_. The _translation table_ is typically a mapping or sequence type that implements indexing via the magic method `__getitem__()`. - [Function][function]: A named (_and often reusable_) section of code that performs a specific task. It may or may not have _arguments_ passed in, and may or may not _return_ data. Created using the `def` keyword. - [Function Arguments][function-arguments]: Parameters passed into a function. In python, these are noted in the `()` following a function name. The example code uses a function named `to_rna()` with an argument of `dna_strand`. -- [Return Value][return-value]: the `return` keyword is used in a _return statement_ at the end of a function. Exits a function and may or may not pass data or an expression back to calling code. Functions in python without an expicit `return` keyword and statment will return (pass back) the singleton object `none`. The example code _returns_ a copy of the passed-in argument (assumed to be a string) that has been mapped through `str.translate()`, using the table made from `str.maketrans()` +- [Return Value][return-value]: the `return` keyword is used in a _return statement_ at the end of a function. Exits a function and may or may not pass data or an expression back to calling code. Functions in python without an explicit `return` keyword and statement will return (pass back) the singleton object `none`. The example code _returns_ a copy of the passed-in argument (assumed to be a string) that has been mapped through `str.translate()`, using the table made from `str.maketrans()` diff --git a/reference/exercise-concepts/robot-simulator.md b/reference/exercise-concepts/robot-simulator.md index 8f80f67c1c3..702ada899aa 100644 --- a/reference/exercise-concepts/robot-simulator.md +++ b/reference/exercise-concepts/robot-simulator.md @@ -67,8 +67,8 @@ class Robot: - [Range][range]: the `range()` built-in type represents an immutable sequence of numbers (or any object that implements the `__index__` dunder method). Used in the example to represent the values from zero to 3 as assigned to NORTH, EAST, SOUTH, WEST. - [Class][class]: the exercise objective is to define a `robot` type. Tested methods are linked to a `robot` class. - [Instantiation][instantiation]: creating different instances of the `robot` class with different data representing different starting positions and bearing are tested. -- [Initialization][initialization]: customizing object instatiation with actions and persisting data. The example uses `__init__` to persist a `compass` object and x, y coordinates assigned to instance attributes. -- [Return Value][return-value]: knowing that functions need not have _explicit_ return statements or values but will return `None` if `return` is not specified. Except for the two `@property`-decorated functions, all of the functions in the example omit an explicit `return` statment and all return `None`. +- [Initialization][initialization]: customizing object instantiation with actions and persisting data. The example uses `__init__` to persist a `compass` object and x, y coordinates assigned to instance attributes. +- [Return Value][return-value]: knowing that functions need not have _explicit_ return statements or values but will return `None` if `return` is not specified. Except for the two `@property`-decorated functions, all of the functions in the example omit an explicit `return` statement and all return `None`. - [Implicit Argument][implicit-argument]: the example uses `self` for methods and properties linked to a specific instance of the class. - [Namespaces][namespaces]: knowing to use `self.` for instance attributes and `self` as first argument to instance methods in a class. Additionally, the example uses `self.()` to call a previously stored method name. - [Instance Methods][instance-methods]: tests for this exercises require one or more instance methods that will take in a set of starting coordinates and a bearing and then accept a series of instructions that "move" the instance to a new set of coordinates and bearing. @@ -76,11 +76,11 @@ class Robot: - [Higher-Order Function][higher-order-function]: a function that takes one or more other functions as arguments, _returning_ a function as its return value. The example uses the built-in `property()` as a higher-order function through `@property`. - [Property][property]: the `property()` built-in is a function that returns a property attribute. When used as a decorator, this transforms the passed-in method into a _getter_ method for read-only attribute with the same name and docstring. - [Assignment][assignment]: the example uses assignment for all the instance properties and `instructions` dictionary. -- [Instance Attributes][instance-attributes]: this exercise rquires one or more instance attributes to persist passed in data. +- [Instance Attributes][instance-attributes]: this exercise requires one or more instance attributes to persist passed in data. - [Mutability][mutability]: in the example, knowing there are no protected or private properties in python and so consciously mutating `self.x`, `self.y` and `self.compass` through the called instance methods. - [Method Parameters][method-parameters]: the example `__init__` method has `self`, direction, x, and y (coordinates) as parameters. It also uses `self` and `commands` (a string) for parameters of the `move()` method. -- [Default Arguments][default-arguments]: pre-setting function arguments to protect against them not being passed by a caller. The example uses `direction = NORTH` and `x=0, y=0` to ensure those values for a `robot` even if they are not initally passed. -- [Dictionary][dictionary]: the example uses a dictionary to map paassed in move arguments to methods that perform the moves. The example also uses a dictionary/mapping created by calling `str.maketrans()`. +- [Default Arguments][default-arguments]: pre-setting function arguments to protect against them not being passed by a caller. The example uses `direction = NORTH` and `x=0, y=0` to ensure those values for a `robot` even if they are not initially passed. +- [Dictionary][dictionary]: the example uses a dictionary to map passed in move arguments to methods that perform the moves. The example also uses a dictionary/mapping created by calling `str.maketrans()`. - [Indexing][indexing]: finding a value by key in a dictionary using `[]` The example uses passed in move arguments as `keys` to look up corresponding `values` (_method names_) for moving the robot in the _instructions_ dictionary. - [Iteration][iteration]: the example uses a `for loop` to iterate through the letters of the passed-in `commands` string and looks up the corresponding values in a dictionary, so that the appropriate methods can be called to move the `robot`. - [Composition][composition]: adding functionality from a class by incorporating an instance of that class in a class you are creating. The example creates a `robot` by instantiating a `compass` and assigning it to the `self`.compass attribute of `robot`.