Skip to content

Commit 63cc10b

Browse files
authored
Further refinements to approaches. Added hits.md file. (#3788)
1 parent d2cf34b commit 63cc10b

File tree

4 files changed

+191
-99
lines changed

4 files changed

+191
-99
lines changed

exercises/practice/wordy/.approaches/functools-reduce/content.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def answer(question):
3333
raise ValueError("syntax error")
3434

3535
# Evaluate the expression from left to right using functools.reduce().
36-
# Look up each operation in the operation dictionary.
36+
# Look up each operation in the OPERATORS dictionary.
3737
return reduce(lambda x, y: OPERATORS[operations.pop(0)](x, y), digits)
3838
```
3939

exercises/practice/wordy/.approaches/introduction.md

Lines changed: 53 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,39 @@ This means that for some of the test cases, the solution will not be the same as
88
## General Guidance
99

1010
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].
11-
If a single number remains after removing the "question", it should be converted to an [`int`][int] and returned as the answer.
11+
12+
13+
If a single number remains after removing the "question" pieces, it should be converted to an [`int`][int] and returned as the answer.
14+
15+
1216
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.
1317
This includes any "extra" spaces between numbers.
1418

19+
1520
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.
1621
As shown in various approaches, there are multiple strategies for validating questions, with no one "canonical" solution.
17-
One very effective approach is to check if a question starts with "What is", ends with "?", and includes only valid operations.
18-
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.
1922

20-
There are various Pythonic ways to go about the cleaning, parsing, and calculation steps of Wordy.
21-
For cleaning the "question" portion of the problem, [`str.removeprefix`][removeprefix] and
23+
24+
One very effective validation approach is to check if a question starts with "What is", ends with "?", and does not include the word "cubed".
25+
Any other question formulation becomes a `ValueError("unknown operation")`.
26+
This very restrictive approach 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.
27+
28+
29+
Proceeding from validation, there are many Pythonic ways to go about the cleaning, parsing, and calculation steps of Wordy.
30+
However, they all follow these general steps:
31+
32+
1. Remove the parts of the question string that do not apply to calculating the answer.
33+
2. Iterate over the question, determining which words are numbers, and which are meant to be mathematical operations.
34+
- _Converting the question string into a `list` of words is hugely helpful here, but not absolutely necessary._
35+
3. **_Starting from the left_**, take the first three elements and convert number strings to `int` and operations words to +, -, *, /.
36+
4. Apply the operation to the numbers, which should result in a single number.
37+
- _Employing a `try-except` block around the conversion and operator application steps can trap any errors thrown and make the code both "safer" and less complex._
38+
5. Use the calculated number from step 4 as the start for the next "trio" (_number, operation, number_) in the question. The calculated number + the remainder of the question becomes the question being worked on in the next iteration.
39+
- _Using a `while-loop` with a test on the length of the question to do calculation is a very common strategy._
40+
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")`.
41+
42+
43+
For cleaning the question, [`str.removeprefix`][removeprefix] and
2244
[`str.removesuffix`][removesuffix] introduced in `Python 3.9` can be very useful:
2345

2446

@@ -53,73 +75,70 @@ You can also use [`str.startswith`][startswith] and [`str.endswith`][endswith] i
5375
```
5476

5577

56-
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.
57-
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.
78+
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.
79+
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.
5880
Finally, [`str.strip`][strip] and its variants are very useful for cleaning up any leftover leading or trailing whitespace.
5981

60-
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.
82+
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.
83+
6184

6285
For math operations, many solutions involve importing and using methods from the [operator][operator] module in combination with different looping, parsing, and substitution strategies.
63-
Some solutions use either [lambda][lambdas] expressions or [dunder/"special" methods][dunder-methods] to replace words with arithmetic operations.
64-
However, the exercise can be solved without using `operator`, `lambdas`, or `dunder-methods`.
86+
Some solutions use either [lambda][lambdas] expressions, [dunder/"special" methods][dunder-methods], or even `eval()` to replace words with arithmetic operations.
87+
However, the exercise can be solved **without** using `operator`, `lambdas`, `dunder-methods` or `eval`.
88+
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.
89+
6590

91+
~~~~exercism/caution
6692
Using [`eval`][eval] for the operations might seem convenient, but it is a [dangerous][eval-danger] and possibly [destructive][eval-destructive] approach.
6793
It is also entirely unnecessary, as the other methods described here are safer and equally performant.
94+
~~~~
6895

6996

7097
## Approach: String, List, and Dictionary Methods
7198

7299

73100
```python
74-
OPERATIONS = {"plus": '+', "minus": '-', "multiplied": '*', "divided": '/'}
75-
76-
77101
def answer(question):
78102
if not question.startswith("What is") or "cubed" in question:
79103
raise ValueError("unknown operation")
80104

81-
question = question.removeprefix("What is").removesuffix("?").strip()
105+
question = question.removeprefix("What is")
106+
question = question.removesuffix("?")
107+
question = question.replace("by", "")
108+
question = question.strip()
82109

83110
if not question:
84111
raise ValueError("syntax error")
85-
86-
if question.isdigit():
87-
return int(question)
88-
89-
formula = []
90-
for operation in question.split():
91-
if operation == 'by':
92-
continue
93-
else:
94-
formula.append(OPERATIONS.get(operation, operation))
95112

113+
formula = question.split()
96114
while len(formula) > 1:
97115
try:
98116
x_value = int(formula[0])
99-
symbol = formula[1]
100117
y_value = int(formula[2])
118+
symbol = formula[1]
101119
remainder = formula[3:]
102120

103-
if symbol == "+":
121+
if symbol == "plus":
104122
formula = [x_value + y_value] + remainder
105-
elif symbol == "-":
123+
elif symbol == "minus":
106124
formula = [x_value - y_value] + remainder
107-
elif symbol == "*":
125+
elif symbol == "multiplied":
108126
formula = [x_value * y_value] + remainder
109-
elif symbol == "/":
127+
elif symbol == "divided":
110128
formula = [x_value / y_value] + remainder
111129
else:
112130
raise ValueError("syntax error")
113131
except:
114132
raise ValueError("syntax error")
115133

116-
return formula[0]
134+
return int(formula[0])
117135
```
118136

119-
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.
137+
This approach uses only data structures and methods (_[str methods][str-methods], [list()][list], loops, etc._) from core Python, and does not import any extra modules.
120138
It may have more lines of code than average, but it is clear to follow and fairly straightforward to reason about.
121139
It does use a [try-except][handling-exceptions] block for handling unknown operators.
122-
As an alternative to the `formula` loop-append, a [list-comprehension][list-comprehension] can be used to create the initial parsed formula.
140+
141+
Alternatives could use a [dictionary][dict] to store word --> operator mappings that could be looked up in the `while-loop` using [`<dict>.get()`][dict-get], among other strategies.
123142

124143
For more details and variations, read the [String, List and Dictionary Methods][approach-string-list-and-dict-methods] approach.
125144

@@ -350,7 +369,7 @@ def answer(question):
350369
```
351370

352371

353-
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].
372+
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].
354373
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].
355374
If desired, the `operator` imports can be replaced with a dictionary of `lambda` expressions or `dunder-methods`.
356375

@@ -418,9 +437,6 @@ For more detail on this solution, take a look at the [dunder method with `__geta
418437
[dict]: https://docs.python.org/3/library/stdtypes.html#dict
419438
[dunder-methods]: https://www.pythonmorsels.com/what-are-dunder-methods/?watch
420439
[endswith]: https://docs.python.org/3.9/library/stdtypes.html#str.endswith
421-
[eval-danger]: https://softwareengineering.stackexchange.com/questions/311507/why-are-eval-like-features-considered-evil-in-contrast-to-other-possibly-harmfu
422-
[eval-destructive]: https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html
423-
[eval]: https://docs.python.org/3/library/functions.html?#eval
424440
[filter]: https://docs.python.org/3/library/functions.html#filter
425441
[find]: https://docs.python.org/3.9/library/stdtypes.html#str.find
426442
[functools-reduce]: https://docs.python.org/3/library/functools.html#functools.reduce
@@ -444,4 +460,5 @@ For more detail on this solution, take a look at the [dunder method with `__geta
444460
[split]: https://docs.python.org/3.9/library/stdtypes.html#str.split
445461
[startswith]: https://docs.python.org/3.9/library/stdtypes.html#str.startswith
446462
[strip]: https://docs.python.org/3.9/library/stdtypes.html#str.strip
463+
[str-methods]: https://docs.python.org/3/library/stdtypes.html#string-methods
447464
[value-error]: https://docs.python.org/3.11/library/exceptions.html#ValueError

0 commit comments

Comments
 (0)