diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ed8f4a432bc..234b07e766c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,4 +6,4 @@ updates: - package-ecosystem: 'github-actions' directory: '/' schedule: - interval: 'daily' + interval: 'monthly' diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index a2257ad1734..c2a5519234d 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -6,20 +6,19 @@ name: Exercises check on: push: branches: - - master - main pull_request: jobs: housekeeping: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Set up Python - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 with: - python-version: 3.11.2 + python-version: 3.13.5 - name: Download & Install dependencies run: | @@ -49,24 +48,20 @@ jobs: ./bin/template_status.py -v -p .problem-specifications canonical_sync: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 needs: housekeeping strategy: matrix: - python-version: [3.7, 3.8, 3.9, 3.10.6, 3.11.2] + python-version: [3.10.6, 3.11.2, 3.12, 3.13.5] steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 with: python-version: ${{ matrix.python-version }} - - name: Install dataclasses package - if: ${{ matrix.python-version == '3.6' }} - run: pip install dataclasses - - name: Install pytest - run: pip install pytest~=7.2.2 + run: pip install pytest~=8.4.0 - name: Check exercises run: | diff --git a/.github/workflows/issue-commenter.yml b/.github/workflows/issue-commenter.yml index f8d9227f3c0..9f7631e1465 100644 --- a/.github/workflows/issue-commenter.yml +++ b/.github/workflows/issue-commenter.yml @@ -5,15 +5,15 @@ 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@692973e3d937129bcbf40652eb9f2f61becf3332 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Read issue-comment.md id: issue-comment - uses: juliangruber/read-file-action@b549046febe0fe86f8cb4f93c24e284433f9ab58 + uses: juliangruber/read-file-action@271ff311a4947af354c6abcd696a306553b9ec18 with: path: .github/issue-comment.md diff --git a/.github/workflows/ping-cross-track-maintainers-team.yml b/.github/workflows/ping-cross-track-maintainers-team.yml new file mode 100644 index 00000000000..b6ec9c5662f --- /dev/null +++ b/.github/workflows/ping-cross-track-maintainers-team.yml @@ -0,0 +1,16 @@ +name: Ping cross-track maintainers team + +on: + pull_request_target: + types: + - opened + +permissions: + pull-requests: write + +jobs: + ping: + if: github.repository_owner == 'exercism' # Stops this job from running on forks + uses: exercism/github-actions/.github/workflows/ping-cross-track-maintainers-team.yml@main + secrets: + github_membership_token: ${{ secrets.COMMUNITY_CONTRIBUTIONS_WORKFLOW_TOKEN }} diff --git a/.github/workflows/pr-commenter.yml b/.github/workflows/pr-commenter.yml index 3b2592cda13..a70deb6b890 100644 --- a/.github/workflows/pr-commenter.yml +++ b/.github/workflows/pr-commenter.yml @@ -4,9 +4,9 @@ on: jobs: pr-comment: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - - uses: exercism/pr-commenter-action@085ef62d2a541a112c3ade1d24deea83665ea186 + - uses: exercism/pr-commenter-action@f4a6aa5acc07742989788e70fd89cdc0980f0d1e with: github-token: "${{ github.token }}" config-file: ".github/pr-commenter.yml" \ No newline at end of file diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index f526390311f..29b31a0401e 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@b5d41d4e1d5dceea10e7104786b73624c18a190f 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 b9600cc8b9a..c9b0d21a713 100644 --- a/.github/workflows/test-runner.yml +++ b/.github/workflows/test-runner.yml @@ -8,8 +8,8 @@ on: jobs: test-runner: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Run test-runner run: docker compose run test-runner diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bc278b027e5..6ff557f7087 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,33 +4,37 @@

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.10` - `3.13.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`. +๐ŸŒŸ   Track tooling (_test-runner, representer, analyzer, and Continuous Integration_) runs on Python `3.13.5`. Exercises are grouped into **concept** exercises which teach the [Python syllabus][python-syllabus], and **practice** exercises, which are unlocked by progressing in the syllabus tree  ๐ŸŒด . Concept exercises are constrained to a small set of language or syntax features. @@ -43,34 +47,38 @@ 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


-- Maintainers are happy to review your work and help troubleshoot with you. ๐Ÿ’› ๐Ÿ’™  +- Maintainers are happy to review your work and help troubleshoot with you. ๐Ÿ’› ๐Ÿ’™  If you need help, comment in the Pull Request/issue.  ๐Ÿ™‹๐Ÿฝโ€โ™€๏ธ   + - **Please wait at least 72 hours before pinging or `@`ing reviewers directly.** - Requests are reviewed as soon as is practical/possible. - - (โ— ) Reviewers may be in a different timezone โŒš , or tied up  ๐Ÿงถ  with other tasks. - - **Please wait at least 72 hours before pinging.** -- If you need help, comment in the Pull Request/issue.  ๐Ÿ™‹๐Ÿฝโ€โ™€๏ธ   + - (โ— ) Keep in mind that reviewers may be in a different timezone โŒš , or tied up  ๐Ÿงถ  with other tasks. - If you would like in-progress feedback/discussion, please mark your Pull Request as a **`[draft]`** - Pull Requests should be focused around a single exercise, issue, or change. - Pull Request titles and descriptions should make clear **what** has changed and **why**. - - Please link  ๐Ÿ”—  to any related issues the PR addresses. + - Please link  ๐Ÿ”—  to any related forum discussions or issues the PR addresses. - ๐Ÿ“› [ Open an issue ][open-an-issue]๐Ÿ“›  and discuss it with  ๐Ÿงฐ  maintainers _**before**_: - creating a Pull Request making significant or breaking changes. - for changes across multiple exercises, even if they are typos or small. @@ -195,13 +203,13 @@ _We know it, and trust us, we are working on fixing it._ But if you see  
-This track officially supports Python `3.7 - 3.11.2` for students completing exercises. -The track `test runner`, `analyzer`, and `representer` run in docker on `python:3.11.2-slim`. +This track officially supports Python `3.10 - 3.13.5` for students completing exercises. +The track `test runner`, `analyzer`, and `representer` run in docker on `python:3.13.5-alpine3.22`. Although the majority of test cases are written using `unittest.TestCase`, -- All exercises should be written for compatibility with Python `3.7` - `3.11.2`. -- Version backward _incompatibility_ (_e.g_ an exercise using features introduced in `3.8`, `3.9`, or `3.10`) should be clearly noted in any exercise hints, links, introductions or other notes. +- All exercises should be written for compatibility with Python `3.10` - `3.13.5`. +- Version backward _incompatibility_ (_e.g_ an exercise using features introduced in Python `3.10`+ that would not work in Python `3.10`) should be clearly noted in any exercise hints, links, introductions or other notes. - Here is an example of how the Python documentation handles [version-tagged  ๐Ÿท ][version-tagged-language-features] feature introduction. @@ -222,7 +230,7 @@ Although the majority of test cases are written using `unittest.TestCase`, - For specifications, refer to [Concept Exercise Anatomy][concept-exercise-anatomy], or [Practice Exercise Anatomy][practice-exercise-anatomy] depending on which type of exercise you are contributing to. -- **Practice exercise**, descriptions and instructions come from a centralized, cross-track [problem specifications][problem-specifications] repository. +- **Practice exercise** descriptions and instructions come from a centralized, cross-track [problem specifications][problem-specifications] repository. - Any updates or changes need to be proposed/approved in `problem-specifications` first. - If Python-specific changes become necessary, they need to be appended to the canonical instructions by creating a `instructions.append.md` file in this (`exercism/Python`) repository. @@ -369,45 +377,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 +424,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..20c3bd1ce0c 100644 --- a/README.md +++ b/README.md @@ -4,22 +4,39 @@

Exercism Python Track

                          [![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.11%20Powered)](https://exercism.org) +  [![Exercism_II](https://img.shields.io/badge/Exercism--Built-9101FF?logo=python&logoColor=FFDF58&labelColor=3D7AAB&label=Python%203.13%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 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. -๐ŸŒŸ   Track exercises support Python `3.7` - `3.11.5`. +๐ŸŒŸ   Track exercises support Python `3.10` - `3.13.13`. Exceptions to this support are noted where they occur. -๐ŸŒŸ   Track tooling (_test-runner, representer, analyzer, and Continuous Integration_) runs on Python `3.11.5`. +๐ŸŒŸ   Track tooling (_test-runner, representer, analyzer, and Continuous Integration_) runs on Python `3.13.13`. Exercises are grouped into **concept** exercises which teach the [Python syllabus][python-syllabus], and **practice** exercises, which are unlocked by progressing in the syllabus tree  ๐ŸŒด . Concept exercises are constrained to a small set of language or syntax features. @@ -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-2026 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/basics/about.md b/concepts/basics/about.md index ef873ce418f..6f932bfd16f 100644 --- a/concepts/basics/about.md +++ b/concepts/basics/about.md @@ -64,20 +64,21 @@ For example, `my_first_variable` can be re-assigned many times using `=`, and ca >>> print(my_first_variable) 2 ->>> my_first_variable = "Now, I'm a string." # You may re-bind a name to a different object type and value. +>>> my_first_variable = "Now, I'm a string." # <--You may re-bind a name to a different object type and value. >>> print(type(my_first_variable)) +>>> my_first_variable = 'You can call me "str".' # <--Strings can be declared using single or double quote marks. >>> print(my_first_variable) -"Now, I'm a string." # Strings can be declared using single or double quote marks. +You can call me "str". -import collections ->>> my_first_variable = collections.Counter([1,1,2,3,3,3,4,5,6,7]) # Now my_first_variable has been re-bound to a Counter object. +>>> import collections +>>> my_first_variable = collections.Counter([1,1,2,3,3,3,4,5,6,7]) # <--Now my_first_variable has been re-bound to a Counter object. >>> print(type(my_first_variable)) >>> print(my_first_variable) ->>> Counter({3: 3, 1: 2, 2: 1, 4: 1, 5: 1, 6: 1, 7: 1}) +Counter({3: 3, 1: 2, 2: 1, 4: 1, 5: 1, 6: 1, 7: 1}) ``` @@ -101,19 +102,19 @@ MY_FIRST_CONSTANT = "Some other value" ## Functions -In Python, units of functionality are encapsulated in [_functions._][functions], which are themselves [objects][objects] (_it's [turtles all the way down][turtles all the way down]_). +In Python, units of functionality are encapsulated in [_functions_][functions], which are themselves [objects][objects] (_it's [turtles all the way down][turtles all the way down]_). Functions can be executed by themselves, passed as arguments to other functions, nested, or bound to a class. When functions are bound to a [class][classes] name, they're referred to as [methods][method objects]. Related functions and classes (_with their methods_) can be grouped together in the same file or module, and imported in part or in whole for use in other programs. The `def` keyword begins a [function definition][function definition]. -Each function can have zero or more formal [parameters][parameters] in `()` parenthesis, followed by a `:` colon. +Each function can have zero or more formal [parameters][parameters] in `()` parentheses, followed by a `:` colon. Statements for the _body_ of the function begin on the line following `def` and must be _indented in a block_: ```python -# The body of a function is indented by 2 spaces, & prints the sum of the numbers. +# The body of a function is indented by 2 spaces & prints the sum of the numbers. def add_two_numbers(number_one, number_two): total = number_one + number_two print(total) @@ -125,7 +126,7 @@ def add_two_numbers(number_one, number_two): # Inconsistent indentation in your code blocks will raise an error. >>> def add_three_numbers_misformatted(number_one, number_two, number_three): ... result = number_one + number_two + number_three # This was indented by 4 spaces. -... print(result) #this was only indented by 3 spaces +... print(result) # <--This was only indented by 3 spaces. ... ... File "", line 3 @@ -144,7 +145,7 @@ def add_two_numbers(number_one, number_two): return number_one + number_two -# Calling the function in the Python terminal returns the sum of the numbers. +# Calling the function in the Python shell returns the sum of the numbers. >>> add_two_numbers(3, 4) 7 @@ -155,28 +156,42 @@ def add_two_numbers(number_one, number_two): 11 ``` -Functions that do not have an _explicit_ `return` expression will _implicitly_ return the [`None`][none] object. -The details of `None` will be covered in a later exercise. +Functions that do not have an _explicit_ expression following a `return` will _implicitly_ return the [`None`][none] object. +The details of `None` will be covered in a later concept. For the purposes of this exercise and explanation, `None` is a placeholder that represents nothing, or null: ```python -# This function does not have an explicit return. -def add_two_numbers(number_one, number_two): - result = number_one + number_two +# This function will return `None` +def square_a_number(number): + square = number * number + return # <-- note that this return is not followed by an expression -# Calling the function in the Python terminal appears +# Calling the function in the Python shell appears # to not return anything at all. ->>> add_two_numbers(5, 7) +>>> square_a_number(2) >>> # Using print() with the function call shows that # the function is actually returning the **None** object. ->>> print(add_two_numbers(5, 7)) +>>> print(square_a_number(2)) None +``` + +Functions that omit `return` will also _implicitly_ return the [`None`][none] object. +This means that if you do not use `return` in a function, Python will return the `None` object for you. +```python + +# This function omits a return keyword altogether +def add_two_numbers(number_one, number_two): + result = number_one + number_two + +>>> add_two_numbers(5, 7) +>>> print(add_two_numbers(5, 7)) +None # Assigning the function call to a variable and printing # the variable will also show None. @@ -192,32 +207,41 @@ Functions are [_called_][calls] or invoked using their name followed by `()`. Dot (`.`) notation is used for calling functions defined inside a class or module. ```python ->>> def number_to_the_power_of(number_one, number_two): - return number_one ** number_two +>>> def raise_to_power(number, power): +... return number ** power ... ->>> number_to_the_power_of(3,3) # Invoking the function with the arguments 3 and 3. +>>> raise_to_power(3,3) # Invoking the function with the arguments 3 and 3. 27 # A mis-match between the number of parameters and the number of arguments will raise an error. ->>> number_to_the_power_of(4,) +>>> raise_to_power(4,) ... Traceback (most recent call last): File "", line 1, in -TypeError: number_to_the_power_of() missing 1 required positional argument: 'number_two' +TypeError: raise_to_power() missing 1 required positional argument: 'power' # Calling methods or functions in classes and modules. >>> start_text = "my silly sentence for examples." ->>> str.upper(start_text) # Calling the upper() method for the built-in str class. -"MY SILLY SENTENCE FOR EXAMPLES." +>>> str.upper(start_text) # <--Calling the upper() method from the built-in str class on start_text. +'MY SILLY SENTENCE FOR EXAMPLES.' + +# Because a string is an instance of the str class, methods can also be called on them "directly". +>>> start_text = "my silly sentence for examples." +>>> start_text.upper() # <--Calling the upper() method on start_text directly. +'MY SILLY SENTENCE FOR EXAMPLES.' + +# Alternatively, we can skip the variable assignment (although this gets messy quick). +>>> "my silly sentence for examples.".upper() +'MY SILLY SENTENCE FOR EXAMPLES.' -# Importing the math module -import math ->>> math.pow(2,4) # Calling the pow() function from the math module ->>> 16.0 +# Importing the math module +>>> import math +>>> math.pow(2,4) # <--Calling the pow() function from the math module. +16.0 ``` @@ -248,14 +272,18 @@ Docstrings are declared using triple double quotes (""") indented at the same le ```python +# An example from PEP257 of a multi-line docstring +# reformatted to use Google style non-type hinted docstrings. +# Some additional details can be found in the Sphinx documentation: +# https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#getting-started -# An example from PEP257 of a multi-line docstring. def complex(real=0.0, imag=0.0): """Form a complex number. - Keyword arguments: - real -- the real part (default 0.0) - imag -- the imaginary part (default 0.0) + Keyword Arguments: + real (float): The real part of the number (default 0.0) + imag (float): The imaginary part of the number (default 0.0) + """ if imag == 0.0 and real == 0.0: @@ -272,33 +300,40 @@ Testing and `doctest` will be covered in a later concept. ```python -# An example on a user-defined function. ->>> def number_to_the_power_of(number_one, number_two): - """Raise a number to an arbitrary power. - - :param number_one: int the base number. - :param number_two: int the power to raise the base number to. - :return: int - number raised to power of second number +# An example on a user-defined function using a Google style docstring. +>>> def raise_to_power(number, power): + """Raise a number to an arbitrary power. + + Parameters: + number (int): The base number. + power (int): The power to raise the base number to. + + Returns: + int: The number raised to the specified power. + + Takes a number and raises it to the specified power, returning the result. - Takes number_one and raises it to the power of number_two, returning the result. - """ + """ - return number_one ** number_two + return number ** power ... # Calling the .__doc__ attribute of the function and printing the result. ->>> print(number_to_the_power_of.__doc__) +>>> print(raise_to_power.__doc__) Raise a number to an arbitrary power. - :param number_one: int the base number. - :param number_two: int the power to raise the base number to. - :return: int - number raised to power of second number +Parameters: + number (int): The base number. + power (int): The power to raise the base number to. - Takes number_one and raises it to the power of number_two, returning the result. +Returns: + int: The number raised to the specified power. +Takes a number and raises it to the specified power, returning the result. +... -# Printing the __doc__ attribute for the built-in type: str. +# Printing the __doc__ attribute of the built-in type: str. >>> print(str.__doc__) str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str @@ -308,10 +343,11 @@ errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). -encoding defaults to sys.getdefaultencoding(). +encoding defaults to 'utf-8'. errors defaults to 'strict'. ``` + [PEP257]: https://www.python.org/dev/peps/pep-0257/ [calls]: https://docs.python.org/3/reference/expressions.html#calls [classes]: https://docs.python.org/3/reference/datamodel.html#classes diff --git a/concepts/basics/introduction.md b/concepts/basics/introduction.md index 2a874394ebb..cb61a0184ab 100644 --- a/concepts/basics/introduction.md +++ b/concepts/basics/introduction.md @@ -2,23 +2,31 @@ Python is a [dynamic and strongly typed][dynamic typing in python] programming language. It employs both [duck typing][duck typing] and [gradual typing][gradual typing], via [type hints][type hints]. +Python puts a strong emphasis on code readability and (_similar to Haskell_) uses [significant indentation][significant indentation] to denote function, method, and class definitions. + +Python was created by Guido van Rossum and first released in 1991. Imperative, declarative (e.g., functional), and object-oriented programming _styles_ are all supported, but internally **[everything in Python is an object][everythings an object]**. -Python puts a strong emphasis on code readability and (_similar to Haskell_) uses [significant indentation][significant indentation] to denote function, method, and class definitions. +We'll dig more into what all of that means as we continue through the Python track concepts. -Python was created by Guido van Rossum and first released in 1991. +This first concept (`basics`) introduces 4 major Python language features: +1. Name Assignment (_variables and constants_), +2. Functions (_the `def` keyword and the `return` keyword_), +3. Comments, and +4. Docstrings. +
## Name Assignment (Variables & Constants) Programmers can bind [_names_][facts-and-myths-about-python-names] (also called _variables_) to any type of object using the assignment `=` operator: ` = `. -A name can be reassigned (or re-bound) to different values (different object types) over its lifetime. +A name can be reassigned (or re-bound) to different values (different object types) over its lifetime: ```python ->>> my_first_variable = 1 # my_first_variable bound to an integer object of value one. ->>> my_first_variable = 2 # my_first_variable re-assigned to integer value 2. +>>> my_first_variable = 1 # <--my_first_variable bound to an integer object of value one. +>>> my_first_variable = 2 # <--my_first_variable re-assigned to integer value 2. >>> print(type(my_first_variable)) @@ -26,31 +34,33 @@ A name can be reassigned (or re-bound) to different values (different object typ >>> print(my_first_variable) 2 ->>> my_first_variable = "Now, I'm a string." # You may re-bind a name to a different object type and value. +>>> my_first_variable = "Now, I'm a string." # <--You may re-bind a name to a different object type and value. >>> print(type(my_first_variable)) +>>> my_first_variable = 'You can call me "str".' # <--Strings can be declared using single or double quote marks. >>> print(my_first_variable) -"Now, I'm a string." # Strings can be declared using single or double quote marks. +You can call me "str". ``` ### Constants -Constants are names meant to be assigned only once in a program. -They should be defined at a [module][module] (file) level, and are typically visible to all functions and classes in the program. -Using `SCREAMING_SNAKE_CASE` signals that the name should not be re-assigned, or its value mutated. +Constants are names meant to be assigned only once in a program โ€” although Python will not prevent re-assignment. +Using `SCREAMING_SNAKE_CASE` signals to anyone reading the code that the name should **not** be re-assigned, or its value mutated. +Constants should be defined at a [module][module] (file) level, and are typically visible to all functions and classes in a program. + ## Functions The `def` keyword begins a [function definition][function definition]. -Each function can have zero or more formal [parameters][parameters] in `()` parenthesis, followed by a `:` colon. +Each function can have zero or more formal [parameters][parameters] in `()` parentheses, followed by a `:` colon. Statements for the _body_ of the function begin on the line following `def` and must be _indented in a block_. ```python -# The body of this function is indented by 2 spaces,& prints the sum of the numbers. +# The body of this function is indented by 2 spaces & prints the sum of the numbers. def add_two_numbers(number_one, number_two): total = number_one + number_two print(total) @@ -62,7 +72,7 @@ def add_two_numbers(number_one, number_two): # Inconsistent indentation in your code blocks will raise an error. >>> def add_three_numbers_misformatted(number_one, number_two, number_three): ... result = number_one + number_two + number_three # This was indented by 4 spaces. -... print(result) #this was only indented by 3 spaces +... print(result) # <--This was only indented by 3 spaces. ... ... File "", line 3 @@ -81,7 +91,7 @@ def add_two_numbers(number_one, number_two): return number_one + number_two -# Calling the function in the Python terminal returns the sum of the numbers. +# Calling the function in the Python shell returns the sum of the numbers. >>> add_two_numbers(3, 4) 7 @@ -92,28 +102,41 @@ def add_two_numbers(number_one, number_two): 11 ``` -Functions that do not have an _explicit_ `return` expression will _implicitly_ return the [`None`][none] object. -The details of `None` will be covered in a later exercise. + +Functions that do not have an _explicit_ expression following a `return` will _implicitly_ return the [`None`][none] object. +The details of `None` will be covered in a later concept. For the purposes of this exercise and explanation, `None` is a placeholder that represents nothing, or null: ```python -# This function does not have an explicit return. -def add_two_numbers(number_one, number_two): - result = number_one + number_two - +# This function will return `None` +def square_a_number(number): + square = number * number + return # <-- note that this return is not followed by an expression -# Calling the function in the Python terminal appears +# Calling the function in the Python shell appears # to not return anything at all. ->>> add_two_numbers(5, 7) +>>> square_a_number(2) >>> # Using print() with the function call shows that # the function is actually returning the **None** object. ->>> print(add_two_numbers(5, 7)) +>>> print(square_a_number(2)) None +``` +Functions that omit `return` will also _implicitly_ return the [`None`][none] object. +This means that if you do not use `return` in a function, Python will return the `None` object for you. + +```python +# This function omits a return keyword altogether. +def add_two_numbers(number_one, number_two): + result = number_one + number_two + +>>> add_two_numbers(5, 7) +>>> print(add_two_numbers(5, 7)) +None # Assigning the function call to a variable and printing # the variable will also show None. @@ -133,29 +156,35 @@ Each line of a comment block must start with the `#` character. ## Docstrings The first statement of a function body can optionally be a [_docstring_][docstring], which concisely summarizes the function or object's purpose. -Docstring conventions are laid out in [PEP257][pep257]. +Docstrings are read by automated documentation tools such as [Sphinx][sphinx] and are returned by calling the special attribute `.__doc__` on the function, method, or class name. +General docstring conventions are laid out in [PEP257][pep257], but exact formats will vary by project and team. Docstrings are declared using triple double quotes (""") indented at the same level as the code block: ```python - -# An example from PEP257 of a multi-line docstring. +# An example from PEP257 of a multi-line docstring +# reformatted to use Google style non-type hinted docstrings. +# Some additional details can be found in the Sphinx documentation: +# https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#getting-started def complex(real=0.0, imag=0.0): """Form a complex number. - Keyword arguments: - real -- the real part (default 0.0) - imag -- the imaginary part (default 0.0) + Keyword Arguments: + real (float): The real part of the number (default 0.0) + imag (float): The imaginary part of the number (default 0.0) + """ if imag == 0.0 and real == 0.0: return complex_zero - ``` -[pep257]: https://www.python.org/dev/peps/pep-0257/ +Docstrings can also function as [lightweight unit tests][doctests], which will be covered in a later concept. + + [comments]: https://realpython.com/python-comments-guide/#python-commenting-basics [docstring]: https://docs.python.org/3/tutorial/controlflow.html#tut-docstrings +[doctests]: https://docs.python.org/3/library/doctest.html [duck typing]: https://en.wikipedia.org/wiki/Duck_typing [dynamic typing in python]: https://stackoverflow.com/questions/11328920/is-python-strongly-typed [everythings an object]: https://docs.python.org/3/reference/datamodel.html @@ -165,6 +194,8 @@ def complex(real=0.0, imag=0.0): [module]: https://docs.python.org/3/tutorial/modules.html [none]: https://docs.python.org/3/library/constants.html [parameters]: https://docs.python.org/3/glossary.html#term-parameter +[pep257]: https://www.python.org/dev/peps/pep-0257/ [return]: https://docs.python.org/3/reference/simple_stmts.html#return -[type hints]: https://docs.python.org/3/library/typing.html [significant indentation]: https://docs.python.org/3/reference/lexical_analysis.html#indentation +[sphinx]: https://www.sphinx-doc.org/en/master/usage/index.html +[type hints]: https://docs.python.org/3/library/typing.html diff --git a/concepts/binary-octal-hexadecimal/about.md b/concepts/binary-octal-hexadecimal/about.md index a7fca3714e3..67646aed2f2 100644 --- a/concepts/binary-octal-hexadecimal/about.md +++ b/concepts/binary-octal-hexadecimal/about.md @@ -18,7 +18,7 @@ A snippet from the base 2 system looks like this, although it continues infinite | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | | 2 \*\* 7 | 2 \*\* 6 | 2 \*\* 5 | 2 \*\* 4 | 2 \*\* 3 | 2 \*\* 2 | 2 \*\* 1 | 2 \*\* 0 | -So if we want to represent the number 6, it would in binary be: 110 +So if we want to represent the number 6 in binary, it would be 110. | Place value | 4 | 2 | 1 | | ------------- | --- | --- | --- | @@ -41,7 +41,6 @@ In Python, we can represent binary literals using the `0b` prefix. If we write `0b10011`, Python will interpret it as a binary number and convert it to base 10. ```python -# 0b10011 >>> 0b10011 19 @@ -86,6 +85,8 @@ However, the usual mathematical operator rules apply: dividing two binary numbe >>> 0b10011/3 6.333333333333333 +``` + ### Converting to and from Binary Representation @@ -133,6 +134,9 @@ For example, `bit_count()` on '0b11011' will return 4: ```python >>> 0b11011.bit_count() 4 +``` + + ~~~~exercism/note If you are working locally, `bit_count()` requires at least Python 3.10. The Exercism online editor currently supports all features through Python 3.11. @@ -148,7 +152,6 @@ In Python, we can represent octal numbers using the `0o` prefix. As with binary, Python automatically converts an octal representation to an `int`. ```python -# 0o123 >>> 0o123 83 ``` @@ -157,7 +160,6 @@ As with binary, octal numbers **are ints** and support all integer operations. Prefixing a number with `0o` that is not in the octal system will raise a `SyntaxError`. ### Converting to and from Octal Representation - To convert an `int` into an octal representation, you can use the built-in [`oct()`][oct] function. This acts similarly to the `bin()` function, returning a string: @@ -165,6 +167,8 @@ This acts similarly to the `bin()` function, returning a string: ```python >>> oct(83) '0o123' +``` + To convert an octal number to an integer, we can use the `int()` function, passing an octal string representation and the base (8) as arguments: @@ -175,22 +179,21 @@ To convert an octal number to an integer, we can use the `int()` function, passi As with binary, giving the wrong base will raise a `ValueError`. -### Hexadecimal +## Hexadecimal [Hexadecimal][hexadecimal] is a base 16 numeral system. It uses the digits 0 - 9 and the letters A, B, C, D, E, and F. A is 10, B is 11, C is 12, D is 13, E is 14, and F is 15. We can represent hexadecimal numbers in Python using the `0x` prefix. -As with binary and octal, Python will automatically convert hexadecimal literals to `int`. +As with binary and octal, Python will automatically convert hexadecimal literals to `int`s. ```python -# 0x123 >>> 0x123 291 ``` -As with binary and octal - hexadecimal literals **are ints**, and you can perform all integer operations. +As with binary and octal โ€” hexadecimal literals **are ints**, and you can perform all integer operations with them. Prefixing a non-hexadecimal number with `0x` will raise a `SyntaxError`. @@ -202,6 +205,8 @@ This acts similarly to the `bin()` function, returning a string: ```python >>> hex(291) '0x123' +``` + To convert a hexadecimal representation to an integer, we can use the `int()` function, passing a hexadecimal string with the base (16) as arguments: diff --git a/concepts/binary-octal-hexadecimal/introduction.md b/concepts/binary-octal-hexadecimal/introduction.md index a06ac922faf..84ff634263d 100644 --- a/concepts/binary-octal-hexadecimal/introduction.md +++ b/concepts/binary-octal-hexadecimal/introduction.md @@ -1,4 +1,4 @@ -# binary, octal, hexadecimal +# Binary, Octal, Hexadecimal Binary, octal, and hexadecimal (_also known as hex_) are different [numeral systems][numeral-systems] with different bases. Binary is base 2, octal is base 8, and hexadecimal is base 16. diff --git a/concepts/bitwise-operators/about.md b/concepts/bitwise-operators/about.md index a4ddb509c11..1cd5a237c29 100644 --- a/concepts/bitwise-operators/about.md +++ b/concepts/bitwise-operators/about.md @@ -112,12 +112,12 @@ See the section below for details. In decimal representation, we distinguish positive and negative numbers by using a `+` or `-` sign to the left of the digits. Using these symbols at a binary level proved inefficient for digital computing and raised the problem that `+0` is not the same as `-0`. -Rather than using `-` and `+`, all modern computers use a [`twos-complement`][twos-complement] representation for negative numbers, right down to the silicon chip level. +Rather than using `-` and `+`, all modern computers use a [`two's complement`][twos-complement] representation for negative numbers, right down to the silicon chip level. This means that all bits are inverted and a number is _**interpreted as negative**_ if the left-most bit (also termed the "most significant bit", or MSB) is a `1`. 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`: @@ -145,7 +145,7 @@ This is **not** the `0b10011001` we would see in languages with fixed-size integ The `~` operator only works as expected with _**unsigned**_ byte or integer types, or with fixed-sized integer types. These numeric types are supported in third-party packages such as [`NumPy`][numpy], [`pandas`][pandas], and [`sympy`][sympy] but not in core Python. -In practice, Python programmers quite often use the shift operators described below and `& | ^` with positive numbers only. +In practice, Python programmers quite often use `&`, `|`, `^`, and the shift operators described below with positive numbers only. Bitwise operations with negative numbers are much less common. One technique is to add [`2**32 (or 1 << 32)`][unsigned-int-python] to a negative value to make an `int` unsigned, but this gets difficult to manage. Another strategy is to work with the [`ctypes`][ctypes-module] module, and use c-style integer types, but this is equally unwieldy. @@ -153,13 +153,13 @@ Another strategy is to work with the [`ctypes`][ctypes-module] module, and use c ## [`Shift operators`][bitwise-shift-operators] -The left-shift operator `x << y` simply moves all the bits in `x` by `y` places to the left, filling the new gaps with zeros. -Note that this is arithmetically identical to multiplying a number by `2**y`. +The left-shift operator `x << y` moves all the bits in `x` by `y` places to the left, filling the new gaps with zeros. +Note that this is arithmetically identical to multiplying a number by `(2**y)`. The right-shift operator `x >> y` does the opposite. -This is arithmetically identical to integer division `x // 2**y`. +This is arithmetically identical to integer division `x // (2**y)`. -Keep in mind the previous section on negative numbers and their pitfalls when shifting. +Keep in mind the previous section on negative numbers and their pitfalls when shifting them in Python. ```python @@ -191,7 +191,7 @@ Keep in mind the previous section on negative numbers and their pitfalls when sh [symmetric-difference]: https://math.stackexchange.com/questions/84184/relation-between-xor-and-symmetric-difference#:~:text=It%20is%20the%20same%20thing,they%20are%20indeed%20the%20same. [sympy]: https://docs.sympy.org/latest/modules/codegen.html#predefined-types [tilde]: https://en.wikipedia.org/wiki/Tilde -[twos-complement]: https://en.wikipedia.org/wiki/Two%27s_complement#:~:text=Two's%20complement%20is%20the%20most,number%20is%20positive%20or%20negative. +[twos-complement]: https://en.wikipedia.org/wiki/Two%27s_complement [unsigned-int-python]: https://stackoverflow.com/a/20768199 [xor-cipher]: https://en.wikipedia.org/wiki/XOR_cipher [xor]: https://stackoverflow.com/a/2451393 diff --git a/concepts/bitwise-operators/introduction.md b/concepts/bitwise-operators/introduction.md index 88aba3a6a7b..07833339ff2 100644 --- a/concepts/bitwise-operators/introduction.md +++ b/concepts/bitwise-operators/introduction.md @@ -1,19 +1,19 @@ # Introduction -Down at the hardware level, transistors can only be on or off: two states that we traditionally represent with `1` and `0`. +Down at the hardware level, [transistors can only be on or off][how-transistors-work]: two states that we traditionally represent with `1` and `0`. These are the [`binary digits`][binary-digits], abbreviated as [`bits`][bits]. Awareness of `bits` and `binary` is particularly important for systems programmers working in low-level languages. - However, for most of the history of computing the programming priority has been to find increasingly sophisticated ways to _abstract away_ this binary reality. In Python (and many other [high-level programming languages][high-level-language]), we work with `int`, `float`, `string` and other defined _types_, up to and including audio and video formats. -We let the Python internals take care of (eventually) translating everything to bits. +Python internals take care of (_eventually_) translating all the higher-level data to bits. Nevertheless, using [bitwise-operators][python-bitwise-operators] and [bitwise operations][python-bitwise-operations] can sometimes have significant advantages in speed and memory efficiency, even in a high-level language like Python. [high-level-language]: https://en.wikipedia.org/wiki/High-level_programming_language +[how-transistors-work]: https://www.build-electronic-circuits.com/how-transistors-work/ [binary-digits]: https://www.khanacademy.org/computing/computers-and-internet/xcae6f4a7ff015e7d:digital-information/xcae6f4a7ff015e7d:binary-numbers/v/the-binary-number-system [bits]: https://en.wikipedia.org/wiki/Bit [python-bitwise-operations]: https://docs.python.org/3/reference/expressions.html#binary-bitwise-operations diff --git a/concepts/bitwise-operators/links.json b/concepts/bitwise-operators/links.json index 7c103c84630..ed251fab33a 100644 --- a/concepts/bitwise-operators/links.json +++ b/concepts/bitwise-operators/links.json @@ -1,8 +1,4 @@ [ - { - "url": "https://wiki.python.org/moin/BitwiseOperators/", - "description": "BitwiseOperators on the Python wiki." - }, { "url": "https://realpython.com/python-bitwise-operators", "description": "Real Python: Bitwise Operators in Python." diff --git a/concepts/bools/about.md b/concepts/bools/about.md index a2680fc06b3..7015fdfafa4 100644 --- a/concepts/bools/about.md +++ b/concepts/bools/about.md @@ -1,6 +1,6 @@ # About -Python represents true and false values with the [`bool`][bools] type, which is a subtype of `int`. +Python represents true and false values with the [`bool`][bools] type, which is a subclass of `int`. There are only two Boolean values in this type: `True` and `False`. These values can be assigned to a variable and combined with the [Boolean operators][boolean-operators] (`and`, `or`, `not`): @@ -22,10 +22,10 @@ Each of the operators has a different precedence, where `not` is evaluated befor Brackets can be used to evaluate one part of the expression before the others: ```python ->>>not True and True +>>> not True and True False ->>>not (True and False) +>>> not (True and False) True ``` @@ -45,25 +45,25 @@ A few `built-ins` are always considered `False` by definition: ```python ->>>bool(None) +>>> bool(None) False ->>>bool(1) +>>> bool(1) True ->>>bool(0) +>>> bool(0) False ->>>bool([1,2,3]) +>>> bool([1,2,3]) True ->>>bool([]) +>>> bool([]) False ->>>bool({"Pig" : 1, "Cow": 3}) +>>> bool({"Pig" : 1, "Cow": 3}) True ->>>bool({}) +>>> bool({}) False ``` @@ -95,10 +95,10 @@ The `bool` type is implemented as a _sub-type_ of _int_. ```python ->>>1 == True +>>> 1 == True True ->>>0 == False +>>> 0 == False True ``` @@ -106,14 +106,14 @@ However, `bools` are **still different** from `ints`, as noted when comparing th ```python ->>>1 is True +>>> 1 is True False ->>>0 is False +>>> 0 is False False ``` -> Note: in python >= 3.8, using a literal (such as 1, '', [], or {}) on the _left side_ of `is` will raise a warning. +> Note: in python >= 3.8, using a literal (such as `1`, `''`, `[]`, or `{}`) on the _left side_ of `is` will raise a warning. It is considered a [Python anti-pattern][comparing to true in the wrong way] to use the equality operator to compare a boolean variable to `True` or `False`. @@ -134,10 +134,8 @@ It is considered a [Python anti-pattern][comparing to true in the wrong way] to ``` -[bool-function]: https://docs.python.org/3/library/functions.html#bool -[bool]: https://docs.python.org/3/library/stdtypes.html#truth [Boolean-operators]: https://docs.python.org/3/library/stdtypes.html#boolean-operations-and-or-not +[bool-function]: https://docs.python.org/3/library/functions.html#bool +[bools]: https://docs.python.org/3/library/stdtypes.html#typebool [comparing to true in the wrong way]: https://docs.quantifiedcode.com/python-anti-patterns/readability/comparison_to_true.html [comparisons]: https://docs.python.org/3/library/stdtypes.html#comparisons - -[bools]: https://docs.python.org/3/library/stdtypes.html#typebool \ No newline at end of file diff --git a/concepts/bools/introduction.md b/concepts/bools/introduction.md index af24137025e..85eb032df25 100644 --- a/concepts/bools/introduction.md +++ b/concepts/bools/introduction.md @@ -1,6 +1,6 @@ # Introduction -Python represents true and false values with the [`bool`][bools] type, which is a subtype of `int`. +Python represents true and false values with the [`bool`][bools] type, which is a subclass of `int`. There are only two values under that type: `True` and `False`. These values can be bound to a variable: @@ -22,4 +22,4 @@ We can evaluate Boolean expressions using the `and`, `or`, and `not` operators. >>> false_variable = not True ``` -[bools]: https://docs.python.org/3/library/stdtypes.html#typebool \ No newline at end of file +[bools]: https://docs.python.org/3/library/stdtypes.html#typebool 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..9b6a8a0dfb7 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] @@ -185,10 +185,10 @@ class Demo: The moment that `.add_two()` is called, and `self.new_var += 2` is read, `new_var` changes from a class variable to an instance variable of the same name. This can be useful during initialization when all instances of a class will need some attribute(s) to start with the same value. -However, the instance variable then shadows* the class variable, making the class variable inaccessible from the instance where it is shadowed. +However, the instance variable then [_shadows_](https://oznetnerd.com/2017/07/17/python-shadowing/) the class variable, making the class variable inaccessible from the instance where it is shadowed. Given this situation, it may be safer and clearer to set instance attributes from the `__init__()` method as `self.`. ~~~~ -_*[_shadows_][shadowing] + ## Methods @@ -240,7 +240,7 @@ class MyClass: def change_location(self, amount): self.location_x += amount self.location_y += amount - return self.location_x, self.location_y + return self.location_x, self.location_y # Make a new test_object with location (3,7) >>> test_object = MyClass((3,7)) @@ -267,7 +267,7 @@ class MyClass: def change_location(self, amount): self.location_x += amount self.location_y += amount - return self.location_x, self.location_y + return self.location_x, self.location_y # Alter class variable number for all instances from within an instance. def increment_number(self): @@ -322,4 +322,3 @@ class MyClass: [dunder]: https://mathspp.com/blog/pydonts/dunder-methods [oop]: https://www.educative.io/blog/object-oriented-programming [dot notation]: https://stackoverflow.com/questions/45179186/understanding-the-dot-notation-in-python -[shadowing]: https://oznetnerd.com/2017/07/17/python-shadowing/ diff --git a/concepts/comparisons/about.md b/concepts/comparisons/about.md index 1d2c677d22a..9aa681ff9f5 100644 --- a/concepts/comparisons/about.md +++ b/concepts/comparisons/about.md @@ -13,7 +13,7 @@ The table below shows the most common Python comparison operators: | `<=` | "less than or equal to" | `a <= b` is `True` if `a < b` or `a == b` in value | | `!=` | "not equal to" | `a != b` is `True` if `a == b` is `False` | | `is` | "identity" | `a is b` is `True` if **_and only if_** `a` and `b` are the same _object_ | -| `is not` | "negated identity" | `a is not b` is `True` if `a` and `b` are **not** the same _object_ | +| `is not` | "negated identity" | `a is not b` is `True` if **_and only if_** `a` and `b` are **not** the same _object_ | | `in` | "containment test" | `a in b` is `True` if `a` is member, subset, or element of `b` | | `not in` | "negated containment test" | `a not in b` is `True` if `a` is not a member, subset, or element of `b` | @@ -146,7 +146,7 @@ True Comparison operators can be chained _arbitrarily_. Note that the evaluation of an expression takes place from `left` to `right`. -For example, `x < y <= z` is equivalent to `x < y` `and` `y <= z`, except that `y` is evaluated **only once**. +For example, `x < y <= z` is equivalent to `x < y and y <= z`, except that `y` is evaluated **only once**. In both cases, `z` is _not_ evaluated **at all** when `x < y` is found to be `False`. This is often called `short-circuit evaluation` - the evaluation stops if the truth value of the expression has already been determined. @@ -180,7 +180,7 @@ Due to their singleton status, `None` and `NotImplemented` should always be comp See the Python reference docs on [value comparisons][value comparisons none] and [PEP8][PEP8 programming recommendations] for more details on this convention. ```python ->>> + # A list of favorite numbers. >>> my_fav_numbers = [1, 2, 3] @@ -218,7 +218,7 @@ The operators `in` and `not in` test for _membership_. For string and bytes types, ` in ` is `True` _**if and only if**_ `` is a substring of ``. ```python ->>> + # A set of lucky numbers. >>> lucky_numbers = {11, 22, 33} >>> 22 in lucky_numbers diff --git a/concepts/comparisons/introduction.md b/concepts/comparisons/introduction.md index e597063c621..40c40ea8a10 100644 --- a/concepts/comparisons/introduction.md +++ b/concepts/comparisons/introduction.md @@ -13,7 +13,7 @@ The table below shows the most common Python comparison operators: | `<=` | "less than or equal to" | `a <= b` is `True` if `a < b` or `a == b` in value | | `!=` | "not equal to" | `a != b` is `True` if `a == b` is `False` | | `is` | "identity" | `a is b` is `True` if **_and only if_** `a` and `b` are the same _object_ | -| `is not` | "negated identity" | `a is not b` is `True` if `a` and `b` are **not** the same _object_ | +| `is not` | "negated identity" | `a is not b` is `True` if **_and only if_** `a` and `b` are **not** the same _object_ | | `in` | "containment test" | `a in b` is `True` if `a` is member, subset, or element of `b` | | `not in` | "negated containment test" | `a not in b` is `True` if `a` is not a member, subset, or element of `b` | diff --git a/concepts/complex-numbers/about.md b/concepts/complex-numbers/about.md index dfe067be4ee..2b0de864e7b 100644 --- a/concepts/complex-numbers/about.md +++ b/concepts/complex-numbers/about.md @@ -3,7 +3,7 @@ `Complex numbers` are not complicated. They just need a less alarming name. -They are so useful, especially in engineering and science, that Python includes [`complex`][complex] as a standard numeric type alongside integers ([`int`s][ints]) and floating-point numbers ([`float`s][floats]). +They are so useful โ€” especially in engineering and science โ€” that Python includes [`complex`][complex] as a standard numeric type, alongside integers ([`int`s][ints]) and floating-point numbers ([`float`s][floats]). ## Basics @@ -143,7 +143,7 @@ Any [mathematical][math-complex] or [electrical engineering][engineering-complex Alternatively, Exercism has a `Complex Numbers` practice exercise where you can implement a complex number class with these operations from first principles. -Integer division is ___not___ possible on complex numbers, so the `//` and `%` operators and `divmod()` functions will fail for the complex number type. +Integer division is ___not___ possible on complex numbers, so the `//` and `%` operators and the `divmod()` function will fail for the complex number type. There are two functions implemented for numeric types that are very useful when working with complex numbers: @@ -235,13 +235,13 @@ If you are reading this on any sort of screen, you are utterly dependent on some 1. __Semiconductor chips__. - These make no sense in classical physics and can only be explained (and designed) by quantum mechanics (QM). - - In QM, everything is complex-valued by definition. (_its waveforms all the way down_) + - In QM, everything is complex-valued by definition. (_it's waveforms all the way down_) -2. __The Fast Fourier Transform algorithm__. +2. __The Fast Fourier Transform (FFT) algorithm__. - FFT is an application of complex numbers, and it is in _everything_ connected to sound transmission, audio processing, photos, and video. - -MP3 and other audio formats use FFT for compression, ensuring more audio can fit within a smaller storage space. - - JPEG compression and MP4 video, among many other image and video formats also use FTT for compression. + - MP3 and other audio formats use FFT for compression, ensuring more audio can fit within a smaller storage space. + - JPEG compression and MP4 video, among many other image and video formats, also use FTT for compression. - FFT is also deployed in the digital filters that allow cellphone towers to separate your personal cell signal from everyone else's. diff --git a/concepts/complex-numbers/introduction.md b/concepts/complex-numbers/introduction.md index a82f47cb6cb..419c3f3d486 100644 --- a/concepts/complex-numbers/introduction.md +++ b/concepts/complex-numbers/introduction.md @@ -3,7 +3,9 @@ `Complex numbers` are not complicated. They just need a less alarming name. -They are so useful, especially in engineering and science (_everything from JPEG compression to quantum mechanics_), that Python includes [`complex`][complex] as a standard numeric type alongside integers ([`int`s][ints]) and floating-point numbers ([`float`s][floats]). + +They are so useful โ€” especially in engineering and science โ€” that Python includes [`complex`][complex] as a standard numeric type, alongside integers ([`int`s][ints]) and floating-point numbers ([`float`s][floats]). + A `complex` value in Python is essentially a pair of floating-point numbers: @@ -74,13 +76,13 @@ There are two common ways to create complex numbers. Most of the [`operators`][operators] used with floats and ints also work with complex numbers. -Integer division is _**not**_ possible on complex numbers, so the `//` and `%` operators and `divmod()` functions will fail for the complex number type. +Integer division is _**not**_ possible on complex numbers, so the `//` and `%` operators and the `divmod()` function will fail for the complex number type. Explaining the rules for complex number multiplication and division is out of scope for this concept (_and you are unlikely to have to perform those operations "by hand" very often_). Any [mathematical][math-complex] or [electrical engineering][engineering-complex] introduction to complex numbers will cover these scenarios, should you want to dig into the topic. -The Python standard library has a [`math`][math-module] module full of useful functionality for working with real numbers and the [`cmath`][cmath] module is its equivalent for working with complex numbers. +The Python standard library has a [`math`][math-module] module full of useful functionality for working with real numbers, and the [`cmath`][cmath] module is its equivalent for working with complex numbers. [cmath]: https://docs.python.org/3/library/cmath.html diff --git a/concepts/conditionals/about.md b/concepts/conditionals/about.md index d0b91f26ce6..8891683f791 100644 --- a/concepts/conditionals/about.md +++ b/concepts/conditionals/about.md @@ -56,20 +56,20 @@ else: [Boolean operations][boolean operations] and [comparisons][comparisons] can be combined with conditionals for more complex testing: -```python +```python >>> def classic_fizzbuzz(number): - if number % 3 == 0 and number % 5 == 0: - say = 'FizzBuzz!' - elif number % 5 == 0: - say = 'Buzz!' - elif number % 3 == 0: - say = 'Fizz!' - else: - say = str(number) +... if number % 3 == 0 and number % 5 == 0: +... say = 'FizzBuzz!' +... elif number % 5 == 0: +... say = 'Buzz!' +... elif number % 3 == 0: +... say = 'Fizz!' +... else: +... say = str(number) +... +... return say - return say - >>> classic_fizzbuzz(15) 'FizzBuzz!' @@ -83,14 +83,14 @@ However, re-writing in this way might obscure that the conditions are intended t ```python >>> def classic_fizzbuzz(number): - if number % 3 == 0 and number % 5 == 0: - return 'FizzBuzz!' - if number % 5 == 0: - return 'Buzz!' - if number % 3 == 0: - return 'Fizz!' - - return str(number) +... if number % 3 == 0 and number % 5 == 0: +... return 'FizzBuzz!' +... if number % 5 == 0: +... return 'Buzz!' +... if number % 3 == 0: +... return 'Fizz!' +... +... return str(number) >>> classic_fizzbuzz(15) 'FizzBuzz!' @@ -102,19 +102,20 @@ However, re-writing in this way might obscure that the conditions are intended t Conditionals can also be nested. + ```python >>> def driving_status(driver_age, test_score): - if test_score >= 80: - if 18 > driver_age >= 16: - status = "Student driver, needs supervision." - elif driver_age == 18: - status = "Permitted driver, on probation." - elif driver_age > 18: - status = "Fully licensed driver." - else: - status = "Unlicensed!" - - return status +... if test_score >= 80: +... if 18 > driver_age >= 16: +... status = "Student driver, needs supervision." +... elif driver_age == 18: +... status = "Permitted driver, on probation." +... elif driver_age > 18: +... status = "Fully licensed driver." +... else: +... status = "Unlicensed!" +... +... return status >>> driving_status(63, 78) @@ -130,19 +131,19 @@ Conditionals can also be nested. ## Conditional expressions or "ternary operators" While Python has no specific `?` ternary operator, it is possible to write single-line `conditional expressions`. -These take the form of `` if `` else ``. +These take the form of ` if else `. Since these expressions can become hard to read, it's recommended to use this single-line form only if it shortens code and helps readability. ```python -def just_the_buzz(number): - return 'Buzz!' if number % 5 == 0 else str(number) +>>> def just_the_buzz(number): +... return 'Buzz!' if number % 5 == 0 else str(number) >>> just_the_buzz(15) 'Buzz!' ->>> just_the_buzz(10) -'10' +>>> just_the_buzz(7) +'7' ``` ## Truthy and Falsy @@ -152,29 +153,29 @@ Objects that are evaluated in this fashion are considered "truthy" or "falsy", a ```python >>> def truthy_test(thing): - if thing: - print('This is Truthy.') - else: - print("Nope. It's Falsey.") +... if thing: +... print('This is Truthy.') +... else: +... print("Nope. It's Falsy.") -# Empty container objects are considered Falsey. +# Empty container objects are considered Falsy. >>> truthy_test([]) -Nope. It's Falsey. +"Nope. It's Falsy." >>> truthy_test(['bear', 'pig', 'giraffe']) -This is Truthy. +'This is Truthy.' -# Empty strings are considered Falsey. +# Empty strings are considered Falsy. >>> truthy_test('') -Nope. It's Falsey. +"Nope. It's Falsy." >>> truthy_test('yes') -This is Truthy. +'This is Truthy.' -# 0 is also considered Falsey. +# 0 is also considered Falsy. >>> truthy_test(0) -Nope. It's Falsey. +"Nope. It's Falsy." ``` [boolean operations]: https://docs.python.org/3/library/stdtypes.html#boolean-operations-and-or-not diff --git a/concepts/conditionals/introduction.md b/concepts/conditionals/introduction.md index ee1d4336207..ba4f098493d 100644 --- a/concepts/conditionals/introduction.md +++ b/concepts/conditionals/introduction.md @@ -58,16 +58,15 @@ else: ```python >>> def classic_fizzbuzz(number): - if number % 3 == 0 and number % 5 == 0: - say = 'FizzBuzz!' - elif number % 5 == 0: - say = 'Buzz!' - elif number % 3 == 0: - say = 'Fizz!' - else: - say = str(number) - - return say +... if number % 3 == 0 and number % 5 == 0: +... say = 'FizzBuzz!' +... elif number % 5 == 0: +... say = 'Buzz!' +... elif number % 3 == 0: +... say = 'Fizz!' +... else: +... say = str(number) +... return say >>> classic_fizzbuzz(15) 'FizzBuzz!' @@ -76,6 +75,7 @@ else: '13' ``` + [boolean operations]: https://docs.python.org/3/library/stdtypes.html#boolean-operations-and-or-not [comparisons]: https://docs.python.org/3/library/stdtypes.html#comparisons [control flow tools]: https://docs.python.org/3/tutorial/controlflow.html#more-control-flow-tools diff --git a/concepts/dict-methods/about.md b/concepts/dict-methods/about.md index d910d3e9168..7af90a77145 100644 --- a/concepts/dict-methods/about.md +++ b/concepts/dict-methods/about.md @@ -1,28 +1,29 @@ # Dictionary Methods in Python The `dict` class in Python provides many useful [methods][dict-methods] for working with dictionaries. -Some were introduced in the concept for `dicts`. +Some were introduced in the concept for `dict`s. Here we cover a few more - along with some techniques for iterating through and manipulating dictionaries. -- `dict.setdefault()` automatically adds keys without throwing a KeyError. -- `dict.fromkeys(iterable, )` creates a new `dict` from any number of iterables. -- `.keys()`, `.values()`, and `.items()` provide convenient iterators. -- `sorted(.items())`. can easily re-order entries in a `dict`. -- `dict_one.update()` updates one `dict` with overlapping values from another `dict`. -- `dict | other_dict` and `dict |= other_dict` merges or updates two `dict`s via operators. -- `reversed(dict.keys())`, `reversed(dict.values())`, or `reversed(dict.items())` produce _reversed_ views. +- `.setdefault()` automatically adds keys without throwing a KeyError. +- `.fromkeys(, )` creates a new `dict` from any number of iterables. +- `.keys()`, `.values()`, and `.items()` provide convenient iterators. +- `sorted(.items())` can easily re-order entries in a `dict`. +- `.update()` updates one `dict` with overlapping values from another `dict`. +- ` | ` and ` |= ` merges or updates two `dict`s via operators. +- `reversed(.keys())`, `reversed(.values())`, or `reversed(.items())` produce _reversed_ views. - `.popitem()` removes and returns a `key`, `value` pair. ## `setdefault()` for Error-Free Insertion -The dictionary concept previously covered that `.get(key, )` returns an existing `value` or the `default value` if a `key` is not found in a dictionary, thereby avoiding a `KeyError`. +The dictionary concept previously covered that `.get(, )` returns an existing `value` or the `default value` if a `key` is not found in a dictionary, thereby avoiding a `KeyError`. This works well in situations where you would rather not have extra error handling but cannot trust that a looked-for `key` will be present. -For a similarly "safe" (_without KeyError_) insertion operation, there is the `.setdefault(key, )` method. -`setdefault(key, )` will return the `value` if the `key` is found in the dictionary. +For a similarly "safe" (_without `KeyError`_) insertion operation, there is the `.setdefault(, )` method. +`.setdefault(, )` will return the `value` if the `key` is found in the dictionary. If the key is **not** found, it will _insert_ the (`key`, `default value`) pair and return the `default value` for use. + ```python >>> palette_I = {'Grassy Green': '#9bc400', 'Purple Mountains Majesty': '#8076a3', 'Misty Mountain Pink': '#f9c5bd'} @@ -38,7 +39,7 @@ If the key is **not** found, it will _insert_ the (`key`, `default value`) pair ## `fromkeys()` to Populate a Dictionary from an Iterable -To quickly populate a dictionary with various `keys` and default values, the _class method_ [`fromkeys(iterable, )`][fromkeys] will iterate through an iterable of `keys` and create a new `dict`. +To quickly populate a dictionary with various `keys` and default values, the _class method_ [`fromkeys(, )`][fromkeys] will iterate through an iterable of `keys` and create a new `dict`. All `values` will be set to the `default value` provided: ```python @@ -71,13 +72,12 @@ If the dictionary is empty, calling `popitem()` will raise a `KeyError`: # All (key, value) pairs have been removed. >>> palette_I.popitem() Traceback (most recent call last): - line 1, in palette_I.popitem() - KeyError: 'popitem(): dictionary is empty' ``` + ## Iterating Over Entries in a Dictionary Via Views The `.keys()`, `.values()`, and `.items()` methods return [_iterable views_][dict-views] of a dictionary. @@ -136,7 +136,7 @@ This allows keys, values, or (`key`, `value`) pairs to be iterated over in Last- ('Purple baseline', '#161748') >>> for item in reversed(palette_II.items()): -... print (item) +... print(item) ... ('Purple baseline', '#161748') ('Green Treeline', '#478559') @@ -166,12 +166,12 @@ This method will take the (`key`,`value`) pairs of `` and write them i Where keys in the two dictionaries _overlap_, the `value` in `dict_one` will be _overwritten_ by the corresponding `value` from `dict_two`: ```python ->>> palette_I = {'Grassy Green': '#9bc400', - 'Purple Mountains Majesty': '#8076a3', - 'Misty Mountain Pink': '#f9c5bd', - 'Factory Stone Purple': '#7c677f', - 'Green Treeline': '#478559', - 'Purple baseline': '#161748'} +>>> palette_I = {'Grassy Green': '#9bc400', + 'Purple Mountains Majesty': '#8076a3', + 'Misty Mountain Pink': '#f9c5bd', + 'Factory Stone Purple': '#7c677f', + 'Green Treeline': '#478559', + 'Purple baseline': '#161748'} >>> palette_III = {'Grassy Green': (155, 196, 0), 'Purple Mountains Majesty': (128, 118, 163), @@ -188,7 +188,7 @@ Where keys in the two dictionaries _overlap_, the `value` in `dict_one` will be 'Green Treeline': '#478559', 'Purple baseline': '#161748'} ``` -## Merge or Update Dictionaries Via the Union (`|`) Operators +## Merge or Update Dictionaries Using Union (`|` and `|=`) Operators Python 3.9 introduces a different means of merging `dicts`: the `union` operators. `dict_one | dict_two` will create a **new dictionary**, made up of the (`key`, `value`) pairs of `dict_one` and `dict_two`. @@ -214,7 +214,7 @@ When both dictionaries share keys, `dict_two` values take precedence. 'Purple baseline': '#161748'} ``` -`dict_one |= other` behaves similar to `.update()`, but in this case, `other` can be either a `dict` or an iterable of (`key`, `value`) pairs: +`dict_one |= other` behaves similar to `.update()`, but in this case, `other` can be either a `dict` or an iterable of (`key`, `value`) pairs: ```python >>> palette_III = {'Grassy Green': (155, 196, 0), @@ -271,7 +271,7 @@ Unless a _sort key_ is specified, the default sort is over dictionary `keys`. 'Misty Mountain Pink': '#f9c5bd'} ``` -## Transposing a Dictionaries Keys and Values +## Transposing Dictionary Keys and Values Swapping keys and values reliably in a dictionary takes a little work, but can be accomplished via a `loop` using `dict.items()` or in a dictionary comprehension. Safe swapping assumes that `dict` keys and values are both _hashable_. @@ -333,10 +333,10 @@ If the values stored in the `dict` are not unique, extra checks become necessary # Iterating over (key, value) pairs using .items() >>> for key, value in extended_color_reference.items(): -... if value in consolidated_colors: #Check if key has already been created. +... if value in consolidated_colors: # <--Check if key has already been created. ... consolidated_colors[value].append(key) ... else: -... consolidated_colors[value] = [key] #Create a value list with the former key in it. +... consolidated_colors[value] = [key] # <--Create a value list with the former key in it. >>> consolidated_colors {'Purple Mountains Majesty': ['#8076a3', (128, 118, 163), (21, 28, 0, 36)], diff --git a/concepts/dict-methods/introduction.md b/concepts/dict-methods/introduction.md index c15fbc113de..b1e8eb8f20a 100644 --- a/concepts/dict-methods/introduction.md +++ b/concepts/dict-methods/introduction.md @@ -4,13 +4,13 @@ The `dict` class in Python provides many useful [methods][dict-methods], some of This concept tackles a few more: -- `dict.setdefault()` automatically adds keys without throwing a `KeyError`. -- `dict.fromkeys(iterable, )` creates a new `dict` from any number of iterables. -- `.keys()`, `.values()`, and `.items()` provide convenient iterators. -- `sorted(.items())`. can easily re-order entries in a `dict`. -- `dict_one.update()` updates one `dict` with overlapping values from another `dict`. -- `dict | other_dict` and `dict |= other_dict` merges or updates two `dict`s via operators. -- `reversed(dict.keys())`, `reversed(dict.values())`, or `reversed(dict.items())` produce _reversed_ views. +- `.setdefault()` automatically adds keys without throwing a KeyError. +- `.fromkeys(, )` creates a new `dict` from any number of iterables. +- `.keys()`, `.values()`, and `.items()` provide convenient iterators. +- `sorted(.items())` can easily re-order entries in a `dict`. +- `.update()` updates one `dict` with overlapping values from another `dict`. +- ` | ` and ` |= ` merges or updates two `dict`s via operators. +- `reversed(.keys())`, `reversed(.values())`, or `reversed(.items())` produce _reversed_ views. - `.popitem()` removes and returns a `key`, `value` pair. [dict-methods]: https://docs.python.org/3/library/stdtypes.html#dict diff --git a/concepts/dicts/about.md b/concepts/dicts/about.md index c34160b2ef6..a525f6248c7 100644 --- a/concepts/dicts/about.md +++ b/concepts/dicts/about.md @@ -3,9 +3,9 @@ A dictionary (`dict`) in Python is a data structure that associates [hashable][term-hashable] _keys_ to _values_ and is known in other programming languages as a resizable [hash table][hashtable-wikipedia], hashmap, or [associative array][associative-array]. Dictionaries are Python's only built-in [mapping type][mapping-types-dict]. -`Keys` must be hashable and unique across the dictionary. -Key types can include `numbers`, `str`, or `tuples` (of _immutable_ values). -They cannot contain _mutable_ data structures such as `lists`, `dict`s, or `set`s. +`keys` must be hashable and unique across the dictionary. +Key types can include `number`s, `str`s, or `tuple`s (of _immutable_ values). +They cannot contain _mutable_ data structures such as `list`s, `dict`s, or `set`s. As of Python 3.7, `dict` key order is guaranteed to be the order in which entries are inserted. `values` can be of any data type or structure. @@ -20,10 +20,10 @@ Dictionaries are especially useful in scenarios where the collection of items is ## Dictionary Construction Dictionaries can be created in many different ways, including: - - Using the [`fromkeys()`][fromkeys] classmethod - - Creating [dictionary comprehensions][dict-comprehensions] - - Merging two dictionaries via unpacking (`**`) - - Merging dictionaries via the `|` (_update_) operator + - Using the [`fromkeys()`][fromkeys] class method. + - Using [dictionary comprehensions][dict-comprehensions]. + - Merging two dictionaries via unpacking (`**`). + - Merging dictionaries via the `|` (_update_) operator. - Using a loop to iteratively add entries to a previously created empty `dict`. The two most straightforward methods are the dictionary _constructor_ and the dictionary _literal_. @@ -35,17 +35,20 @@ The two most straightforward methods are the dictionary _constructor_ and the di ```python # Passing a list of key,value tuples. ->>> wombat = dict([('name', 'Wombat'),('speed', 23), - ('land_animal', True)]) +>>> wombat = dict([('name', 'Wombat'), +... ('speed', 23), +... ('land_animal', True)]) {'name': 'Wombat', 'speed': 23, 'land_animal': True} # Using key=value arguments. ->>> bear = dict(name="Black Bear", speed=40, land_animal=True) +>>> bear = dict(name="Black Bear", +... speed=40, +... land_animal=True) {'name': 'Black Bear', 'speed': 40, 'land_animal': True} ``` -The [documentation on `dicts`][dicts-docs] outlines additional variations and options in constructor use. +The [documentation on `dict`s][dicts-docs] outlines additional variations and options in constructor use. ### Dictionary Literals @@ -74,41 +77,41 @@ Dictionaries can be arbitrarily nested: ```python animals = { - "Real" : { - "Winged" : { - "Sparrow" : {'name': 'sparrow','speed': 12, 'land_animal': True}, - "Kestrel" : {'name': 'kestrel', 'speed': 15, 'land_animal': True} - }, - "Legged" : { - "Wombat" : {'name': 'Wombat', 'speed': 23, 'land_animal': True}, - "Black Bear": {'name': 'Black Bear', 'speed': 40, 'land_animal': True}, - "Polecat" : {'name': 'Polecat', 'speed': 15, 'land_animal': True} - }, - "Other" : { - "Whale" : {'name': 'Blue Whale', 'speed': 35, 'land_animal': False}, - "Orca" : {'name': 'Orca', 'speed': 45, 'land_animal': False}, - "Snake" : {'name': 'Python', 'speed': 25, 'land_animal': True} - } - }, + "Real" : { + "Winged" : { + "Sparrow" : {'name': 'sparrow','speed': 12, 'land_animal': True}, + "Kestrel" : {'name': 'kestrel', 'speed': 15, 'land_animal': True} + }, + "Legged" : { + "Wombat" : {'name': 'Wombat', 'speed': 23, 'land_animal': True}, + "Black Bear": {'name': 'Black Bear', 'speed': 40, 'land_animal': True}, + "Polecat" : {'name': 'Polecat', 'speed': 15, 'land_animal': True} + }, + "Other" : { + "Whale" : {'name': 'Blue Whale', 'speed': 35, 'land_animal': False}, + "Orca" : {'name': 'Orca', 'speed': 45, 'land_animal': False}, + "Snake" : {'name': 'Python', 'speed': 25, 'land_animal': True} + } + }, - "Imaginary": { - "Winged" : { - "Dragon" : {'name': 'Fire Dragon','speed': 100, 'land_animal': True}, - "Phoenix" : {'name': 'Phoenix', 'speed': 1500, 'land_animal': True} - }, - "Legged" : { - "Sphinx" : {'name': 'Sphinx','speed': 10, 'land_animal': True}, - "Minotaur" : {'name': 'Minotaur', 'speed': 5, 'land_animal': True} - }, - "Other" : {} - } - } + "Imaginary": { + "Winged" : { + "Dragon" : {'name': 'Fire Dragon','speed': 100, 'land_animal': True}, + "Phoenix" : {'name': 'Phoenix', 'speed': 1500, 'land_animal': True} + }, + "Legged" : { + "Sphinx" : {'name': 'Sphinx','speed': 10, 'land_animal': True}, + "Minotaur" : {'name': 'Minotaur', 'speed': 5, 'land_animal': True} + }, + "Other" : {} + } + } ``` ## Accessing Values in a `dict` You can access a `value` in a dictionary using a _key_ in square brackets. -If a key does not exist, a `KeyError` is thrown: +If a key does not exist in the dictionary, a `KeyError` is thrown: ```python >>> bear["speed"] @@ -172,7 +175,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'} @@ -183,9 +186,9 @@ New `key`:`value` pairs can be _added_ in the same fashion: ## Removing (Pop-ing and del) Dictionary Entries -You can use the `.pop()` method to delete a dictionary entry. -`.pop()` removes the (`key`, `value`) pair and returns the `value` for use. -Like `.get()`, `.pop()` accepts second argument (_`dict.pop(, )`_) that will be returned if the `key` is not found. +You can use the `.pop()` method to delete a dictionary entry. +`.pop()` removes the (`key`, `value`) pair and returns the `value` for use. +Like `.get()`, `.pop()` accepts second argument (_`.pop(, )`_) that will be returned if the `key` is not found. This prevents a `KeyError` being raised: ```python @@ -208,8 +211,8 @@ KeyError: 'name' 'Unknown' ``` -You can also use the `del` statement to remove a single or multiple entries. -A `KeError` is raised if the entry to be removed is not found in the dictionary: +You can also use the `del` statement to remove one or more entries. +A `KeyError` is raised if the entry to be removed is not found in the dictionary: ```python >>> wombat = {'name': 'Wombat', @@ -245,7 +248,7 @@ You can access _values_ within the same loop by using _square brackets_: ```python >>> for key in bear: ->>> print((key, bear[key])) #this prints a tuple of (key, value) +... print((key, bear[key])) # <--This prints a tuple of (key, value). ('name', 'Black Bear') ('speed', 40) ('land_animal', True) @@ -257,7 +260,7 @@ You can also use the `.items()` method, which returns (`key`, `value`) tuples: # dict.items() forms (key, value tuples) that can be # unpacked and iterated over. >>> for key, value in whale.items(): ->>> print(key, ":", value) +... print(key, ":", value) name : Blue Whale speed : 25 land_animal : False @@ -271,11 +274,11 @@ For a detailed explanation of dictionaries in Python, the [official documentatio ## Extending Dictionary Functionality: The Collections Module -The [`collections`][collections-docs] module adds specialized functionality to Python's standard collection-based datatypes (`dictionary`, `set`, `list`, `tuple`). +The [`collections`][collections-docs] module adds specialized functionality to Python's standard collection-based datatypes (`dict`, `set`, `list`, `tuple`). Three of the most useful dictionary-based classes are: - [`Counter`][counter-dicts] automatically counts items and returns them in a `dict` with the items as keys and their counts as values. -- [`OrderedDict`][ordered-dicts-docs], has methods specialized for arranging the order of dictionary entries. +- [`OrderedDict`][ordered-dicts-docs] has methods specialized for arranging the order of dictionary entries. - [`defaultdict`][default-dicts] uses a factory method to set a default value if a `key` is not found when trying to retrieve or assign to a dictionary entry. [associative-array]: https://en.wikipedia.org/wiki/Associative_array#:~:text=In%20computer%20science%2C%20an%20associative,a%20function%20with%20finite%20domain. diff --git a/concepts/dicts/introduction.md b/concepts/dicts/introduction.md index 5c8a772480b..676bf11066d 100644 --- a/concepts/dicts/introduction.md +++ b/concepts/dicts/introduction.md @@ -4,9 +4,9 @@ A dictionary (`dict`) in Python is a data structure that associates [hashable][t Dictionaries are Python's only built-in [mapping type][mapping-types-dict]. -`Keys` must be hashable and unique across the dictionary. -Key types can include `numbers`, `str`, or `tuples` (of _immutable_ values). -They cannot contain _mutable_ data structures such as `lists`, `dict`s, or `set`s. +`keys` must be hashable and unique across the dictionary. +Key types can include `number`s, `str`s, or `tuple`s (of _immutable_ values). +They cannot contain _mutable_ data structures such as `list`s, `dict`s, or `set`s. As of Python 3.7, `dict` key order is guaranteed to be the order in which entries are inserted. `values` can be of any data type or structure. diff --git a/concepts/enums/about.md b/concepts/enums/about.md index 27b264c22e1..dacd7bfd755 100644 --- a/concepts/enums/about.md +++ b/concepts/enums/about.md @@ -1,14 +1,19 @@ # About -In Python, [an enum][enum-docs] is a set of unique names that are bound unique, **constant** values. Enums are defined by inheriting an `Enum` class. Built-in enum types are available in the module `enum` and the class `Enum` can be imported using `from enum import Enum`. +In Python, [an enum][enum-docs] is a set of unique names that are bound unique, **constant** values. +`Enums` are defined by inheriting an `Enum` class. +Built-in enum types are available in the module `enum` and the class `Enum` can be imported using `from enum import Enum`. + ```python +from enum import Enum + class Color(Enum): RED = 1 GREEN = 2 ``` -Note that the values of the enum members can be any data types such as str, tuple, float, etc. +Note that the values of the `enum` members can be any data types such as `str`, `tuple`, `float`, etc. ```python class Color(Enum): @@ -16,9 +21,11 @@ class Color(Enum): GREEN = 'green' ``` -Enums can also be created via the following [functional API][enum-functional-api]. +`Enums` can also be created using [function-call syntax][enum-functional-example]. ```python +from enum import Enum + Animal = Enum('Animal', 'ANT BEE CAT DOG') list(Animal) #=> [, , , ] @@ -27,7 +34,7 @@ Animal.ANT.value #=> 1 ``` -When assigning the same value to two members in an enum, the latter assigned member will be an alias to the formed one. It is not allowed to use the same name for two members of an enum. +When assigning the same value to two members in an `enum`, the latter assigned member will be an alias to the former one. It is not allowed to use the same name for two different members of an `enum`. ```python class Color(Enum): @@ -50,12 +57,13 @@ for member in Color: # __members__.items() helps you to loop through alias as well for member in Color.__members__.items(): print(member) -#=>('RED', ) -#=>('GREEN', ) -#=>('ALIAS_OF_RED', ) +#=> ('RED', ) +#=> ('GREEN', ) +#=> ('ALIAS_OF_RED', ) ``` -Enum members can be compared using [`is` (_identity operator_)][identity-keyword] or `is not`. The `==` or `!=` (_equality operators_) work likewise. +`Enum` members can be compared using [`is` (_identity operator_)][identity-keyword] or `is not`. The `==` or `!=` (_equality operators_) work likewise: + ```python a = Color.RED @@ -76,10 +84,10 @@ class Shape(Enum): OVAL = auto() ``` -To disallow aliasing (_preventing duplicate values with different names_), the `@unique` decorator may be used. +To disallow aliasing (_preventing duplicate values with different names_), the [class decorator][class-decorator] [`@enum.unique`][enum-unique-decorator] decorator may be used. ```python -@unique +@enum.unique class Shape(Enum): CIRCLE = 1 SQUARE = 2 @@ -87,7 +95,7 @@ class Shape(Enum): #=> ValueError: duplicate values found in : TRIANGLE -> CIRCLE ``` -To access an enum member for a given value, this notation can be used: `EnumName(value)`. +To access an `enum` member for a given value, this notation can be used: `()`. ```python g = Color(2) @@ -99,15 +107,23 @@ g #=> ``` -A custom [restricted `Enum`][restricted-enums] can be written by subclassing `Enum` with any mix-in or data-type. For example: +A custom [restricted `Enum`][restricted-enums] can be written by subclassing the `Enum` class with any mix-in or data-type. +For example: + ```python class StrEnum(str, Enum): pass ``` -[enum-docs]: https://docs.python.org/3/library/enum.html -[enum-auto-docs]: https://docs.python.org/3/library/enum.html#using-auto -[enum-functional-api]: https://docs.python.org/3/library/enum.html#functional-api -[restricted-enums]: https://docs.python.org/3/library/enum.html#restricted-enum-subclassing +Subclassing `Enum` is only allowed if the `enum` does **not** define any members. +See the [`enum` how-to][enum-docs] and the [`enum` cookbook][cookbook] for more details and explanations. + +[class-decorator]: https://docs.python.org/3/reference/compound_stmts.html#class-definitions +[cookbook]: https://docs.python.org/3/howto/enum.html#enum-cookbook +[enum-auto-docs]: https://docs.python.org/3/library/enum.html#enum.auto +[enum-docs]: https://docs.python.org/3/howto/enum.html#enum-basic-tutorial +[enum-functional-example]: https://docs.python.org/3/library/enum.html [identity-keyword]: https://www.w3schools.com/python/ref_keyword_is.asp +[restricted-enums]: https://docs.python.org/3/howto/enum.html#restricted-enum-subclassing +[enum-unique-decorator]: https://docs.python.org/3/library/enum.html#enum.unique diff --git a/concepts/enums/introduction.md b/concepts/enums/introduction.md index ea9c9000e07..bf4e8b9d043 100644 --- a/concepts/enums/introduction.md +++ b/concepts/enums/introduction.md @@ -1,14 +1,19 @@ # Introduction -In Python, [an enum](https://docs.python.org/3/library/enum.html) is a set of names that are bound to unique `literal`, or `constant` values. Enums are defined by inheriting an `Enum` class. Built-in enum types are available in the module `enum` and the class `Enum` can be imported using `from enum import Enum`. +In Python, [an `enum`][enum-docs] is a set of names that are bound to unique `literal`, or `constant` values. +`Enums` are defined by inheriting from or subclassing an `Enum` class. +Built-in `enum` types are available in the module `enum` and the class `Enum` can be imported using `from enum import Enum`. + ```python +from enum import Enum + class Color(Enum): RED = 1 GREEN = 2 ``` -Note that the values of the enum members can be any data types such as str, tuple, float, etc. +Note that the values of the `enum` members can be any data types such as `str`, `tuple`, `float`, etc. ```python class Color(Enum): @@ -16,7 +21,7 @@ class Color(Enum): GREEN = 'green' ``` -When assigning the same value to two members in an enum, the latter assigned member will be an alias to the formed one. It is not allowed to use the same name for two members of an enum. +When assigning the same value to two members in an `enum`, the latter assigned member will be an alias to the former one. It is not allowed to use the same name for two different members of an `enum`. ```python class Color(Enum): @@ -31,7 +36,7 @@ Color.ALIAS_OF_RED.value #=> 1 ``` -Iterating through the members of the enum can be done with the standard `for member in` syntax: +Iterating through the members of the `enum` can be done with the standard `for member in` syntax: ```python for member in Color: @@ -40,7 +45,7 @@ for member in Color: #=> (GREEN, 2) ``` -Enum members can be compared using [`is` (_identity operator_)](https://www.w3schools.com/python/ref_keyword_is.asp) or `is not`. The `==` or `!=` (_equality_operators_) work likewise. +`Enum` members can be compared using [`is` (_identity operator_)][identity-keyword] or `is not`. The `==` or `!=` (_equality operators_) work likewise: ```python a = Color.RED @@ -52,7 +57,7 @@ a == Color.RED #=> True ``` -To access an enum member for a given value, `EnumName(value)` can be used: +To access an `enum` member for a given value, `()` can be used: ```python g = Color(2) @@ -63,3 +68,6 @@ g is Color.GREEN g #=> ``` + +[enum-docs]: https://docs.python.org/3/library/enum.html +[identity-keyword]: https://www.w3schools.com/python/ref_keyword_is.asp diff --git a/concepts/fractions/about.md b/concepts/fractions/about.md index d41124c39c4..e582c53141a 100644 --- a/concepts/fractions/about.md +++ b/concepts/fractions/about.md @@ -1,6 +1,6 @@ # About -The [`Fractions`][fractions] module allows us to create and work with [`rational numbers`][rational]: fractions with an integer numerator divided by an integer denominator. +The [`fractions`][fractions] module allows us to create and work with [`rational numbers`][rational]: fractions with an integer numerator divided by an integer denominator. For example, we can store `2/3` as an exact fraction instead of the approximate `float` value `0.6666...` @@ -8,10 +8,10 @@ For example, we can store `2/3` as an exact fraction instead of the approximate Unlike `int`, `float`, and `complex` numbers, fractions do not have a literal form. -However, the fractions constructor is quite flexible. +However, the fraction constructor is quite flexible. -Most obviously, it can take take two integers. -Common factors are automatically removed, converting the fraction to its "lowest form": the smallest integers that accurately represent the fraction. +Most obviously, it can take two integers. +Common factors are automatically removed, converting the fraction to its "lowest form" (_the smallest integers that accurately represent the fraction_): ```python @@ -29,7 +29,7 @@ Fraction(2, 3) # automatically simplified True ``` -The fractions constructor can also parse a string representation: +The fraction constructor can also parse a string representation: ```python diff --git a/concepts/fractions/introduction.md b/concepts/fractions/introduction.md index 437ccbbeb07..156aa3ff280 100644 --- a/concepts/fractions/introduction.md +++ b/concepts/fractions/introduction.md @@ -1,13 +1,13 @@ # Introduction -The [`Fractions`][fractions] module allows us to create and work with [`rational numbers`][rational]: fractions with an integer numerator divided by an integer denominator. +The [`fractions`][fractions] module allows us to create and work with [`rational numbers`][rational]: fractions with an integer numerator divided by an integer denominator. For example, we can store `2/3` as an exact fraction instead of the approximate `float` value `0.6666...`. Unlike `int`, `float`, and `complex` numbers, fractions do not have a literal form. -However, the fractions constructor is quite flexible. +However, the fraction constructor is quite flexible. -Most obviously, it can take take two integers as arguments. -Common factors are automatically removed, converting the fraction to its "lowest form": the smallest integers that accurately represent the fraction: +Most obviously, it can take two integers as arguments. +Common factors are automatically removed, converting the fraction to its "lowest form" (_the smallest integers that accurately represent the fraction_): ```python >>> from fractions import Fraction @@ -24,7 +24,7 @@ Fraction(2, 3) # automatically simplified True ``` -The fractions constructor can also parse a string representation: +The fraction constructor can also parse a string representation: ```python >>> f3 = Fraction('2/3') diff --git a/concepts/functools/about.md b/concepts/functools/about.md index e5afb577d39..cbc5cd89d96 100644 --- a/concepts/functools/about.md +++ b/concepts/functools/about.md @@ -1,312 +1 @@ -# About - -The functools module is for higher-order functions: functions that act on or return other ***[functions](https://docs.python.org/3/tutorial/controlflow.html#defining-functions)***. It provides functions for working with other functions and callable objects to use or extend them without completely rewriting them. - -## Memoizing the function calls - -**Memoizing:** Storing the result of some expensive function, which is called with the same input again and again. So, we don't have to run the function repeatedly. - -### ```@functools.lru_cache(maxsize=128, typed=False)``` - -***[@functools.lru_cache(maxsize=128, typed=False)](https://docs.python.org/3/library/functools.html#functools.lru_cache)*** Decorator to wrap a function with a memoizing callable that saves up to the maxsize most recent calls. It can save time when an expensive or I/O bound function is periodically called with the same arguments. - -Since a dictionary is used to cache results, the positional and keyword arguments to the function must be hashable. - -Here ```maxsize = 128``` means that it is going to memoize latest 128 function calls at max. - -The lru_cache works the same way but it can cache at max maxsize calls and if type = True, then the function arguments of different types will be cached separately i.e. 5 and 5.0 will be cached differently. - -### ```@functools.cache(user_function)``` - -***[@functools.cache(user_function)](https://docs.python.org/3/library/functools.html#functools.cache)*** the same as lru_cache(maxsize=None), creating a thin wrapper around a dictionary lookup for the function arguments. Because it never needs to evict old values, this is smaller and faster than ```lru_cache()``` with a size limit. - -```python - ->>> @cache ->>> def factorial(n): ->>> return n * factorial(n-1) if n else 1 - ->>> factorial(10) # no previously cached result, makes 11 recursive calls -3628800 ->>> factorial(5) # just looks up cached value result -120 ->>> factorial(12) # makes two new recursive calls, the other 10 are cached -479001600 - -# The lru_cache works the same way but it can cache at max maxsize calls and if type = True, then the function arguments of different types will be cached separately. - -# Some types such as str and int may be cached separately even when typed is false. - ->>> @lru_cache(maxsize = 128) ->>> def factorial(n): ->>> return n * factorial(n-1) if n else 1 - ->>> factorial(10) -3628800 - -# by the Following we can fetch the information about the cache. ->>> factorial.cache_info() -CacheInfo(hits=0, misses=11, maxsize=128, currsize=11) -``` - -## Generic functions - -***[Generic functions](https://pymotw.com/3/functools/#generic-functions)*** are those which perform the operation based on the argument given to them. In statically typed languages it can be done by function overloading. - -In python functools provides the `singledispatch()` decorator to register a set of generic functions for automatic switching based on the type of the first argument to a function. - -The ```register()``` attribute of the function serves as another decorator for registering alternative implementations.To add overloaded implementations to the function, use the ```register(type)``` attribute of the generic function. - -When user is going to call the function with the integer argument, then it will be redirected to the function decorated with ```register(int)``` decorator. - -The first function wrapped with singledispatch() is the default implementation if no other type-specific function is found, default implementation will be called. - -```python - ->>> from functools import singledispatch - ->>> @singledispatch - def fun(arg): - print("default argument string: ", arg) - - ->>> fun.register(int) - def _(arg): - print("This is an integer: ", arg) - ->>> fun.register(list) - def _(arg): - print("This is a list: ", arg) - ->>> fun("Hello") -"default argument string: Hello" - ->>> fun(10) -"This is an integer: 10" - ->>> fun([1,2,3]) -"This is a list: [1,2,3]" - -# This will call the default function as we didn't registered any function with float. ->>> fun(2.45) -"default argument string: 2.45" - -``` - -For class methods we can use ***[singledispatchmethod(func)](https://docs.python.org/3/library/functools.html#functools.singledispatchmethod)*** to register a set of generic methods for automatic switching based on the type of the first non-self or non-class argument to a function. - -```python - ->>> class Negator: - @singledispatchmethod - def neg(self, arg): - raise NotImplementedError("Cannot negate a") - - @neg.register(int) - def _(self, arg): - return -arg - - @neg.register(bool) - def _(self, arg): - return not arg - ->>> obj = Negator() - -# Going to call function which is register with bool datatype. ->>> obj.neg(True) -False - -# Going to call function which is register with int datatype. ->>> obj.neg(10) --10 - -# Going to call default function and will display an error message. ->>> obj.neg("String") - -``` - -## Partial - -`functools.partial(func, /, *args, **keywords)` return a new ***[partial object](https://docs.python.org/3/library/functools.html#partial-objects)*** which when called will behave like func called with the positional arguments args and keyword arguments keywords. If more arguments are supplied to the call, they are appended to args.The ***[partial](https://docs.python.org/3/library/functools.html#functools.partial)*** is used for partial function application which โ€œfreezesโ€ some portion of a functionโ€™s arguments and/or keywords resulting in a new object with a simplified signature. - -```python - ->>> def add(a, b): - print(f"got a={a}, b={b}") - print(a+b) - ->>> a = partial(add, 10) ->>> a(4) -"got a=10, b=4" -14 - -# 10 got assigned to a because partial start assigning arguments from the left. - ->>> a = partial(add, b=10) ->>> a(4) -"got a=4, b=10" -14 - -# But By using the keywords we can assign the value to the arguments at right - -``` - -### partial Objects - -partial objects are callable objects created by partial(). They have three read-only attributes: - -```partial.func``` - -A callable object or function. Calls to the partial object will be forwarded to func with new arguments and keywords. - -```partial.args``` - -The leftmost positional arguments that will be prepended to the positional arguments provided to a partial object call. - -```partial.keywords``` - -The keyword arguments that will be supplied when the partial object is called. - -```python - ->>> from functools import partial - ->>> pow_2 = partial(pow, exp = 2) - ->>> pow_2.func == pow -True - ->>> pow_2.args -() - ->>> pow_2.keywords -{'exp': 2} - ->>> two_pow = partial(pow, 2) - ->>> two_pow(3) # 2(frezzed) ^ 3 = 8 == pow(2 [fixed] ,3 [passed by user]) -8 - ->>> pow_2.args -(2,) - -``` - -The ```pow_2.func``` is same as the ```pow``` function. - -Here ```pow_2.args``` returns an empty tuple because we do not pass any positional argument to our partial object call. - -```pow_2.keywords``` returns a dictionary of keywords argument which will be supplied when the partial object is called. - -Here ```two_pow.args``` returns a ```(2,)``` tuple because we passed 2 as an argument while creating the partial object, which fixed the value of ```base``` argument as ```2```. - -### ```partialmethod``` - -***[functools.partialmethod(func, /, *args, **keywords)](https://docs.python.org/3/library/functools.html#functools.partialmethod)*** Return a new partialmethod descriptor which behaves like partial except that it is designed to be used as a method definition rather than being directly callable. - -```python - ->>> class Cell: - def __init__(self): - self.alive = False - - def set_state(self, state): - self.alive = bool(state) - - # going to return a method set_state with argument state = True - set_alive = partialmethod(set_state, True) - # going to return a method set_state with argument state = False - set_dead = partialmethod(set_state, False) - ->>> c = Cell() ->>> c.alive -False ->>> c.set_alive() ->>> c.alive -True - -``` - -## Wraps - -### `functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)` - -***[functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)](https://docs.python.org/3/library/functools.html#functools.update_wrapper)*** Update a wrapper function to look like the wrapped function. The optional arguments are tuples to specify which attributes of the original function are assigned directly to the matching attributes on the wrapper function and which attributes of the wrapper function are updated with the corresponding attributes from the original function. - -WRAPPER_ASSIGNMENTS (which assigns to the wrapper functionโ€™s `__module__`, `__name__`, `__qualname__`, `__annotations__` and `__doc__`, the documentation string) - -WRAPPER_UPDATES (which updates the wrapper functionโ€™s `__dict__`, i.e. the instance dictionary). - -```python - -# without update_wrapper() - ->>> def decorator(func): - def wrapper(name): - """Going to say Hello""" - print("hello",name) - func(name) - return wrapper - - ->>> @decorator - def fun(name): - """Going to Wish""" - print("good morning",name) - -# In bigger python code base this will cause problem while debugging the code. ->>> fun.__name__ -'wrapper' ->>> fun.__doc__ -'Going to say Hello' - -# with update_wrapper() - ->>> def decorator(func): - def wrapper(name): - """Going to say Hello""" - print("hello",name) - func(name) - update_wrapper(wrapper, func) - return wrapper - - ->>> @decorator - def fun(name): - """Going to Wish""" - print("good morning",name) - -# Now the wrapper function just look like the wrapped(fun) function ->>> fun.__name__ -'fun' ->>> fun.__doc__ -'Going to Wish' -``` - -### `functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)` - -***[functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)](https://docs.python.org/3/library/functools.html#functools.wraps)*** is a convenience function for invoking update_wrapper() as a function decorator when defining a wrapper function. It is equivalent to partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated). - -```python - -# This going to work same as the above where we are using the update_wrapper() function ->>> def decorator(func): - @wraps(fun) - def wrapper(name): - """Going to say Hello""" - print("hello",name) - func(name) - return wrapper - - ->>> @decorator - def fun(name): - """Going to Wish""" - print("good morning",name) - -# Now the wrapper function just look like the wrapped(fun) function ->>> fun.__name__ -'fun' ->>> fun.__doc__ -'Going to Wish' -``` +#TODO: Add about for this concept. diff --git a/concepts/functools/introduction.md b/concepts/functools/introduction.md index c91aedc81bd..bbe12ffd5e9 100644 --- a/concepts/functools/introduction.md +++ b/concepts/functools/introduction.md @@ -1,43 +1 @@ -# Introduction - -The functools module is for higher-order functions: functions that act on or return other ***[functions](https://docs.python.org/3/tutorial/controlflow.html#defining-functions)***. It provides functions for working with other functions and callable objects to use or extend them without completely rewriting them. - -## Memoizing the function calls - -**Memoizing:** Storing the result of some expensive function, which is called with the same input again and again. So, we don't have to run the function repeatedly. - -### ```@functools.lru_cache(maxsize=128, typed=False)``` - -***[@functools.lru_cache(maxsize=128, typed=False)](https://docs.python.org/3/library/functools.html#functools.lru_cache)*** Decorator to wrap a function with a memoizing callable that saves up to the maxsize most recent calls. It can save time when an expensive or I/O bound function is periodically called with the same arguments. - -Since a dictionary is used to cache results, the positional and keyword arguments to the function must be hashable. - -Here ```maxsize = 128``` means that it is going to memoize latest 128 function calls at max. - -### ```@functools.cache(user_function)``` - -***[@functools.cache(user_function)](https://docs.python.org/3/library/functools.html#functools.cache)*** the same as lru_cache(maxsize=None), creating a thin wrapper around a dictionary lookup for the function arguments. Because it never needs to evict old values, this is smaller and faster than ```lru_cache()``` with a size limit. - -## Generic functions - -***[Generic functions](https://pymotw.com/3/functools/#generic-functions)*** are those which preform the operation based on the argument given to them. - -In statically typed languages it can be done by function overloading, In python functools provides the ```singledispatch(func)``` decorator to register a set of generic functions for automatic switching based on the type of the first argument to a function. - -For class methods we can use ***[singledispatchmethod(func)](https://docs.python.org/3/library/functools.html#functools.singledispatchmethod)*** to register a set of generic methods for automatic switching based on the type of the first non-self or non-class argument to a function. - -## Partial - -`functools.partial(func, /, *args, **keywords)` return a new ***[partial object](https://docs.python.org/3/library/functools.html#partial-objects)*** which when called will behave like func called with the positional arguments args and keyword arguments keywords. If more arguments are supplied to the call, they are appended to args.The ***[partial](https://docs.python.org/3/library/functools.html#functools.partial)*** is used for partial function application which โ€œfreezesโ€ some portion of a functionโ€™s arguments and/or keywords resulting in a new object with a simplified signature. - -***[functools.partialmethod(func, /, *args, **keywords)](https://docs.python.org/3/library/functools.html#functools.partialmethod)*** Return a new partialmethod descriptor which behaves like partial except that it is designed to be used as a method definition rather than being directly callable. - -## Wraps - -### `functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)` - -***[functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)](https://docs.python.org/3/library/functools.html#functools.update_wrapper)*** Update a wrapper function to look like the wrapped function. The optional arguments are tuples to specify which attributes of the original function are assigned directly to the matching attributes on the wrapper function and which attributes of the wrapper function are updated with the corresponding attributes from the original function. - -### `functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)` - -***[functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)](https://docs.python.org/3/library/functools.html#functools.wraps)*** is a convenience function for invoking update_wrapper() as a function decorator when defining a wrapper function. It is equivalent to partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated). +#TODO: Add introduction for this concept. diff --git a/concepts/generators/about.md b/concepts/generators/about.md index 59b5035d6b9..4b2e74cbad2 100644 --- a/concepts/generators/about.md +++ b/concepts/generators/about.md @@ -35,9 +35,33 @@ The rationale behind this is that you use a generator when you do not need all t This saves memory and processing power, since only the value you are _currently working on_ is calculated. + ## Using a generator -Generators may be used in place of most `iterables` in Python. This includes _functions_ or _objects_ that require an `iterable`/`iterator` as an argument. +Generators (_technically [`generator-iterator`s][generator-iterator] โ€” see the note below._) are a type of `iterator` and can be used anywhere in Python where an `iterator` or `iterable` is expected. +This includes _functions_ or _objects_ that require an `iterable`/`iterator` as an argument. +For a deeper dive, see [How to Make an Iterator in Python][how-to-iterator]. + + +~~~~exercism/note + +Generator-iterators are a special sub-set of [iterators][iterator]. +`Iterators` are the mechanism/protocol that enables looping over _iterables_. +Generator-iterators and the iterators returned by common Python [`iterables`][iterables] act very similarly, but there are some important differences to note: + +- They are _[lazily evaluated][lazy evaluation]_; iteration is _one-way_ and there is no "backing up" to a previous value. +- They are _consumed_ by iterating over the returned values; there is no resetting or saving in memory. +- They are not sortable and cannot be reversed. +- They are not sequence types, and _do not_ have `indexes`. + You cannot reference a previous or future value using addition or subtraction and you cannot use bracket (`[]`) notation or slicing. +- They cannot be used with the `len()` function, as they have no length. +- They can be _finite_ or _infinite_ - be careful when collecting all values from an _infinite_ `generator-iterator`! + +[iterator]: https://docs.python.org/3.11/glossary.html#term-iterator +[iterables]: https://wiki.python.org/moin/Iterator +[lazy evaluation]: https://en.wikipedia.org/wiki/Lazy_evaluation +~~~~ + To use the `squares_generator()` generator: @@ -140,7 +164,8 @@ Generators are also very helpful when a process or calculation is _complex_, _ex Now whenever `__next__()` is called on the `infinite_sequence` object, it will return the _previous number_ + 1. -[generator-iterator]: https://docs.python.org/3.11/glossary.html#term-generator-iterator +[generator-iterator]: https://docs.python.org/3/glossary.html#term-generator-iterator +[how-to-iterator]: https://treyhunner.com/2018/06/how-to-make-an-iterator-in-python/#Generators:_the_easy_way_to_make_an_iterator [iterables]: https://wiki.python.org/moin/Iterator [iterator]: https://docs.python.org/3.11/glossary.html#term-iterator [lazy iterator]: https://en.wikipedia.org/wiki/Lazy_evaluation diff --git a/concepts/list-methods/about.md b/concepts/list-methods/about.md index 3a5132fd0ad..00b41f325e5 100644 --- a/concepts/list-methods/about.md +++ b/concepts/list-methods/about.md @@ -11,7 +11,7 @@ Lists support both [common][common sequence operations] and [mutable][mutable se Python provides many useful [methods][list-methods] for working with lists. Because lists are mutable, list-methods **alter the original list object** passed into the method. -If mutation is undesirable, a `shallow copy` (_at minimum__) of the original `list` needs to be made via `slice` or `.copy()`. +If mutation is undesirable, a `shallow copy` (_at minimum_) of the original `list` needs to be made via `slice` or `.copy()`. ## Adding Items @@ -47,7 +47,8 @@ If `` is greater than the final index on the list, the item will be added ``` An `iterable` can be _combined_ with an existing list (concatenating the two) via `.extend()`. -`.extend()` will _unpack_ the supplied iterable, adding its elements in the same order to the end of the target list (_using `.append()` in this circumstance would add the entire iterable as a **single item**._). +`.extend()` will _unpack_ the supplied iterable, adding its elements in the same order to the end of the target list. +Using `.append()` in this circumstance would add the entire iterable as a _**single item**_. ```python @@ -136,10 +137,22 @@ The order of list elements can be reversed _**in place**_ with `.reverse( [3, 2, 1] ``` -List elements can be sorted _**in place**_ using `.sort()`. - Internally, Python uses [`Timsort`][timsort] to arrange the elements. - The default order is _ascending_. - The Python docs have [additional tips and techniques for sorting][sorting how to] `lists` effectively. +A list can be re-ordered _**in place**_ with the help of [`.sort()`][sort]. +Default sort order is _ascending_ from the left. +The Python docs offer [additional tips and techniques for sorting][sorting how to]. + + +~~~~exercism/note + From 2002 to 2022, Python used an algorithm called [`Timsort`][timsort] internally to arrange lists, but switched to [`Powersort`][powersort] from `Python 3.11` onward. +You can read more details and discussion on the change from the core Python team in the GitHub [issue 78742][78742]. + +For technical details on the algorithm, see the J. Ian Munro and Sebastian Wild paper [Nearly-Optimal Mergesorts: Fast, Practical Sorting Methods That Optimally Adapt to Existing Runs][nearly-optimal-mergesorts] + +[78742]: https://github.com/python/cpython/issues/78742 +[nearly-optimal-mergesorts]: https://arxiv.org/abs/1805.04154 +[powersort]: https://www.wild-inter.net/publications/munro-wild-2018 +[timsort]: https://en.wikipedia.org/wiki/Timsort +~~~~ ```python @@ -254,7 +267,7 @@ For a detailed explanation of names, values, list, and nested list behavior, tak [set]: https://docs.python.org/3/library/stdtypes.html#set [shallow vs deep]: https://realpython.com/copying-python-objects/ [slice notation]: https://docs.python.org/3/reference/expressions.html#slicings +[sort]: https://docs.python.org/3/library/stdtypes.html#list.sort [sorted]: https://docs.python.org/3/library/functions.html#sorted [sorting how to]: https://docs.python.org/3/howto/sorting.html -[timsort]: https://en.wikipedia.org/wiki/Timsort [tuple]: https://docs.python.org/3/library/stdtypes.html#tuple diff --git a/concepts/lists/about.md b/concepts/lists/about.md index f7d4054eef0..51c44060d5c 100644 --- a/concepts/lists/about.md +++ b/concepts/lists/about.md @@ -49,15 +49,15 @@ For readability, line breaks can be used when there are many elements or nested ```python >>> lots_of_entries = [ - "Rose", - "Sunflower", - "Poppy", - "Pansy", - "Tulip", - "Fuchsia", - "Cyclamen", - "Lavender" - ] +... "Rose", +... "Sunflower", +... "Poppy", +... "Pansy", +... "Tulip", +... "Fuchsia", +... "Cyclamen", +... "Lavender" +... ] >>> lots_of_entries ['Rose', 'Sunflower', 'Poppy', 'Pansy', 'Tulip', 'Fuchsia', 'Cyclamen', 'Lavender'] @@ -65,10 +65,10 @@ For readability, line breaks can be used when there are many elements or nested # Each data structure is on its own line to help clarify what they are. >>> nested_data_structures = [ - {"fish": "gold", "monkey": "brown", "parrot": "grey"}, - ("fish", "mammal", "bird"), - ['water', 'jungle', 'sky'] - ] +... {"fish": "gold", "monkey": "brown", "parrot": "grey"}, +... ("fish", "mammal", "bird"), +... ['water', 'jungle', 'sky'] +... ] >>> nested_data_structures [{'fish': 'gold', 'monkey': 'brown', 'parrot': 'grey'}, ('fish', 'mammal', 'bird'), ['water', 'jungle', 'sky']] @@ -174,7 +174,7 @@ Indexes can be from **`left`** --> **`right`** (_starting at zero_) or **`right` 'Toast' ``` -A section of a list can be accessed via _slice notation_ (`[start:stop]`). +A section of a list can be accessed via _slice notation_ (`[:]`). A _slice_ is defined as an element sequence at position `index`, such that `start <= index < stop`. [_Slicing_][slice notation] returns a copy of the "sliced" items and does not modify the original `list`. @@ -207,7 +207,7 @@ Lists supply an [_iterator_][iterator], and can be looped through/over in the sa >>> colors = ["Orange", "Green", "Grey", "Blue"] >>> for item in colors: ... print(item) -... + Orange Green Grey @@ -218,7 +218,7 @@ Blue >>> colors = ["Orange", "Green", "Grey", "Blue"] >>> for index, item in enumerate(colors): ... print(item, ":", index) -... + Orange : 0 Green : 1 Grey : 2 @@ -229,7 +229,7 @@ Blue : 3 >>> numbers_to_cube = [5, 13, 12, 16] >>> for number in numbers_to_cube: ... print(number**3) -... + 125 2197 1728 @@ -335,7 +335,7 @@ This reference complication becomes exacerbated when working with nested or mult from pprint import pprint # This will produce a game grid that is 8x8, pre-populated with zeros. ->>> game_grid = [[0]*8] *8 +>>> game_grid = [[0]*8]*8 >>> pprint(game_grid) [[0, 0, 0, 0, 0, 0, 0, 0], diff --git a/concepts/loops/about.md b/concepts/loops/about.md index 0f39e733d0c..e3322af0e3b 100644 --- a/concepts/loops/about.md +++ b/concepts/loops/about.md @@ -31,7 +31,6 @@ The keywords `break`, `continue`, and `else` help customize loop behavior. The basic [`for`][for statement] `loop` in Python is better described as a _`for each`_ which cycles through the values of any [iterable object][iterable], terminating when there are no values returned from calling [`next()`][next built-in] (_raising a [`StopIteration`][stopiteration]_). ```python - >>> word_list = ["bird", "chicken", "barrel", "bongo"] >>> for word in word_list: @@ -95,7 +94,6 @@ Interestingly, `range()` [_is not an iterator_][range is not an iterator], and c If both values and indexes are needed, the built-in [`enumerate()`][enumerate] will return an [`iterator`][iterator] over (`index`, `value`) pairs: ```python - >>> word_list = ["bird", "chicken", "barrel", "apple"] # *index* and *word* are the loop variables. @@ -152,15 +150,15 @@ The `enumerate()` function can also be set to `start` the index count The [`continue`][continue statement] keyword can be used to skip forward to the next iteration cycle: ```python -word_list = ["bird", "chicken", "barrel", "bongo", "sliver", "apple", "bear"] - -# This will skip *bird*, at index 0 -for index, word in enumerate(word_list): - if index == 0: - continue - if word.startswith("b"): - print(f"{word.title()} (at index {index}) starts with a b.") - +>>> word_list = ["bird", "chicken", "barrel", "bongo", "sliver", "apple", "bear"] +... +... # This will skip *bird*, at index 0 +... for index, word in enumerate(word_list): +... if index == 0: +... continue +... if word.startswith("b"): +... print(f"{word.title()} (at index {index}) starts with a b.") +... 'Barrel (at index 2) starts with a b.' 'Bongo (at index 3) starts with a b.' 'Bear (at index 6) starts with a b.' @@ -176,9 +174,9 @@ The [`break`][break statement] (_like in many C-related languages_) keyword can ... if word.startswith("b"): ... print(f"{word.title()} (at index {index}) starts with a B.") ... elif word == "sliver": -... break +... break ... else: -... print(f"{word.title()} doesn't start with a B.") +... print(f"{word.title()} doesn't start with a B.") ... print("loop broken.") ... 'Bird (at index 0) starts with a B.' @@ -202,11 +200,11 @@ The loop [`else` clause][loop else] is unique to Python and can be used for "wra ... word = word.title() ... if word.startswith("B"): ... print(f"{word} (at index {index}) starts with a B.") - -...# This executes once *StopIteration* is raised and -...# there are no more items to iterate through. -...# Note the indentation, which lines up with the for keyword. -...else: +... +... # This executes once *StopIteration* is raised and +... # There are no more items to iterate through. +... # Note the indentation, which lines up with the for keyword. +... else: ... print(f"Found the above b-words, out of {len(word_list)} words in the word list.") ... 'Bird (at index 0) starts with a B.' @@ -227,7 +225,7 @@ The loop [`else` clause][loop else] is unique to Python and can be used for "wra ... # This statement does not run, because a *break* was triggered. ... else: -... print(f"Found the above b-words, out of {len(word_list)} words in the word list.") +... print(f"Found the above b-words, out of {len(word_list)} words in the word list.") ... 'Bird (at index 0) starts with a B.' 'Barrel (at index 2) starts with a B.' diff --git a/concepts/numbers/about.md b/concepts/numbers/about.md index 1155bcf7a5c..3fa63c140d9 100644 --- a/concepts/numbers/about.md +++ b/concepts/numbers/about.md @@ -135,7 +135,7 @@ Numbers can be converted from `int` to `floats` and `floats` to `int` using the ## Round -Python provides a built-in function [`round(number, )`][round] to round off a floating point number to a given number of decimal places. +Python provides a built-in function [`round(, )`][round] to round off a floating point number to a given number of decimal places. If no number of decimal places is specified, the number is rounded off to the nearest integer and will return an `int`: ```python diff --git a/concepts/sets/about.md b/concepts/sets/about.md index 321c5116c9a..944f98d1b52 100644 --- a/concepts/sets/about.md +++ b/concepts/sets/about.md @@ -1,18 +1,18 @@ # Sets A [`set`][type-set] is a _mutable_ and _unordered_ collection of [_hashable_][hashable] objects. -Set members must be distinct -- duplicate items are not allowed. -They can hold multiple different data types and even nested structures like a `tuple` of `tuples` -- as long as all elements can be _hashed_. -Sets also come in an immutable [`frozensets`][type-frozenset] flavor. +Set members must be distinct โ€” duplicate items are not allowed. +They can hold multiple different data types and even nested structures like a `tuple` of `tuples` โ€” as long as all elements can be _hashed_. +Sets also come in an immutable [`frozenset`][type-frozenset] flavor. Sets are most commonly used to quickly remove duplicates from other data structures or item groupings. They are also used for efficient comparisons when sequencing and duplicate tracking are not needed. Like other collection types (_dictionaries, lists, tuples_), `sets` support: -- Iteration via `for item in ` +- Iteration via `for item in `, - Membership checking via `in` and `not in`, - Length calculation through `len()`, and -- Shallow copies through `copy()` +- Shallow copies through `copy()`. `sets` do not support: - Indexing of any kind @@ -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({'โ˜๏ธ', 'โญ๏ธ', '๐ŸŒ'})} ``` @@ -172,12 +174,12 @@ Traceback (most recent call last): Sets have methods that generally mimic [mathematical set operations][mathematical-sets]. Most (_not all_) of these methods have an [operator][operator] equivalent. -Methods generally take any `iterable` as an argument, while operators require that both sides of the operation are `sets` or `frozensets`. +Methods generally take any `iterable` as an argument, while operators require that both sides of the operation are `set`s or `frozenset`s. ### Membership Testing Between Sets -The `.isdisjoint()` method is used to test if a `sets` elements have any overlap with the elements of another. +The `.isdisjoint()` method is used to test if a `set`'s elements have any overlap with the elements of another. The method will accept any `iterable` or `set` as an argument. It will return `True` if the two sets have **no elements in common**, `False` if elements are **shared**. @@ -273,9 +275,9 @@ True ### 'Proper' Subsets and Supersets ` < ` and ` > ` are used to test for _proper subsets_. -A `set` is a proper subset if (`` <= ``) **AND** (`` != ``) for the `<` operator. +A `set` is a proper subset if (` <= `) **AND** (` != `) for the `<` operator. -A `set is a proper superset if `(`` >= ``) **AND** (`` != ``) for the `>` operator. +A `set` is a proper superset if (` >= `) **AND** (` != `) for the `>` operator. These operators have no method equivalent: ```python @@ -334,7 +336,7 @@ The operator form of this method is ` | | | ... ### Set Differences `.difference(*)` returns a new `set` with elements from the original `` that are not in ``. -The operator version of this method is ` - - - ...`. +The operator version of this method is ` - - - ... - `. ```python >>> berries_and_veggies = {'Asparagus', @@ -360,7 +362,7 @@ The operator version of this method is ` - - - 'Goose Berries', 'Strawberries'} # Operators require sets. ->>> berries_and_veggies - just_berries +>>> berries_and_veggies - berries {'Artichokes','Asparagus','Broccoli','Kale', 'Ramps','Rhubarb','Walking Onions','Watercress'} ``` @@ -368,7 +370,7 @@ The operator version of this method is ` - - - ### Set Intersections `.intersection(*)` returns a new `set` with elements common to the original `set` and all `` (in other words, the `set` where everything [intersects][intersection]). -The operator version of this method is ` & & & ... ` +The operator version of this method is ` & & & ... & ` ```python >>> perennials = {'Annatto','Asafetida','Asparagus','Azalea', @@ -383,7 +385,7 @@ The operator version of this method is ` & & & . >>> herbs = ['Annatto','Asafetida','Basil','Chervil','Cilantro', 'Curry Leaf','Fennel','Kaffir Lime','Lavender', - 'Marjoram','Mint','Oregano','Summer Savory' + 'Marjoram','Mint','Oregano','Summer Savory', 'Tarragon','Wild Bergamot','Wild Celery', 'Winter Savory'] @@ -418,8 +420,8 @@ The operator version of this method is ` ^ `. >>> fruit_and_flowers ^ plants_1 {'๐ŸŒฒ', '๐ŸŒธ', '๐ŸŒด', '๐ŸŒต','๐ŸŒบ', '๐ŸŒป'} ->>> fruit_and_flowers ^ plants_2 -{ '๐Ÿฅ‘', '๐ŸŒด','๐ŸŒฒ', '๐ŸŒต', '๐Ÿˆ', '๐Ÿฅญ'} +>>> fruit_and_flowers ^ set(plants_2) +{'๐Ÿฅญ', '๐ŸŒด', '๐ŸŒต', '๐Ÿˆ', '๐ŸŒฒ', '๐Ÿฅ‘'} ``` ~~~~exercism/note diff --git a/concepts/sets/introduction.md b/concepts/sets/introduction.md index 4c264f0903e..551e295e6a0 100644 --- a/concepts/sets/introduction.md +++ b/concepts/sets/introduction.md @@ -1,18 +1,18 @@ # Sets A [`set`][type-set] is a _mutable_ and _unordered_ collection of [_hashable_][hashable] objects. -Set members must be distinct -- duplicate items are not allowed. -They can hold multiple different data types and even nested structures like a `tuple` of `tuples` -- as long as all elements can be _hashed_. -Sets also come in an immutable [`frozensets`][type-frozenset] flavor. +Set members must be distinct โ€” duplicate items are not allowed. +They can hold multiple different data types and even nested structures like a `tuple` of `tuples` โ€” as long as all elements can be _hashed_. +Sets also come in an immutable [`frozenset`][type-frozenset] flavor. Sets are most commonly used to quickly remove duplicates from other data structures or item groupings. They are also used for efficient comparisons when sequencing and duplicate tracking are not needed. Like other collection types (_dictionaries, lists, tuples_), `sets` support: -- Iteration via `for item in ` +- Iteration via `for item in `, - Membership checking via `in` and `not in`, - Length calculation through `len()`, and -- Shallow copies through `copy()` +- Shallow copies through `copy()`. `sets` do not support: - Indexing of any kind diff --git a/concepts/string-formatting/.meta/config.json b/concepts/string-formatting/.meta/config.json index 4e3955fc293..9f4edb07327 100644 --- a/concepts/string-formatting/.meta/config.json +++ b/concepts/string-formatting/.meta/config.json @@ -1,5 +1,11 @@ { "blurb": "There are four main string formatting methods. A '%' formatting mini-language is supported, but is considered outdated. String interpolation (f-strings) and 'str.format()'are newer, and can be used for complex or conditional substitution. 'string.template()' substitution is used for internationalization, where f-strings will not translate.", - "authors": ["valentin-p"], - "contributors": ["j08k", "BethanyG"] + "authors": [ + "valentin-p" + ], + "contributors": [ + "j08k", + "BethanyG", + "BNAndras" + ] } diff --git a/concepts/string-formatting/about.md b/concepts/string-formatting/about.md index f3b2756b768..be2ac0a1220 100644 --- a/concepts/string-formatting/about.md +++ b/concepts/string-formatting/about.md @@ -30,57 +30,94 @@ In this example, we insert two variable values in the sentence: one `str` and on The expressions evaluated can be almost anything. Some of the (wide range) of possibilities that can be evaluated: `str`, `numbers`, variables, arithmetic expressions, conditional expressions, built-in types, slices, functions, lambdas, comprehensions or **any** objects with either `__str__` or `__repr__` methods defined. -Some examples: +Going from simple to complex: + +**Inserting a variable** โ€” the simplest use of a f-string is to place a variable directly into the string. + +```python +# Assigning a variable +>>> name = "World" + +# Inserting that variable +>>> f'Hello, {name}!' +'Hello, World!' +``` + +**Expressions inside `{}`** โ€” any valid Python expression can be evaluated inside the braces. +Note that using double quotes inside a single-quoted f-string (or vice versa) avoids the need for escape sequences: ```python -# A dictionary of key:value pairs. +# A dictionary of key:value pairs >>> waves = {'water': 1, 'light': 3, 'sound': 5} -# Using the name waves in an f-string. ->>> f'"A dict can be represented with f-string: {waves}."' -'"A dict can be represented with f-string: {\'water\': 1, \'light\': 3, \'sound\': 5}."' +# Inserting the whole dict +>>> f'Wave ranks: {waves}' +"Wave ranks: {'water': 1, 'light': 3, 'sound': 5}" + +# An expression can be evaluated inline +>>> f"Tenfold the value of 'light' is {waves['light'] * 10}." +"Tenfold the value of 'light' is 30." + +# A method call can also be evaluated inline +>>> f'{"hello world!".title()} is a classic greeting.' +'Hello World! is a classic greeting.' -# Here, we pull a value from the dictionary by using the key ->>> f'Tenfold the value of "light" is {waves["light"] * 10}.' -'Tenfold the value of "light" is 30.' +# An f-string can be nested inside another f-string +>>> f"{f'hello world!'.title()} is a classic greeting." +'Hello World! is a classic greeting.' ``` -Replacement fields (_the `{}` in the f-string_) support output control mechanisms such as width, alignment, precision. -This specification is started in the [format specification mini-language][format-mini-language]. +**Output formatting** โ€” the [format specification mini-language][format-mini-language] can be used to control alignment, numeric precision, and much more. +The format specification goes after the value, separated by a `:`. -A more complex example of an `f-string` that includes output control: +```python +# Right-align a value to ten characters, rounding it to 3 decimal places. +>>> value = 1 / 7 +>>> f'One seventh is {value:10.3f}.' +'One seventh is 0.143.' + +# A format specification can be set using variables as well. +>>> padding = 10 +>>> precision = 3 +>>> f'One seventh is {value:{padding}.{precision}f}.' +'One seventh is 0.143.' +``` + +**Putting it all together** โ€” variables, expressions, function calls, and output formatting: ```python -# Assigning variables >>> precision = 3 ->>> verb = "see" ->>> the_end = ['end', 'of', 'transmission'] +>>> f"{30e8 * 111_000:6.{precision}e}" +'3.330e+14' -# Reassigning verb to 'meet'. >>> verb = 'meet' +>>> the_end = ['end', 'of', 'transmission'] +>>> f'"Have a {"NICE".lower()} day, I will {verb} you after {30e8 * 111_000:6.{precision}e} light-years."{the_end}' +'"Have a nice day, I will meet you after 3.330e+14 light-years."[\'end\', \'of\', \'transmission\']' -# This example includes a function, str, a nested f-string, an arithmetic expression, -# precision formatting, bracket escaping and object formatting. ->>> f'"Have a {"NICE".lower()} day, I will {verb} you after {f"{30e8 * 111_000:6.{precision}e}"} light-years."{{{the_end}}}' -'"Have a nice day, I will meet you after 3.330e+14 light-years."{[\'end\', \'of\', \'transmission\']}' +# Did you notice the escaped single-quotes in the previous example? +# Using double quotes instead of single quotes for the f-string means the list's single-quoted strings print cleanly. +>>> f"Have a nice day. {the_end}" +"Have a nice day. ['end', 'of', 'transmission']" ``` -There are a few limitations to be aware of. -`f-string` expressions cannot be empty, they cannot contain comments. +There are two main limitations to be aware of. +`f-string` expressions can not be empty. +[Additionally, before Python 3.12, they could not contain comments.][pep-0701] ```python >>> f"An empty expression will error: {}" SyntaxError: f-string: empty expression not allowed >>> word = 'word' ->>> f"""A comment in a triple quoted f-string will error: { +>>> f"""A comment in a triple quoted f-string: { word # I chose a nice variable }""" -SyntaxError: f-string expression part cannot include '#' +'A comment in a triple quoted f-string: word' ``` ~~~~exercism/caution -String interpolation cannot be used together with the [GNU gettext API][gnu-gettext-api] for internationalization (I18N) and localization (L10N), so it is recommended that the `string.Template(template)` class or the `str.format()` method outlined below be used instead of an `f-string` in any "string wrapping" translation scenarios. +String interpolation can not be used together with the [GNU gettext API][gnu-gettext-api] for internationalization (I18N) and localization (L10N), so it is recommended that the `string.Template(template)` class or the `str.format()` method outlined below be used instead of an `f-string` in any "string wrapping" translation scenarios. Also keep in mind that using expressions inside the `f-string` brackets `{}` is similar to using `eval()` or `exec()`, so it isn't very safe and should be used sparingly. ~~~~ @@ -105,7 +142,7 @@ The complete formatting specifier pattern is `{[][!][:` can be a named placeholder or a number or empty. - `!` is optional and should be one of this three conversions: `!s` for [`str()`][str-conversion], `!r` for [`repr()`][repr-conversion] or `!a` for [`ascii()`][ascii-conversion]. By default, `str()` is used. -- `:` is optional and has a lot of options, which we are [listed here][format-specifiers]. +- `:` is optional and controls how the value is displayed. More information about possible options can be [found here][format-specifiers]. Example of conversions for a diacritical letter: @@ -132,13 +169,39 @@ Example of conversions for a diacritical letter: "She said her name is not Chloe but 'Zoรซ'." ``` -Example of using format specifiers: +Examples of common format specifiers: ```python -# Formats the object at index 0 as a decimal with zero places, -# then as a right-aligned binary number in an 8 character wide field. ->>> "The number {0:d} has a representation in binary: '{0: >8b}'.".format(42) -"The number 42 has a representation in binary: ' 101010'." +# Integer and binary/hex representations of the same number +>>> my_num = 42 +>>> f"{my_num} in binary is {my_num:b}. In hex, it is {my_num:x}" +"42 in binary is 101010. In hex, it is 2a" + +# Alignment: left (<), right (>), and center (^) using up to ten characters total +>>> f"[{"left":<10}] [{"right":>10}] [{"center":^10}]" +"[left ] [ right] [ center ]" + +# Float precision and scientific notation up to three decimal places +>>> pi = 3.141592653589793 +>>> f"fixed: {pi:.3} scientific: {pi:.3e}" +"fixed: 3.142 scientific: 3.142e+00" + +# Thousands separator and percentage +>>> balance = 1000 +>>> rate = 0.0225 +>>> f"Balance: ${balance:,.0f} Interest rate: {rate:.1%}" +"Balance: $1,000 Interest rate: 2.2%" + +# Putting it all together +>>> items = [("Widget", 1250, 9.991), ("Gadget", 37, 24.503), ("Doohickey", 4, 149.002)] +>>> header = f"{"Item":<12} {"Qty":>6} {"Price":>9}" +>>> print(header) +Item Qty Price +>>> for name, qty, price in items: +... print(f"{name:<12} {qty:>6} {price:>9.2f}") +Widget 1250 9.99 +Gadget 37 24.50 +Doohickey 4 149.00 ``` More examples are shown at the end of [this documentation][summary-string-format]. @@ -177,8 +240,10 @@ If you want to add multiple variables to a string, you need to supply a [tuple][ ## Template Strings -[`string.Template()`][string.Template()] is a class from the `string` module (_as opposed to the built-in `str` type_), which is part of the Python standard library, but has to be imported for use. -Template strings support `$`-based substitution and are much simpler and less capable than the other options mentioned here, but can be very useful for when complicated internationalization is needed, or outside inputs need to be sanitized. +[`string.Template()`][string.Template()] (_not to be confused with Python 3.14 [t-strings]_) is a class from the `string` module (_as opposed to the built-in `str` type_), which is part of the Python standard library, but has to be imported for use. +Template strings support `$`-based substitution and are much simpler and less capable than the other options mentioned here. +However, they can be very useful for when complicated internationalization is needed, or outside inputs need to be sanitized. +`string.Template` is considered safer for untrusted user input because it prevents evaluating arbitrary expressions or accessing object attributes, which mitigates format-string injection attacks. ```python >>> from string import Template @@ -204,8 +269,8 @@ A few quick guidelines: If you don't need to internationalize, they should be the Python 3.6+ preferred method. 2. `str.format()` is versatile, very powerful and compatible with both `gnu gettext` and most versions of Python. 3. If simplicity, safety, and/or heavy internationalization is what you need, `string.Template()` can be used to mitigate risks when inputs from users need to be handled, and for wrapping translation strings. -4. The `%` operator is not supported in some newer distributions of Python and should mostly be used for compatibility with old code. -`%` formatting` can lead to issues displaying non-ascii and unicode characters and has more errors and less functionality than other methods. +4. The `%` operator is generally considered deprecated for new code, though it still works in modern Python. It should mostly be used for compatibility with older codebases. +`%` formatting can lead to issues displaying non-ASCII and Unicode characters and has more errors and less functionality than other methods. Check your specific Python distribution for support details if you intend to use it. If you want to go further: [all about formatting][all-about-formatting] and [Python String Formatting Best Practices][formatting best practices] are good places to start. @@ -216,6 +281,7 @@ If you want to go further: [all about formatting][all-about-formatting] and [Pyt [format-specifiers]: https://www.python.org/dev/peps/pep-3101/#standard-format-specifiers [formatting best practices]: https://realpython.com/python-string-formatting/ [pep-0498]: https://peps.python.org/pep-0498 +[pep-0701]: https://peps.python.org/pep-0701/ [printf-style-docs]: https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting [repr-conversion]: https://www.w3resource.com/python/built-in-function/repr.php [str-conversion]: https://www.w3resource.com/python/built-in-function/str.php @@ -224,5 +290,6 @@ If you want to go further: [all about formatting][all-about-formatting] and [Pyt [string.Template()]: https://docs.python.org/3/library/string.html#template-strings [summary-string-format]: https://www.w3schools.com/python/ref_string_format.asp [template-string]: https://docs.python.org/3/library/string.html#template-strings +[t-strings]: https://realpython.com/python-t-strings/ [tuples]: https://www.w3schools.com/python/python_tuples.asp [zen-of-python]: https://www.python.org/dev/peps/pep-0020/ diff --git a/concepts/string-formatting/introduction.md b/concepts/string-formatting/introduction.md index aa476de9ca0..ed88cc4d01c 100644 --- a/concepts/string-formatting/introduction.md +++ b/concepts/string-formatting/introduction.md @@ -3,18 +3,20 @@ ## String Formatting in Python The [Zen of Python][zen-of-python] asserts there should be "one _obvious_ way to do something in Python". -But when it comes to string formatting, things are a little .... _less zen_. -It can be surprising to find out that there are **four** main ways to perform string formatting in Python - each for a different scenario. -Some of this is due to Python's long history and some of it is due to considerations like internationalization or input sanitation. +For Python 3.6+, **literal string interpolation** (**`f-strings`**) is often the obvious and preferred way to format strings: -With 4 different paths to take, how do you decide what to use? +```python +>>> adjective = "easy" +>>> f"This is an {adjective} way to format strings!" +'This is an easy way to format strings!' +``` -1. `f-strings` are the newest and easiest to read. -If you don't need to internationalize, they should be the Python 3.6+ preferred method. -2. `str.format()` is versatile, very powerful and compatible with both `gnu gettext` and most versions of Python. -3. If simplicity, safety, and/or heavy internationalization is what you need, `string.Template()` can be used to mitigate risks when inputs need to be handled and for wrapping translation strings. -4. The `%` operator should mostly be used for compatibility with old code. -`%` formatting` can lead to issues displaying non-ascii and unicode characters and has more errors and less functionality than other methods. +However, given Python's long history and different use-cases, it might not be surprising that there are **three** other common ways to perform string formatting in Python: + +1. `str.format()` is versatile, very powerful and compatible with both `gnu gettext` and most versions of Python. +2. If simplicity, safety, and/or heavy internationalization is what you need, `string.Template()` can be used to mitigate risks when inputs need to be handled and for wrapping translation strings. +3. The `%` operator is generally considered deprecated for new code, though it still works in modern Python. +It should mostly be used for compatibility with older codebases. `%` formatting can lead to issues displaying non-ASCII and Unicode characters and has more errors and less functionality than other methods.Check your specific Python distribution for support details if you intend to use it. If you want to go further: [all about formatting][all-about-formatting] and [Python String Formatting Best Practices][formatting best practices] are good places to start. diff --git a/concepts/string-methods/about.md b/concepts/string-methods/about.md index 308b89d9d6a..6c9c843b97c 100644 --- a/concepts/string-methods/about.md +++ b/concepts/string-methods/about.md @@ -5,13 +5,15 @@ This may include letters, diacritical marks, positioning characters, numbers, cu 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 using ` + ` or `.join()` and split via `.split()`. + They also offer multiple other formatting and assembly options. To further work with strings, Python provides a rich set of [string methods][str-methods] for searching, cleaning, transforming, translating, and many other operations. Some of the more commonly used `str` methods include: -- Checking for prefixes/suffixes with `startswith()` and `endswith()` +- Checking for prefixes/suffixes with `.startswith()` and `.endswith()` - Altering string casing with methods like `.title()`, `.upper()`/`.lower()`, and `.swapcase()` - Removing leading or trailing characters from a string using `.strip()`, `.lstrip()`, or `.rstrip()` - Replacing substrings with the `.replace(, )` method @@ -32,7 +34,7 @@ True >>> 'Do you want to ๐Ÿ’ƒ?'.endswith('๐Ÿ’ƒ') False ->> 'The quick brown fox jumped over the lazy dog.'.endswith('dog') +>>> 'The quick brown fox jumped over the lazy dog.'.endswith('dog') False ``` @@ -116,7 +118,7 @@ Just the place for a Snark! I have said it thrice: 'book keeper' ``` -:star:**Newly added in Python `3.9`** +๐ŸŒŸ**Newly added in Python `3.9`** Python `3.9` introduces two new string methods that make removing prefixes and suffixes much easier. @@ -143,7 +145,7 @@ Python `3.9` introduces two new string methods that make removing prefixes and s For more examples and methods the [informal tutorial][informal tutorial] is a nice jumping-off point. [How to Unicode][howto unicode] in the Python docs offers great detail on Unicode, encoding, bytes, and other technical considerations for working with strings in Python. -Python also supports regular expressions via the `re` module, which will be covered in a future exercise. +Python also supports regular expressions via the `re` module, which will be covered in a future concept. [Lewis Carroll]: https://www.poetryfoundation.org/poets/lewis-carroll diff --git a/concepts/string-methods/introduction.md b/concepts/string-methods/introduction.md index e20e58e7014..941e4e81bb1 100644 --- a/concepts/string-methods/introduction.md +++ b/concepts/string-methods/introduction.md @@ -4,9 +4,10 @@ A `str` in Python is an [immutable sequence][text sequence] of [Unicode code poi These may include letters, diacritical marks, positioning characters, numbers, currency symbols, emoji, punctuation, various spaces, line breaks, and more. 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. -They can be concatenated with `+`, or via `.join()`, split via `.split()`, and offer multiple formatting and assembly options. +They can be concatenated using ` + ` or `.join()`, split with `.split()`, and offer multiple formatting and assembly options. To further work with strings, Python provides a rich set of [string methods][str-methods] that can assist with searching, cleaning, transforming, translating, and many other operations. + Being _immutable_, a `str` object's value in memory doesn't change; methods that appear to modify a string return a new copy or instance of that `str` object. [common sequence operations]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations diff --git a/concepts/strings/about.md b/concepts/strings/about.md index 0107f6e70f0..19920adad31 100644 --- a/concepts/strings/about.md +++ b/concepts/strings/about.md @@ -4,21 +4,22 @@ A `str` in Python is an [immutable sequence][text sequence] of [Unicode code poi These may include letters, diacritical marks, positioning characters, numbers, currency symbols, emoji, punctuation, space and line break characters, and more. For a deep dive on what information a string encodes (or, _"how does a computer know how to translate zeroes and ones into letters?"_), [this blog post is enduringly helpful][joel-on-text]. -The Python docs also provide a very detailed [unicode HOWTO][unicode how-to] that discusses Pythons support for the Unicode specification in the `str`, `bytes` and `re` modules, considerations for locales, and some common issues with encoding and translation. +The Python docs also provide a very detailed [unicode HOWTO][unicode how-to] that discusses Python's support for the Unicode specification in the `str`, `bytes` and `re` modules, considerations for locales, and some common issues with encoding and translation. 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 `.join()` and split via `.split()`. +They also offer multiple additional formatting, assembly, and templating options. -A `str` literal can be declared via single `'` or double `"` quotes. The escape `\` character is available as needed. +A `str` literal can be declared using single `'` or double `"` quotes. The escape `\` character is available as needed. ```python >>> single_quoted = 'These allow "double quoting" without "escape" characters.' ->>> double_quoted = "These allow embedded 'single quoting', so you don't have to use an 'escape' character". +>>> double_quoted = "These allow embedded 'single quoting', so you don't have to use an 'escape' character." ``` @@ -101,7 +102,7 @@ There is no separate โ€œcharacterโ€ or "rune" type in Python, so indexing a str True ``` -Substrings can be selected via _slice notation_, using [`[:stop:]`][common sequence operations] to produce a new string. +Substrings can be selected via _slice notation_, using [`[::]`][common sequence operations] to produce a new string. Results exclude the `stop` index. If no `start` is given, the starting index will be 0. If no `stop` is given, the `stop` index will be the end of the string. @@ -168,12 +169,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 +184,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 +223,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/strings/introduction.md b/concepts/strings/introduction.md index 61b5fbd5795..22770fde4c8 100644 --- a/concepts/strings/introduction.md +++ b/concepts/strings/introduction.md @@ -5,12 +5,13 @@ These could include letters, diacritical marks, positioning characters, numbers, 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. -Strings can be concatenated with `+`, or via `.join()`, split via `.split()`, and offer multiple types of formatting, interpolation, and templating. +Strings can be concatenated with ` + ` or `.join()` and split via `.split()`. +They also offer multiple additional formatting, assembly, and templating options. Being immutable, a `str` object's value in memory doesn't change; methods that appear to modify a string return a new copy or instance of `str`. For a deep dive on what information a string encodes (or, _"how does a computer know how to translate zeroes and ones into letters?"_), [this blog post is enduringly helpful][joel-on-text]. -The Python docs also provide a very detailed [unicode HOWTO][unicode how-to] that discusses Pythons support for the Unicode specification in the `str`, `bytes` and `re` modules, considerations for locales, and some common issues with encoding and translation. +The Python docs also provide a very detailed [unicode HOWTO][unicode how-to] that discusses Python's support for the Unicode specification in the `str`, `bytes` and `re` modules, considerations for locales, and some common issues with encoding and translation. [common sequence operations]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations diff --git a/concepts/tuples/about.md b/concepts/tuples/about.md index ea8179e2d8a..e61cd39eadb 100644 --- a/concepts/tuples/about.md +++ b/concepts/tuples/about.md @@ -57,8 +57,7 @@ To include both keys and values in a tuple made from a dictionary, use `.i which will return an iterator of (`key`, `value`) `tuples`. ```python -source_data = {"fish": "gold", - "monkey": "brown"} +>>> source_data = {"fish": "gold", "monkey": "brown"} >>> multiple_elements_dict_1 = tuple(source_data) ('fish', 'monkey') @@ -81,7 +80,7 @@ Because the `tuple()` constructor only takes _iterables_ (or nothing) as argumen ``` Note that generally parentheses are **not** required to create a `tuple` literal - only commas. -However, using `(, )` is considered more readable in most circumstances. +However, using `(, )` is considered more readable in most circumstances. Parentheses are also required in cases of ambiguity, such as an empty or one-item tuple or where a function takes a tuple as an argument. ```python @@ -102,7 +101,7 @@ Other data structures can be included as `tuple` elements, including other `tupl (["fish", "gold", "monkey", "brown", "parrot", "grey"], ("fish", "mammal", "bird")) ``` -Tuples can be concatenated using plus `+` operator, which unpacks each `tuple` creating a new, combined `tuple`. +Tuples can be concatenated using plus `+` operator, which unpacks each `tuple`, creating a new, combined `tuple`. ```python >>> new_via_concatenate = ("George", 5) + ("cat", "Tabby") @@ -122,8 +121,7 @@ Indexes can be from **`left`** --> **`right`** (_starting at zero_) or **`right` Tuples can also be copied in whole or in part via _slice notation_ or using `.copy()`. ```python - ->>> student_info = ("Alyssa", "grade 3", "female", 8 ) +>>> student_info = ("Alyssa", "grade 3", "female", 8) #name is at index 0 or index -4 >>> student_name = student_info[0] @@ -146,10 +144,9 @@ Elements inside tuples can be _iterated over_ in a loop using `for item in )` can be used. ```python ->>> student_info = ("Alyssa", "grade 3", "female", 8 ) +>>> student_info = ("Alyssa", "grade 3", "female", 8) >>> for item in student_info: ... print(item) - ... Alyssa grade 3 @@ -157,8 +154,7 @@ female 8 >>> for index, item in enumerate(student_info): -... print("Index is: " + str(index) + ", value is: " + str(item) +".") - +... print("Index is: " + str(index) + ", value is: " + str(item) + ".") ... Index is: 0, value is: Alyssa. Index is: 1, value is: grade 3. @@ -172,9 +168,7 @@ Index is: 3, value is: 8. Tuples are often used as _records_ containing data that is _organizationally_ or _conceptually_ homogeneous and treated as a single unit of information -- even if individual elements are of _heterogeneous_ data types. ```python - ->>> student_info = ("Alyssa", "grade 3", "female", 8 ) - +>>> student_info = ("Alyssa", "grade 3", "female", 8) ``` Tuples are also used when homogeneous immutable sequences of data are needed for [`hashability`][hashability], storage in a `set`, or creation of keys in a dictionary. @@ -183,21 +177,20 @@ Note that while `tuples` are in most cases _immutable_, because they can contain Using a mutable data type within a `tuple` will make the enclosing `tuple` **un-hashable**. ```python - >>> cmyk_color_map = { - (.69, .3, .48, .1) : ("Teal 700", (59, 178, 146), 0x3BB292), - (0, .5, 1, 0) : ("Pantone 151", (247, 127, 1), 0xF77F01), - (.37, .89, 0, .44) : ("Pantone 267", (89, 16, 142), 0x59108E), - (0, 1, .46, .45) : ("Pantone 228", (140, 0, 76), 0x8C004C) - } - ->>>> unique_rgb_colors = { - (59, 178, 146), - (247, 127, 1), - (89, 16, 142), - (140, 0, 76), - (76, 0, 140) - } + (.69, .3, .48, .1) : ("Teal 700", (59, 178, 146), 0x3BB292), + (0, .5, 1, 0) : ("Pantone 151", (247, 127, 1), 0xF77F01), + (.37, .89, 0, .44) : ("Pantone 267", (89, 16, 142), 0x59108E), + (0, 1, .46, .45) : ("Pantone 228", (140, 0, 76), 0x8C004C) + } + +>>> unique_rgb_colors = { + (59, 178, 146), + (247, 127, 1), + (89, 16, 142), + (140, 0, 76), + (76, 0, 140) + } >>> teal_700 = hash((59, 178, 146)) @@ -205,7 +198,6 @@ Using a mutable data type within a `tuple` will make the enclosing `tuple` **un- Traceback (most recent call last): File "", line 1, in TypeError: unhashable type: 'list' - ``` ## Extended tuples and related data types diff --git a/concepts/unpacking-and-multiple-assignment/about.md b/concepts/unpacking-and-multiple-assignment/about.md index 1cec2f92ec9..b269b312adc 100644 --- a/concepts/unpacking-and-multiple-assignment/about.md +++ b/concepts/unpacking-and-multiple-assignment/about.md @@ -8,7 +8,7 @@ A very common example of this behavior is `for item in list`, where `item` takes This allows for code to be more concise and readable, and is done by separating the variables to be assigned with a comma such as `first, second, third = (1,2,3)` or `for index, item in enumerate(iterable)`. The special operators `*` and `**` are often used in unpacking contexts. -`*` can be used to combine multiple `lists`/`tuples` into one `list`/`tuple` by _unpacking_ each into a new common `list`/`tuple`. +`*` can be used to combine multiple `list`s/`tuple`s into one `list`/`tuple` by _unpacking_ each into a new common `list`/`tuple`. `**` can be used to combine multiple dictionaries into one dictionary by _unpacking_ each into a new common `dict`. When the `*` operator is used without a collection, it _packs_ a number of values into a `list`. @@ -73,7 +73,7 @@ Since `tuples` are immutable, you can't swap elements in a `tuple`. The examples below use `lists` but the same concepts apply to `tuples`. ~~~~ -In Python, it is possible to [unpack the elements of `list`/`tuple`/`dictionary`][unpacking] into distinct variables. +In Python, it is possible to [unpack the elements of a `list`/`tuple`/`dict`][unpacking] into distinct variables. Since values appear within `lists`/`tuples` in a specific order, they are unpacked into variables in the same order: ```python @@ -94,7 +94,7 @@ If there are values that are not needed then you can use `_` to flag them: ### Deep unpacking -Unpacking and assigning values from a `list`/`tuple` inside of a `list` or `tuple` (_also known as nested lists/tuples_), works in the same way a shallow unpacking does, but often needs qualifiers to clarify the values context or position: +Unpacking and assigning values from a `list`/`tuple` inside of a `list` or `tuple` (_also known as nested lists/tuples_), works in the same way a shallow unpacking does, but often needs qualifiers to clarify the value's context or position: ```python >>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]] @@ -129,9 +129,9 @@ ValueError: too many values to unpack (expected 1) ### Unpacking a list/tuple with `*` -When [unpacking a `list`/`tuple`][packing and unpacking] you can use the `*` operator to capture the "leftover" values. +When [unpacking a `list`/`tuple`][packing and unpacking] you can use the `*` operator to capture "leftover" values. This is clearer than slicing the `list`/`tuple` (_which in some situations is less readable_). -For example, we can extract the first element and then assign the remaining values into a new `list` without the first element: +For example, we can extract the first element and pack the remaining values into a new `list` without the first element: ```python >>> fruits = ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"] @@ -169,7 +169,7 @@ We can also use `*` in deep unpacking: ### Unpacking a dictionary -[Unpacking a dictionary][packing and unpacking] is a bit different than unpacking a `list`/`tuple`. +[Unpacking a dictionary][packing and unpacking] is a bit different from unpacking a `list`/`tuple`. Iteration over dictionaries defaults to the **keys**. So when unpacking a `dict`, you can only unpack the **keys** and not the **values**: @@ -180,7 +180,7 @@ So when unpacking a `dict`, you can only unpack the **keys** and not the **value "apple" ``` -If you want to unpack the values then you can use the `values()` method: +If you want to unpack the values then you can use the `.values()` method: ```python >>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3} @@ -189,9 +189,9 @@ If you want to unpack the values then you can use the `values()` method: 6 ``` -If both **keys** and **values** are needed, use the `items()` method. -Using `items()` will generate tuples with **key-value** pairs. -This is because of [`dict.items()` generates an iterable with key-value `tuples`][items]. +If both **keys** and **values** are needed, use the [`.items()`][items] method. +`.items()` generates an [iterable view][view-objects] containing **key-value** pairs. +These can be unpacked into a `tuple`: ```python >>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3} @@ -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]) @@ -367,8 +375,12 @@ Since `zip()` takes multiple iterables and returns a `list` of `tuples` with the ``` [args and kwargs]: https://www.geeksforgeeks.org/args-kwargs-python/ -[items]: https://www.geeksforgeeks.org/python-dictionary-items-method/ +[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/concepts/unpacking-and-multiple-assignment/introduction.md b/concepts/unpacking-and-multiple-assignment/introduction.md index 59cab3b4ec3..c8878eb1800 100644 --- a/concepts/unpacking-and-multiple-assignment/introduction.md +++ b/concepts/unpacking-and-multiple-assignment/introduction.md @@ -8,7 +8,7 @@ A very common example of this behavior is `for item in list`, where `item` takes This allows for code to be more concise and readable, and is done by separating the variables to be assigned with a comma such as `first, second, third = (1,2,3)` or `for index, item in enumerate(iterable)`. The special operators `*` and `**` are often used in unpacking contexts. -`*` can be used to combine multiple `lists`/`tuples` into one `list`/`tuple` by _unpacking_ each into a new common `list`/`tuple`. +`*` can be used to combine multiple `list`s/`tuple`s into one `list`/`tuple` by _unpacking_ each into a new common `list`/`tuple`. `**` can be used to combine multiple dictionaries into one dictionary by _unpacking_ each into a new common `dict`. When the `*` operator is used without a collection, it _packs_ a number of values into a `list`. diff --git a/config.json b/config.json index 38dd7520e3c..31f53360614 100644 --- a/config.json +++ b/config.json @@ -363,21 +363,6 @@ ], "difficulty": 1 }, - { - "slug": "wordy", - "name": "Wordy", - "uuid": "af50bb9a-e400-49ce-966f-016c31720be1", - "practices": ["string-methods"], - "prerequisites": [ - "basics", - "lists", - "loops", - "strings", - "string-methods", - "numbers" - ], - "difficulty": 1 - }, { "slug": "resistor-color", "name": "Resistor Color", @@ -526,6 +511,14 @@ ], "difficulty": 1 }, + { + "slug": "line-up", + "name": "Line Up", + "uuid": "e09d877e-489b-4dbd-89c5-f6b15e867b67", + "practices": ["string-formatting"], + "prerequisites": ["basics", "strings"], + "difficulty": 1 + }, { "slug": "difference-of-squares", "name": "Difference of Squares", @@ -601,7 +594,7 @@ "slug": "square-root", "name": "Square Root", "uuid": "c32f994a-1080-4f05-bf88-051975a75d64", - "practices": ["numbers"], + "practices": [], "prerequisites": ["basics", "numbers", "conditionals", "loops"], "difficulty": 2 }, @@ -675,6 +668,21 @@ ], "difficulty": 2 }, + { + "slug": "eliuds-eggs", + "name": "Eliud's Eggs", + "uuid": "356e2d29-7efc-4fa3-bec7-8b61c3e967da", + "practices": ["loops"], + "prerequisites": [ + "basics", + "lists", + "list-methods", + "loops", + "strings", + "string-methods" + ], + "difficulty": 2 + }, { "slug": "protein-translation", "name": "Protein Translation", @@ -767,7 +775,7 @@ "slug": "nth-prime", "name": "Nth Prime", "uuid": "a20924d2-fe6d-4714-879f-3239feb9d2f2", - "practices": [], + "practices": ["generators"], "prerequisites": [ "basics", "bools", @@ -967,17 +975,24 @@ "difficulty": 2 }, { - "slug": "eliuds-eggs", - "name": "Eliud's Eggs", - "uuid": "356e2d29-7efc-4fa3-bec7-8b61c3e967da", + "slug": "game-of-life", + "name": "Conway's Game of Life", + "uuid": "1675a497-d3b2-4772-bbee-4edae5a44e91", + "practices": [], + "prerequisites": [ + "lists" + ], + "difficulty": 3 + }, + { + "slug": "state-of-tic-tac-toe", + "name": "State of Tic-Tac-Toe", + "uuid": "cb40b36f-f7dc-4018-aad5-38976defb352", "practices": ["loops"], "prerequisites": [ - "basics", - "lists", - "list-methods", - "loops", - "strings", - "string-methods" + "conditionals", + "lists", + "loops" ], "difficulty": 3 }, @@ -1210,6 +1225,21 @@ ], "difficulty": 3 }, + { + "slug": "wordy", + "name": "Wordy", + "uuid": "af50bb9a-e400-49ce-966f-016c31720be1", + "practices": ["string-methods"], + "prerequisites": [ + "basics", + "lists", + "loops", + "strings", + "string-methods", + "numbers" + ], + "difficulty": 3 + }, { "slug": "crypto-square", "name": "Crypto Square", @@ -1328,17 +1358,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 +1442,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 +1533,7 @@ "slug": "scale-generator", "name": "Scale Generator", "uuid": "8cd58325-61fc-46fd-85f9-425b4c41f3de", - "practices": ["generators"], + "practices": [], "prerequisites": [ "basics", "bools", @@ -1503,12 +1551,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", @@ -1717,6 +1760,14 @@ ], "difficulty": 5 }, + { + "slug": "camicia", + "name": "Camicia", + "uuid": "ec314b34-2ee3-4eec-9a97-aece9e5fd6c2", + "practices": [], + "prerequisites": ["lists", "sets", "strings"], + "difficulty": 5 + }, { "slug": "rational-numbers", "name": "Rational Numbers", @@ -1770,6 +1821,17 @@ ], "difficulty": 5 }, + { + "slug": "relative-distance", + "name": "Relative Distance", + "uuid": "d590865c-ef30-424a-8cfb-7f31f04dee1b", + "practices": [], + "prerequisites": [ + "lists", + "dicts" + ], + "difficulty": 5 + }, { "slug": "dot-dsl", "name": "DOT DSL", @@ -2223,6 +2285,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/ABOUT.md b/docs/ABOUT.md index 6177394a518..8f06e20267a 100644 --- a/docs/ABOUT.md +++ b/docs/ABOUT.md @@ -20,14 +20,14 @@ Code can be written and executed from the command line, in an interactive interp The [zen of Python (PEP 20)][the zen of python] and [What is Pythonic?][what is pythonic] lay out additional philosophies and perspectives on the language. -Tests and tooling for this track currently support `3.7` - `3.11.2` (_tests_) and [`Python 3.11.2`][311-new-features] (_tooling_). -It is highly recommended that students upgrade to at least `Python 3.8`, as some features used by this track may not be supported in earlier versions. +Tests and tooling for this track currently support `3.10` - `3.13.5` (_tests_) and [`Python 3.13.5`][313-new-features] (_tooling_). +It is highly recommended that students upgrade to at least `Python 3.10`, as some features used by this track may not be supported in earlier versions. That being said, most of the exercises will work with `Python 3.6+`, or even earlier versions. But we don't guarantee support for versions not listed under [Active Python Releases][active-python-releases]. We will try to note when a feature is only available in a certain version. -Complete documentation for the current release of Python (3.11.x) can be found at [docs.python.org][python docs]. +Complete documentation for the current release of Python (3.13.x) can be found at [docs.python.org][python docs]. - [Python Tutorial][python tutorial] - [Python Library Reference][python library reference] @@ -37,8 +37,8 @@ Complete documentation for the current release of Python (3.11.x) can be found a - [Python Glossary of Terms][python glossary of terms] -[311-new-features]: https://docs.python.org/3/whatsnew/3.11.html -[active-python-releases]: https://www.python.org/downloads/ +[313-new-features]: https://docs.python.org/3/whatsnew/3.13.html +[active-python-releases]: https://devguide.python.org/versions/#full-chart [duck typing]: https://en.wikipedia.org/wiki/Duck_typing [dynamic typing in python]: https://stackoverflow.com/questions/11328920/is-python-strongly-typed [editors for python]: https://djangostars.com/blog/python-ide/ @@ -49,16 +49,16 @@ Complete documentation for the current release of Python (3.11.x) can be found a [peps]: https://www.python.org/dev/peps/ [psf membership]: https://www.python.org/psf/membership/ [psf]: https://www.python.org/psf/ -[python docs]: https://docs.python.org/3/ -[python faqs]: https://docs.python.org/3/faq/index.html +[python docs]: https://docs.python.org/3.13/ +[python faqs]: https://docs.python.org/3.13/faq/index.html [python for beginners]: https://www.python.org/about/gettingstarted/ [python glossary of terms]: https://docs.python.org/3/glossary.html -[python how tos]: https://docs.python.org/3/howto/index.html +[python how tos]: https://docs.python.org/3.13/howto/index.html [python is used extensively]: https://www.python.org/about/apps/ -[python language reference]: https://docs.python.org/3/reference/index.html -[python library reference]: https://docs.python.org/3/library/index.html -[python tutorial]: https://docs.python.org/3/tutorial/index.html -[significant indentation]: https://docs.python.org/3/reference/lexical_analysis.html#indentation +[python language reference]: https://docs.python.org/3.13/reference/index.html +[python library reference]: https://docs.python.org/3.13/library/index.html +[python tutorial]: https://docs.python.org/3.13/tutorial/index.html +[significant indentation]: https://docs.python.org/3.13/reference/lexical_analysis.html#indentation [the zen of python]: https://www.python.org/dev/peps/pep-0020/ [type hints]: https://docs.python.org/3/library/typing.html [what is pythonic]: https://blog.startifact.com/posts/older/what-is-pythonic.html diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 7be6910710d..d70d15153f7 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -14,24 +14,25 @@ Some quick links into the documentation by operating system: - [Windows][windows] Additionally, this Microsoft article on [installing Python on windows][python-on-windows] is helpful. - [Unix & Linux Systems][unix-and-linux] (_these largely work for MacOS as well_) -- [MacOS][macos] : **This is outdated.** - We recommend reviewing some of the methods outlined in the Real Python article [Installing Python][installing-python] or the articles by Brett Cannon linked above. +- [MacOS][macos] + We also recommend reviewing some of the methods outlined in the Real Python article [Installing Python][installing-python] or the articles by Brett Cannon linked above. -Exercism tests and tooling currently support `3.7` - `3.11.5` (_tests_) and [`Python 3.11.5`][311-new-features] (_tooling_). + +Exercism tests and tooling currently support `3.10` - `3.13.5` (_tests_) and [`Python 3.13.5`][313-new-features] (_tooling_). Exceptions to this support are noted where they occur. Most of the exercises will work with `Python 3.6+`, or even earlier versions. But we don't guarantee support for versions not listed under [Active Python Releases][active-python-releases]. -Please refer to the [Python 3.11.x documentation][3.11 docs] for what is currently supported. +Please refer to the [Python 3.13.x documentation][3.13 docs] for what is currently supported. -[3.11 docs]: https://docs.python.org/3.11/ -[311-new-features]: https://docs.python.org/3/whatsnew/3.11.html +[3.13 docs]: https://docs.python.org/3.13/ +[313-new-features]: https://docs.python.org/3/whatsnew/3.13.html [Python-three downloads]: https://www.python.org/downloads/ [active-python-releases]: https://www.python.org/downloads/ [helpful guide]: https://realpython.com/installing-python/ -[installing-python]: https://realpython.com/installing-python/#what-your-options-are_1 +[installing-python]: https://realpython.com/installing-python/ [macos]: https://docs.python.org/3/using/mac.html [python-m-pip]: https://snarky.ca/why-you-should-use-python-m-pip/ [python-on-windows]: https://docs.microsoft.com/en-us/windows/python/beginners diff --git a/docs/LEARNING.md b/docs/LEARNING.md index 50a3259eed7..4a85339a936 100644 --- a/docs/LEARNING.md +++ b/docs/LEARNING.md @@ -6,21 +6,26 @@ 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. - [Python Documentation Tutorial][Python Documentation Tutorial] +- [Stanford Code in Place Course][code in place] +- [Stanford Code in Place Self-Guided][cip self guided] - [Automate the Boring Stuff with Python (_book_)][automate the boring stuff] -- [Automate the Boring Stuff Videos (_covers first 15 book chapters_)][automate the videos] +- [Automate the Boring Stuff with Python Workbook (_exercises_)][automate the boring stuff workbook] +- [Think Python][Think Python] +- [Beyond the Basic Stuff with Python][beyond the basics] +- [Beyond the Basic Stuff with Python Video][beyond the basics video] +- [Python Programming Exercises, Gently Explained][PPEG] +- [Python for Non-Programmers][python-for-non-programmers] - [Learn X in Y minutes (where X = Python3)][Learn X in Y minutes] - [Python at Free Code Camp][python at free code camp] +- [Python at Khan Academy][python at kahn academy] - [Intro to Python (_python-course.eu_)][python-course.eu] -- [Think Python][Think Python] -- [Python for Non-Programmers][python-for-non-programmers] -- [Python 4 Everyone][python4everyone] - [Googles Python Class][googles python class] - [Microsoft's Python Learning Path][MS Python] - [Introduction to Computer Science and Programming in Python (MIT)][mitocw600] @@ -30,14 +35,19 @@ Below you will find some additional jumping-off places to start your learning jo [CS50P]: https://pll.harvard.edu/course/cs50s-introduction-programming-python?delta=0 [Learn X in Y minutes]: https://learnxinyminutes.com/docs/python3/ [MS Python]: https://docs.microsoft.com/en-us/learn/paths/python-language/ +[PPEG]: https://inventwithpython.com/pythongently/ [Python Documentation Tutorial]: https://docs.python.org/3/tutorial/index.html -[Python at Free Code Camp]: https://www.freecodecamp.org/learn/scientific-computing-with-python/ -[Think Python]: http://www.greenteapress.com/thinkpython/html/index.html -[automate the boring stuff]: https://automatetheboringstuff.com/2e/ -[automate the videos]: https://www.youtube.com/watch?v=1F_OgqRuSdI&list=PL0-84-yl1fUnRuXGFe_F7qSH1LEnn9LkW -[googles python class]: https://developers.google.com/edu/python/introduction +[Python at Free Code Camp]: https://www.freecodecamp.org/learn/python-v9/ +[Think Python]: https://allendowney.github.io/ThinkPython/ +[automate the boring stuff workbook]: https://automatetheboringstuff.com/#toc +[automate the boring stuff]: https://automatetheboringstuff.com/#toc +[beyond the basics video]: https://www.youtube.com/watch?v=kSrnLbioN6w +[beyond the basics]: https://inventwithpython.com/beyond/ +[cip self guided]: https://codeinplace.stanford.edu/public/studenthome +[code in place]: https://codeinplace.stanford.edu/ +[googles python class]: https://developers.google.com/edu/python [mitocw600]: https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-0001-introduction-to-computer-science-and-programming-in-python-fall-2016/ +[python at kahn academy]: https://www.khanacademy.org/computing/intro-to-python-fundamentals [python-course.eu]: https://python-course.eu/python-tutorial/ [python-for-non-programmers]: https://store.lerner.co.il/python-for-non-programmers-live -[python4everyone]: https://www.py4e.com/ [wikipython]: https://en.wikipedia.org/wiki/Python_(programming_language) diff --git a/docs/PROBLEM-SOLVING.md b/docs/PROBLEM-SOLVING.md index 198b9ea5ef9..47a99e8a8ab 100644 --- a/docs/PROBLEM-SOLVING.md +++ b/docs/PROBLEM-SOLVING.md @@ -8,9 +8,9 @@ Below are some community-sourced articles, videos, and books that can help you d - Free Code Camp offers a good overview article on [How to Think Like a Programmer - Lessons in Problem-Solving][free-code-camp-think-like-a-programmer]. -- Kurtis Pykes writing for Towards Data Science has a nice [hands-on tutorial for problem-solving][Kurtis Pykes: Hands-on Tutorial - How to Improve Your Problem-Solving Skills as A Programmer]. -- UC Berkeley has a nice PDF summary of [G. Polya's Problem Solving Techniques][g-polya-how-to-solve-it-summary]. +- Jeremy Howard, founder of fast.ai has a nice summary of [G. Polya's Problem Solving Techniques][g-polya-how-to-solve-it-summary]. - Originally written in 1945 as guidance for tackling complicated math problems,[G. Polya's How To Solve It][g-polya-how-to-solve-it] (full book) is still _excellent_ advice for problem-solving in general. +- Kurtis Pykes writing for Towards Data Science has a nice [hands-on tutorial for problem-solving][Kurtis Pykes: Hands-on Tutorial - How to Improve Your Problem-Solving Skills as A Programmer]. - Mentioned in the Free Code Camp Article, V. Anton Spraul's [Think Like a Programmer: An Introduction to Creative Problem Solving][v-anton-spraul-think-like-a-programmer] is a more modern and programming-focused take on the same general methods Polya outlines for mathematics. - [Felienne Hermans][felienne-hermans] is more focused on _how_ people learn the art of coding and how that maps to learning in general. She has written [The Programmers Brian][programmers-brain-free-online], with strategies for reading code better, thinking about code clearly, writing better code, and becoming a better code collaborator. @@ -32,8 +32,8 @@ Membership (paid) information is available at [acm(dot)org][association-for-comp [felienne-hermans-programming-is-writing-is-programming]: https://www.youtube.com/watch?v=uO3a4HIBDU4 [felienne-hermans]: https://www.felienne.com/ [free-code-camp-think-like-a-programmer]: https://www.freecodecamp.org/news/how-to-think-like-a-programmer-lessons-in-problem-solving-d1d8bf1de7d2/ -[g-polya-how-to-solve-it-summary]: https://math.berkeley.edu/~gmelvin/polya.pdf -[g-polya-how-to-solve-it]: https://press.princeton.edu/books/paperback/9780691164076/how-to-solve-it +[g-polya-how-to-solve-it-summary]: https://gist.github.com/jph00/d60301884c56fe063101a7cc6193b3af +[g-polya-how-to-solve-it]: https://archive.org/details/howtosolveit0000gpol_c0p2/page/n9/mode/2up [paul-vickers-how-to-think-like-a-programmer]: https://www.researchgate.net/publication/236270907_How_to_Think_like_a_Programmer_Problem_Solving_for_the_Bewildered [programmers-brain-free-online]: https://www.manning.com/books/the-programmers-brain#toc [programmers-brain-manning]: https://www.manning.com/books/the-programmers-brain diff --git a/docs/RESOURCES.md b/docs/RESOURCES.md index ea8527592ab..f04fcf601d0 100644 --- a/docs/RESOURCES.md +++ b/docs/RESOURCES.md @@ -5,24 +5,26 @@ - [The Docs on pip][pip] - [Tall, Snarky Canadian (_The Blog of Core Python Developer Brett Cannon_)][Tall, Snarky Canadian] - [Practical Python][practical python] -- [Python 3 Module of the Week (PyMOTW-3)][pymotw-3] -- [Beyond the Basic Stuff with Python][Beyond the Basic Stuff with Python] -- [The Big Book of Small Python Projects][The Big Book of Small Python Projects] - [Data Structures and Information Retrieval in Python][Data Structures and Information Retrieval in Python] -- [python practice projects][python practice projects] -- [Python Courses eu][python-course.eu main] -- [Fluent Python Notebooks][fluent-notebooks] +- [Fluent Python, 2nd Edition][Fluent Python 2] (_you might be able to find free access through a library or online_) +- [Fluent Python Notebooks][fluent-notebooks] (_these are based on the first edition, but still really good_) +- [The Big Book of Small Python Projects][The Big Book of Small Python Projects] +- [Practice Python Projects][practice python projects] +- [Dataquest: Python Projects for Beginners][Python projects for beginners] +- [Mouse vs Python (Mike Driscoll's blog)][mouse vs python] +- [Simon Willison's Weblog][simon willison] -[Beyond the Basic Stuff with Python]: https://inventwithpython.com/beyond/ [Data Structures and Information Retrieval in Python]: https://allendowney.github.io/DSIRP/ [Practical Python]: https://dabeaz-course.github.io/practical-python/ +[Python projects for beginners]: https://www.dataquest.io/blog/python-projects-for-beginners/ [Tall, Snarky Canadian]: https://snarky.ca/ [The Big Book of Small Python Projects]: http://inventwithpython.com/bigbookpython/ [The Python Library Reference]: https://docs.python.org/3/library/index.html +[fluent python 2]: https://www.oreilly.com/library/view/fluent-python-2nd/9781492056348/ [fluent-notebooks]: https://github.com/AllenDowney/fluent-python-notebooks +[mouse vs python]: https://www.blog.pythonlibrary.org/ [pip]: https://pip.pypa.io/en/stable/user_guide/ -[pymotw-3]: https://pymotw.com/3/ +[practice python projects]: https://learnbyexample.github.io/practice_python_projects/preface.html [python docs]: https://docs.python.org/3/ -[python practice projects]: http://pythonpracticeprojects.com/ -[python-course.eu main]: https://python-course.eu/ +[simon willison]: https://simonwillison.net/ 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..11713826c14 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -15,6 +15,7 @@ If you have an editor, IDE, tool, or plugin recommendation, we encourage you to - [Environments](#virtual-environments) - [Venv](#creating-a-virtual-environment-with-venv) - [Conda](#creating-a-virtual-environment-using-conda) + - [uv](#uv) - [Virtual Environment Wrapper](#virtual-environment-wrapper) - [Editors and IDEs](#editors-and-ides) - [Visual Studio Code](#visual-studio-code) @@ -30,7 +31,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.10 - 3.13.5` (_exercises and tests_) and `Python 3.13.5` (_tooling_). +Our online test runner currently uses `pytest 8.4.0` and `pytest-subtests 0.14.2`. +Our online analyzer uses `pylint 4.0.4`. +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].
@@ -42,10 +46,16 @@ Python virtual environments offer lightweight runtime and package isolation. Different environments can hold different versions of the Python runtime together with any project or library dependencies. This helps avoid bugs and incompatibilities caused by upgrading a library for one project that "breaks" a dependency in a different one. -There are two major *virtual environment* tools in use today, the Python standard library [`venv`][venv] and the third-party [`conda env`][condaenv], using the [`conda`][conda] package manager and (_usually_) the Anaconda Python distribution. -Both of are straightforward to use and/or install. +There are many *virtual environment* and *virtual environment-like* tools in use today, and a growing movement to use [inline script metadata][PEP723], [dev containers][dev containers], or [docker][docker] instead of virtual environments. +It is important to try out different strategies and find one that suits your development style and project needs. +Don't assume that the strategies here are one-size-fits all, or are your only options. +For a more general rundown of tools not covered here, check out [this blog post by Bas Nijholt][bas nihholt] and stay curious. -Additionally, [`PyEnv`][pyenv] and [virtualenvwrapper][virtualenvwrapper] are tools that can help to manage multiple versions of Python and multiple Python environments on the same machine. + +There are three major *virtual environment* tools we'll cover here, the Python standard library [`venv`][venv], the third-party [`conda env`][condaenv], using the [`conda`][conda] package manager and (_usually_) the Anaconda Python distribution, and the third-party `pip` + `venv` replacement [`uv`][uv]. +All three are fairly straightforward to use and/or install. + +Additionally, [`PyEnv`][pyenv], [`Poetry`][poetry], and [virtualenvwrapper][virtualenvwrapper] are tools that can help to manage multiple versions of Python and multiple Python environments on the same machine.
@@ -205,6 +215,19 @@ Executing transaction: done
+### UV + +Working on Projects with uv: [uv][uv-docs] + +`uv` is a Python package + project management tool from [Astral][astral] written in [Rust][rust programming language]. +It was designed to replace `pip`, `venv`, `poetry`, and other Python packaging/dependency management tools. +Unlike `Conda` or `Mamba`, uv is specialized/tailored to only Python. + However, uv does support building Python extension modules written in other languages such as Fortran. +Astral is also the developer of the popular [Ruff][ruff] linter and [TY][ty], a static type checker for Python type hints, both written in Rust. + + +Rather than go into detail here, the [UV documentation][uv documentation] is your best source for installation and setup. + ### Virtual Environment wrapper Documents and background: [virtualenvwrapper][virtualenvwrapper]. @@ -366,14 +389,19 @@ You can also [develop plugins][sublime plugin development] of your own for the e [Installing Python locally]: https://exercism.org/docs/tracks/Python/installation [MS Python extension]: https://marketplace.visualstudio.com/items?itemName=ms-python.python +[PEP723]: https://packaging.python.org/en/latest/specifications/inline-script-metadata/#inline-script-metadata [anaconda]: https://www.anaconda.com/products/individual +[astral]: https://astral.sh/about [atom]: https://atom.io/ +[bas nihholt]: https://www.nijho.lt/post/python-environments/ [conda command ref]: https://docs.conda.io/projects/conda/en/latest/commands.html#conda-vs-pip-vs-virtualenv-commands [conda-cheatsheet]: https://docs.conda.io/projects/conda/en/latest/_downloads/843d9e0198f2a193a3484886fa28163c/conda-cheatsheet.pdf [conda-docs]: https://docs.conda.io/projects/conda/en/latest/user-guide/index.html [conda]: https://docs.conda.io/projects/conda/en/latest/index.html [condaenv]: https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html +[dev containers]: https://andypickup.com/developing-in-python-with-dev-containers-part-1-setup-f1aeb89cbfed [docker in vscode]: https://code.visualstudio.com/docs/containers/overview +[docker]: https://www.docker.com/blog/containerized-python-development-part-1/ [emacs at fullstack python]: https://www.fullstackpython.com/emacs.html [emacs setup at real python]: https://realpython.com/emacs-the-best-python-editor [emacs wiki python programming]: https://www.emacswiki.org/emacs/PythonProgrammingInEmacs#h5o-4 @@ -387,6 +415,7 @@ You can also [develop plugins][sublime plugin development] of your own for the e [linting python in vscode]: https://code.visualstudio.com/docs/python/linting [miniconda]: https://docs.conda.io/en/latest/miniconda.html [opensource spacemacs guide]: https://opensource.com/article/19/12/spacemacs +[poetry]: https://github.com/python-poetry/poetry [pycharm config venv]: https://www.jetbrains.com/help/pycharm/creating-virtual-environment.html [pycharm database tools]: https://www.jetbrains.com/help/pycharm/relational-databases.html [pycharm debug configuration]: https://www.jetbrains.com/help/pycharm/run-debug-configuration-py-test.html @@ -404,6 +433,8 @@ You can also [develop plugins][sublime plugin development] of your own for the e [python testing in vscode]: https://code.visualstudio.com/docs/python/testing [python web dev in vscode]: https://code.visualstudio.com/docs/python/tutorial-django [rtorr vim cheat sheet]: https://vim.rtorr.com/ +[ruff]: https://docs.astral.sh/ruff/ +[rust programming language]: https://rust-lang.org/ [spacemacs github repo]: https://github.com/syl20bnr/spacemacs [spacemacs official docs]: https://github.com/syl20bnr/spacemacs#documentation [spacemacs python layer]: https://www.spacemacs.org/layers/+lang/python/README.html @@ -431,6 +462,10 @@ You can also [develop plugins][sublime plugin development] of your own for the e [sublime support docs]: https://www.sublimetext.com/support [sublime text 4]: https://www.sublimetext.com/ [sublime text at real python]: https://realpython.com/setting-up-sublime-text-3-for-full-stack-python-development/ +[ty]: https://docs.astral.sh/ty/ +[uv documentation]: https://docs.astral.sh/uv/getting-started/ +[uv-docs]: https://docs.astral.sh/uv/guides/projects/#working-on-projects +[uv]: https://github.com/astral-sh/uv [venv wrapper tutorial]: https://virtualenvwrapper.readthedocs.io/en/latest/plugins.html#plugins [venv]: https://docs.python.org/3.9/tutorial/venv.html [vim cheat sheet]: https://vimsheet.com/ diff --git a/exercises/concept/black-jack/.docs/instructions.md b/exercises/concept/black-jack/.docs/instructions.md index e95c5fadb9f..f294d4744ef 100644 --- a/exercises/concept/black-jack/.docs/instructions.md +++ b/exercises/concept/black-jack/.docs/instructions.md @@ -99,7 +99,7 @@ False ## 5. Splitting pairs -If the players first two cards are of the same value, such as two sixes, or a `Q` and `K` a player may choose to treat them as two separate hands. +If the first two cards in a hand are of the same value (_for example, two sixes or a `Q` and `K`_), a player may choose to treat them as two separate hands. This is known as "splitting pairs". Define the `can_split_pairs(, )` function with parameters `card_one` and `card_two`, which are a pair of cards. diff --git a/exercises/concept/black-jack/.meta/exemplar.py b/exercises/concept/black-jack/.meta/exemplar.py index 27c7a5dc07e..e30df633169 100644 --- a/exercises/concept/black-jack/.meta/exemplar.py +++ b/exercises/concept/black-jack/.meta/exemplar.py @@ -8,12 +8,15 @@ def value_of_card(card): """Determine the scoring value of a card. - :param card: str - given card. - :return: int - value of a given card. See below for values. + Parameters: + card (str): The given card. - 1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10 - 2. 'A' (ace card) = 1 - 3. '2' - '10' = numerical value. + Returns: + int: The value of a given card. See below for values. + + 1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10 + 2. 'A' (ace card) = 1 + 3. '2' - '10' = numerical value. """ if card in ('JQK'): @@ -31,12 +34,16 @@ def value_of_card(card): def higher_card(card_one, card_two): """Determine which card has a higher value in the hand. - :param card_one, card_two: str - cards dealt in hand. See below for values. - :return: str or tuple - resulting Tuple contains both cards if they are of equal value. + Parameters: + card_one (str): First card dealt in the hand. See below for values. + card_two (str): Second card dealt in the hand. See below for values. + + 1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10 + 2. 'A' (ace card) = 1 + 3. '2' - '10' = numerical value. - 1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10 - 2. 'A' (ace card) = 1 - 3. '2' - '10' = numerical value. + Returns: + str or tuple: The resulting tuple contains both cards if they are of equal value. """ card_one_value = value_of_card(card_one) @@ -55,14 +62,18 @@ def higher_card(card_one, card_two): def value_of_ace(card_one, card_two): - """Calculate the most advantageous value for the ace card. + """Calculate the most advantageous value for an upcoming ace card. + + Parameters: + card_one (str): First card dealt in the hand. See below for values. + card_two (str): Second card dealt in the hand. See below for values. - :param card_one, card_two: str - card dealt. See below for values. - :return: int - either 1 or 11 value of the upcoming ace card. + 1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10 + 2. 'A' (ace card) = 11 (if already in hand) + 3. '2' - '10' = numerical value. - 1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10 - 2. 'A' (ace card) = 11 (if already in hand) - 3. '2' - '10' = numerical value. + Returns: + int: Either 1 or 11, which is the value of the upcoming ace card. """ card_one_value = 11 if card_one == 'A' else value_of_card(card_one) @@ -76,12 +87,16 @@ def value_of_ace(card_one, card_two): def is_blackjack(card_one, card_two): """Determine if the hand is a 'natural' or 'blackjack'. - :param card_one, card_two: str - card dealt. See below for values. - :return: bool - is the hand is a blackjack (two cards worth 21). + Parameters: + card_one (str): First card dealt in the hand. See below for values. + card_two (str): Second card dealt in the hand. See below for values. - 1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10 - 2. 'A' (ace card) = 11 (if already in hand) - 3. '2' - '10' = numerical value. + 1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10 + 2. 'A' (ace card) = 11 (if already in hand) + 3. '2' - '10' = numerical value. + + Returns: + bool: Is the hand is a blackjack (two cards worth 21). """ return (card_one == 'A' or card_two == 'A') and (value_of_card(card_one) == 10 or value_of_card(card_two) == 10) @@ -90,8 +105,12 @@ def is_blackjack(card_one, card_two): def can_split_pairs(card_one, card_two): """Determine if a player can split their hand into two hands. - :param card_one, card_two: str - cards dealt. - :return: bool - can the hand be split into two pairs? (i.e. cards are of the same value). + Parameters: + card_one (str): First card in the hand. + card_two (str): Second card in the hand. + + Returns: + bool: Can the hand be split into two pairs? (i.e. cards are of the same value). """ return value_of_card(card_one) == value_of_card(card_two) @@ -100,8 +119,12 @@ def can_split_pairs(card_one, card_two): def can_double_down(card_one, card_two): """Determine if a blackjack player can place a double down bet. - :param card_one, card_two: str - first and second cards in hand. - :return: bool - can the hand can be doubled down? (i.e. totals 9, 10 or 11 points). + Parameters: + card_one (str): First card in the hand. + card_two (str): Second card in the hand. + + Returns: + bool: Can the hand can be doubled down? (i.e. totals 9, 10 or 11 points). """ return 8 < value_of_card(card_one) + value_of_card(card_two) < 12 diff --git a/exercises/concept/black-jack/black_jack.py b/exercises/concept/black-jack/black_jack.py index 9ce6ca5ba4d..25cd61f9073 100644 --- a/exercises/concept/black-jack/black_jack.py +++ b/exercises/concept/black-jack/black_jack.py @@ -8,12 +8,15 @@ def value_of_card(card): """Determine the scoring value of a card. - :param card: str - given card. - :return: int - value of a given card. See below for values. + Parameters: + card (str): The given card. - 1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10 - 2. 'A' (ace card) = 1 - 3. '2' - '10' = numerical value. + Returns: + int: The value of a given card. See below for values. + + 1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10 + 2. 'A' (ace card) = 1 + 3. '2' - '10' = numerical value. """ pass @@ -22,26 +25,34 @@ def value_of_card(card): def higher_card(card_one, card_two): """Determine which card has a higher value in the hand. - :param card_one, card_two: str - cards dealt in hand. See below for values. - :return: str or tuple - resulting Tuple contains both cards if they are of equal value. + Parameters: + card_one (str): First card dealt in the hand. See below for values. + card_two (str): Second card dealt in the hand. See below for values. + + 1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10 + 2. 'A' (ace card) = 1 + 3. '2' - '10' = numerical value. - 1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10 - 2. 'A' (ace card) = 1 - 3. '2' - '10' = numerical value. + Returns: + str or tuple: The resulting tuple contains both cards if they are of equal value. """ pass def value_of_ace(card_one, card_two): - """Calculate the most advantageous value for the ace card. + """Calculate the most advantageous value for an upcoming ace card. + + Parameters: + card_one (str): First card dealt in the hand. See below for values. + card_two (str): Second card dealt in the hand. See below for values. - :param card_one, card_two: str - card dealt. See below for values. - :return: int - either 1 or 11 value of the upcoming ace card. + 1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10 + 2. 'A' (ace card) = 11 (if already in hand) + 3. '2' - '10' = numerical value. - 1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10 - 2. 'A' (ace card) = 11 (if already in hand) - 3. '2' - '10' = numerical value. + Returns: + int: Either 1 or 11, which is the value of the upcoming ace card. """ pass @@ -50,12 +61,16 @@ def value_of_ace(card_one, card_two): def is_blackjack(card_one, card_two): """Determine if the hand is a 'natural' or 'blackjack'. - :param card_one, card_two: str - card dealt. See below for values. - :return: bool - is the hand is a blackjack (two cards worth 21). + Parameters: + card_one (str): First card dealt in the hand. See below for values. + card_two (str): Second card dealt in the hand. See below for values. - 1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10 - 2. 'A' (ace card) = 11 (if already in hand) - 3. '2' - '10' = numerical value. + 1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10 + 2. 'A' (ace card) = 11 (if already in hand) + 3. '2' - '10' = numerical value. + + Returns: + bool: Is the hand is a blackjack (two cards worth 21). """ pass @@ -64,8 +79,12 @@ def is_blackjack(card_one, card_two): def can_split_pairs(card_one, card_two): """Determine if a player can split their hand into two hands. - :param card_one, card_two: str - cards dealt. - :return: bool - can the hand be split into two pairs? (i.e. cards are of the same value). + Parameters: + card_one (str): First card in the hand. + card_two (str): Second card in the hand. + + Returns: + bool: Can the hand be split into two pairs? (i.e. cards are of the same value). """ pass @@ -74,8 +93,12 @@ def can_split_pairs(card_one, card_two): def can_double_down(card_one, card_two): """Determine if a blackjack player can place a double down bet. - :param card_one, card_two: str - first and second cards in hand. - :return: bool - can the hand can be doubled down? (i.e. totals 9, 10 or 11 points). + Parameters: + card_one (str): First card in the hand. + card_two (str): Second card in the hand. + + Returns: + bool: Can the hand can be doubled down? (i.e. totals 9, 10 or 11 points). """ pass diff --git a/exercises/concept/card-games/.meta/exemplar.py b/exercises/concept/card-games/.meta/exemplar.py index d6531f01865..bb108e9ecc3 100644 --- a/exercises/concept/card-games/.meta/exemplar.py +++ b/exercises/concept/card-games/.meta/exemplar.py @@ -7,8 +7,11 @@ def get_rounds(number): """Create a list containing the current and next two round numbers. - :param number: int - current round number. - :return: list - current round and the two that follow. + Parameters: + number (int): The current round number. + + Returns: + list: The current round number and the two that follow. """ return [number, number + 1, number + 2] @@ -17,9 +20,12 @@ def get_rounds(number): def concatenate_rounds(rounds_1, rounds_2): """Concatenate two lists of round numbers. - :param rounds_1: list - first rounds played. - :param rounds_2: list - second set of rounds played. - :return: list - all rounds played. + Parameters: + rounds_1 (list): The first rounds played. + rounds_2 (list): The second group of rounds played. + + Returns: + list: All rounds played. """ return rounds_1 + rounds_2 @@ -28,9 +34,12 @@ def concatenate_rounds(rounds_1, rounds_2): def list_contains_round(rounds, number): """Check if the list of rounds contains the specified number. - :param rounds: list - rounds played. - :param number: int - round number. - :return: bool - was the round played? + Parameters: + rounds (list): The rounds played. + number (int): The round number. + + Returns: + bool: Was the round played? """ return number in rounds @@ -39,8 +48,11 @@ def list_contains_round(rounds, number): def card_average(hand): """Calculate and returns the average card value from the list. - :param hand: list - cards in hand. - :return: float - average value of the cards in the hand. + Parameters: + hand (list): The cards in the hand. + + Returns: + float: The average value of the cards in the hand. """ return sum(hand) / len(hand) @@ -49,8 +61,11 @@ def card_average(hand): def approx_average_is_average(hand): """Return if the (average of first and last card values) OR ('middle' card) == calculated average. - :param hand: list - cards in hand. - :return: bool - does one of the approximate averages equal the `true average`? + Parameters: + hand (list): The cards in the hand. + + Returns: + bool: Does one of the approximate averages equal the `true average`? """ real_average = card_average(hand) @@ -68,8 +83,11 @@ def approx_average_is_average(hand): def average_even_is_average_odd(hand): """Return if the (average of even indexed card values) == (average of odd indexed card values). - :param hand: list - cards in hand. - :return: bool - are even and odd averages equal? + Parameters: + hand (list): The cards in the hand. + + Returns: + bool: Are the even and odd averages equal? """ return card_average(hand[::2]) == card_average(hand[1::2]) @@ -78,8 +96,11 @@ def average_even_is_average_odd(hand): def maybe_double_last(hand): """Multiply a Jack card value in the last index position by 2. - :param hand: list - cards in hand. - :return: list - hand with Jacks (if present) value doubled. + Parameters: + hand (list): The cards in the hand. + + Returns: + list: The hand with Jacks (if present) value doubled. """ if hand[-1] == 11: diff --git a/exercises/concept/card-games/lists.py b/exercises/concept/card-games/lists.py index 03fb417330a..56c85a3366b 100644 --- a/exercises/concept/card-games/lists.py +++ b/exercises/concept/card-games/lists.py @@ -7,8 +7,11 @@ def get_rounds(number): """Create a list containing the current and next two round numbers. - :param number: int - current round number. - :return: list - current round and the two that follow. + Parameters: + number (int): The current round number. + + Returns: + list: The current round number and the two that follow. """ pass @@ -17,9 +20,12 @@ def get_rounds(number): def concatenate_rounds(rounds_1, rounds_2): """Concatenate two lists of round numbers. - :param rounds_1: list - first rounds played. - :param rounds_2: list - second set of rounds played. - :return: list - all rounds played. + Parameters: + rounds_1 (list): The first rounds played. + rounds_2 (list): The second group of rounds played. + + Returns: + list: All rounds played. """ pass @@ -28,9 +34,12 @@ def concatenate_rounds(rounds_1, rounds_2): def list_contains_round(rounds, number): """Check if the list of rounds contains the specified number. - :param rounds: list - rounds played. - :param number: int - round number. - :return: bool - was the round played? + Parameters: + rounds (list): The rounds played. + number (int): The round number. + + Returns: + bool: Was the round played? """ pass @@ -39,8 +48,11 @@ def list_contains_round(rounds, number): def card_average(hand): """Calculate and returns the average card value from the list. - :param hand: list - cards in hand. - :return: float - average value of the cards in the hand. + Parameters: + hand (list): The cards in the hand. + + Returns: + float: The average value of the cards in the hand. """ pass @@ -49,8 +61,11 @@ def card_average(hand): def approx_average_is_average(hand): """Return if the (average of first and last card values) OR ('middle' card) == calculated average. - :param hand: list - cards in hand. - :return: bool - does one of the approximate averages equal the `true average`? + Parameters: + hand (list): The cards in the hand. + + Returns: + bool: Does one of the approximate averages equal the `true average`? """ pass @@ -59,8 +74,11 @@ def approx_average_is_average(hand): def average_even_is_average_odd(hand): """Return if the (average of even indexed card values) == (average of odd indexed card values). - :param hand: list - cards in hand. - :return: bool - are even and odd averages equal? + Parameters: + hand (list): The cards in the hand. + + Returns: + bool: Are the even and odd averages equal? """ pass @@ -69,8 +87,11 @@ def average_even_is_average_odd(hand): def maybe_double_last(hand): """Multiply a Jack card value in the last index position by 2. - :param hand: list - cards in hand. - :return: list - hand with Jacks (if present) value doubled. + Parameters: + hand (list): The cards in the hand. + + Returns: + list: The hand with Jacks (if present) value doubled. """ pass diff --git a/exercises/concept/cater-waiter/.docs/hints.md b/exercises/concept/cater-waiter/.docs/hints.md index 89f51753bca..c8b5c0badef 100644 --- a/exercises/concept/cater-waiter/.docs/hints.md +++ b/exercises/concept/cater-waiter/.docs/hints.md @@ -10,52 +10,52 @@ ## 1. Clean up Dish Ingredients -- The `set()` constructor can take any [iterable][iterable] as an argument. [lists:python/lists](https://exercism.lol/tracks/python/concepts/lists) are iterable. -- Remember: [tuples:python/tuples](https://exercism.lol/tracks/python/concepts/tuples) can be formed using `(, )` or via the `tuple()` constructor. +- The `set()` constructor can take any [iterable][iterable] as an argument. [concept: lists](/tracks/python/concepts/lists) are iterable. +- Remember: [concept: tuples](/tracks/python/concepts/tuples) can be formed using `(, )` or via the `tuple()` constructor. ## 2. Cocktails and Mocktails - A `set` is _disjoint_ from another set if the two sets share no elements. -- The `set()` constructor can take any [iterable][iterable] as an argument. [lists:python/lists](https://exercism.lol/tracks/python/concepts/lists) are iterable. -- In Python, [strings:python/strings](https://exercism.lol/tracks/python/concepts/strings) can be concatenated with the `+` sign. +- The `set()` constructor can take any [iterable][iterable] as an argument. [concept: lists](/tracks/python/concepts/lists) are iterable. +- In Python, [concept: strings](/tracks/python/concepts/strings) can be concatenated with the `+` sign. ## 3. Categorize Dishes -- Using [loops:python/loops](https://exercism.lol/tracks/python/concepts/loops) to iterate through the available meal categories might be useful here. +- Using [concept: loops](/tracks/python/concepts/loops) to iterate through the available meal categories might be useful here. - If all the elements of `` are contained within ``, then ` <= `. - The method equivalent of `<=` is `.issubset()` -- [tuples:python/tuples](https://exercism.lol/tracks/python/concepts/tuples) can contain any data type, including other tuples. Tuples can be formed using `(, )` or via the `tuple()` constructor. -- Elements within [tuples:python/tuples](https://exercism.lol/tracks/python/concepts/tuples) can be accessed from the left using a 0-based index number, or from the right using a -1-based index number. -- The `set()` constructor can take any [iterable][iterable] as an argument. [lists:python/lists](https://exercism.lol/tracks/python/concepts/lists) are iterable. -- [strings:python/strings](https://exercism.lol/tracks/python/concepts/strings) can be concatenated with the `+` sign. +- [concept: tuples](/tracks/python/concepts/tuples) can contain any data type, including other tuples. Tuples can be formed using `(, )` or via the `tuple()` constructor. +- Elements within [concept: tuples](/tracks/python/concepts/tuples) can be accessed from the left using a 0-based index number, or from the right using a -1-based index number. +- The `set()` constructor can take any [iterable][iterable] as an argument. [concept: lists](/tracks/python/concepts/lists) are iterable. +- [concept: strings](/tracks/python/concepts/strings) can be concatenated with the `+` sign. ## 4. Label Allergens and Restricted Foods - A set _intersection_ are the elements shared between `` and ``. - The set method equivalent of `&` is `.intersection()` -- Elements within [tuples:python/tuples](https://exercism.lol/tracks/python/concepts/tuples) can be accessed from the left using a 0-based index number, or from the right using a -1-based index number. -- The `set()` constructor can take any [iterable][iterable] as an argument. [lists:python/lists](https://exercism.lol/tracks/python/concepts/lists) are iterable. -- [tuples:python/tuples](https://exercism.lol/tracks/python/concepts/tuples) can be formed using `(, )` or via the `tuple()` constructor. +- Elements within [concept: tuples](/tracks/python/concepts/tuples) can be accessed from the left using a 0-based index number, or from the right using a -1-based index number. +- The `set()` constructor can take any [iterable][iterable] as an argument. [concept: lists](/tracks/python/concepts/lists) are iterable. +- [concept: tuples](/tracks/python/concepts/tuples) can be formed using `(, )` or via the `tuple()` constructor. ## 5. Compile a "Master List" of Ingredients - A set _union_ is where ` and `` are combined into a single `set` - The set method equivalent of `|` is `.union()` -- Using [loops:python/loops](https://exercism.lol/tracks/python/concepts/loops) to iterate through the various dishes might be useful here. +- Using [concept: loops](/tracks/python/concepts/loops) to iterate through the various dishes might be useful here. ## 6. Pull out Appetizers for Passing on Trays - A set _difference_ is where the elements of `` are removed from ``, e.g. ` - `. - The set method equivalent of `-` is `.difference()` -- The `set()` constructor can take any [iterable][iterable] as an argument. [lists:python/lists](https://exercism.lol/tracks/python/concepts/lists) are iterable. -- The [list:python/lists](https://exercism.lol/tracks/python/concepts/lists) constructor can take any [iterable][iterable] as an argument. Sets are iterable. +- The `set()` constructor can take any [iterable][iterable] as an argument. [concept: lists](/tracks/python/concepts/lists) are iterable. +- The [concept: list](/tracks/python/concepts/lists) constructor can take any [iterable][iterable] as an argument. Sets are iterable. ## 7. Find Ingredients Used in Only One Recipe - A set _symmetric difference_ is where elements appear in `` or ``, but not **_both_** sets. - A set _symmetric difference_ is the same as subtracting the `set` _intersection_ from the `set` _union_, e.g. `( | ) - ( & )` - A _symmetric difference_ of more than two `sets` will include elements that are repeated more than two times across the input `sets`. To remove these cross-set repeated elements, the _intersections_ between set pairs needs to be subtracted from the symmetric difference. -- Using [loops:python/loops](https://exercism.lol/tracks/python/concepts/loops) to iterate through the various dishes might be useful here. +- Using [concept: loops](/tracks/python/concepts/loops) to iterate through the various dishes might be useful here. [hashable]: https://docs.python.org/3.7/glossary.html#term-hashable diff --git a/exercises/concept/cater-waiter/.docs/instructions.md b/exercises/concept/cater-waiter/.docs/instructions.md index 696883c3c93..4ed51a444d5 100644 --- a/exercises/concept/cater-waiter/.docs/instructions.md +++ b/exercises/concept/cater-waiter/.docs/instructions.md @@ -4,7 +4,7 @@ You and your business partners operate a small catering company. You've just agr ## 1. Clean up Dish Ingredients -The event recipes were added from various sources and their ingredients appear to have duplicate (_or more_) entries -- you don't want to end up purchasing excess items! +The event recipes were added from various sources and their ingredients appear to have duplicate (_or more_) entries โ€” you don't want to end up purchasing excess items! Before the shopping and cooking can commence, each dish's ingredient list needs to be "cleaned". Implement the `clean_ingredients(, )` function that takes the name of a dish and a `list` of ingredients. @@ -41,10 +41,11 @@ Implement the `check_drinks(, )` function that ta ## 3. Categorize Dishes The guest list includes diners with different dietary needs, and your staff will need to separate the dishes into Vegan, Vegetarian, Paleo, Keto, and Omnivore. +A dish belongs to a category only if all of its ingredients appear in the category's ingredient set. Implement the `categorize_dish(, )` function that takes a dish name and a `set` of that dish's ingredients. The function should return a string with the `dish name: ` (_which meal category the dish belongs to_). -All dishes will "fit" into one of the categories imported from `sets_categories_data.py` (VEGAN, VEGETARIAN, PALEO, KETO, or OMNIVORE). +All dishes given will "fit" into one of the categories imported from `sets_categories_data.py` (VEGAN, VEGETARIAN, PALEO, KETO, or OMNIVORE). ```python >>> from sets_categories_data import VEGAN, VEGETARIAN, PALEO, KETO, OMNIVORE @@ -138,5 +139,5 @@ from sets_categories_data import example_dishes, EXAMPLE_INTERSECTION >>> singleton_ingredients(example_dishes, EXAMPLE_INTERSECTION) ... -{'vegetable oil', 'vegetable stock', 'barley malt', 'tofu', 'fresh basil', 'lemon', 'ginger', 'honey', 'spaghetti', 'cornstarch', 'yeast', 'red onion', 'breadcrumbs', 'mixed herbs', 'garlic powder', 'celeriac', 'lemon zest', 'sunflower oil', 'mushrooms', 'silken tofu', 'smoked tofu', 'bell pepper', 'cashews', 'oregano', 'tomatoes', 'parsley', 'red pepper flakes', 'rosemary'} +{'garlic powder', 'sunflower oil', 'mixed herbs', 'cornstarch', 'celeriac', 'honey', 'mushrooms', 'bell pepper', 'rosemary', 'parsley', 'lemon', 'yeast', 'vegetable oil', 'vegetable stock', 'silken tofu', 'tofu', 'cashews', 'lemon zest', 'smoked tofu', 'spaghetti', 'ginger', 'breadcrumbs', 'tomatoes', 'barley malt', 'red pepper flakes', 'oregano', 'red onion', 'fresh basil'} ``` diff --git a/exercises/concept/cater-waiter/.docs/introduction.md b/exercises/concept/cater-waiter/.docs/introduction.md index 94a0b4c7669..dd5cc106ea9 100644 --- a/exercises/concept/cater-waiter/.docs/introduction.md +++ b/exercises/concept/cater-waiter/.docs/introduction.md @@ -1,8 +1,9 @@ # Sets -A [`set`][type-set] is a _mutable_ and _unordered_ collection of [_hashable_][hashable] objects. -Set members must be distinct -- duplicate items are not allowed. -They can hold multiple different data types and even nested structures like a `tuple` of `tuples` -- as long as all elements can be _hashed_. + +A [set][type-set] is a _mutable_ and _unordered_ collection of [_hashable_][hashable] objects. +Set members must be distinct โ€” duplicate items are not allowed. +They can hold multiple different data types and even nested structures like a `tuple` of `tuples` โ€” as long as all elements can be _hashed_. Sets also come in an immutable [`frozensets`][type-frozenset] flavor. Sets are most commonly used to quickly remove duplicates from other data structures or item groupings. @@ -31,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!','ะŸั€ะธะฒั–ั‚!', 'ใ“ใ‚“ใซใกใฏ๏ผ', @@ -90,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 @@ -100,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 @@ -126,6 +127,27 @@ There is no operator equivalent: ```python +# Both mammals and additional_animals are lists. +>>> mammals = ['squirrel','dog','cat','cow', 'tiger', 'elephant'] +>>> additional_animals = ['pangolin', 'panda', 'parrot', + 'lemur', 'tiger', 'pangolin'] + +# Animals is a dict. +>>> animals = {'chicken': 'white', + 'sparrow': 'grey', + 'eagle': 'brown and white', + 'albatross': 'grey and white', + 'crow': 'black', + 'elephant': 'grey', + 'dog': 'rust', + 'cow': 'black and white', + 'tiger': 'orange and black', + 'cat': 'grey', + 'squirrel': 'black'} + +# Birds is a set. +>>> birds = {'crow','sparrow','eagle','chicken', 'albatross'} + # Mammals and birds don't share any elements. >>> birds.isdisjoint(mammals) True @@ -236,7 +258,7 @@ The operator version of this method is ` & & & .. >>> herbs = ['Annatto','Asafetida','Basil','Chervil','Cilantro', 'Curry Leaf','Fennel','Kaffir Lime','Lavender', - 'Marjoram','Mint','Oregano','Summer Savory' + 'Marjoram','Mint','Oregano','Summer Savory', 'Tarragon','Wild Bergamot','Wild Celery', 'Winter Savory'] @@ -311,7 +333,7 @@ The operator version of this method is ` - - - 'Goose Berries', 'Strawberries'} # Operators require sets. ->>> berries_and_veggies - just_berries +>>> berries_and_veggies - berries {'Artichokes','Asparagus','Broccoli','Kale', 'Ramps','Rhubarb','Walking Onions','Watercress'} ``` @@ -338,8 +360,8 @@ The operator version of this method is ` ^ `: >>> fruit_and_flowers ^ plants_1 {'๐ŸŒฒ', '๐ŸŒธ', '๐ŸŒด', '๐ŸŒต','๐ŸŒบ', '๐ŸŒป'} ->>> fruit_and_flowers ^ plants_2 -{ '๐Ÿฅ‘', '๐ŸŒด','๐ŸŒฒ', '๐ŸŒต', '๐Ÿˆ', '๐Ÿฅญ'} +>>> fruit_and_flowers ^ set(plants_2) +{'๐Ÿฅญ', '๐ŸŒด', '๐ŸŒต', '๐Ÿˆ', '๐ŸŒฒ', '๐Ÿฅ‘'} ``` ~~~~exercism/note diff --git a/exercises/concept/cater-waiter/.meta/exemplar.py b/exercises/concept/cater-waiter/.meta/exemplar.py index 8f48c520f2e..b00cbef26a1 100644 --- a/exercises/concept/cater-waiter/.meta/exemplar.py +++ b/exercises/concept/cater-waiter/.meta/exemplar.py @@ -13,12 +13,16 @@ def clean_ingredients(dish_name, dish_ingredients): """Remove duplicates from `dish_ingredients`. - :param dish_name: str - containing the dish name. - :param dish_ingredients: list - dish ingredients. - :return: tuple - containing (dish_name, ingredient set). + Parameters: + dish_name (str): The name of the dish. + dish_ingredients (list): The ingredients for the dish. + + Returns: + tuple: Containing (dish name, ingredient set). This function should return a `tuple` with the name of the dish as the first item, followed by the de-duped `set` of ingredients as the second item. + """ return dish_name, set(dish_ingredients) @@ -27,12 +31,16 @@ def clean_ingredients(dish_name, dish_ingredients): def check_drinks(drink_name, drink_ingredients): """Append "Cocktail" (alcohol) or "Mocktail" (no alcohol) to `drink_name`, based on `drink_ingredients`. - :param drink_name: str - name of the drink. - :param drink_ingredients: list - ingredients in the drink. - :return: str - drink_name appended with "Mocktail" or "Cocktail". + Parameters: + drink_name (str): Name of the drink. + drink_ingredients (list): Ingredients in the drink. + + Returns: + str: `drink_name` appended with "Mocktail" or "Cocktail". The function should return the name of the drink followed by "Mocktail" (non-alcoholic) and drink name followed by "Cocktail" (includes alcohol). + """ if not ALCOHOLS.isdisjoint(drink_ingredients): @@ -44,13 +52,16 @@ def check_drinks(drink_name, drink_ingredients): def categorize_dish(dish_name, dish_ingredients): """Categorize `dish_name` based on `dish_ingredients`. - :param dish_name: str - dish to be categorized. - :param dish_ingredients: list - ingredients for the dish. - :return: str - the dish name appended with ": ". + Parameters: + dish_name (str): The dish to be categorized. + dish_ingredients (set): The ingredients for the dish. + + Returns: + str: The dish name appended with ": ". This function should return a string with the `dish name: ` (which meal category the dish belongs to). `` can be any one of (VEGAN, VEGETARIAN, PALEO, KETO, or OMNIVORE). - All dishes will "fit" into one of the categories imported from `sets_categories_data.py` + All dishes will "fit" into one of the categories imported from `sets_categories_data.py`. """ @@ -69,8 +80,11 @@ def categorize_dish(dish_name, dish_ingredients): def tag_special_ingredients(dish): """Compare `dish` ingredients to `SPECIAL_INGREDIENTS`. - :param dish: tuple - of (dish name, list of dish ingredients). - :return: tuple - containing (dish name, dish special ingredients). + Parameters: + dish (tuple): (dish name, list of dish ingredients). + + Returns: + tuple: Containing (dish name, dish special ingredients). Return the dish name followed by the `set` of ingredients that require a special note on the dish description. For the purposes of this exercise, all allergens or special ingredients that need to be tracked are in the @@ -81,12 +95,17 @@ def tag_special_ingredients(dish): def compile_ingredients(dishes): - """Create a master list of ingredients. + """Determine which `dishes` are designated `appetizers` and remove them. + + Parameters: + dishes (list): Group of dish names. + appetizers (list): Group of appetizer names. - :param dishes: list - of dish ingredient sets. - :return: set - of ingredients compiled from `dishes`. + Returns: + list: Group of dish names that do not appear on appetizer list. - This function should return a `set` of all ingredients from all listed dishes. + The function should return the list of dish names with appetizer names removed. + Either list could contain duplicates and may require de-duping. """ combined_ingredients = set() @@ -100,9 +119,12 @@ def compile_ingredients(dishes): def separate_appetizers(dishes, appetizers): """Determine which `dishes` are designated `appetizers` and remove them. - :param dishes: list - of dish names. - :param appetizers: list - of appetizer names. - :return: list - of dish names that do not appear on appetizer list. + Parameters: + dishes (list): Group of dish names. + appetizers (list): Group of appetizer names. + + Returns: + list: Group of dish names that do not appear on appetizer list. The function should return the list of dish names with appetizer names removed. Either list could contain duplicates and may require de-duping. @@ -112,15 +134,18 @@ def separate_appetizers(dishes, appetizers): def singleton_ingredients(dishes, intersection): - """Determine which `dishes` have a singleton ingredient (an ingredient that only appears once across dishes). + """Find singleton ingredients within the group of dishes (ingredients that only appear once across dishes). + + Parameters: + dishes (list): Group of ingredient sets. + intersection (set): Can be one of `_INTERSECTIONS` constants imported from `sets_categories_data.py`. - :param dishes: list - of ingredient sets. - :param intersection: constant - can be one of `_INTERSECTION` constants imported from `sets_categories_data.py`. - :return: set - containing singleton ingredients. + Returns: + set: Containing singleton ingredients. Each dish is represented by a `set` of its ingredients. - Each `_INTERSECTION` is an `intersection` of all dishes in the category. `` can be any one of: + Each `_INTERSECTIONS` is an `intersection` of all dishes in the category. `` can be any one of: (VEGAN, VEGETARIAN, PALEO, KETO, or OMNIVORE). The function should return a `set` of ingredients that only appear in a single dish. diff --git a/exercises/concept/cater-waiter/sets.py b/exercises/concept/cater-waiter/sets.py index b0202e6a5fb..a0c92f63b85 100644 --- a/exercises/concept/cater-waiter/sets.py +++ b/exercises/concept/cater-waiter/sets.py @@ -13,12 +13,16 @@ def clean_ingredients(dish_name, dish_ingredients): """Remove duplicates from `dish_ingredients`. - :param dish_name: str - containing the dish name. - :param dish_ingredients: list - dish ingredients. - :return: tuple - containing (dish_name, ingredient set). + Parameters: + dish_name (str): The name of the dish. + dish_ingredients (list): The ingredients for the dish. + + Returns: + tuple: Containing (dish name, ingredient set). This function should return a `tuple` with the name of the dish as the first item, followed by the de-duped `set` of ingredients as the second item. + """ pass @@ -27,9 +31,12 @@ def clean_ingredients(dish_name, dish_ingredients): def check_drinks(drink_name, drink_ingredients): """Append "Cocktail" (alcohol) or "Mocktail" (no alcohol) to `drink_name`, based on `drink_ingredients`. - :param drink_name: str - name of the drink. - :param drink_ingredients: list - ingredients in the drink. - :return: str - drink_name appended with "Mocktail" or "Cocktail". + Parameters: + drink_name (str): Name of the drink. + drink_ingredients (list): Ingredients in the drink. + + Returns: + str: `drink_name` appended with "Mocktail" or "Cocktail". The function should return the name of the drink followed by "Mocktail" (non-alcoholic) and drink name followed by "Cocktail" (includes alcohol). @@ -42,14 +49,16 @@ def check_drinks(drink_name, drink_ingredients): def categorize_dish(dish_name, dish_ingredients): """Categorize `dish_name` based on `dish_ingredients`. - :param dish_name: str - dish to be categorized. - :param dish_ingredients: list - ingredients for the dish. - :return: str - the dish name appended with ": ". + Parameters: + dish_name (str): The dish to be categorized. + dish_ingredients (set): The ingredients for the dish. + + Returns: + str: The dish name appended with ": ". This function should return a string with the `dish name: ` (which meal category the dish belongs to). `` can be any one of (VEGAN, VEGETARIAN, PALEO, KETO, or OMNIVORE). All dishes will "fit" into one of the categories imported from `sets_categories_data.py` - """ pass @@ -58,8 +67,11 @@ def categorize_dish(dish_name, dish_ingredients): def tag_special_ingredients(dish): """Compare `dish` ingredients to `SPECIAL_INGREDIENTS`. - :param dish: tuple - of (dish name, list of dish ingredients). - :return: tuple - containing (dish name, dish special ingredients). + Parameters: + dish (tuple): (dish name, list of dish ingredients). + + Returns: + tuple: Containing (dish name, dish special ingredients). Return the dish name followed by the `set` of ingredients that require a special note on the dish description. For the purposes of this exercise, all allergens or special ingredients that need to be tracked are in the @@ -72,8 +84,11 @@ def tag_special_ingredients(dish): def compile_ingredients(dishes): """Create a master list of ingredients. - :param dishes: list - of dish ingredient sets. - :return: set - of ingredients compiled from `dishes`. + Parameters: + dishes (list): Dish ingredient sets. + + Returns: + set: Ingredients compiled from `dishes`. This function should return a `set` of all ingredients from all listed dishes. """ @@ -84,9 +99,12 @@ def compile_ingredients(dishes): def separate_appetizers(dishes, appetizers): """Determine which `dishes` are designated `appetizers` and remove them. - :param dishes: list - of dish names. - :param appetizers: list - of appetizer names. - :return: list - of dish names that do not appear on appetizer list. + Parameters: + dishes (list): Group of dish names. + appetizers (list): Group of appetizer names. + + Returns: + list: Group of dish names that do not appear on appetizer list. The function should return the list of dish names with appetizer names removed. Either list could contain duplicates and may require de-duping. @@ -96,11 +114,14 @@ def separate_appetizers(dishes, appetizers): def singleton_ingredients(dishes, intersection): - """Determine which `dishes` have a singleton ingredient (an ingredient that only appears once across dishes). + """Find singleton ingredients within the group of dishes (ingredients that only appear once across dishes). + + Parameters: + dishes (list): Group of ingredient sets. + intersection (set): Can be one of `_INTERSECTIONS` constants imported from `sets_categories_data.py`. - :param dishes: list - of ingredient sets. - :param intersection: constant - can be one of `_INTERSECTIONS` constants imported from `sets_categories_data.py`. - :return: set - containing singleton ingredients. + Returns: + set: Containing singleton ingredients. Each dish is represented by a `set` of its ingredients. 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/.docs/introduction.md b/exercises/concept/chaitanas-colossal-coaster/.docs/introduction.md index 764de5fc54d..bfbf0537522 100644 --- a/exercises/concept/chaitanas-colossal-coaster/.docs/introduction.md +++ b/exercises/concept/chaitanas-colossal-coaster/.docs/introduction.md @@ -150,10 +150,16 @@ The `.reverse()` method will reverse the order of elements **in-place**. ``` -A list can be re-ordered _**in place**_ with the help of `.sort()`. - Internally, Python uses [`Timsort`][timsort] to arrange the list. - Default order is _ascending_ from the left. - The Python docs offer [additional tips and techniques for sorting][sorting how to] lists effectively. +A list can be re-ordered _**in place**_ with the help of [`.sort()`][sort]. +Default sort order is _ascending_ from the left. +The Python docs offer [additional tips and techniques for sorting][sorting how to]. + +~~~~exercism/note + From 2002 to 2022, Python used an algorithm called [`Timsort`][timsort] internally to arrange lists, but switched to [`Powersort`][powersort] from `Python 3.11` onward. + +[powersort]: https://www.wild-inter.net/publications/munro-wild-2018 +[timsort]: https://en.wikipedia.org/wiki/Timsort +~~~~ ```python @@ -233,7 +239,6 @@ ValueError: 10 is not in list 3 ``` - [common sequence operations]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations [dict]: https://docs.python.org/3/library/stdtypes.html#dict [list-methods]: https://docs.python.org/3/tutorial/datastructures.html#more-on-lists @@ -242,7 +247,7 @@ ValueError: 10 is not in list [sequence type]: https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range [set]: https://docs.python.org/3/library/stdtypes.html#set [slice notation]: https://docs.python.org/3/reference/expressions.html#slicings +[sort]: https://docs.python.org/3/library/stdtypes.html#list.sort [sorted]: https://docs.python.org/3/library/functions.html#sorted [sorting how to]: https://docs.python.org/3/howto/sorting.html -[timsort]: https://en.wikipedia.org/wiki/Timsort [tuple]: https://docs.python.org/3/library/stdtypes.html#tuple diff --git a/exercises/concept/chaitanas-colossal-coaster/.meta/exemplar.py b/exercises/concept/chaitanas-colossal-coaster/.meta/exemplar.py index 24a83949647..5d40f939b8e 100644 --- a/exercises/concept/chaitanas-colossal-coaster/.meta/exemplar.py +++ b/exercises/concept/chaitanas-colossal-coaster/.meta/exemplar.py @@ -4,11 +4,14 @@ def add_me_to_the_queue(express_queue, normal_queue, ticket_type, person_name): """Add a person to the 'express' or 'normal' queue depending on the ticket number. - :param express_queue: list - names in the Fast-track queue. - :param normal_queue: list - names in the normal queue. - :param ticket_type: int - type of ticket. 1 = express, 0 = normal. - :param person_name: str - name of person to add to a queue. - :return: list - the (updated) queue the name was added to. + Parameters: + express_queue (list): The names in the Fast-track queue. + normal_queue (list): The names in the normal queue. + ticket_type (int): Type of ticket. 1 = express, 0 = normal. + person_name (str): The name of person to add to a queue. + + Returns: + list: The (updated) queue the name was added to. """ result = express_queue if ticket_type == 1 else normal_queue @@ -19,9 +22,12 @@ def add_me_to_the_queue(express_queue, normal_queue, ticket_type, person_name): def find_my_friend(queue, friend_name): """Search the queue for a name and return their queue position (index). - :param queue: list - names in the queue. - :param friend_name: str - name of friend to find. - :return: int - index at which the friends name was found. + Parameters: + queue (list): The names in the queue. + friend_name (str): The name of friend to find. + + Returns: + int: The index at which the friends name was found. """ return queue.index(friend_name) @@ -30,10 +36,13 @@ def find_my_friend(queue, friend_name): def add_me_with_my_friends(queue, index, person_name): """Insert the late arrival's name at a specific index of the queue. - :param queue: list - names in the queue. - :param index: int - the index at which to add the new name. - :param person_name: str - the name to add. - :return: list - queue updated with new name. + Parameters: + queue (list): The names in the queue. + index (int): The index at which to add the new name. + person_name (str): The name to add. + + Returns: + list: The queue updated with new name. """ queue.insert(index, person_name) @@ -43,9 +52,12 @@ def add_me_with_my_friends(queue, index, person_name): def remove_the_mean_person(queue, person_name): """Remove the mean person from the queue by the provided name. - :param queue: list - names in the queue. - :param person_name: str - name of mean person. - :return: list - queue update with the mean persons name removed. + Parameters: + queue (list): The names in the queue. + person_name (str): The name of mean person. + + Returns: + list: The queue updated with the mean persons name removed. """ queue.remove(person_name) @@ -55,9 +67,12 @@ def remove_the_mean_person(queue, person_name): def how_many_namefellows(queue, person_name): """Count how many times the provided name appears in the queue. - :param queue: list - names in the queue. - :param person_name: str - name you wish to count or track. - :return: int - the number of times the name appears in the queue. + Parameters: + queue (list): The names in the queue. + person_name (str): The name you wish to count or track. + + Returns: + int: The number of times the name appears in the queue. """ return queue.count(person_name) @@ -66,8 +81,11 @@ def how_many_namefellows(queue, person_name): def remove_the_last_person(queue): """Remove the person in the last index from the queue and return their name. - :param queue: list - names in the queue. - :return: str - name that has been removed from the end of the queue. + Parameters: + queue (list): The names in the queue. + + Returns: + str: The name that has been removed from the end of the queue. """ return queue.pop() @@ -76,8 +94,11 @@ def remove_the_last_person(queue): def sorted_names(queue): """Sort the names in the queue in alphabetical order and return the result. - :param queue: list - names in the queue. - :return: list - copy of the queue in alphabetical order. + Parameters: + queue (list): The names in the queue. + + Returns: + list: A copy of the queue in alphabetical order. """ new_queue = queue[:] diff --git a/exercises/concept/chaitanas-colossal-coaster/list_methods.py b/exercises/concept/chaitanas-colossal-coaster/list_methods.py index 6603b5adf67..5a83088e3aa 100644 --- a/exercises/concept/chaitanas-colossal-coaster/list_methods.py +++ b/exercises/concept/chaitanas-colossal-coaster/list_methods.py @@ -4,11 +4,14 @@ def add_me_to_the_queue(express_queue, normal_queue, ticket_type, person_name): """Add a person to the 'express' or 'normal' queue depending on the ticket number. - :param express_queue: list - names in the Fast-track queue. - :param normal_queue: list - names in the normal queue. - :param ticket_type: int - type of ticket. 1 = express, 0 = normal. - :param person_name: str - name of person to add to a queue. - :return: list - the (updated) queue the name was added to. + Parameters: + express_queue (list): The names in the Fast-track queue. + normal_queue (list): The names in the normal queue. + ticket_type (int): Type of ticket. 1 = express, 0 = normal. + person_name (str): The name of person to add to a queue. + + Returns: + list: The (updated) queue the name was added to. """ pass @@ -17,9 +20,12 @@ def add_me_to_the_queue(express_queue, normal_queue, ticket_type, person_name): def find_my_friend(queue, friend_name): """Search the queue for a name and return their queue position (index). - :param queue: list - names in the queue. - :param friend_name: str - name of friend to find. - :return: int - index at which the friends name was found. + Parameters: + queue (list): The names in the queue. + friend_name (str): The name of friend to find. + + Returns: + int: The index at which the friends name was found. """ pass @@ -28,10 +34,13 @@ def find_my_friend(queue, friend_name): def add_me_with_my_friends(queue, index, person_name): """Insert the late arrival's name at a specific index of the queue. - :param queue: list - names in the queue. - :param index: int - the index at which to add the new name. - :param person_name: str - the name to add. - :return: list - queue updated with new name. + Parameters: + queue (list): The names in the queue. + index (int): The index at which to add the new name. + person_name (str): The name to add. + + Returns: + list: The queue updated with new name. """ pass @@ -40,9 +49,12 @@ def add_me_with_my_friends(queue, index, person_name): def remove_the_mean_person(queue, person_name): """Remove the mean person from the queue by the provided name. - :param queue: list - names in the queue. - :param person_name: str - name of mean person. - :return: list - queue update with the mean persons name removed. + Parameters: + queue (list): The names in the queue. + person_name (str): The name of mean person. + + Returns: + list: The queue updated with the mean persons name removed. """ pass @@ -51,9 +63,12 @@ def remove_the_mean_person(queue, person_name): def how_many_namefellows(queue, person_name): """Count how many times the provided name appears in the queue. - :param queue: list - names in the queue. - :param person_name: str - name you wish to count or track. - :return: int - the number of times the name appears in the queue. + Parameters: + queue (list): The names in the queue. + person_name (str): The name you wish to count or track. + + Returns: + int: The number of times the name appears in the queue. """ pass @@ -62,8 +77,11 @@ def how_many_namefellows(queue, person_name): def remove_the_last_person(queue): """Remove the person in the last index from the queue and return their name. - :param queue: list - names in the queue. - :return: str - name that has been removed from the end of the queue. + Parameters: + queue (list): The names in the queue. + + Returns: + str: The name that has been removed from the end of the queue. """ pass @@ -72,8 +90,11 @@ def remove_the_last_person(queue): def sorted_names(queue): """Sort the names in the queue in alphabetical order and return the result. - :param queue: list - names in the queue. - :return: list - copy of the queue in alphabetical order. + Parameters: + queue (list): The names in the queue. + + Returns: + list: A copy of the queue in alphabetical order. """ pass diff --git a/exercises/concept/chaitanas-colossal-coaster/list_methods_test.py b/exercises/concept/chaitanas-colossal-coaster/list_methods_test.py index c519ff255cd..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) - self.assertListEqual(actual_result, expected, msg=error_message) + 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.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): @@ -140,22 +248,69 @@ 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'] - actual_result = remove_the_last_person(test_data) - expected = 'Rocket' - error_message = (f'Called remove_the_last_person({test_data}).' - f'The function returned {actual_result}, but the tests ' - f'expected {expected} as the person who was removed.') + test_data = [ + (['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, (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'\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, queue), (expected_result, expected_queue), msg=error_message) - self.assertIs(actual_result, expected, 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) + 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(actual_result, expected_result, msg=error_message) + + @pytest.mark.task(taskno=7) + 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/currency-exchange/.docs/introduction.md b/exercises/concept/currency-exchange/.docs/introduction.md index c5d3d5caa66..dff50b17398 100644 --- a/exercises/concept/currency-exchange/.docs/introduction.md +++ b/exercises/concept/currency-exchange/.docs/introduction.md @@ -26,7 +26,7 @@ You can see more details and discussions in the following resources: ## Arithmetic -Python fully supports arithmetic between `ints` and `floats`. It will convert narrower numbers to match their less narrow counterparts when used with the binary arithmetic operators (`+`, `-`, `*`, `/`, `//`, and `%`). When division with `/`, `//` returns the quotient and `%` returns the remainder. +Python fully supports arithmetic between `ints` and `floats`. It will convert narrower numbers to match their less narrow counterparts when used with the binary arithmetic operators (`+`, `-`, `*`, `/`, `//`, and `%`). Python considers `ints` narrower than `floats`. So, using a float in an expression ensures the result will be a float too. However, when doing division, the result will always be a float, even if only integers are used. @@ -61,7 +61,7 @@ If an int result is needed, you can use `//` to truncate the result. 1 ``` -To convert a float to an integer, you can use `int()`. Also, to convert an integer to a float, you can use `float()`. +To convert a float to an integer, you can use `int()`. To convert an integer to a float, you can use `float()`. ```python >>> int(6 / 2) diff --git a/exercises/concept/currency-exchange/.meta/exemplar.py b/exercises/concept/currency-exchange/.meta/exemplar.py index 1cd9b2262ee..d835bce65c3 100644 --- a/exercises/concept/currency-exchange/.meta/exemplar.py +++ b/exercises/concept/currency-exchange/.meta/exemplar.py @@ -6,72 +6,157 @@ """ def exchange_money(budget, exchange_rate): - """ + """Calculate estimated value after exchange. + + Parameters: + budget (float): Tthe amount of money you are planning to exchange. + exchange_rate (float): The unit value of the foreign currency. + + Returns: + float: The exchanged value of the foreign currency you can receive. + + Examples: + >>> exchange_money(127.5, 1.2) + 106.25 + + >>> exchange_money(200, 1.10) + 181.82 + + This function calculates and returns the (estimated) value of the exchanged currency. - :param budget: float - amount of money you are planning to exchange. - :param exchange_rate: float - unit value of the foreign currency. - :return: float - exchanged value of the foreign currency you can receive. """ return budget / exchange_rate def get_change(budget, exchanging_value): - """ + """Calculate currency left after an exchange. + + Parameters: + budget (float): The amount of money you own. + exchanging_value (float): The amount of your money you want to exchange now. + + Returns: + float: The amount left of your starting currency after the exchange + + Examples: + >>> get_change(127.5, 120.0) + 7.5 + + >>> get_change(300.75, 150.25) + 150.50 + + This function calcultes and returns the amount of money left over from the budget + after an exchange. - :param budget: float - amount of money you own. - :param exchanging_value: float - amount of your money you want to exchange now. - :return: float - amount left of your starting currency after exchanging. """ return budget - exchanging_value def get_value_of_bills(denomination, number_of_bills): - """ + """Calculate the total value of currency at current denomination. + + Parameters: + denomination (int): The value of a single unit (bill). + number_of_bills (int): The total number of units (bills). + + Returns: + int: Calculated value of the units (bills). + + Examples: + >>> get_value_of_bills(5, 128) + 640 + + >>> get_value_of_bills(15.13, 16) + 242 + + This function calculates and returns the total value of the bills (excluding fractionaal amounts). - :param denomination: int - the value of a bill. - :param number_of_bills: int - total number of bills. - :return: int - calculated value of the bills. """ return denomination * number_of_bills def get_number_of_bills(amount, denomination): - """ + """Calculate the number of currency units (bills) within the amount. + + Parameters: + amount (float): The total starting value. + denomination (int): The value of a single unit (bill). + + Returns: + int: The number of units (bills) that can be obtained from the amount. + + Examples: + >>> get_number_of_bills(127.5, 5) + 25 + + >>> get_number_of_bills(35.16, 10) + 3 + + This function calcluates and returns the number pf currency units (bills) that can + be obtained from the given amount. Whole bills only - no fractioal amounts. - :param amount: float - the total starting value. - :param denomination: int - the value of a single bill. - :return: int - number of bills that can be obtained from the amount. """ return int(amount) // denomination def get_leftover_of_bills(amount, denomination): - """ + """Calculate leftover amount after exchanging into bills. + + Parameters: + amount (float): The total starting value. + denomination (int): The value of a single unit (bill). + + Returns: + float: The amount that is "leftover", given the current denomination. + + Examples: + >>> get_leftover_of_bills(127.5, 20) + 7.5 + + >>> get_leftover_of_bills(153.2, 10) + 3.20 + + This function calculates and returns the leftover amount that cannot be + returned from starting amount, due to the currency denomination. - :param amount: float - the total starting value. - :param denomination: int - the value of a single bill. - :return: float - the amount that is "leftover", given the current denomination. """ return amount % denomination def exchangeable_value(budget, exchange_rate, spread, denomination): - """ + """Calculate the maximum value of the new currency. + + Parameters: + budget (float): The amount of your money you are planning to exchange. + exchange_rate (float): The unit value of the foreign currency. + spread (int): The percentage that is taken as an exchange fee. + denomination (int) The value of a single unit (bill). + + Returns: + int: The maximum value you can get in the new currency. - :param budget: float - the amount of your money you are planning to exchange. - :param exchange_rate: float - the unit value of the foreign currency. - :param spread: int - percentage that is taken as an exchange fee. - :param denomination: int - the value of a single bill. - :return: int - maximum value you can get. + Examples: + >>> exchangeable_value(127.25, 1.20, 10, 20) + 80 + + >>> exchangeable_value(127.25, 1.20, 10, 5) + 95 + + Note: + The currency denomination is a whole number and cannot be sub-divided. + + This function calculates and returns the maximum value of the new currency after + determining the exchange rate plus the spread. """ exchange_fee = (exchange_rate / 100) * spread exchange_value = exchange_money(budget, exchange_rate + exchange_fee) number_of_bills = get_number_of_bills(exchange_value, denomination) value_of_bills = get_value_of_bills(denomination, number_of_bills) + return value_of_bills diff --git a/exercises/concept/currency-exchange/exchange.py b/exercises/concept/currency-exchange/exchange.py index b58df5cbe48..c135c33beb9 100644 --- a/exercises/concept/currency-exchange/exchange.py +++ b/exercises/concept/currency-exchange/exchange.py @@ -8,68 +8,152 @@ def exchange_money(budget, exchange_rate): - """ + """Calculate estimated value after exchange. + + Parameters: + budget (float): Tthe amount of money you are planning to exchange. + exchange_rate (float): The unit value of the foreign currency. + + Returns: + float: The exchanged value of the foreign currency you can receive. + + Examples: + >>> exchange_money(127.5, 1.2) + 106.25 + + >>> exchange_money(200, 1.10) + 181.82 + + This function calculates and returns the (estimated) value of the exchanged currency. - :param budget: float - amount of money you are planning to exchange. - :param exchange_rate: float - unit value of the foreign currency. - :return: float - exchanged value of the foreign currency you can receive. """ pass def get_change(budget, exchanging_value): - """ + """Calculate currency left after an exchange. + + Parameters: + budget (float): The amount of money you own. + exchanging_value (float): The amount of your money you want to exchange now. + + Returns: + float: The amount left of your starting currency after the exchange + + Examples: + >>> get_change(127.5, 120.0) + 7.5 + + >>> get_change(300.75, 150.25) + 150.50 + + This function calcultes and returns the amount of money left over from the budget + after an exchange. - :param budget: float - amount of money you own. - :param exchanging_value: float - amount of your money you want to exchange now. - :return: float - amount left of your starting currency after exchanging. """ pass def get_value_of_bills(denomination, number_of_bills): - """ + """Calculate the total value of currency at current denomination. + + Parameters: + denomination (int): The value of a single unit (bill). + number_of_bills (int): The total number of units (bills). + + Returns: + int: Calculated value of the units (bills). + + Examples: + >>> get_value_of_bills(5, 128) + 640 + + >>> get_value_of_bills(15.13, 16) + 242 + + This function calculates and returns the total value of the bills (excluding fractionaal amounts). - :param denomination: int - the value of a bill. - :param number_of_bills: int - total number of bills. - :return: int - calculated value of the bills. """ pass def get_number_of_bills(amount, denomination): - """ + """Calculate the number of currency units (bills) within the amount. + + Parameters: + amount (float): The total starting value. + denomination (int): The value of a single unit (bill). + + Returns: + int: The number of units (bills) that can be obtained from the amount. + + Examples: + >>> get_number_of_bills(127.5, 5) + 25 + + >>> get_number_of_bills(35.16, 10) + 3 + + This function calcluates and returns the number pf currency units (bills) that can + be obtained from the given amount. Whole bills only - no fractioal amounts. - :param amount: float - the total starting value. - :param denomination: int - the value of a single bill. - :return: int - number of bills that can be obtained from the amount. """ pass def get_leftover_of_bills(amount, denomination): - """ + """Calculate leftover amount after exchanging into bills. + + Parameters: + amount (float): The total starting value. + denomination (int): The value of a single unit (bill). + + Returns: + float: The amount that is "leftover", given the current denomination. + + Examples: + >>> get_leftover_of_bills(127.5, 20) + 7.5 + + >>> get_leftover_of_bills(153.2, 10) + 3.20 + + This function calculates and returns the leftover amount that cannot be + returned from starting amount, due to the currency denomination. - :param amount: float - the total starting value. - :param denomination: int - the value of a single bill. - :return: float - the amount that is "leftover", given the current denomination. """ pass def exchangeable_value(budget, exchange_rate, spread, denomination): - """ + """Calculate the maximum value of the new currency. + + Parameters: + budget (float): The amount of your money you are planning to exchange. + exchange_rate (float): The unit value of the foreign currency. + spread (int): The percentage that is taken as an exchange fee. + denomination (int) The value of a single unit (bill). + + Returns: + int: The maximum value you can get in the new currency. + + Examples: + >>> exchangeable_value(127.25, 1.20, 10, 20) + 80 + + >>> exchangeable_value(127.25, 1.20, 10, 5) + 95 + + Note: + The currency denomination is a whole number and cannot be sub-divided. - :param budget: float - the amount of your money you are planning to exchange. - :param exchange_rate: float - the unit value of the foreign currency. - :param spread: int - percentage that is taken as an exchange fee. - :param denomination: int - the value of a single bill. - :return: int - maximum value you can get. + This function calculates and returns the maximum value of the new currency after + determining the exchange rate plus the spread. """ pass diff --git a/exercises/concept/ellens-alien-game/.docs/hints.md b/exercises/concept/ellens-alien-game/.docs/hints.md index e3045b60169..54df99eee44 100644 --- a/exercises/concept/ellens-alien-game/.docs/hints.md +++ b/exercises/concept/ellens-alien-game/.docs/hints.md @@ -20,7 +20,7 @@ ## 4. The `teleport` Method - Remember that `object methods` are always passed `self` as the first parameter. -- Instance attributes can be updated from a method by using `self.` = ``. +- Instance attributes can be updated from a method by using `self. = `. ## 5. The `collision_detection` Method @@ -39,4 +39,4 @@ - A `tuple` would be a _single_ parameter. - The Alien constructor takes _2 parameters_. - Unpacking what is _inside_ the tuple would yield two parameters. -- The standalone function is outside of the `class` +- The standalone function is outside of the `class`. diff --git a/exercises/concept/ellens-alien-game/.docs/instructions.md b/exercises/concept/ellens-alien-game/.docs/instructions.md index 81ec62dbe5a..1093895f251 100644 --- a/exercises/concept/ellens-alien-game/.docs/instructions.md +++ b/exercises/concept/ellens-alien-game/.docs/instructions.md @@ -31,9 +31,9 @@ It is up to you if `hit()` takes healths points _to_ or _below_ zero. ```python >>> alien = Alien(0, 0) ->>> alien.health # Initialized health value. +>>> alien.health 3 # Decrements health by 1 point. @@ -103,8 +103,8 @@ For example: 2 >>> alien_one.total_aliens_created 2 ->>> Alien.total_aliens_created # Accessing the variable from the class directly +>>> Alien.total_aliens_created 2 ``` @@ -120,7 +120,7 @@ For example: >>> aliens = new_aliens_collection(alien_start_positions) ... >>> for alien in aliens: - print(alien.x_coordinate, alien.y_coordinate) +... print(alien.x_coordinate, alien.y_coordinate) (4, 7) (-1, 0) ``` diff --git a/exercises/concept/ellens-alien-game/.docs/introduction.md b/exercises/concept/ellens-alien-game/.docs/introduction.md index ea1fc940fa4..fead4bf6401 100644 --- a/exercises/concept/ellens-alien-game/.docs/introduction.md +++ b/exercises/concept/ellens-alien-game/.docs/introduction.md @@ -182,7 +182,7 @@ class MyClass: def change_location(self, amount): self.location_x += amount self.location_y += amount - return self.location_x, self.location_y + return self.location_x, self.location_y # Make a new test_object with location (3,7) >>> test_object = MyClass((3,7)) @@ -209,7 +209,7 @@ class MyClass: def change_location(self, amount): self.location_x += amount self.location_y += amount - return self.location_x, self.location_y + return self.location_x, self.location_y # Alter class variable number for all instances from within an instance. def increment_number(self): diff --git a/exercises/concept/ellens-alien-game/.meta/exemplar.py b/exercises/concept/ellens-alien-game/.meta/exemplar.py index ace31649c75..1aeae691340 100644 --- a/exercises/concept/ellens-alien-game/.meta/exemplar.py +++ b/exercises/concept/ellens-alien-game/.meta/exemplar.py @@ -4,19 +4,18 @@ class Alien: """Create an Alien object with location x_coordinate and y_coordinate. - Attributes - ---------- - (class)total_aliens_created: int - x_coordinate: int - Position on the x-axis. - y_coordinate: int - Position on the y-axis. - health: int - Number of health points. - - Methods - ------- - hit(): Decrement Alien health by one point. - is_alive(): Return a boolean to indicate if Alien is alive (if health is > 0). - teleport(new_x_coordinate, new_y_coordinate): Move Alien object to new coordinates. - collision_detection(other): Implementation TBD. + Attributes: + (class) total_aliens_created (int): Total number of Alien instances. + x_coordinate (int): Position on the x-axis. + y_coordinate (int): Position on the y-axis. + health (int): Number of health points. + + Methods: + hit(): Decrement Alien health by one point. + is_alive(): Return a boolean for if Alien is alive (if health is > 0). + teleport(new_x_coordinate, new_y_coordinate): Move Alien object to new coordinates. + collision_detection(other): Implementation TBD. + """ total_aliens_created = 0 @@ -24,14 +23,18 @@ class Alien: def __init__(self, x_coordinate, y_coordinate): """Initialize a new Alien object and increment total_aliens_created by 1. - :param x_coordinate: int - Alien position on the x-axis - :param y_coordinate: int - Alien position on the y-axis + Parameters: + x_coordinate (int): Position on the x-axis. + y_coordinate (int): Position on the y-axis. + health (int): Number of health points. - :attribute x_coordinate: int - Alien position on the x-axis - :attribute y_coordinate: int - Alien position on the y-axis - :attribute health: int (3) - Initial Alien health points. + Attributes: + x_coordinate (int): Position on the x-axis. + y_coordinate (int): Position on the y-axis. + health (int): Number of health points. Defaults to 3. - :return: object - New Alien. + Returns: + Alien (Alien Object): New Alien. """ Alien.total_aliens_created += 1 @@ -43,7 +46,8 @@ def __init__(self, x_coordinate, y_coordinate): def hit(self): """Decrement Alien health by 1. - :return: None + Returns: + None """ #There are two valid interpretations for this method/task. @@ -54,7 +58,8 @@ def hit(self): def is_alive(self): """Return if the Alien is alive. - :return: boolean + Returns: + bool: Is the Alien Alive? """ return self.health > 0 @@ -62,29 +67,38 @@ def is_alive(self): def teleport(self, new_x_coordinate, new_y_coordinate): """Change Alien location. - :param new_x_coordinate: int - New location on x-axis. - :param new_y_coordinate: int - New location on y-axis. + Parameters: + new_x_coordinate (int): New location on x-axis. + new_y_coordinate (int): New location on y-axis. - :return: None + Returns: + None """ + self.x_coordinate = new_x_coordinate self.y_coordinate = new_y_coordinate def collision_detection(self, other): """Detect collisions with another Alien. - :param other: object - Other Alien object. + Parameters: + other (object): Other Alien object. - :return: None + Returns: + None """ pass + def new_aliens_collection(positions): """Create a list of Alien instances from a list of coordinate tuples. - :param positions: list - List of tuples of (x, y) coordinates. + Parameters: + positions (list[tuple]): List of (x, y) coordinates in tuples. - :return: list - List of Alien objects. + Returns: + list[object]: List of Alien objects. """ + return [Alien(position[0], position[1]) for position in positions] diff --git a/exercises/concept/ellens-alien-game/classes.py b/exercises/concept/ellens-alien-game/classes.py index a9a3d1edae4..f64d1055e34 100644 --- a/exercises/concept/ellens-alien-game/classes.py +++ b/exercises/concept/ellens-alien-game/classes.py @@ -4,22 +4,22 @@ class Alien: """Create an Alien object with location x_coordinate and y_coordinate. - Attributes - ---------- - (class)total_aliens_created: int - x_coordinate: int - Position on the x-axis. - y_coordinate: int - Position on the y-axis. - health: int - Number of health points. - - Methods - ------- - hit(): Decrement Alien health by one point. - is_alive(): Return a boolean for if Alien is alive (if health is > 0). - teleport(new_x_coordinate, new_y_coordinate): Move Alien object to new coordinates. - collision_detection(other): Implementation TBD. + Attributes: + (class) total_aliens_created (int): Total number of Alien instances. + x_coordinate (int): Position on the x-axis. + y_coordinate (int): Position on the y-axis. + health (int): Number of health points. + + Methods: + hit(): Decrement Alien health by one point. + is_alive(): Return a boolean for if Alien is alive (if health is > 0). + teleport(new_x_coordinate, new_y_coordinate): Move Alien object to new coordinates. + collision_detection(other): Implementation TBD. + """ pass -#TODO: create the new_aliens_collection() function below to call your Alien class with a list of coordinates. +#TODO (Student): Create the new_aliens_collection() function below to call your Alien class with a list of coordinates + diff --git a/exercises/concept/ellens-alien-game/classes_test.py b/exercises/concept/ellens-alien-game/classes_test.py index 3d2b986be4d..a73e652cfa5 100644 --- a/exercises/concept/ellens-alien-game/classes_test.py +++ b/exercises/concept/ellens-alien-game/classes_test.py @@ -198,15 +198,19 @@ def test_new_aliens_collection(self): test_data = [(-2, 6), (1, 5), (-4, -3)] actual_result = new_aliens_collection(test_data) + length_message = (f'We called your function with a list of coordinates that was' + f' {len(test_data)} items long, but the list of Alien ' + f'objects returned was {len(actual_result)} items long.') - error_message = "new_aliens_collection() must return a list of Alien objects." + self.assertEqual(len(test_data), len(actual_result), msg=length_message) for obj in actual_result: - self.assertIsInstance(obj, Alien, msg=error_message) + object_message = "new_aliens_collection() must return a list of Alien objects." + self.assertIsInstance(obj, Alien, msg=object_message) for position, obj in zip(test_data, actual_result): - position_error = (f'After calling new_aliens_collection({test_data}), ' - f'found {obj} initialized to position {(obj.x_coordinate, obj.y_coordinate)}, ' - f'but the tests expected {obj} to be at position {position} instead.') + position_message = (f'After calling new_aliens_collection({test_data}), ' + f'found {obj} initialized to position {(obj.x_coordinate, obj.y_coordinate)}, ' + f'but the tests expected {obj} to be at position {position} instead.') - self.assertEqual(position, (obj.x_coordinate, obj.y_coordinate), msg=position_error) + self.assertEqual(position, (obj.x_coordinate, obj.y_coordinate), msg=position_message) diff --git a/exercises/concept/ghost-gobble-arcade-game/.docs/instructions.md b/exercises/concept/ghost-gobble-arcade-game/.docs/instructions.md index 04b0b51a423..d79e6ed1bbf 100644 --- a/exercises/concept/ghost-gobble-arcade-game/.docs/instructions.md +++ b/exercises/concept/ghost-gobble-arcade-game/.docs/instructions.md @@ -42,7 +42,7 @@ True ## 4. Define if Pac-Man wins Define the `win()` function that takes three parameters (_if Pac-Man has eaten all of the dots_, _if Pac-Man has a power pellet active_, and _if Pac-Man is touching a ghost_) and returns a Boolean value if Pac-Man wins. - The function should return `True` if Pac-Man has eaten all of the dots and has not lost based on the parameters defined in part 3. + The function should return `True` if Pac-Man has eaten all of the dots and has not lost based on the rules defined in part 3. ```python >>> win(False, True, False) diff --git a/exercises/concept/ghost-gobble-arcade-game/.docs/introduction.md b/exercises/concept/ghost-gobble-arcade-game/.docs/introduction.md index a0743f7a115..994c0c39cb8 100644 --- a/exercises/concept/ghost-gobble-arcade-game/.docs/introduction.md +++ b/exercises/concept/ghost-gobble-arcade-game/.docs/introduction.md @@ -1,6 +1,6 @@ # Introduction -Python represents true and false values with the [`bool`][bools] type, which is a subtype of `int`. +Python represents true and false values with the [`bool`][bools] type, which is a subclass of `int`. There are only two values in this type: `True` and `False`. These values can be bound to a variable: diff --git a/exercises/concept/ghost-gobble-arcade-game/.meta/exemplar.py b/exercises/concept/ghost-gobble-arcade-game/.meta/exemplar.py index 4de10a25d55..f27df85d70c 100644 --- a/exercises/concept/ghost-gobble-arcade-game/.meta/exemplar.py +++ b/exercises/concept/ghost-gobble-arcade-game/.meta/exemplar.py @@ -4,9 +4,13 @@ def eat_ghost(power_pellet_active, touching_ghost): """Verify that Pac-Man can eat a ghost if he is empowered by a power pellet. - :param power_pellet_active: bool - does the player have an active power pellet? - :param touching_ghost: bool - is the player touching a ghost? - :return: bool - can a ghost be eaten? + Parameters: + power_pellet_active (bool): Does the player have an active power pellet? + touching_ghost (bool): Is the player touching a ghost? + + Returns: + bool: Can a ghost be eaten? + """ return power_pellet_active and touching_ghost @@ -15,9 +19,13 @@ def eat_ghost(power_pellet_active, touching_ghost): def score(touching_power_pellet, touching_dot): """Verify that Pac-Man has scored when a power pellet or dot has been eaten. - :param touching_power_pellet: bool - is the player touching a power pellet? - :param touching_dot: bool - is the player touching a dot? - :return: bool - has the player scored or not? + Parameters: + touching_power_pellet (bool): Is the player touching a power pellet? + touching_dot (bool): Is the player touching a dot? + + Returns: + bool: Has the player scored or not? + """ return touching_power_pellet or touching_dot @@ -26,9 +34,12 @@ def score(touching_power_pellet, touching_dot): def lose(power_pellet_active, touching_ghost): """Trigger the game loop to end (GAME OVER) when Pac-Man touches a ghost without his power pellet. - :param power_pellet_active: bool - does the player have an active power pellet? - :param touching_ghost: bool - is the player touching a ghost? - :return: bool - has the player lost the game? + Parameters: + power_pellet_active (bool): Does the player have an active power pellet? + touching_ghost (bool): Is the player touching a ghost? + + Returns: + bool: Has the player lost the game? """ return not power_pellet_active and touching_ghost @@ -37,10 +48,13 @@ def lose(power_pellet_active, touching_ghost): def win(has_eaten_all_dots, power_pellet_active, touching_ghost): """Trigger the victory event when all dots have been eaten. - :param has_eaten_all_dots: bool - has the player "eaten" all the dots? - :param power_pellet_active: bool - does the player have an active power pellet? - :param touching_ghost: bool - is the player touching a ghost? - :return: bool - has the player won the game? + Parameters: + has_eaten_all_dots (bool): Has the player "eaten" all the dots? + power_pellet_active (bool): Does the player have an active power pellet? + touching_ghost (bool): Is the player touching a ghost? + + Returns: + bool: Has the player won the game? """ return has_eaten_all_dots and not lose(power_pellet_active, touching_ghost) diff --git a/exercises/concept/ghost-gobble-arcade-game/arcade_game.py b/exercises/concept/ghost-gobble-arcade-game/arcade_game.py index b2848e0c718..adb546aa1c9 100644 --- a/exercises/concept/ghost-gobble-arcade-game/arcade_game.py +++ b/exercises/concept/ghost-gobble-arcade-game/arcade_game.py @@ -4,9 +4,13 @@ def eat_ghost(power_pellet_active, touching_ghost): """Verify that Pac-Man can eat a ghost if he is empowered by a power pellet. - :param power_pellet_active: bool - does the player have an active power pellet? - :param touching_ghost: bool - is the player touching a ghost? - :return: bool - can a ghost be eaten? + Parameters: + power_pellet_active (bool): Does the player have an active power pellet? + touching_ghost (bool): Is the player touching a ghost? + + Returns: + bool: Can a ghost be eaten? + """ pass @@ -15,9 +19,13 @@ def eat_ghost(power_pellet_active, touching_ghost): def score(touching_power_pellet, touching_dot): """Verify that Pac-Man has scored when a power pellet or dot has been eaten. - :param touching_power_pellet: bool - is the player touching a power pellet? - :param touching_dot: bool - is the player touching a dot? - :return: bool - has the player scored or not? + Parameters: + touching_power_pellet (bool): Is the player touching a power pellet? + touching_dot (bool): Is the player touching a dot? + + Returns: + bool: Has the player scored or not? + """ pass @@ -26,9 +34,12 @@ def score(touching_power_pellet, touching_dot): def lose(power_pellet_active, touching_ghost): """Trigger the game loop to end (GAME OVER) when Pac-Man touches a ghost without his power pellet. - :param power_pellet_active: bool - does the player have an active power pellet? - :param touching_ghost: bool - is the player touching a ghost? - :return: bool - has the player lost the game? + Parameters: + power_pellet_active (bool): Does the player have an active power pellet? + touching_ghost (bool): Is the player touching a ghost? + + Returns: + bool: Has the player lost the game? """ pass @@ -37,10 +48,13 @@ def lose(power_pellet_active, touching_ghost): def win(has_eaten_all_dots, power_pellet_active, touching_ghost): """Trigger the victory event when all dots have been eaten. - :param has_eaten_all_dots: bool - has the player "eaten" all the dots? - :param power_pellet_active: bool - does the player have an active power pellet? - :param touching_ghost: bool - is the player touching a ghost? - :return: bool - has the player won the game? + Parameters: + has_eaten_all_dots (bool): Has the player "eaten" all the dots? + power_pellet_active (bool): Does the player have an active power pellet? + touching_ghost (bool): Is the player touching a ghost? + + Returns: + bool: Has the player won the game? """ pass diff --git a/exercises/concept/guidos-gorgeous-lasagna/.docs/hints.md b/exercises/concept/guidos-gorgeous-lasagna/.docs/hints.md index 90065248520..fca46fc65fd 100644 --- a/exercises/concept/guidos-gorgeous-lasagna/.docs/hints.md +++ b/exercises/concept/guidos-gorgeous-lasagna/.docs/hints.md @@ -10,7 +10,9 @@ ## 1. Define expected bake time in minutes -- You need to [name][naming] a constant, and [assign][assignment] it an [integer][numbers] value. +- You need to [name][naming] a [constant][constants], and [assign][assignment] it an [integer][numbers] value. + This constant should be the first thing after the docstring that is at the top of the file. + Remember to remove the #TODO comment after defining the constant. ## 2. Calculate remaining bake time in minutes @@ -22,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 @@ -35,12 +38,16 @@ ## 5. Update the recipe with notes - Clearly [commenting][comments] and [documenting][docstrings] your code according to [PEP257][pep257] is always recommended. +- Some examples of Google-style docstrings can be found in the Sphinx documentation for the [napoleon module][napoleon]. [assignment]: https://docs.python.org/3/reference/simple_stmts.html#grammar-token-assignment-stmt [comments]: https://realpython.com/python-comments-guide/ +[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/ +[napoleon]: https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#module-sphinx.ext.napoleon [numbers]: https://docs.python.org/3/tutorial/introduction.html#numbers [pep257]: https://www.python.org/dev/peps/pep-0257/ [python as a calculator]: https://docs.python.org/3/tutorial/introduction.html#using-python-as-a-calculator diff --git a/exercises/concept/guidos-gorgeous-lasagna/.docs/instructions.md b/exercises/concept/guidos-gorgeous-lasagna/.docs/instructions.md index 2d61cec837d..f3acc137c11 100644 --- a/exercises/concept/guidos-gorgeous-lasagna/.docs/instructions.md +++ b/exercises/concept/guidos-gorgeous-lasagna/.docs/instructions.md @@ -4,63 +4,95 @@ You're going to write some code to help you cook a gorgeous lasagna from your fa You have five tasks, all related to cooking your recipe. -## 1. Define expected bake time in minutes +
-Define an `EXPECTED_BAKE_TIME` constant that returns how many minutes the lasagna should bake in the oven. +~~~~exercism/note +We have started the first function definition for you in the stub file, but you will need to write the remaining function definitions yourself. +You will also need to define any constants yourself. +Read the #TODO comment lines in the stub file carefully. +Once you are done with a task, remove the TODO comment. +~~~~ + +
+ +## 1. Define expected bake time in minutes as a constant + +Define the `EXPECTED_BAKE_TIME` [constant][constants] that represents how many minutes the lasagna should bake in the oven. According to your cookbook, the Lasagna should be in the oven for 40 minutes: ```python ->>> import lasagna ->>> lasagna.EXPECTED_BAKE_TIME +>>> print(EXPECTED_BAKE_TIME) 40 ``` ## 2. Calculate remaining bake time in minutes -Implement the `bake_time_remaining()` function that takes the actual minutes the lasagna has been in the oven as an argument and returns how many minutes the lasagna still needs to bake based on the `EXPECTED_BAKE_TIME`. +Complete the `bake_time_remaining()` function that takes the actual minutes the lasagna has been in the oven as an argument and returns how many minutes the lasagna still needs to bake based on the `EXPECTED_BAKE_TIME` constant. ```python ->>> from lasagna import bake_time_remaining >>> bake_time_remaining(30) 10 ``` + ## 3. Calculate preparation time in minutes -Implement the `preparation_time_in_minutes(number_of_layers)` function that takes the number of layers you want to add to the lasagna as an argument and returns how many minutes you would spend making them. +Define the `preparation_time_in_minutes()` [function][functions] that takes the `number_of_layers` you want to add to the lasagna as an argument and returns how many minutes you would spend making them. Assume each layer takes 2 minutes to prepare. ```python ->>> from lasagna import preparation_time_in_minutes +>>> def preparation_time_in_minutes(number_of_layers): + ... + ... + >>> preparation_time_in_minutes(2) 4 ``` -## 4. Calculate total elapsed cooking time (prep + bake) in minutes -Implement the `elapsed_time_in_minutes(number_of_layers, elapsed_bake_time)` function that has two parameters: `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've been cooking, or the sum of your preparation time and the time the lasagna has already spent baking in the oven. +## 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. + ```python ->>> from lasagna import elapsed_time_in_minutes +>>> def elapsed_time_in_minutes(number_of_layers, elapsed_bake_time): + ... + ... + >>> elapsed_time_in_minutes(3, 20) 26 ``` + ## 5. Update the recipe with notes -Go back through the recipe, adding "notes" in the form of function docstrings. +Go back through the recipe, adding "notes" in the form of [function docstrings][function-docstrings]. ```python def elapsed_time_in_minutes(number_of_layers, elapsed_bake_time): """Calculate the elapsed cooking time. - - :param number_of_layers: int - the number of layers in the lasagna. - :param elapsed_bake_time: int - elapsed cooking time. - :return: int - total time elapsed (in minutes) preparing and cooking. - - This function takes two integers representing the number of lasagna layers and the - time already spent baking and calculates the total elapsed minutes spent cooking the - lasagna. + + Parameters: + number_of_layers (int): The number of layers in the lasagna. + elapsed_bake_time (int): Time the lasagna has been baking in the oven. + + Returns: + int: The total time elapsed (in minutes) preparing and baking. + + This function takes two integers representing the number of lasagna + layers and the time already spent baking the lasagna. It calculates + the total elapsed minutes spent cooking (preparing + baking). + """ ``` + +[constants]: https://stackoverflow.com/a/2682752 +[functions]: https://docs.python.org/3/tutorial/controlflow.html#defining-functions +[function-docstrings]: https://docs.python.org/3/tutorial/controlflow.html#documentation-strings diff --git a/exercises/concept/guidos-gorgeous-lasagna/.docs/introduction.md b/exercises/concept/guidos-gorgeous-lasagna/.docs/introduction.md index ffe2bedd6a3..8f06aed4004 100644 --- a/exercises/concept/guidos-gorgeous-lasagna/.docs/introduction.md +++ b/exercises/concept/guidos-gorgeous-lasagna/.docs/introduction.md @@ -14,17 +14,20 @@ This first exercise introduces 4 major Python language features: 3. Comments, and 4. Docstrings. +
~~~~exercism/note -In general, content, tests, and analyzer tooling for the Python track follow the style conventions outlined in [PEP 8](https://www.python.org/dev/peps/pep-0008/) and [PEP 257](https://www.python.org/dev/peps/pep-0257/) for Python code style, with the additional (strong) suggestion that there be no single letter variable names. +In general, content, tests, and analyzer tooling for the Python track follow the style conventions outlined in [PEP 8](https://www.python.org/dev/peps/pep-0008/) and [PEP 257](https://www.python.org/dev/peps/pep-0257/) for Python code style, with the additional (strong) suggestion that there be no single letter variable names or variables named ["_"][uses of _ in Python]. On the Python track, [variables][variables] are always written in [`snake_case`][snake case], and constants in `SCREAMING_SNAKE_CASE`. [variables]: https://realpython.com/python-variables/ [snake case]: https://en.wikipedia.org/wiki/Snake_case +[uses of _ in Python]: https://medium.com/better-programming/how-to-use-underscore-properly-in-python-37df5e05ba4c ~~~~ +
## Name Assignment (Variables & Constants) @@ -33,8 +36,8 @@ A name can be reassigned (or re-bound) to different values (different object typ ```python ->>> my_first_variable = 1 # my_first_variable bound to an integer object of value one. ->>> my_first_variable = 2 # my_first_variable re-assigned to integer value 2. +>>> my_first_variable = 1 #<-- my_first_variable bound to an integer object of value one. +>>> my_first_variable = 2 #<-- my_first_variable re-assigned to integer value 2. >>> print(type(my_first_variable)) @@ -42,12 +45,13 @@ A name can be reassigned (or re-bound) to different values (different object typ >>> print(my_first_variable) 2 ->>> my_first_variable = "Now, I'm a string." # You may re-bind a name to a different object type and value. +>>> my_first_variable = "Now, I'm a string." #<-- You may re-bind a name to a different object type and value. >>> print(type(my_first_variable)) +>>> my_first_variable = 'You can call me "str".' #<-- Strings can be declared using single or double quote marks. >>> print(my_first_variable) -"Now, I'm a string." # Strings can be declared using single or double quote marks. +You can call me "str". ``` @@ -61,7 +65,7 @@ Using `SCREAMING_SNAKE_CASE` signals that the name should not be re-assigned, or ## Functions The `def` keyword begins a [function definition][function definition]. -Each function can have zero or more formal [parameters][parameters] in `()` parenthesis, followed by a `:` colon. +Each function can have zero or more formal [parameters][parameters] in `()` parentheses, followed by a `:` colon. Statements for the _body_ of the function begin on the line following `def` and must be _indented in a block_. @@ -90,13 +94,14 @@ IndentationError: unindent does not match any outer indentation level Functions _explicitly_ return a value or object via the [`return`][return] keyword: + ```python # Function definition on first line, explicit return used on final line. -def add_two_numbers(number_one, number_two): - return number_one + number_two +>>> def add_two_numbers(number_one, number_two): + return number_one + number_two -# Calling the function in the Python terminal returns the sum of the numbers. +# Calling the function in the Python shell returns the sum of the numbers. >>> add_two_numbers(3, 4) 7 @@ -108,28 +113,41 @@ def add_two_numbers(number_one, number_two): ``` -Functions that do not have an _explicit_ `return` expression will _implicitly_ return the [`None`][none] object. +Functions that do not have an _explicit_ expression following a `return` will _implicitly_ return the [`None`][none] object. The details of `None` will be covered in a later exercise. For the purposes of this exercise and explanation, `None` is a placeholder that represents nothing, or null: ```python -# This function does not have an explicit return. -def add_two_numbers(number_one, number_two): - result = number_one + number_two +# This function will return `None` +def square_a_number(number): + square = number * number + return # <-- note that this return is not followed by an expression -# Calling the function in the Python terminal appears +# Calling the function in the Python shell appears # to not return anything at all. ->>> add_two_numbers(5, 7) +>>> square_a_number(2) >>> # Using print() with the function call shows that # the function is actually returning the **None** object. ->>> print(add_two_numbers(5, 7)) +>>> print(square_a_number(2)) None +``` + +Functions that omit `return` will also _implicitly_ return the [`None`][none] object. +This means that if you do not use `return` in a function, Python will return the `None` object for you. +```python +# This function omits a return keyword altogether. +def add_two_numbers(number_one, number_two): + result = number_one + number_two + +>>> add_two_numbers(5, 7) +>>> print(add_two_numbers(5, 7)) +None # Assigning the function call to a variable and printing # the variable will also show None. @@ -145,32 +163,33 @@ Functions are [_called_][calls] or invoked using their name followed by `()`. Dot (`.`) notation is used for calling functions defined inside a class or module. ```python ->>> def number_to_the_power_of(number_one, number_two): - return number_one ** number_two +>>> def raise_to_power(number, power): +... return number ** power ... ->>> number_to_the_power_of(3,3) # Invoking the function with the arguments 3 and 3. +>>> raise_to_power(3,3) # <--Invoking the function with the arguments 3 and 3. 27 # A mismatch between the number of parameters and the number of arguments will raise an error. ->>> number_to_the_power_of(4,) +>>> raise_to_power(4,) ... Traceback (most recent call last): File "", line 1, in -TypeError: number_to_the_power_of() missing 1 required positional argument: 'number_two' +TypeError: raise_to_power() missing 1 required positional argument: 'power' # Calling methods or functions in classes and modules. >>> start_text = "my silly sentence for examples." ->>> str.upper(start_text) # Calling the upper() method for the built-in str class. -"MY SILLY SENTENCE FOR EXAMPLES." +>>> str.upper(start_text) # <--Calling the upper() method from the built-in str class on start_text. +'MY SILLY SENTENCE FOR EXAMPLES.' + # Importing the math module -import math +>>> import math ->>> math.pow(2,4) # Calling the pow() function from the math module ->>> 16.0 +>>> math.pow(2,4) # <--Calling the pow() function from the math module. +16.0 ``` @@ -189,13 +208,18 @@ Docstrings are declared using triple double quotes (""") indented at the same le ```python -# An example from PEP257 of a multi-line docstring. +# An example from PEP257 of a multi-line docstring +# reformatted to use Google style non-type hinted docstrings. +# Some additional details can be found in the Sphinx documentation: +# https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#getting-started + def complex(real=0.0, imag=0.0): """Form a complex number. - Keyword arguments: - real -- the real part (default 0.0) - imag -- the imaginary part (default 0.0) + Keyword Arguments: + real (float): The real part of the number (default 0.0) + imag (float): The imaginary part of the number (default 0.0) + """ if imag == 0.0 and real == 0.0: @@ -204,38 +228,47 @@ def complex(real=0.0, imag=0.0): ``` -Docstrings are read by automated documentation tools and are returned by calling the special attribute `.__doc__` on the function, method, or class name. -Docstring conventions are laid out in [PEP257][pep257]. +Docstrings are read by automated documentation tools such as [Sphinx][sphinx] and are returned by calling the special attribute `.__doc__` on the function, method, or class name. +General docstring conventions are laid out in [PEP257][pep257], but exact formats will vary by project and team. +Exercism concept exercises try to follow the Google style for un-type hinted code. Docstrings can also function as [lightweight unit tests][doctests], which will be covered in a later exercise. ```python -# An example on a user-defined function. ->>> def number_to_the_power_of(number_one, number_two): - """Raise a number to an arbitrary power. - - :param number_one: int the base number. - :param number_two: int the power to raise the base number to. - :return: int - number raised to power of second number +# An example on a user-defined function using a Google style docstring. +>>> def raise_to_power(number, power): + """Raise a number to an arbitrary power. + + Parameters: + number (int): The base number. + power (int): The power to raise the base number to. + + Returns: + int: The number raised to the specified power. + + Takes a number and raises it to the specified power, returning the result. - Takes number_one and raises it to the power of number_two, returning the result. - """ + """ - return number_one ** number_two + return number ** power ... # Calling the .__doc__ attribute of the function and printing the result. ->>> print(number_to_the_power_of.__doc__) +>>> print(raise_to_power.__doc__) Raise a number to an arbitrary power. - :param number_one: int the base number. - :param number_two: int the power to raise the base number to. - :return: int - number raised to power of second number +Parameters: + number (int): The base number. + power (int): The power to raise the base number to. - Takes number_one and raises it to the power of number_two, returning the result. +Returns: + int: The number raised to the specified power. + +Takes a number and raises it to the specified power, returning the result. ``` + [calls]: https://docs.python.org/3/reference/expressions.html#calls [comments]: https://realpython.com/python-comments-guide/#python-commenting-basics [docstring]: https://docs.python.org/3/tutorial/controlflow.html#tut-docstrings @@ -251,4 +284,5 @@ Raise a number to an arbitrary power. [parameters]: https://docs.python.org/3/glossary.html#term-parameter [pep257]: https://www.python.org/dev/peps/pep-0257/ [return]: https://docs.python.org/3/reference/simple_stmts.html#return +[sphinx]: https://www.sphinx-doc.org/en/master/usage/index.html [type hints]: https://docs.python.org/3/library/typing.html diff --git a/exercises/concept/guidos-gorgeous-lasagna/.meta/exemplar.py b/exercises/concept/guidos-gorgeous-lasagna/.meta/exemplar.py index ff0bff38e1a..093571a2936 100644 --- a/exercises/concept/guidos-gorgeous-lasagna/.meta/exemplar.py +++ b/exercises/concept/guidos-gorgeous-lasagna/.meta/exemplar.py @@ -11,8 +11,11 @@ def bake_time_remaining(elapsed_bake_time): """Calculate the bake time remaining. - :param elapsed_bake_time: int - baking time already elapsed. - :return: int - remaining bake time (in minutes) derived from 'EXPECTED_BAKE_TIME'. + Parameters: + elapsed_bake_time (int): The baking time already elapsed. + + Returns: + int: The remaining bake time (in minutes) derived from 'EXPECTED_BAKE_TIME'. Function that takes the actual minutes the lasagna has been in the oven as an argument and returns how many minutes the lasagna still needs to bake @@ -25,11 +28,15 @@ def bake_time_remaining(elapsed_bake_time): def preparation_time_in_minutes(number_of_layers): """Calculate the preparation time. - :param number_of_layers: int - the number of lasagna layers made. - :return: int - amount of prep time (in minutes), based on 2 minutes per layer added. + Parameters: + number_of_layers(int): The number of lasagna layers made. + + Returns: + int: Amount of prep time (in minutes), based on 2 minutes per layer added. This function takes an integer representing the number of layers added to the dish, calculating preparation time using a time of 2 minutes per layer added. + """ return number_of_layers * PREPARATION_TIME @@ -38,13 +45,17 @@ def preparation_time_in_minutes(number_of_layers): def elapsed_time_in_minutes(number_of_layers, elapsed_bake_time): """Calculate the elapsed time. - :param number_of_layers: int - the number of layers in the lasagna. - :param elapsed_bake_time: int - elapsed cooking time. - :return: int - total time elapsed (in in minutes) preparing and cooking. + Parameters: + number_of_layers (int): The number of layers in the lasagna. + elapsed_bake_time (int): Elapsed cooking time. + + Returns: + int: Total time elapsed (in minutes) preparing + cooking. This function takes two integers representing the number of lasagna layers and the time already spent baking and calculates the total elapsed minutes spent cooking the lasagna. + """ return preparation_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 90d0102584c..bdf8ca9b778 100644 --- a/exercises/concept/guidos-gorgeous-lasagna/lasagna.py +++ b/exercises/concept/guidos-gorgeous-lasagna/lasagna.py @@ -8,15 +8,18 @@ """ -#TODO: define the 'EXPECTED_BAKE_TIME' constant. +#TODO (student): define your EXPECTED_BAKE_TIME (required) and PREPARATION_TIME (optional) constants below. -#TODO: Remove 'pass' and complete the 'bake_time_remaining()' function below. +#TODO (student): Remove 'pass' and complete the 'bake_time_remaining()' function below. def bake_time_remaining(): """Calculate the bake time remaining. - :param elapsed_bake_time: int - baking time already elapsed. - :return: int - remaining bake time (in minutes) derived from 'EXPECTED_BAKE_TIME'. + Parameters: + elapsed_bake_time (int): The baking time already elapsed. + + Returns: + int: The remaining bake time (in minutes) derived from 'EXPECTED_BAKE_TIME'. Function that takes the actual minutes the lasagna has been in the oven as an argument and returns how many minutes the lasagna still needs to bake @@ -26,10 +29,16 @@ def bake_time_remaining(): pass -#TODO: Define the 'preparation_time_in_minutes()' function below. -# You might also consider using 'PREPARATION_TIME' here, if you have it defined. +#TODO (student): Define the 'preparation_time_in_minutes()' function below. +# 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, and make changes to your code. + + + +#TODO (student): define the 'elapsed_time_in_minutes()' function below. -#TODO: define the 'elapsed_time_in_minutes()' function below. -# Remember to add a docstring (you can copy and then alter the one from bake_time_remaining.) +# TODO (student): Remember to go back and add docstrings to all your functions +# (you can copy and then alter the one from bake_time_remaining.) diff --git a/exercises/concept/inventory-management/.docs/hints.md b/exercises/concept/inventory-management/.docs/hints.md index dbdfe09ae79..14fab1f150c 100644 --- a/exercises/concept/inventory-management/.docs/hints.md +++ b/exercises/concept/inventory-management/.docs/hints.md @@ -9,7 +9,7 @@ - You need a [for loop][for-loop] to iterate the list of items, then insert each item in the dictionary if missing and increment the item count using the dictionary accessor. - You can use [`dict.setdefault`][dict setdefault] to make sure the value is set before incrementing the count of the item. -- This function should [return][return-keyword] a dict]. +- This function should [return][return-keyword] a dict. ## 2. Add items from a list to an existing dictionary diff --git a/exercises/concept/inventory-management/.docs/introduction.md b/exercises/concept/inventory-management/.docs/introduction.md index 738f36ef754..4671d985766 100644 --- a/exercises/concept/inventory-management/.docs/introduction.md +++ b/exercises/concept/inventory-management/.docs/introduction.md @@ -49,7 +49,7 @@ A `dict` can also be directly entered as a _dictionary literal_, using curly bra ## Accessing Values in a Dictionary You can access an entry in a dictionary using a _key_ in square (`[]`) brackets. -If a `key` does not exist n the `dict`, a `KeyError` is thrown: +If a `key` does not exist in the `dict`, a `KeyError` is thrown: ```python >>> bear["speed"] @@ -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/inventory-management/.meta/exemplar.py b/exercises/concept/inventory-management/.meta/exemplar.py index ac02bad30d4..140c76d8328 100644 --- a/exercises/concept/inventory-management/.meta/exemplar.py +++ b/exercises/concept/inventory-management/.meta/exemplar.py @@ -4,8 +4,11 @@ def create_inventory(items): """Create a dict that tracks the amount (count) of each element on the `items` list. - :param items: list - list of items to create an inventory from. - :return: dict - the inventory dictionary. + Parameters: + items (list): Items to create an inventory from. + + Returns: + dict: The inventory dictionary. """ inventory = {} @@ -16,9 +19,12 @@ def create_inventory(items): def add_items(inventory, items): """Add or increment items in inventory using elements from the items `list`. - :param inventory: dict - dictionary of existing inventory. - :param items: list - list of items to update the inventory with. - :return: dict - the inventory updated with the new items. + Parameters: + inventory (dict): Dictionary of existing inventory. + items (list): List of items to update the inventory with. + + Returns: + dict: The inventory updated with the new items. """ for item in items: @@ -30,9 +36,12 @@ def add_items(inventory, items): def decrement_items(inventory, items): """Decrement items in inventory using elements from the `items` list. - :param inventory: dict - inventory dictionary. - :param items: list - list of items to decrement from the inventory. - :return: dict - updated inventory with items decremented. + Parameters: + inventory (dict): Inventory dictionary. + items (list): List of items to decrement from the inventory. + + Returns: + dict: Updated inventory with items decremented. """ for item in items: @@ -44,9 +53,12 @@ def decrement_items(inventory, items): def remove_item(inventory, item): """Remove item from inventory if it matches `item` string. - :param inventory: dict - inventory dictionary. - :param item: str - item to remove from the inventory. - :return: dict - updated inventory with item removed. Current inventory if item does not match. + Parameters: + inventory (dict): Inventory dictionary. + item (str): Item to remove from the inventory. + + Returns: + dict: Updated inventory with item removed. Current inventory if item does not match. """ if item in inventory: @@ -57,8 +69,11 @@ def remove_item(inventory, item): def list_inventory(inventory): """Create a list containing only available (item_name, item_count > 0) pairs in inventory. - :param inventory: dict - an inventory dictionary. - :return: list of tuples - list of key, value pairs from the inventory dictionary. + Parameters: + inventory (dict): An inventory dictionary. + + Returns: + list[tuple]: List of key, value tuples from the inventory dictionary. """ output = [] diff --git a/exercises/concept/inventory-management/dicts.py b/exercises/concept/inventory-management/dicts.py index 2600eceb27d..ae17da80e12 100644 --- a/exercises/concept/inventory-management/dicts.py +++ b/exercises/concept/inventory-management/dicts.py @@ -4,8 +4,11 @@ def create_inventory(items): """Create a dict that tracks the amount (count) of each element on the `items` list. - :param items: list - list of items to create an inventory from. - :return: dict - the inventory dictionary. + Parameters: + items (list): Items to create an inventory from. + + Returns: + dict: The inventory dictionary. """ pass @@ -14,9 +17,12 @@ def create_inventory(items): def add_items(inventory, items): """Add or increment items in inventory using elements from the items `list`. - :param inventory: dict - dictionary of existing inventory. - :param items: list - list of items to update the inventory with. - :return: dict - the inventory updated with the new items. + Parameters: + inventory (dict): Dictionary of existing inventory. + items (list): List of items to update the inventory with. + + Returns: + dict: The inventory updated with the new items. """ pass @@ -25,9 +31,12 @@ def add_items(inventory, items): def decrement_items(inventory, items): """Decrement items in inventory using elements from the `items` list. - :param inventory: dict - inventory dictionary. - :param items: list - list of items to decrement from the inventory. - :return: dict - updated inventory with items decremented. + Parameters: + inventory (dict): Inventory dictionary. + items (list): List of items to decrement from the inventory. + + Returns: + dict: Updated inventory with items decremented. """ pass @@ -36,9 +45,12 @@ def decrement_items(inventory, items): def remove_item(inventory, item): """Remove item from inventory if it matches `item` string. - :param inventory: dict - inventory dictionary. - :param item: str - item to remove from the inventory. - :return: dict - updated inventory with item removed. Current inventory if item does not match. + Parameters: + inventory (dict): Inventory dictionary. + item (str): Item to remove from the inventory. + + Returns: + dict: Updated inventory with item removed. Current inventory if item does not match. """ pass @@ -47,9 +59,11 @@ def remove_item(inventory, item): def list_inventory(inventory): """Create a list containing only available (item_name, item_count > 0) pairs in inventory. - :param inventory: dict - an inventory dictionary. - :return: list of tuples - list of key, value pairs from the inventory dictionary. + Parameters: + inventory (dict): An inventory dictionary. + + Returns: + list[tuple]: List of key, value tuples from the inventory dictionary. """ pass - diff --git a/exercises/concept/little-sisters-essay/.docs/introduction.md b/exercises/concept/little-sisters-essay/.docs/introduction.md index fc327a12505..0eacedfee07 100644 --- a/exercises/concept/little-sisters-essay/.docs/introduction.md +++ b/exercises/concept/little-sisters-essay/.docs/introduction.md @@ -3,10 +3,11 @@ The `str` class offers [many useful methods][str methods] for working with and composing strings. These include searching, cleaning, splitting, transforming, translating, and many other techniques. -Strings are [immutable sequences][text sequence] of [Unicode code points][unicode code points] -- individual "characters" or 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 are [sequences][text sequence] of [Unicode code points][unicode code points] -- individual "characters" or 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 implement all [common sequence operations][common sequence operations]. -Strings can be iterated through using `for item in ` or `for index, item in enumerate()` syntax. -They can be concatenated using the `+` operator or via `.join()` and implement all [common sequence operations][common sequence operations]. +They can be iterated through using `for item in ` or `for index, item in enumerate()` syntax. +They can also be concatenated using ` + ` or `.join()`. Strings are _immutable_, meaning the value of a `str` object in memory cannot change. Functions or methods that operate on a `str` (_like the ones we are learning about here_) will return a new `instance` of that `str` object instead of modifying the original `str`. diff --git a/exercises/concept/little-sisters-essay/.meta/exemplar.py b/exercises/concept/little-sisters-essay/.meta/exemplar.py index c51e930d820..fa70976a519 100644 --- a/exercises/concept/little-sisters-essay/.meta/exemplar.py +++ b/exercises/concept/little-sisters-essay/.meta/exemplar.py @@ -4,8 +4,11 @@ def capitalize_title(title): """Convert the first letter of each word in the title to uppercase if needed. - :param title: str - title string that needs title casing. - :return: str - title string in title case (first letters capitalized). + Parameters: + title (str): Essay title that needs title casing. + + Returns: + str: The title string in title case (first letters capitalized). """ return title.title() @@ -14,8 +17,11 @@ def capitalize_title(title): def check_sentence_ending(sentence): """Check the ending of the sentence to verify that a period is present. - :param sentence: str - a sentence to check. - :return: bool - is the sentence punctuated correctly? + Parameters: + sentence (str): A sentence to check. + + Returns: + bool: Is the sentence punctuated correctly? """ return sentence.endswith(".") @@ -24,8 +30,11 @@ def check_sentence_ending(sentence): def clean_up_spacing(sentence): """Trim any leading or trailing whitespace from the sentence. - :param sentence: str - a sentence to clean of leading and trailing space characters. - :return: str - a sentence that has been cleaned of leading and trailing space characters. + Parameters: + sentence (str): A sentence to clean of leading and trailing space characters. + + Returns: + str: A sentence that has been cleaned of leading and trailing space characters. """ clean_sentence = sentence.strip() @@ -35,10 +44,13 @@ def clean_up_spacing(sentence): def replace_word_choice(sentence, old_word, new_word): """Replace a word in the provided sentence with a new one. - :param sentence: str - a sentence to replace words in. - :param old_word: str - word to replace. - :param new_word: str - replacement word. - :return: str - input sentence with new words in place of old words. + Parameters: + sentence (str): A sentence to replace words in. + old_word (str): The word to replace. + new_word (str): The replacement word. + + Returns: + str: Input sentence with new words in place of old words. """ better_sentence = sentence.replace(old_word, new_word) diff --git a/exercises/concept/little-sisters-essay/string_methods.py b/exercises/concept/little-sisters-essay/string_methods.py index 5c5b9ce66dd..3c1797741c7 100644 --- a/exercises/concept/little-sisters-essay/string_methods.py +++ b/exercises/concept/little-sisters-essay/string_methods.py @@ -4,8 +4,11 @@ def capitalize_title(title): """Convert the first letter of each word in the title to uppercase if needed. - :param title: str - title string that needs title casing. - :return: str - title string in title case (first letters capitalized). + Parameters: + title (str): Essay title that needs title casing. + + Returns: + str: The title string in title case (first letters capitalized). """ pass @@ -14,18 +17,24 @@ def capitalize_title(title): def check_sentence_ending(sentence): """Check the ending of the sentence to verify that a period is present. - :param sentence: str - a sentence to check. - :return: bool - return True if punctuated correctly with period, False otherwise. + Parameters: + sentence (str): A sentence to check. + + Returns: + bool: Is the sentence punctuated correctly? """ pass def clean_up_spacing(sentence): - """Verify that there isn't any whitespace at the start and end of the sentence. + """Trim any leading or trailing whitespace from the sentence. - :param sentence: str - a sentence to clean of leading and trailing space characters. - :return: str - a sentence that has been cleaned of leading and trailing space characters. + Parameters: + sentence (str): A sentence to clean of leading and trailing space characters. + + Returns: + str: A sentence that has been cleaned of leading and trailing space characters. """ pass @@ -34,10 +43,13 @@ def clean_up_spacing(sentence): def replace_word_choice(sentence, old_word, new_word): """Replace a word in the provided sentence with a new one. - :param sentence: str - a sentence to replace words in. - :param old_word: str - word to replace. - :param new_word: str - replacement word. - :return: str - input sentence with new words in place of old words. + Parameters: + sentence (str): A sentence to replace words in. + old_word (str): The word to replace. + new_word (str): The replacement word. + + Returns: + str: Input sentence with new words in place of old words. """ pass diff --git a/exercises/concept/little-sisters-vocab/.docs/hints.md b/exercises/concept/little-sisters-vocab/.docs/hints.md index 2e5540805c4..eabd05e4734 100644 --- a/exercises/concept/little-sisters-vocab/.docs/hints.md +++ b/exercises/concept/little-sisters-vocab/.docs/hints.md @@ -14,14 +14,16 @@ 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 - Strings can be indexed or sliced from either the left (starting at 0) or the right (starting at -1). -- If you want the last code point of an arbitrary-length string, you can use [-1]. -- The last three letters in a string can be "sliced off" using a negative index. e.g. 'beautiful'[:-3] == 'beauti' +- If you want the last code point of an arbitrary-length string, you can use `[-1]`. +- The last three letters in a string can be "sliced off" using a negative index. e.g. `beautiful'[:-3] == 'beauti` ## 4. Extract and transform 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..db6090fa8ad 100644 --- a/exercises/concept/little-sisters-vocab/.docs/introduction.md +++ b/exercises/concept/little-sisters-vocab/.docs/introduction.md @@ -12,7 +12,7 @@ A `str` literal can be declared via single `'` or double `"` quotes. The escape >>> single_quoted = 'These allow "double quoting" without "escape" characters.' ->>> double_quoted = "These allow embedded 'single quoting', so you don't have to use an 'escape' character". +>>> double_quoted = "These allow embedded 'single quoting', so you don't have to use an 'escape' character." >>> escapes = 'If needed, a \'slash\' can be used as an escape character within a string when switching quote styles won\'t work.' ``` @@ -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/little-sisters-vocab/.meta/exemplar.py b/exercises/concept/little-sisters-vocab/.meta/exemplar.py index c71d4902cae..ed024abaf00 100644 --- a/exercises/concept/little-sisters-vocab/.meta/exemplar.py +++ b/exercises/concept/little-sisters-vocab/.meta/exemplar.py @@ -4,26 +4,32 @@ def add_prefix_un(word): """Take the given word and add the 'un' prefix. - :param word: str - containing the root word. - :return: str - of root word prepended with 'un'. + Parameters: + word (str): The root word. + + Returns: + str: Root word prepended with 'un'. """ return 'un' + word def make_word_groups(vocab_words): - """Transform a list containing a prefix and words into a string with the prefix followed by the words with prefix prepended. + """Transform a list containing a prefix and words. + + Parameters: + vocab_words (list[str]): Vocabulary words with prefix at first index. + + Returns: + str: Prefix followed by vocabulary words with prefix applied. - :param vocab_words: list - of vocabulary words with prefix in first index. - :return: str - of prefix followed by vocabulary words with - prefix applied. + This function takes a `vocab_words` list of strings and returns a string + with the prefix and the words with prefix applied, separated by ' :: '. - This function takes a `vocab_words` list and returns a string - with the prefix and the words with prefix applied, separated - by ' :: '. + Examples: + >>> list('en', 'close', 'joy', 'lighten') + 'en :: enclose :: enjoy :: enlighten'. - For example: list('en', 'close', 'joy', 'lighten'), - produces the following string: 'en :: enclose :: enjoy :: enlighten'. """ prefix = vocab_words[0] @@ -35,10 +41,19 @@ def make_word_groups(vocab_words): def remove_suffix_ness(word): """Remove the suffix from the word while keeping spelling in mind. - :param word: str - of word to remove suffix from. - :return: str - of word with suffix removed & spelling adjusted. + Parameters: + word (str): Word to remove suffix from. + + Returns: + str: Word with suffix removed & spelling adjusted. + + Examples: + >>> remove_suffix_ness('heaviness') + 'heavy' + + >>> remove_suffix_ness('sadness') + 'sad' - For example: "heaviness" becomes "heavy", but "sadness" becomes "sad". """ word = word[:-4] @@ -51,11 +66,20 @@ def remove_suffix_ness(word): def adjective_to_verb(sentence, index): """Change the adjective within the sentence to a verb. - :param sentence: str - that uses the word in sentence. - :param index: int - index of the word to remove and transform. - :return: str - word that changes the extracted adjective to a verb. + Parameters: + sentence (str): The word used in a sentence as an adjective. + index (int): Index of the adjective to remove and transform. + + Returns: + str: The extracted adjective in verb form. + + Examples: + >>> adjective_to_verb('It got dark as the sun set.', 2) + 'darken' + + >>> adjective_to_verb('The ink stains her fingers black.', -1) + 'blacken' - For example, ("It got dark as the sun set", 2) becomes "darken". """ word = sentence.split()[index] diff --git a/exercises/concept/little-sisters-vocab/strings.py b/exercises/concept/little-sisters-vocab/strings.py index 39ae7bb80c8..de0e37edf3d 100644 --- a/exercises/concept/little-sisters-vocab/strings.py +++ b/exercises/concept/little-sisters-vocab/strings.py @@ -4,26 +4,32 @@ def add_prefix_un(word): """Take the given word and add the 'un' prefix. - :param word: str - containing the root word. - :return: str - of root word prepended with 'un'. + Parameters: + word (str): The root word. + + Returns: + str: Root word prepended with 'un'. """ pass def make_word_groups(vocab_words): - """Transform a list containing a prefix and words into a string with the prefix followed by the words with prefix prepended. + """Transform a list containing a prefix and words. + + Parameters: + vocab_words (list[str]): Vocabulary words with prefix at first index. + + Returns: + str: Prefix followed by vocabulary words with prefix applied. - :param vocab_words: list - of vocabulary words with prefix in first index. - :return: str - of prefix followed by vocabulary words with - prefix applied. + This function takes a `vocab_words` list of strings and returns a string + with the prefix and the words with prefix applied, separated by ' :: '. - This function takes a `vocab_words` list and returns a string - with the prefix and the words with prefix applied, separated - by ' :: '. + Examples: + >>> list('en', 'close', 'joy', 'lighten') + 'en :: enclose :: enjoy :: enlighten'. - For example: list('en', 'close', 'joy', 'lighten'), - produces the following string: 'en :: enclose :: enjoy :: enlighten'. """ pass @@ -32,10 +38,19 @@ def make_word_groups(vocab_words): def remove_suffix_ness(word): """Remove the suffix from the word while keeping spelling in mind. - :param word: str - of word to remove suffix from. - :return: str - of word with suffix removed & spelling adjusted. + Parameters: + word (str): Word to remove suffix from. + + Returns: + str: Word with suffix removed & spelling adjusted. + + Examples: + >>> remove_suffix_ness('heaviness') + 'heavy' + + >>> remove_suffix_ness('sadness') + 'sad' - For example: "heaviness" becomes "heavy", but "sadness" becomes "sad". """ pass @@ -44,11 +59,20 @@ def remove_suffix_ness(word): def adjective_to_verb(sentence, index): """Change the adjective within the sentence to a verb. - :param sentence: str - that uses the word in sentence. - :param index: int - index of the word to remove and transform. - :return: str - word that changes the extracted adjective to a verb. + Parameters: + sentence (str): The word used in a sentence as an adjective. + index (int): Index of the adjective to remove and transform. + + Returns: + str: The extracted adjective in verb form. + + Examples: + >>> adjective_to_verb('It got dark as the sun set.', 2) + 'darken' + + >>> adjective_to_verb('The ink stains her fingers black.', -1) + 'blacken' - For example, ("It got dark as the sun set.", 2) becomes "darken". """ pass diff --git a/exercises/concept/locomotive-engineer/.docs/hints.md b/exercises/concept/locomotive-engineer/.docs/hints.md index 208188c0add..877f4decb97 100644 --- a/exercises/concept/locomotive-engineer/.docs/hints.md +++ b/exercises/concept/locomotive-engineer/.docs/hints.md @@ -2,7 +2,7 @@ ## General -- To extract multiple arguments in the function parameters so can you pack them with the `*args` operator for `list` or `tuples` or `**kwargs` for keyword-based arguments. +- A function can be defined to take multiple arguments packaged together by using the `*args` parameter for `list` & `tuple` arguments, or the `**kwargs` parameter for dictionary/keyword-based arguments. - To pack or unpack use the `*` or `**` operator. ## 1. Create a list of all wagons @@ -11,7 +11,7 @@ ## 2. Fix list of wagons -- Using unpacking with the `*` operator, lets you extract the first two elements of a `list` while keeping the rest intact. +- Using unpacking with the `*` operator allows you to extract the first two elements of a `list` while keeping the rest intact. - To add another `list` into an existing `list`, you can use the `*` operator to "spread" the `list`. ## 3. Add missing stops @@ -28,7 +28,7 @@ ## 5. Fix the wagon depot -- `zip(*iterators)` can use used to transpose a nested `list`. +- `zip(*iterators)` can be used to transpose a nested `list`. - To extract data from zipped iterators, you can use a for loop. -- you can also unpack zipped iterators using `*`. +- you can also unpack zipped iterators using `*`. `[*content] = zip(iterator_1, iterator_2)` will unzip the `tuple` produced by `zip()` into a `list`. diff --git a/exercises/concept/locomotive-engineer/.docs/introduction.md b/exercises/concept/locomotive-engineer/.docs/introduction.md index 66d9ba15810..b10fff1217f 100644 --- a/exercises/concept/locomotive-engineer/.docs/introduction.md +++ b/exercises/concept/locomotive-engineer/.docs/introduction.md @@ -9,6 +9,7 @@ The special operators `*` and `**` are often used in unpacking contexts and with `*` and `**` should not be confused with `*` and `**`. While `*` and `**` are used for multiplication and exponentiation respectively, `*` and `**` are used as packing and unpacking operators. ~~~~ + ## Multiple assignment In multiple assignment, the number of variables on the left side of the assignment operator (`=`) must match the number of values on the right side. @@ -55,6 +56,7 @@ For example: Since `tuples` are immutable, you can't swap elements in a `tuple`. + ## Unpacking ~~~~exercism/note @@ -80,9 +82,10 @@ If there are values that are not needed then you can use `_` to flag them: "cherry" ``` + ### Deep unpacking -Unpacking and assigning values from a `list`/`tuple` inside of a `list` or `tuple` (_also known as nested lists/tuples_), works in the same way a shallow unpacking does, but often needs qualifiers to clarify the values context or position: +Unpacking and assigning values from a `list`/`tuple` enclosed inside a `list` or `tuple` (_also known as nested lists/tuples_) works in the same way a shallow unpacking does โ€” but often needs qualifiers to clarify the context or position: ```python >>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]] @@ -119,7 +122,7 @@ ValueError: too many values to unpack (expected 1) When [unpacking a `list`/`tuple`][packing and unpacking] you can use the `*` operator to capture the "leftover" values. This is clearer than slicing the `list`/`tuple` (_which in some situations is less readable_). -For example, we can extract the first element and then assign the remaining values into a new `list` without the first element: +For example, the first element can be extracted and then the remaining values can be placed into a new `list` without the first element: ```python >>> fruits = ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"] @@ -157,7 +160,7 @@ We can also use `*` in deep unpacking: ### Unpacking a dictionary -[Unpacking a dictionary][packing and unpacking] is a bit different than unpacking a `list`/`tuple`. +[Unpacking a dictionary][packing and unpacking] is a bit different from unpacking a `list`/`tuple`. Iteration over dictionaries defaults to the **keys**. So when unpacking a `dict`, you can only unpack the **keys** and not the **values**: @@ -168,7 +171,7 @@ So when unpacking a `dict`, you can only unpack the **keys** and not the **value "apple" ``` -If you want to unpack the values then you can use the `values()` method: +If you want to unpack the values then you can use the `.values()` method: ```python >>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3} @@ -177,9 +180,9 @@ If you want to unpack the values then you can use the `values()` method: 6 ``` -If both **keys** and **values** are needed, use the `items()` method. -Using `items()` will generate tuples with **key-value** pairs. -This is because of [`dict.items()` generates an iterable with key-value `tuples`][items]. +If both **keys** and **values** are needed, use the [`.items()`][items] method. +`.items()` generates an [iterable view][view-objects] containing **key-value** pairs. +These can be unpacked into a `tuple`: ```python >>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3} @@ -210,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. @@ -238,8 +245,8 @@ This will pack all **key**-**value** pairs from one dictionary into another dict ### Packing with function parameters When you create a function that accepts an arbitrary number of arguments, you can use [`*args` or `**kwargs`][args and kwargs] in the function definition. -`*args` is used to pack an arbitrary number of positional (non-keyworded) arguments and -`**kwargs` is used to pack an arbitrary number of keyword arguments. +`*args` is used to pack an arbitrary number of positional (_non-keyword_) arguments as a `tuple` and +`**kwargs` is used to pack an arbitrary number of keyword arguments as a dictionary. Usage of `*args`: @@ -344,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]) @@ -355,8 +362,11 @@ Since `zip()` takes multiple iterables and returns a `list` of `tuples` with the ``` [args and kwargs]: https://www.geeksforgeeks.org/args-kwargs-python/ -[items]: https://www.geeksforgeeks.org/python-dictionary-items-method/ +[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/locomotive-engineer/.meta/exemplar.py b/exercises/concept/locomotive-engineer/.meta/exemplar.py index bda295d2699..f7a3a40e6c2 100644 --- a/exercises/concept/locomotive-engineer/.meta/exemplar.py +++ b/exercises/concept/locomotive-engineer/.meta/exemplar.py @@ -2,10 +2,13 @@ def get_list_of_wagons(*args): - """Return a list of wagons. + """Return a list of wagons, given an arbitrary amount of wagon numbers. - :param *args: arbitrary number of wagons. - :return: list - list of wagons. + Parameters: + An arbitrary number of wagon numbers, unpacked. + + Returns: + list: A list of wagon numbers. """ return list(args) @@ -14,9 +17,12 @@ def get_list_of_wagons(*args): def fix_list_of_wagons(each_wagons_id, missing_wagons): """Fix the list of wagons. - :param each_wagons_id: list - the list of wagons. - :param missing_wagons: list - the list of missing wagons. - :return: list - list of wagons. + Parameters: + each_wagons_id (list[int]): The list of wagons. + missing_wagons (list[int]): The list of missing wagons. + + Returns: + list[int]: The corrected list of wagons. """ first, second, locomotive, *rest = each_wagons_id @@ -27,9 +33,12 @@ def fix_list_of_wagons(each_wagons_id, missing_wagons): def add_missing_stops(route, **kwargs): """Add missing stops to route dict. - :param route: dict - the dict of routing information. - :param **kwargs: arbitrary number of stops. - :return: dict - updated route dictionary. + Parameters: + route (dict): The dict of routing information. + (dict): An arbitrary number of stops. + + Returns: + dict: The updated route dictionary. """ return {**route, "stops": list(kwargs.values())} @@ -38,9 +47,12 @@ def add_missing_stops(route, **kwargs): def extend_route_information(route, more_route_information): """Extend route information with more_route_information. - :param route: dict - the route information. - :param more_route_information: dict - extra route information. - :return: dict - extended route information. + Parameters: + route (dict): The route information. + more_route_information (dict): The extra route information. + + Returns: + dict: The extended route information. """ return {**route, **more_route_information} @@ -49,8 +61,11 @@ def extend_route_information(route, more_route_information): def fix_wagon_depot(wagons_rows): """Fix the list of rows of wagons. - :param wagons_rows: list[tuple] - the list of rows of wagons. - :return: list[tuple] - list of rows of wagons. + Parameters: + wagons_rows (list[list[tuple]]): The list of rows of wagons. + + Returns: + list[list[tuple]]: the list of rows of wagons. """ [*row_one], [*row_two], [*row_three] = zip(*wagons_rows) diff --git a/exercises/concept/locomotive-engineer/locomotive_engineer.py b/exercises/concept/locomotive-engineer/locomotive_engineer.py index 180329208cb..f57209b7a85 100644 --- a/exercises/concept/locomotive-engineer/locomotive_engineer.py +++ b/exercises/concept/locomotive-engineer/locomotive_engineer.py @@ -2,10 +2,13 @@ def get_list_of_wagons(): - """Return a list of wagons. + """Return a list of wagons, given an arbitrary amount of wagon numbers. - :param: arbitrary number of wagons. - :return: list - list of wagons. + Parameters: + An arbitrary number of wagon numbers, unpacked. + + Returns: + list: A list of wagon numbers. """ pass @@ -13,19 +16,25 @@ def get_list_of_wagons(): def fix_list_of_wagons(each_wagons_id, missing_wagons): """Fix the list of wagons. - :param each_wagons_id: list - the list of wagons. - :param missing_wagons: list - the list of missing wagons. - :return: list - list of wagons. + Parameters: + each_wagons_id (list[int]): The list of wagons. + missing_wagons (list[int]): The list of missing wagons. + + Returns: + list[int]: The corrected list of wagons. """ pass -def add_missing_stops(): +def add_missing_stops(route): """Add missing stops to route dict. - :param route: dict - the dict of routing information. - :param: arbitrary number of stops. - :return: dict - updated route dictionary. + Parameters: + route (dict): The dict of routing information. + (dict): An arbitrary number of stops. + + Returns: + dict: The updated route dictionary. """ pass @@ -33,9 +42,12 @@ def add_missing_stops(): def extend_route_information(route, more_route_information): """Extend route information with more_route_information. - :param route: dict - the route information. - :param more_route_information: dict - extra route information. - :return: dict - extended route information. + Parameters: + route (dict): The route information. + more_route_information (dict): The extra route information. + + Returns: + dict: The extended route information. """ pass @@ -43,7 +55,10 @@ def extend_route_information(route, more_route_information): def fix_wagon_depot(wagons_rows): """Fix the list of rows of wagons. - :param wagons_rows: list[list[tuple]] - the list of rows of wagons. - :return: list[list[tuple]] - list of rows of wagons. + Parameters: + wagons_rows (list[list[tuple]]): The list of rows of wagons. + + Returns: + list[list[tuple]]: the list of rows of wagons. """ pass diff --git a/exercises/concept/making-the-grade/.docs/hints.md b/exercises/concept/making-the-grade/.docs/hints.md index 3e8deff9581..eee64b21ac7 100644 --- a/exercises/concept/making-the-grade/.docs/hints.md +++ b/exercises/concept/making-the-grade/.docs/hints.md @@ -2,15 +2,15 @@ ## General -- [`while`][while-loops] loops are used for _indefinite_ (uncounted) iteration -- [`for`][for-loops] loops are used for _definite_, (counted) iteration. +- [`while`][while-loops] loops are used for _indefinite_ (uncounted) iteration. +- [`for`][for-loops] loops are used for _definite_ (counted) iteration. - The keywords [`break` and `continue`][control flow] help customize loop behavior. -- [`range(, stop, )`][range] can be used to generate a sequence for a loop counter. +- [`range(, , )`][range] can be used to generate a sequence for a loop counter. - The built-in [`enumerate()`][enumerate] will return (``, ``) pairs to iterate over. Also being familiar with the following can help with completing the tasks: -- [`lists`][list]: indexing, nested lists, [`.append`][append and pop], [`.pop()`][append and pop]. +- [`lists`][list]: indexing, nested lists, [`.append()`][append and pop], [`.pop()`][append and pop]. - [`str`][str]: `str()` constructor, using the `+` to concatenate strings, optionally, [`f-strings`][f-strings]. ## 1. Rounding Scores @@ -22,7 +22,7 @@ Also being familiar with the following can help with completing the tasks: ## 2. Non-Passing Students - There's no need to declare `loop` counters or `index` counters when iterating through an object using a `for` loop. -- A results counter does need to be set up and _incremented_ -- you'll want to `return` the count of non-passing students when the loop terminates. +- A results counter does need to be set up and _incremented_ โ€” you'll want to `return` the count of non-passing students when the loop terminates. ## 3. The "Best" diff --git a/exercises/concept/making-the-grade/.meta/exemplar.py b/exercises/concept/making-the-grade/.meta/exemplar.py index 5aa75084005..2a8f52846b0 100644 --- a/exercises/concept/making-the-grade/.meta/exemplar.py +++ b/exercises/concept/making-the-grade/.meta/exemplar.py @@ -4,8 +4,11 @@ def round_scores(student_scores): """Round all provided student scores. - :param student_scores: list - float or int of student exam scores. - :return: list - student scores *rounded* to nearest integer value. + Parameters: + student_scores (list[float]): Student exam scores. + + Returns: + list[int]: Student scores *rounded* to the nearest integer value. """ rounded = [] @@ -17,8 +20,11 @@ def round_scores(student_scores): def count_failed_students(student_scores): """Count the number of failing students out of the group provided. - :param student_scores: list - containing int student scores. - :return: int - count of student scores at or below 40. + Parameters: + student_scores (list[int]): Student scores as ints. + + Returns: + int: The count of student scores at or below 40. """ non_passing = 0 @@ -31,9 +37,12 @@ def count_failed_students(student_scores): def above_threshold(student_scores, threshold): """Determine how many of the provided student scores were 'the best' based on the provided threshold. - :param student_scores: list - of integer scores. - :param threshold: int - threshold to cross to be the "best" score. - :return: list - of integer scores that are at or above the "best" threshold. + Parameters: + student_scores (list[int]): Integer scores. + threshold (int): The threshold to cross to be the "best" score. + + Returns: + list[int]: Integer scores that are at or above the "best" threshold. """ above = [] @@ -47,11 +56,14 @@ def above_threshold(student_scores, threshold): def letter_grades(highest): """Create a list of grade thresholds based on the provided highest grade. - :param highest: int - value of highest exam score. - :return: list - of lower threshold scores for each D-A letter grade interval. - For example, where the highest score is 100, and failing is <= 40, - The result would be [41, 56, 71, 86]: + Parameters: + highest (int): The value of the highest exam score. + Returns: + list[int]: Lower threshold scores for each D-A letter grade interval. + + For example, where the highest score is 100, and failing is <= 40, + The result would be [41, 56, 71, 86]: 41 <= "D" <= 55 56 <= "C" <= 70 71 <= "B" <= 85 @@ -66,11 +78,14 @@ def letter_grades(highest): def student_ranking(student_scores, student_names): - """Organize the student's rank, name, and grade information in ascending order. + """Organize the student's rank, name, and grade information in descending order. + + Parameters: + student_scores (list): Scores in descending order. + student_names (list[str]): Student names by exam score in descending order. - :param student_scores: list - of scores in descending order. - :param student_names: list - of string names by exam score in descending order. - :return: list - of strings in format [". : "]. + Returns: + list[str]: Strings in format [". : "]. """ results = [] @@ -84,8 +99,11 @@ def student_ranking(student_scores, student_names): def perfect_score(student_info): """Create a list that contains the name and grade of the first student to make a perfect score on the exam. - :param student_info: list - of [, ] lists. - :return: list - first `[, 100]` or `[]` if no student score of 100 is found. + Parameters: + student_info (list[list[str, int]]): List of [, ] lists. + + Returns: + list: First `[, 100]` found OR `[]` if no student score of 100 is found. """ result = [] diff --git a/exercises/concept/making-the-grade/loops.py b/exercises/concept/making-the-grade/loops.py index ecf7d06774c..5bc0c72722f 100644 --- a/exercises/concept/making-the-grade/loops.py +++ b/exercises/concept/making-the-grade/loops.py @@ -4,8 +4,11 @@ def round_scores(student_scores): """Round all provided student scores. - :param student_scores: list - float or int of student exam scores. - :return: list - student scores *rounded* to nearest integer value. + Parameters: + student_scores (list[float]): Student exam scores. + + Returns: + list[int]: Student scores *rounded* to the nearest integer value. """ pass @@ -14,8 +17,11 @@ def round_scores(student_scores): def count_failed_students(student_scores): """Count the number of failing students out of the group provided. - :param student_scores: list - containing int student scores. - :return: int - count of student scores at or below 40. + Parameters: + student_scores (list[int]): Student scores as ints. + + Returns: + int: The count of student scores at or below 40. """ pass @@ -24,9 +30,12 @@ def count_failed_students(student_scores): def above_threshold(student_scores, threshold): """Determine how many of the provided student scores were 'the best' based on the provided threshold. - :param student_scores: list - of integer scores. - :param threshold: int - threshold to cross to be the "best" score. - :return: list - of integer scores that are at or above the "best" threshold. + Parameters: + student_scores (list[int]): Integer scores. + threshold (int): The threshold to cross to be the "best" score. + + Returns: + list[int]: Integer scores that are at or above the "best" threshold. """ pass @@ -35,11 +44,14 @@ def above_threshold(student_scores, threshold): def letter_grades(highest): """Create a list of grade thresholds based on the provided highest grade. - :param highest: int - value of highest exam score. - :return: list - of lower threshold scores for each D-A letter grade interval. - For example, where the highest score is 100, and failing is <= 40, - The result would be [41, 56, 71, 86]: + Parameters: + highest (int): The value of the highest exam score. + Returns: + list[int]: Lower threshold scores for each D-A letter grade interval. + + For example, where the highest score is 100, and failing is <= 40, + The result would be [41, 56, 71, 86]: 41 <= "D" <= 55 56 <= "C" <= 70 71 <= "B" <= 85 @@ -52,9 +64,12 @@ def letter_grades(highest): def student_ranking(student_scores, student_names): """Organize the student's rank, name, and grade information in descending order. - :param student_scores: list - of scores in descending order. - :param student_names: list - of string names by exam score in descending order. - :return: list - of strings in format [". : "]. + Parameters: + student_scores (list): Scores in descending order. + student_names (list[str]): Student names by exam score in descending order. + + Returns: + list[str]: Strings in format [". : "]. """ pass @@ -63,8 +78,11 @@ def student_ranking(student_scores, student_names): def perfect_score(student_info): """Create a list that contains the name and grade of the first student to make a perfect score on the exam. - :param student_info: list - of [, ] lists. - :return: list - first `[, 100]` or `[]` if no student score of 100 is found. + Parameters: + student_info (list[list[str, int]]): List of [, ] lists. + + Returns: + list: First `[, 100]` found OR `[]` if no student score of 100 is found. """ pass diff --git a/exercises/concept/mecha-munch-management/.docs/hints.md b/exercises/concept/mecha-munch-management/.docs/hints.md index 3287768ff5f..2c8b35b2cc1 100644 --- a/exercises/concept/mecha-munch-management/.docs/hints.md +++ b/exercises/concept/mecha-munch-management/.docs/hints.md @@ -9,13 +9,13 @@ It's OK to be simple and direct with the functions you are writing. The dictionary section of the [official tutorial][dicts-docs] and the mapping type [official library reference][mapping-types-dict] are excellent places to look for more help with all these methods. -## 1. Add Item(s) to the Users Shopping Cart +## 1. Add Item(s) to the User's Shopping Cart - You will need to iterate through each item in `items_to_add`. - You can avoid a `KeyError` when a key is missing by using a `dict` [method][set-default] that takes a _default value_ as one of its arguments. - It is also possible to accomplish the same thing manually in the `loop` by using some checking and error handling, but the `dict` method is easier. -## 2. Read in Items Listed in the Users Notes App +## 2. Read in Items Listed in the User's Notes App - Remember, Python's got a method for _everything_. This one is a _classmethod_ that's an easy way to [populate a `dict`][fromkeys] with keys. - This `dict` method returns a _new dictionary_, populated with default values. If no value is given, the default value will become `None` @@ -25,13 +25,13 @@ The dictionary section of the [official tutorial][dicts-docs] and the mapping ty - Don't overthink this one! This can be solved in **one** `dict` method call. - The key word here is .... [_update_][update]. -## 4. Sort the Items in the User Cart +## 4. Sort the Items in the User's Cart - What method would you call to get an [iterable view of items][items] in the dictionary? - If you had a `list` or a `tuple`, what [`built-in`][builtins] function might you use to sort them? - The built-in function you want is the one that returns a _copy_, and doesn't mutate the original. -## 5. Send User Shopping Cart to Store for Fulfillment +## 5. Send the User's Shopping Cart to the Store for Fulfillment - Having a fresh, empty dictionary here as the `fulfillment_cart` might be handy for adding in items. - `Looping` through the members of the cart might be the most direct way of accessing things here. @@ -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 13d88f39fa2..fc3d0ff7cb1 100644 --- a/exercises/concept/mecha-munch-management/.docs/instructions.md +++ b/exercises/concept/mecha-munch-management/.docs/instructions.md @@ -1,10 +1,10 @@ # Instructions -Mecha Munchโ„ข, a grocery shopping automation company has just hired you to work on their ordering app. +Mecha Munchโ„ข, a grocery shopping automation company, has just hired you to work on their ordering app. Your team is tasked with building an MVP (_[minimum viable product][mvp]_) that manages all the basic shopping cart activities, allowing users to add, remove, and sort their grocery orders. Thankfully, a different team is handling all the money and check-out functions! -## 1. Add Item(s) to the Users Shopping Cart +## 1. Add Item(s) to the User's Shopping Cart The MVP should allow the user to add items to their shopping cart. This could be a single item or multiple items at once. @@ -13,25 +13,25 @@ If a user wants to add 2 Oranges, 'Oranges' will appear twice in the input itera If the user already has the item in their cart, the cart quantity should be increased by 1. If the item is _new_ to the cart, it should be added with a quantity of 1. -Create the function `add_items(, )` that takes a cart dictionary and any list-like iterable of items to add as arguments. +Create the function `add_item(, )` that takes a cart dictionary and any list-like iterable of items to add as arguments. It should return a new/updated shopping cart dictionary for the user. ```python ->>> add_items({'Banana': 3, 'Apple': 2, 'Orange': 1}, +>>> add_item({'Banana': 3, 'Apple': 2, 'Orange': 1}, ('Apple', 'Apple', 'Orange', 'Apple', 'Banana')) {'Banana': 4, 'Apple': 5, 'Orange': 2} ->>> add_items({'Banana': 3, 'Apple': 2, 'Orange': 1}, +>>> add_item({'Banana': 3, 'Apple': 2, 'Orange': 1}, ['Banana', 'Orange', 'Blueberries', 'Banana']) {'Banana': 5, 'Apple': 2, 'Orange': 2, 'Blueberries': 1} ``` -## 2. Read in Items Listed in the Users Notes App +## 2. Read in Items Listed in the User's Notes App Uh-oh. Looks like the product team is engaging in [feature creep][feature creep]. They want to add extra functionality to the MVP. -The application now has to create a shopping cart by reading items off a users notes app. +The application now has to create a shopping cart by reading items off a user's notes app. Convenient for the users, but slightly more work for the team. Create the function `read_notes()` that can take any list-like iterable as an argument. @@ -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}} ``` @@ -93,7 +97,7 @@ Create the function `sort_entries()` that takes a shopping cart/dictionary ## 5. Send User Shopping Cart to Store for Fulfillment -The app needs to send a given users cart to the store for fulfillment. +The app needs to send a given user's cart to the store for fulfillment. However, the shoppers in the store need to know which store aisle the item can be found in and if the item needs refrigeration. So (_rather arbitrarily_) the "fulfillment cart" needs to be sorted in reverse alphabetical order with item quantities combined with location and refrigeration information. diff --git a/exercises/concept/mecha-munch-management/.docs/introduction.md b/exercises/concept/mecha-munch-management/.docs/introduction.md index 983b905c276..6f63d8acd60 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', @@ -171,7 +171,7 @@ Where keys in the two dictionaries _overlap_, the `value` in `dict_one` will be 'Green Treeline': '#478559', 'Purple baseline': '#161748'} ``` -## Merge or Update Dictionaries Via the Union (`|`) Operators +## Merge or Update Dictionaries Using Union (`|` and `|=`) Operators Python 3.9 introduces a different means of merging `dicts`: the `union` operators. `dict_one | dict_two` will create a **new dictionary**, made up of the (`key`, `value`) pairs of `dict_one` and `dict_two`. @@ -191,7 +191,7 @@ When both dictionaries share keys, `dict_two` values take precedence. 'Purple baseline': '#161748'} ``` -`dict_one |= other` behaves similar to `.update()`, but in this case, `other` can be either a `dict` or an iterable of (`key`, `value`) pairs: +`dict_one |= other` behaves similar to `.update()`, but in this case, `other` can be either a `dict` or an iterable of (`key`, `value`) pairs: ```python >>> palette_III = {'Grassy Green': (155, 196, 0), 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/.meta/exemplar.py b/exercises/concept/mecha-munch-management/.meta/exemplar.py index ea25110a3be..e5074607cba 100644 --- a/exercises/concept/mecha-munch-management/.meta/exemplar.py +++ b/exercises/concept/mecha-munch-management/.meta/exemplar.py @@ -4,9 +4,12 @@ def add_item(current_cart, items_to_add): """Add items to shopping cart. - :param current_cart: dict - the current shopping cart. - :param items_to_add: iterable - items to add to the cart. - :return: dict - the updated user cart dictionary. + Parameters: + current_cart (dict): The current shopping cart. + items_to_add (iterable): The items to add to the cart. + + Returns: + dict: The updated user cart dictionary. """ for item in items_to_add: @@ -19,8 +22,11 @@ def add_item(current_cart, items_to_add): def read_notes(notes): """Create user cart from an iterable notes entry. - :param notes: iterable of items to add to cart. - :return: dict - a user shopping cart dictionary. + Parameters: + notes (iterable): Group of items to add to cart. + + Returns: + dict: A user shopping cart dictionary. """ return dict.fromkeys(notes, 1) @@ -29,9 +35,12 @@ def read_notes(notes): 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. - :return: dict - updated "recipe ideas" dict. + Parameters: + ideas (dict): The "recipe ideas" dict. + recipe_updates (iterable): Updates for the ideas section. + + Returns: + dict: The updated "recipe ideas" dict. """ ideas.update(recipe_updates) @@ -39,22 +48,29 @@ def update_recipes(ideas, recipe_updates): def sort_entries(cart): - """Sort a users shopping cart in alphabetically order. + """Sort a user's shopping cart in alphabetical order. + + Parameters: + cart (dict): A user's shopping cart dictionary. - :param cart: dict - a users shopping cart dictionary. - :return: dict - users shopping cart sorted in alphabetical order. + Returns: + dict: A user's shopping cart sorted in alphabetical order. """ return dict(sorted(cart.items())) def send_to_store(cart, aisle_mapping): - """Combine users order to aisle and refrigeration information. + """Combine user's order to aisle and refrigeration information. - :param cart: dict - users shopping cart dictionary. - :param aisle_mapping: dict - aisle and refrigeration information dictionary. - :return: dict - fulfillment dictionary ready to send to store. + Parameters: + cart (dict): The user's shopping cart dictionary. + aisle_mapping (dict): The aisle and refrigeration information dictionary. + + Returns: + dict: The fulfillment dictionary ready to send to store. """ + fulfillment_cart = {} for key in cart.keys(): @@ -66,9 +82,12 @@ def send_to_store(cart, aisle_mapping): def update_store_inventory(fulfillment_cart, store_inventory): """Update store inventory levels with user order. - :param fulfillment cart: dict - fulfillment cart to send to store. - :param store_inventory: dict - store available inventory - :return: dict - store_inventory updated. + Parameters: + fulfillment cart (dict): The fulfillment cart to send to store. + store_inventory (dict): The stores available inventory. + + Returns: + dict: The store_inventory updated. """ for key, values in fulfillment_cart.items(): diff --git a/exercises/concept/mecha-munch-management/dict_methods.py b/exercises/concept/mecha-munch-management/dict_methods.py index f502fe00ab9..a2a535c4b7b 100644 --- a/exercises/concept/mecha-munch-management/dict_methods.py +++ b/exercises/concept/mecha-munch-management/dict_methods.py @@ -4,9 +4,12 @@ def add_item(current_cart, items_to_add): """Add items to shopping cart. - :param current_cart: dict - the current shopping cart. - :param items_to_add: iterable - items to add to the cart. - :return: dict - the updated user cart dictionary. + Parameters: + current_cart (dict): The current shopping cart. + items_to_add (iterable): The items to add to the cart. + + Returns: + dict: The updated user cart dictionary. """ pass @@ -15,8 +18,11 @@ def add_item(current_cart, items_to_add): def read_notes(notes): """Create user cart from an iterable notes entry. - :param notes: iterable of items to add to cart. - :return: dict - a user shopping cart dictionary. + Parameters: + notes (iterable): Group of items to add to cart. + + Returns: + dict: A user shopping cart dictionary. """ pass @@ -25,30 +31,39 @@ def read_notes(notes): 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. - :return: dict - updated "recipe ideas" dict. + Parameters: + ideas (dict): The "recipe ideas" dict. + recipe_updates (iterable): Updates for the ideas section. + + Returns: + dict: The updated "recipe ideas" dict. """ pass def sort_entries(cart): - """Sort a users shopping cart in alphabetically order. + """Sort a user's shopping cart in alphabetical order. - :param cart: dict - a users shopping cart dictionary. - :return: dict - users shopping cart sorted in alphabetical order. + Parameters: + cart (dict): A user's shopping cart dictionary. + + Returns: + dict: A user's shopping cart sorted in alphabetical order. """ pass def send_to_store(cart, aisle_mapping): - """Combine users order to aisle and refrigeration information. + """Combine user's order to aisle and refrigeration information. + + Parameters: + cart (dict): The user's shopping cart dictionary. + aisle_mapping (dict): The aisle and refrigeration information dictionary. - :param cart: dict - users shopping cart dictionary. - :param aisle_mapping: dict - aisle and refrigeration information dictionary. - :return: dict - fulfillment dictionary ready to send to store. + Returns: + dict: The fulfillment dictionary ready to send to store. """ pass @@ -57,9 +72,12 @@ def send_to_store(cart, aisle_mapping): def update_store_inventory(fulfillment_cart, store_inventory): """Update store inventory levels with user order. - :param fulfillment cart: dict - fulfillment cart to send to store. - :param store_inventory: dict - store available inventory - :return: dict - store_inventory updated. + Parameters: + fulfillment cart (dict): The fulfillment cart to send to store. + store_inventory (dict): The stores available inventory. + + Returns: + dict: The store_inventory updated. """ pass diff --git a/exercises/concept/mecha-munch-management/dict_methods_test.py b/exercises/concept/mecha-munch-management/dict_methods_test.py index 63fc1874a05..376a48aa1e7 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,22 +56,8 @@ 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): - with self.subTest(f'variation #{variant}', input_data=input_data, expecred=expected): + for variant, (input_data, expected) in enumerate(sort_entries_data, start=1): + with self.subTest(f'variation #{variant}', input_data=input_data, expected=expected): actual_result = sort_entries(input_data) error_msg = (f'Called sort_entries({input_data}). ' f'The function returned {actual_result}, but the tests ' @@ -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/.meta/exemplar.py b/exercises/concept/meltdown-mitigation/.meta/exemplar.py index c2d8ddd7ede..ef2bb20147d 100644 --- a/exercises/concept/meltdown-mitigation/.meta/exemplar.py +++ b/exercises/concept/meltdown-mitigation/.meta/exemplar.py @@ -4,14 +4,19 @@ def is_criticality_balanced(temperature, neutrons_emitted): """Verify criticality is balanced. - :param temperature: int or float - temperature value in kelvin. - :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: - - 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. + Parameters: + temperature (int or float): The temperature value in kelvin. + neutrons_emitted (int or float): The number of neutrons emitted per second. + + Returns: + bool: Is criticality balanced? + + Note: + 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. + """ output = temperature * neutrons_emitted @@ -26,21 +31,24 @@ def is_criticality_balanced(temperature, neutrons_emitted): def reactor_efficiency(voltage, current, theoretical_max_power): """Assess reactor efficiency zone. - :param voltage: int or float - voltage value. - :param current: int or float - current value. - :param theoretical_max_power: int or float - power that corresponds to a 100% efficiency. - :return: str - one of ('green', 'orange', 'red', or 'black'). + Parameters: + voltage (int or float): Voltage value. + current (int or float): Current value. + theoretical_max_power (int or float): The power level that corresponds to a 100% efficiency. - Efficiency can be grouped into 4 bands: + Returns: + str: One of ('green', 'orange', 'red', or 'black'). - 1. green -> efficiency of 80% or more, - 2. orange -> efficiency of less than 80% but at least 60%, - 3. red -> efficiency below 60%, but still 30% or more, - 4. black -> less than 30% efficient. + Note: + Efficiency can be grouped into 4 bands: + 1. green -> efficiency of 80% or more, + 2. orange -> efficiency of less than 80% but at least 60%, + 3. red -> efficiency below 60%, but still 30% or more, + 4. black -> less than 30% efficient. - The percentage value is calculated as - (generated power/ theoretical max power)*100 - where generated power = voltage * current + The percentage value is calculated as + (generated power/ theoretical max power)*100 + where generated power = voltage * current """ generated_power = voltage * current @@ -61,14 +69,18 @@ def reactor_efficiency(voltage, current, theoretical_max_power): def fail_safe(temperature, neutrons_produced_per_second, threshold): """Assess and return status code for the reactor. - :param temperature: int or float - value of the temperature in kelvin. - :param neutrons_produced_per_second: int or float - neutron flux. - :param threshold: int or float - threshold for category. - :return: str - one of ('LOW', 'NORMAL', 'DANGER'). + Parameters: + temperature (int or float): The value of the temperature in kelvin. + neutrons_produced_per_second (int or float): The neutron flux. + threshold (int or float): The threshold for the category. + + Returns: + str: One of ('LOW', 'NORMAL', 'DANGER'). - 1. 'LOW' -> `temperature * neutrons per second` < 90% of `threshold` - 2. 'NORMAL' -> `temperature * neutrons per second` +/- 10% of `threshold` - 3. 'DANGER' -> `temperature * neutrons per second` is not in the above-stated ranges + Note: + 1. 'LOW' -> `temperature * neutrons per second` < 90% of `threshold` + 2. 'NORMAL' -> `temperature * neutrons per second` +/- 10% of `threshold` + 3. 'DANGER' -> `temperature * neutrons per second` is not in the above-stated ranges """ output = temperature * neutrons_produced_per_second diff --git a/exercises/concept/meltdown-mitigation/conditionals.py b/exercises/concept/meltdown-mitigation/conditionals.py index 1eb0a571ff5..01bbf5af4d0 100644 --- a/exercises/concept/meltdown-mitigation/conditionals.py +++ b/exercises/concept/meltdown-mitigation/conditionals.py @@ -4,14 +4,19 @@ def is_criticality_balanced(temperature, neutrons_emitted): """Verify criticality is balanced. - :param temperature: int or float - temperature value in kelvin. - :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: - - 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. + Parameters: + temperature (int or float): The temperature value in kelvin. + neutrons_emitted (int or float): The number of neutrons emitted per second. + + Returns: + bool: Is criticality balanced? + + Note: + 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. + """ pass @@ -20,21 +25,24 @@ def is_criticality_balanced(temperature, neutrons_emitted): def reactor_efficiency(voltage, current, theoretical_max_power): """Assess reactor efficiency zone. - :param voltage: int or float - voltage value. - :param current: int or float - current value. - :param theoretical_max_power: int or float - power that corresponds to a 100% efficiency. - :return: str - one of ('green', 'orange', 'red', or 'black'). + Parameters: + voltage (int or float): Voltage value. + current (int or float): Current value. + theoretical_max_power (int or float): The power level that corresponds to a 100% efficiency. - Efficiency can be grouped into 4 bands: + Returns: + str: One of ('green', 'orange', 'red', or 'black'). - 1. green -> efficiency of 80% or more, - 2. orange -> efficiency of less than 80% but at least 60%, - 3. red -> efficiency below 60%, but still 30% or more, - 4. black -> less than 30% efficient. + Note: + Efficiency can be grouped into 4 bands: + 1. green -> efficiency of 80% or more, + 2. orange -> efficiency of less than 80% but at least 60%, + 3. red -> efficiency below 60%, but still 30% or more, + 4. black -> less than 30% efficient. - The percentage value is calculated as - (generated power/ theoretical max power)*100 - where generated power = voltage * current + The percentage value is calculated as + (generated power/ theoretical max power)*100 + where generated power = voltage * current """ pass @@ -43,14 +51,18 @@ def reactor_efficiency(voltage, current, theoretical_max_power): def fail_safe(temperature, neutrons_produced_per_second, threshold): """Assess and return status code for the reactor. - :param temperature: int or float - value of the temperature in kelvin. - :param neutrons_produced_per_second: int or float - neutron flux. - :param threshold: int or float - threshold for category. - :return: str - one of ('LOW', 'NORMAL', 'DANGER'). + Parameters: + temperature (int or float): The value of the temperature in kelvin. + neutrons_produced_per_second (int or float): The neutron flux. + threshold (int or float): The threshold for the category. + + Returns: + str: One of ('LOW', 'NORMAL', 'DANGER'). - 1. 'LOW' -> `temperature * neutrons per second` < 90% of `threshold` - 2. 'NORMAL' -> `temperature * neutrons per second` +/- 10% of `threshold` - 3. 'DANGER' -> `temperature * neutrons per second` is not in the above-stated ranges + Note: + 1. 'LOW' -> `temperature * neutrons per second` < 90% of `threshold` + 2. 'NORMAL' -> `temperature * neutrons per second` +/- 10% of `threshold` + 3. 'DANGER' -> `temperature * neutrons per second` is not in the above-stated ranges """ pass diff --git a/exercises/concept/plane-tickets/.docs/instructions.md b/exercises/concept/plane-tickets/.docs/instructions.md index edd92680b1d..7584c53f38e 100644 --- a/exercises/concept/plane-tickets/.docs/instructions.md +++ b/exercises/concept/plane-tickets/.docs/instructions.md @@ -75,10 +75,10 @@ The function should then return a _dictionary_ of `passenger` as _key_, and `sea Conda Airlines would like to have a unique code for each ticket. Since they are a big airline, they have a lot of flights. This means that there are multiple flights with the same seat number. -They want you to create a system that creates a unique ticket that is _12_ characters long string code for identification. +They want you to create a system that formulates a unique ticket that is a _12_ character long string. This code begins with the `assigned_seat` followed by the `flight_id`. -The rest of the code is appended by `0s`. +The rest of the code is filled with `0s`. Implement a function `generate_codes(, )` that accepts a `list` of `seat_numbers` and a `string` with the flight number. The function should then return a `generator` that yields a `ticket_number`. diff --git a/exercises/concept/plane-tickets/.docs/introduction.md b/exercises/concept/plane-tickets/.docs/introduction.md index d17f90c812c..ac0a53a8ef2 100644 --- a/exercises/concept/plane-tickets/.docs/introduction.md +++ b/exercises/concept/plane-tickets/.docs/introduction.md @@ -1,7 +1,7 @@ # Generators -A `generator` is a function or expression that returns a special type of [iterator][iterator] called [generator iterator][generator-iterator]. -`Generator-iterators` are [lazy][lazy iterator]: they do not store their `values` in memory, but _generate_ their values when needed. +A `generator` is a function or expression that returns a special type of [iterator][iterator] called a [`generator iterator`][generator-iterator]. +`Generator-iterator`s are [lazy][lazy iterator]: they do not store their `values` in memory, but _generate_ their values when needed. A generator function looks like any other function, but contains one or more [yield expressions][yield expression]. Each `yield` will suspend code execution, saving the current execution state (_including all local variables and try-statements_). @@ -144,8 +144,6 @@ Now whenever `__next__()` is called on the `infinite_sequence` object, it will r [generator-iterator]: https://docs.python.org/3.11/glossary.html#term-generator-iterator -[iterables]: https://wiki.python.org/moin/Iterator [iterator]: https://docs.python.org/3.11/glossary.html#term-iterator -[lazy evaluation]: https://en.wikipedia.org/wiki/Lazy_evaluation [lazy iterator]: https://en.wikipedia.org/wiki/Lazy_evaluation [yield expression]: https://docs.python.org/3.11/reference/expressions.html#yield-expressions diff --git a/exercises/concept/plane-tickets/.meta/exemplar.py b/exercises/concept/plane-tickets/.meta/exemplar.py index 8261795c66e..498a54e6cbe 100644 --- a/exercises/concept/plane-tickets/.meta/exemplar.py +++ b/exercises/concept/plane-tickets/.meta/exemplar.py @@ -8,13 +8,16 @@ def generate_seat_letters(number): """Generate a series of letters for airline seats. - :param number: int - total number of seat letters to be generated. - :return: generator - generator that yields seat letters. + Parameters: + number (int): Total number of seat letters to be generated. - Seat letters are generated from A to D. - After D it should start again with A. + Returns: + generator: A generator that yields seat letters. - Example: A, B, C, D + Note: + Seat letters are generated from A to D. + After D the sequence starts again with A. + For example: A, B, C, D, A, B """ @@ -25,17 +28,18 @@ def generate_seat_letters(number): def generate_seats(number): """Generate a series of identifiers for airline seats. - :param number: int - total number of seats to be generated. - :return: generator - generator that yields seat numbers. + Parameters: + number (int): The total number of seats to be generated. - A seat number consists of the row number and the seat letter. + Returns: + generator: A generator that yields seat numbers. - There is no row 13. - Each row has 4 seats. + Note: + A seat number consists of the row number and the seat letter. + There is no row 13, and each row has 4 seats. - Seats should be sorted from low to high. - - Example: 3C, 3D, 4A, 4B + Seats should be sorted from low to high. + For example: 3C, 3D, 4A, 4B """ @@ -53,10 +57,12 @@ def generate_seats(number): def assign_seats(passengers): """Assign seats to passengers. - :param passengers: list[str] - a list of strings containing names of passengers. - :return: dict - with the names of the passengers as keys and seat numbers as values. + Parameters: + passengers (list[str]): A list of strings containing names of passengers. - Example output: {"Adele": "1A", "Bjรถrk": "1B"} + Returns: + dict: With passenger names as keys and seat numbers as values. + Example output: {"Adele": "1A", "Bjรถrk": "1B"} """ @@ -66,12 +72,16 @@ def assign_seats(passengers): output[passenger] = seat_number return output + def generate_codes(seat_numbers, flight_id): """Generate codes for a ticket. - :param seat_numbers: list[str] - list of seat numbers. - :param flight_id: str - string containing the flight identifier. - :return: generator - generator that yields 12 character long ticket codes. + Parameters: + seat_numbers (list[str]): A list of seat numbers. + flight_id (str): A string containing the flight identifier. + + Returns: + generator: A generator that yields 12 character long ticket codes. """ diff --git a/exercises/concept/plane-tickets/generators.py b/exercises/concept/plane-tickets/generators.py index 2f88c0619a5..c55dc9434d2 100644 --- a/exercises/concept/plane-tickets/generators.py +++ b/exercises/concept/plane-tickets/generators.py @@ -4,13 +4,16 @@ def generate_seat_letters(number): """Generate a series of letters for airline seats. - :param number: int - total number of seat letters to be generated. - :return: generator - generator that yields seat letters. + Parameters: + number (int): Total number of seat letters to be generated. - Seat letters are generated from A to D. - After D it should start again with A. + Returns: + generator: A generator that yields seat letters. - Example: A, B, C, D + Note: + Seat letters are generated from A to D. + After D the sequence starts again with A. + For example: A, B, C, D, A, B """ @@ -20,40 +23,48 @@ def generate_seat_letters(number): def generate_seats(number): """Generate a series of identifiers for airline seats. - :param number: int - total number of seats to be generated. - :return: generator - generator that yields seat numbers. + Parameters: + number (int): The total number of seats to be generated. - A seat number consists of the row number and the seat letter. + Returns: + generator: A generator that yields seat numbers. - There is no row 13. - Each row has 4 seats. + Note: + A seat number consists of the row number and the seat letter. + There is no row 13, and each row has 4 seats. - Seats should be sorted from low to high. - - Example: 3C, 3D, 4A, 4B + Seats should be sorted from low to high. + For example: 3C, 3D, 4A, 4B """ pass + def assign_seats(passengers): """Assign seats to passengers. - :param passengers: list[str] - a list of strings containing names of passengers. - :return: dict - with the names of the passengers as keys and seat numbers as values. + Parameters: + passengers (list[str]): A list of strings containing names of passengers. - Example output: {"Adele": "1A", "Bjรถrk": "1B"} + Returns: + dict: With passenger names as keys and seat numbers as values. + Example output: {"Adele": "1A", "Bjรถrk": "1B"} """ pass + def generate_codes(seat_numbers, flight_id): """Generate codes for a ticket. - :param seat_numbers: list[str] - list of seat numbers. - :param flight_id: str - string containing the flight identifier. - :return: generator - generator that yields 12 character long ticket codes. + Parameters: + seat_numbers (list[str]): A list of seat numbers. + flight_id (str): A string containing the flight identifier. + + Returns: + generator: A generator that yields 12 character long ticket codes. """ diff --git a/exercises/concept/plane-tickets/generators_test.py b/exercises/concept/plane-tickets/generators_test.py index 12a3532aa5a..1596d424ed4 100644 --- a/exercises/concept/plane-tickets/generators_test.py +++ b/exercises/concept/plane-tickets/generators_test.py @@ -134,4 +134,7 @@ def test_generate_codes(self): f'The function returned {actual_result}, but the tests ' f'expected {expected} when generating ticket numbers.') - self.assertEqual(list(generate_codes(seat_numbers, flight_id)), expected, msg=error_message) + # Note: DO NOT call the function here again, in case the student is using list.pop() + # to process the input. If another call is done with that condition, + # the test will fail with a terrible error message. + self.assertEqual(actual_result, expected, msg=error_message) diff --git a/exercises/concept/tisbury-treasure-hunt/.docs/hints.md b/exercises/concept/tisbury-treasure-hunt/.docs/hints.md index 55697511dc0..168e93ba7ff 100644 --- a/exercises/concept/tisbury-treasure-hunt/.docs/hints.md +++ b/exercises/concept/tisbury-treasure-hunt/.docs/hints.md @@ -3,7 +3,7 @@ ## General -- [Tuples][tuples] are immutable [sequence Types][sequence types] that can contain any data type. +- [Tuples][tuples] are immutable [sequence types][sequence types] that can contain any data type. - Tuples are [iterable][iterable]. If you need indexes as well as values, use [`enumerate()`][enumerate] - Elements within tuples can be accessed via [bracket notation][bracket notation], using a zero-based index from the left, or -1 from the right. Other [Common Sequence Operations][common sequence operations] can also be used when working with tuples. @@ -32,7 +32,7 @@ - Remember: tuples are _immutable_, but the contents can be accessed via _index_ using _bracket notation_. - Tuples don't have to use parentheses unless there is _ambiguity_. - Python has multiple methods of string formatting. [`str.format()`][str.format] and [`f-strings`][f-strings] are two very common ones. -- There are multiple textual formatting options available via Pythons [`format specification mini-language`][format specification mini-language]. +- There are multiple textual formatting options available via Python's [`format specification mini-language`][format specification mini-language]. [bracket notation]: https://stackoverflow.com/questions/30250282/whats-the-difference-between-the-square-bracket-and-dot-notations-in-python diff --git a/exercises/concept/tisbury-treasure-hunt/.meta/exemplar.py b/exercises/concept/tisbury-treasure-hunt/.meta/exemplar.py index 1b4baa26aa7..e4429a0e1e4 100644 --- a/exercises/concept/tisbury-treasure-hunt/.meta/exemplar.py +++ b/exercises/concept/tisbury-treasure-hunt/.meta/exemplar.py @@ -4,8 +4,11 @@ def get_coordinate(record): """Return coordinate value from a tuple containing the treasure name, and treasure coordinate. - :param record: tuple - with a (treasure, coordinate) pair. - :return: str - the extracted map coordinate. + Parameters: + record (tuple): A (treasure, coordinate) pair. + + Returns: + str: The extracted map coordinate. """ return record[1] @@ -14,8 +17,11 @@ def get_coordinate(record): def convert_coordinate(coordinate): """Split the given coordinate into tuple containing its individual components. - :param coordinate: str - a string map coordinate - :return: tuple - the string coordinate split into its individual components. + Parameters: + coordinate (str): A string map coordinate. + + Returns: + tuple: The string coordinate split into its individual components. """ return tuple(coordinate) @@ -24,9 +30,12 @@ def convert_coordinate(coordinate): def compare_records(azara_record, rui_record): """Compare two record types and determine if their coordinates match. - :param azara_record: tuple - a (treasure, coordinate) pair. - :param rui_record: tuple - a (location, tuple(coordinate_1, coordinate_2), quadrant) trio. - :return: bool - do the coordinates match? + Parameters: + azara_record (tuple): A (treasure, coordinate) pair. + rui_record (tuple): A (location, tuple(coordinate_1, coordinate_2), quadrant) trio. + + Returns: + bool: Do the coordinates match? """ return convert_coordinate(azara_record[1]) in rui_record @@ -35,9 +44,12 @@ def compare_records(azara_record, rui_record): def create_record(azara_record, rui_record): """Combine the two record types (if possible) and create a combined record group. - :param azara_record: tuple - a (treasure, coordinate) pair. - :param rui_record: tuple - a (location, coordinate, quadrant) trio. - :return: tuple or str - the combined record (if compatible), or the string "not a match" (if incompatible). + Parameters: + azara_record (tuple): A (treasure, coordinate) pair. + rui_record (tuple): A (location, coordinate, quadrant) trio. + + Returns: + tuple or str: The combined record (if compatible), or the string "not a match" (if incompatible). """ result = "not a match" @@ -51,12 +63,16 @@ def create_record(azara_record, rui_record): def clean_up(combined_record_group): """Clean up a combined record group into a multi-line string of single records. - :param combined_record_group: tuple - everything from both participants. - :return: str - everything "cleaned", excess coordinates and information are removed. + Parameters: + combined_record_group (tuple): Everything from both participants. + + Returns: + str: Everything "cleaned", excess coordinates and information are removed. - The return statement should be a multi-lined string with items separated by newlines. + Note: + The return statement is a multi-lined string with items separated by newlines. + (see HINTS.md for an example). - (see HINTS.md for an example). """ report = "" diff --git a/exercises/concept/tisbury-treasure-hunt/tuples.py b/exercises/concept/tisbury-treasure-hunt/tuples.py index 92336d88ec1..459557f334b 100644 --- a/exercises/concept/tisbury-treasure-hunt/tuples.py +++ b/exercises/concept/tisbury-treasure-hunt/tuples.py @@ -4,8 +4,11 @@ def get_coordinate(record): """Return coordinate value from a tuple containing the treasure name, and treasure coordinate. - :param record: tuple - with a (treasure, coordinate) pair. - :return: str - the extracted map coordinate. + Parameters: + record (tuple): A (treasure, coordinate) pair. + + Returns: + str: The extracted map coordinate. """ pass @@ -14,8 +17,11 @@ def get_coordinate(record): def convert_coordinate(coordinate): """Split the given coordinate into tuple containing its individual components. - :param coordinate: str - a string map coordinate - :return: tuple - the string coordinate split into its individual components. + Parameters: + coordinate (str): A string map coordinate. + + Returns: + tuple: The string coordinate split into its individual components. """ pass @@ -24,9 +30,12 @@ def convert_coordinate(coordinate): def compare_records(azara_record, rui_record): """Compare two record types and determine if their coordinates match. - :param azara_record: tuple - a (treasure, coordinate) pair. - :param rui_record: tuple - a (location, tuple(coordinate_1, coordinate_2), quadrant) trio. - :return: bool - do the coordinates match? + Parameters: + azara_record (tuple): A (treasure, coordinate) pair. + rui_record (tuple): A (location, tuple(coordinate_1, coordinate_2), quadrant) trio. + + Returns: + bool: Do the coordinates match? """ pass @@ -35,9 +44,12 @@ def compare_records(azara_record, rui_record): def create_record(azara_record, rui_record): """Combine the two record types (if possible) and create a combined record group. - :param azara_record: tuple - a (treasure, coordinate) pair. - :param rui_record: tuple - a (location, coordinate, quadrant) trio. - :return: tuple or str - the combined record (if compatible), or the string "not a match" (if incompatible). + Parameters: + azara_record (tuple): A (treasure, coordinate) pair. + rui_record (tuple): A (location, coordinate, quadrant) trio. + + Returns: + tuple or str: The combined record (if compatible), or the string "not a match" (if incompatible). """ pass @@ -46,12 +58,16 @@ def create_record(azara_record, rui_record): def clean_up(combined_record_group): """Clean up a combined record group into a multi-line string of single records. - :param combined_record_group: tuple - everything from both participants. - :return: str - everything "cleaned", excess coordinates and information are removed. + Parameters: + combined_record_group (tuple): Everything from both participants. + + Returns: + str: Everything "cleaned", excess coordinates and information are removed. - The return statement should be a multi-lined string with items separated by newlines. + Note: + The return statement is a multi-lined string with items separated by newlines. + (see HINTS.md for an example). - (see HINTS.md for an example). """ pass 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/acronym/acronym_test.py b/exercises/practice/acronym/acronym_test.py index 984deef60d2..a10f2bf3dc2 100644 --- a/exercises/practice/acronym/acronym_test.py +++ b/exercises/practice/acronym/acronym_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/acronym/canonical-data.json -# File last updated on 2023-07-20 +# File last updated on 2026-02-19 import unittest @@ -10,6 +10,7 @@ class AcronymTest(unittest.TestCase): + def test_basic(self): self.assertEqual(abbreviate("Portable Network Graphics"), "PNG") 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/affine-cipher/affine_cipher_test.py b/exercises/practice/affine-cipher/affine_cipher_test.py index f6d7c106c33..db265953691 100644 --- a/exercises/practice/affine-cipher/affine_cipher_test.py +++ b/exercises/practice/affine-cipher/affine_cipher_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/affine-cipher/canonical-data.json -# File last updated on 2023-07-20 +# File last updated on 2026-02-19 import unittest @@ -11,6 +11,7 @@ class AffineCipherTest(unittest.TestCase): + def test_encode_yes(self): self.assertEqual(encode("yes", 5, 7), "xbt") diff --git a/exercises/practice/all-your-base/.approaches/config.json b/exercises/practice/all-your-base/.approaches/config.json new file mode 100644 index 00000000000..21b3a127207 --- /dev/null +++ b/exercises/practice/all-your-base/.approaches/config.json @@ -0,0 +1,7 @@ +{ + "introduction": { + "authors": ["colinleach", + "BethanyG"], + "contributors": [] + } +} diff --git a/exercises/practice/all-your-base/.approaches/introduction.md b/exercises/practice/all-your-base/.approaches/introduction.md new file mode 100644 index 00000000000..f083c9c08b3 --- /dev/null +++ b/exercises/practice/all-your-base/.approaches/introduction.md @@ -0,0 +1,169 @@ +# Introduction + +The main aim of `All Your Base` is to understand how non-negative integers work in different bases. +Given that mathematical understanding, implementation can be relatively straightforward. + + +For this approach and its variations, no attempt was made to benchmark performance as this would distract from the main focus of writing clear, correct code for conversion. + + +## General guidance + +All successful solutions for base conversion involve three steps: + +1. Check that inputs are valid (no non-integer or negative values). +2. Convert the input list to a Python `int`, per the examples given in the instructions. +3. Convert the `int` from step 2 into an output list in the new base. + +Some programmers prefer to separate the two conversions into separate functions, others put everything in a single function. +This is largely a matter of taste, and either structure can be made reasonably concise and readable. + + +## 1. Checking the inputs + +Solution code should check that the input base is at least 2, and that the output base is 2 or greater. +Bases outside the range should rase `ValueError`s for input base and output base respectively. + +```python + if input_base < 2: + raise ValueError("input base must be >= 2") + + if not all( 0 <= digit < input_base for digit in digits) : + raise ValueError("all digits must satisfy 0 <= d < input base") + + if not output_base >= 2: + raise ValueError("output base must be >= 2") + +``` + +Additionally, all input numbers should be positive integers greater or equal to 0 and strictly less than the given number base. +For the familiar base-10 system, that would mean 0 to 9. +As implemented, the tests require that invalid inputs raise a `ValueError` with "all digits must satisfy 0 <= d < input base" as an error message. + + +## 2. Convert the input digits to an `int` + +The next step in the conversion process requires that the input list of numbers be converted into a single integer. +The four code fragments below all show variations of this conversion: + +```python +# Simple loop + value = 0 + for digit in digits: + value = input_base * value + digit + +# Loop, separating the arithmetic steps + value = 0 + for digit in digits: + value *= input_base + value += digit + +# Sum a generator expression over reversed digits + value = sum(digit * input_base ** position for position, digit in enumerate(reversed(digits))) + +# Sum a generator expression with alternative reversing + value = sum(digit * (input_base ** (len(digits) - 1 - index)) for index, digit in enumerate(digits)) +``` + +In the first two, the `value *= input_base` step essentially left-shifts all the previous digits, and `value += digit` adds a new digit on the right. +In the two generator expressions, an exponentation like `input_base ** position` left-shifts the current digit to the appropriate position in the output. + + +````exercism/note + +It is important to think about these procedures until they makes sense: these short code fragments are the main point of the exercise. +In each code fragment, the Python `int` is called `value`, a deliberately neutral identifier. +Surprisingly many students use names like `decimal` or `base10` for the intermediate value, which is misleading. + +A Python `int` is an object with a complicated (but largely hidden) implementation. +There are methods to convert an `int` to string representations such as octal, binary or hexadecimal, but these do not change the internal representation. +```` + + +## 3. Convert the intermediate `int` to output digits + +The `int` created in step 2 can now be reversed, using a different base. + +Again, there are multiple code snippets shown below, which all do the same thing (essentially). +In each case, we need the value and the remainder of integer division. +The first snippet adds new digits at the start of the `list`, while the next two add them at the end. +The final snippet uses [`collections.deque()`][deque] to prepend, then converts to a `list` in the `return` statement. + + +These snippets represent choices of where to take the performance hit: appending to the end is a **much** faster and more memory efficient way to grow a `list` (O(1)), but the solution then needs an extra reverse step, incurring O(n) performance for the reversal. +_Prepending_ to the `list` is very expensive, as every addition needs to move all other elements of the list "over" into new memory. +The `deque` has O(1) prepends and appends, but then needs to be converted to a `list` before being returned, which is an O(n) operation. + + +```python +from collections import deque + + +out = [] + +# Step forward, insert new digits at index 0 (front of list). +# Least performant, and not recommended for large amounts of data. + while value > 0: + out.insert(0, value % output_base) + value = value // output_base + +# Append values to the end (mor efficient), then reverse the list. + while value: + out.append(value % output_base) + value //= output_base + out.reverse() + +# Use divmod() and reverse list, same efficiency a above. + while value: + div, mod = divmod(value, output_base) + out.append(mod) + value = div + out.reverse() + +# Use deque() for effcient appendleft(), convert to list. + converted_digits = deque() + + while number > 0: + converted_digits.appendleft(number % output_base) + number = number // output_base + + return list(converted_digits) or [0] +``` + + +Finally, we return the digits just calculated. + +A minor complication is that a zero value needs to be `[0]`, not `[]` according to the tests. +Here, we cover this case in the `return` statement, but it could also have been trapped at the beginning of the program, with an early `return`: + + +```python +# return, with guard for empty list + return out or [0] +``` + +## Recursion option + +An unusual solution to the two conversions is shown below. +It works, and the problem is small enough to avoid stack overflow (Python has no tail recursion). + + +In practice, few Python programmers would take this approach without carefully thinking about the bounds of the program and any possible memoization/performance optimizations they could take to avoid issues. +While Python *allows* recursion, it does nothing to *encourage* it, and the default recursion limit is set to only 1000 stack frames. + + +```python +def base_to_dec(input_base, digits): + if not digits: + return 0 + return input_base * base_to_dec(input_base, digits[:-1]) + digits[-1] + + +def dec_to_base(number, output_base): + if not number: + return [] + return [number % output_base] + dec_to_base(number // output_base, output_base) +``` + +[deque]: https://docs.python.org/3/library/collections.html#collections.deque + diff --git a/exercises/practice/allergies/.meta/config.json b/exercises/practice/allergies/.meta/config.json index b918bdbccef..552ebfc960b 100644 --- a/exercises/practice/allergies/.meta/config.json +++ b/exercises/practice/allergies/.meta/config.json @@ -31,5 +31,5 @@ }, "blurb": "Given a person's allergy score, determine whether or not they're allergic to a given item, and their full list of allergies.", "source": "Exercise by the JumpstartLab team for students at The Turing School of Software and Design.", - "source_url": "https://turing.edu" + "source_url": "https://www.turing.edu/" } diff --git a/exercises/practice/alphametics/alphametics_test.py b/exercises/practice/alphametics/alphametics_test.py index 6279b805c59..30e54813093 100644 --- a/exercises/practice/alphametics/alphametics_test.py +++ b/exercises/practice/alphametics/alphametics_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/alphametics/canonical-data.json -# File last updated on 2023-07-20 +# File last updated on 2026-02-19 import unittest @@ -10,6 +10,7 @@ class AlphameticsTest(unittest.TestCase): + def test_puzzle_with_three_letters(self): self.assertEqual(solve("I + BB == ILL"), {"I": 1, "B": 9, "L": 0}) 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/.approaches/config.json b/exercises/practice/atbash-cipher/.approaches/config.json index ed1edeb5065..dc57da36ca2 100644 --- a/exercises/practice/atbash-cipher/.approaches/config.json +++ b/exercises/practice/atbash-cipher/.approaches/config.json @@ -1,6 +1,7 @@ { "introduction": { - "authors": ["safwansamsudeen"] + "authors": ["safwansamsudeen"], + "contributors": ["yrahcaz7"] }, "approaches": [ { @@ -8,14 +9,16 @@ "slug": "mono-function", "title": "Mono-function", "blurb": "Use one function for both tasks", - "authors": ["safwansamsudeen"] + "authors": ["safwansamsudeen"], + "contributors": ["yrahcaz7"] }, { "uuid": "9a7a17e0-4ad6-4d97-a8b9-c74d47f3e000", "slug": "separate-functions", - "title": "Separate Functions", + "title": "Separate functions", "blurb": "Use separate functions, and perhaps helper ones", - "authors": ["safwansamsudeen"] + "authors": ["safwansamsudeen"], + "contributors": ["yrahcaz7"] } ] } diff --git a/exercises/practice/atbash-cipher/.approaches/introduction.md b/exercises/practice/atbash-cipher/.approaches/introduction.md index 6c7180eff9a..ce9786409a8 100644 --- a/exercises/practice/atbash-cipher/.approaches/introduction.md +++ b/exercises/practice/atbash-cipher/.approaches/introduction.md @@ -1,44 +1,54 @@ # Introduction + Atbash cipher in Python can be solved in many ways. ## General guidance -The first thing is to have a "key" mapping - possibly in a `dict` or `str.maketrans`, otherwise the value would have to be calculated on the fly. -Then, you have to "clean" up the string to be encoded by removing numbers/whitespace. + +The first thing is to have a "key" mapping โ€” possibly in a `dict` or `str.maketrans()`, otherwise the value would have to be calculated on the fly. +Next, you have to "clean" up the string to be encoded by removing punctuation/whitespace. Finally, you break it up into chunks of five before returning it. -For decoding, it's similar - clean up (which automatically joins the chunks) and translate using the _same_ key - the realization that the same key can be used is crucial in solving this in an idiomatic manner. +For decoding, the process is similar โ€” clean up (_which automatically joins the chunks_) and translate using the **_same_** key โ€” realizing that the same key can be used is crucial in solving this in an idiomatic manner. + +## Approach: Separate functions + +We use `str.maketrans()` to create the encoding. +In `encode()`, we use a [generator expression][generator-expression] in `str.join()`. -## Approach: separate functions -We use `str.maketrans` to create the encoding. -In `encode`, we use a [generator expression][generator-expression] in `str.join`. ```python from string import ascii_lowercase + ENCODING = str.maketrans(ascii_lowercase, ascii_lowercase[::-1]) -def encode(text: str): +def encode(text): res = "".join(chr for chr in text.lower() if chr.isalnum()).translate(ENCODING) return " ".join(res[index:index+5] for index in range(0, len(res), 5)) -def decode(text: str): - return "".join(chr.lower() for chr in text if chr.isalnum()).translate(ENCODING) +def decode(text): + return "".join(chr.lower() for chr in text if not chr.isspace()).translate(ENCODING) ``` + Read more on this [approach here][approach-separate-functions]. -## Approach: mono-function -Notice that there the majority of the code is repetitive? -A fun way to solve this would be to keep it all inside the `encode` function, and merely chunk it if `decode` is False: -For variation, this approach shows a different way to translate the text. +## Approach: Mono-function + +Notice that the majority of the code is repetitive? +A fun way to solve this would be to keep it all inside the `encode()` function, and merely chunk it if `decode` is `False`: +For variation, this approach also shows a different way to translate the text. + ```python from string import ascii_lowercase as asc_low + ENCODING = {chr: asc_low[id] for id, chr in enumerate(asc_low[::-1])} -def encode(text: str, decode: bool = False): - res = "".join(ENCODING.get(chr, chr) for chr in text.lower() if chr.isalnum()) - return res if decode else " ".join(res[index:index+5] for index in range(0, len(res), 5)) +def encode(text, decode = False): + line = "".join(ENCODING.get(chr, chr) for chr in text.lower() if chr.isalnum()) + return line if decode else " ".join(line[index:index+5] for index in range(0, len(line), 5)) -def decode(text: str): +def decode(text): return encode(text, True) ``` + For more detail, [read here][approach-mono-function]. [approach-separate-functions]: https://exercism.org/tracks/python/exercises/atbash-cipher/approaches/separate-functions diff --git a/exercises/practice/atbash-cipher/.approaches/mono-function/content.md b/exercises/practice/atbash-cipher/.approaches/mono-function/content.md index 879664ce207..0c0da8e42a1 100644 --- a/exercises/practice/atbash-cipher/.approaches/mono-function/content.md +++ b/exercises/practice/atbash-cipher/.approaches/mono-function/content.md @@ -1,46 +1,54 @@ -## Approach: Mono-function -Notice that there the majority of the code is repetitive? -A fun way to solve this would be to keep it all inside the `encode` function, and merely chunk it if `decode` is False: -For variation, this approach shows a different way to translate the text. +# Approach: Mono-function + +Notice that the majority of the code is repetitive? +A fun way to solve this would be to keep it all inside the `encode()` function, and merely chunk it if `decode` is `False`: +For variation, this approach also shows a different way to translate the text. + ```python from string import ascii_lowercase as asc_low + ENCODING = {chr: asc_low[id] for id, chr in enumerate(asc_low[::-1])} -def encode(text: str, decode: bool = False): - res = "".join(ENCODING.get(chr, chr) for chr in text.lower() if chr.isalnum()) - return res if decode else " ".join(res[index:index+5] for index in range(0, len(res), 5)) +def encode(text, decode = False): + line = "".join(ENCODING.get(chr, chr) for chr in text.lower() if chr.isalnum()) + return line if decode else " ".join(line[index:index+5] for index in range(0, len(line), 5)) -def decode(text: str): +def decode(text): return encode(text, True) ``` -To explain the translation: we use a `dict` comprehension in which we reverse the ASCII lowercase digits, and enumerate through them - that is, `z` is 0, `y` is 1, and so on. -We access the character at that index and set it to the value of `c` - so `z` translates to `a`. -In the calculation of the result, we try to obtain the value of the character using `dict.get`, which accepts a default parameter. -In this case, the character itself is the default - that is, numbers won't be found in the translation key, and thus should remain as numbers. +Here, we use a dictionary comprehension in which we reverse the order of the ASCII lowercase digits and enumerate through them โ€” that is, `z` is at index 0, `y` is at index 1, and so on. +For each code point, we set the value of `chr` in the resulting dictionary to the code point at the respective index โ€” so `z` translates to `a`. + +In the calculation of the result, we try to obtain the value of the code point using `dict.get()`, which accepts a default parameter. +In this case, the code point itself is the default โ€” that is, numbers won't be found in the translation key, and thus should remain as numbers. + +We use a [conditional expression (also known as a ternary operator)][conditional-expression] to check if we actually mean to decode the function, in which case we return the result as is. +If not, we "chunk" the result by joining every five code points with a space. -We use a [ternary operator][ternary-operator] to check if we actually mean to decode the function, in which case we return the result as is. -If not, we chunk the result by joining every five characters with a space. +Another possible way to solve this would be to use a function that returns another function (_a higher-order function or [closure][closure]_) that encodes or decodes based on the outer function's parameter: -Another possible way to solve this would be to use a function that returns a function that encodes or decodes based on the parameters: ```python -from string import ascii_lowercase as alc +from string import ascii_lowercase as asc_low -lowercase = {chr: alc[id] for id, chr in enumerate(alc[::-1])} +ENCODING = {chr: asc_low[id] for id, chr in enumerate(asc_low[::-1])} -def code(decode=False): +def code(decode = False): def func(text): - line = "".join(lowercase.get(chr, chr) for chr in text.lower() if chr.isalnum()) + line = "".join(ENCODING.get(chr, chr) for chr in text.lower() if chr.isalnum()) return line if decode else " ".join(line[index:index+5] for index in range(0, len(line), 5)) return func - encode = code() decode = code(True) ``` -The logic is the same - we've instead used one function that generates two _other_ functions based on the boolean value of its parameter. -`encode` is set to the function that's returned, and performs encoding. -`decode` is set a function that _decodes_. -[ternary-operator]: https://www.tutorialspoint.com/ternary-operator-in-python -[decorator]: https://realpython.com/primer-on-python-decorators/ \ No newline at end of file +The logic is the same โ€” the only change is that now we use use one function that generates two _other_ functions based on the boolean value of its parameter. + +Here, we first call `code()` with no argument and set `encode` to the function that's returned, which performs encoding. +Then we call `code(True)` to get the decoding version of the function and set `decode` to that function. + +After that, we can call `encode()` and `decode()` as normal, and both functions successfully perform their indended task. + +[closure]: https://realpython.com/python-closure/ +[conditional-expression]: https://docs.python.org/3/reference/expressions.html#conditional-expressions diff --git a/exercises/practice/atbash-cipher/.approaches/mono-function/snippet.txt b/exercises/practice/atbash-cipher/.approaches/mono-function/snippet.txt index 84e8b793008..24ba495094a 100644 --- a/exercises/practice/atbash-cipher/.approaches/mono-function/snippet.txt +++ b/exercises/practice/atbash-cipher/.approaches/mono-function/snippet.txt @@ -1,8 +1,8 @@ from string import ascii_lowercase as asc_low ENCODING = {chr: asc_low[id] for id, chr in enumerate(asc_low[::-1])} -def encode(text: str, decode: bool = False): - res = "".join(ENCODING.get(chr, chr) for chr in text.lower() if chr.isalnum()) - return res if decode else " ".join(res[index:index+5] for index in range(0, len(res), 5)) -def decode(text: str): +def encode(text, decode = False): + line = "".join(ENCODING.get(chr, chr) for chr in text.lower() if chr.isalnum()) + return line if decode else " ".join(line[index:index+5] for index in range(0, len(line), 5)) +def decode(text): return encode(text, True) \ No newline at end of file diff --git a/exercises/practice/atbash-cipher/.approaches/separate-functions/content.md b/exercises/practice/atbash-cipher/.approaches/separate-functions/content.md index 60e02a22055..1890625819c 100644 --- a/exercises/practice/atbash-cipher/.approaches/separate-functions/content.md +++ b/exercises/practice/atbash-cipher/.approaches/separate-functions/content.md @@ -1,45 +1,57 @@ -## Approach: Separate Functions -We use `str.maketrans` to create the encoding. -`.maketrans`/`.translate` is extremely fast compared to other methods of translation. -If you're interested, [read more][str-maketrans] about it. +# Approach: Separate functions + +We use `str.maketrans()` to create the encoding. +`str.maketrans()`/`str.translate()` is extremely fast compared to other methods of translation. +If you're interested, you can [read more about it here][str-maketrans]. + +In `encode()`, we use a [generator expression][generator-expression] in `str.join()`, which is more efficient โ€” and neater โ€” than a list comprehension. -In `encode`, we use a [generator expression][generator-expression] in `str.join`, which is more efficient - and neater - than a list comprehension. ```python from string import ascii_lowercase + ENCODING = str.maketrans(ascii_lowercase, ascii_lowercase[::-1]) -def encode(text: str): +def encode(text): res = "".join(chr for chr in text.lower() if chr.isalnum()).translate(ENCODING) return " ".join(res[index:index+5] for index in range(0, len(res), 5)) -def decode(text: str): - return "".join(chr.lower() for chr in text if chr.isalnum()).translate(ENCODING) +def decode(text): + return "".join(chr.lower() for chr in text if not chr.isspace()).translate(ENCODING) ``` -In `encode`, we first join together every character if the character is alphanumeric - as we use `text.lower()`, the characters are all lowercase as needed. -Then, we translate it and return a version joining every five characters with a space in between. -`decode` does the exact same thing, except it doesn't return a chunked output. -Instead of cleaning the input by checking that it's alphanumeric, we check that it's not a whitespace character. +In `encode()`, we first join together every code point that is an alphanumeric character โ€” as we use `text.lower()`, the characters are all lowercase as needed. +Then, we translate it and return a version joining every five code points with a space in between. + +`decode()` does the exact same thing, except it doesn't return a chunked output and it cleans the input differently. +To clean the input, `decode()` only removes code points that are whitespace characters instead of all non-alphanumeric characters. It might be cleaner to use helper functions: + ```python from string import ascii_lowercase + ENCODING = str.maketrans(ascii_lowercase, ascii_lowercase[::-1]) + + def clean(text): return "".join([chr.lower() for chr in text if chr.isalnum()]) + def chunk(text): return " ".join(text[index:index+5] for index in range(0, len(text), 5)) + def encode(text): return chunk(clean(text).translate(ENCODING)) def decode(text): return clean(text).translate(ENCODING) ``` -Note that checking that `chr` _is_ alphanumeric achieves the same result as checking that it's _not_ whitespace, although it's not as explicit. + +Note that for `decode()`, checking that `chr` _is_ alphanumeric achieves the same result as checking that it _is not_ whitespace, although it's not as explicit. As this is a helper function, this is acceptable enough. -You can also make `chunk` recursive: +You can also make `chunk()` recursive, but this is not recommended: + ```python def chunk(text): if len(text) <= 5: @@ -48,4 +60,4 @@ def chunk(text): ``` [generator-expression]: https://www.programiz.com/python-programming/generator -[str-maketrans]: https://www.programiz.com/python-programming/methods/string/maketrans \ No newline at end of file +[str-maketrans]: https://www.programiz.com/python-programming/methods/string/maketrans diff --git a/exercises/practice/atbash-cipher/.approaches/separate-functions/snippet.txt b/exercises/practice/atbash-cipher/.approaches/separate-functions/snippet.txt index fbfe0b75fa5..f57a8dda721 100644 --- a/exercises/practice/atbash-cipher/.approaches/separate-functions/snippet.txt +++ b/exercises/practice/atbash-cipher/.approaches/separate-functions/snippet.txt @@ -1,8 +1,8 @@ from string import ascii_lowercase ENCODING = str.maketrans(ascii_lowercase, ascii_lowercase[::-1]) -def encode(text: str): +def encode(text): res = "".join(chr for chr in text.lower() if chr.isalnum()).translate(ENCODING) return " ".join(res[index:index+5] for index in range(0, len(res), 5)) -def decode(text: str): +def decode(text): return "".join(chr.lower() for chr in text if not chr.isspace()).translate(ENCODING) \ No newline at end of file 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/config.json b/exercises/practice/bob/.meta/config.json index ec0fc617762..c3f5356c329 100644 --- a/exercises/practice/bob/.meta/config.json +++ b/exercises/practice/bob/.meta/config.json @@ -43,5 +43,5 @@ }, "blurb": "Bob is a lackadaisical teenager. In conversation, his responses are very limited.", "source": "Inspired by the 'Deaf Grandma' exercise in Chris Pine's Learn to Program tutorial.", - "source_url": "https://pine.fm/LearnToProgram/?Chapter=06" + "source_url": "https://pine.fm/LearnToProgram/chap_06.html" } 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..46410fa62ba 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 2026-02-19 import unittest @@ -10,6 +10,7 @@ class BobTest(unittest.TestCase): + def test_stating_something(self): self.assertEqual(response("Tom-ay-to, tom-aaaah-to."), "Whatever.") @@ -79,12 +80,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 +95,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/book-store/.meta/config.json b/exercises/practice/book-store/.meta/config.json index 87dd6d2cc65..d3c13189f95 100644 --- a/exercises/practice/book-store/.meta/config.json +++ b/exercises/practice/book-store/.meta/config.json @@ -28,5 +28,5 @@ }, "blurb": "To try and encourage more sales of different books from a popular 5 book series, a bookshop has decided to offer discounts of multiple-book purchases.", "source": "Inspired by the harry potter kata from Cyber-Dojo.", - "source_url": "https://cyber-dojo.org" + "source_url": "https://cyber-dojo.org/creator/home" } diff --git a/exercises/practice/book-store/book_store_test.py b/exercises/practice/book-store/book_store_test.py index 87b0051faa2..d25bdc5a25b 100644 --- a/exercises/practice/book-store/book_store_test.py +++ b/exercises/practice/book-store/book_store_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/book-store/canonical-data.json -# File last updated on 2023-07-20 +# File last updated on 2026-02-19 import unittest @@ -10,6 +10,7 @@ class BookStoreTest(unittest.TestCase): + def test_only_a_single_book(self): basket = [1] self.assertEqual(total(basket), 800) diff --git a/exercises/practice/camicia/.docs/instructions.md b/exercises/practice/camicia/.docs/instructions.md new file mode 100644 index 00000000000..db62fcef27d --- /dev/null +++ b/exercises/practice/camicia/.docs/instructions.md @@ -0,0 +1,84 @@ +# Instructions + +In this exercise, you will simulate a game very similar to the classic card game **Camicia**. +Your program will receive the initial configuration of two players' decks and must simulate the game until it ends (or detect that it will never end). + +## Rules + +- The deck is split between **two players**. + The player's cards are read from left to right, where the leftmost card is the top of the deck. +- A round consists of both players playing at least one card. +- Players take turns placing the **top card** of their deck onto a central pile. +- If the card is a **number card** (2-10), play simply passes to the other player. +- If the card is a **payment card**, a penalty must be paid: + - **J** โ†’ opponent must pay 1 card + - **Q** โ†’ opponent must pay 2 cards + - **K** โ†’ opponent must pay 3 cards + - **A** โ†’ opponent must pay 4 cards +- If the player paying a penalty reveals another payment card, that player stops paying the penalty. + The other player must then pay a penalty based on the new payment card. +- If the penalty is fully paid without interruption, the player who placed the **last payment card** collects the central pile and places it at the bottom of their deck. + That player then starts the next round. +- If a player runs out of cards and is unable to play a card (either while paying a penalty or when it is their turn), the other player collects the central pile. +- The moment when a player collects cards from the central pile is called a **trick**. +- If a player has all the cards in their possession after a trick, the game **ends**. +- The game **enters a loop** as soon as the decks are identical to what they were earlier during the game, **not** counting number cards! + +## Examples + +A small example of a match that ends. + +| Round | Player A | Player B | Pile | Penalty Due | +| :---- | :----------- | :------------------------- | :------------------------- | :---------- | +| 1 | 2 A 7 8 Q 10 | 3 4 5 6 K 9 J | | - | +| 1 | A 7 8 Q 10 | 3 4 5 6 K 9 J | 2 | - | +| 1 | A 7 8 Q 10 | 4 5 6 K 9 J | 2 3 | - | +| 1 | 7 8 Q 10 | 4 5 6 K 9 J | 2 3 A | Player B: 4 | +| 1 | 7 8 Q 10 | 5 6 K 9 J | 2 3 A 4 | Player B: 3 | +| 1 | 7 8 Q 10 | 6 K 9 J | 2 3 A 4 5 | Player B: 2 | +| 1 | 7 8 Q 10 | K 9 J | 2 3 A 4 5 6 | Player B: 1 | +| 1 | 7 8 Q 10 | 9 J | 2 3 A 4 5 6 K | Player A: 3 | +| 1 | 8 Q 10 | 9 J | 2 3 A 4 5 6 K 7 | Player A: 2 | +| 1 | Q 10 | 9 J | 2 3 A 4 5 6 K 7 8 | Player A: 1 | +| 1 | 10 | 9 J | 2 3 A 4 5 6 K 7 8 Q | Player B: 2 | +| 1 | 10 | J | 2 3 A 4 5 6 K 7 8 Q 9 | Player B: 1 | +| 1 | 10 | - | 2 3 A 4 5 6 K 7 8 Q 9 J | Player A: 1 | +| 1 | - | - | 2 3 A 4 5 6 K 7 8 Q 9 J 10 | - | +| 2 | - | 2 3 A 4 5 6 K 7 8 Q 9 J 10 | - | - | + +status: `"finished"`, cards: 13, tricks: 1 + +This is a small example of a match that loops. + +| Round | Player A | Player B | Pile | Penalty Due | +| :---- | :------- | :------- | :---- | :---------- | +| 1 | J 2 3 | 4 J 5 | - | - | +| 1 | 2 3 | 4 J 5 | J | Player B: 1 | +| 1 | 2 3 | J 5 | J 4 | - | +| 2 | 2 3 J 4 | J 5 | - | - | +| 2 | 3 J 4 | J 5 | 2 | - | +| 2 | 3 J 4 | 5 | 2 J | Player A: 1 | +| 2 | J 4 | 5 | 2 J 3 | - | +| 3 | J 4 | 5 2 J 3 | - | - | +| 3 | J 4 | 2 J 3 | 5 | - | +| 3 | 4 | 2 J 3 | 5 J | Player B: 1 | +| 3 | 4 | J 3 | 5 J 2 | - | +| 4 | 4 5 J 2 | J 3 | - | - | + +The start of round 4 matches the start of round 2. +Recall, the value of the number cards does not matter. + +status: `"loop"`, cards: 8, tricks: 3 + +## Your Task + +- Using the input, simulate the game following the rules above. +- Determine the following information regarding the game: + - **Status**: `"finished"` or `"loop"` + - **Cards**: total number of cards played throughout the game + - **Tricks**: number of times the central pile was collected + +~~~~exercism/advanced +For those who want to take on a more exciting challenge, the hunt for other records for the longest game with an end is still open. +There are 653,534,134,886,878,245,000 (approximately 654 quintillion) possibilities, and we haven't calculated them all yet! +~~~~ diff --git a/exercises/practice/camicia/.docs/introduction.md b/exercises/practice/camicia/.docs/introduction.md new file mode 100644 index 00000000000..761d8a82c50 --- /dev/null +++ b/exercises/practice/camicia/.docs/introduction.md @@ -0,0 +1,24 @@ +# Introduction + +One rainy afternoon, you sit at the kitchen table playing cards with your grandmother. +The game is her take on [Camicia][bmn]. + +At first it feels like just another friendly match: cards slapped down, laughter across the table, the occasional victorious grin from Nonna. +But as the game stretches on, something strange happens. +The same cards keep cycling back. +You play card after card, yet the end never seems to come. + +You start to wonder. +_Will this game ever finish? +Or could we keep playing forever?_ + +Later, driven by curiosity, you search online and to your surprise you discover that what happened wasn't just bad luck. +You and your grandmother may have stumbled upon one of the longest possible sequences! +Suddenly, you're hooked. +What began as a casual game has turned into a quest: _how long can such a game really last?_ +_Can you find a sequence even longer than the one you played at the kitchen table?_ +_Perhaps even long enough to set a new world record?_ + +And so, armed with nothing but a deck of cards and some algorithmic ingenuity, you decide to investigate... + +[bmn]: https://en.wikipedia.org/wiki/Beggar-my-neighbour diff --git a/exercises/practice/camicia/.meta/config.json b/exercises/practice/camicia/.meta/config.json new file mode 100644 index 00000000000..9956e52178b --- /dev/null +++ b/exercises/practice/camicia/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "BNAndras" + ], + "files": { + "solution": [ + "camicia.py" + ], + "test": [ + "camicia_test.py" + ], + "example": [ + ".meta/example.py" + ] + }, + "blurb": "Simulate the card game and determine whether the match ends or enters an infinite loop.", + "source": "Beggar-My-Neighbour", + "source_url": "https://www.richardpmann.com/beggar-my-neighbour-records.html" +} diff --git a/exercises/practice/camicia/.meta/example.py b/exercises/practice/camicia/.meta/example.py new file mode 100644 index 00000000000..7451490d956 --- /dev/null +++ b/exercises/practice/camicia/.meta/example.py @@ -0,0 +1,70 @@ +def simulate_game(player_a, player_b): + hand_a = list(map(get_value, player_a)) + hand_b = list(map(get_value, player_b)) + turn = "A" + pile = [] + seen = set() + total_tricks = 0 + cards_played = 0 + current_debt = 0 + + while True: + if not pile: + state = (tuple(hand_a), tuple(hand_b), turn) + if state in seen: + return { + "status": "loop", + "tricks": total_tricks, + "cards": cards_played + } + seen.add(state) + + active_hand = hand_a if turn == "A" else hand_b + other_hand = hand_b if turn == "A" else hand_a + + if not active_hand: + extra_trick = 0 if not pile else 1 + return { + "status": "finished", + "tricks": total_tricks + extra_trick, + "cards": cards_played, + } + + card_val = active_hand.pop(0) + pile.append(card_val) + cards_played += 1 + + if card_val > 0: + current_debt = card_val + turn = "B" if turn == "A" else "A" + else: + if current_debt > 0: + current_debt -= 1 + if not current_debt: + other_hand.extend(pile) + pile = [] + total_tricks += 1 + current_debt = 0 + + if not hand_a or not hand_b: + return { + "status": "finished", + "tricks": total_tricks, + "cards": cards_played + } + + turn = "B" if turn == "A" else "A" + else: + turn = "B" if turn == "A" else "A" + + +def get_value(card): + if card == "J": + return 1 + if card == "Q": + return 2 + if card == "K": + return 3 + if card == "A": + return 4 + return 0 diff --git a/exercises/practice/camicia/.meta/template.j2 b/exercises/practice/camicia/.meta/template.j2 new file mode 100644 index 00000000000..f7e443d6510 --- /dev/null +++ b/exercises/practice/camicia/.meta/template.j2 @@ -0,0 +1,19 @@ +{%- import "generator_macros.j2" as macros with context -%} +{{ macros.canonical_ref() }} + +{{ macros.header() }} + +class {{ exercise | camel_case }}Test(unittest.TestCase): + {% for case in cases -%} + def test_{{ case["description"] | to_snake }}(self): + {%- if case["input"]["playerA"]|length > 10 or case["input"]["playerB"]|length > 10 %} + # fmt: off + player_a = {{ case["input"]["playerA"] }} + player_b = {{ case["input"]["playerB"] }} + # fmt: on + {%- else %} + player_a = {{ case["input"]["playerA"] }} + player_b = {{ case["input"]["playerB"] }} + {%- endif %} + self.assertEqual(simulate_game(player_a, player_b), {{ case["expected"] }}) + {% endfor -%} diff --git a/exercises/practice/camicia/.meta/tests.toml b/exercises/practice/camicia/.meta/tests.toml new file mode 100644 index 00000000000..18d3fdd99f7 --- /dev/null +++ b/exercises/practice/camicia/.meta/tests.toml @@ -0,0 +1,94 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[0b7f737c-3ecd-4a55-b34d-e65c62a85c28] +description = "two cards, one trick" + +[27c19d75-53a5-48e5-b33b-232c3884d4f3] +description = "three cards, one trick" + +[9b02dd49-efaf-4b71-adca-a05c18a7c5b0] +description = "four cards, one trick" + +[fa3f4479-466a-4734-a001-ab79bfe27260] +description = "the ace reigns supreme" + +[07629689-f589-4f54-a6d1-8ce22776ce72] +description = "the king beats ace" + +[54d4a1c5-76fb-4d1e-8358-0e0296ac0601] +description = "the queen seduces the king" + +[c875500c-ff3d-47a4-bd1e-b60b90da80aa] +description = "the jack betrays the queen" + +[436875da-96ca-4149-be22-0b78173b8125] +description = "the 10 just wants to put on a show" + +[5be39bb6-1b34-4ce6-a1cd-0fcc142bb272] +description = "simple loop with decks of 3 cards" + +[2795dc21-0a2a-4c38-87c2-5a42e1ff15eb] +description = "the story is starting to get a bit complicated" + +[6999dfac-3fdc-41e2-b64b-38f4be228712] +description = "two tricks" + +[83dcd4f3-e089-4d54-855a-73f5346543a3] +description = "more tricks" + +[3107985a-f43e-486a-9ce8-db51547a9941] +description = "simple loop with decks of 4 cards" + +[dca32c31-11ed-49f6-b078-79ab912c1f7b] +description = "easy card combination" + +[1f8488d0-48d3-45ae-b819-59cedad0a5f4] +description = "easy card combination, inverted decks" + +[98878d35-623a-4d05-b81a-7bdc569eb88d] +description = "mirrored decks" + +[3e0ba597-ca10-484b-87a3-31a7df7d6da3] +description = "opposite decks" + +[92334ddb-aaa7-47fa-ab36-e928a8a6a67c] +description = "random decks #1" + +[30477523-9651-4860-84a3-e1ac461bb7fa] +description = "random decks #2" + +[20967de8-9e94-4e0e-9010-14bc1c157432] +description = "Kleber 1999" + +[9f2fdfe8-27f3-4323-816d-6bce98a9c6f7] +description = "Collins 2006" + +[c90b6f8d-7013-49f3-b5cb-14ea006cca1d] +description = "Mann and Wu 2007" + +[a3f1fbc5-1d0b-499a-92a5-22932dfc6bc8] +description = "Nessler 2012" + +[9cefb1ba-e6d1-4ab7-9d8f-76d8e0976d5f] +description = "Anderson 2013" + +[d37c0318-5be6-48d0-ab72-a7aaaff86179] +description = "Rucklidge 2014" + +[4305e479-ba87-432f-8a29-cd2bd75d2f05] +description = "Nessler 2021" + +[252f5cc3-b86d-4251-87ce-f920b7a6a559] +description = "Nessler 2022" + +[b9efcfa4-842f-4542-8112-8389c714d958] +description = "Casella 2024, first infinite game found" diff --git a/exercises/practice/camicia/camicia.py b/exercises/practice/camicia/camicia.py new file mode 100644 index 00000000000..61ac6ed552d --- /dev/null +++ b/exercises/practice/camicia/camicia.py @@ -0,0 +1,2 @@ +def simulate_game(player_a, player_b): + pass diff --git a/exercises/practice/camicia/camicia_test.py b/exercises/practice/camicia/camicia_test.py new file mode 100644 index 00000000000..6ad8db8a9f1 --- /dev/null +++ b/exercises/practice/camicia/camicia_test.py @@ -0,0 +1,267 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/camicia/canonical-data.json +# File last updated on 2026-01-24 + +import unittest + +from camicia import ( + simulate_game, +) + + +class CamiciaTest(unittest.TestCase): + def test_two_cards_one_trick(self): + player_a = ["2"] + player_b = ["3"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 2, "tricks": 1}, + ) + + def test_three_cards_one_trick(self): + player_a = ["2", "4"] + player_b = ["3"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 3, "tricks": 1}, + ) + + def test_four_cards_one_trick(self): + player_a = ["2", "4"] + player_b = ["3", "5", "6"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 4, "tricks": 1}, + ) + + def test_the_ace_reigns_supreme(self): + player_a = ["2", "A"] + player_b = ["3", "4", "5", "6", "7"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 7, "tricks": 1}, + ) + + def test_the_king_beats_ace(self): + player_a = ["2", "A"] + player_b = ["3", "4", "5", "6", "K"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 7, "tricks": 1}, + ) + + def test_the_queen_seduces_the_king(self): + player_a = ["2", "A", "7", "8", "Q"] + player_b = ["3", "4", "5", "6", "K"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 10, "tricks": 1}, + ) + + def test_the_jack_betrays_the_queen(self): + player_a = ["2", "A", "7", "8", "Q"] + player_b = ["3", "4", "5", "6", "K", "9", "J"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 12, "tricks": 1}, + ) + + def test_the_10_just_wants_to_put_on_a_show(self): + player_a = ["2", "A", "7", "8", "Q", "10"] + player_b = ["3", "4", "5", "6", "K", "9", "J"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 13, "tricks": 1}, + ) + + def test_simple_loop_with_decks_of_3_cards(self): + player_a = ["J", "2", "3"] + player_b = ["4", "J", "5"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "loop", "cards": 8, "tricks": 3}, + ) + + def test_the_story_is_starting_to_get_a_bit_complicated(self): + # fmt: off + player_a = ['2', '6', '6', 'J', '4', 'K', 'Q', '10', 'K', 'J', 'Q', '2', '3', 'K', '5', '6', 'Q', 'Q', 'A', 'A', '6', '9', 'K', 'A', '8', 'K', '2', 'A', '9', 'A', 'Q', '4', 'K', 'K', 'K', '3', '5', 'K', '8', 'Q', '3', 'Q', '7', 'J', 'K', 'J', '9', 'J', '3', '3', 'K', 'K', 'Q', 'A', 'K', '7', '10', 'A', 'Q', '7', '10', 'J', '4', '5', 'J', '9', '10', 'Q', 'J', 'J', 'K', '6', '10', 'J', '6', 'Q', 'J', '5', 'J', 'Q', 'Q', '8', '3', '8', 'A', '2', '6', '9', 'K', '7', 'J', 'K', 'K', '8', 'K', 'Q', '6', '10', 'J', '10', 'J', 'Q', 'J', '10', '3', '8', 'K', 'A', '6', '9', 'K', '2', 'A', 'A', '10', 'J', '6', 'A', '4', 'J', 'A', 'J', 'J', '6', '2', 'J', '3', 'K', '2', '5', '9', 'J', '9', '6', 'K', 'A', '5', 'Q', 'J', '2', 'Q', 'K', 'A', '3', 'K', 'J', 'K', '2', '5', '6', 'Q', 'J', 'Q', 'Q', 'J', '2', 'J', '9', 'Q', '7', '7', 'A', 'Q', '7', 'Q', 'J', 'K', 'J', 'A', '7', '7', '8', 'Q', '10', 'J', '10', 'J', 'J', '9', '2', 'A', '2'] + player_b = ['7', '2', '10', 'K', '8', '2', 'J', '9', 'A', '5', '6', 'J', 'Q', '6', 'K', '6', '5', 'A', '4', 'Q', '7', 'J', '7', '10', '2', 'Q', '8', '2', '2', 'K', 'J', 'A', '5', '5', 'A', '4', 'Q', '6', 'Q', 'K', '10', '8', 'Q', '2', '10', 'J', 'A', 'Q', '8', 'Q', 'Q', 'J', 'J', 'A', 'A', '9', '10', 'J', 'K', '4', 'Q', '10', '10', 'J', 'K', '10', '2', 'J', '7', 'A', 'K', 'K', 'J', 'A', 'J', '10', '8', 'K', 'A', '7', 'Q', 'Q', 'J', '3', 'Q', '4', 'A', '3', 'A', 'Q', 'Q', 'Q', '5', '4', 'K', 'J', '10', 'A', 'Q', 'J', '6', 'J', 'A', '10', 'A', '5', '8', '3', 'K', '5', '9', 'Q', '8', '7', '7', 'J', '7', 'Q', 'Q', 'Q', 'A', '7', '8', '9', 'A', 'Q', 'A', 'K', '8', 'A', 'A', 'J', '8', '4', '8', 'K', 'J', 'A', '10', 'Q', '8', 'J', '8', '6', '10', 'Q', 'J', 'J', 'A', 'A', 'J', '5', 'Q', '6', 'J', 'K', 'Q', '8', 'K', '4', 'Q', 'Q', '6', 'J', 'K', '4', '7', 'J', 'J', '9', '9', 'A', 'Q', 'Q', 'K', 'A', '6', '5', 'K'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 361, "tricks": 1}, + ) + + def test_two_tricks(self): + player_a = ["J"] + player_b = ["3", "J"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 5, "tricks": 2}, + ) + + def test_more_tricks(self): + player_a = ["J", "2", "4"] + player_b = ["3", "J", "A"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 12, "tricks": 4}, + ) + + def test_simple_loop_with_decks_of_4_cards(self): + player_a = ["2", "3", "J", "6"] + player_b = ["K", "5", "J", "7"] + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "loop", "cards": 16, "tricks": 4}, + ) + + def test_easy_card_combination(self): + # fmt: off + player_a = ['4', '8', '7', '5', '4', '10', '3', '9', '7', '3', '10', '10', '6', '8', '2', '8', '5', '4', '5', '9', '6', '5', '2', '8', '10', '9'] + player_b = ['6', '9', '4', '7', '2', '2', '3', '6', '7', '3', 'A', 'A', 'A', 'A', 'K', 'K', 'K', 'K', 'Q', 'Q', 'Q', 'Q', 'J', 'J', 'J', 'J'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 40, "tricks": 4}, + ) + + def test_easy_card_combination_inverted_decks(self): + # fmt: off + player_a = ['3', '3', '5', '7', '3', '2', '10', '7', '6', '7', 'A', 'A', 'A', 'A', 'K', 'K', 'K', 'K', 'Q', 'Q', 'Q', 'Q', 'J', 'J', 'J', 'J'] + player_b = ['5', '10', '8', '2', '6', '7', '2', '4', '9', '2', '6', '10', '10', '5', '4', '8', '4', '8', '6', '9', '8', '5', '9', '3', '4', '9'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 40, "tricks": 4}, + ) + + def test_mirrored_decks(self): + # fmt: off + player_a = ['2', 'A', '3', 'A', '3', 'K', '4', 'K', '2', 'Q', '2', 'Q', '10', 'J', '5', 'J', '6', '10', '2', '9', '10', '7', '3', '9', '6', '9'] + player_b = ['6', 'A', '4', 'A', '7', 'K', '4', 'K', '7', 'Q', '7', 'Q', '5', 'J', '8', 'J', '4', '5', '8', '9', '10', '6', '8', '3', '8', '5'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 59, "tricks": 4}, + ) + + def test_opposite_decks(self): + # fmt: off + player_a = ['4', 'A', '9', 'A', '4', 'K', '9', 'K', '6', 'Q', '8', 'Q', '8', 'J', '10', 'J', '9', '8', '4', '6', '3', '6', '5', '2', '4', '3'] + player_b = ['10', '7', '3', '2', '9', '2', '7', '8', '7', '5', 'J', '7', 'J', '10', 'Q', '10', 'Q', '3', 'K', '5', 'K', '6', 'A', '2', 'A', '5'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 151, "tricks": 21}, + ) + + def test_random_decks_1(self): + # fmt: off + player_a = ['K', '10', '9', '8', 'J', '8', '6', '9', '7', 'A', 'K', '5', '4', '4', 'J', '5', 'J', '4', '3', '5', '8', '6', '7', '7', '4', '9'] + player_b = ['6', '3', 'K', 'A', 'Q', '10', 'A', '2', 'Q', '8', '2', '10', '10', '2', 'Q', '3', 'K', '9', '7', 'A', '3', 'Q', '5', 'J', '2', '6'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 542, "tricks": 76}, + ) + + def test_random_decks_2(self): + # fmt: off + player_a = ['8', 'A', '4', '8', '5', 'Q', 'J', '2', '6', '2', '9', '7', 'K', 'A', '8', '10', 'K', '8', '10', '9', 'K', '6', '7', '3', 'K', '9'] + player_b = ['10', '5', '2', '6', 'Q', 'J', 'A', '9', '5', '5', '3', '7', '3', 'J', 'A', '2', 'Q', '3', 'J', 'Q', '4', '10', '4', '7', '4', '6'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 327, "tricks": 42}, + ) + + def test_kleber_1999(self): + # fmt: off + player_a = ['4', '8', '9', 'J', 'Q', '8', '5', '5', 'K', '2', 'A', '9', '8', '5', '10', 'A', '4', 'J', '3', 'K', '6', '9', '2', 'Q', 'K', '7'] + player_b = ['10', 'J', '3', '2', '4', '10', '4', '7', '5', '3', '6', '6', '7', 'A', 'J', 'Q', 'A', '7', '2', '10', '3', 'K', '9', '6', '8', 'Q'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 5790, "tricks": 805}, + ) + + def test_collins_2006(self): + # fmt: off + player_a = ['A', '8', 'Q', 'K', '9', '10', '3', '7', '4', '2', 'Q', '3', '2', '10', '9', 'K', 'A', '8', '7', '7', '4', '5', 'J', '9', '2', '10'] + player_b = ['4', 'J', 'A', 'K', '8', '5', '6', '6', 'A', '6', '5', 'Q', '4', '6', '10', '8', 'J', '2', '5', '7', 'Q', 'J', '3', '3', 'K', '9'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 6913, "tricks": 960}, + ) + + def test_mann_and_wu_2007(self): + # fmt: off + player_a = ['K', '2', 'K', 'K', '3', '3', '6', '10', 'K', '6', 'A', '2', '5', '5', '7', '9', 'J', 'A', 'A', '3', '4', 'Q', '4', '8', 'J', '6'] + player_b = ['4', '5', '2', 'Q', '7', '9', '9', 'Q', '7', 'J', '9', '8', '10', '3', '10', 'J', '4', '10', '8', '6', '8', '7', 'A', 'Q', '5', '2'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 7157, "tricks": 1007}, + ) + + def test_nessler_2012(self): + # fmt: off + player_a = ['10', '3', '6', '7', 'Q', '2', '9', '8', '2', '8', '4', 'A', '10', '6', 'K', '2', '10', 'A', '5', 'A', '2', '4', 'Q', 'J', 'K', '4'] + player_b = ['10', 'Q', '4', '6', 'J', '9', '3', 'J', '9', '3', '3', 'Q', 'K', '5', '9', '5', 'K', '6', '5', '7', '8', 'J', 'A', '7', '8', '7'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 7207, "tricks": 1015}, + ) + + def test_anderson_2013(self): + # fmt: off + player_a = ['6', '7', 'A', '3', 'Q', '3', '5', 'J', '3', '2', 'J', '7', '4', '5', 'Q', '10', '5', 'A', 'J', '2', 'K', '8', '9', '9', 'K', '3'] + player_b = ['4', 'J', '6', '9', '8', '5', '10', '7', '9', 'Q', '2', '7', '10', '8', '4', '10', 'A', '6', '4', 'A', '6', '8', 'Q', 'K', 'K', '2'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 7225, "tricks": 1016}, + ) + + def test_rucklidge_2014(self): + # fmt: off + player_a = ['8', 'J', '2', '9', '4', '4', '5', '8', 'Q', '3', '9', '3', '6', '2', '8', 'A', 'A', 'A', '9', '4', '7', '2', '5', 'Q', 'Q', '3'] + player_b = ['K', '7', '10', '6', '3', 'J', 'A', '7', '6', '5', '5', '8', '10', '9', '10', '4', '2', '7', 'K', 'Q', '10', 'K', '6', 'J', 'J', 'K'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 7959, "tricks": 1122}, + ) + + def test_nessler_2021(self): + # fmt: off + player_a = ['7', '2', '3', '4', 'K', '9', '6', '10', 'A', '8', '9', 'Q', '7', 'A', '4', '8', 'J', 'J', 'A', '4', '3', '2', '5', '6', '6', 'J'] + player_b = ['3', '10', '8', '9', '8', 'K', 'K', '2', '5', '5', '7', '6', '4', '3', '5', '7', 'A', '9', 'J', 'K', '2', 'Q', '10', 'Q', '10', 'Q'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 7972, "tricks": 1106}, + ) + + def test_nessler_2022(self): + # fmt: off + player_a = ['2', '10', '10', 'A', 'J', '3', '8', 'Q', '2', '5', '5', '5', '9', '2', '4', '3', '10', 'Q', 'A', 'K', 'Q', 'J', 'J', '9', 'Q', 'K'] + player_b = ['10', '7', '6', '3', '6', 'A', '8', '9', '4', '3', 'K', 'J', '6', 'K', '4', '9', '7', '8', '5', '7', '8', '2', 'A', '7', '4', '6'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "finished", "cards": 8344, "tricks": 1164}, + ) + + def test_casella_2024_first_infinite_game_found(self): + # fmt: off + player_a = ['2', '8', '4', 'K', '5', '2', '3', 'Q', '6', 'K', 'Q', 'A', 'J', '3', '5', '9', '8', '3', 'A', 'A', 'J', '4', '4', 'J', '7', '5'] + player_b = ['7', '7', '8', '6', '10', '10', '6', '10', '7', '2', 'Q', '6', '3', '2', '4', 'K', 'Q', '10', 'J', '5', '9', '8', '9', '9', 'K', 'A'] + # fmt: on + self.assertEqual( + simulate_game(player_a, player_b), + {"status": "loop", "cards": 474, "tricks": 66}, + ) 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/circular-buffer/circular_buffer_test.py b/exercises/practice/circular-buffer/circular_buffer_test.py index eb0663cf503..031d970fabc 100644 --- a/exercises/practice/circular-buffer/circular_buffer_test.py +++ b/exercises/practice/circular-buffer/circular_buffer_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/circular-buffer/canonical-data.json -# File last updated on 2023-07-20 +# File last updated on 2026-02-19 import unittest @@ -12,6 +12,7 @@ class CircularBufferTest(unittest.TestCase): + def test_reading_empty_buffer_should_fail(self): buf = CircularBuffer(1) with self.assertRaises(BufferError) as err: 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/connect/.meta/tests.toml b/exercises/practice/connect/.meta/tests.toml index 59ec615e39a..951b87e5c42 100644 --- a/exercises/practice/connect/.meta/tests.toml +++ b/exercises/practice/connect/.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. [6eff0df4-3e92-478d-9b54-d3e8b354db56] description = "an empty board has no winner" @@ -23,6 +30,12 @@ description = "nobody wins crossing adjacent angles" [cd61c143-92f6-4a8d-84d9-cb2b359e226b] description = "X wins crossing from left to right" +[495e33ed-30a9-4012-b46e-d7c4d5fe13c3] +description = "X wins with left-hand dead end fork" + +[ab167ab0-4a98-4d0f-a1c0-e1cddddc3d58] +description = "X wins with right-hand dead end fork" + [73d1eda6-16ab-4460-9904-b5f5dd401d0b] description = "O wins crossing from top to bottom" diff --git a/exercises/practice/connect/connect_test.py b/exercises/practice/connect/connect_test.py index e7303d35131..2ed17be4f48 100644 --- a/exercises/practice/connect/connect_test.py +++ b/exercises/practice/connect/connect_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/connect/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2026-05-01 import unittest @@ -10,6 +10,7 @@ class ConnectTest(unittest.TestCase): + def test_an_empty_board_has_no_winner(self): game = ConnectGame( """. . . . . @@ -74,6 +75,26 @@ def test_x_wins_crossing_from_left_to_right(self): winner = game.get_winner() self.assertEqual(winner, "X") + def test_x_wins_with_left_hand_dead_end_fork(self): + game = ConnectGame( + """. . X . + X X . . + . X X X + O O O O""" + ) + winner = game.get_winner() + self.assertEqual(winner, "X") + + def test_x_wins_with_right_hand_dead_end_fork(self): + game = ConnectGame( + """. . X X + X X . . + . X X . + O O O O""" + ) + winner = game.get_winner() + self.assertEqual(winner, "X") + def test_o_wins_crossing_from_top_to_bottom(self): game = ConnectGame( """. O . . 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..c948f3d631c --- /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_squared = x_coord**2 + y_coord**2 + return (radius_squared<=1)*5 + (radius_squared<=25)*4 + (radius_squared<=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_squared = x_coord**2 + y_coord**2 + return int(radius_squared<=1)*5 + int(radius_squared<=25)*4 + int(radius_squared<=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_squared" 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..f09eb53386f --- /dev/null +++ b/exercises/practice/darts/.approaches/booleans-as-ints/snippet.txt @@ -0,0 +1,3 @@ +def score(x_coord, y_coord): + radius_squared = x_coord**2 + y_coord**2 + return (radius_squared<=1)*5 + (radius_squared<=25)*4 + (radius_squared<=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..337f370bf9e --- /dev/null +++ b/exercises/practice/darts/.approaches/config.json @@ -0,0 +1,56 @@ +{ + "introduction": { + "authors": ["bethanyg"], + "contributors": ["yrahcaz7"] + }, + "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"], + "contributors": ["yrahcaz7"] + }, + { + "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"], + "contributors": ["yrahcaz7"] + }, + { + "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"], + "contributors": ["yrahcaz7"] + }, + { + "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"], + "contributors": ["yrahcaz7"] + }, + { + "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"], + "contributors": ["yrahcaz7"] + }, + { + "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 dict.get.", + "authors": ["bethanyg"], + "contributors": ["yrahcaz7"] + } + ] +} \ 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..62c79f36a0d --- /dev/null +++ b/exercises/practice/darts/.approaches/dict-and-dict-get/content.md @@ -0,0 +1,70 @@ +# 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..8d2f426d84e --- /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..041ce80f1e4 --- /dev/null +++ b/exercises/practice/darts/.approaches/dict-and-generator/content.md @@ -0,0 +1,74 @@ +# 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} + + return max(point for distance, point in + rules.items() if throw <= distance, + default=0) +``` + + +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 +from math import inf + +def score(x_coord, y_coord): + throw = x_coord**2 + y_coord**2 + rules = {1: 10, 25: 5, 100: 1, inf: 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, inf: 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}.items() if + (x_coord**2 + y_coord**2) <= distance, + default=0) +``` + +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..1e2b61a3557 --- /dev/null +++ b/exercises/practice/darts/.approaches/dict-and-generator/snippet.txt @@ -0,0 +1,7 @@ +def score(x_coord, y_coord): + throw = x_coord**2 + y_coord**2 + rules = {1: 10, 25: 5, 100: 1} + + return max(point for distance, point in + rules.items() if throw <= distance, + default=0) \ 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..9bbb7ecf8c4 --- /dev/null +++ b/exercises/practice/darts/.approaches/if-statements/content.md @@ -0,0 +1,72 @@ +# 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_squared = x_coord**2 + y_coord**2 + + if distance_squared <= 1: return 10 + if distance_squared <= 25: return 5 + if distance_squared <= 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_squared = x_coord**2 + y_coord**2 + points = 10 + + if distance_squared > 100: points = 0 + if 25 < distance_squared <= 100: points = 1 + if 1 < distance_squared <= 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..b91a4285e6d --- /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..f4ca20dcf0c --- /dev/null +++ b/exercises/practice/darts/.approaches/introduction.md @@ -0,0 +1,149 @@ +# 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) + + for distance, points in rules: + if throw <= distance: + return points + + return 0 +``` + + +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} + + return max(point for distance, point in + rules.items() if throw <= distance, + default=0) +``` + +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_squared = x_coord**2 + y_coord**2 + return (radius_squared<=1)*5 + (radius_squared<=25)*4 + (radius_squared<=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-booleans-as-ints] 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_coord, y_coord): + match ceil(hypot(x_coord, y_coord)): + 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-match-case] 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-booleans-as-ints]: https://exercism.org/tracks/python/exercises/darts/approaches/booleans-as-ints +[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-match-case]: https://exercism.org/tracks/python/exercises/darts/approaches/match-case +[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..39bb3c35b8b --- /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_coord, y_coord): + throw = ceil(hypot(x_coord, y_coord)) + + 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_coord, y_coord): + match ceil(hypot(x_coord, y_coord)): + 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_coord, y_coord): + throw = ceil(hypot(x_coord, y_coord)) + + 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_coord, y_coord): + throw = ceil(hypot(x_coord, y_coord)) + + 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 previous variations show this clearly_) and might be harder to reason about/understand for some programmers: + + +```python +from math import hypot, ceil + +def score(x_coord, y_coord): + match throw := ceil(hypot(x_coord, y_coord)): + 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..564f4999491 --- /dev/null +++ b/exercises/practice/darts/.approaches/match-case/snippet.txt @@ -0,0 +1,8 @@ +from math import hypot, ceil + +def score(x_coord, y_coord): + match ceil(hypot(x_coord, y_coord)): + 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..ec92b3142e9 --- /dev/null +++ b/exercises/practice/darts/.approaches/tuple-and-loop/content.md @@ -0,0 +1,59 @@ +# 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) + + for distance, points in rules: + if throw <= distance: + return points + + return 0 +``` + +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)] + + 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} + + 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..f777f4c04ec --- /dev/null +++ b/exercises/practice/darts/.approaches/tuple-and-loop/snippet.txt @@ -0,0 +1,8 @@ +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 \ No newline at end of file diff --git a/exercises/practice/diamond/diamond_test.py b/exercises/practice/diamond/diamond_test.py index 6a3a2295098..448f65d5326 100644 --- a/exercises/practice/diamond/diamond_test.py +++ b/exercises/practice/diamond/diamond_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/diamond/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2026-02-19 import unittest @@ -10,6 +10,7 @@ class DiamondTest(unittest.TestCase): + def test_degenerate_case_with_a_single_a_row(self): result = ["A"] self.assertEqual(rows("A"), result) diff --git a/exercises/practice/difference-of-squares/difference_of_squares_test.py b/exercises/practice/difference-of-squares/difference_of_squares_test.py index aa7271907ac..3536a20f8f8 100644 --- a/exercises/practice/difference-of-squares/difference_of_squares_test.py +++ b/exercises/practice/difference-of-squares/difference_of_squares_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/difference-of-squares/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2026-02-19 import unittest @@ -12,6 +12,7 @@ class DifferenceOfSquaresTest(unittest.TestCase): + def test_square_of_sum_1(self): self.assertEqual(square_of_sum(1), 1) diff --git a/exercises/practice/diffie-hellman/diffie_hellman_test.py b/exercises/practice/diffie-hellman/diffie_hellman_test.py index e24c4e742a1..f6a2a9c1cfd 100644 --- a/exercises/practice/diffie-hellman/diffie_hellman_test.py +++ b/exercises/practice/diffie-hellman/diffie_hellman_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/diffie-hellman/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2026-02-19 import unittest @@ -12,11 +12,12 @@ class DiffieHellmanTest(unittest.TestCase): + def test_private_key_is_greater_than_1_and_less_than_p(self): for prime in [5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]: with self.subTest(f"prime={prime}"): key = private_key(prime) - self.assertTrue(1 < key < prime, msg=f"{key} out of range, expected to be >1 and <{prime}") # fmt: skip + self.assertTrue(1 < key < prime, msg=f"{key} out of range, expected to be >1 and <{prime}") # fmt: skip def test_private_key_is_random(self): """ @@ -30,19 +31,19 @@ def test_can_calculate_public_key_using_private_key(self): p = 23 g = 5 private_key = 6 - self.assertEqual(8, public_key(p, g, private_key, )) # fmt: skip + self.assertEqual(8, public_key(p, g, private_key, )) # fmt: skip def test_can_calculate_public_key_when_given_a_different_private_key(self): p = 23 g = 5 private_key = 15 - self.assertEqual(19, public_key(p, g, private_key, )) # fmt: skip + self.assertEqual(19, public_key(p, g, private_key, )) # fmt: skip def test_can_calculate_secret_using_other_party_s_public_key(self): p = 23 their_public_key = 19 my_private_key = 6 - self.assertEqual(2, secret(p, their_public_key, my_private_key, )) # fmt: skip + self.assertEqual(2, secret(p, their_public_key, my_private_key, )) # fmt: skip def test_key_exchange(self): p = 23 diff --git a/exercises/practice/dnd-character/.approaches/ability-method/content.md b/exercises/practice/dnd-character/.approaches/ability-method/content.md new file mode 100644 index 00000000000..43753f6522b --- /dev/null +++ b/exercises/practice/dnd-character/.approaches/ability-method/content.md @@ -0,0 +1,43 @@ +# Use the `ability()` Method to Generate and Return the Dice Rolls + + +```python +from random import sample + +class Character: + def __init__(self): + self.strength = self.ability() + self.dexterity = self.ability() + self.constitution = self.ability() + self.intelligence = self.ability() + self.wisdom = self.ability() + self.charisma = self.ability() + + self.hitpoints = 10 + modifier(self.constitution) + + def ability(self): + values = sample(range(1, 7), 4) + return sum(values) - min(values) + +def modifier(constitution): + return (constitution - 10)//2 +``` + + +This approach uses a single `ability()` method to calculate the dice rolls and return an ability value. +`ability()` is then called in `__init__()` to populate the listed-out character attributes. +`self.hitpoints` calls the stand-alone `modifier()` function, adding it to 10 for the character's hitpoints attribute. + +This approach is valid and passes all the tests. +However, it will trigger an analyzer comment about there being "too few public methods", since there are no methods for this class beyond the one that calculates attribute values. + + +The "too few" rule encourages you to think about the design of the class: is it worth the effort to create the class if it only holds attribute values for a character? +What other functionality should this class hold? +Should you separate dice rolls from ability values? +Is the class better as a [dataclass][dataclass], with dice roll as a utility function? + +None of these (_including the analyzer complaint about too few methods_) is a hard and fast rule or requirement - all are considerations for the class as you build out a larger program. + + +[dataclass]: https://docs.python.org/3/library/dataclasses.html diff --git a/exercises/practice/dnd-character/.approaches/ability-method/snippet.txt b/exercises/practice/dnd-character/.approaches/ability-method/snippet.txt new file mode 100644 index 00000000000..272a00c1ca0 --- /dev/null +++ b/exercises/practice/dnd-character/.approaches/ability-method/snippet.txt @@ -0,0 +1,8 @@ + ... + def __init__(self): + for ability_type in self.ABILITIES: + setattr(self, ability_type, self.ability()) + + def ability(self): + values = sample(range(1, 7), 4) + return sum(values) - min(values) \ No newline at end of file diff --git a/exercises/practice/dnd-character/.approaches/config.json b/exercises/practice/dnd-character/.approaches/config.json new file mode 100644 index 00000000000..0132d2027af --- /dev/null +++ b/exercises/practice/dnd-character/.approaches/config.json @@ -0,0 +1,37 @@ +{ + "introduction": { + "authors": ["colinleach", + "BethanyG"], + "contributors": [] + }, + "approaches": [ + { + "uuid": "952835d1-e9d1-4dc3-b2c2-aad703e8db2e", + "slug": "ability-method", + "title": "Ability Method", + "blurb": "Use one method to generate dice rolls and return ability values.", + "authors": ["BethanyG"] + }, + { + "uuid": "5022c80c-8ca1-45e4-aa5f-d91bb3f53441", + "slug": "dice-roll-static-method", + "title": "Dice Roll Static Method", + "blurb": "Use a separate static method to conduct dice rolls.", + "authors": ["BethanyG"] + }, + { + "uuid": "98af6c1d-5ab4-476d-9041-30b8f55e13eb", + "slug": "stand-alone-dice-roll-function", + "title": "Stand-alone Dice Roll Function", + "blurb": "Create a dice roll function outside the Character class.", + "authors": ["BethanyG"] + }, + { + "uuid": "5dd9d9b0-bfa5-43b2-8e95-787425349fc4", + "slug": "loop-and-setattr-in-init", + "title": "Loop and setattr in init", + "blurb": "Use a tuple of attributes, a loop, and setattr in init to assign ability values.", + "authors": ["BethanyG"] + } + ] +} diff --git a/exercises/practice/dnd-character/.approaches/dice-roll-static-method/content.md b/exercises/practice/dnd-character/.approaches/dice-roll-static-method/content.md new file mode 100644 index 00000000000..ef358c60698 --- /dev/null +++ b/exercises/practice/dnd-character/.approaches/dice-roll-static-method/content.md @@ -0,0 +1,47 @@ +# Move Dice Rolls Into a Static Method Separate from the `ability` Method + + +```python +from math import floor +from random import choice + +class Character: + def __init__(self): + self.strength = Character.dice_rolls() + self.dexterity = Character.dice_rolls() + self.constitution = Character.dice_rolls() + self.intelligence = Character.dice_rolls() + self.wisdom = Character.dice_rolls() + self.charisma = Character.dice_rolls() + + self.hitpoints = 10 + modifier(self.constitution) + + def ability(self): + return choice([*vars(self).values()]) + + @staticmethod + def dice_rolls(): + values = sorted(choice(range(1,7)) for dice in range(4))[::-1] + return sum(values[:-1]) + +def modifier(constitution): + return floor((constitution - 10)/2) +``` + +This approach separates the `ability()` method from a [`static method`][staticmethod] that calculates dice rolls. +`ability()` returns the value of a randomly chosen character ability using [`random.choice`][random-choice] but does not roll dice or calculate values. +Instead, `dice_rolls()` handles the rolls/values using [`random.choice`][random-choice] for selection. + +The argument for this is that the logic/functionality of rolling dice 4 times and summing the top three values is not really related to a DnD character or their abilities - it is independent and likely useful across a wider scope than just the character class. +However, it might be tidier to include it in the character class, rather than "clutter" the program or module with an additional stand-alone function. +Declaring `dice_rolls()` as a static method allows other callers to use the function with or without instantiating a new `Character` object. +It also makes it cleaner to maintain, should the method or number of the dice rolls change. + +`dice_rolls()` is then called in `__init__()` to populate the listed-out character attributes. +Note that it needs to be called with the class name: `Character.dice_rolls()`. +`self.hitpoints` then calls the second stand-alone `modifier()` function, adding it to 10 for the character's `hitpoints` attribute. +`modifieer()` in this example uses [`math.floor`][math-floor] for calculating the `hitpoints` value. + +[math-floor]: https://docs.python.org/3/library/math.html#math.floor +[random-choice]: https://docs.python.org/3/library/random.html#random.choice +[staticmethod]: https://docs.python.org/3/library/functions.html#staticmethod diff --git a/exercises/practice/dnd-character/.approaches/dice-roll-static-method/snippet.txt b/exercises/practice/dnd-character/.approaches/dice-roll-static-method/snippet.txt new file mode 100644 index 00000000000..4ed53a4e01f --- /dev/null +++ b/exercises/practice/dnd-character/.approaches/dice-roll-static-method/snippet.txt @@ -0,0 +1,8 @@ + ... + def ability(self): + return choice([*vars(self).values()]) + + @staticmethod + def dice_roll(): + values = sample(range(1, 7), 4) + return sum(sorted(values, reverse=True)[:-1]) \ No newline at end of file diff --git a/exercises/practice/dnd-character/.approaches/introduction.md b/exercises/practice/dnd-character/.approaches/introduction.md new file mode 100644 index 00000000000..b83d049d4e9 --- /dev/null +++ b/exercises/practice/dnd-character/.approaches/introduction.md @@ -0,0 +1,261 @@ +# Introduction + +The DnD Character exercise has two main purposes: + +1. Practice class-based programming, especially initialization of instance variables and the creation of methods. +2. Practice the use of pseudo-random numbers and methods in the Python [`random`][random] module. + +There are no complicated decisions to make about which algorithm to use, as the tests for this exercise constrain the implementation. +However, there is variation in how class and object variables are declared and initialized, how methods are declared, and which random functions are employed. + +These approaches will mostly explore these variations in Python syntax. + + +## General considerations + +Several items are specifically required and tested for: + +- A standalone (_outside the `Character` class_) function called `modifier()`. +- A `Character` class. +- Instance variables for 6 named abilities, plus one for hit points. +- An instance method called `ability()` to return ability values. + +Further methods are optional, but may be helpful to simulate random dice rolls and calculate ability scores. +Some methods (_such as `ability()`, or the optional `dice_roll()`_) may be better as [static methods][static-methods], since they do not necessarily require the class or object as a parameter but are still tied to the class logic/purpose. + + +### The `modifier()` function + +This stand-alone function modifies the characters constitution score by subtracting 10 from the total and dividing by 2. + +In Python 3.x, the division operators are `/` for floating point and `//` for integer or 'floor' division. +`modifier()` needs to use integer division, and return a result that does not have a decimal. +Function equivalents to `/` and `//` are [`operator.truediv(a, b)`][operator-trudiv] and [`operator.floordiv(a, b)`][operator-floordiv] (or [`math.floor(x)`][math-floor]). + +Integer division will always round "down": not to the nearest integer and not towards zero: + +```python +>>> 11 // 3 +3 +>>> -11 // 3 +-4 +``` + +Here are examples using both operators and functions imported from `math` and from `operator`: + +```python +def modifier(constitution): + return (constitution - 10)//2 + +# You can import the math module and use `floor()` +from math import floor + +def modifier(constitution): + return floor((constitution - 10)/2) + +# Another strategy is to use `floordiv()` from the operator module. +from operator import floordiv + +def modifier(constitution): + return floordiv((constitution - 10), 2) + +``` + +Using function equivalents in a solution will work, but they do create overhead due to module import and function calls. + + +### Dice rolls + +The instructions are to roll four 6-sided dice and record the sum of the largest three rolls. + +To simulate a roll of the dice, we need the [`random`][random] module which produces pseudo-random numbers. +The [`secrets`][secrets] module, which produces cryptographically strong random numbers is not needed here. + +Within `random`, there are various suitable methods available. + These include [`random.randint()`][randint] to produce an integer in a range, [`random.choice()`][choice] to select from a sequence of values, or even [`random.sample()`][random-sample] for multiple rolls 'sampled' from a distribution, group, or range of values. + + ````exercism/note + +`randint(lower, upper)` _**includes**_ the upper bound, in contrast to the built-in [`range(lower, upper)`][range], or Python's [slice notation][slice] which both _**exclude**_ the upper bound. + +[slice]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations +[range]: https://docs.python.org/3/library/stdtypes.html#range +```` + + +To roll four dice may need a loop or comprehension: + +```python +from random import randint, choice, sample + + # Choosing a pseudo-random number between 1 and 6 (inclusive) four different times. + def dice_rolls_randint(self): + rolls = [] + + for dice in rage(4): + rolls.append(randint(1, 6)) + + return rolls + + # Choosing from a range sequence between 1 and 7 (exclusive) four different times. + def dice_rolls_choice(self): + return [choice(range(1,7)) for dice in range(4)] + + # Choosing for different 'samples' (rolls) from a sample range + # between 1 and 7 (exclusive). This avoids a comprehension, + # since random.sample returns a list of unique values from the range. + def four_dice_rolls_sample(self): + return sample(range(1, 7), 4) + +``` + +Some community solutions begin with a call to `random.seed()` but (_at least in recent versions of Python_) calling this without a parameter has exactly the same effect as omitting it. +The value of this method is in debugging situations when it helps to have a reproducible sequence of results. +Then we would call it with a known seed such as `random.seed(42)`. + +After rolling four dice, we next need to sum the largest three scores, discarding the lowest. +Many programmers use `sorted()` and a slice for determining the three largest values: + + +```python +# The dice variable was generated as a 4-element list, as above. +sum(sorted(dice)[1:]) + +# The list can also be reverse sorted. +sum(sorted(dice, reverse=True)[:-1]) + +``` + +If slicing is hard to read or confusing, the built-ins `sum()` and `min()` can be used to subtract the smallest score from the total. +In this second case, `dice` can be any sequence, not just a list. +Because we are calculating a `min()` value, a generator expression cannot be used. + +```python +# The dice variable was generated as a 4-element sequence, as above. +sum(dice) - min(dice) + +``` + +This strategy might be considered more readable to some, since `min()` is very clear as to what is being calculated. +This is also more efficient when the input list is long and the order of values doesn't need to be preserved. + + +### The ability() Method + +The directions do not state how `ability()` should be implemented, but a look at [the tests][dnd-tests] indicate that any of the character's ability scores or a freshly generated score can be returned provided the score falls in the required range. +This means that the method can return a random ability value chosen from `vars(self).values()` **or** it can calculate and return a random value for use in an ability score. +Some solutions use this method as their "dice roll", and call it to populate abilities in `__init__`. +Other solutions separate out "dice rolls" or "ability score" logic into a separate method or function, leaving `ability()` as a method that returns a random choice from the character's already assigned abilities. + +Either strategy above will pass the tests, but separating out "dice roll" logic from a random character ability score can be both clearer and more reusable in scenarios where the same "dice roll" is used to calculate/populate some other metric. +Moving the "dice roll" or "ability score" out of the class altogether or making it a static method also lets it be called/used without instantiating the class and can make it clearer what is being calculated. + + +## Class initialization + +The various abilities need to be set just once for each character, which is most conveniently done in the class `__init__` method. + +The examples below assume that `modifier()` and `self.ability()` are implemented as discussed in the sections above. +The explicit approach is simple but rather verbose and repetitive. +It does have the advantage of declaring the abilities very explicitly, so that a reader can quickly determine how many and of what type the abilities are: + + +```python +class Character: + def __init__(self): + self.strength = self.ability() + self.dexterity = self.ability() + self.constitution = self.ability() + self.intelligence = self.ability() + self.wisdom = self.ability() + self.charisma = self.ability() + self.hitpoints = 10 + modifier(self.constitution) + + ... + + +# Using a dice_roll static method instead of self.ability() +class Character: + def __init__(self): + self.strength = Character.dice_roll() + self.dexterity = Character.dice_roll() + self.constitution = Character.dice_roll() + self.intelligence = Character.dice_roll() + self.wisdom = Character.dice_roll() + self.charisma = Character.dice_roll() + self.hitpoints = 10 + modifier(self.constitution) + + ... +``` + + +Alternatively, we could start from an iterable of ability names and loop over these using [`setattr()`][setattr] to write the values into the objects attribute dictionary. +This sacrifices a bit of readability/clarity for less verbosity and (somewhat) easier ability additions: + + +```python +# Setting a global ABILITIES constant. +# This enables other classes to "see" the abilities. +# This could be useful for a larger program that modifies +# or adds abilities outside the Character class. +ABILITIES = ('strength', 'dexterity', 'constitution', + 'intelligence', 'wisdom','charisma') + + +class Character: + def __init__(self): + + for ability_name in ABILITIES: + setattr(self, ability_name, self.ability()) + + self.hitpoints = modifier(self.constitution) + 10 + + +# Conversely, we can declare ABILITIES as a +# class attribute. This ensures that all objects made from +# the class share ABILITIES and they are only added or +# modified through the Character class. ABILITIES are not +# visible globally. +class Character: + + abilities = ('strength', 'dexterity', 'constitution', + 'intelligence', 'wisdom','charisma') + + def __init__(self): + + for ability_name in Character.abilities: + setattr(self, ability_name, self.ability()) + + self.hitpoints = modifier(self.constitution) + 10 +``` + +Listing out each ability vs looping through and using `setattr()` has identical results for the object. +Both calculate a score for an ability and write that ability + score into the object attribute dictionary when an object is instantiated from the class. + + +## Putting things together + +The four approaches below combine various options from the previous sections to show how a solution would work with them. +More variations are possible, but these cover most of the main decision differences. + +- [One `ability` method][approach-ability-method] +- [Dice roll static method][approach-dice-roll-static-method] +- [Dice roll stand-alone method][approach-stand-alone-dice-roll-function] +- [Loop and `setattr()` in `__init__`][approach-loop-and-setattr-in-init] + + +[approach-ability-method]: https://exercism.org/tracks/python/exercises/dnd-character/approaches/ability-method +[approach-dice-roll-static-method]: https://exercism.org/tracks/python/exercises/dnd-character/approaches/dice-roll-static-method +[approach-loop-and-setattr-in-init]: https://exercism.org/tracks/python/exercises/dnd-character/approaches/loop-and-setattr-in-init +[approach-stand-alone-dice-roll-function]: https://exercism.org/tracks/python/exercises/dnd-character/approaches/tand-alone-dice-roll-function +[choice]: https://docs.python.org/3/library/random.html#random.choice +[dnd-tests]: https://github.com/exercism/python/blob/main/exercises/practice/dnd-character/dnd_character_test.py +[math-floor]: https://docs.python.org/3/library/math.html#math.floor +[operator-floordiv]: https://docs.python.org/3/library/operator.html#operator.floordiv +[operator-trudiv]: https://docs.python.org/3/library/operator.html#operator.truediv +[randint]: https://docs.python.org/3/library/random.html#random.randint +[random-sample]: https://docs.python.org/3/library/random.html#random.sample +[random]: https://exercism.org/tracks/python/concepts/random +[secrets]: https://docs.python.org/3/library/secrets.html +[setattr]: https://docs.python.org/3/library/functions.html#setattr +[static-methods]: https://www.digitalocean.com/community/tutorials/python-static-method diff --git a/exercises/practice/dnd-character/.approaches/loop-and-setattr-in-init/content.md b/exercises/practice/dnd-character/.approaches/loop-and-setattr-in-init/content.md new file mode 100644 index 00000000000..1159ccf0dd3 --- /dev/null +++ b/exercises/practice/dnd-character/.approaches/loop-and-setattr-in-init/content.md @@ -0,0 +1,84 @@ +# Loop Through an Abilities Tuple to Set Attribute Values in `__init__` + + +```python +from random import choice, sample + +class Character: + + abilities = ('strength', 'dexterity', 'constitution', 'intelligence', 'wisdom', 'charisma') + + def __init__(self): + for ability_type in Character.abilities: + setattr(self, ability_type, Character.dice_rolls()) + + self.hitpoints = 10 + modifier(self.constitution) + + def ability(self): + return choice([*vars(self).values()]) + + @staticmethod + def dice_rolls(): + values = sample(range(1, 7), 4) + return sum(values) - min(values) + +def modifier(constitution): + return (constitution - 10)//2 +``` + + +This approach uses a `tuple` to hold character attributes in a [`class variable`][class-variable] or `class attribute`. +Since this variable is common to all instances of the class, it can be looped through during object initialization to create instance variables and assign them values using [`setattr`][setattr]. + +This strategy has several benefits: +1. The `__init__` is less verbose and the abilities are easier to maintain. +2. Organizationally, attributes remain with the class, making it clearer where they apply. +3. Attributes are inherited in any subclass and can be added to or overridden by them. +4. Changes to attributes are reflected in all new objets and subclasses automatically. + +Because `hitpoints` is calculated differently, it is assigned a value outside the `tuple` and `loop`. + +The remainder of the class body is the same as in the [dice roll static method][approach-dice-roll-static-method] approach (_as is the variant below_). + + +```python +from random import choice, sample + +CHARACTER_ABILITIES = ('strength', 'dexterity', 'constitution', + 'intelligence', 'wisdom', 'charisma') + +class Character: + + def __init__(self): + for ability_type in CHARACTER_ABILITIES: + setattr(self, ability_type, Character.dice_rolls()) + + self.hitpoints = 10 + modifier(self.constitution) + + def ability(self): + return choice([*vars(self).values()]) + + @staticmethod + def dice_rolls(): + values = sample(range(1, 7), 4) + return sum(values) - min(values) + +def modifier(constitution): + return (constitution - 10)//2 +``` + +Above, the character attributes are moved out of the class into a [`constant`][constant] at the module level. +The Character `__init__` method loops through them using `setattr` to create instance variables and assign them values, similar to the first example. +Again, because `hitpoints` is calculated differently, it is assigned a value outside the `tuple` and `loop`. +Making the character attributes a constant has the advantage of being visible to all other classes and functions defined in the module. + This could be easier if the attributes are being used by multiple classes beyond the Character class. +This also avoids having to reference the Character class when using or modifying the abilities and could help with clarity and maintenance in the larger program. +However, modifying the abilities in this context would also be visible at the module level, and could have wide or unintended consequences, so should be commented/documented carefully. + +The remainder of the class body is the same as in the [dice roll static method][approach-dice-roll-static-method] approach. + + +[approach-dice-roll-static-method]: https://exercism.org/tracks/python/exercises/dnd-character/approaches/dice-roll-static-method +[class-variable]: https://docs.python.org/3/tutorial/classes.html#class-and-instance-variables +[constant]: https://peps.python.org/pep-0008/#constants +[setattr]: https://docs.python.org/3/library/functions.html#setattr diff --git a/exercises/practice/dnd-character/.approaches/loop-and-setattr-in-init/snippet.txt b/exercises/practice/dnd-character/.approaches/loop-and-setattr-in-init/snippet.txt new file mode 100644 index 00000000000..353296a9576 --- /dev/null +++ b/exercises/practice/dnd-character/.approaches/loop-and-setattr-in-init/snippet.txt @@ -0,0 +1,8 @@ +class Character: + ABILITIES = ('strength', 'dexterity', 'constitution', 'intelligence', 'wisdom', 'charisma') + + def __init__(self): + for ability_type in self.ABILITIES: + setattr(self, ability_type, self.ability()) + self.hitpoints = 10 + modifier(self.constitution) + ... \ No newline at end of file diff --git a/exercises/practice/dnd-character/.approaches/stand-alone-dice-roll-function/content.md b/exercises/practice/dnd-character/.approaches/stand-alone-dice-roll-function/content.md new file mode 100644 index 00000000000..66d293a07d6 --- /dev/null +++ b/exercises/practice/dnd-character/.approaches/stand-alone-dice-roll-function/content.md @@ -0,0 +1,99 @@ +# Separate Dice Rolls into a Stand-Alone Dice Roll Function + + +```python +from random import choice, randint +from operator import floordiv + + +class Character: + def __init__(self): + self.strength = dice_rolls() + self.dexterity = dice_rolls() + self.constitution = dice_rolls() + self.intelligence = dice_rolls() + self.wisdom = dice_rolls() + self.charisma = dice_rolls() + + self.hitpoints = 10 + modifier(self.constitution) + + def ability(self): + return choice([*vars(self).values()]) + + +def dice_rolls(): + values = sorted(randint(1, 6) for item in range(4)) + return sum(values[1:]) + +def modifier(constitution): + return floordiv((constitution - 10), 2) +``` + + +This approach separates the `ability()` method from a stand-alone function that calculates dice rolls. +`ability()` returns the value of a randomly chosen character ability using [`random.choice`][randon-choice], but does not roll dice or calculate values. +Instead, `dice_rolls()` handles the rolls/values, using [`random.randint`][random-randint] to generate them. +The argument for this is that the logic/functionality of rolling dice 4 times and summing the top three values is not really related to a DnD character or their abilities - it is independent and likely useful across a wider scope than just the character class. +It also makes it cleaner to maintain, should the method or number of the dice rolls change. + +`dice_rolls()` is then called in `__init__()` to populate the listed-out character attributes. +`self.hitpoints` calls the second stand-alone `modifier()` function, adding it to 10 for the character's `hitpoints` attribute. +Note that `modifier()` uses the [`operator.floordiv`][operator-floordiv] method to trunkate the value. + +This approach is valid and passes all the tests. +However, it will trigger an analyzer comment about there being "too few public methods", since there are no methods for this class beyond `ability()`. + +The "too few" rule encourages you to think about the design of the class: is it worth the effort to create the class if it only holds attribute values for a character? +What other functionality should this class hold? +Should the `dice_roll()` function be outside or inside (_as a regular method or a static method_) the class? + +None of these (_including the analyzer complaint about too few methods_) is a hard and fast rule or requirement - all are considerations for the class as you build out a larger program. + +An alternative is to write a [dataclass][dataclass], although the design discussion and questions above remain the same: + + +```python +from random import choice, sample +from dataclasses import dataclass + +@dataclass +class Character: + + strength: int = 0 + dexterity: int = 0 + constitution: int = 0 + intelligence: int = 0 + wisdom: int = 0 + charisma: int = 0 + hitpoints: int = 0 + + def __post_init__(self): + for ability in vars(self): + setattr(self, ability, dice_rolls()) + + self.hitpoints = 10 + modifier(self.constitution) + + def ability(self): + return choice([*vars(self).values()]) + + +def dice_rolls(): + values = sample(range(1, 7), 4) + return sum(values) - min(values) + +def modifier(constitution): + return (constitution - 10)//2 +``` + + +Note that here there is a [`__post_init__`][post-init] method to assign ability values to the attributes, and that the attributes must start with a default value (_otherwise, they can't be assigned to in post-init_). +`hitpoints` has the same treatment as the other attributes, and requires assignment in post-init. + +`dice_rolls()` uses [`random.sample`][random-sample] for roll values here and `modifier()` uses the floor-division operator `//`. + +[dataclass]: https://docs.python.org/3/library/dataclasses.html +[operator-floordiv]: https://docs.python.org/3/library/operator.html#operator.floordiv +[post-init]: https://docs.python.org/3/library/dataclasses.html#post-init-processing +[random-randint]: https://docs.python.org/3/library/random.html#random.randint +[random-sample]: https://docs.python.org/3/library/random.html#random.randint +[randon-choice]: https://docs.python.org/3/library/random.html#random.choice diff --git a/exercises/practice/dnd-character/.approaches/stand-alone-dice-roll-function/snippet.txt b/exercises/practice/dnd-character/.approaches/stand-alone-dice-roll-function/snippet.txt new file mode 100644 index 00000000000..eaa4735de98 --- /dev/null +++ b/exercises/practice/dnd-character/.approaches/stand-alone-dice-roll-function/snippet.txt @@ -0,0 +1,8 @@ +class Character: + ... + def ability(self): + return choice([*vars(self).values()]) + +def dice_roll(): + values = sample(range(1, 7), 4) + return sum(sorted(values)[1:]) \ 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/dot-dsl/.meta/tests.toml b/exercises/practice/dot-dsl/.meta/tests.toml new file mode 100644 index 00000000000..3c56379a6eb --- /dev/null +++ b/exercises/practice/dot-dsl/.meta/tests.toml @@ -0,0 +1,67 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[3a50c618-2571-466b-9ee9-346d9943912e] +description = "empty graph" + +[5067feea-e49b-4a9d-865e-4502d6b0540c] +description = "graph with one node" + +[b66cf871-88c6-489a-b0b9-7c79b6819c45] +description = "graph with one node with attribute" + +[f7841da3-c0f8-4541-b594-21b626a764d2] +description = "graph with one edge" + +[bbee70e1-6b0d-4f3a-bd4e-41cd2cfc0e39] +description = "graph with one attribute" + +[ac736158-6684-418d-93d5-7b284e43294e] +description = "graph with comments" + +[69068da9-7690-4d4d-a728-f5c2bf132a33] +description = "graph with nodes, edges, and attributes" + +[f6c53993-3937-4959-bcde-dc16411113ae] +description = "multiple edges on one line" + +[b853dfc1-1f05-45aa-bc98-b0fc6b57529b] +description = "only 1 edge between nodes" + +[bdc0fdac-aa46-457f-8385-65736ccdc1c7] +description = "malformed input" + +[f5c4f77d-359c-434a-9c33-b9eb795bafdd] +description = "malformed edge" + +[2238f6b8-20bb-489f-8ca0-084d1771d758] +description = "malformed edge 2" + +[4e3a4386-9e80-4315-b70f-253a06a2234e] +description = "invalid edge type" + +[793adce3-bd19-4458-ac41-c989f7f8d9db] +description = "multiple edges missing a node" + +[e2930b2c-3a03-4d8f-abe9-78a125a915a7] +description = "multiple edges missing a connector" + +[55d3f722-f9f1-46e1-b308-da61607952ab] +description = "empty attribute" + +[4ee2a9c3-54b1-4825-bd58-2b78c2c53953] +description = "malformed attribute" + +[382a13c8-6419-4286-8dd2-eac708f3e2a8] +description = "empty attribute name" + +[a6f9e6ab-8c3e-4475-a9fe-5dd061cadec6] +description = "non-alphanumeric node name" 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/etl/.meta/config.json b/exercises/practice/etl/.meta/config.json index 32aab8ba3ba..41f0fde304a 100644 --- a/exercises/practice/etl/.meta/config.json +++ b/exercises/practice/etl/.meta/config.json @@ -29,5 +29,5 @@ }, "blurb": "Change the data format for scoring a game to more easily add other languages.", "source": "Based on an exercise by the JumpstartLab team for students at The Turing School of Software and Design.", - "source_url": "https://turing.edu" + "source_url": "https://www.turing.edu/" } diff --git a/exercises/practice/etl/etl_test.py b/exercises/practice/etl/etl_test.py index d6eed70a574..ef24e9af484 100644 --- a/exercises/practice/etl/etl_test.py +++ b/exercises/practice/etl/etl_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/etl/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2026-02-19 import unittest @@ -10,6 +10,7 @@ class EtlTest(unittest.TestCase): + def test_single_letter(self): legacy_data = {1: ["A"]} data = {"a": 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/food-chain/food_chain_test.py b/exercises/practice/food-chain/food_chain_test.py index 0cd42356ab1..4ce21d81339 100644 --- a/exercises/practice/food-chain/food_chain_test.py +++ b/exercises/practice/food-chain/food_chain_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/food-chain/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2026-02-19 import unittest @@ -10,6 +10,7 @@ class FoodChainTest(unittest.TestCase): + def test_fly(self): self.assertEqual( recite(1, 1), 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..848da9ed2f9 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 2026-02-19 import unittest @@ -11,6 +11,7 @@ class ForthTest(unittest.TestCase): + def test_parsing_and_numbers_numbers_just_get_pushed_onto_the_stack(self): self.assertEqual(evaluate(["1 2 3 4 5"]), [1, 2, 3, 4, 5]) @@ -36,6 +37,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 +59,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 +81,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 +113,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/game-of-life/.docs/instructions.md b/exercises/practice/game-of-life/.docs/instructions.md new file mode 100644 index 00000000000..49531406489 --- /dev/null +++ b/exercises/practice/game-of-life/.docs/instructions.md @@ -0,0 +1,11 @@ +# Instructions + +After each generation, the cells interact with their eight neighbors, which are cells adjacent horizontally, vertically, or diagonally. + +The following rules are applied to each cell: + +- Any live cell with two or three live neighbors lives on. +- Any dead cell with exactly three live neighbors becomes a live cell. +- All other cells die or stay dead. + +Given a matrix of 1s and 0s (corresponding to live and dead cells), apply the rules to each cell, and return the next generation. diff --git a/exercises/practice/game-of-life/.docs/introduction.md b/exercises/practice/game-of-life/.docs/introduction.md new file mode 100644 index 00000000000..2347b936e44 --- /dev/null +++ b/exercises/practice/game-of-life/.docs/introduction.md @@ -0,0 +1,9 @@ +# Introduction + +[Conway's Game of Life][game-of-life] is a fascinating cellular automaton created by the British mathematician John Horton Conway in 1970. + +The game consists of a two-dimensional grid of cells that can either be "alive" or "dead." + +After each generation, the cells interact with their eight neighbors via a set of rules, which define the new generation. + +[game-of-life]: https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life diff --git a/exercises/practice/game-of-life/.meta/config.json b/exercises/practice/game-of-life/.meta/config.json new file mode 100644 index 00000000000..f89d6569022 --- /dev/null +++ b/exercises/practice/game-of-life/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "BNAndras" + ], + "files": { + "solution": [ + "game_of_life.py" + ], + "test": [ + "game_of_life_test.py" + ], + "example": [ + ".meta/example.py" + ] + }, + "blurb": "Implement Conway's Game of Life.", + "source": "Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life" +} diff --git a/exercises/practice/game-of-life/.meta/example.py b/exercises/practice/game-of-life/.meta/example.py new file mode 100644 index 00000000000..d4a430a2a3e --- /dev/null +++ b/exercises/practice/game-of-life/.meta/example.py @@ -0,0 +1,34 @@ +def tick(matrix): + def count_neighbors(r, c): + count = 0 + for dr, dc in [ + (-1, -1), (-1, 0), (-1, 1), + (0, -1), (0, 1), + (1, -1), (1, 0), (1, 1), + ]: + nr, nc = r + dr, c + dc + if 0 <= nr < rows and 0 <= nc < cols: + count += matrix[nr][nc] + return count + + if not matrix: + return [] + + rows = len(matrix) + cols = len(matrix[0]) + + new_matrix = [] + for r in range(rows): + new_row = [] + for c in range(cols): + neighbors = count_neighbors(r, c) + current = matrix[r][c] + state = 0 + if current == 1 and (neighbors == 2 or neighbors == 3): + state = 1 + elif current == 0 and neighbors == 3: + state = 1 + new_row.append(state) + new_matrix.append(new_row) + + return new_matrix diff --git a/exercises/practice/game-of-life/.meta/template.j2 b/exercises/practice/game-of-life/.meta/template.j2 new file mode 100644 index 00000000000..324b1519baa --- /dev/null +++ b/exercises/practice/game-of-life/.meta/template.j2 @@ -0,0 +1,22 @@ +{% import "generator_macros.j2" as macros with context %} +{{ macros.canonical_ref() }} + +{{ macros.header() }} + +class {{ exercise | camel_case }}Test(unittest.TestCase): + {% for case in cases %} + def test_{{ case.description | to_snake }}(self): + matrix = [ + {% for row in case.input.matrix %} + {{ row }}, + {% endfor %} + ] + expected = [ + {% for row in case.expected %} + {{ row }}, + {% endfor %} + ] + self.assertEqual( + tick(matrix), expected + ) + {% endfor %} diff --git a/exercises/practice/game-of-life/.meta/tests.toml b/exercises/practice/game-of-life/.meta/tests.toml new file mode 100644 index 00000000000..398cd4546e5 --- /dev/null +++ b/exercises/practice/game-of-life/.meta/tests.toml @@ -0,0 +1,34 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[ae86ea7d-bd07-4357-90b3-ac7d256bd5c5] +description = "empty matrix" + +[4ea5ccb7-7b73-4281-954a-bed1b0f139a5] +description = "live cells with zero live neighbors die" + +[df245adc-14ff-4f9c-b2ae-f465ef5321b2] +description = "live cells with only one live neighbor die" + +[2a713b56-283c-48c8-adae-1d21306c80ae] +description = "live cells with two live neighbors stay alive" + +[86d5c5a5-ab7b-41a1-8907-c9b3fc5e9dae] +description = "live cells with three live neighbors stay alive" + +[015f60ac-39d8-4c6c-8328-57f334fc9f89] +description = "dead cells with three live neighbors become alive" + +[2ee69c00-9d41-4b8b-89da-5832e735ccf1] +description = "live cells with four or more neighbors die" + +[a79b42be-ed6c-4e27-9206-43da08697ef6] +description = "bigger matrix" diff --git a/exercises/practice/game-of-life/game_of_life.py b/exercises/practice/game-of-life/game_of_life.py new file mode 100644 index 00000000000..70694804ddd --- /dev/null +++ b/exercises/practice/game-of-life/game_of_life.py @@ -0,0 +1,2 @@ +def tick(matrix): + pass diff --git a/exercises/practice/game-of-life/game_of_life_test.py b/exercises/practice/game-of-life/game_of_life_test.py new file mode 100644 index 00000000000..24f89e6f73c --- /dev/null +++ b/exercises/practice/game-of-life/game_of_life_test.py @@ -0,0 +1,118 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/game-of-life/canonical-data.json +# File last updated on 2026-02-19 + +import unittest + +from game_of_life import ( + tick, +) + + +class GameOfLifeTest(unittest.TestCase): + + def test_empty_matrix(self): + matrix = [] + expected = [] + self.assertEqual(tick(matrix), expected) + + def test_live_cells_with_zero_live_neighbors_die(self): + matrix = [ + [0, 0, 0], + [0, 1, 0], + [0, 0, 0], + ] + expected = [ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + ] + self.assertEqual(tick(matrix), expected) + + def test_live_cells_with_only_one_live_neighbor_die(self): + matrix = [ + [0, 0, 0], + [0, 1, 0], + [0, 1, 0], + ] + expected = [ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + ] + self.assertEqual(tick(matrix), expected) + + def test_live_cells_with_two_live_neighbors_stay_alive(self): + matrix = [ + [1, 0, 1], + [1, 0, 1], + [1, 0, 1], + ] + expected = [ + [0, 0, 0], + [1, 0, 1], + [0, 0, 0], + ] + self.assertEqual(tick(matrix), expected) + + def test_live_cells_with_three_live_neighbors_stay_alive(self): + matrix = [ + [0, 1, 0], + [1, 0, 0], + [1, 1, 0], + ] + expected = [ + [0, 0, 0], + [1, 0, 0], + [1, 1, 0], + ] + self.assertEqual(tick(matrix), expected) + + def test_dead_cells_with_three_live_neighbors_become_alive(self): + matrix = [ + [1, 1, 0], + [0, 0, 0], + [1, 0, 0], + ] + expected = [ + [0, 0, 0], + [1, 1, 0], + [0, 0, 0], + ] + self.assertEqual(tick(matrix), expected) + + def test_live_cells_with_four_or_more_neighbors_die(self): + matrix = [ + [1, 1, 1], + [1, 1, 1], + [1, 1, 1], + ] + expected = [ + [1, 0, 1], + [0, 0, 0], + [1, 0, 1], + ] + self.assertEqual(tick(matrix), expected) + + def test_bigger_matrix(self): + matrix = [ + [1, 1, 0, 1, 1, 0, 0, 0], + [1, 0, 1, 1, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 1, 1, 0], + [1, 0, 0, 0, 1, 1, 0, 0], + [1, 1, 0, 0, 0, 1, 1, 1], + [0, 0, 1, 0, 1, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 1, 1], + ] + expected = [ + [1, 1, 0, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 0], + [1, 0, 1, 1, 1, 1, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 1], + [1, 1, 0, 0, 1, 0, 0, 1], + [1, 1, 0, 1, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 1], + ] + self.assertEqual(tick(matrix), expected) diff --git a/exercises/practice/gigasecond/.meta/config.json b/exercises/practice/gigasecond/.meta/config.json index 76af3dac048..bb13ae47094 100644 --- a/exercises/practice/gigasecond/.meta/config.json +++ b/exercises/practice/gigasecond/.meta/config.json @@ -31,5 +31,5 @@ }, "blurb": "Given a moment, determine the moment that would be after a gigasecond has passed.", "source": "Chapter 9 in Chris Pine's online Learn to Program tutorial.", - "source_url": "https://pine.fm/LearnToProgram/?Chapter=09" + "source_url": "https://pine.fm/LearnToProgram/chap_09.html" } diff --git a/exercises/practice/go-counting/go_counting_test.py b/exercises/practice/go-counting/go_counting_test.py index aec80a1b369..036e3c26fd7 100644 --- a/exercises/practice/go-counting/go_counting_test.py +++ b/exercises/practice/go-counting/go_counting_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/go-counting/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2026-02-19 import unittest @@ -13,6 +13,7 @@ class GoCountingTest(unittest.TestCase): + def test_black_corner_territory_on_5x5_board(self): board = Board([" B ", " B B ", "B W B", " W W ", " W "]) stone, territory = board.territory(x=0, y=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/grade-school/grade_school_test.py b/exercises/practice/grade-school/grade_school_test.py index 30d91c6c57d..638c776474a 100644 --- a/exercises/practice/grade-school/grade_school_test.py +++ b/exercises/practice/grade-school/grade_school_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/grade-school/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2026-02-19 import unittest @@ -10,6 +10,7 @@ class GradeSchoolTest(unittest.TestCase): + def test_roster_is_empty_when_no_student_is_added(self): school = School() expected = [] 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/grains/grains_test.py b/exercises/practice/grains/grains_test.py index 177f91faa1a..bd924723348 100644 --- a/exercises/practice/grains/grains_test.py +++ b/exercises/practice/grains/grains_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/grains/canonical-data.json -# File last updated on 2023-09-27 +# File last updated on 2026-02-19 import unittest @@ -11,6 +11,7 @@ class GrainsTest(unittest.TestCase): + def test_grains_on_square_1(self): self.assertEqual(square(1), 1) diff --git a/exercises/practice/grep/.meta/config.json b/exercises/practice/grep/.meta/config.json index b79d15e0c75..70eb4116dca 100644 --- a/exercises/practice/grep/.meta/config.json +++ b/exercises/practice/grep/.meta/config.json @@ -27,5 +27,5 @@ }, "blurb": "Search a file for lines matching a regular expression pattern. Return the line number and contents of each matching line.", "source": "Conversation with Nate Foster.", - "source_url": "https://www.cs.cornell.edu/Courses/cs3110/2014sp/hw/0/ps0.pdf" + "source_url": "https://www.cs.cornell.edu/courses/cs3110/2014sp/hw/0/ps0.pdf" } 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/hangman/.meta/tests.toml b/exercises/practice/hangman/.meta/tests.toml new file mode 100644 index 00000000000..12917b7c4cb --- /dev/null +++ b/exercises/practice/hangman/.meta/tests.toml @@ -0,0 +1,40 @@ +# 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. + +[2419ffe6-16d8-4059-856a-9a101998a418] +description = "Initially 9 failures are allowed and no letters are guessed" + +[17c4296d-daab-44dc-8155-37c77caa52c1] +description = "After 10 failures the game is over" + +[77c9ae1f-bbc4-4ed4-b67e-08110cbcfc17] +description = "Losing with several correct guesses" + +[25101d8d-9874-405b-9825-193a14b69753] +description = "Feeding a correct letter removes underscores" + +[8e6bd521-bc9b-458f-9cce-f57f4140c173] +description = "Feeding a correct letter twice counts as a failure" + +[5e6971b7-5e5f-49c2-b85d-1dd6aeb53bd5] +description = "Guessing a repeated letter reveals all instances" + +[a6c69d92-01ef-4b81-b9d9-801131e79bbb] +description = "Getting all the letters right makes for a win" + +[2dc47994-b434-4a26-b70c-1eebeff77fe4] +description = "Winning on the last guess is still a win" + +[52801d56-6963-494b-a901-5736e46ddc12] +description = "Guessing after a lose is error" + +[29a874f3-a413-4e1b-9a60-6be018e70b60] +description = "Guessing after a win is error" 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..4153c3e9e1c 100644 --- a/exercises/practice/killer-sudoku-helper/.docs/instructions.md +++ b/exercises/practice/killer-sudoku-helper/.docs/instructions.md @@ -74,12 +74,12 @@ 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/ +[sudoku-rules]: https://en.wikipedia.org/wiki/Sudoku +[killer-guide]: https://en.wikipedia.org/wiki/Killer_sudoku [one-solution-img]: https://assets.exercism.org/images/exercises/killer-sudoku-helper/example1.png [four-solutions-img]: https://assets.exercism.org/images/exercises/killer-sudoku-helper/example2.png [not-possible-img]: https://assets.exercism.org/images/exercises/killer-sudoku-helper/example3.png -[clover-puzzle]: https://app.crackingthecryptic.com/sudoku/HqTBn3Pr6R +[clover-puzzle]: https://sudokupad.app/HqTBn3Pr6R [goodliffe-video]: https://youtu.be/c_NjEbFEeW0?t=1180 diff --git a/exercises/practice/kindergarten-garden/.meta/config.json b/exercises/practice/kindergarten-garden/.meta/config.json index 58767fd3908..30995a2fc22 100644 --- a/exercises/practice/kindergarten-garden/.meta/config.json +++ b/exercises/practice/kindergarten-garden/.meta/config.json @@ -28,5 +28,5 @@ }, "blurb": "Given a diagram, determine which plants each child in the kindergarten class is responsible for.", "source": "Exercise by the JumpstartLab team for students at The Turing School of Software and Design.", - "source_url": "https://turing.edu" + "source_url": "https://www.turing.edu/" } 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/line-up/.docs/instructions.md b/exercises/practice/line-up/.docs/instructions.md new file mode 100644 index 00000000000..9e686ecbffb --- /dev/null +++ b/exercises/practice/line-up/.docs/instructions.md @@ -0,0 +1,19 @@ +# Instructions + +Given a name and a number, your task is to produce a sentence using that name and that number as an [ordinal numeral][ordinal-numeral]. +Yaสปqลซb expects to use numbers from 1 up to 999. + +Rules: + +- Numbers ending in 1 (unless ending in 11) โ†’ `"st"` +- Numbers ending in 2 (unless ending in 12) โ†’ `"nd"` +- Numbers ending in 3 (unless ending in 13) โ†’ `"rd"` +- All other numbers โ†’ `"th"` + +Examples: + +- `"Mary", 1` โ†’ `"Mary, you are the 1st customer we serve today. Thank you!"` +- `"John", 12` โ†’ `"John, you are the 12th customer we serve today. Thank you!"` +- `"Dahir", 162` โ†’ `"Dahir, you are the 162nd customer we serve today. Thank you!"` + +[ordinal-numeral]: https://en.wikipedia.org/wiki/Ordinal_numeral diff --git a/exercises/practice/line-up/.docs/introduction.md b/exercises/practice/line-up/.docs/introduction.md new file mode 100644 index 00000000000..ea07268ae3b --- /dev/null +++ b/exercises/practice/line-up/.docs/introduction.md @@ -0,0 +1,7 @@ +# Introduction + +Your friend Yaสปqลซb works the counter at a deli in town, slicing, weighing, and wrapping orders for a line of hungry customers that gets longer every day. +Waiting customers are starting to lose track of who is next, so he wants numbered tickets they can use to track the order in which they arrive. + +To make the customers feel special, he does not want the ticket to have only a number on it. +They shall get a proper English sentence with their name and number on it. diff --git a/exercises/practice/line-up/.meta/config.json b/exercises/practice/line-up/.meta/config.json new file mode 100644 index 00000000000..c7417123792 --- /dev/null +++ b/exercises/practice/line-up/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "BNAndras" + ], + "files": { + "solution": [ + "line_up.py" + ], + "test": [ + "line_up_test.py" + ], + "example": [ + ".meta/example.py" + ] + }, + "blurb": "Help lining up customers at Yaสปqลซb's Deli.", + "source": "mk-mxp, based on previous work from Exercism contributors codedge and neenjaw", + "source_url": "https://forum.exercism.org/t/new-exercise-ordinal-numbers/19147" +} diff --git a/exercises/practice/line-up/.meta/example.py b/exercises/practice/line-up/.meta/example.py new file mode 100644 index 00000000000..a847e80c99c --- /dev/null +++ b/exercises/practice/line-up/.meta/example.py @@ -0,0 +1,19 @@ +def line_up(name, number): + suffix = get_suffix(number) + return f"{name}, you are the {number}{suffix} customer we serve today. Thank you!" + + +def get_suffix(number): + if 11 <= number % 100 <= 13: + return "th" + + mod_10 = number % 10 + + if mod_10 == 1: + return "st" + if mod_10 == 2: + return "nd" + if mod_10 == 3: + return "rd" + + return "th" diff --git a/exercises/practice/line-up/.meta/template.j2 b/exercises/practice/line-up/.meta/template.j2 new file mode 100644 index 00000000000..df7283398b5 --- /dev/null +++ b/exercises/practice/line-up/.meta/template.j2 @@ -0,0 +1,10 @@ +{%- import "generator_macros.j2" as macros with context -%} +{{ macros.canonical_ref() }} + +{{ macros.header(imports=['line_up']) }} + +class {{ exercise | camel_case }}Test(unittest.TestCase): + {% for case in cases -%} + def test_{{ case["description"] | to_snake }}(self): + self.assertEqual(line_up("{{ case["input"]["name"] }}", {{ case["input"]["number"] }}), "{{ case["expected"] }}") + {% endfor -%} diff --git a/exercises/practice/line-up/.meta/tests.toml b/exercises/practice/line-up/.meta/tests.toml new file mode 100644 index 00000000000..36fdf1d0cd3 --- /dev/null +++ b/exercises/practice/line-up/.meta/tests.toml @@ -0,0 +1,67 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[7760d1b8-4864-4db4-953b-0fa7c047dbc0] +description = "format smallest non-exceptional ordinal numeral 4" + +[e8b7c715-6baa-4f7b-8fb3-2fa48044ab7a] +description = "format greatest single digit non-exceptional ordinal numeral 9" + +[f370aae9-7ae7-4247-90ce-e8ff8c6934df] +description = "format non-exceptional ordinal numeral 5" + +[37f10dea-42a2-49de-bb92-0b690b677908] +description = "format non-exceptional ordinal numeral 6" + +[d8dfb9a2-3a1f-4fee-9dae-01af3600054e] +description = "format non-exceptional ordinal numeral 7" + +[505ec372-1803-42b1-9377-6934890fd055] +description = "format non-exceptional ordinal numeral 8" + +[8267072d-be1f-4f70-b34a-76b7557a47b9] +description = "format exceptional ordinal numeral 1" + +[4d8753cb-0364-4b29-84b8-4374a4fa2e3f] +description = "format exceptional ordinal numeral 2" + +[8d44c223-3a7e-4f48-a0ca-78e67bf98aa7] +description = "format exceptional ordinal numeral 3" + +[6c4f6c88-b306-4f40-bc78-97cdd583c21a] +description = "format smallest two digit non-exceptional ordinal numeral 10" + +[e257a43f-d2b1-457a-97df-25f0923fc62a] +description = "format non-exceptional ordinal numeral 11" + +[bb1db695-4d64-457f-81b8-4f5a2107e3f4] +description = "format non-exceptional ordinal numeral 12" + +[60a3187c-9403-4835-97de-4f10ebfd63e2] +description = "format non-exceptional ordinal numeral 13" + +[2bdcebc5-c029-4874-b6cc-e9bec80d603a] +description = "format exceptional ordinal numeral 21" + +[74ee2317-0295-49d2-baf0-d56bcefa14e3] +description = "format exceptional ordinal numeral 62" + +[b37c332d-7f68-40e3-8503-e43cbd67a0c4] +description = "format exceptional ordinal numeral 100" + +[0375f250-ce92-4195-9555-00e28ccc4d99] +description = "format exceptional ordinal numeral 101" + +[0d8a4974-9a8a-45a4-aca7-a9fb473c9836] +description = "format non-exceptional ordinal numeral 112" + +[06b62efe-199e-4ce7-970d-4bf73945713f] +description = "format exceptional ordinal numeral 123" diff --git a/exercises/practice/line-up/line_up.py b/exercises/practice/line-up/line_up.py new file mode 100644 index 00000000000..c6d20c4e0a7 --- /dev/null +++ b/exercises/practice/line-up/line_up.py @@ -0,0 +1,2 @@ +def line_up(name, number): + pass diff --git a/exercises/practice/line-up/line_up_test.py b/exercises/practice/line-up/line_up_test.py new file mode 100644 index 00000000000..1b593c4ddb6 --- /dev/null +++ b/exercises/practice/line-up/line_up_test.py @@ -0,0 +1,125 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/line-up/canonical-data.json +# File last updated on 2026-01-23 + +import unittest + +from line_up import ( + line_up, +) + + +class LineUpTest(unittest.TestCase): + def test_format_smallest_non_exceptional_ordinal_numeral_4(self): + self.assertEqual( + line_up("Gianna", 4), + "Gianna, you are the 4th customer we serve today. Thank you!", + ) + + def test_format_greatest_single_digit_non_exceptional_ordinal_numeral_9(self): + self.assertEqual( + line_up("Maarten", 9), + "Maarten, you are the 9th customer we serve today. Thank you!", + ) + + def test_format_non_exceptional_ordinal_numeral_5(self): + self.assertEqual( + line_up("Petronila", 5), + "Petronila, you are the 5th customer we serve today. Thank you!", + ) + + def test_format_non_exceptional_ordinal_numeral_6(self): + self.assertEqual( + line_up("Attakullakulla", 6), + "Attakullakulla, you are the 6th customer we serve today. Thank you!", + ) + + def test_format_non_exceptional_ordinal_numeral_7(self): + self.assertEqual( + line_up("Kate", 7), + "Kate, you are the 7th customer we serve today. Thank you!", + ) + + def test_format_non_exceptional_ordinal_numeral_8(self): + self.assertEqual( + line_up("Maximiliano", 8), + "Maximiliano, you are the 8th customer we serve today. Thank you!", + ) + + def test_format_exceptional_ordinal_numeral_1(self): + self.assertEqual( + line_up("Mary", 1), + "Mary, you are the 1st customer we serve today. Thank you!", + ) + + def test_format_exceptional_ordinal_numeral_2(self): + self.assertEqual( + line_up("Haruto", 2), + "Haruto, you are the 2nd customer we serve today. Thank you!", + ) + + def test_format_exceptional_ordinal_numeral_3(self): + self.assertEqual( + line_up("Henriette", 3), + "Henriette, you are the 3rd customer we serve today. Thank you!", + ) + + def test_format_smallest_two_digit_non_exceptional_ordinal_numeral_10(self): + self.assertEqual( + line_up("Alvarez", 10), + "Alvarez, you are the 10th customer we serve today. Thank you!", + ) + + def test_format_non_exceptional_ordinal_numeral_11(self): + self.assertEqual( + line_up("Jacqueline", 11), + "Jacqueline, you are the 11th customer we serve today. Thank you!", + ) + + def test_format_non_exceptional_ordinal_numeral_12(self): + self.assertEqual( + line_up("Juan", 12), + "Juan, you are the 12th customer we serve today. Thank you!", + ) + + def test_format_non_exceptional_ordinal_numeral_13(self): + self.assertEqual( + line_up("Patricia", 13), + "Patricia, you are the 13th customer we serve today. Thank you!", + ) + + def test_format_exceptional_ordinal_numeral_21(self): + self.assertEqual( + line_up("Washi", 21), + "Washi, you are the 21st customer we serve today. Thank you!", + ) + + def test_format_exceptional_ordinal_numeral_62(self): + self.assertEqual( + line_up("Nayra", 62), + "Nayra, you are the 62nd customer we serve today. Thank you!", + ) + + def test_format_exceptional_ordinal_numeral_100(self): + self.assertEqual( + line_up("John", 100), + "John, you are the 100th customer we serve today. Thank you!", + ) + + def test_format_exceptional_ordinal_numeral_101(self): + self.assertEqual( + line_up("Zeinab", 101), + "Zeinab, you are the 101st customer we serve today. Thank you!", + ) + + def test_format_non_exceptional_ordinal_numeral_112(self): + self.assertEqual( + line_up("Knud", 112), + "Knud, you are the 112th customer we serve today. Thank you!", + ) + + def test_format_exceptional_ordinal_numeral_123(self): + self.assertEqual( + line_up("Yma", 123), + "Yma, you are the 123rd customer we serve today. Thank you!", + ) 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/luhn/luhn_test.py b/exercises/practice/luhn/luhn_test.py index 58234eb7d55..7d26c9ae02f 100644 --- a/exercises/practice/luhn/luhn_test.py +++ b/exercises/practice/luhn/luhn_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/luhn/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2026-02-19 import unittest @@ -10,6 +10,7 @@ class LuhnTest(unittest.TestCase): + def test_single_digit_strings_can_not_be_valid(self): self.assertIs(Luhn("1").valid(), False) diff --git a/exercises/practice/markdown/.docs/instructions.md b/exercises/practice/markdown/.docs/instructions.md index 9b756d9917b..b3f3044c66f 100644 --- a/exercises/practice/markdown/.docs/instructions.md +++ b/exercises/practice/markdown/.docs/instructions.md @@ -10,4 +10,4 @@ Your challenge is to re-write this code to make it easier to read and maintain w It would be helpful if you made notes of what you did in your refactoring in comments so reviewers can see that, but it isn't strictly necessary. The most important thing is to make the code better! -[markdown]: https://guides.github.com/features/mastering-markdown/ +[markdown]: https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax diff --git a/exercises/practice/matrix/.meta/config.json b/exercises/practice/matrix/.meta/config.json index 544b7bb0d6b..71eb89cb711 100644 --- a/exercises/practice/matrix/.meta/config.json +++ b/exercises/practice/matrix/.meta/config.json @@ -29,5 +29,5 @@ }, "blurb": "Given a string representing a matrix of numbers, return the rows and columns of that matrix.", "source": "Exercise by the JumpstartLab team for students at The Turing School of Software and Design.", - "source_url": "https://turing.edu" + "source_url": "https://www.turing.edu/" } 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/perfect-numbers/.meta/tests.toml b/exercises/practice/perfect-numbers/.meta/tests.toml index 3232bb44e0f..81d484081c2 100644 --- a/exercises/practice/perfect-numbers/.meta/tests.toml +++ b/exercises/practice/perfect-numbers/.meta/tests.toml @@ -1,42 +1,52 @@ -# 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. [163e8e86-7bfd-4ee2-bd68-d083dc3381a3] -description = "Smallest perfect number is classified correctly" +description = "Perfect numbers -> Smallest perfect number is classified correctly" [169a7854-0431-4ae0-9815-c3b6d967436d] -description = "Medium perfect number is classified correctly" +description = "Perfect numbers -> Medium perfect number is classified correctly" [ee3627c4-7b36-4245-ba7c-8727d585f402] -description = "Large perfect number is classified correctly" +description = "Perfect numbers -> Large perfect number is classified correctly" [80ef7cf8-9ea8-49b9-8b2d-d9cb3db3ed7e] -description = "Smallest abundant number is classified correctly" +description = "Abundant numbers -> Smallest abundant number is classified correctly" [3e300e0d-1a12-4f11-8c48-d1027165ab60] -description = "Medium abundant number is classified correctly" +description = "Abundant numbers -> Medium abundant number is classified correctly" [ec7792e6-8786-449c-b005-ce6dd89a772b] -description = "Large abundant number is classified correctly" +description = "Abundant numbers -> Large abundant number is classified correctly" + +[05f15b93-849c-45e9-9c7d-1ea131ef7d10] +description = "Abundant numbers -> Perfect square abundant number is classified correctly" [e610fdc7-2b6e-43c3-a51c-b70fb37413ba] -description = "Smallest prime deficient number is classified correctly" +description = "Deficient numbers -> Smallest prime deficient number is classified correctly" [0beb7f66-753a-443f-8075-ad7fbd9018f3] -description = "Smallest non-prime deficient number is classified correctly" +description = "Deficient numbers -> Smallest non-prime deficient number is classified correctly" [1c802e45-b4c6-4962-93d7-1cad245821ef] -description = "Medium deficient number is classified correctly" +description = "Deficient numbers -> Medium deficient number is classified correctly" [47dd569f-9e5a-4a11-9a47-a4e91c8c28aa] -description = "Large deficient number is classified correctly" +description = "Deficient numbers -> Large deficient number is classified correctly" [a696dec8-6147-4d68-afad-d38de5476a56] -description = "Edge case (no factors other than itself) is classified correctly" +description = "Deficient numbers -> Edge case (no factors other than itself) is classified correctly" [72445cee-660c-4d75-8506-6c40089dc302] -description = "Zero is rejected (not a natural number)" +description = "Invalid inputs -> Zero is rejected (as it is not a positive integer)" [2d72ce2c-6802-49ac-8ece-c790ba3dae13] -description = "Negative integer is rejected (not a natural number)" +description = "Invalid inputs -> Negative integer is rejected (as it is not a positive integer)" diff --git a/exercises/practice/perfect-numbers/perfect_numbers_test.py b/exercises/practice/perfect-numbers/perfect_numbers_test.py index eef8661cef1..e9e55937e61 100644 --- a/exercises/practice/perfect-numbers/perfect_numbers_test.py +++ b/exercises/practice/perfect-numbers/perfect_numbers_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/perfect-numbers/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2026-02-25 import unittest @@ -30,6 +30,9 @@ def test_medium_abundant_number_is_classified_correctly(self): def test_large_abundant_number_is_classified_correctly(self): self.assertEqual(classify(33550335), "abundant") + def test_perfect_square_abundant_number_is_classified_correctly(self): + self.assertEqual(classify(196), "abundant") + class DeficientNumbersTest(unittest.TestCase): def test_smallest_prime_deficient_number_is_classified_correctly(self): 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/phone-number/.meta/config.json b/exercises/practice/phone-number/.meta/config.json index f2fe78bd3fa..d318dada52d 100644 --- a/exercises/practice/phone-number/.meta/config.json +++ b/exercises/practice/phone-number/.meta/config.json @@ -30,5 +30,5 @@ }, "blurb": "Clean up user-entered phone numbers so that they can be sent SMS messages.", "source": "Exercise by the JumpstartLab team for students at The Turing School of Software and Design.", - "source_url": "https://turing.edu" + "source_url": "https://www.turing.edu/" } diff --git a/exercises/practice/pig-latin/.meta/config.json b/exercises/practice/pig-latin/.meta/config.json index 6dd8c462e33..160f5e8113e 100644 --- a/exercises/practice/pig-latin/.meta/config.json +++ b/exercises/practice/pig-latin/.meta/config.json @@ -26,5 +26,5 @@ }, "blurb": "Implement a program that translates from English to Pig Latin.", "source": "The Pig Latin exercise at Test First Teaching by Ultrasaurus", - "source_url": "https://github.com/ultrasaurus/test-first-teaching/blob/master/learn_ruby/pig_latin/" + "source_url": "https://github.com/ultrasaurus/test-first-teaching/tree/master/learn_ruby/pig_latin" } 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..10c2f76419d 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 2026-02-19 import unittest @@ -10,6 +10,7 @@ class PigLatinTest(unittest.TestCase): + def test_word_beginning_with_a(self): self.assertEqual(translate("apple"), "appleay") @@ -40,6 +41,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/pov/pov_test.py b/exercises/practice/pov/pov_test.py index 2436ebc2db9..0399300eac0 100644 --- a/exercises/practice/pov/pov_test.py +++ b/exercises/practice/pov/pov_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/pov/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2026-02-19 import unittest @@ -10,6 +10,7 @@ class PovTest(unittest.TestCase): + def test_results_in_the_same_tree_if_the_input_tree_is_a_singleton(self): tree = Tree("x") expected = Tree("x") diff --git a/exercises/practice/prime-factors/prime_factors_test.py b/exercises/practice/prime-factors/prime_factors_test.py index 4f6865036e5..a1d5d4d2fbb 100644 --- a/exercises/practice/prime-factors/prime_factors_test.py +++ b/exercises/practice/prime-factors/prime_factors_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/prime-factors/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2026-02-19 import unittest @@ -10,6 +10,7 @@ class PrimeFactorsTest(unittest.TestCase): + def test_no_factors(self): self.assertEqual(factors(1), []) 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/rail-fence-cipher/rail_fence_cipher_test.py b/exercises/practice/rail-fence-cipher/rail_fence_cipher_test.py index f82066ca274..4cbca167cb9 100644 --- a/exercises/practice/rail-fence-cipher/rail_fence_cipher_test.py +++ b/exercises/practice/rail-fence-cipher/rail_fence_cipher_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/rail-fence-cipher/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2026-02-19 import unittest @@ -11,6 +11,7 @@ class RailFenceCipherTest(unittest.TestCase): + def test_encode_with_two_rails(self): self.assertMultiLineEqual(encode("XOXOXOXOXOXOXOXOXO", 2), "XXXXXXXXXOOOOOOOOO") 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/relative-distance/.docs/instructions.append.md b/exercises/practice/relative-distance/.docs/instructions.append.md new file mode 100644 index 00000000000..241ff7d310e --- /dev/null +++ b/exercises/practice/relative-distance/.docs/instructions.append.md @@ -0,0 +1,28 @@ +# Instructions append + +## Class-based solution + +The tests for this exercise expect your solution to be implemented as a `RelativeDistance` class in Python. +Your `RelativeDistance` class should be initialized using `family_tree`, a dictionary where the keys are individuals and the values are lists of that individual's children. +You will also need to implement a `degree_of_separation` method which will return the degree of separation between `person_a` and `person_b` who are individuals in the family tree. + +If you are unfamiliar with classes in Python, here is a brief overview of how to implement the `RelativeDistance` class: + +A class is a blueprint for creating objects, bundling attributes (data) and methods (functionality) together. +In this exercise, you are given stubbed implementations for the `__init__` special method used to create an instance of the `RelativeDistance` class as well as the `degree_of_separation` method. +To access the `family_tree` data from within the `degree_of_separation` method, you will need to first assign it within the `__init__` method to an appropriate attribute on `self`, which represents the current instance of the `RelativeDistance` class. +Then you can add your logic to the `degree_of_separation` method to calculate the degree of separation between `person_a` and `person_b`. + +## Exception messages + +Sometimes it is necessary to [raise an exception](https://docs.python.org/3/tutorial/errors.html#raising-exceptions). +When you do this, you should always include a **meaningful error message** to indicate what the source of the error is. +This makes your code more readable and helps significantly with debugging. +For situations where you know that the error source will be a certain type, you can choose to raise one of the [built in error types](https://docs.python.org/3/library/exceptions.html#base-classes), but should still include a meaningful message. + +This particular exercise requires that you use the [raise statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement) to "throw" multiple `ValueError`s. +In the first scenario, you will need to raise a `ValueError` when either one or both of the people passed to the `RelativeDistance.degree_of_separation` method are not present in the family tree. +If both people are present in the family tree, you will need to raise a `ValueError` when there is no valid connection between them as defined by the rules. +The tests will only pass if you both `raise` the expected `exception` type and include the expected message with it. + +Please check the tests and their expected results carefully. diff --git a/exercises/practice/relative-distance/.docs/instructions.md b/exercises/practice/relative-distance/.docs/instructions.md new file mode 100644 index 00000000000..64ca4e43744 --- /dev/null +++ b/exercises/practice/relative-distance/.docs/instructions.md @@ -0,0 +1,39 @@ +# Instructions + +Your task is to determine the degree of separation between two individuals in a family tree. +This is similar to the pop culture idea that every Hollywood actor is [within six degrees of Kevin Bacon][six-bacons]. + +- You will be given an input, with all parent names and their children. +- Each name is unique, a child _can_ have one or two parents. +- The degree of separation is defined as the shortest number of connections from one person to another. +- If two individuals are not connected, return a value that represents "no known relationship." + Please see the test cases for the actual implementation. + +## Example + +Given the following family tree: + +```text + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Helena โ”‚ โ”‚ Erdล‘s โ”œโ”€โ”€โ”€โ”€โ”€โ”ค Shusaku โ”‚ + โ””โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”ฌโ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”Œโ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ” +โ”‚ Isla โ”œโ”€โ”€โ”€โ”€โ”€โ”ค Tariq โ”‚ โ”‚ Kevin โ”‚ +โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ +โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ” +โ”‚ Uma โ”‚ โ”‚ Morphy โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +The degree of separation between Tariq and Uma is 2 (Tariq โ†’ Isla โ†’ Uma). +There's no known relationship between Isla and Kevin, as there is no connection in the given data. +The degree of separation between Uma and Isla is 1. + +~~~~exercism/note +Isla and Tariq are siblings and have a separation of 1. +Similarly, this implementation would report a separation of 2 from you to your father's brother. +~~~~ + +[six-bacons]: https://en.wikipedia.org/wiki/Six_Degrees_of_Kevin_Bacon diff --git a/exercises/practice/relative-distance/.docs/introduction.md b/exercises/practice/relative-distance/.docs/introduction.md new file mode 100644 index 00000000000..34073b40ac6 --- /dev/null +++ b/exercises/practice/relative-distance/.docs/introduction.md @@ -0,0 +1,12 @@ +# Introduction + +You've been hired to develop **Noble Knots**, the hottest new dating app for nobility! +With centuries of royal intermarriage, things have gottenโ€ฆ _complicated_. +To avoid any _oops-we're-twins_ situations, your job is to build a system that checks how closely two people are related. + +Noble Knots is inspired by Iceland's "[Islendinga-App][islendiga-app]," which is backed up by a database that traces all known family connections between Icelanders from the time of the settlement of Iceland. +Your algorithm will determine the **degree of separation** between two individuals in the royal family tree. + +Will your app help crown a perfect match? + +[islendiga-app]: https://web.archive.org/web/20250816223614/http://www.islendingaapp.is/information-in-english/ diff --git a/exercises/practice/relative-distance/.meta/additional_tests.json b/exercises/practice/relative-distance/.meta/additional_tests.json new file mode 100644 index 00000000000..0b42263ce85 --- /dev/null +++ b/exercises/practice/relative-distance/.meta/additional_tests.json @@ -0,0 +1,41 @@ +{ + "cases": [ + { + "description": "person A not in tree", + "property": "degreeOfSeparation", + "input": { + "family_tree": { + "Priya": ["Rami"] + }, + "person_a": "Kaito", + "person_b": "Priya" + }, + "expected": {"error": "Person A not in family tree."} + }, + { + "description": "person B not in tree", + "property": "degreeOfSeparation", + "input": { + "family_tree": { + "Priya": ["Rami"] + }, + "person_a": "Priya", + "person_b": "Kaito" + }, + "expected": {"error": "Person B not in family tree."} + }, + { + "description": "no connection between individuals", + "property": "degreeOfSeparation", + "input": { + "family_tree": { + "Priya": ["Rami"], + "Kaito": ["Elif"] + }, + "person_a": "Priya", + "person_b": "Kaito" + }, + "expected": {"error": "No connection between person A and person B."} + } + ] +} diff --git a/exercises/practice/relative-distance/.meta/config.json b/exercises/practice/relative-distance/.meta/config.json new file mode 100644 index 00000000000..97abf1e0337 --- /dev/null +++ b/exercises/practice/relative-distance/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "BNAndras" + ], + "files": { + "solution": [ + "relative_distance.py" + ], + "test": [ + "relative_distance_test.py" + ], + "example": [ + ".meta/example.py" + ] + }, + "blurb": "Given a family tree, calculate the degree of separation.", + "source": "vaeng", + "source_url": "https://github.com/exercism/problem-specifications/pull/2537" +} diff --git a/exercises/practice/relative-distance/.meta/example.py b/exercises/practice/relative-distance/.meta/example.py new file mode 100644 index 00000000000..2de98263b71 --- /dev/null +++ b/exercises/practice/relative-distance/.meta/example.py @@ -0,0 +1,36 @@ +import collections +import itertools + +class RelativeDistance: + def __init__(self, family_tree): + self.neighbors = collections.defaultdict(set) + for parent, children in family_tree.items(): + for child in children: + self.neighbors[parent].add(child) + self.neighbors[child].add(parent) + + for child1, child2 in itertools.combinations(children, 2): + self.neighbors[child1].add(child2) + self.neighbors[child2].add(child1) + + def degree_of_separation(self, person_a, person_b): + if person_a not in self.neighbors: + raise ValueError("Person A not in family tree.") + if person_b not in self.neighbors: + raise ValueError("Person B not in family tree.") + + queue = collections.deque([(person_a, 0)]) + visited = {person_a} + + while queue: + current, degree = queue.popleft() + + if current == person_b: + return degree + + for neighbor in self.neighbors.get(current, []): + if neighbor not in visited: + visited.add(neighbor) + queue.append((neighbor, degree + 1)) + + raise ValueError("No connection between person A and person B.") diff --git a/exercises/practice/relative-distance/.meta/template.j2 b/exercises/practice/relative-distance/.meta/template.j2 new file mode 100644 index 00000000000..99d8ed42e7e --- /dev/null +++ b/exercises/practice/relative-distance/.meta/template.j2 @@ -0,0 +1,44 @@ +{%- import "generator_macros.j2" as macros with context -%} +{{ macros.canonical_ref() }} + +{{ macros.header(imports=['RelativeDistance']) }} + +class {{ exercise | camel_case }}Test(unittest.TestCase): + {% for case in cases -%} + def test_{{ case["description"] | to_snake }}(self): + family_tree = { + {%- for person, relatives in case["input"]["familyTree"].items() %} + "{{ person }}": {{ relatives }}, + {%- endfor %} + } + {%- if case["expected"] is none %} + with self.assertRaises(ValueError): + RelativeDistance(family_tree).degree_of_separation("{{ case["input"]["personA"] }}", "{{ case["input"]["personB"] }}") + {%- else %} + self.assertEqual( + RelativeDistance(family_tree).degree_of_separation("{{ case["input"]["personA"] }}", "{{ case["input"]["personB"] }}"), + {{ case["expected"] }} + ) + {%- endif %} + {% endfor -%} + + # Additional track-specific tests + {% for case in additional_cases -%} + def test_{{ case["description"] | to_snake }}(self): + family_tree = { + {%- for person, relatives in case["input"]["family_tree"].items() %} + "{{ person }}": {{ relatives }}, + {%- endfor %} + } + {%- if case["expected"]["error"] %} + with self.assertRaises(ValueError) as err: + RelativeDistance(family_tree).degree_of_separation("{{ case["input"]["person_a"] }}", "{{ case["input"]["person_b"] }}") + self.assertEqual(type(err.exception), ValueError) + self.assertEqual(err.exception.args[0], "{{ case["expected"]["error"] }}") + {%- else %} + self.assertEqual( + RelativeDistance(family_tree).degree_of_separation("{{ case["input"]["person_a"] }}", "{{ case["input"]["person_b"] }}"), + {{ case["expected"] }} + ) + {%- endif %} + {% endfor -%} diff --git a/exercises/practice/relative-distance/.meta/tests.toml b/exercises/practice/relative-distance/.meta/tests.toml new file mode 100644 index 00000000000..25560343fa2 --- /dev/null +++ b/exercises/practice/relative-distance/.meta/tests.toml @@ -0,0 +1,33 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[4a1ded74-5d32-47fb-8ae5-321f51d06b5b] +description = "Direct parent-child relation" + +[30d17269-83e9-4f82-a0d7-8ef9656d8dce] +description = "Sibling relationship" + +[8dffa27d-a8ab-496d-80b3-2f21c77648b5] +description = "Two degrees of separation, grandchild" + +[34e56ec1-d528-4a42-908e-020a4606ee60] +description = "Unrelated individuals" +comment = "skipped in favor of test-specific tests" +include = false + +[93ffe989-bad2-48c4-878f-3acb1ce2611b] +description = "Complex graph, cousins" + +[2cc2e76b-013a-433c-9486-1dbe29bf06e5] +description = "Complex graph, no shortcut, far removed nephew" + +[46c9fbcb-e464-455f-a718-049ea3c7400a] +description = "Complex graph, some shortcuts, cross-down and cross-up, cousins several times removed, with unrelated family tree" diff --git a/exercises/practice/relative-distance/relative_distance.py b/exercises/practice/relative-distance/relative_distance.py new file mode 100644 index 00000000000..21f09f2f74f --- /dev/null +++ b/exercises/practice/relative-distance/relative_distance.py @@ -0,0 +1,6 @@ +class RelativeDistance: + def __init__(self, family_tree): + pass + + def degree_of_separation(self, person_a, person_b): + pass diff --git a/exercises/practice/relative-distance/relative_distance_test.py b/exercises/practice/relative-distance/relative_distance_test.py new file mode 100644 index 00000000000..bedc24f873f --- /dev/null +++ b/exercises/practice/relative-distance/relative_distance_test.py @@ -0,0 +1,246 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/relative-distance/canonical-data.json +# File last updated on 2026-01-30 + +import unittest + +from relative_distance import ( + RelativeDistance, +) + + +class RelativeDistanceTest(unittest.TestCase): + def test_direct_parent_child_relation(self): + family_tree = { + "Vera": ["Tomoko"], + "Tomoko": ["Aditi"], + } + self.assertEqual( + RelativeDistance(family_tree).degree_of_separation("Vera", "Tomoko"), 1 + ) + + def test_sibling_relationship(self): + family_tree = { + "Dalia": ["Olga", "Yassin"], + } + self.assertEqual( + RelativeDistance(family_tree).degree_of_separation("Olga", "Yassin"), 1 + ) + + def test_two_degrees_of_separation_grandchild(self): + family_tree = { + "Khadija": ["Mateo"], + "Mateo": ["Rami"], + } + self.assertEqual( + RelativeDistance(family_tree).degree_of_separation("Khadija", "Rami"), 2 + ) + + def test_complex_graph_cousins(self): + family_tree = { + "Aiko": ["Bao", "Carlos"], + "Bao": ["Dalia", "Elias"], + "Carlos": ["Fatima", "Gustavo"], + "Dalia": ["Hassan", "Isla"], + "Elias": ["Javier"], + "Fatima": ["Khadija", "Liam"], + "Gustavo": ["Mina"], + "Hassan": ["Noah", "Olga"], + "Isla": ["Pedro"], + "Javier": ["Quynh", "Ravi"], + "Khadija": ["Sofia"], + "Liam": ["Tariq", "Uma"], + "Mina": ["Viktor", "Wang"], + "Noah": ["Xiomara"], + "Olga": ["Yuki"], + "Pedro": ["Zane", "Aditi"], + "Quynh": ["Boris"], + "Ravi": ["Celine"], + "Sofia": ["Diego", "Elif"], + "Tariq": ["Farah"], + "Uma": ["Giorgio"], + "Viktor": ["Hana", "Ian"], + "Wang": ["Jing"], + "Xiomara": ["Kaito"], + "Yuki": ["Leila"], + "Zane": ["Mateo"], + "Aditi": ["Nia"], + "Boris": ["Oscar"], + "Celine": ["Priya"], + "Diego": ["Qi"], + "Elif": ["Rami"], + "Farah": ["Sven"], + "Giorgio": ["Tomoko"], + "Hana": ["Umar"], + "Ian": ["Vera"], + "Jing": ["Wyatt"], + "Kaito": ["Xia"], + "Leila": ["Yassin"], + "Mateo": ["Zara"], + "Nia": ["Antonio"], + "Oscar": ["Bianca"], + "Priya": ["Cai"], + "Qi": ["Dimitri"], + "Rami": ["Ewa"], + "Sven": ["Fabio"], + "Tomoko": ["Gabriela"], + "Umar": ["Helena"], + "Vera": ["Igor"], + "Wyatt": ["Jun"], + "Xia": ["Kim"], + "Yassin": ["Lucia"], + "Zara": ["Mohammed"], + } + self.assertEqual( + RelativeDistance(family_tree).degree_of_separation("Dimitri", "Fabio"), 9 + ) + + def test_complex_graph_no_shortcut_far_removed_nephew(self): + family_tree = { + "Aiko": ["Bao", "Carlos"], + "Bao": ["Dalia", "Elias"], + "Carlos": ["Fatima", "Gustavo"], + "Dalia": ["Hassan", "Isla"], + "Elias": ["Javier"], + "Fatima": ["Khadija", "Liam"], + "Gustavo": ["Mina"], + "Hassan": ["Noah", "Olga"], + "Isla": ["Pedro"], + "Javier": ["Quynh", "Ravi"], + "Khadija": ["Sofia"], + "Liam": ["Tariq", "Uma"], + "Mina": ["Viktor", "Wang"], + "Noah": ["Xiomara"], + "Olga": ["Yuki"], + "Pedro": ["Zane", "Aditi"], + "Quynh": ["Boris"], + "Ravi": ["Celine"], + "Sofia": ["Diego", "Elif"], + "Tariq": ["Farah"], + "Uma": ["Giorgio"], + "Viktor": ["Hana", "Ian"], + "Wang": ["Jing"], + "Xiomara": ["Kaito"], + "Yuki": ["Leila"], + "Zane": ["Mateo"], + "Aditi": ["Nia"], + "Boris": ["Oscar"], + "Celine": ["Priya"], + "Diego": ["Qi"], + "Elif": ["Rami"], + "Farah": ["Sven"], + "Giorgio": ["Tomoko"], + "Hana": ["Umar"], + "Ian": ["Vera"], + "Jing": ["Wyatt"], + "Kaito": ["Xia"], + "Leila": ["Yassin"], + "Mateo": ["Zara"], + "Nia": ["Antonio"], + "Oscar": ["Bianca"], + "Priya": ["Cai"], + "Qi": ["Dimitri"], + "Rami": ["Ewa"], + "Sven": ["Fabio"], + "Tomoko": ["Gabriela"], + "Umar": ["Helena"], + "Vera": ["Igor"], + "Wyatt": ["Jun"], + "Xia": ["Kim"], + "Yassin": ["Lucia"], + "Zara": ["Mohammed"], + } + self.assertEqual( + RelativeDistance(family_tree).degree_of_separation("Lucia", "Jun"), 14 + ) + + def test_complex_graph_some_shortcuts_cross_down_and_cross_up_cousins_several_times_removed_with_unrelated_family_tree( + self, + ): + family_tree = { + "Aiko": ["Bao", "Carlos"], + "Bao": ["Dalia"], + "Carlos": ["Fatima", "Gustavo"], + "Dalia": ["Hassan", "Isla"], + "Fatima": ["Khadija", "Liam"], + "Gustavo": ["Mina"], + "Hassan": ["Noah", "Olga"], + "Isla": ["Pedro"], + "Javier": ["Quynh", "Ravi"], + "Khadija": ["Sofia"], + "Liam": ["Tariq", "Uma"], + "Mina": ["Viktor", "Wang"], + "Noah": ["Xiomara"], + "Olga": ["Yuki"], + "Pedro": ["Zane", "Aditi"], + "Quynh": ["Boris"], + "Ravi": ["Celine"], + "Sofia": ["Diego", "Elif"], + "Tariq": ["Farah"], + "Uma": ["Giorgio"], + "Viktor": ["Hana", "Ian"], + "Wang": ["Jing"], + "Xiomara": ["Kaito"], + "Yuki": ["Leila"], + "Zane": ["Mateo"], + "Aditi": ["Nia"], + "Boris": ["Oscar"], + "Celine": ["Priya"], + "Diego": ["Qi"], + "Elif": ["Rami"], + "Farah": ["Sven"], + "Giorgio": ["Tomoko"], + "Hana": ["Umar"], + "Ian": ["Vera"], + "Jing": ["Wyatt"], + "Kaito": ["Xia"], + "Leila": ["Yassin"], + "Mateo": ["Zara"], + "Nia": ["Antonio"], + "Oscar": ["Bianca"], + "Priya": ["Cai"], + "Qi": ["Dimitri"], + "Rami": ["Ewa"], + "Sven": ["Fabio"], + "Tomoko": ["Gabriela"], + "Umar": ["Helena"], + "Vera": ["Igor"], + "Wyatt": ["Jun"], + "Xia": ["Kim"], + "Yassin": ["Lucia"], + "Zara": ["Mohammed"], + } + self.assertEqual( + RelativeDistance(family_tree).degree_of_separation("Wyatt", "Xia"), 12 + ) + + # Additional track-specific tests + def test_person_a_not_in_tree(self): + family_tree = { + "Priya": ["Rami"], + } + with self.assertRaises(ValueError) as err: + RelativeDistance(family_tree).degree_of_separation("Kaito", "Priya") + self.assertEqual(type(err.exception), ValueError) + self.assertEqual(err.exception.args[0], "Person A not in family tree.") + + def test_person_b_not_in_tree(self): + family_tree = { + "Priya": ["Rami"], + } + with self.assertRaises(ValueError) as err: + RelativeDistance(family_tree).degree_of_separation("Priya", "Kaito") + self.assertEqual(type(err.exception), ValueError) + self.assertEqual(err.exception.args[0], "Person B not in family tree.") + + def test_no_connection_between_individuals(self): + family_tree = { + "Priya": ["Rami"], + "Kaito": ["Elif"], + } + with self.assertRaises(ValueError) as err: + RelativeDistance(family_tree).degree_of_separation("Priya", "Kaito") + self.assertEqual(type(err.exception), ValueError) + self.assertEqual( + err.exception.args[0], "No connection between person A and person B." + ) 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/resistor-color-expert/resistor_color_expert_test.py b/exercises/practice/resistor-color-expert/resistor_color_expert_test.py index 2ba4b877d9d..47e7fc63440 100644 --- a/exercises/practice/resistor-color-expert/resistor_color_expert_test.py +++ b/exercises/practice/resistor-color-expert/resistor_color_expert_test.py @@ -49,3 +49,13 @@ def test_brown_red_orange_green_and_blue(self): self.assertEqual( resistor_label(["brown", "red", "orange", "green", "blue"]), "12.3 megaohms ยฑ0.25%" ) + + def test_brown_black_brown_yellow_and_violet(self): + self.assertEqual( + resistor_label(["brown", "black", "brown", "yellow", "violet"]), "1.01 megaohms ยฑ0.1%" + ) + + def test_brown_black_red_and_red(self): + self.assertEqual( + resistor_label(["brown", "black", "red", "red"]), "1 kiloohms ยฑ2%" + ) diff --git a/exercises/practice/rest-api/.docs/instructions.md b/exercises/practice/rest-api/.docs/instructions.md index af223ba4b4d..e889b1bf207 100644 --- a/exercises/practice/rest-api/.docs/instructions.md +++ b/exercises/practice/rest-api/.docs/instructions.md @@ -43,6 +43,6 @@ Your task is to implement a simple [RESTful API][restful-wikipedia] that receive [restful-wikipedia]: https://en.wikipedia.org/wiki/Representational_state_transfer [iou]: https://en.wikipedia.org/wiki/IOU -[github-rest]: https://developer.github.com/v3/ +[github-rest]: https://docs.github.com/en/rest [reddit-rest]: https://web.archive.org/web/20231202231149/https://www.reddit.com/dev/api/ [restfulapi]: https://restfulapi.net/ 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/reverse-string/.meta/config.json b/exercises/practice/reverse-string/.meta/config.json index bbc16e8661b..ef54d455e21 100644 --- a/exercises/practice/reverse-string/.meta/config.json +++ b/exercises/practice/reverse-string/.meta/config.json @@ -24,5 +24,5 @@ }, "blurb": "Reverse a given string.", "source": "Introductory challenge to reverse an input string", - "source_url": "https://medium.freecodecamp.org/how-to-reverse-a-string-in-javascript-in-3-different-ways-75e4763c68cb" + "source_url": "https://www.freecodecamp.org/news/how-to-reverse-a-string-in-javascript-in-3-different-ways-75e4763c68cb" } diff --git a/exercises/practice/rna-transcription/.approaches/config.json b/exercises/practice/rna-transcription/.approaches/config.json index 9ab41145480..7ec0363a3f6 100644 --- a/exercises/practice/rna-transcription/.approaches/config.json +++ b/exercises/practice/rna-transcription/.approaches/config.json @@ -9,14 +9,16 @@ "slug": "translate-maketrans", "title": "translate maketrans", "blurb": "Use translate with maketrans to return the value.", - "authors": ["bobahop"] + "authors": ["bobahop"], + "contributors": ["yrahcaz7"] }, { "uuid": "fbc6be87-dec4-4c4b-84cf-fcc1ed2d6d41", "slug": "dictionary-join", "title": "dictionary join", "blurb": "Use a dictionary look-up with join to return the value.", - "authors": ["bobahop"] + "authors": ["bobahop"], + "contributors": ["yrahcaz7"] } ] } diff --git a/exercises/practice/rna-transcription/.approaches/dictionary-join/content.md b/exercises/practice/rna-transcription/.approaches/dictionary-join/content.md index f3ec1f755fb..ead5254ad9c 100644 --- a/exercises/practice/rna-transcription/.approaches/dictionary-join/content.md +++ b/exercises/practice/rna-transcription/.approaches/dictionary-join/content.md @@ -1,12 +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) ``` This approach starts by defining a [dictionary][dictionaries] to map the DNA values to RNA values. @@ -16,15 +15,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 over each code point 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..032532946e2 100644 --- a/exercises/practice/rna-transcription/.approaches/introduction.md +++ b/exercises/practice/rna-transcription/.approaches/introduction.md @@ -7,18 +7,17 @@ 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): return dna_strand.translate(LOOKUP) - ``` For more information, check the [`translate()` with `maketrans()` approach][approach-translate-maketrans]. @@ -26,20 +25,25 @@ 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) ``` For more information, check the [dictionary look-up with `join()` approach][approach-dictionary-join]. ## 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. + + +~~~~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. +~~~~ -[ASCII]: https://www.asciitable.com/ +[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..374cadd65e9 100644 --- a/exercises/practice/rna-transcription/.approaches/translate-maketrans/content.md +++ b/exercises/practice/rna-transcription/.approaches/translate-maketrans/content.md @@ -1,12 +1,11 @@ # `translate()` with `maketrans()` ```python -LOOKUP = str.maketrans("GCTA", "CGAU") +LOOKUP = str.maketrans('GCTA', 'CGAU') def to_rna(dna_strand): return dna_strand.translate(LOOKUP) - ``` This approach starts by defining a [dictionary][dictionaries] (also called a translation table in this context) by calling the [`maketrans()`][maketrans] method. @@ -15,20 +14,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-strings]. + +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/rotational-cipher/.approaches/alphabet/content.md b/exercises/practice/rotational-cipher/.approaches/alphabet/content.md index a8fa1cd6611..7897b74eb5a 100644 --- a/exercises/practice/rotational-cipher/.approaches/alphabet/content.md +++ b/exercises/practice/rotational-cipher/.approaches/alphabet/content.md @@ -1,16 +1,16 @@ # Alphabet ```python -AlPHABET = "abcdefghijklmnopqrstuvwxyz" +ALPHABET = "abcdefghijklmnopqrstuvwxyz" def rotate(text, key): result = "" for letter in text: if letter.isalpha(): if letter.isupper(): - result += AlPHABET[(AlPHABET.index(letter.lower()) + key) % 26].upper() + result += ALPHABET[(ALPHABET.index(letter.lower()) + key) % 26].upper() else: - result += AlPHABET[(AlPHABET.index(letter) + key) % 26] + result += ALPHABET[(ALPHABET.index(letter) + key) % 26] else: result += letter return result @@ -22,9 +22,9 @@ The function `rotate()` is then declared, and a variable `result` is defined as The text argument is then iterated over via a [`for loop`][for-loop]. Each element is checked to make sure it is a letter, and subsequently checked if it is uppercase or lowercase. Uppercase letters are converted to lowercase. -Then the index of each letter is found in the `AlPHABET` constant. +Then the index of each letter is found in the `ALPHABET` constant. The numeric key value is added to the letter index and [modulo (`%`)][modulo] 26 is used on the result. -Finally, the new number is used as an index into the `AlPHABET` constant, and the resulting letter is converted back to uppercase. +Finally, the new number is used as an index into the `ALPHABET` constant, and the resulting letter is converted back to uppercase. Lowercase letters follow the same process without the conversion steps. @@ -36,7 +36,7 @@ If only English letters are needed, the constant [`string.ascii_lowercase`][asci ```python from string import ascii_lowercase -AlPHABET = ascii_lowercase +ALPHABET = ascii_lowercase ``` [ascii_lowercase]: https://docs.python.org/3/library/string.html#string.ascii_letters diff --git a/exercises/practice/rotational-cipher/.approaches/alphabet/snippet.txt b/exercises/practice/rotational-cipher/.approaches/alphabet/snippet.txt index ade372000b6..292f3b28f18 100644 --- a/exercises/practice/rotational-cipher/.approaches/alphabet/snippet.txt +++ b/exercises/practice/rotational-cipher/.approaches/alphabet/snippet.txt @@ -1,8 +1,8 @@ for letter in text: if letter.isalpha(): if letter.isupper(): - result += AlPHABET[(AlPHABET.index(letter.lower()) + key) % 26].upper() + result += ALPHABET[(ALPHABET.index(letter.lower()) + key) % 26].upper() else: - result += AlPHABET[(AlPHABET.index(letter) + key) % 26] + result += ALPHABET[(ALPHABET.index(letter) + key) % 26] else: result += letter \ No newline at end of file diff --git a/exercises/practice/rotational-cipher/.approaches/config.json b/exercises/practice/rotational-cipher/.approaches/config.json index 5cf51697a64..f58b5dff27b 100644 --- a/exercises/practice/rotational-cipher/.approaches/config.json +++ b/exercises/practice/rotational-cipher/.approaches/config.json @@ -1,7 +1,7 @@ { "introduction": { "authors": ["meatball133", "bethanyg"], - "contributors": [] + "contributors": ["yrahcaz7"] }, "approaches": [ { @@ -16,21 +16,24 @@ "slug": "alphabet", "title": "Alphabet", "blurb": "Using the alphabet to rotate the alphabet", - "authors": ["meatball133", "bethanyg"] + "authors": ["meatball133", "bethanyg"], + "contributors": ["yrahcaz7"] }, { "uuid": "e539d1a5-f497-402b-a232-7e889f4323c1", "slug": "str-translate", "title": "Str Translate", "blurb": "Using str.translate to rotate the alphabet", - "authors": ["meatball133", "bethanyg"] + "authors": ["meatball133", "bethanyg"], + "contributors": ["yrahcaz7"] }, { "uuid": "0c74890e-d96e-47a2-a8bf-93c45dd67f94", "slug": "recursion", "title": "Recursion", "blurb": "Using Recursion to rotate the alphabet", - "authors": ["meatball133", "bethanyg"] + "authors": ["meatball133", "bethanyg"], + "contributors": ["yrahcaz7"] } ] } diff --git a/exercises/practice/rotational-cipher/.approaches/introduction.md b/exercises/practice/rotational-cipher/.approaches/introduction.md index 047d9950eca..182193236ae 100644 --- a/exercises/practice/rotational-cipher/.approaches/introduction.md +++ b/exercises/practice/rotational-cipher/.approaches/introduction.md @@ -54,16 +54,16 @@ Here, if we want to use the Scandinavian letter: **รฅ**, we can simply insert it ```python # This only uses English characters -AlPHABET = "abcdefghijklmnopqrstuvwxyz" +ALPHABET = "abcdefghijklmnopqrstuvwxyz" def rotate(text, key): result = "" for letter in text: if letter.isalpha(): if letter.isupper(): - result += AlPHABET[(AlPHABET.index(letter.lower()) + key) % 26].upper() + result += ALPHABET[(ALPHABET.index(letter.lower()) + key) % 26].upper() else: - result += AlPHABET[(AlPHABET.index(letter) + key) % 26] + result += ALPHABET[(ALPHABET.index(letter) + key) % 26] else: result += letter return result @@ -82,11 +82,11 @@ The benefit of this approach is that it has no visible loop, making the code mor ```python -AlPHABET = "abcdefghijklmnopqrstuvwxyz" +ALPHABET = "abcdefghijklmnopqrstuvwxyz" def rotate(text, key): - translator = AlPHABET[key:] + AlPHABET[:key] - return text.translate(str.maketrans(AlPHABET + AlPHABET.upper(), translator + translator.upper())) + translator = ALPHABET[key:] + ALPHABET[:key] + return text.translate(str.maketrans(ALPHABET + ALPHABET.upper(), translator + translator.upper())) ``` For more information, check out the [Str translate approach][approach-str-translate]. @@ -106,7 +106,7 @@ Calculate your base case carefully to avoid errors. ```python -AlPHABET = "abcdefghijklmnopqrstuvwxyz" +ALPHABET = "abcdefghijklmnopqrstuvwxyz" def rotate(text, key): if text == "": @@ -114,9 +114,9 @@ def rotate(text, key): first_letter, rest = text[0], text[1:] if first_letter.isalpha(): if first_letter.isupper(): - return AlPHABET[(AlPHABET.index(first_letter.lower()) + key) % 26].upper() + rotate(rest, key) + return ALPHABET[(ALPHABET.index(first_letter.lower()) + key) % 26].upper() + rotate(rest, key) else: - return AlPHABET[(AlPHABET.index(first_letter) + key) % 26] + rotate(rest, key) + return ALPHABET[(ALPHABET.index(first_letter) + key) % 26] + rotate(rest, key) else: return first_letter + rotate(rest, key) ``` diff --git a/exercises/practice/rotational-cipher/.approaches/recursion/content.md b/exercises/practice/rotational-cipher/.approaches/recursion/content.md index 48ff3facae1..7211b5ae6eb 100644 --- a/exercises/practice/rotational-cipher/.approaches/recursion/content.md +++ b/exercises/practice/rotational-cipher/.approaches/recursion/content.md @@ -1,7 +1,7 @@ # Recursion ```python -AlPHABET = "abcdefghijklmnopqrstuvwxyz" +ALPHABET = "abcdefghijklmnopqrstuvwxyz" def rotate(text, key): if text == "": @@ -9,9 +9,9 @@ def rotate(text, key): first_letter, rest = text[0], text[1:] if first_letter.isalpha(): if first_letter.isupper(): - return AlPHABET[(AlPHABET.index(first_letter.lower()) + key) % 26].upper() + rotate(rest, key) + return ALPHABET[(ALPHABET.index(first_letter.lower()) + key) % 26].upper() + rotate(rest, key) else: - return AlPHABET[(AlPHABET.index(first_letter) + key) % 26] + rotate(rest, key) + return ALPHABET[(ALPHABET.index(first_letter) + key) % 26] + rotate(rest, key) else: return first_letter + rotate(rest, key) ``` diff --git a/exercises/practice/rotational-cipher/.approaches/recursion/snippet.txt b/exercises/practice/rotational-cipher/.approaches/recursion/snippet.txt index 098c419fe7b..ae0ff78fb55 100644 --- a/exercises/practice/rotational-cipher/.approaches/recursion/snippet.txt +++ b/exercises/practice/rotational-cipher/.approaches/recursion/snippet.txt @@ -1,8 +1,8 @@ first_letter, rest = text[0], text[1:] if first_letter.isalpha(): if first_letter.isupper(): - return AlPHABET[(AlPHABET.index(first_letter.lower()) + key) % 26].upper() + rotate(rest, key) + return ALPHABET[(ALPHABET.index(first_letter.lower()) + key) % 26].upper() + rotate(rest, key) else: - return AlPHABET[(AlPHABET.index(first_letter) + key) % 26] + rotate(rest, key) + return ALPHABET[(ALPHABET.index(first_letter) + key) % 26] + rotate(rest, key) else: return first_letter + rotate(rest, key) \ No newline at end of file diff --git a/exercises/practice/rotational-cipher/.approaches/str-translate/content.md b/exercises/practice/rotational-cipher/.approaches/str-translate/content.md index b3d37110c8b..ac00db3e9b0 100644 --- a/exercises/practice/rotational-cipher/.approaches/str-translate/content.md +++ b/exercises/practice/rotational-cipher/.approaches/str-translate/content.md @@ -1,11 +1,11 @@ # Str Translate ```python -AlPHABET = "abcdefghijklmnopqrstuvwxyz" +ALPHABET = "abcdefghijklmnopqrstuvwxyz" def rotate(text, key): - translator = AlPHABET[key:] + AlPHABET[:key] - return text.translate(str.maketrans(AlPHABET + AlPHABET.upper(), translator + translator.upper())) + translator = ALPHABET[key:] + ALPHABET[:key] + return text.translate(str.maketrans(ALPHABET + ALPHABET.upper(), translator + translator.upper())) ``` This approach uses the [`.translate`][translate] method. @@ -14,10 +14,10 @@ To create a translation table we use [`str.makestrans`][maketrans]. This approach starts with defining a constant of all the lowercase letters in the alphabet. Then the function `rotate()` is declared. -A `translator` variable defined with the value of the `AlPHABET` constant [sliced][slicing] from the key to the end and then sliced from the start to the key. +A `translator` variable defined with the value of the `ALPHABET` constant [sliced][slicing] from the key to the end and then sliced from the start to the key. This is done so we have 2 strings which are the same but shifted by the key value. -Say we have the `AlPHABET` constant with the value of `abcdefghijklmnopqrstuvwxyz` and the key is 3. +Say we have the `ALPHABET` constant with the value of `abcdefghijklmnopqrstuvwxyz` and the key is 3. Then the `translator` variable will have the value of `defghijklmnopqrstuvwxyzabc`. `str.translate` is then called on the `text` argument. @@ -25,7 +25,7 @@ Then the `translator` variable will have the value of `defghijklmnopqrstuvwxyzab To create a translation table, `str.makestrans` is used. `makestrans` takes 2 arguments: the first is the string to be translated, and the second is the string the first argument should be translated to. -For our solution, the first argument is the `AlPHABET` constant + the `AlPHABET` constant in uppercase. +For our solution, the first argument is the `ALPHABET` constant + the `ALPHABET` constant in uppercase. The second argument is the `translator` variable + uppercase `translator` variable. `str.makestrans` takes the [Unicode][unicode] values of the first argument and maps them to the corresponding Unicode values in the second argument, creating a `dict`. diff --git a/exercises/practice/rotational-cipher/.approaches/str-translate/snippet.txt b/exercises/practice/rotational-cipher/.approaches/str-translate/snippet.txt index 75350ae4063..61a63b38d06 100644 --- a/exercises/practice/rotational-cipher/.approaches/str-translate/snippet.txt +++ b/exercises/practice/rotational-cipher/.approaches/str-translate/snippet.txt @@ -1,5 +1,5 @@ -AlPHABET = "abcdefghijklmnopqrstuvwxyz" +ALPHABET = "abcdefghijklmnopqrstuvwxyz" def rotate(text, key): - translator = AlPHABET[key:] + AlPHABET[:key] - return text.translate(str.maketrans(AlPHABET + AlPHABET.upper(), translator + translator.upper())) \ No newline at end of file + translator = ALPHABET[key:] + ALPHABET[:key] + return text.translate(str.maketrans(ALPHABET + ALPHABET.upper(), translator + translator.upper())) \ No newline at end of file diff --git a/exercises/practice/rotational-cipher/.articles/config.json b/exercises/practice/rotational-cipher/.articles/config.json index fe3d6dc2a27..40cfdbec8ee 100644 --- a/exercises/practice/rotational-cipher/.articles/config.json +++ b/exercises/practice/rotational-cipher/.articles/config.json @@ -5,7 +5,8 @@ "slug": "performance", "title": "Performance deep dive", "blurb": "Deep dive to find out the performance between different approaches", - "authors": ["meatball133", "bethanyg"] + "authors": ["meatball133", "bethanyg"], + "contributors": ["yrahcaz7"] } ] } diff --git a/exercises/practice/rotational-cipher/.articles/performance/code/Benchmark.py b/exercises/practice/rotational-cipher/.articles/performance/code/Benchmark.py index 2919024a1f2..e37e7938ae9 100644 --- a/exercises/practice/rotational-cipher/.articles/performance/code/Benchmark.py +++ b/exercises/practice/rotational-cipher/.articles/performance/code/Benchmark.py @@ -6,8 +6,8 @@ print(sys.version) -AlPHABET = "abcdefghijklmnopqrstuvwxyz" -COMBINATIONS = itertools.combinations_with_replacement(f"{AlPHABET[:13]}{AlPHABET[:13].upper()} 12,", 2) +ALPHABET = "abcdefghijklmnopqrstuvwxyz" +COMBINATIONS = itertools.combinations_with_replacement(f"{ALPHABET[:13]}{ALPHABET[:13].upper()} 12,", 2) TEST_TEST = "".join([element for sublist in COMBINATIONS for element in sublist]) def rotate_ascii(text, key): @@ -28,17 +28,17 @@ def rotate_alphabet(text, key): for letter in text: if letter.isalpha(): if letter.isupper(): - result += AlPHABET[(AlPHABET.index(letter.lower()) + key) % 26].upper() + result += ALPHABET[(ALPHABET.index(letter.lower()) + key) % 26].upper() else: - result += AlPHABET[(AlPHABET.index(letter) + key) % 26] + result += ALPHABET[(ALPHABET.index(letter) + key) % 26] else: result += letter return result def rotate_translate(text, key): - translator = AlPHABET[key:] + AlPHABET[:key] - return text.translate(str.maketrans(AlPHABET + AlPHABET.upper(), translator + translator.upper())) + translator = ALPHABET[key:] + ALPHABET[:key] + return text.translate(str.maketrans(ALPHABET + ALPHABET.upper(), translator + translator.upper())) def rotate_recursion(text, key): @@ -47,9 +47,9 @@ def rotate_recursion(text, key): first_letter, rest = text[0], text[1:] if first_letter.isalpha(): if first_letter.isupper(): - return AlPHABET[(AlPHABET.index(first_letter.lower()) + key) % 26].upper() + rotate_recursion(rest, key) + return ALPHABET[(ALPHABET.index(first_letter.lower()) + key) % 26].upper() + rotate_recursion(rest, key) else: - return AlPHABET[(AlPHABET.index(first_letter) + key) % 26] + rotate_recursion(rest, key) + return ALPHABET[(ALPHABET.index(first_letter) + key) % 26] + rotate_recursion(rest, key) else: return first_letter + rotate_recursion(rest, key) diff --git a/exercises/practice/rotational-cipher/.articles/performance/content.md b/exercises/practice/rotational-cipher/.articles/performance/content.md index 8401b40e255..f53da0935fb 100644 --- a/exercises/practice/rotational-cipher/.articles/performance/content.md +++ b/exercises/practice/rotational-cipher/.articles/performance/content.md @@ -35,6 +35,7 @@ For a short string as input, is the alphabet approach the fastest, followed by a This means that if you know the input is a short string, the fastest approach is to use the alphabet, and forgo the overhead of making and saving a translation dictionary. On the other hand, if the input is a long string, the overhead of making a dictionary is amortized over the length of the text to be translated, and the fastest approach becomes `str.translate`. +[approaches]: https://exercism.org/tracks/python/exercises/rotational-cipher/dig_deeper [approach-recursion]: https://exercism.org/tracks/python/exercises/rotational-cipher/approaches/recursion [approach-str-translate]: https://exercism.org/tracks/python/exercises/rotational-cipher/approaches/str-translate [approach-ascii-values]: https://exercism.org/tracks/python/exercises/rotational-cipher/approaches/ascii-values diff --git a/exercises/practice/rotational-cipher/rotational_cipher_test.py b/exercises/practice/rotational-cipher/rotational_cipher_test.py index ca22735ef9b..9c226ace3f1 100644 --- a/exercises/practice/rotational-cipher/rotational_cipher_test.py +++ b/exercises/practice/rotational-cipher/rotational_cipher_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/rotational-cipher/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2026-02-19 import unittest @@ -10,6 +10,7 @@ class RotationalCipherTest(unittest.TestCase): + def test_rotate_a_by_0_same_output_as_input(self): self.assertEqual(rotate("a", 0), "a") 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/sgf-parsing/sgf_parsing_test.py b/exercises/practice/sgf-parsing/sgf_parsing_test.py index c33a5dbecff..35e08d66dfe 100644 --- a/exercises/practice/sgf-parsing/sgf_parsing_test.py +++ b/exercises/practice/sgf-parsing/sgf_parsing_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/sgf-parsing/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2026-02-19 import unittest @@ -11,6 +11,7 @@ class SgfParsingTest(unittest.TestCase): + def test_empty_input(self): input_string = "" with self.assertRaises(ValueError) as err: 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/.meta/tests.toml b/exercises/practice/simple-linked-list/.meta/tests.toml new file mode 100644 index 00000000000..b1e320ebbbb --- /dev/null +++ b/exercises/practice/simple-linked-list/.meta/tests.toml @@ -0,0 +1,101 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[962d998c-c203-41e2-8fbd-85a7b98b79b9] +description = "count -> Empty list has length of zero" + +[9760262e-d7e4-4639-9840-87e2e2fbb115] +description = "count -> Singleton list has length of one" + +[d9955c90-637c-441b-b41d-8cfb48e924a8] +description = "count -> Non-empty list has correct length" + +[0c3966db-58f9-4632-b94c-8ea13e54c2c8] +description = "pop -> Pop from empty list is an error" + +[a4f9d2e1-7425-49ef-9ee8-6c0cb3407cf0] +description = "pop -> Can pop from singleton list" + +[6dcbb2c9-d98a-47bc-a010-9c19703d3ea2] +description = "pop -> Can pop from non-empty list" + +[e83aade9-f030-4096-aaf0-f9dc6491e6cf] +description = "pop -> Can pop multiple items" + +[5c46bcf2-c0a9-4654-ae17-f3192436fcf1] +description = "pop -> Pop updates the count" + +[70d747a1-2e84-4ebc-bc3f-dcbee6a05f6b] +description = "push -> Can push to an empty list" +include = false + +[f3197f0a-1fea-45a5-939f-4a5ea60387ec] +description = "push -> Can push to an empty list" +reimplements = "70d747a1-2e84-4ebc-bc3f-dcbee6a05f6b" + +[391e332e-1f91-4033-b1e0-0e0c17812fa7] +description = "push -> Can push to a non-empty list" + +[ed4b0e01-3bbd-4895-af25-152b5914b3da] +description = "push -> Push updates count" + +[41666790-b932-4e5a-b323-e848a83d12d5] +description = "push -> Push and pop" + +[930a4a5c-76f6-47ec-9be3-4e70993173a1] +description = "peek -> Peek on empty list is an error" + +[43255a50-d919-4e81-afce-e4a271eaedbd] +description = "peek -> Can peek on singleton list" + +[48353020-e25d-4621-a854-e35fb1e15fa7] +description = "peek -> Can peek on non-empty list" + +[96fcead9-a713-46c2-8005-3f246c873851] +description = "peek -> Peek does not change the count" + +[7576ed05-7ff7-4b84-8efb-d34d62c110f5] +description = "peek -> Can peek after a pop and push" + +[b97d00b6-2fab-435d-ae74-3233dcc13698] +description = "toList LIFO -> Empty linked list to list is empty" + +[eedeb95f-b5cf-431d-8ad6-5854ba6b251c] +description = "toList LIFO -> To list with multiple values" + +[838678de-eaf3-4c14-b34e-7e35b6d851e8] +description = "toList LIFO -> To list after a pop" + +[03fc83a5-48a8-470b-a2d2-a286c5e8365f] +description = "toList FIFO -> Empty linked list to list is empty" + +[1282484e-a58c-426a-972e-90746bda61fc] +description = "toList FIFO -> To list with multiple values" + +[05ca3109-1249-4c0c-a567-a3b2f8352a7c] +description = "toList FIFO -> To list after a pop" + +[5e6c1a3d-e34b-46d3-be59-3f132a820ed5] +description = "reverse -> Reversed empty list has same values" + +[93c87ed3-862a-474f-820b-ba3fd6b6daf6] +description = "reverse -> Reversed singleton list is same list" + +[92851ebe-9f52-4406-b92e-0718c441a2ab] +description = "reverse -> Reversed non-empty list is reversed" +include = false + +[1210eeda-b23f-4790-930c-7ac6d0c8e723] +description = "reverse -> Reversed non-empty list is reversed" +reimplements = "92851ebe-9f52-4406-b92e-0718c441a2ab" + +[9b53af96-7494-4cfa-9b77-b7366fed5c4c] +description = "reverse -> Double reverse" 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/space-age/.meta/config.json b/exercises/practice/space-age/.meta/config.json index 2c6189d870c..c5bb8daf1ff 100644 --- a/exercises/practice/space-age/.meta/config.json +++ b/exercises/practice/space-age/.meta/config.json @@ -30,5 +30,5 @@ }, "blurb": "Given an age in seconds, calculate how old someone is in terms of a given planet's solar years.", "source": "Partially inspired by Chapter 1 in Chris Pine's online Learn to Program tutorial.", - "source_url": "https://pine.fm/LearnToProgram/?Chapter=01" + "source_url": "https://pine.fm/LearnToProgram/chap_01.html" } 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/state-of-tic-tac-toe/.docs/instructions.md b/exercises/practice/state-of-tic-tac-toe/.docs/instructions.md new file mode 100644 index 00000000000..1a03ebb6cb4 --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.docs/instructions.md @@ -0,0 +1,101 @@ +# Instructions + +In this exercise, you're going to implement a program that determines the state of a [tic-tac-toe][] game. +(_You may also know the game as "noughts and crosses" or "Xs and Os"._) + +The game is played on a 3ร—3 grid. +Players take turns to place `X`s and `O`s on the grid. +The game ends when one player has won by placing three of marks in a row, column, or along a diagonal of the grid, or when the entire grid is filled up. + +In this exercise, we will assume that `X` starts. + +It's your job to determine which state a given game is in. + +There are 3 potential game states: + +- The game is **ongoing**. +- The game ended in a **draw**. +- The game ended in a **win**. + +If the given board is invalid, throw an appropriate error. + +If a board meets the following conditions, it is invalid: + +- The given board cannot be reached when turns are taken in the correct order (remember that `X` starts). +- The game was played after it already ended. + +## Examples + +### Ongoing game + +```text + | | + X | | +___|___|___ + | | + | X | O +___|___|___ + | | + O | X | + | | +``` + +### Draw + +```text + | | + X | O | X +___|___|___ + | | + X | X | O +___|___|___ + | | + O | X | O + | | +``` + +### Win + +```text + | | + X | X | X +___|___|___ + | | + | O | O +___|___|___ + | | + | | + | | +``` + +### Invalid + +#### Wrong turn order + +```text + | | + O | O | X +___|___|___ + | | + | | +___|___|___ + | | + | | + | | +``` + +#### Continued playing after win + +```text + | | + X | X | X +___|___|___ + | | + O | O | O +___|___|___ + | | + | | + | | +``` + +[tic-tac-toe]: https://en.wikipedia.org/wiki/Tic-tac-toe diff --git a/exercises/practice/state-of-tic-tac-toe/.meta/config.json b/exercises/practice/state-of-tic-tac-toe/.meta/config.json new file mode 100644 index 00000000000..737a603c6e4 --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "BNAndras" + ], + "files": { + "solution": [ + "state_of_tic_tac_toe.py" + ], + "test": [ + "state_of_tic_tac_toe_test.py" + ], + "example": [ + ".meta/example.py" + ] + }, + "blurb": "Determine the game state of a match of Tic-Tac-Toe.", + "source": "Created by Sascha Mann for the Julia track of the Exercism Research Experiment.", + "source_url": "https://github.com/exercism/research_experiment_1/tree/julia-dev/exercises/julia-1-a" +} diff --git a/exercises/practice/state-of-tic-tac-toe/.meta/example.py b/exercises/practice/state-of-tic-tac-toe/.meta/example.py new file mode 100644 index 00000000000..5f0bf70291a --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.meta/example.py @@ -0,0 +1,49 @@ +def gamestate(board): + def check_if_won(player): + # Rows + for row in board: + if all(cell == player for cell in row): + return True + + # Cols + for col in range(3): + if all(board[row][col] == player for row in range(3)): + return True + + # top left to bottom right + if all(board[i][i] == player for i in range(3)): + return True + + # top right to bottom left + if all(board[i][2-i] == player for i in range(3)): + return True + return False + + x_count = sum(row.count("X") for row in board) + o_count = sum(row.count("O") for row in board) + + if o_count > x_count: + raise ValueError("Wrong turn order: O started") + if x_count > o_count + 1: + raise ValueError("Wrong turn order: X went twice") + + x_won = check_if_won("X") + o_won = check_if_won("O") + + if x_won and o_won: + raise ValueError("Impossible board: game should have ended after the game was won") + + if x_won: + if x_count == o_count: + raise ValueError("Impossible board: game should have ended after the game was won") + return "win" + + if o_won: + if x_count > o_count: + raise ValueError("Impossible board: game should have ended after the game was won") + return "win" + + if x_count + o_count == 9: + return "draw" + + return "ongoing" diff --git a/exercises/practice/state-of-tic-tac-toe/.meta/template.j2 b/exercises/practice/state-of-tic-tac-toe/.meta/template.j2 new file mode 100644 index 00000000000..826f2703b41 --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.meta/template.j2 @@ -0,0 +1,28 @@ +{% import "generator_macros.j2" as macros with context %} +{{ macros.canonical_ref() }} + +{{ macros.header() }} + +{% macro test_case(case) -%} + def test_{{ case["description"]|to_snake }}(self): + board = [ + {% for row in case["input"]["board"] -%} + "{{ row }}", + {% endfor -%} + ] + {%- if case is error_case %} + with self.assertRaises(ValueError) as err: + {{ case["property"]|to_snake }}(board) + self.assertEqual(type(err.exception), ValueError) + self.assertEqual(err.exception.args[0], "{{ case["expected"]["error"] }}") + {%- else %} + self.assertEqual({{ case["property"]|to_snake }}(board), "{{ case["expected"] }}") + {%- endif %} +{%- endmacro %} + +class {{ exercise|camel_case }}Test(unittest.TestCase): + {%- for category in cases %} + {%- for case in category["cases"] %} + {{ test_case(case) }} + {%- endfor %} + {%- endfor %} diff --git a/exercises/practice/state-of-tic-tac-toe/.meta/tests.toml b/exercises/practice/state-of-tic-tac-toe/.meta/tests.toml new file mode 100644 index 00000000000..8fc25e2118d --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.meta/tests.toml @@ -0,0 +1,101 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[fe8e9fa9-37af-4d7e-aa24-2f4b8517161a] +description = "Won games -> Finished game where X won via left column victory" + +[96c30df5-ae23-4cf6-bf09-5ef056dddea1] +description = "Won games -> Finished game where X won via middle column victory" + +[0d7a4b0a-2afd-4a75-8389-5fb88ab05eda] +description = "Won games -> Finished game where X won via right column victory" + +[bd1007c0-ec5d-4c60-bb9f-1a4f22177d51] +description = "Won games -> Finished game where O won via left column victory" + +[c032f800-5735-4354-b1b9-46f14d4ee955] +description = "Won games -> Finished game where O won via middle column victory" + +[662c8902-c94a-4c4c-9d9c-e8ca513db2b4] +description = "Won games -> Finished game where O won via right column victory" + +[2d62121f-7e3a-44a0-9032-0d73e3494941] +description = "Won games -> Finished game where X won via top row victory" + +[108a5e82-cc61-409f-aece-d7a18c1beceb] +description = "Won games -> Finished game where X won via middle row victory" +include = false + +[346527db-4db9-4a96-b262-d7023dc022b0] +description = "Won games -> Finished game where X won via middle row victory" +reimplements = "108a5e82-cc61-409f-aece-d7a18c1beceb" + +[a013c583-75f8-4ab2-8d68-57688ff04574] +description = "Won games -> Finished game where X won via bottom row victory" + +[2c08e7d7-7d00-487f-9442-e7398c8f1727] +description = "Won games -> Finished game where O won via top row victory" + +[bb1d6c62-3e3f-4d1a-9766-f8803c8ed70f] +description = "Won games -> Finished game where O won via middle row victory" + +[6ef641e9-12ec-44f5-a21c-660ea93907af] +description = "Won games -> Finished game where O won via bottom row victory" + +[ab145b7b-26a7-426c-ab71-bf418cd07f81] +description = "Won games -> Finished game where X won via falling diagonal victory" + +[7450caab-08f5-4f03-a74b-99b98c4b7a4b] +description = "Won games -> Finished game where X won via rising diagonal victory" + +[c2a652ee-2f93-48aa-a710-a70cd2edce61] +description = "Won games -> Finished game where O won via falling diagonal victory" + +[5b20ceea-494d-4f0c-a986-b99efc163bcf] +description = "Won games -> Finished game where O won via rising diagonal victory" + +[035a49b9-dc35-47d3-9d7c-de197161b9d4] +description = "Won games -> Finished game where X won via a row and a column victory" + +[e5dfdeb0-d2bf-4b5a-b307-e673f69d4a53] +description = "Won games -> Finished game where X won via two diagonal victories" + +[b42ed767-194c-4364-b36e-efbfb3de8788] +description = "Drawn games -> Draw" + +[227a76b2-0fef-4e16-a4bd-8f9d7e4c3b13] +description = "Drawn games -> Another draw" + +[4d93f15c-0c40-43d6-b966-418b040012a9] +description = "Ongoing games -> Ongoing game: one move in" + +[c407ae32-4c44-4989-b124-2890cf531f19] +description = "Ongoing games -> Ongoing game: two moves in" + +[199b7a8d-e2b6-4526-a85e-78b416e7a8a9] +description = "Ongoing games -> Ongoing game: five moves in" + +[1670145b-1e3d-4269-a7eb-53cd327b302e] +description = "Invalid boards -> Invalid board: X went twice" + +[47c048e8-b404-4bcf-9e51-8acbb3253f3b] +description = "Invalid boards -> Invalid board: O started" + +[b1dc8b13-46c4-47db-a96d-aa90eedc4e8d] +description = "Invalid boards -> Invalid board" +include = false + +[6c1920f2-ab5c-4648-a0c9-997414dda5eb] +description = "Invalid boards -> Invalid board: X won and O kept playing" +reimplements = "b1dc8b13-46c4-47db-a96d-aa90eedc4e8d" + +[4801cda2-f5b7-4c36-8317-3cdd167ac22c] +description = "Invalid boards -> Invalid board: players kept playing after a win" diff --git a/exercises/practice/state-of-tic-tac-toe/state_of_tic_tac_toe.py b/exercises/practice/state-of-tic-tac-toe/state_of_tic_tac_toe.py new file mode 100644 index 00000000000..b41c78f383f --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/state_of_tic_tac_toe.py @@ -0,0 +1,2 @@ +def gamestate(board): + pass diff --git a/exercises/practice/state-of-tic-tac-toe/state_of_tic_tac_toe_test.py b/exercises/practice/state-of-tic-tac-toe/state_of_tic_tac_toe_test.py new file mode 100644 index 00000000000..173ecf887a0 --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/state_of_tic_tac_toe_test.py @@ -0,0 +1,245 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/state-of-tic-tac-toe/canonical-data.json +# File last updated on 2026-01-30 + +import unittest + +from state_of_tic_tac_toe import ( + gamestate, +) + + +class StateOfTicTacToeTest(unittest.TestCase): + def test_finished_game_where_x_won_via_left_column_victory(self): + board = [ + "XOO", + "X ", + "X ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_middle_column_victory(self): + board = [ + "OXO", + " X ", + " X ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_right_column_victory(self): + board = [ + "OOX", + " X", + " X", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_left_column_victory(self): + board = [ + "OXX", + "OX ", + "O ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_middle_column_victory(self): + board = [ + "XOX", + " OX", + " O ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_right_column_victory(self): + board = [ + "XXO", + " XO", + " O", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_top_row_victory(self): + board = [ + "XXX", + "XOO", + "O ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_middle_row_victory(self): + board = [ + "O ", + "XXX", + " O ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_bottom_row_victory(self): + board = [ + " OO", + "O X", + "XXX", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_top_row_victory(self): + board = [ + "OOO", + "XXO", + "XX ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_middle_row_victory(self): + board = [ + "XX ", + "OOO", + "X ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_bottom_row_victory(self): + board = [ + "XOX", + " XX", + "OOO", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_falling_diagonal_victory(self): + board = [ + "XOO", + " X ", + " X", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_rising_diagonal_victory(self): + board = [ + "O X", + "OX ", + "X ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_falling_diagonal_victory(self): + board = [ + "OXX", + "OOX", + "X O", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_rising_diagonal_victory(self): + board = [ + " O", + " OX", + "OXX", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_a_row_and_a_column_victory(self): + board = [ + "XXX", + "XOO", + "XOO", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_two_diagonal_victories(self): + board = [ + "XOX", + "OXO", + "XOX", + ] + self.assertEqual(gamestate(board), "win") + + def test_draw(self): + board = [ + "XOX", + "XXO", + "OXO", + ] + self.assertEqual(gamestate(board), "draw") + + def test_another_draw(self): + board = [ + "XXO", + "OXX", + "XOO", + ] + self.assertEqual(gamestate(board), "draw") + + def test_ongoing_game_one_move_in(self): + board = [ + " ", + "X ", + " ", + ] + self.assertEqual(gamestate(board), "ongoing") + + def test_ongoing_game_two_moves_in(self): + board = [ + "O ", + " X ", + " ", + ] + self.assertEqual(gamestate(board), "ongoing") + + def test_ongoing_game_five_moves_in(self): + board = [ + "X ", + " XO", + "OX ", + ] + self.assertEqual(gamestate(board), "ongoing") + + def test_invalid_board_x_went_twice(self): + board = [ + "XX ", + " ", + " ", + ] + with self.assertRaises(ValueError) as err: + gamestate(board) + self.assertEqual(type(err.exception), ValueError) + self.assertEqual(err.exception.args[0], "Wrong turn order: X went twice") + + def test_invalid_board_o_started(self): + board = [ + "OOX", + " ", + " ", + ] + with self.assertRaises(ValueError) as err: + gamestate(board) + self.assertEqual(type(err.exception), ValueError) + self.assertEqual(err.exception.args[0], "Wrong turn order: O started") + + def test_invalid_board_x_won_and_o_kept_playing(self): + board = [ + "XXX", + "OOO", + " ", + ] + with self.assertRaises(ValueError) as err: + gamestate(board) + self.assertEqual(type(err.exception), ValueError) + self.assertEqual( + err.exception.args[0], + "Impossible board: game should have ended after the game was won", + ) + + def test_invalid_board_players_kept_playing_after_a_win(self): + board = [ + "XXX", + "OOO", + "XOX", + ] + with self.assertRaises(ValueError) as err: + gamestate(board) + self.assertEqual(type(err.exception), ValueError) + self.assertEqual( + err.exception.args[0], + "Impossible board: game should have ended after the game was won", + ) diff --git a/exercises/practice/sublist/.approaches/config.json b/exercises/practice/sublist/.approaches/config.json index ce54db9c14e..22ae02518b8 100644 --- a/exercises/practice/sublist/.approaches/config.json +++ b/exercises/practice/sublist/.approaches/config.json @@ -1,6 +1,7 @@ { "introduction": { - "authors": ["safwansamsudeen"] + "authors": ["safwansamsudeen"], + "contributors": ["yrahcaz7"] }, "approaches": [ { @@ -8,14 +9,30 @@ "slug": "list-manipulation", "title": "List manipulation", "blurb": "Manipulate and check lists to solve the exercise", - "authors": ["safwansamsudeen"] + "authors": ["safwansamsudeen"], + "contributors": ["yrahcaz7"] }, { "uuid": "61366160-c859-4d16-9085-171428209b8d", "slug": "using-strings", "title": "Using strings", "blurb": "Convert the lists to string and use string manipulation to solve the exercise", - "authors": ["safwansamsudeen"] + "authors": ["safwansamsudeen"], + "contributors": ["yrahcaz7"] + }, + { + "uuid": "b2695c39-c1c7-47f0-bfcd-5e9703674bea", + "slug": "manual-loop", + "title": "Manual looping", + "blurb": "Manually track indexes while looping through the lists to solve the exercise", + "authors": ["yrahcaz7"] + }, + { + "uuid": "a1eeaf9b-a9b3-421e-bfad-44f7e1575450", + "slug": "sort-lists", + "title": "Sorting lists", + "blurb": "Sort the lists to determine the shorter and longer ones to solve the exercise", + "authors": ["yrahcaz7"] } ] } diff --git a/exercises/practice/sublist/.approaches/introduction.md b/exercises/practice/sublist/.approaches/introduction.md index 42f991ef086..13c10b2dba1 100644 --- a/exercises/practice/sublist/.approaches/introduction.md +++ b/exercises/practice/sublist/.approaches/introduction.md @@ -1,24 +1,24 @@ # Introduction -There are two broad ways to solve Sublist. + +There are four broad ways to solve Sublist, though one of them ("using strings") is not recommended. ## General guidance -To write the code, you need to branch out (probably with `if`) into the four different possible conditions, and return the appropriate name of the category. -## Approach: list manipulation +To write the code, you need to branch out (probably with `if`) into the four different possible conditions, and return the appropriate category (`SUBLIST`, `SUPERLIST`, `EQUAL`, or `UNEQUAL`). + +Note that you shouldn't return the category's value directly, as that would introduce [magic values][magic-values] into your code. + +## Approach: List manipulation + The direct approach would be to manipulate and check the given lists to solve this. This solution uses a helper function, which simplifies things, but the approach can be implemented without it. ```python -SUBLIST = 1 -SUPERLIST = 2 -EQUAL = 3 -UNEQUAL = 4 - -def check_sub_sequences(list_one, list_two): - n1 = len(list_one) - n2 = len(list_two) - return any(list_two[i:i+n1] == list_one for i in range(n2 - n1 + 1)) - +def check_sub_sequences(list_a, list_b): + len_a = len(list_a) + len_b = len(list_b) + return any(list_b[i : i + len_a] == list_a for i in range(len_b - len_a + 1)) + def sublist(list_one, list_two): if list_one == list_two: return EQUAL @@ -31,29 +31,89 @@ def sublist(list_one, list_two): Read more on the [detail of this approach][approach-list-manipulation]. -## Approach: using strings -Another seemingly clever approach is to convert the lists to strings and then -use the `in` operator to check for sub-sequences. -**However, this does not work.** +## Approach: Manual looping + +This approach uses a helper function that manually loops through the lists to determine if the first list is a sublist of the second one. +This approach is the longest one by far, though it may be more comprehensible to some. + ```python -SUBLIST = 1 -SUPERLIST = 2 -EQUAL = 3 -UNEQUAL = 4 +def check_sub_sequences(list_a, list_b): + len_a, len_b = len(list_a), len(list_b) + index_a, index_b = 0, 0 + next_index_b = 1 + + while index_a < len_a and index_b < len_b: + if list_a[index_a] == list_b[index_b]: + index_a += 1 + else: + index_a, index_b = 0, next_index_b + next_index_b += 1 + index_b += 1 + + if index_a == len_a: + if len_a == len_b: + return EQUAL + return SUBLIST + return UNEQUAL def sublist(list_one, list_two): - list_one_check = (str(list_one).strip("[]") + ",") - list_two_check = (str(list_two).strip("[]") + ",") + result = check_sub_sequences(list_one, list_two) + + if result == UNEQUAL and check_sub_sequences(list_two, list_one) == SUBLIST: + result = SUPERLIST + return result +``` + +Learn more about the [details of this approach here][approach-manual-loop]. + +## Approach: Sorting lists + +This approach uses the `sorted()` function to determine which list is shorter and which is longer. +Knowing this information, one can implement a simplified version of the list manipulation approach. + +```python +def sublist(list_one, list_two): + if list_one == list_two: + return EQUAL + if not list_one: + return SUBLIST + if not list_two: + return SUPERLIST + + shorter, longer = sorted((list_one, list_two), key=len) + + for index in range(len(longer) - len(shorter) + 1): + if longer[index : index + len(shorter)] == shorter: + return SUPERLIST if longer is list_one else SUBLIST + + return UNEQUAL +``` + +Read more on the [detail of this approach][approach-sort-lists]. + +## Approach: Using strings + +Another seemingly clever approach is to convert the lists to strings and then use the `in` operator to check for sub-sequences. +**However, this does not work.** + +```python +def sublist(list_one, list_two): + list_one_check = str(list_one).strip("[]") + "," + list_two_check = str(list_two).strip("[]") + "," if list_one_check == list_two_check: return EQUAL - elif list_one_check in list_two_check: + if list_one_check in list_two_check: return SUBLIST - elif list_two_check in list_one_check: + if list_two_check in list_one_check: return SUPERLIST return UNEQUAL ``` + To understand more about this approach and **why it fails**, [read here][approach-using-strings]. +[magic-values]: https://stackoverflow.com/questions/47882/what-is-a-magic-number-and-why-is-it-bad [approach-list-manipulation]: https://exercism.org/tracks/python/exercises/sublist/approaches/list-manipulation +[approach-manual-loop]: https://exercism.org/tracks/python/exercises/sublist/approaches/manual-loop +[approach-sort-lists]: https://exercism.org/tracks/python/exercises/sublist/approaches/sort-lists [approach-using-strings]: https://exercism.org/tracks/python/exercises/sublist/approaches/using-strings diff --git a/exercises/practice/sublist/.approaches/list-manipulation/content.md b/exercises/practice/sublist/.approaches/list-manipulation/content.md index ac374b730e7..3e68260292e 100644 --- a/exercises/practice/sublist/.approaches/list-manipulation/content.md +++ b/exercises/practice/sublist/.approaches/list-manipulation/content.md @@ -1,4 +1,5 @@ # List manipulation + The direct approach would be to manipulate and check the given lists to solve this. This solution uses a helper function, which simplifies things, but the approach can be implemented without it. @@ -8,11 +9,11 @@ SUPERLIST = 2 EQUAL = 3 UNEQUAL = 4 -def check_sub_sequences(list_one, list_two): - n1 = len(list_one) - n2 = len(list_two) - return any(list_two[i:i+n1] == list_one for i in range(n2 - n1 + 1)) - +def check_sub_sequences(list_a, list_b): + len_a = len(list_a) + len_b = len(list_b) + return any(list_b[i : i + len_a] == list_a for i in range(len_b - len_a + 1)) + def sublist(list_one, list_two): if list_one == list_two: return EQUAL @@ -23,16 +24,21 @@ def sublist(list_one, list_two): return UNEQUAL ``` -We first check for equality using the `==` operator, if so, then we return `EQUAL`. -A common way to do this differently would be to return `1` directly, but this is better practice as we [remove magic values][magic values]. +~~~~exercism/note +You might wonder why the lists in the helper function are named `list_a` and `list_b` instead of `list_one` and `list_two`. +This is because if the parameters have the same name, Pylint thinks the parameters are being passed in incorrectly when we call `check_sub_sequences(list_two, list_one)`. +(The exact warning generated is [`W1114 arguments-out-of-order`][w1114].) + +[w1114]: https://pylint.readthedocs.io/en/stable/user_guide/messages/warning/arguments-out-of-order.html +~~~~ -After that we call `check_sub_sequences` passing in `list_one` and `list_two`. -In the helper function, we check if `any` of the possible sub-sequences in `list_two` of length `n1` (the length of the first list) are equal to the first list. -If so, then we conclude that `list_one` is a `SUBLIST` of `list_two`. +In this approach, we first check for equality using the `==` operator, and if the lists are equal, then we return `EQUAL`. +After that, we call `check_sub_sequences()`, passing in `list_one` and `list_two` for the parameters `list_a` and `list_b`. -To find whether `list_one` is a `SUPERLIST` of `list_two`, we just reverse this process - pass in the lists in the opposite order. -Thus, we check if `any` of the possible sub-sequences in `list_one` of length `n2` (the length of the second list) are equal to the second list. +In the helper function, we check if `any` of the possible sub-sequences in `list_b` of length `len_a` (the length of `list_a`) are equal to `list_a`. +If so, then we conclude that `list_a` is a `SUBLIST` of `list_b`. -If none of the above conditions are true, we conclude that the two lists are unequal. +To find whether `list_one` is a `SUPERLIST` of `list_two`, we just reverse this process โ€” pass in the lists in the opposite order. +Thus, we check if `any` of the possible sub-sequences in `list_one` of the length of `list_two` are equal to `list_two`. -[magic values]: https://stackoverflow.com/questions/47882/what-is-a-magic-number-and-why-is-it-bad \ No newline at end of file +If none of the above conditions are true, we conclude that the two lists are `UNEQUAL`. diff --git a/exercises/practice/sublist/.approaches/manual-loop/content.md b/exercises/practice/sublist/.approaches/manual-loop/content.md new file mode 100644 index 00000000000..f55e28cc077 --- /dev/null +++ b/exercises/practice/sublist/.approaches/manual-loop/content.md @@ -0,0 +1,56 @@ +# Manual looping + +This approach uses a helper function that manually loops through the lists to determine if the first list is a sublist of the second one. +This approach is the longest one by far, though it may be more comprehensible to some. + +```python +SUBLIST = 1 +SUPERLIST = 2 +EQUAL = 3 +UNEQUAL = 4 + +def check_sub_sequences(list_a, list_b): + len_a, len_b = len(list_a), len(list_b) + index_a, index_b = 0, 0 + next_index_b = 1 + + while index_a < len_a and index_b < len_b: + if list_a[index_a] == list_b[index_b]: + index_a += 1 + else: + index_a, index_b = 0, next_index_b + next_index_b += 1 + index_b += 1 + + if index_a == len_a: + if len_a == len_b: + return EQUAL + return SUBLIST + return UNEQUAL + +def sublist(list_one, list_two): + result = check_sub_sequences(list_one, list_two) + + if result == UNEQUAL and check_sub_sequences(list_two, list_one) == SUBLIST: + result = SUPERLIST + return result +``` + +~~~~exercism/note +You might wonder why the lists in the helper function are named `list_a` and `list_b` instead of `list_one` and `list_two`. +This is because if the parameters have the same name, Pylint thinks the parameters are being passed in incorrectly when we call `check_sub_sequences(list_two, list_one)`. +(The exact warning generated is [`W1114 arguments-out-of-order`][w1114].) + +[w1114]: https://pylint.readthedocs.io/en/stable/user_guide/messages/warning/arguments-out-of-order.html +~~~~ + +In this approach, the first thing `sublist()` does is call the helper function. +That function then loops through the lists, keeping track of an index for both lists so it can test all necessary combinations to determine if `list_one` is a sublist of `list_two`. + +However, the helper function only determines if `list_one` is equal to or a sublist of `list_two`, not if `list_one` is a superlist of `list_two`. +That is why if the helper function returns `UNEQUAL`, `sublist()` needs to make sure that it isn't acutally a superlist. + +`sublist()` does this by calling the helper function with its arguments reversed: `check_sub_sequences(list_two, list_one)`. +If the result is `SUBLIST`, that means `list_two` is a sublist of `list_one`, thus `list_one` must be a superlist of `list_two`. + +Thus all possibilities are covered, and `sublist()` returns the result. diff --git a/exercises/practice/sublist/.approaches/manual-loop/snippet.txt b/exercises/practice/sublist/.approaches/manual-loop/snippet.txt new file mode 100644 index 00000000000..081ed4aae12 --- /dev/null +++ b/exercises/practice/sublist/.approaches/manual-loop/snippet.txt @@ -0,0 +1,8 @@ +while index_one < len(list_one) and index_two < len(list_two): + if list_one[index_one] == list_two[index_two]: + index_one += 1 + else: + index_one = 0 + index_two = next_index_two + next_index_two += 1 + index_two += 1 \ No newline at end of file diff --git a/exercises/practice/sublist/.approaches/sort-lists/content.md b/exercises/practice/sublist/.approaches/sort-lists/content.md new file mode 100644 index 00000000000..ede91c1fac2 --- /dev/null +++ b/exercises/practice/sublist/.approaches/sort-lists/content.md @@ -0,0 +1,44 @@ +# Sorting lists + +This approach uses the `sorted()` function to determine which list is shorter and which is longer. +Knowing this information, one can implement a simplified version of the [list manipulation approach][approach-list-manipulation]. + +```python +SUBLIST = 1 +SUPERLIST = 2 +EQUAL = 3 +UNEQUAL = 4 + +def sublist(list_one, list_two): + if list_one == list_two: + return EQUAL + if not list_one: + return SUBLIST + if not list_two: + return SUPERLIST + + shorter, longer = sorted((list_one, list_two), key=len) + + for index in range(len(longer) - len(shorter) + 1): + if longer[index : index + len(shorter)] == shorter: + return SUPERLIST if longer is list_one else SUBLIST + + return UNEQUAL +``` + +Here, the case of the lists being equal is checked first. +Then the special cases of empty lists are handled, returning `SUBLIST` or `SUPERLIST` as necessary. + +Once those simple cases are out of the way, the `sorted()` function is used with the keyword argument `key` set to the `len()` function. +This makes `sorted()` sort the items according to their length. + +Once `sorted()` does its work, we use multiple assignment to unpack the results into the `shorter` and `longer` variables. +Then, for each slice of length `len(shorter)` in `longer`, we test if that slice is equal to `shorter`. + +If we find such a slice, that means `shorter` is a sublist of `longer`. +Then we use a [conditional expression][conditional-expression] along with the `is` operator to return `SUBLIST` or `SUPERLIST` depending on which of the original lists is `longer`. + +If we do not find such a slice, we can eliminate `SUBLIST` and `SUPERLIST` from the possible categories, thus the two lists must be `UNEQUAL`. + +[approach-list-manipulation]: https://exercism.org/tracks/python/exercises/sublist/approaches/list-manipulation +[conditional-expression]: https://docs.python.org/3/reference/expressions.html#conditional-expressions diff --git a/exercises/practice/sublist/.approaches/sort-lists/snippet.txt b/exercises/practice/sublist/.approaches/sort-lists/snippet.txt new file mode 100644 index 00000000000..f106f76255f --- /dev/null +++ b/exercises/practice/sublist/.approaches/sort-lists/snippet.txt @@ -0,0 +1,8 @@ +def sublist(list_one, list_two): + ... + shorter, longer = sorted((list_one, list_two), key=len) + + for index in range(len(longer) - len(shorter) + 1): + if longer[index : index + len(shorter)] == shorter: + return SUPERLIST if longer is list_one else SUBLIST + return UNEQUAL \ No newline at end of file diff --git a/exercises/practice/sublist/.approaches/using-strings/content.md b/exercises/practice/sublist/.approaches/using-strings/content.md index ff960902dc9..60c49d168b2 100644 --- a/exercises/practice/sublist/.approaches/using-strings/content.md +++ b/exercises/practice/sublist/.approaches/using-strings/content.md @@ -1,13 +1,12 @@ # Using strings + ~~~~exercism/caution -**This approach does not work, and this document exists to explain that.** +**This approach does not work (_it will not generalize to all cases_), and this document exists to explain that.** Please do not use it in your code. ~~~~ -Another seemingly clever solution is to convert the lists to strings and then -use the `in` operator to check for sub-sequences. -Note that this approach, even if it worked, is not as performant as the -previous one. +Another seemingly clever solution is to convert the lists to strings and then use the `in` operator to check for sub-sequences. + ```python SUBLIST = 1 SUPERLIST = 2 @@ -20,28 +19,36 @@ def sublist(list_one, list_two): if list_one_check == list_two_check: return EQUAL - elif list_one_check in list_two_check: + if list_one_check in list_two_check: return SUBLIST - elif list_two_check in list_one_check: + if list_two_check in list_one_check: return SUPERLIST return UNEQUAL ``` + Let's parse the code to see what it does. -In this approach, we convert the lists to strings, so `[1, 2, 3]` becomes `"[1, 2, 3]"`, remove the brackets `"1, 2, 3"`, and add a comma `"1, 2, 3,"`. +In this approach, we convert the lists to strings, so `[1, 2, 3]` becomes `"[1, 2, 3]"`, remove the brackets `"1, 2, 3"`, and add a comma `"1, 2, 3,"`. We check equality and then use the `in` operator to check for `SUBLIST` or `SUPERLIST`, and finally return `UNEQUAL`. -We add a comma because, say, we call `sublist` with `[1, 2]` and `[1, 22]`. `"1, 2" in "1, 22"` evaluates to `True`, so -the **function would wrongly mark it as `SUBLIST`**. +We add a comma because, say, we call `sublist` with `[1, 2]` and `[1, 22]`. `"1, 2" in "1, 22"` evaluates to `True`, so the **function would wrongly mark it as `SUBLIST`**. + +This case can be handled by changing the code like this: + +```python +list_one_check = str(list_one).strip("[]") + "," +list_two_check = str(list_two).strip("[]") + "," +``` + +Yet, even though this code would pass all of the tests in the Exercism test suite, it would still fail in some cases. +For example, if we call `sublist` with `[1, 2]` and `[5, "1, 2,", 7]`, the function would return `SUBLIST` when it should actually return `UNEQUAL`. + +This could be avoided by changing the code to use a separator that isn't the default one: -This test can be overridden by changing the code like this: ```python -list_one_check = str(list_one).strip("[]") + ',' -list_two_check = str(list_two).strip("[]") + ',' +list_one_check = "|".join(str(item) for item in list_one) + "|" +list_two_check = "|".join(str(item) for item in list_two) + "|" ``` -Yet, the test case (which doesn't exist in the Exercism test suite) `["1", "2"]` and `["5", "'1', '2',", "7"]` would -fail. -Students can add any arbitrary string into the representation to try to "defeat" this test - `list_one_check = str -(list_one) + TOKEN`. The test suite currently test `TOKEN = ''`, but not others. +However, this only avoids the (theoretical) test and does not fix the solution. For example, a test with the inputs `[1, 2]` and `[5, "1|2|", 7]` would now fail. -[gen-exp]: https://www.programiz.com/python-programming/generator \ No newline at end of file +No matter what separator is chosen, there will always be at least one input for which the function will return the wrong result. **This is why no approach that converts the lists to strings can ever be correct for all possible inputs.** diff --git a/exercises/practice/sublist/.approaches/using-strings/snippet.txt b/exercises/practice/sublist/.approaches/using-strings/snippet.txt index 26fc3ec0ec7..4d4b6439294 100644 --- a/exercises/practice/sublist/.approaches/using-strings/snippet.txt +++ b/exercises/practice/sublist/.approaches/using-strings/snippet.txt @@ -1,8 +1,8 @@ -# Failing approach +# WARNING: Failing approach def sublist(list_one, list_two): list_one_check = str(list_one).strip("[]") ... - elif list_one_check in list_two_check: + if list_one_check in list_two_check: return SUBLIST ... return UNEQUAL \ No newline at end of file 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..234f7feec0d --- /dev/null +++ b/exercises/practice/swift-scheduling/swift_scheduling_test.py @@ -0,0 +1,110 @@ +# 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 2026-02-19 + +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/.meta/tests.toml b/exercises/practice/tree-building/.meta/tests.toml new file mode 100644 index 00000000000..fcdc1d508ae --- /dev/null +++ b/exercises/practice/tree-building/.meta/tests.toml @@ -0,0 +1,58 @@ +# 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. + +[761790a3-4c27-461a-b4e9-8bce8ccee5a1] +description = "empty list" + +[dcc89dc3-eb39-4f26-a3cd-964e607c95ff] +description = "single record" + +[dcdb80f0-e5da-43e1-8b8d-6f307be89c0e] +description = "three records in order" + +[2ff5b8f8-d95e-401e-9359-233919488d22] +description = "three records in reverse order" + +[de798d3b-8905-4446-a114-a0dd2476d945] +description = "more than two children" + +[13dd9b3c-6137-415f-b6fe-5044c1dfbc50] +description = "binary tree" + +[5cfd29dc-166b-47da-84ca-1c60b5ae5941] +description = "unbalanced tree" + +[a05ddb5d-2d11-4948-88d3-b5f18a44ddce] +description = "one root node and has parent" + +[9ed09df2-8fd6-4e37-aa37-e7753c057a1a] +description = "root node has parent" + +[8755a2c4-2c6b-4396-b155-b5bf4b6bc280] +description = "no root node" + +[c6ef8f9a-4045-4949-a1e1-e0ae804e4af4] +description = "duplicate node" + +[7a7b77a6-3447-4905-b79c-d22bfe43f408] +description = "duplicate root" + +[c6f51bd7-3608-4390-b446-dfd1bcbf3ddc] +description = "non-continuous" + +[1f3d1b50-4494-4b22-b88a-68f32f7d321d] +description = "cycle directly" + +[ac568b50-3f9b-4cb4-b602-e0eb13de4269] +description = "cycle indirectly" + +[cf954b21-3cef-420c-8e72-d19547505e1f] +description = "higher id parent of lower id" 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/triangle/.meta/config.json b/exercises/practice/triangle/.meta/config.json index 041bf28ccfb..8fcf4b82adc 100644 --- a/exercises/practice/triangle/.meta/config.json +++ b/exercises/practice/triangle/.meta/config.json @@ -32,5 +32,5 @@ }, "blurb": "Determine if a triangle is equilateral, isosceles, or scalene.", "source": "The Ruby Koans triangle project, parts 1 & 2", - "source_url": "https://web.archive.org/web/20220831105330/http://rubykoans.com" + "source_url": "https://www.rubykoans.com/" } diff --git a/exercises/practice/triangle/triangle_test.py b/exercises/practice/triangle/triangle_test.py index b279c83c325..d4f7269e398 100644 --- a/exercises/practice/triangle/triangle_test.py +++ b/exercises/practice/triangle/triangle_test.py @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/triangle/canonical-data.json -# File last updated on 2023-07-19 +# File last updated on 2026-02-19 import unittest @@ -12,6 +12,7 @@ class EquilateralTriangleTest(unittest.TestCase): + def test_all_sides_are_equal(self): self.assertIs(equilateral([2, 2, 2]), True) @@ -29,6 +30,7 @@ def test_sides_may_be_floats(self): class IsoscelesTriangleTest(unittest.TestCase): + def test_last_two_sides_are_equal(self): self.assertIs(isosceles([3, 4, 4]), True) @@ -58,6 +60,7 @@ def test_sides_may_be_floats(self): class ScaleneTriangleTest(unittest.TestCase): + def test_no_sides_are_equal(self): self.assertIs(scalene([5, 4, 6]), True) 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..7404128a882 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 2026-02-19 import unittest @@ -11,12 +11,16 @@ class VariableLengthQuantityTest(unittest.TestCase): + def test_zero(self): self.assertEqual(encode([0x0]), [0x0]) 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 +30,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 +42,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 +54,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 +66,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/config.json b/exercises/practice/wordy/.approaches/config.json index ed71a84650f..957fc960bfd 100644 --- a/exercises/practice/wordy/.approaches/config.json +++ b/exercises/practice/wordy/.approaches/config.json @@ -1,15 +1,64 @@ { "introduction": { - "authors": ["bobahop"], - "contributors": [] + "authors": ["BethanyG"], + "contributors": ["bobahop", "yrahcaz7"] }, "approaches": [ + { + "uuid": "4eeb0638-671a-4289-a83c-583b616dc698", + "slug": "string-list-and-dict-methods", + "title": "String, List, and Dictionary Methods", + "blurb": "Use Core Python Features to Solve Word Problems.", + "authors": ["BethanyG"], + "contributors": ["yrahcaz7"] + }, + { + "uuid": "d3ff485a-defe-42d9-b9c6-c38019221ffa", + "slug": "import-callables-from-operator", + "title": "Import Callables from the Operator Module", + "blurb": "Use Operator Module Methods to Solve Word Problems.", + "authors": ["BethanyG"], + "contributors": ["yrahcaz7"] + }, + { + "uuid": "61f44943-8a12-471b-ab15-d0d10fa4f72f", + "slug": "regex-with-operator-module", + "title": "Regex with the Operator Module", + "blurb": "Use Regex with the Callables from Operator to solve word problems.", + "authors": ["BethanyG"], + "contributors": ["yrahcaz7"] + }, + { + "uuid": "46bd15dd-cae4-4eb3-ac63-a8b631a508d1", + "slug": "lambdas-in-a-dictionary", + "title": "Lambdas in a Dictionary to Return Functions", + "blurb": "Use lambdas in a dictionary to return functions for solving word problems.", + "authors": ["BethanyG"], + "contributors": ["yrahcaz7"] + }, + { + "uuid": "2e643b88-9b76-45a1-98f4-b211919af061", + "slug": "recursion", + "title": "Recursion for Iteration", + "blurb": "Use recursion with other strategies to solve word problems.", + "authors": ["BethanyG"], + "contributors": ["yrahcaz7"] + }, + { + "uuid": "1e136304-959c-4ad1-bc4a-450d13e5f668", + "slug": "functools-reduce", + "title": "functools.reduce for Calculation", + "blurb": "Use functools.reduce with other strategies to calculate solutions.", + "authors": ["BethanyG"], + "contributors": ["yrahcaz7"] + }, { "uuid": "d643e2b4-daee-422d-b8d3-2cad2f439db5", "slug": "dunder-getattribute", - "title": "dunder with __getattribute__", + "title": "Dunder with __getattribute__", "blurb": "Use dunder methods with __getattribute__.", - "authors": ["bobahop"] + "authors": ["bobahop"], + "contributors": ["yrahcaz7"] } ] } diff --git a/exercises/practice/wordy/.approaches/dunder-getattribute/content.md b/exercises/practice/wordy/.approaches/dunder-getattribute/content.md index 76280615bc5..26652cc693f 100644 --- a/exercises/practice/wordy/.approaches/dunder-getattribute/content.md +++ b/exercises/practice/wordy/.approaches/dunder-getattribute/content.md @@ -11,92 +11,96 @@ 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 not question: + raise ValueError("syntax error") + + if question.startswith("-") and question[1:].isdigit(): + return -int(question[1:]) + if question.isdigit(): + return int(question) found_op = False for name, op in OPS.items(): if name in question: question = question.replace(name, op) found_op = True - if not found_op: raise ValueError("unknown operation") + if not found_op: + raise ValueError("unknown operation") ret = question.split() while len(ret) > 1: try: x, op, y, *tail = ret - if op not in OPS.values(): raise ValueError("syntax error") + if op not in OPS.values(): + raise ValueError("syntax error") ret = [int(x).__getattribute__(op)(int(y)), *tail] except: raise ValueError("syntax error") return ret[0] - ``` -This approach begins by defining a [dictionary][dictionaries] of the word keys with their related [dunder][dunder] methods. +This approach begins by defining a [dictionary][dictionaries] of the word keys with their related [dunder method][dunder] values. +Since only whole numbers are involved, the available dunder methods are those for the [`int`][int] class/namespace. +The supported methods for the `int()` namespace can be found by using `print(dir(int))` or `print(int.__dict__)` in a Python terminal. +See [this StackOverflow post][dir-vs-__dict__] for more details. -~~~~exercism/note -They are called "dunder" methods because they have **d**ouble **under**scores at the beginning and end of the method name. -They are also called magic methods. -~~~~ - -Since only whole numbers are involved, the dunder methods are those for [`int`][int]. -The supported methods for `int` can be found by using `print(dir(int))`. +
~~~~exercism/note -The built-in [`dir`](https://docs.python.org/3/library/functions.html?#dir) function returns a list of valid attributes for an object. +The built-in [`dir`](https://docs.python.org/3/library/functions.html?#dir) function returns a list of all valid attributes for an object. +The dunder method [`.__dict__`](https://docs.python.org/3/reference/datamodel.html#object.__dict__) is a mapping of an object's writable attributes. ~~~~ -Python doesn't _enforce_ having real constant values, -but the `OPS` dictionary 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 input question to the `answer` function is cleaned using the [`removeprefix`][removeprefix], [`removesuffix`][removesuffix], and [`strip`][strip] methods. -The method calls are [chained][method-chaining], so that the output from one call is the input for the next call. -If the input has no characters left, -it uses the [falsiness][falsiness] of an empty string with the [`not`][not] operator to return the [`ValueError`][value-error] for having a syntax error. +The `OPS` dictionary is defined with all uppercase letters, which is the naming convention for a Python [constant][const]. +It indicates that the value should not be changed. -Next, the [`isdigit`][isdigit] method is used to see if all of the remaining characters in the input are digits. -If so, it uses the [`int()`][int-constructor] constructor to return the string as an integer. +The input question to the `answer()` function is cleaned using the [`removeprefix`][removeprefix], [`removesuffix`][removesuffix], and [`strip`][strip] string methods. +The method calls are [chained][method-chaining], so the output from one call is the input for the next call. +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 elements in the `OPS` dictionary are iterated. -If the key name is in the input, then the [`replace()`][replace] method is used to replace the name in the input with the dunder method value. -If none of the key names are found in the input, then a `ValueError` is returned for having an unknown operation. +Next, the [`str.startswith()`][startswith] and [`str.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 used for the remaining string. +If the `str.isdigit()` checks pass, the [`int()` constructor][int-constructor] is used to return the string as an integer with the proper sign. -At this point the input question is [`split()`][split] into a list of its words, which is then iterated while its [`len()`][len] is greater than 1. +Next, the elements in the `OPS` dictionary are iterated over. +If the key name is in the input, the [`str.replace`][replace] method is used to replace the name in the input with the dunder method value. +If none of the key names are found in the input, a `ValueError("unknown operation")` is raised. -Within a [try][exception-handling], the list is [destructured][destructure] into `x, op, y, *tail`. -If `op` is not in the supported dunder methods, it raises `ValueError("syntax error")`. -If there are any other exceptions raised in the try, `except` raises `ValueError("syntax error")` +At this point, the input question is [`split()`][split] into a `list` of its words, which is then iterated over while its [`len()`][len] is greater than 1. -Next, it converts `x` to an `int` and calls the [`__getattribute__`][getattribute] for its dunder method and calls it, -passing it `y` converted to an `int`. +Within a [`try-except`][exception-handling] block, the list is [unpacked][unpacking] (_see also [concept:python/unpacking-and-multiple-assignment]()_) into the variables `x`, `op`, `y`, and `*tail`. +If `op` is not in the `OPS` dictionary, a `ValueError("syntax error")` is raised. -It sets the list to the result of the dunder method plus the remaining elements in `*tail`. +The `except` block will catch this error (or any other error raised inside the `try` block), and `raise` a `ValueError("syntax error")` instead. +(You can look at [exception chaining in the Python docs][exception-chaining] for further detail on this subject.) -~~~~exercism/note -The `*` prefix in `*tail` [unpacks](https://treyhunner.com/2018/10/asterisks-in-python-what-they-are-and-how-to-use-them/) the `tail` list back into its elements. -This concept is also a part of [unpacking-and-multiple-assignment](https://exercism.org/tracks/python/concepts/unpacking-and-multiple-assignment) concept in the syllabus. -~~~~ +Next, `x` is converted to an `int` and [`__getattribute__`][getattribute] is called for the dunder method (`op`) to apply to `x`. +`y` is then converted to an `int` and passed as the second argument to `op`. -When the loop exhausts, the first element of the list is selected as the function return value. +Then `ret` is redefined to a `list` containing the result of the dunder method plus the remaining elements in `*tail`. +When `ret` reaches `len() == 1` and the loop ends, the first element of `ret` is returned as the answer. + +[const]: https://realpython.com/python-constants/ [dictionaries]: https://docs.python.org/3/tutorial/datastructures.html#dictionaries +[dir-vs-__dict__]: https://stackoverflow.com/a/14361362 [dunder]: https://www.tutorialsteacher.com/python/magic-methods-in-python +[exception-chaining]: https://docs.python.org/3/tutorial/errors.html#exception-chaining +[exception-handling]: https://docs.python.org/3/tutorial/errors.html#handling-exceptions +[falsiness]: https://www.pythontutorial.net/python-basics/python-boolean/ +[getattribute]: https://docs.python.org/3/reference/datamodel.html?#object.__getattribute__ +[int-constructor]: https://docs.python.org/3/library/functions.html?#int [int]: https://docs.python.org/3/library/stdtypes.html#typesnumeric -[const]: https://realpython.com/python-constants/ -[removeprefix]: https://docs.python.org/3/library/stdtypes.html#str.removeprefix -[removesuffix]: https://docs.python.org/3/library/stdtypes.html#str.removesuffix -[strip]: https://docs.python.org/3/library/stdtypes.html#str.strip +[isdigit]: https://docs.python.org/3/library/stdtypes.html?#str.isdigit +[len]: https://docs.python.org/3/library/functions.html?#len [method-chaining]: https://www.tutorialspoint.com/Explain-Python-class-method-chaining [not]: https://docs.python.org/3/library/operator.html?#operator.__not__ -[falsiness]: https://www.pythontutorial.net/python-basics/python-boolean/ -[value-error]: https://docs.python.org/3/library/exceptions.html?#ValueError -[isdigit]: https://docs.python.org/3/library/stdtypes.html?#str.isdigit -[int-constructor]: https://docs.python.org/3/library/functions.html?#int +[removeprefix]: https://docs.python.org/3/library/stdtypes.html#str.removeprefix +[removesuffix]: https://docs.python.org/3/library/stdtypes.html#str.removesuffix [replace]: https://docs.python.org/3/library/stdtypes.html?#str.replace [split]: https://docs.python.org/3/library/stdtypes.html?#str.split -[len]: https://docs.python.org/3/library/functions.html?#len -[exception-handling]: https://docs.python.org/3/tutorial/errors.html#handling-exceptions -[destructure]: https://riptutorial.com/python/example/14981/destructuring-assignment -[getattribute]: https://docs.python.org/3/reference/datamodel.html?#object.__getattribute__ +[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/ diff --git a/exercises/practice/wordy/.approaches/dunder-getattribute/snippet.txt b/exercises/practice/wordy/.approaches/dunder-getattribute/snippet.txt index d3cc3d16701..7e648873b33 100644 --- a/exercises/practice/wordy/.approaches/dunder-getattribute/snippet.txt +++ b/exercises/practice/wordy/.approaches/dunder-getattribute/snippet.txt @@ -5,4 +5,4 @@ while len(ret) > 1: ret = [int(x).__getattribute__(op)(int(y)), *tail] except: raise ValueError("syntax error") -return ret[0] +return ret[0] \ No newline at end of file diff --git a/exercises/practice/wordy/.approaches/functools-reduce/content.md b/exercises/practice/wordy/.approaches/functools-reduce/content.md new file mode 100644 index 00000000000..01c03a54f7c --- /dev/null +++ b/exercises/practice/wordy/.approaches/functools-reduce/content.md @@ -0,0 +1,125 @@ +# `functools.reduce()` for Calculation + +```python +from operator import add, mul, sub +from operator import floordiv as div +from functools import reduce + + +# Define a lookup table for mathematical operations +OPERATORS = {"plus": add, "minus": sub, "multiplied": mul, "divided": div} + +def answer(question): + # Check for basic validity right away, and fail out with error if not valid. + if not question.startswith("What is") or "cubed" in question: + raise ValueError("unknown operation") + + # Use the built-in filter() to clean and split the question. + 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()) + else None for element in question[::2]] + + # If there is a mis-match between operators and numbers, throw an error. + if len(digits) - 1 != len(operations) or None in digits: + raise ValueError("syntax error") + + # Evaluate the expression from left to right using functools.reduce(). + # Look up each operation in the OPERATORS dictionary. + return reduce(lambda x, y: OPERATORS[operations.pop(0)](x, y), digits) +``` + +This approach replaces the `while-loop` or `recursion` used in many solutions with a call to [`functools.reduce`][functools-reduce]. +It requires that the question be separated into candidate digits and candidate operators, which is accomplished here via [list slicing][sequence-operations] (_for some additional information on working with `lists`, see [concept:python/lists]()_). + +A nested call to `filter()` and `split()` within a `list` constructor is used to clean and process the question into an initial `list` of digit and operator strings. +However, this could easily be accomplished by either using [chained][method-chaining] string methods or a list comprehension: + +```python + # Alternative 1 is chaining various string methods together. + # The wrapping () invoke implicit concatenation for the chained functions. + question = (question.removeprefix("What is") + .removesuffix("?") + .replace("by", "") + .strip()).split() # <-- This split() turns the string into a list. + + + # Alternative 2 to the nested calls is to use a list comprehension: + question = [item for item in + question.strip("?").split() + if item not in ("What", "is", "by")] # <-- The [] of the comprehension invokes implicit concatenation. +``` + +Since "valid" questions are all in the form of `digit-operator-digit` (_and so on_), it is safe to assume that every other element beginning at index 0 is a "number", and every other element beginning at index 1 is an operator. +By that definition, the `operators` list is 1 shorter in `len()` than the `digits` list. +Anything else (_or having `None`/an unknown operation in the operations list_) is a `ValueError("syntax error")`. + + +The final call to `functools.reduce` essentially performs the same steps as the `while-loop` implementation, with the `lambda-expression` passing successive items of the `digits` list to the popped and looked-up operation from the operations `list` (_used as a [callable][callable] with `()`_), until it is reduced to one number and returned. +A `try-except` is not needed here because the error scenarios are already filtered out in the `if` check right before the call to `reduce()`. + +`functools.reduce` is certainly convenient, and it makes the solution much shorter. +However, it is also hard to understand what is happening if you have not worked with a `reduce` or `foldl` function in the past. +It could be argued that writing the code as a `while-loop` or recursive function is easier to reason about for non-functional programmers. + +
+ +## Variation 1: Use a dictionary of `lambdas` instead of importing from `operator` + +The imports from the `operator` module can be swapped out for a dictionary of `lambda-expressions` (or calls to `dunder-methods`), if so desired. +The same cautions apply here as were discussed in the [lambdas in a dictionary][approach-lambdas-in-a-dictionary] approach: + +```python +from functools import reduce + +# Define a lookup table for mathematical operations +OPERATORS = { + "plus": lambda x, y: x + y, + "minus": lambda x, y: x - y, + "multiplied": lambda x, y: x * y, + "divided": lambda x, y: x / y +} + +def answer(question): + # Check for basic validity right away, and fail out with error if not valid. + if not question.startswith("What is") or "cubed" in question: + raise ValueError("unknown operation") + + # Clean and split the question into a list for processing. + question = [item for item in + question.strip("?").split() if + item not in ("What", "is", "by")] + + # 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()) + else None for element in question[::2]] + + # If there is a mis-match between operators and numbers, toss error. + if len(digits)-1 != len(operations) or None in digits: + raise ValueError("syntax error") + + # Evaluate the expression from left to right using functools.reduce(). + # Look up each operation in the OPERATORS dictionary. + result = reduce(lambda x, y: OPERATORS[operations.pop(0)](x, y), digits) + + return result +``` + +[approach-lambdas-in-a-dictionary]: https://exercism.org/tracks/python/exercises/wordy/approaches/lambdas-in-a-dictionary +[callable]: https://treyhunner.com/2019/04/is-it-a-class-or-a-function-its-a-callable/ +[functools-reduce]: https://docs.python.org/3/library/functools.html#functools.reduce +[method-chaining]: https://www.tutorialspoint.com/Explain-Python-class-method-chaining +[sequence-operations]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations diff --git a/exercises/practice/wordy/.approaches/functools-reduce/snippet.txt b/exercises/practice/wordy/.approaches/functools-reduce/snippet.txt new file mode 100644 index 00000000000..2c1cb3afbd6 --- /dev/null +++ b/exercises/practice/wordy/.approaches/functools-reduce/snippet.txt @@ -0,0 +1,7 @@ +OPERATORS = {"plus": add, "minus": sub, "multiplied": mul, "divided": div} +... +operations = question[1::2] +digits = [int(element) if (element.isdigit() or element[1:].isdigit()) + else None for element in question[::2]] +... +return reduce(lambda x, y: OPERATORS[operations.pop(0)](x, y), digits) \ No newline at end of file diff --git a/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md b/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md new file mode 100644 index 00000000000..618b7daf198 --- /dev/null +++ b/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md @@ -0,0 +1,91 @@ +# Import Callables from the `operator` Module + +```python +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 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 + equation = [OPERATIONS[operation](int(x_value), int(y_value)), + *rest] + except: + raise ValueError("syntax error") + + return equation[0] +``` + +This approach is nearly identical to the [string, list, and dict methods][approach-string-list-and-dict-methods] approach, so it is recommended to review that before going over this one. +The two major differences are the `operator` module, and the elimination of the `if-elif-else` block. + + +The solution begins by importing basic mathematical operations as methods from the [`operator`][operator] module. +`add`, `mul` and `sub` keep their original names, while `floordiv` is [aliased][aliasing] to `div`. +These functions are then stored in a dictionary that serves as a lookup table when the problems are processed. +These operations are later used as [callables][callable] by putting `()` after the name, and supplying arguments between the parentheses. + + +In `answer()`, the question is first checked for validity, cleaned, and finally split into a `list` using [`str.startswith`][startswith], [`str.removeprefix`][removeprefix]/[`str.removesuffix`][removesuffix], [`str.strip()`][strip], and [`str.split()`][split]. +Next, checks for digits and an empty string are done, and the word "by" is filtered from the equation `list` by using a [list comprehension][list-comprehension]. + + +The equation `list` is then processed in a `while-loop` within a [`try-except`][handling-exceptions] block. +The `list` is [unpacked][unpacking] (_see also [concept:python/unpacking-and-multiple-assignment]()_) into `x_value`, `operation`, `y_value`, and `*rest`, and reduced by looking up and calling the mathematical function in the `OPERATIONS` dictionary and passing in `int(x_value)` and `int(y_value)` as arguments. + + +The processing of the equation `list` continues until its `len() == 1`, at which point the single element is returned as the answer. + + +To walk through this step-by-step, you can interact with this code on [`pythontutor.com`][pythontutor]. + + +Using a list comprehension to filter out "by" can be replaced with the [`str.replace`][str-replace] method during question cleaning. +[Implicit concatenation][implicit-concatenation] can be used to improve the readability of the [chained][chaining-method-calls] method calls: + +```python +question = (question.removeprefix("What is") + .removesuffix("?") + .replace("by", "") + .strip()) # <-- Enclosing parentheses means these lines are automatically joined by the interpreter. +``` + +The call to `str.replace` could instead be chained with the call to `str.split` when creating the equation `list`: + +```python +equation = question.replace("by", "").split() +``` + +[aliasing]: https://mimo.org/glossary/python +[approach-string-list-and-dict-methods]: https://exercism.org/tracks/python/exercises/wordy/approaches/string-list-and-dict-methods +[callable]: https://treyhunner.com/2019/04/is-it-a-class-or-a-function-its-a-callable/ +[chaining-method-calls]: https://nikhilakki.in/understanding-method-chaining-in-python +[handling-exceptions]: https://docs.python.org/3.11/tutorial/errors.html#handling-exceptions +[implicit-concatenation]: https://docs.python.org/3/reference/lexical_analysis.html#implicit-line-joining +[list-comprehension]: https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions +[operator]: https://docs.python.org/3/library/operator.html#module-operator +[pythontutor]: https://pythontutor.com/visualize.html#code=from%20operator%20import%20add,%20mul,%20sub%0Afrom%20operator%20import%20floordiv%20as%20div%0A%0AOPERATIONS%20%3D%20%7B%22plus%22%3A%20add,%20%22minus%22%3A%20sub,%20%22multiplied%22%3A%20mul,%20%22divided%22%3A%20div%7D%0A%0Adef%20answer%28question%29%3A%0A%20%20%20%20if%20not%20question.startswith%28%22What%20is%22%29%20or%20%22cubed%22%20in%20question%3A%0A%20%20%20%20%20%20%20%20raise%20ValueError%28%22unknown%20operation%22%29%0A%20%20%20%20%0A%20%20%20%20question%20%3D%20question.removeprefix%28%22What%20is%22%29.removesuffix%28%22%3F%22%29.strip%28%29%0A%0A%20%20%20%20if%20question.isdigit%28%29%3A%20%0A%20%20%20%20%20%20%20%20return%20int%28question%29%0A%20%20%20%20%0A%20%20%20%20if%20not%20question%3A%20%0A%20%20%20%20%20%20%20%20raise%20ValueError%28%22syntax%20error%22%29%0A%20%20%20%20%0A%20%20%20%20equation%20%3D%20%5Bword%20for%20word%20in%20question.split%28%29%20if%20word%20!%3D%20%22by%22%5D%0A%20%20%20%20%0A%20%20%20%20while%20len%28equation%29%20%3E%201%3A%0A%20%20%20%20%20%20%20%20try%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20x_value,%20operation,%20y_value,%20*rest%20%3D%20equation%0A%20%20%20%20%20%20%20%20%20%20%20%20equation%20%3D%20%5BOPERATIONS%5Boperation%5D%28int%28x_value%29,%20int%28y_value%29%29,%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20*rest%5D%0A%20%20%20%20%20%20%20%20except%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20raise%20ValueError%28%22syntax%20error%22%29%0A%20%20%20%20%0A%20%20%20%20return%20equation%5B0%5D%0A%20%20%20%20%0Aprint%28answer%28%22What%20is%202%20plus%202%20plus%203%3F%22%29%29&curInstr=0&mode=display&origin=opt-frontend.js&py=311 +[removeprefix]: https://docs.python.org/3.9/library/stdtypes.html#str.removeprefix +[removesuffix]: https://docs.python.org/3.9/library/stdtypes.html#str.removesuffix +[split]: https://docs.python.org/3.9/library/stdtypes.html#str.split +[startswith]: https://docs.python.org/3.9/library/stdtypes.html#str.startswith +[str-replace]: https://docs.python.org/3/library/stdtypes.html#str.replace +[strip]: https://docs.python.org/3.9/library/stdtypes.html#str.strip +[unpacking]: https://treyhunner.com/2018/10/asterisks-in-python-what-they-are-and-how-to-use-them/ \ No newline at end of file diff --git a/exercises/practice/wordy/.approaches/import-callables-from-operator/snippet.txt b/exercises/practice/wordy/.approaches/import-callables-from-operator/snippet.txt new file mode 100644 index 00000000000..de87954eaef --- /dev/null +++ b/exercises/practice/wordy/.approaches/import-callables-from-operator/snippet.txt @@ -0,0 +1,8 @@ +OPERATIONS = {"plus": add, "minus": sub, "multiplied": mul, "divided": div} +while len(equation) > 1: + try: + x_value, operation, y_value, *rest = equation + equation = [OPERATIONS[operation](int(x_value), int(y_value)), *rest] + except: + raise ValueError("syntax error") +return equation[0] \ No newline at end of file diff --git a/exercises/practice/wordy/.approaches/introduction.md b/exercises/practice/wordy/.approaches/introduction.md index 9faba81cbcd..13834278857 100644 --- a/exercises/practice/wordy/.approaches/introduction.md +++ b/exercises/practice/wordy/.approaches/introduction.md @@ -1,19 +1,395 @@ # Introduction -There are various ways to solve Wordy. -Using [`eval`][eval] is a [convenient but potentially dangerous][eval-danger] approach. -Another approach could replace the operation words with [dunder][dunder] methods. +The objective of the Wordy exercise is to parse and evaluate small/simple mathematical word problems, returning the result as an integer. +These questions do not require complex or [PEMDAS][PEMDAS]-based evaluation and are instead processed from left-to-right _in sequence_. +This means that for some of the test cases, the solution will not be the same as if the word problem was evaluated like a traditional math problem. + +
+ +## General Guidance + +The key to a Wordy solution is to remove the "question" portion of the sentence (_"What is", "?"_) and process the remaining words between numbers as [operators][mathematical operators]. +If a single number remains after removing the "question" pieces, it should be converted to an [`int`][int] and returned as the answer. + +Any words or word-number combinations that do not fall into the simple mathematical evaluation pattern (_number-operator-number_) should [`raise`][raise-statement] a [`ValueError`][value-error] with a message. +This includes any "extra" spaces between numbers. +As shown in various approaches, there are multiple strategies for validating questions, with no one "canonical" solution. + +A whole class of error can be eliminated up front by checking if a question starts with "What is", ends with "?", and does not include the word "cubed". +Any other question formulation becomes a `ValueError("unknown operation")`. +This could lead to future maintenance issues if the definition of a question ever changes or operations are added, but for the purposes of passing the current Wordy tests, it works well. + +
~~~~exercism/note -They are called "dunder" methods because they have **d**ouble **under**scores at the beginning and end of the method name. -They are also called magic methods. +There are many Pythonic ways to go about the cleaning, parsing, and calculation steps of Wordy. +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._ +3. **_Starting from the left_**, take the first three elements and convert number strings to `int`s and operation 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._ +5. Use the calculated number from step 4 as the start for the next "trio" (_number, operation, number_) in the question. The calculated number plus 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._ +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")`. +~~~~ + +
+ +For question cleaning, [`str.removeprefix`][removeprefix] and +[`str.removesuffix`][removesuffix] introduced in `Python 3.9` can be very useful: + +```python +>>> "Supercalifragilisticexpialidocious".removeprefix("Super") +'califragilisticexpialidocious' + +>>> "Supercalifragilisticexpialidocious".removesuffix("expialidocious") +'Supercalifragilistic' + + +# 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' +``` + +You can also use [`str.startswith`][startswith] and [`str.endswith`][endswith] in conjunction with [string slicing][sequence-operations] for cleaning: + +```python +>>> if "Supercalifragilisticexpialidocious".startswith("Super"): + new_string = "Supercalifragilisticexpialidocious"[5:] +>>> new_string +'califragilisticexpialidocious' + + +>>> if new_string.endswith("expialidocious"): + new_string = new_string[:15] +>>> new_string +'califragilistic' +``` + +Different combinations of [`str.find`][find], [`str.rfind`][rfind], or [`str.index`][index] with string slicing could also be used to clean up the initial question. +A [regex][regex] could be used to process the question as well, but might be considered overkill given the fixed nature of the prefix/suffix and operations. +Finally, [`str.strip`][strip] and its variants are very useful for cleaning up any leftover leading or trailing whitespace. + +Many solutions then use [`str.split`][split] to process the remaining "cleaned" question into a `list` for convenient looping/iteration, although other strategies can also be used: + +```python +>>> sentence = "The quick brown fox jumped over the lazy dog 10 times" +>>> sentence.split() +['The', 'quick', 'brown', 'fox', 'jumped', 'over', 'the', 'lazy', 'dog', '10', 'times'] +``` + +For math operations, many solutions involve importing and using methods from the [operator][operator] module. +Some solutions use either [lambda][lambdas] expressions, [dunder/"special" methods][dunder-methods], or even `eval()` to replace words with arithmetic operations. + +However, the exercise can be solved without using `operator`, `lambdas`, `dunder-methods` or `eval`. +It is recommended that you first start by solving it _without_ "advanced" strategies, and then refine your solution into something more compact or complex as you learn and practice. + +
+ +~~~~exercism/caution +Using [`eval`][eval] for the operations might seem convenient, but it is a [dangerous][eval-danger] and possibly [destructive][eval-destructive] approach. +It is also entirely unnecessary, as the other methods described here are safer and equally performant. + +[eval-danger]: https://softwareengineering.stackexchange.com/questions/311507/why-are-eval-like-features-considered-evil-in-contrast-to-other-possibly-harmfu +[eval-destructive]: https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html +[eval]: https://docs.python.org/3/library/functions.html?#eval ~~~~ -The dunder methods can be called by using the [`__getattribute__`][getattribute] method for [`int`][int]. +
+ +_____________ + +## Approach: String, List, and Dictionary Methods + +```python +def answer(question): + if not question.startswith("What is") or "cubed" in question: + raise ValueError("unknown operation") + + question = question.removeprefix("What is") + question = question.removesuffix("?") + question = question.replace("by", "") + question = question.strip() + + if not question: + raise ValueError("syntax error") + + formula = question.split() + while len(formula) > 1: + try: + x_value = int(formula[0]) + y_value = int(formula[2]) + symbol = formula[1] + remainder = formula[3:] + + if symbol == "plus": + formula = [x_value + y_value] + remainder + elif symbol == "minus": + formula = [x_value - y_value] + remainder + elif symbol == "multiplied": + formula = [x_value * y_value] + remainder + elif symbol == "divided": + formula = [x_value / y_value] + remainder + else: + raise ValueError("syntax error") + except: + raise ValueError("syntax error") + + return int(formula[0]) +``` + +This approach uses only data structures and methods (_[`str` methods][str-methods], [`list()`][list], loOPERATORS, etc._) from core Python, and does not import any extra modules. +It may have more lines of code than average, but it is clear to follow and fairly straightforward to reason about. + +This approach uses a [`try-except`][handling-exceptions] statement for handling unknown operators. +It does this by raising an error inside the `try` block when `symbol` does not match any operator word. +The `except` block will catch this error (or any other error raised inside the `try` block), and `raise` a `ValueError("syntax error")` instead. +(You can look at [exception chaining in the Python docs][exception-chaining] for further detail on this subject.) + +Alternatives could use a [dictionary][dict] to store word to operator mappings that could be looked up in the `while-loop` using [`.get()`][dict-get], among other strategies. + +For more details and variations, read the [String, List and Dictionary Methods][approach-string-list-and-dict-methods] approach. + +
+ +## Approach: Import Callables from the `operator` Module + +```python +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.startswith("-") and question[1:].isdigit()) or question.isdigit(): + return int(question) + + if not question: + raise ValueError("syntax error") + + equation = [word for word in question.split() if word != "by"] + + while len(equation) > 1: + try: + x_value, operation, y_value, *rest = equation + equation = [OPERATIONS[operation](int(x_value), int(y_value)), + *rest] + except: + raise ValueError("syntax error") + + return equation[0] +``` + +This solution imports methods from the `operator` module, and uses them in a dictionary/lookup map. +Like the first approach, it uses a [`try-except`][handling-exceptions] block for handling unknown operators. +It also uses a [list comprehension][list-comprehension] to create the parsed "formula" and employs [concept:python/unpacking-and-multiple-assignment](). + +For more details and options, take a look at the [Import Callables from the `operator` Module][approach-import-callables-from-operator] approach. + +
+ +## Approach: Regex with the `operator` Module + +```python +import re +from operator import add, mul, sub +from operator import floordiv as div + +OPERATIONS = {"plus": add, "minus": sub, "multiplied by": mul, "divided by": div} -## General guidance +REGEX = { + "number": re.compile(r"-?\d+"), + "operator": re.compile(f"(?:{'|'.join(OPERATIONS)})\\b") +} + + +def get_number(question): + pattern = REGEX["number"].match(question) + if not pattern: + raise ValueError("syntax error") + return [question.removeprefix(pattern.group(0)).lstrip(), + int(pattern.group(0))] + +def get_operation(question): + pattern = REGEX["operator"].match(question) + if not pattern: + raise ValueError("unknown operation") + return [question.removeprefix(pattern.group(0)).lstrip(), + OPERATIONS[pattern.group(0)]] + +def answer(question): + prefix = "What is" + if not question.startswith(prefix): + raise ValueError("unknown operation") + + question = question.removesuffix("?").removeprefix(prefix).lstrip() + question, result = get_number(question) + + while len(question) > 0: + if REGEX["number"].match(question): + raise ValueError("syntax error") + + question, operation = get_operation(question) + question, num = get_number(question) + + result = operation(result, num) + + return result +``` + +This approach uses a dictionary of regex patterns for matching numbers and operators, paired with a dictionary of operations imported from the `operator` module. +It pulls number and operator processing out into separate functions and uses a while loop in `answer()` to evaluate the word problem. +It also uses multiple assignment for various variables. +It is longer than some solutions, but clearer and potentially easier to maintain due to the separate `get_operation()` and `get_number()` functions. + +For more details, take a look at the [Regex with the `operator` Module][approach-regex-with-operator-module] approach. + +
+ +## Approach: Lambdas in a Dictionary to Return Functions + +```python +OPERATIONS = { + "minus": lambda a, b: a - b, + "plus": lambda a, b: a + b, + "multiplied": lambda a, b: a * b, + "divided": lambda a, b: a / b +} + + +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.startswith("-") and question[1:].isdigit()) or question.isdigit(): + return int(question) + + if not question: + raise ValueError("syntax error") + + equation = [word for word in question.split() if word != "by"] + + while len(equation) > 1: + try: + x_value, operation, y_value, *rest = equation + equation = [OPERATIONS[operation](int(x_value), int(y_value)), + *rest] + except: + raise ValueError("syntax error") + + return equation[0] +``` + +Rather than import methods from the `operator` module, this approach defines a series of [`lambda expressions`][lambdas] in the `OPERATIONS` dictionary. +These `lambdas` then return a function that takes two numbers as arguments, returning the result. + +One drawback of this strategy over using named functions or methods from `operator` is the lack of debugging information should something go wrong with evaluation. +Lambda expressions are all named `"lambda"` in stack traces, so it becomes less clear where an error is coming from if you have a number of lambda expressions within a large program. +Since this is not a large program, debugging these `lambdas` is fairly straightforward. +These "hand-crafted" `lambdas` could also introduce a mathematical error, although for the simple problems in Wordy, this is a fairly small consideration. + +For more details, take a look at the [Lambdas in a Dictionary][approach-lambdas-in-a-dictionary] approach. + +
+ +## Approach: Recursion + +```python +from operator import add, mul, sub +from operator import floordiv as div -Parsing should verify that the expression in words can be translated to a valid mathematical expression. +OPERATIONS = {"plus": add, "minus": sub, "multiplied": mul, "divided": div} + +def answer(question): + return calculate(clean(question)) + +def clean(question): + if not question.startswith("What is") or "cubed" in question: + raise ValueError("unknown operation") + + return (question.removeprefix("What is") + .removesuffix("?") + .replace("by", "") + .strip()).split() + +def calculate(equation): + if len(equation) == 1: + return int(equation[0]) + + try: + x_value, operation, y_value, *rest = equation + equation = [OPERATIONS[operation](int(x_value), + int(y_value)), *rest] + except: + raise ValueError("syntax error") + + return calculate(equation) +``` + +Like previous approaches that substitute methods from `operator` for `lambdas` or list comprehensions for `loOPERATORS` that append to a `list` โ€” `recursion` can be substituted for the `while-loop` that many solutions use to process a parsed word problem. +Depending on who is reading the code, `recursion` may or may not be easier to reason about. +It may also be more (_or less!_) performant than using a `while-loop` or `functools.reduce` (_see below_), depending on how the various cleaning and error-checking actions are performed. + +The dictionary in this example could use functions from `operator`, `lambdas`, `dunder-methods`, or other strategies โ€” as long as they can be applied in the `calculate()` function. + +For more details, take a look at the [Recursion for Iteration][approach-recursion] approach. + +
+ +## Approach: `functools.reduce()` + +```python +from operator import add, mul, sub +from operator import floordiv as div +from functools import reduce + + +OPERATORS = {"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 = list(filter(lambda x: + x not in ("What", "is", "by"), + question.strip("?").split())) + + operations = question[1::2] + digits = [int(element) if (element.isdigit() or + element[1:].isdigit()) else None for + element in question[::2]] + + if len(digits)-1 != len(operations) or None in digits: + raise ValueError("syntax error") + + result = reduce(lambda x, y: OPERATORS[operations.pop(0)](x, y), digits) + + return result +``` + +This approach replaces the `while-loop` used in many solutions (_or the `recursion` strategy outlined in the approach above_) with a call to [`functools.reduce`][functools-reduce]. +It also employs a lookup dictionary for methods imported from the `operator` module, as well as a list comprehension, the built-in [`filter`][filter] function, and multiple string [slices][sequence-operations]. +If desired, the `operator` imports can be replaced with a dictionary of `lambda` expressions or `dunder-methods`. + +This solution may be a little less clear to follow or reason about due to the slicing syntax and the particular syntax of both `filter` and `fuctools.reduce`. + +For more details and variations, take a look at the [`functools.reduce` for Calculation][approach-functools-reduce] approach. + +
## Approach: Dunder methods with `__getattribute__` @@ -28,33 +404,79 @@ 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 not question: + raise ValueError("syntax error") + + if question.startswith("-") and question[1:].isdigit(): + return -int(question[1:]) + if question.isdigit(): + return int(question) found_op = False for name, op in OPS.items(): if name in question: question = question.replace(name, op) found_op = True - if not found_op: raise ValueError("unknown operation") + if not found_op: + raise ValueError("unknown operation") ret = question.split() while len(ret) > 1: try: x, op, y, *tail = ret - if op not in OPS.values(): raise ValueError("syntax error") + if op not in OPS.values(): + raise ValueError("syntax error") ret = [int(x).__getattribute__(op)(int(y)), *tail] except: raise ValueError("syntax error") return ret[0] - ``` -For more information, check the [dunder method with `__getattribute__` approach][approach-dunder-getattribute]. +This approach uses the [dunder methods][dunder-methods] / ["special methods"][special-methods] / "magic methods" associated with the `int` class, using the dunder method called [`.__getattribute__`][getattribute] to find the [callable][callable] operation in the `int` class [namespace][namespace] / dictionary. +This works because the operators for basic math (_`+`, `-`, `*`, `/`, `//`, `%`, `**`_) have been implemented as callable methods for all integers (_as well as floats and other numeric types_) and are automatically loaded when the Python interpreter is loaded. -[eval]: https://docs.python.org/3/library/functions.html?#eval -[eval-danger]: https://diveintopython3.net/advanced-iterators.html#eval -[dunder]: https://www.tutorialsteacher.com/python/magic-methods-in-python -[getattribute]: https://docs.python.org/3/reference/datamodel.html?#object.__getattribute__ -[int]: https://docs.python.org/3/library/stdtypes.html#typesnumeric +As described in the first link, it is considered bad form to directly call a dunder method (_there are some exceptions_), as they are intended mostly for internal Python use, user-defined class customization, and operator overloading (_a specific form of class-customization_). + +This is why the `operator` module exists โ€” It is a vehicle for providing callable methods for basic math when **not** overloading or customizing class functionality. + +For more detail on this solution, take a look at the [dunder methods with `__getattribute__` approach][approach-dunder-getattribute]. + +[PEMDAS]: https://www.mathnasium.com/blog/what-is-pemdas [approach-dunder-getattribute]: https://exercism.org/tracks/python/exercises/wordy/approaches/dunder-getattribute +[approach-functools-reduce]: https://exercism.org/tracks/python/exercises/wordy/approaches/functools-reduce +[approach-import-callables-from-operator]: https://exercism.org/tracks/python/exercises/wordy/approaches/import-callables-from-operator +[approach-lambdas-in-a-dictionary]: https://exercism.org/tracks/python/exercises/wordy/approaches/lambdas-in-a-dictionary +[approach-recursion]: https://exercism.org/tracks/python/exercises/wordy/approaches/recursion +[approach-regex-with-operator-module]: https://exercism.org/tracks/python/exercises/wordy/approaches/regex-with-operator-module +[approach-string-list-and-dict-methods]: https://exercism.org/tracks/python/exercises/wordy/approaches/string-list-and-dict-methods +[callable]: https://treyhunner.com/2019/04/is-it-a-class-or-a-function-its-a-callable/ +[dict-get]: https://docs.python.org/3/library/stdtypes.html#dict.get +[dict]: https://docs.python.org/3/library/stdtypes.html#dict +[dunder-methods]: https://www.pythonmorsels.com/what-are-dunder-methods/ +[endswith]: https://docs.python.org/3.9/library/stdtypes.html#str.endswith +[exception-chaining]: https://docs.python.org/3/tutorial/errors.html#exception-chaining +[filter]: https://docs.python.org/3/library/functions.html#filter +[find]: https://docs.python.org/3.9/library/stdtypes.html#str.find +[functools-reduce]: https://docs.python.org/3/library/functools.html#functools.reduce +[getattribute]: https://docs.python.org/3/reference/datamodel.html#object.__getattribute__ +[handling-exceptions]: https://docs.python.org/3.11/tutorial/errors.html#handling-exceptions +[index]: https://docs.python.org/3.9/library/stdtypes.html#str.index +[int]: https://docs.python.org/3/library/stdtypes.html#typesnumeric +[lambdas]: https://docs.python.org/3/howto/functional.html#small-functions-and-the-lambda-expression +[list-comprehension]: https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions +[list]: https://docs.python.org/3/library/stdtypes.html#list +[mathematical operators]: https://www.w3schools.com/python/gloss_python_arithmetic_operators.asp +[namespace]: https://docs.python.org/3/tutorial/classes.html#python-scopes-and-namespaces +[operator]: https://docs.python.org/3/library/operator.html#module-operator +[raise-statement]: https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement +[regex]: https://docs.python.org/3/library/re.html#module-re +[removeprefix]: https://docs.python.org/3.9/library/stdtypes.html#str.removeprefix +[removesuffix]: https://docs.python.org/3.9/library/stdtypes.html#str.removesuffix +[rfind]: https://docs.python.org/3.9/library/stdtypes.html#str.rfind +[sequence-operations]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations +[special-methods]: https://docs.python.org/3/reference/datamodel.html#specialnames +[split]: https://docs.python.org/3.9/library/stdtypes.html#str.split +[startswith]: https://docs.python.org/3.9/library/stdtypes.html#str.startswith +[strip]: https://docs.python.org/3.9/library/stdtypes.html#str.strip +[str-methods]: https://docs.python.org/3/library/stdtypes.html#string-methods +[value-error]: https://docs.python.org/3.11/library/exceptions.html#ValueError diff --git a/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/content.md b/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/content.md new file mode 100644 index 00000000000..75f5da8a260 --- /dev/null +++ b/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/content.md @@ -0,0 +1,100 @@ +# Lambdas in a Dictionary to Return Functions + +```python +OPERATIONS = { + "minus": lambda a, b: a - b, + "plus": lambda a, b: a + b, + "multiplied": lambda a, b: a * b, + "divided": lambda a, b: a / b +} + + +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.startswith("-") and question[1:].isdigit()) or question.isdigit(): + return int(question) + + if not question: + raise ValueError("syntax error") + + equation = question.replace("by", "").split() + + while len(equation) > 1: + try: + x_value, operation, y_value, *rest = equation + equation = [OPERATIONS[operation](int(x_value), int(y_value)), + *rest] + except: + raise ValueError("syntax error") + + return equation[0] +``` + +This approach is nearly identical to the [string, list, and dict methods][approach-string-list-and-dict-methods] and the [import callables from the `operator` module][approach-import-callables-from-operator] approaches, so it is recommended that you review those before going over this one. +The major difference here is the use of [`lambda expressions`][lambdas] in place of `operator` methods or string representations in the `OPERATIONS` dictionary. + +`lambda expressions` are small "throwaway" expressions that are simple enough to not require a formal function definition or name. +They are most commonly used in [`key functions`][key-functions], the built-ins [`map`][map] and [`filter`][filter], and in [`functools.reduce`][functools-reduce]. +`lambdas` are also often defined in areas where a function is needed for one-time use or callback but it would be onerous or confusing to create a full function definition. +The two forms are parsed identically (_they are both function definitions_), but in the case of [`lambdas`][lambda], the function name is always "lambda" and the expression cannot contain statements or annotations. + +For example, the code above could be re-written to include user-defined functions as opposed to `lambda expressions`: + +```python +def _add(x, y): + return x + y + +def _mul(x, y): + return x * y + +def _div(x, y): + return x // y + +def _sub(x, y): + return x - y + +def answer(question): + operations = {"minus": _sub, "plus": _add, "multiplied": _mul, "divided": _div} + + if not question.startswith("What is") or "cubed" in question: + raise ValueError("unknown operation") + + question = question.removeprefix("What is").removesuffix("?").strip() + + if (question.startswith("-") and question[1:].isdigit()) or question.isdigit(): + return int(question) + + if not question: + raise ValueError("syntax error") + + equation = question.replace("by", "").split() + + while len(equation) > 1: + try: + x_value, operation, y_value, *rest = equation + equation = [operations[operation](int(x_value), int(y_value)), + *rest] + except: + raise ValueError("syntax error") + + return equation[0] +``` + +However, this makes the code more verbose and does not improve readability. +In addition, the functions need to have a leading underscore to indicate that they are internal functions for the module, which helps avoid potential shadowing or name conflict (see [PEP 8][pep-8-naming-styles] for more detail). + +It is better and cleaner in this circumstance to use `lambda expressions` for the functions โ€” although it could be argued that importing and using the methods from `operator` is even better and clearer. + +[approach-import-callables-from-operator]: https://exercism.org/tracks/python/exercises/wordy/approaches/import-callables-from-operator +[approach-string-list-and-dict-methods]: https://exercism.org/tracks/python/exercises/wordy/approaches/string-list-and-dict-methods +[filter]: https://docs.python.org/3/library/functions.html#filter +[functools-reduce]: https://docs.python.org/3/library/functools.html#functools.reduce +[key-functions]: https://docs.python.org/3/howto/sorting.html#key-functions +[lambda]: https://docs.python.org/3/reference/expressions.html#lambda +[lambdas]: https://docs.python.org/3/howto/functional.html#small-functions-and-the-lambda-expression +[map]: https://docs.python.org/3/library/functions.html#map +[pep-8-naming-styles]: https://peps.python.org/pep-0008/#descriptive-naming-styles diff --git a/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/snippet.txt b/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/snippet.txt new file mode 100644 index 00000000000..1364338e161 --- /dev/null +++ b/exercises/practice/wordy/.approaches/lambdas-in-a-dictionary/snippet.txt @@ -0,0 +1,6 @@ +OPERATIONS = { + "minus": lambda a, b: a - b, + "plus": lambda a, b: a + b, + "multiplied": lambda a, b: a * b, + "divided": lambda a, b: a / b +} \ No newline at end of file diff --git a/exercises/practice/wordy/.approaches/recursion/content.md b/exercises/practice/wordy/.approaches/recursion/content.md new file mode 100644 index 00000000000..4cad29778d4 --- /dev/null +++ b/exercises/practice/wordy/.approaches/recursion/content.md @@ -0,0 +1,285 @@ +# Recursion for Iteration + +[Any function that can be written iteratively (_with loops_) can be written using recursion][recursion-and-iteration], and [vice-versa][recursion-is-not-a-superpower]. +A recursive strategy [may not always be obvious][looping-vs-recursion] or easy โ€” but it is always possible. +So the `while-loop`s used in other approaches to Wordy can be re-written to use recursive calls. + +
+ +That being said, Python famously does not perform [tail-call optimization][tail-call-optimization], and limits recursive calls on the stack to a depth of 1000 frames, so it is important to only use recursion where you are confident that it can complete within the limit (_or something close to it_). +[Memoization][memoization] and other strategies in [dynamic programming][dynamic-programming] can help to make recursion more efficient and "shorter" in Python, but it's always good to give it careful consideration. + +
+ +Recursion works best with problem spaces that resemble trees, include [backtracking][backtracking], or become progressively smaller. +Some examples include financial processes like calculating [amortization][amortization] and [depreciation][depreciation], tracking [radiation reduction through nuclei decay][nuclei-decay], and algorithms like [biscetion search][bisection-search], [depth-first search][dfs], and [merge sort][merge-sort]. + +
+ +Other algorithms such as [breadth-first search][bfs], [Dijkstra's algorithm][dijkstra], and the [Bellman-Ford Algorithm][bellman-ford] lend themselves better to loops. + +
+ +```python +from operator import add, mul, sub +from operator import floordiv as div + +# Define a lookup table for mathematical operations. +OPERATIONS = {"plus": add, "minus": sub, "multiplied": mul, "divided": div} + + +def answer(question): + # Call clean() and feed it to calculate(). + return calculate(clean(question)) + +def clean(question): + # It's not a question unless it starts with "What is". + if not question.startswith("What is") or "cubed" in question: + raise ValueError("unknown operation") + + # Remove the unnecessary parts of the question and + # parse the cleaned question into a list of items to process. + # The wrapping () invoke implicit concatenation for the chained functions. + return (question.removeprefix("What is") + .removesuffix("?") + .replace("by", "") + .strip()).split() # <-- This split() turns the string into a list. + +# Recursively calculate the first piece of the equation, +# calling calculate() on the product plus the remainder. +# Return the solution when len(equation) is one. +def calculate(equation): + if len(equation) == 1: + return int(equation[0]) + + try: + # Unpack the equation into first int, operator, and second int. + # Stuff the remainder into *rest. + x_value, operation, y_value, *rest = equation + + # Redefine the equation list as the product of the first three + # variables concatenated with the unpacked remainder. + equation = [OPERATIONS[operation](int(x_value), + int(y_value)), *rest] + except: + raise ValueError("syntax error") + + # Call calculate() with the redefined/partially reduced equation. + return calculate(equation) +``` + +This approach separates the solution into three functions: + +1. `answer()`, which takes the question and calls `calculate(clean())`, returning the answer to the question. +2. `clean()`, which takes a question string and returns a `list` of parsed words and numbers to calculate from. +3. `calculate()`, which performs the calculations on the `list` recursively, until a single number (_the [base case][base-case] check_) is returned as the answer โ€” or an error is thrown. + +
+ +The cleaning logic is separate from the processing logic so that the cleaning steps aren't repeated over and over with each recursive `calculate()` call. +This separation also makes it easier to make changes without creating conflict or confusion. + +`calculate()` performs the same steps as the `while-loop` from the [Import Callables from the `operator` Module][approach-import-callables-from-operator] approach and others. +The difference being that the `while-loop` test for `len() == 1` now occurs as an `if` condition in the function (_the base case_), and the "looping" is now a call to `calculate()` in the `else` condition. +`calculate()` can also use many of the strategies detailed in other approaches, as long as they work with the recursion. + +
+ +`clean()` can also use any of the strategies detailed in other approaches, two of which are below: + +```python + # Alternative 1 to the chained calls is to use a list comprehension: + return [item for item in + question.strip("?").split() + if item not in ("What", "is", "by")] # <-- The [] of the comprehension invokes implicit concatenation. + + + # Alternative 2 is the built-in filter(), but it can be somewhat hard to read. + return list(filter(lambda x: + x not in ("What", "is", "by"), + question.strip("?").split())) # <-- The () in list() also invokes implicit concatenation. +``` + +
+ +## Variation 1: Use regex for matching, cleaning, and calculating + +```python +import re +from operator import add, mul, sub +from operator import floordiv as div + +# This regex looks for any number 0-9 that may or may not have a - in front of it. +DIGITS = re.compile(r"-?\d+") + +# These regex look for a number (x or y) before and after a phrase or word. +OPERATORS = { + mul: re.compile(r"(?P-?\d+) multiplied by (?P-?\d+)"), + div: re.compile(r"(?P-?\d+) divided by (?P-?\d+)"), + add: re.compile(r"(?P-?\d+) plus (?P-?\d+)"), + sub: re.compile(r"(?P-?\d+) minus (?P-?\d+)"), +} + +# This regex looks for any digit 0-9 (optionally negative) followed by any valid operation, +# ending in any digit (optionally negative). +VALIDATE = re.compile(r"(?P-?\d+) (multiplied by|divided by|plus|minus) (?P-?\d+)") + + +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 after cleaning, there is only one number, return it as an int. + if DIGITS.fullmatch(question): + return int(question) + + # If after cleaning, there isn't anything, raise an error. + if not question: + raise ValueError("syntax error") + + # Call the recursive calculate() function. + return calculate(question) + +# Recursively calculate the first piece of the equation, +# calling calculate() on the product plus the remainder. +# Return the solution when len(equation) is one. +def calculate(question): + new_question = "" + + for symbol, pattern in OPERATORS.items(): + # Declare match variable and assign the pattern match as a value. + if match := pattern.match(question): + + # Attempt to calculate the first num symbol num trio. + # Convert strings to ints where needed. + first_calc = f"{symbol(int(match["x"]), int(match["y"]))}" + + # Strip the pattern from the question. + remainder = question.removeprefix(match.group()) + + # Create new question with first calculation + the remainder + new_question = first_calc + remainder + + # Check if there is just a single number, so that it can be returned. + # This is the "base case" of this recursive function. + if DIGITS.fullmatch(new_question): + return int(new_question) + + # Check if the new question is still a "valid" question. + # Error out if not. + if not VALIDATE.match(new_question): + raise ValueError("syntax error") + + # Otherwise, call itself to process the new question. + return calculate(new_question) +``` + +This variation shows how the dictionary of operators from the `operator` module can be augmented with [regex][re] to perform string matching for a question. +Regex are also used here to check that a question is a valid one and to ensure that the base case (_nothing but digits are left in the question_) is met for the recursive call in `calculate()`. +The regex patterns use [named groups][named-groups] for easy reference, but it's not necessary to do so. + + +Interestingly, `calculate()` loops through `dict.items()` to find symbols, using a [walrus operator][walrus] to complete successive regex matches and composing an [`f-string`][f-string] to perform the calculation. +The question remains a `str` throughout the process, so `question.removeprefix(match.group())` is used to "reduce" the original question to form a remainder that is then concatenated with the `f-string` to form a new question. + + +Because each new iteration of the question needs to be validated, there are `if` statements at the end that return the answer, throw a `ValueError("syntax error")`, or make the recursive call. + + +Note that the `for-loop` and `VALIDATE` use [`re.match`][re-match], but the `DIGITS` validation uses [`re.fullmatch`][re-fullmatch]. + +
+ +## Variation 2: Use regex, recurse within the for-loop + +~~~~exercism/caution +As of the time of writing, this variation of the approach passes all of the tests that are currently in the Exercism test suite. +**However**, it gives an incorrect answer to any sufficiently long problem, such as the following: "What is 1 plus -10 multiplied by 3 minus 4?" + +Parsing this example from left to right, we get `((1 + -10) * 3) - 4 == -31`. +However, this variation returns `9` in this case, [as seen here][recursion-in-loop-pythontutor]. + +[recursion-in-loop-pythontutor]: https://pythontutor.com/visualize.html#code=import%20re%0Afrom%20operator%20import%20add,%20mul,%20sub%0Afrom%20operator%20import%20floordiv%20as%20div%0A%0ADIGITS%20%3D%20re.compile%28r%22-%3F%5Cd%2B%22%29%0A%0AOPERATORS%20%3D%20%28%0A%20%20%20%20%28mul,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20multiplied%20by%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%20%20%20%20%28div,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20divided%20by%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%20%20%20%20%28add,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20plus%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%20%20%20%20%28sub,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20minus%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%29%0A%0Adef%20answer%28question%29%3A%0A%20%20%20%20if%20not%20question.startswith%28%22What%20is%22%29%20or%20%22cubed%22%20in%20question%3A%0A%20%20%20%20%20%20%20%20raise%20ValueError%28%22unknown%20operation%22%29%0A%0A%20%20%20%20question%20%3D%20question.removeprefix%28%22What%20is%22%29.removesuffix%28%22%3F%22%29.strip%28%29%0A%0A%20%20%20%20if%20not%20question%3A%0A%20%20%20%20%20%20%20%20raise%20ValueError%28%22syntax%20error%22%29%0A%0A%20%20%20%20return%20calculate%28question%29%0A%0Adef%20calculate%28question%29%3A%0A%20%20%20%20if%20DIGITS.fullmatch%28question%29%3A%0A%20%20%20%20%20%20%20%20return%20int%28question%29%0A%0A%20%20%20%20for%20operation,%20pattern%20in%20OPERATORS%3A%0A%20%20%20%20%20%20%20%20if%20matches%20%3A%3D%20pattern.match%28question%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20operation%28calculate%28matches%5B%22x%22%5D%29,%20calculate%28matches%5B%22y%22%5D%29%29%20%23%20%3C--%20the%20loop%20is%20paused%20here%20to%20make%20the%20two%20recursive%20calls.%0A%20%20%20%20raise%20ValueError%28%22syntax%20error%22%29%0A%0Aprint%28answer%28%22What%20is%201%20plus%20-10%20multiplied%20by%203%20minus%204%3F%22%29%29%0A%0Aprint%28%22Answer%20should%20be%3A%22,%20%28%281%20%2B%20-10%29%20*%203%29%20-%204%29%0A%0Aprint%28%22Result%20from%20wrong%20order%3A%22,%20%281%20%2B%20-10%29%20*%20%283%20-%204%29%29&curInstr=0&mode=display&origin=opt-frontend.js&py=311 +~~~~ + +```python +import re +from operator import add, mul, sub +from operator import floordiv as div + +DIGITS = re.compile(r"-?\d+") + +OPERATORS = ( + (mul, re.compile(r"(?P.*) multiplied by (?P.*)")), + (div, re.compile(r"(?P.*) divided by (?P.*)")), + (add, re.compile(r"(?P.*) plus (?P.*)")), + (sub, re.compile(r"(?P.*) minus (?P.*)")), +) + +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 not question: + raise ValueError("syntax error") + + return calculate(question) + +def calculate(question): + if DIGITS.fullmatch(question): + return int(question) + + for operation, pattern in OPERATORS: + if matches := pattern.match(question): + return operation(calculate(matches["x"]), calculate(matches["y"])) # <-- The loop is paused here to make the two recursive calls. + raise ValueError("syntax error") +``` + +This solution uses a `tuple` of nested `tuple`s containing the operators and the regex in place of the dictionaries that have been used in the previous approaches. +This saves some space, but requires that the nested `tuple`s be unpacked as the main `tuple` is iterated over (_note the `for operation, pattern in OPERATORS:` in the `for-loop`_) so that operations can be matched to strings in the question. +The regex is also more generic than the example above (_anything before and after the operation words is allowed_). + +Recursion is used a bit differently here from the previous variations โ€” the calls are placed [within the `for-loop`][recursion-within-loops]. +Because the regex are more generic, they will match a `digit-operation-digit` trio in a longer question, so the line `return operation(calculate(matches["x"]), calculate(matches["y"]))` is effectively splitting a question into parts that can then be worked on in their own stack frames. + + +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 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 of the process (including how it fails on long inputs), you can step through the code on [pythontutor.com][recursion-in-loop-pythontutor]. + +[amortization]: https://www.investopedia.com/terms/a/amortization.asp +[approach-import-callables-from-operator]: https://exercism.org/tracks/python/exercises/wordy/approaches/import-callables-from-operator +[backtracking]: https://en.wikipedia.org/wiki/Backtracking +[base-case]: https://en.wikipedia.org/wiki/Recursion_(computer_science)#Base_case +[bellman-ford]: https://www.programiz.com/dsa/bellman-ford-algorithm +[bfs]: https://en.wikipedia.org/wiki/Breadth-first_search +[bisection-search]: https://en.wikipedia.org/wiki/Bisection_method +[depreciation]: https://www.investopedia.com/terms/d/depreciation.asp +[dfs]: https://en.wikipedia.org/wiki/Depth-first_search +[dijkstra]: https://www.programiz.com/dsa/dijkstra-algorithm +[dynamic-programming]: https://algo.monster/problems/dynamic_programming_intro +[f-string]: https://docs.python.org/3.11/reference/lexical_analysis.html#formatted-string-literals +[looping-vs-recursion]: https://softwareengineering.stackexchange.com/questions/303242/is-there-anything-that-can-be-done-with-recursion-that-cant-be-done-with-loops +[memoization]: https://inventwithpython.com/recursion/chapter7.html +[merge-sort]: https://www.digitalocean.com/community/tutorials/merge-sort-algorithm-java-c-python +[named-groups]: https://docs.python.org/3/howto/regex.html#non-capturing-and-named-groups +[nuclei-decay]: https://courses.lumenlearning.com/suny-physics/chapter/31-5-half-life-and-activity/ +[re-fullmatch]: https://docs.python.org/3/library/re.html#re.full-match +[re-match]: https://docs.python.org/3/library/re.html#re.match +[re]: https://docs.python.org/3/library/re.html +[recursion-and-iteration]: https://web.mit.edu/6.102/www/sp23/classes/11-recursive-data-types/recursion-and-iteration-review.html#:~:text=The%20converse%20is%20also%20true,we%20are%20trying%20to%20solve. +[recursion-in-loop-pythontutor]: https://pythontutor.com/visualize.html#code=import%20re%0Afrom%20operator%20import%20add,%20mul,%20sub%0Afrom%20operator%20import%20floordiv%20as%20div%0A%0ADIGITS%20%3D%20re.compile%28r%22-%3F%5Cd%2B%22%29%0A%0AOPERATORS%20%3D%20%28%0A%20%20%20%20%28mul,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20multiplied%20by%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%20%20%20%20%28div,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20divided%20by%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%20%20%20%20%28add,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20plus%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%20%20%20%20%28sub,%20re.compile%28r%22%28%3FP%3Cx%3E.*%29%20minus%20%28%3FP%3Cy%3E.*%29%22%29%29,%0A%29%0A%0Adef%20answer%28question%29%3A%0A%20%20%20%20if%20not%20question.startswith%28%22What%20is%22%29%20or%20%22cubed%22%20in%20question%3A%0A%20%20%20%20%20%20%20%20raise%20ValueError%28%22unknown%20operation%22%29%0A%0A%20%20%20%20question%20%3D%20question.removeprefix%28%22What%20is%22%29.removesuffix%28%22%3F%22%29.strip%28%29%0A%0A%20%20%20%20if%20not%20question%3A%0A%20%20%20%20%20%20%20%20raise%20ValueError%28%22syntax%20error%22%29%0A%0A%20%20%20%20return%20calculate%28question%29%0A%0Adef%20calculate%28question%29%3A%0A%20%20%20%20if%20DIGITS.fullmatch%28question%29%3A%0A%20%20%20%20%20%20%20%20return%20int%28question%29%0A%0A%20%20%20%20for%20operation,%20pattern%20in%20OPERATORS%3A%0A%20%20%20%20%20%20%20%20if%20matches%20%3A%3D%20pattern.match%28question%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20operation%28calculate%28matches%5B%22x%22%5D%29,%20calculate%28matches%5B%22y%22%5D%29%29%20%23%20%3C--%20the%20loop%20is%20paused%20here%20to%20make%20the%20two%20recursive%20calls.%0A%20%20%20%20raise%20ValueError%28%22syntax%20error%22%29%0A%0Aprint%28answer%28%22What%20is%201%20plus%20-10%20multiplied%20by%203%20minus%204%3F%22%29%29%0A%0Aprint%28%22Answer%20should%20be%3A%22,%20%28%281%20%2B%20-10%29%20*%203%29%20-%204%29%0A%0Aprint%28%22Result%20from%20wrong%20order%3A%22,%20%281%20%2B%20-10%29%20*%20%283%20-%204%29%29&curInstr=0&mode=display&origin=opt-frontend.js&py=311 +[recursion-is-not-a-superpower]: https://inventwithpython.com/blog/2021/09/05/recursion-is-not-a-superpower-an-iterative-ackermann/ +[recursion-within-loops]: https://stackoverflow.com/questions/4795527/how-recursion-works-inside-a-for-loop +[tail-call-optimization]: https://neopythonic.blogspot.com/2009/04/tail-recursion-elimination.html +[walrus]: https://docs.python.org/3/reference/expressions.html#grammar-token-python-grammar-assignment_expression diff --git a/exercises/practice/wordy/.approaches/recursion/snippet.txt b/exercises/practice/wordy/.approaches/recursion/snippet.txt new file mode 100644 index 00000000000..0a6a15c20d3 --- /dev/null +++ b/exercises/practice/wordy/.approaches/recursion/snippet.txt @@ -0,0 +1,8 @@ +def calculate(equation): + if len(equation) == 1: return int(equation[0]) + try: + x_value, operation, y_value, *rest = equation + equation = [OPERATIONS[operation](int(x_value), int(y_value)), *rest] + except: + raise ValueError("syntax error") + return calculate(equation) \ No newline at end of file diff --git a/exercises/practice/wordy/.approaches/regex-with-operator-module/content.md b/exercises/practice/wordy/.approaches/regex-with-operator-module/content.md new file mode 100644 index 00000000000..0bf7362cf17 --- /dev/null +++ b/exercises/practice/wordy/.approaches/regex-with-operator-module/content.md @@ -0,0 +1,97 @@ +# Regex with the `operator` Module + +```python +import re +from operator import add, mul, sub +from operator import floordiv as div + +OPERATIONS = {"plus": add, "minus": sub, "multiplied by": mul, "divided by": div} + +REGEX = { + "number": re.compile(r"-?\d+"), + "operator": re.compile(f"(?:{'|'.join(OPERATIONS)})\\b") +} + +# Helper function to extract a number from the question. +def get_number(question): + # Match a number. + pattern = REGEX["number"].match(question) + + # Toss an error if there is no match. + if not pattern: + raise ValueError("syntax error") + + # Remove the matched pattern from the question, and convert that + # same pattern to an int. Return the modified question and the int. + return [question.removeprefix(pattern.group(0)).lstrip(), + int(pattern.group(0))] + +# Helper function to extract an operation from the question. +def get_operation(question): + # Match an operation word. + pattern = REGEX["operator"].match(question) + + # Toss an error if there is no match. + if not pattern: + raise ValueError("unknown operation") + + # Remove the matched pattern from the question, and look up that same + # pattern in OPERATIONS. Return the modified question and the operator. + return [question.removeprefix(pattern.group(0)).lstrip(), + OPERATIONS[pattern.group(0)]] + +def answer(question): + prefix = "What is" + + # Toss an error right away if the question isn't valid. + if not question.startswith(prefix): + raise ValueError("unknown operation") + + # Clean the question by removing the suffix and prefix and whitespace. + question = question.removesuffix("?").removeprefix(prefix).lstrip() + + # The question should start with a number. + question, result = get_number(question) + + # While there are portions of the question left, continue to process. + while len(question) > 0: + # Can't have a number followed by a number. + if REGEX["number"].match(question): + raise ValueError("syntax error") + + # Call get_operation and unpack the result + # into question and operation. + question, operation = get_operation(question) + + # Call get_number and unpack the result + # into question and num + question, num = get_number(question) + + # Perform the calculation, using result and num + # as arguments to operation. + result = operation(result, num) + + return result +``` + +This approach uses two dictionaries: one of operations imported from `operators`, and another that holds regex for matching digits and matching operations in the text of a question. + +It defines two "helper" functions (`get_number()` and `get_operation()`), that both take a question and use the regex patterns to remove, convert, and return a number (_`get_number()`_) or an operation (_`get_operation()`_), along with a modified "new question". + +In the `answer()` function, the question is checked for validity (_if it starts with "What is"_), and a `ValueError("unknown operation")` it raised if it is not a valid question. +Next, the question is cleaned with [`str.removeprefix`][removeprefix] and [`str.removesuffix`][removesuffix], removing "What is" and "?". +Left-trailing whitespace is stripped with the help of [`str.lstrip()`][lstrip]. +After that, the variable `result` is declared with an initial value from `get_number()`. + +The question is then iterated over via a `while-loop`, which calls `get_operation()` and `get_number()` โ€” "reducing" the question by removing the leading numbers and operator. +The return values from each call are [unpacked][unpacking] into a "leftover" question portion, and the number or operator. +The returned operation is then used as a [callable][callable] by putting `()` after the name, with `result` and the "new" number (_returned from `get_number()`_) passed as arguments. +The loop then proceeds with the processing of the "new question", until the `len()` is 0. + +Once there is no more question to process, `result` is returned as the answer. + +[callable]: https://treyhunner.com/2019/04/is-it-a-class-or-a-function-its-a-callable/ +[lstrip]: https://docs.python.org/3/library/stdtypes.html#str.lstrip +[removeprefix]: https://docs.python.org/3.9/library/stdtypes.html#str.removeprefix +[removesuffix]: https://docs.python.org/3.9/library/stdtypes.html#str.removesuffix +[unpacking]: https://treyhunner.com/2018/10/asterisks-in-python-what-they-are-and-how-to-use-them/ diff --git a/exercises/practice/wordy/.approaches/regex-with-operator-module/snippet.txt b/exercises/practice/wordy/.approaches/regex-with-operator-module/snippet.txt new file mode 100644 index 00000000000..e9f549370a8 --- /dev/null +++ b/exercises/practice/wordy/.approaches/regex-with-operator-module/snippet.txt @@ -0,0 +1,8 @@ +while len(question) > 0: + if REGEX["number"].match(question): + raise ValueError("syntax error") + + question, operation = get_operation(question) + question, num = get_number(question) + + result = operation(result, num) \ No newline at end of file diff --git a/exercises/practice/wordy/.approaches/string-list-and-dict-methods/content.md b/exercises/practice/wordy/.approaches/string-list-and-dict-methods/content.md new file mode 100644 index 00000000000..eaa5b334048 --- /dev/null +++ b/exercises/practice/wordy/.approaches/string-list-and-dict-methods/content.md @@ -0,0 +1,224 @@ +# String, List, and Dictionary Methods + +```python +def answer(question): + if not question.startswith("What is") or "cubed" in question: + raise ValueError("unknown operation") + + question = question.removeprefix("What is") + question = question.removesuffix("?") + question = question.replace("by", "") + question = question.strip() + + if not question: + raise ValueError("syntax error") + + formula = question.split() + while len(formula) > 1: + try: + x_value = int(formula[0]) + y_value = int(formula[2]) + symbol = formula[1] + remainder = formula[3:] + + if symbol == "plus": + formula = [x_value + y_value] + remainder + elif symbol == "minus": + formula = [x_value - y_value] + remainder + elif symbol == "multiplied": + formula = [x_value * y_value] + remainder + elif symbol == "divided": + formula = [x_value / y_value] + remainder + else: + raise ValueError("syntax error") + except: + raise ValueError("syntax error") + + return int(formula[0]) +``` + +Within the `answer()` function, the question is first checked for "unknown operations" by validating that it starts with "What is" ([`str.startswith`][startswith], [`str.endswith`][endswith]) and does not include the word "cubed" (_which is an invalid operation_). +This eliminates all the [current cases][unknown-operation-tests] where a [`ValueError("unknown operation")`][value-error] needs to be [raised][raise-statement]. +Should the definition of a question expand or change, this strategy would need to be revised. + + +The question is then "cleaned" by removing the prefix `"What is"` and the suffix `"?"` ([`str.removeprefix`][removeprefix], [`str.removesuffix`][removesuffix]), replacing `"by"` with `""` ([`str.replace`][str-replace]), and [stripping][strip] any leading or trailing whitespace. + + +If the question is now an empty string, a `ValueError("syntax error")` is raised. + + +The remaining question string is then converted into a `list` of elements via [`str.split`][split], and that `list` is iterated over using a `while-loop` with a `len() > 1` condition. + +Within a [`try-except`][handling-exceptions] block to trap/handle any errors (_which will all map to `ValueError("syntax error")`_), the question `list` is divided up among 4 variables using [bracket notation][bracket-notation]: + +1. The first element, `x_value`. This is assumed to be a number, so it is converted to an `int()` +2. The third element, `y_value`. This is also assumed to be a number and converted to an `int()`. +3. The second element, `symbol`. This is assumed to be an operator, and is left as-is. +4. The `remainder` of the question, if there is any. This is a [slice][list-slice] starting at index 3 and going to the end. + + +`symbol` is then tested for "plus", "minus", "multiplied", or "divided", and the `formula` list is modified by applying the given operation, and creating a new `formula` `list` by concatenating a `list` of the first product with the `remainder` list. +If `symbol` doesn't match any known operators, a `ValueError("syntax error")` is raised. +(Note that this is still inside the `try-except` block, so the [exception is chained][exception-chaining].) + +Once `len(formula) == 1`, the first element (`formula[0]`) is converted to an `int()` and returned as the answer. + +
+ +## Variation 1: Use a Dictionary for Lookup/Replace + +```python +OPERATIONS = {"plus": "+", "minus": "-", "multiplied": "*", "divided": "/"} + + +def answer(question): + if not question.startswith("What is") or "cubed" in question: + raise ValueError("unknown operation") + + question = question.removeprefix("What is").removesuffix("?").replace("by", "").strip() + + if not question: + raise ValueError("syntax error") + + formula = [] + for operation in question.split(): + formula.append(OPERATIONS.get(operation, operation)) + + while len(formula) > 1: + try: + x_value = int(formula[0]) + y_value = int(formula[2]) + symbol = formula[1] + remainder = formula[3:] + + if symbol == "+": + formula = [x_value + y_value] + remainder + elif symbol == "-": + formula = [x_value - y_value] + remainder + elif symbol == "*": + formula = [x_value * y_value] + remainder + elif symbol == "/": + formula = [x_value / y_value] + remainder + else: + raise ValueError("syntax error") + except: + raise ValueError("syntax error") + + return int(formula[0]) +``` + +~~~~exercism/note +[Method chaining][method-chaining] is used in the clean step for this variation, and is the equivalent of assigning and re-assigning `question` as is done in the initial approach. +This is because `str.startswith`, `str.endswith`, and `str.replace` all return strings, so the output of one can be used as the input to the next. + +[method-chaining]: https://www.tutorialspoint.com/Explain-Python-class-method-chaining +~~~~ + +This variation creates a dictionary to map operation words to symbols. +It pre-processes the question string into a `formula` list by looking up the operation words and replacing them with the symbols via the [`.get`][dict-get] method, which takes a [default argument][default-argument] for when a [`KeyError`][keyerror] is thrown. +Here the default for `dict.get()` is set to the element being iterated over, which is effectively _"if not found, skip it"_. +This means that the number strings will be passed through, even though they would otherwise toss an error. +The results of iterating through the question are appended to `formula` via [`list.append`][list-append]. + + +This dictionary is not necessary, but does potentially make adding/tracking future operations easier, although the `if-elif-else` block in the `while-loop` is equally awkward for maintenance (_see the [import callables from operator approach][approach-import-callables-from-operator] for a way to replace the block_). + +The `while-loop`, `if-elif-else` block, and the `try-except` block are then the same as in the initial approach. + +~~~~exercism/note +There are a couple of common alternatives to the `loop-append` used here: + +1. [List comprehensions][list-comprehension] duplicate the same process in a more succinct and declarative fashion. This one also includes filtering out "by": + ```python + formula = [OPERATIONS.get(operation, operation) for + operation in question.split() if operation != "by"] + ``` + +2. The built-in [`filter()`][filter] and [`map()`][map] functions used with a [`lambda`][lambdas] to process the elements of the list. + This is identical in process to both the `loop-append` and the list comprehension, but might be easier to reason about for those coming from a more functional programming language: + + ```python + formula = list(map(lambda op: OPERATIONS.get(op, op), + filter(lambda op: op != "by", question.split()))) + ``` + +[list-comprehension]: https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions +[lambdas]: https://docs.python.org/3/howto/functional.html#small-functions-and-the-lambda-expression +[filter]: https://docs.python.org/3/library/functions.html#filter +[map]: https://docs.python.org/3/library/functions.html#map +~~~~ + +Rather than indexing and slicing, [concept:python/unpacking-and-multiple-assignment]() can be used to assign the variables. +However, this does require a modification to the returned formula `list`: + +```python + x_value, symbol, y_value, *remainder = formula # <-- Unpacking won't allow conversion to int() here. + + ... + if symbol == "+": + formula = [int(x_value) + int(y_value)] + remainder # <-- Instead, conversion to int() must happen here. + ... + + return int(formula[0]) +``` + +
+ +## Variation 2: Structural Pattern Matching to Replace `if-elif-else` + +Introduced in Python 3.10, [structural pattern matching][structural-pattern-matching] can be used to replace the `if-elif-else` chain in the `while-loop` used in the two approaches above. +In some circumstances, this could be easier to read and/or reason about: + +```python +def answer(question): + if not question.startswith("What is") or "cubed" in question: + raise ValueError("unknown operation") + + question = question.removeprefix("What is").removesuffix("?").replace("by", "").strip() + + if not question: + raise ValueError("syntax error") + + formula = question.split() + while len(formula) > 1: + try: + x_value, symbol, y_value, *remainder = formula # <-- Unpacking and multiple assignment. + + match symbol: + case "plus": + formula = [int(x_value) + int(y_value)] + remainder + case "minus": + formula = [int(x_value) - int(y_value)] + remainder + case "multiplied": + formula = [int(x_value) * int(y_value)] + remainder + case "divided": + formula = [int(x_value) / int(y_value)] + remainder + case _: + raise ValueError("syntax error") # <-- Fall through case for no match. + except: + raise ValueError("syntax error") # <-- Error handling for anything else that goes wrong. + + return int(formula[0]) +``` + +[approach-import-callables-from-operator]: https://exercism.org/tracks/python/exercises/wordy/approaches/import-callables-from-operator +[bracket-notation]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations +[default-argument]: https://docs.python.org/3/tutorial/controlflow.html#default-argument-values +[dict-get]: https://docs.python.org/3/library/stdtypes.html#dict.get +[endswith]: https://docs.python.org/3.9/library/stdtypes.html#str.endswith +[exception-chaining]: https://docs.python.org/3/tutorial/errors.html#exception-chaining +[handling-exceptions]: https://docs.python.org/3.11/tutorial/errors.html#handling-exceptions +[keyerror]: https://docs.python.org/3/library/exceptions.html#KeyError +[list-append]: https://docs.python.org/3/tutorial/datastructures.html#more-on-lists +[list-slice]: https://www.pythonmorsels.com/slicing/ +[raise-statement]: https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement +[removeprefix]: https://docs.python.org/3.9/library/stdtypes.html#str.removeprefix +[removesuffix]: https://docs.python.org/3.9/library/stdtypes.html#str.removesuffix +[split]: https://docs.python.org/3.9/library/stdtypes.html#str.split +[startswith]: https://docs.python.org/3.9/library/stdtypes.html#str.startswith +[str-replace]: https://docs.python.org/3/library/stdtypes.html#str.replace +[strip]: https://docs.python.org/3.9/library/stdtypes.html#str.strip +[structural-pattern-matching]: https://peps.python.org/pep-0636/ +[unknown-operation-tests]: https://github.com/exercism/python/blob/main/exercises/practice/wordy/wordy_test.py#L58-L68 +[value-error]: https://docs.python.org/3.11/library/exceptions.html#ValueError diff --git a/exercises/practice/wordy/.approaches/string-list-and-dict-methods/snippet.txt b/exercises/practice/wordy/.approaches/string-list-and-dict-methods/snippet.txt new file mode 100644 index 00000000000..ccf9cc3052d --- /dev/null +++ b/exercises/practice/wordy/.approaches/string-list-and-dict-methods/snippet.txt @@ -0,0 +1,8 @@ +try: + x_value, y_value, symbol, remainder = int(formula[0]), int(formula[2]), formula[1], formula[3:] + if symbol == "+": formula = [x_value + y_value] + remainder + elif symbol == "-": formula = [x_value - y_value] + remainder + elif symbol == "*": formula = [x_value * y_value] + remainder + elif symbol == "/": formula = [x_value / y_value] + remainder + else: raise ValueError("syntax error") +except: raise ValueError("syntax error") \ No newline at end of file diff --git a/exercises/practice/wordy/.docs/hints.md b/exercises/practice/wordy/.docs/hints.md new file mode 100644 index 00000000000..95e798f7dbc --- /dev/null +++ b/exercises/practice/wordy/.docs/hints.md @@ -0,0 +1,40 @@ +# Hints + +## General + +- This challenge is all about validating, "cleaning up", and processing the given question in the **_correct order_**. +- If you read the directions and tests carefully, you will find there are three conditions that need to be met for a question to be "valid". +If a question is not valid, you will need to [raise ][raise-statement] a [ValueError][value-error] with a message (_`ValueError("unknown operation")`_). + It is best if you get this particular check out of the way before doing anything else. +- Processing a question before calculating the answer is all about utilizing [string methods][str-methods]. +A few popular ones to investigate include `str.removeprefix`, `str.removesuffix`, `str.replace`, and `str.strip`. +Others you might want to check out are `str.startswith`, `str.endswith`, and `in` (_which can apply to more than just strings_). + +- It is possible to iterate over a string. However, it is **much** easier to iterate over a list of _words_ if the string you are processing is a sentence or fragment with spaces. [`str.split`][split] can break apart a string and return a list of "words". +- A [`while-loop`][while-loop] is very useful for iterating over the question to process items. +- For fewer error checks and cleaner error-handling, a [`try-except`][handling-exceptions] is recommended as you process the question. +- **Remember**: the question is processed **_left-to-right_**. That means "1 plus 12 minus 3 multiplied by 4" gets processed by: + - Calculating "1 plus 12" (13), + - Calculating "13 minus 3" (10), + - Calculating "10 multiplied by 4" (40). + - The result of the first calculation is _concatenated with the remainder of the question to form a new question for the next step_. + - This technique is sometimes called [the accumulator pattern][accumulator-pattern], or [fold][fold] / [foldl][foldl] in functional programming languages like [Haskell][haskell-folds] or Lisp. + - Python includes two methods that are purpose-built to apply the `accumulator-pattern` to iterable data structures like `lists`, `tuples`, and `strings`. + [`functools.reduce`][reduce] and [`itertools.accumulate`][accumulate] could be interesting to investigate here. + +- This exercise has many potential solutions and many paths you can take along the way. + No path is manifestly "better" than another, although a particular path may be more interesting or better suited to what you want to learn or explore right now. + + +[accumulate]: https://docs.python.org/3/library/itertools.html#itertools.accumulate +[accumulator-pattern]: https://muzny.github.io/csci1200-notes/08/2/accumulator.html +[fold]: https://en.wikipedia.org/wiki/Fold_(higher-order_function) +[foldl]: https://slim.computer/eecs-111-ta-guide/material/higher-order/Fold.html +[handling-exceptions]: https://docs.python.org/3.11/tutorial/errors.html#handling-exceptions +[haskell-folds]: https://www.ashwinnarayan.com/post/a-study-on-haskell-folds/ +[raise-statement]: https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement +[reduce]: https://docs.python.org/3/library/functools.html#functools.reduce +[split]: https://docs.python.org/3.9/library/stdtypes.html#str.split +[str-methods]: https://docs.python.org/3/library/stdtypes.html#string-methods +[value-error]: https://docs.python.org/3.11/library/exceptions.html#ValueError +[while-loop]: https://python-practice.com/learn/loops/while_loop/ diff --git a/exercises/practice/wordy/.docs/instructions.append.md b/exercises/practice/wordy/.docs/instructions.append.md index 0a0d309f7e6..c5972700aad 100644 --- a/exercises/practice/wordy/.docs/instructions.append.md +++ b/exercises/practice/wordy/.docs/instructions.append.md @@ -2,11 +2,14 @@ ## 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. +Sometimes it is necessary to [raise an exception][raise-an-exception]. 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][built-in-errors], 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` if the question passed to `answer()` is malformed/invalid, or contains an unknown operation. The tests will only pass if you both `raise` the `exception` and include a message with it. +This particular exercise requires that you use the [raise statement][raise-statement] to "throw" a `ValueError` if the question passed to `answer()` is malformed/invalid, or contains an unknown operation. The tests will only pass if you both `raise` the `exception` and include a message with it. +**Please note**: The message needed is different for each scenario, even though the same _error type_ is being raised. +Check the tests carefully. + +To raise a [`ValueError`][value-error] with a message, write the message as an argument to the `exception` type: -To raise a `ValueError` with a message, write the message as an argument to the `exception` type: ```python # if the question contains an unknown operation. @@ -15,3 +18,28 @@ raise ValueError("unknown operation") # if the question is malformed or invalid. raise ValueError("syntax error") ``` + +To _handle_ a raised error within a particular code block, one can use a [try-except][handling-exceptions]. +`try-except` blocks "wrap" the code that could potentially cause an error, mapping all the exceptions to one error, multiple errors, or other pieces of code to deal with the problem: + + +```python +while len(equation) > 1: + try: + # The questionable/error-prone code goes here,in an indented block + # It can contain statements, loops, if-else blocks, or other executable code. + x_value, operation, y_value, *rest = equation + ... + ... + + except: + # Code for what to do when an error gets thrown in the code above. + # This could be one error, or more complicated logging, error checking and messaging. + raise ValueError("syntax error") +``` + +[built-in-errors]: https://docs.python.org/3.11/library/exceptions.html#built-in-exceptions +[handling-exceptions]: https://docs.python.org/3.11/tutorial/errors.html#handling-exceptions +[raise-an-exception]: https://docs.python.org/3/tutorial/errors.html#raising-exceptions +[raise-statement]: https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement +[value-error]: https://docs.python.org/3.11/library/exceptions.html#ValueError diff --git a/exercises/practice/wordy/.meta/additional_tests.json b/exercises/practice/wordy/.meta/additional_tests.json index 089dd45046b..447b532c051 100644 --- a/exercises/practice/wordy/.meta/additional_tests.json +++ b/exercises/practice/wordy/.meta/additional_tests.json @@ -1,5 +1,29 @@ { "cases": [ + { + "description": "4 number question", + "property": "answer", + "input": { + "question": "What is 1 plus -10 multiplied by 3 minus 4?" + }, + "expected": -31 + }, + { + "description": "4 number question with zero", + "property": "answer", + "input": { + "question": "What is 12 minus 0 divided by 6 plus 5?" + }, + "expected": 7 + }, + { + "description": "5 number question", + "property": "answer", + "input": { + "question": "What is 3 multiplied by 6 minus 2 divided by 4 plus 11?" + }, + "expected": 15 + }, { "description": "Missing operation", "property": "answer", diff --git a/exercises/practice/wordy/.meta/config.json b/exercises/practice/wordy/.meta/config.json index 07c63417075..33783d17376 100644 --- a/exercises/practice/wordy/.meta/config.json +++ b/exercises/practice/wordy/.meta/config.json @@ -21,7 +21,8 @@ "rootulp", "sjakobi", "tqa236", - "yawpitch" + "yawpitch", + "yrahcaz7" ], "files": { "solution": [ 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..b49b1e71205 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 2026-05-22 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?") @@ -105,6 +111,17 @@ def test_reject_prefix_notation(self): # Additional tests for this track + def test_4_number_question(self): + self.assertEqual(answer("What is 1 plus -10 multiplied by 3 minus 4?"), -31) + + def test_4_number_question_with_zero(self): + self.assertEqual(answer("What is 12 minus 0 divided by 6 plus 5?"), 7) + + def test_5_number_question(self): + self.assertEqual( + answer("What is 3 multiplied by 6 minus 2 divided by 4 plus 11?"), 15 + ) + def test_missing_operation(self): with self.assertRaises(ValueError) as err: answer("What is 2 2 minus 3?") diff --git a/exercises/shared/.docs/help.md b/exercises/shared/.docs/help.md index ef95bd6193b..f4114f8b2cb 100644 --- a/exercises/shared/.docs/help.md +++ b/exercises/shared/.docs/help.md @@ -3,13 +3,13 @@ Below are some resources for getting help if you run into trouble: - [The PSF](https://www.python.org) hosts Python downloads, documentation, and community resources. +- [Python Community Forums](https://discuss.python.org/) help, PEP discussion, core Python committers, and more. - [The Exercism Community on Discord](https://exercism.org/r/discord) +- [The Exercism Community Discussion Forums](https://forum.exercsim.org) - [Python Community on Discord](https://pythondiscord.com/) is a very helpful and active community. - [/r/learnpython/](https://www.reddit.com/r/learnpython/) is a subreddit designed for Python learners. - [#python on Libera.chat](https://www.python.org/community/irc/) this is where the core developers for the language hang out and get work done. -- [Python Community Forums](https://discuss.python.org/) - [Free Code Camp Community Forums](https://forum.freecodecamp.org/) -- [CodeNewbie Community Help Tag](https://community.codenewbie.org/t/help) - [Pythontutor](http://pythontutor.com/) for stepping through small code snippets visually. Additionally, [StackOverflow](http://stackoverflow.com/questions/tagged/python) is a good spot to search for your problem/question to see if it has been answered already. diff --git a/exercises/shared/.docs/representations.md b/exercises/shared/.docs/representations.md index 8439ca9aab4..ec803d01dc8 100644 --- a/exercises/shared/.docs/representations.md +++ b/exercises/shared/.docs/representations.md @@ -7,7 +7,7 @@ The [Python representer][representer] processes and normalizes student solutions - For troubleshooting purposes, `representation.out` includes starting AST, edited AST, and a code representation with normalizations applied. - Removals: - - typehints + - typehints (_including typehints in dataclasses, see Normalizations below_) - `print()` statements - `if __name__ == __main__` blocks - comments @@ -28,6 +28,8 @@ The [Python representer][representer] processes and normalizes student solutions - **1e2+1_23e0+4.4e-1** --> 100.0 + 123.0 + 0.44 #223.44 - **7e6+7e5+5e4+9.98e2+4.45_779e-1** -->7000000.0 + 700000.0 + 50000.0 + 998.0 + 0.445779 #7750998.445779 - **(7e6+7e5+5e4+9.98e1+4.457_79e-1)+(1e2+1.23e1+4.444_23e-1)*1*j** --> (7000000.0 + 700000.0 + 50000.0 + 99.8 + 0.445779 + (100.0 + 12.3 + 0.444423) * 1j) #7750100.245779+112.744423j + - functions and classes that have empty bodies after docstring removal have `pass` assigned to the body. + - dataclasses that have type hinted but unassigned data members have those data members assigned `None` as a value. See the docstring starting on line 153 of normalizer.py for more details. [representer]: https://github.com/exercism/python-representer/tree/main/representer [python-ast]: https://docs.python.org/3/library/ast.html#module-ast diff --git a/exercises/shared/.docs/tests.md b/exercises/shared/.docs/tests.md index 5d1c9b9959d..8b6d608a486 100644 --- a/exercises/shared/.docs/tests.md +++ b/exercises/shared/.docs/tests.md @@ -13,32 +13,32 @@ Extended information can be found in our website [Python testing guide][Python t ### Running Tests -To run the included tests, navigate to the folder where the exercise is stored using `cd` in your terminal (_replace `{exercise-folder-location}` below with your path_). +To run the included tests, navigate to the folder where the exercise is stored using `cd` in your terminal (_replace `` below with your path_). Test files usually end in `_test.py`, and are the same tests that run on the website when a solution is uploaded. Linux/MacOS ```bash -$ cd {path/to/exercise-folder-location} +$ cd ``` Windows ```powershell -PS C:\Users\foobar> cd {path\to\exercise-folder-location} +PS C:\Users\foobar> cd ```
-Next, run the `pytest` command in your terminal, replacing `{exercise_test.py}` with the name of the test file: +Next, run the `pytest` command in your terminal, replacing `` with the name of the test file: Linux/MacOS ```bash -$ python3 -m pytest -o markers=task {exercise_test.py} +$ python3 -m pytest -o markers=task ==================== 7 passed in 0.08s ==================== ``` Windows ```powershell -PS C:\Users\foobar> py -m pytest -o markers=task {exercise_test.py} +PS C:\Users\foobar> py -m pytest -o markers=task ==================== 7 passed in 0.08s ==================== ``` diff --git a/pylintrc b/pylintrc index 09795978bc4..4c362789429 100644 --- a/pylintrc +++ b/pylintrc @@ -2,373 +2,345 @@ # This pylintrc file contains a best-effort configuration to uphold the # best-practices and style for the Python track of exercism.org. # -# It is based on the Google pylintrc & the Google Python style guide: -# https://google.github.io/styleguide/pyguide.html, -# https://google.github.io/styleguide/pylintrc +# It is based on the default pylint rc with enabled extensions, and is the same +# pylintrc file used in our generic Analyzer. # -# Additions & alterations to the Google Style guide are noted in +# Additions & alterations to the base configuration are noted in # the Exercism Python Track contributing guide: # https://github.com/exercism/python/blob/main/CONTRIBUTING.md ########################################################################### -[MASTER] +[MAIN] -# Files or directories to be skipped. They should be base names, not paths. +analyse-fallback-blocks=no +clear-cache-post-run=yes +extension-pkg-allow-list= +extension-pkg-whitelist= ignore=third_party - -# Files or directories matching the regex patterns are skipped. The regex -# matches against base names, not paths. -ignore-patterns= - -# Pickle collected data for later comparisons. -persistent=no - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Use multiple processes to speed up Pylint. jobs=4 - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. +limit-inference-results=100 unsafe-load-any-extension=no +persistent=no +prefer-stubs=no +recursive=no +# Specify a score threshold under which the program will exit with error. +fail-under=10 -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence= - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -#enable= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once).You can also use "--disable=all" to -# disable everything first and then re-enable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use"--disable=all --enable=classes -# --disable=W" -# As of Pylint 2.6+, the following options are not supported, so they're commented out: -# -# misplaced-comparison-constant -# relative-import -# input-builtin -# inconsistent-return-statements -# no-absolute-import -# raising-string -# round-builtin - -disable=arguments-differ, - attribute-defined-outside-init, - fixme, - global-statement, - implicit-str-concat-in-sequence, - import-error, - import-self, - locally-disabled, - no-else-break, - no-else-continue, - no-else-raise, - no-else-return, - no-member, - no-name-in-module, - signature-differs, - suppressed-message, - too-many-boolean-expressions, - too-many-branches, - too-many-locals, - too-many-public-methods, - too-many-return-statements, - too-many-statements, - unnecessary-pass, - unused-argument, - useless-suppression - - -[REPORTS] - -# Set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html. You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=colorized - -# Tells whether to display a full report or only the messages -reports=no - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details -#msg-template= +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins = pylint.extensions.bad_builtin, + # pylint.extensions.code_style, + pylint.extensions.comparison_placement, + pylint.extensions.consider_refactoring_into_while_condition, + # pylint.extensions.docparams, + pylint.extensions.dunder, + pylint.extensions.eq_without_hash, + pylint.extensions.for_any_all, + # pylint.extensions.mccabe, + pylint.extensions.no_self_use, + pylint.extensions.overlapping_exceptions, + pylint.extensions.private_import, + pylint.extensions.redefined_loop_name, + pylint.extensions.set_membership, [BASIC] -# Good variable names which should always be accepted, separated by a comma -good-names=main,_ - -# Bad variable names which should always be refused, separated by a comma -bad-names=x,y,i,l - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Include a hint for the correct naming format with invalid-name -include-naming-hint=no +module-naming-style=snake_case +const-naming-style=UPPER_CASE +variable-naming-style=snake_case +function-naming-style=snake_case +argument-naming-style=snake_case +attr-naming-style=snake_case +class-naming-style=PascalCase +class-attribute-naming-style=any +class-const-naming-style=UPPER_CASE +method-naming-style=snake_case +inlinevar-naming-style=snake_case + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=yes + +# Bad variable names which should always be refused, separated by a comma. +bad-names=x, + y, + i, + l, + a, + b, + j, + o, + z + +# Good variable names which should always be accepted, separated by a comma. +good-names=main, # List of decorators that produce properties, such as abc.abstractproperty. Add # to this list to register other decorators that produce valid properties. -property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl - -# Regular expression matching correct function names -function-rgx=^(?:(?PsetUp|tearDown|setUpModule|tearDownModule)|(?P_?[A-Z][a-zA-Z0-9]*)|(?P_?[a-z][a-z0-9_]*))$ - -# Regular expression matching correct variable names -variable-rgx=^[a-z][a-z0-9_]*$ +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty, + cached_property.cached_property, + cached_property.threaded_cached_property, + cached_property.cached_property_with_ttl, + cached_property.threaded_cached_property_with_ttl -# Regular expression matching correct constant names -const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ - -# Regular expression matching correct attribute names -attr-rgx=^_{0,2}[a-z][a-z0-9_]*$ - -# Regular expression matching correct argument names -argument-rgx=^[a-z][a-z0-9_]*$ +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=5 -# Regular expression matching correct class attribute names -class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs=^.{1,2}$ -# Regular expression matching correct inline iteration names -inlinevar-rgx=^[a-z][a-z0-9_]*$ +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= -# Regular expression matching correct class names -class-rgx=^_?[A-Z][a-zA-Z0-9]*$ -# Regular expression matching correct module names -module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$ +[CLASSES] -# Regular expression matching correct method names -method-rgx=(?x)^(?:(?P_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P_{0,2}[a-z][a-z0-9_]*))$ +check-protected-access-in-special-methods=no +defining-attr-methods=__init__, + __new__, + setUp -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$ +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=10 +valid-classmethod-first-arg=cls, + class_ +valid-metaclass-classmethod-first-arg=mcs -[TYPECHECK] -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager +[DESIGN] -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes +max-args=5 +max-attributes=7 +max-bool-expr=5 +max-branches=12 +max-locals=15 +max-parents=7 +max-positional-arguments=5 +max-public-methods=20 +max-returns=6 +max-statements=50 +min-public-methods=2 -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local +[EXCEPTIONS] -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=StandardError, + Exception, + BaseException [FORMAT] +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + # Maximum number of characters on a single line. max-line-length=120 -# TODO(https://github.com/PyCQA/pylint/issues/3352): Direct pylint to exempt -# lines made too long by directives to pytype. - # Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=(?x)( - ^\s*(\#\ )??$| - ^\s*(from\s+\S+\s+)?import\s+.+$) +ignore-long-lines=(?x)(^\s*(\#\ )??$|^\s*(from\s+\S+\s+)?import\s+.+$) -# Allow the body of an if to be on the same line as the test if there is no -# else. +# Maximum number of lines in a module. +max-module-lines=99999 + +single-line-class-stmt=no single-line-if-stmt=yes -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -# no-space-check= -# Maximum number of lines in a module -max-module-lines=99999 +[IMPORTS] -# String used as indentation unit. Currently 4, consistent with -# PEP 8. -indent-string=' ' +# Allow explicit reexports by alias from a package __init__. +allow-reexport-from-package=no -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=regsub, + TERMIOS, + Bastion, + rexec, + sets +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant, + absl -[MISCELLANEOUS] +[LOGGING] -# List of note tags to take in consideration, separated by a comma. -notes=TODO +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging,absl.logging -[STRING] -# This flag controls whether inconsistent-quotes generates a warning when the -# character used as a quote delimiter is used inconsistently within a module. -check-quote-consistency=yes +[MESSAGES CONTROL] +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence=HIGH, + CONTROL_FLOW, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED + +# Disable the message, report, category or checker with the given id(s). +disable=raw-checker-failed, + locally-disabled, + suppressed-message, + arguments-differ, + fixme, + line-too-long, + global-statement, + import-error, + no-member, + signature-differs, + too-many-locals, + too-many-public-methods, + too-many-return-statements, + too-many-statements, + unnecessary-pass -[VARIABLES] +# Include some helpful details on errors messages for naming rules: +include-naming-hint = yes -# Tells whether we should check for unused import in __init__ files. -init-import=no -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_) +[METHOD_ARGS] -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods=requests.api.delete, + requests.api.get, + requests.api.head, + requests.api.options, + requests.api.patch, + requests.api.post, + requests.api.put, + requests.api.request -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks= -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools +[MISCELLANEOUS] +check-fixme-in-docstring=no +notes=TODO -[LOGGING] -# Logging modules to check that the string format arguments are in logging -# function parameter format -logging-modules=logging,absl.logging +[REFACTORING] +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 -[SIMILARITIES] +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error -# Minimum lines number of a similarity. -min-similarity-lines=4 +# Let 'consider-using-join' be raised when the separator to join on would be +# non-empty (resulting in expected fixes of the type: ``"- " + " - +# ".join(items)``) +suggest-join-with-non-empty-separator=yes -# Ignore comments when computing similarities. -ignore-comments=yes -# Ignore docstrings when computing similarities. -ignore-docstrings=yes +[REPORTS] -# Ignore imports when computing similarities. -ignore-imports=no +reports=no [SPELLING] -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. spelling-store-unknown-words=no -[IMPORTS] +[STRING] -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub, - TERMIOS, - Bastion, - rexec, - sets +check-quote-consistency=yes +check-str-concat-over-line-jumps=yes -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= +[VARIABLES] -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= +allow-global-unused-variables=yes +dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_) +ignored-argument-names=_.*|^ignored_|^unused_ +redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= +# Tells whether we should check for unused import in __init__ files. +init-import=no -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant, absl +[TYPECHECK] -[CLASSES] +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls, - class_ +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes -[EXCEPTIONS] +# The maximum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=StandardError, - Exception, - BaseException +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= 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`. diff --git a/requirements-generator.txt b/requirements-generator.txt index d472d158b9b..7385618bb4f 100644 --- a/requirements-generator.txt +++ b/requirements-generator.txt @@ -1,6 +1,9 @@ -black<=22.3.0 -flake8~=5.0.4 -Jinja2~=3.1.2 -python-dateutil==2.8.1 -markupsafe==2.0.1 -tomli>=1.1.0; python_full_version < '3.11.2' +Jinja2~=3.1.6 +black<=25.1.0 +flake8~=7.3.0 +markupsafe==3.0.2 +pytest-subtests~=0.14.2 +pytest~=8.4.0 +python-dateutil~=2.9.0 +requests~=2.32.4 +tomli>=2.2.1; python_full_version < '3.11.2' diff --git a/requirements.txt b/requirements.txt index 712608f8550..6d2f03a4e7d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,7 @@ -flake8~=5.0.4 -pylint~=2.17.1 -black<=22.3.0 -yapf~=0.32.0 -tomli>=1.1.0; python_full_version < '3.11.2' +black<=25.1.0 +yapf~=0.43.0 +flake8~=7.3.0 +pylint ~=4.0.4 +pytest~=8.4.0 +pytest-subtests~=0.14.2 +tomli>=2.2.1; python_full_version < '3.11.2'