diff --git a/exercises/practice/wordy/.approaches/config.json b/exercises/practice/wordy/.approaches/config.json index ed71a846..79a3a114 100644 --- a/exercises/practice/wordy/.approaches/config.json +++ b/exercises/practice/wordy/.approaches/config.json @@ -1,10 +1,52 @@ { "introduction": { - "authors": ["bobahop"], - "contributors": [] + "authors": ["BethanyG"], + "contributors": ["bobahop"] }, "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"] + }, + { + "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"] + }, + { + "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"] + }, + { + "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"] + }, + { + "uuid": "2e643b88-9b76-45a1-98f4-b211919af061", + "slug": "recursion", + "title": "Recursion for iteration.", + "blurb": "Use recursion with other strategies to solve word problems.", + "authors": ["BethanyG"] + }, + { + "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"] + }, + { "uuid": "d643e2b4-daee-422d-b8d3-2cad2f439db5", "slug": "dunder-getattribute", "title": "dunder with __getattribute__", diff --git a/exercises/practice/wordy/.approaches/dunder-getattribute/content.md b/exercises/practice/wordy/.approaches/dunder-getattribute/content.md index 76280615..31c42e7a 100644 --- a/exercises/practice/wordy/.approaches/dunder-getattribute/content.md +++ b/exercises/practice/wordy/.approaches/dunder-getattribute/content.md @@ -1,5 +1,6 @@ # Dunder methods with `__getattribute__` + ```python OPS = { "plus": "__add__", @@ -33,70 +34,61 @@ def answer(question): ``` -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-methods`][dunder] methods. +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 [SO: Difference between dir() and __dict__][dir-vs-__dict__] for differences between the two. ~~~~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. +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 objects writable attributes. ~~~~ -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))`. +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. -~~~~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. -~~~~ - -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 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 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. +it uses the [falsiness][falsiness] of an empty string with the [`not`][not] operator to return a `ValueError("syntax error")`. -Next, the [`isdigit`][isdigit] method is used to see if all of the remaining characters in the input are digits. +Next, the [`isdigit`][isdigit] method is used to see if the remaining characters in the input are digits. If so, it uses the [`int()`][int-constructor] constructor to return the string as an integer. -Next, the 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 elements in the `OPS` dictionary are iterated over. +If the key name is in the input, then the [`str.replace`][replace] method is used to replace the name in the input with the `dunder-method` value. +If none of the key names are found in the input, a `ValueError("unknown operation")` is returned. -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. +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. -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")` +Within a [try-except][exception-handling] block, the list is [unpacked][unpacking] (_see also [Concept: unpacking][unpacking-and-multiple-assignment]_) into the variables `x, op, y, and *tail`. +If `op` is not in the supported `dunder-methods` dictionary, a `ValueError("syntax error")` is raised. +If there are any other exceptions raised within the `try` block, they are "caught"/ handled in the `except` clause by raising a `ValueError("syntax error")`. -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`. +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 arguemnt to `op`. -It sets the list to the result of the dunder method plus the remaining elements in `*tail`. - -~~~~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. -~~~~ +Then `ret` is redefined to a `list` containing the result of the dunder method plus the remaining elements in `*tail`. When the loop exhausts, the first element of the list is selected as the function return value. -[dictionaries]: https://docs.python.org/3/tutorial/datastructures.html#dictionaries -[dunder]: https://www.tutorialsteacher.com/python/magic-methods-in-python -[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 +[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-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 +[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 +[unpacking]: https://treyhunner.com/2018/10/asterisks-in-python-what-they-are-and-how-to-use-them/ +[unpacking-and-multiple-assignment]: https://exercism.org/tracks/python/concepts/unpacking-and-multiple-assignment diff --git a/exercises/practice/wordy/.approaches/functools-reduce/content.md b/exercises/practice/wordy/.approaches/functools-reduce/content.md new file mode 100644 index 00000000..08f21a8a --- /dev/null +++ b/exercises/practice/wordy/.approaches/functools-reduce/content.md @@ -0,0 +1,126 @@ +# 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") + + # Using the built-in filter() to clean & split the 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, 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 operation 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: lists](/tracks/python/concepts/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 + return (question.removeprefix("What is") + .removesuffix("?") + .replace("by", "") + .strip()).split() # <-- this split() turns the string into a list. + + + # Alternative 2 to the nested calls to filter and split 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. +``` + + +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` (_made [callable][callable] by adding `()`_), 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 makes the solution much shorter. +But 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 operator 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 operation dictionary. + result = reduce(lambda x, y: OPERATORS[operations.pop(0)](x, y), digits) + + return result +``` + +[approach-lambdas-in-a-dictionary]: https://exercise.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 00000000..f8d5a294 --- /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 00000000..0c268376 --- /dev/null +++ b/exercises/practice/wordy/.approaches/import-callables-from-operator/content.md @@ -0,0 +1,93 @@ +# 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.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 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. +These functions (_floordiv is [aliased][aliasing] to "div"_) are stored in a dictionary that serves as a lookup table when the problems are processed. +These operations are later made [callable][callable] by using `()` after the name, and supplying arguments. + + +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], [strip][strip], and [split][split]. +Checks for digits and an empty string are done, and the word "by" is filtered from the equation `list` 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: unpacking and multiple assignment](/tracks/python/concepts/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 it is of `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 () means these lines are automatically joined by the interpreter. +``` + + +The call to `str.replace` could instead be chained to the call to `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://exercise.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/render.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'by'%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&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false +[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 00000000..d5cb5a13 --- /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 9faba81c..ae8900e0 100644 --- a/exercises/practice/wordy/.approaches/introduction.md +++ b/exercises/practice/wordy/.approaches/introduction.md @@ -1,22 +1,367 @@ # 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 problems 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. -~~~~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. -~~~~ -The dunder methods can be called by using the [`__getattribute__`][getattribute] method for [`int`][int]. +## General Guidance -## 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", 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. + +One way to reduce the number of `raise` statements/ `ValueError`s needed in the code is to determine if a problem is a "valid" question _before_ proceeding to parsing and calculation. +As shown in various approaches, there are multiple strategies for validating questions, with no one "canonical" solution. +One very effective approach is to check if a question starts with "What is", ends with "?", and includes only valid operations. +That 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. + +There are various Pythonic ways to go about the cleaning, parsing, and calculation steps of Wordy. +For cleaning the "question" portion of the problem, [`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. +>>> '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 be used to clean up the initial word problem. +A [regex][regex] could also be used to process the question, 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 iteration, although other strategies are also used. + +For math operations, many solutions involve importing and using methods from the [operator][operator] module in combination with different looping, parsing, and substitution strategies. +Some solutions use either [lambda][lambdas] expressions or [dunder/"special" methods][dunder-methods] to replace words with arithmetic operations. +However, the exercise can be solved without using `operator`, `lambdas`, or `dunder-methods`. + +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. + + +## Approach: String, List, and Dictionary Methods + + +```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("?").strip() + + if not question: + raise ValueError("syntax error") + + if question.isdigit(): + return int(question) + + formula = [] + for operation in question.split(): + if operation == 'by': + continue + else: + formula.append(OPERATIONS.get(operation, operation)) + + while len(formula) > 1: + try: + x_value = int(formula[0]) + symbol = formula[1] + y_value = int(formula[2]) + 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 formula[0] +``` + +This approach uses only data structures and methods (_[dict][dict], [dict.get()][dict-get] and [list()][list]_) 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. +It does use a [try-except][handling-exceptions] block for handling unknown operators. +As an alternative to the `formula` loop-append, a [list-comprehension][list-comprehension] can be used to create the initial parsed formula. + +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.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: unpacking and multiple assignment](/tracks/python/concepts/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 and 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') +} + + +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-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.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 + +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]) + else: + 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 `loops` 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][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. -Parsing should verify that the expression in words can be translated to a valid mathematical expression. ## Approach: Dunder methods with `__getattribute__` + ```python OPS = { "plus": "__add__", @@ -50,11 +395,52 @@ def answer(question): ``` -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 number 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 - as 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 method with `__getattribute__` approach][approach-dunder-getattribute]. + + +[PEMDAS]: https://www.mathnasium.com/math-centers/eagan/news/what-pemdas-e [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://exercise.org/tracks/python/exercises/wordy/approaches/lambdas-in-a-dictionary +[approach-recursion]: https://exercise.org/tracks/python/exercises/wordy/approaches/recursion +[approach-regex-with-operator-module]: https://exercise.org/tracks/python/exercises/wordy/approaches/regex-with-operator-module +[approach-string-list-and-dict-methods]: https://exercise.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/?watch +[endswith]: https://docs.python.org/3.9/library/stdtypes.html#str.endswith +[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 +[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 +[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 00000000..6836e7b4 --- /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.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][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.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 carry a trailing underscore to avoid potential shadowing or name conflict. +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://exercise.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 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 00000000..3769bef8 --- /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 00000000..5dd0a7ba --- /dev/null +++ b/exercises/practice/wordy/.approaches/recursion/content.md @@ -0,0 +1,266 @@ +# Recursion + + +[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, Pyton 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-firs search][dfs], and [merge sort][merge-sort]_). + +Other algorithms such as [breadth-first search][bfs], [Dijkstra's algorithm][dijkstra], and [Bellman-Ford Algorithm][bellman-ford] lend themselves better to iteration. + + +```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 + the remainder. +# Return the solution when len(equation) is one. +def calculate(equation): + if len(equation) == 1: + return int(equation[0]) + else: + 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 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 in processing or calculating without creating conflict or confusion. + +Note that `calculate()` performs the same steps as the `while-loop` from [Import Callables from the Operator Module][approach-import-callables-from-operator] 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, toss 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 + 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. + elif not VALIDATE.match(new_question): + raise ValueError("syntax error") + + # Otherwise, call yourself to process the new question. + else: + return calculate(new_question) +``` + + +This variation shows how the dictionary of operators from `operator` 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 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 is an `if-elif-else` block at the end that returns the answer, throws a `ValueError("syntax error")`, or makes the recursive call. + + +Note that the `for-loop` and VALIDATE use [`re.match`][re-match], but DIGITS validation uses [`re.fullmatch`][re-fullmatch]. + + +## Variation 2: Use Regex, Recurse within the For-loop + + +```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 match := pattern.match(question): + return operation(calculate(match['x']), calculate(match['y'])) #<-- the loop is paused here to make the two recursive calls. + raise ValueError("syntax error") +``` + +This solution uses a `tuple` of nested `tuples` containing the operators from `operator` and regex in place of the dictionaries that have been used in the previous approaches. +This saves some space, but requires that the nested `tuples` 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(match['x']), calculate(match['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 would then be re-arranged to `mul(calculate("1 plus -10"), calculate("13 divided by 2"))` +3. At this point the loop would pause as the two recursive calls to `calculate()` spawn +4. The loops would run again — and so would the calls to `calculate()`, until there wasn't any match that caused a split of the question or an error. +5. One at a time, the numbers would then be returned, until the main `mul(calculate("1 plus -10"), calculate("13 divided by 2"))` could be solved, at which point the answer would be returned. + +For a more visual picture, 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 +[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/render.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%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%20%20%20%20%29%0A%0Adef%20answer%28question%29%3A%0A%20%20%20%20if%20not%20question.startswith%28%20%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%20%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%20%20%20%20%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%20%20%20%20%20%20%20%20%0A%20%20%20%20for%20operation,%20pattern%20in%20OPERATORS%3A%0A%20%20%20%20%20%20%20%20if%20match%20%3A%3D%20pattern.match%28question%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20operation%28calculate%28match%5B'x'%5D%29,%20calculate%28match%5B'y'%5D%29%29%20%23%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%2013%20divided%20by%202%3F%22%29%29&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false +[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 00000000..373481f8 --- /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]) + else: + 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 00000000..d3d5c214 --- /dev/null +++ b/exercises/practice/wordy/.approaches/regex-with-operator-module/content.md @@ -0,0 +1,98 @@ +# Regex and 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 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 (_does it start 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] & [`str.removesuffix`][removesuffix], removing "What is" and "?". +Left-trailing white space is stripped with the help of [`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 made [callable][callable] using `()`, with result and the "new" number (_returned from `get_number()`_) passed as arguments. +The `loop` then proceeds with 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 00000000..4d89edb5 --- /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 00000000..3c3dcfe8 --- /dev/null +++ b/exercises/practice/wordy/.approaches/string-list-and-dict-methods/content.md @@ -0,0 +1,161 @@ +# String, List, and Dictionary Methods + + +```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("?").strip() + + if not question: + raise ValueError("syntax error") + + formula = [] + for operation in question.split(): + if operation == 'by': + continue + else: + formula.append(OPERATIONS.get(operation, operation)) + + while len(formula) > 1: + try: + x_value, y_value = int(formula[0]), 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]) +``` + +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]) and [stripping][strip] any leading or trailing whitespaces. + + +If the question is now an empty string, a `ValueError("syntax error")` is raised. + + +Next, the question is [split][split] into a `list` and iterated over, with each element looked up and replaced from the OPERATIONS dictionary. +The [`dict.get`][dict-get] method is used for this, as it takes a 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 avoids error handling, extra logic, or interruption when an element is not found. + One exception here is the word "by", which is explicitly skipped within the `for-loop`, so that it doesn't appear in the formula to be processed. + This filtering out could also be accomplished by using [`str.replace`][str-replace] in the cleaning step or during the `split` step. + The results of iterating through the question are then appended to a new formula `list`. + + + +````exercism/note +There are a couple of common alternatives to the `loop-append`: + +1. [`list-comprehensions`][list-comprehension] duplicate the same process in a more succinct and declarative fashion: + ```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 x : OPERATIONS.get(x, x), + filter(lambda x: x != "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 +```` + + +After the formula `list` is composed, it is processed in a `while-loop`. + +The processing within the `loop` is wrapped in a [try-except][handling-exceptions] block to trap any errors and raise them as `ValueError("syntax error")`. +While each type of error could be checked for individually, it is not necessary since only `ValueError("syntax error")` is required here. + +1. `x_value` and `y_value` are assigned to the first element and third element of the list using [bracket notation][bracket-notation], and converted to integers. + - Rather than indexing and slicing, [concept: unpacking and multiple assignment](/tracks/python/concepts/unpacking-and-multiple-assignment) can be used to assign the variables. + This does require a modification to the returned formula `list`: + ```python + x_value, operation, 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]) + ``` + +2. `symbol` is assigned to the second element of the list. +3. `remainder` is assigned to a [slice][list-slice] of everything else in the `list`. + +The `symbol` is then tested in the `if-elif-else` block and the formula `list` is modified by calculating the operation on `x_value` and `y_value` and then appending whatever part of the question remains. + +Once the formula `list` is calculated down to a number, that number is converted to an `int` and returned as the answer. + + +````exercism/note +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`. +In some circumstances, this could be easier to read and/or reason about: + + +```python + while len(formula) > 1: + try: + x_value, symbol, y_value, *remainder = formula + + match symbol: + case "+": + formula = [int(x_value) + int(y_value)] + remainder + case "-": + formula = [int(x_value) - int(y_value)] + remainder + case "*": + formula = [int(x_value) * int(y_value)] + remainder + case "/": + formula = [int(x_value) / int(y_value)] + remainder + case _: + raise ValueError("syntax error") + except: + raise ValueError("syntax error") +``` + +[structural-pattern-matching]: https://peps.python.org/pep-0636/ +```` + +[bracket-notation]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations +[dict-get]: https://docs.python.org/3/library/stdtypes.html#dict.get +[endswith]: https://docs.python.org/3.9/library/stdtypes.html#str.endswith +[handling-exceptions]: https://docs.python.org/3.11/tutorial/errors.html#handling-exceptions +[keyerror]: https://docs.python.org/3/library/exceptions.html#KeyError +[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 +[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 00000000..700804b6 --- /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/instructions.append.md b/exercises/practice/wordy/.docs/instructions.append.md index 0a0d309f..ca646b7b 100644 --- a/exercises/practice/wordy/.docs/instructions.append.md +++ b/exercises/practice/wordy/.docs/instructions.append.md @@ -2,11 +2,12 @@ ## 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. + +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 +16,22 @@ 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] : + +```python +while len(equation) > 1: + try: + x_value, operation, y_value, *rest = equation + + ... + + except: + 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