Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions exercises/practice/wordy/.approaches/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,23 @@
"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.",
"blurb": "Use core Python features to solve word problems.",
"authors": ["BethanyG"],
"contributors": ["yrahcaz7"]
},
{
"uuid": "d3ff485a-defe-42d9-b9c6-c38019221ffa",
"slug": "import-callables-from-operator",
"title": "Import Callables from the Operator Module",
"blurb": "Use Operator Module Methods to Solve Word Problems.",
"title": "Import callables from the operator module",
"blurb": "Use operator module methods to solve word problems.",
"authors": ["BethanyG"],
"contributors": ["yrahcaz7"]
},
{
"uuid": "61f44943-8a12-471b-ab15-d0d10fa4f72f",
"slug": "regex-with-operator-module",
"title": "Regex with the Operator Module",
"blurb": "Use Regex with the Callables from Operator to solve word problems.",
"blurb": "Use regex with the callables from the operator module to solve word problems.",
"authors": ["BethanyG"],
"contributors": ["yrahcaz7"]
},
Expand Down Expand Up @@ -56,9 +56,16 @@
"uuid": "d643e2b4-daee-422d-b8d3-2cad2f439db5",
"slug": "dunder-getattribute",
"title": "Dunder with __getattribute__",
"blurb": "Use dunder methods with __getattribute__.",
"blurb": "Use dunder methods with __getattribute__ to calculate solutions.",
"authors": ["bobahop"],
"contributors": ["yrahcaz7"]
},
{
"uuid": "924aa814-0b03-40c1-9ee4-90b7fcdc1ff8",
"slug": "tuple-and-index",
"title": "Tuples with sequence.index",
"blurb": "Use tuples with the sequence.index method to calculate solutions.",
"authors": ["yrahcaz7"]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ However, this could easily be accomplished by either using [chained][method-chai

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")`.
Anything else (_or having [`None`][none]/an unknown operation in the operations list_) is a `ValueError("syntax error")`.


The final call to `functools.reduce` essentially performs the same steps as the `while-loop` implementation, with the `lambda-expression` passing successive items of the `digits` list to the popped and looked-up operation from the operations `list` (_used as a [callable][callable] with `()`_), until it is reduced to one number and returned.
Expand All @@ -72,7 +72,7 @@ It could be argued that writing the code as a `while-loop` or recursive function

<br>

## Variation 1: Use a dictionary of `lambdas` instead of importing from `operator`
## Variation 1: Use a dictionary of `lambda`s instead of importing from `operator`

The imports from the `operator` module can be swapped out for a dictionary of `lambda-expressions` (or calls to `dunder-methods`), if so desired.
The same cautions apply here as were discussed in the [lambdas in a dictionary][approach-lambdas-in-a-dictionary] approach:
Expand Down Expand Up @@ -122,4 +122,5 @@ def answer(question):
[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
[none]: https://docs.python.org/3/library/constants.html#None
[sequence-operations]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations
81 changes: 72 additions & 9 deletions exercises/practice/wordy/.approaches/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ You can also use [`str.startswith`][startswith] and [`str.endswith`][endswith] i
'califragilistic'
```

Different combinations of [`str.find`][find], [`str.rfind`][rfind], or [`str.index`][index] with string slicing could also be used to clean up the initial question.
Different combinations of [`str.find`][find], [`str.rfind`][rfind], or [`str.index`][str-index] with string slicing could also be used to clean up the initial question.
A [regex][regex] could be used to process the question as well, but might be considered overkill given the fixed nature of the prefix/suffix and operations.
Finally, [`str.strip`][strip] and its variants are very useful for cleaning up any leftover leading or trailing whitespace.

Expand All @@ -88,7 +88,7 @@ Many solutions then use [`str.split`][split] to process the remaining "cleaned"
For math operations, many solutions involve importing and using methods from the [operator][operator] module.
Some solutions use either [lambda][lambdas] expressions, [dunder/"special" methods][dunder-methods], or even `eval()` to replace words with arithmetic operations.

However, the exercise can be solved without using `operator`, `lambdas`, `dunder-methods` or `eval`.
However, the exercise can be solved without using `operator`, `lambda`s, `dunder-methods` or `eval`.
It is recommended that you first start by solving it _without_ "advanced" strategies, and then refine your solution into something more compact or complex as you learn and practice.

<br>
Expand Down Expand Up @@ -145,7 +145,7 @@ def answer(question):
return int(formula[0])
```

This approach uses only data structures and methods (_[`str` methods][str-methods], [`list()`][list], loOPERATORS, etc._) from core Python, and does not import any extra modules.
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.
It may have more lines of code than average, but it is clear to follow and fairly straightforward to reason about.

This approach uses a [`try-except`][handling-exceptions] statement for handling unknown operators.
Expand Down Expand Up @@ -295,12 +295,12 @@ def answer(question):
```

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.
These `lambda`s 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.
Since this is not a large program, debugging these `lambda`s is fairly straightforward.
These "hand-crafted" `lambda`s 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.

Expand Down Expand Up @@ -340,11 +340,11 @@ def calculate(equation):
return calculate(equation)
```

Like previous approaches that substitute methods from `operator` for `lambdas` or list comprehensions for `loOPERATORS` that append to a `list` — `recursion` can be substituted for the `while-loop` that many solutions use to process a parsed word problem.
Like previous approaches that substitute methods from `operator` for `lambda`s 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.
The dictionary in this example could use functions from `operator`, `lambda`s, `dunder-methods`, or other strategies — as long as they can be applied in the `calculate()` function.

For more details, take a look at the [Recursion for Iteration][approach-recursion] approach.

Expand Down Expand Up @@ -441,6 +441,67 @@ This is why the `operator` module exists — It is a vehicle for providing calla

For more detail on this solution, take a look at the [dunder methods with `__getattribute__` approach][approach-dunder-getattribute].

## Tuples with `sequence.index()`

```python
OPERATOR_WORDS = ("plus", "minus", "multiplied", "divided")

EXTRA_OPERATOR_WORDS = (None, None, "by", "by")


def answer(question):
if not question.startswith("What is ") or not question.endswith("?"):
raise ValueError("syntax error")

words = question[:-1].split(" ")
result = str_to_int(words[2])
index = 3

while index < len(words):
try:
operator_index = OPERATOR_WORDS.index(words[index])
except ValueError:
str_to_int(words[index], "unknown operation")
raise ValueError("syntax error")

operand_index = index + 1
if EXTRA_OPERATOR_WORDS[operator_index] is not None:
if index + 1 >= len(words) or words[index + 1] != EXTRA_OPERATOR_WORDS[operator_index]:
raise ValueError("syntax error")
operand_index += 1

if operand_index >= len(words):
raise ValueError("syntax error")

operand = str_to_int(words[operand_index])
match operator_index:
case 0: result += operand
case 1: result -= operand
case 2: result *= operand
case 3: result //= operand

index = operand_index + 1

return result


def str_to_int(string, err_msg = "syntax error"):
try:
return int(string)
except ValueError:
raise ValueError(err_msg)
```

This approach only uses data structures and methods from core Python, and does not import any modules.
However, it is rather different from the [String, List and Dictionary Methods][approach-string-list-and-dict-methods] approach.

This approach uses a [`try-except`][handling-exceptions] block for handling incorrect operator placement and unknown operators.
It does this by using [`OPERATOR_WORDS.index()`][sequence-index-method] to find the index of the operator, as `.index()` will throw an error if it does not find the item in `OPERATOR_WORDS`.
The `except` block will catch this error and `raise` either a `ValueError("syntax error")` or a `ValueError("unknown operation")` instead.
(You can look at [exception chaining in the Python docs][exception-chaining] for further detail on this subject.)

For more details, read the [Tuples with `sequence.index()`][approach-tuple-and-index] approach.

[PEMDAS]: https://www.mathnasium.com/blog/what-is-pemdas
[approach-dunder-getattribute]: https://exercism.org/tracks/python/exercises/wordy/approaches/dunder-getattribute
[approach-functools-reduce]: https://exercism.org/tracks/python/exercises/wordy/approaches/functools-reduce
Expand All @@ -449,6 +510,7 @@ For more detail on this solution, take a look at the [dunder methods with `__get
[approach-recursion]: https://exercism.org/tracks/python/exercises/wordy/approaches/recursion
[approach-regex-with-operator-module]: https://exercism.org/tracks/python/exercises/wordy/approaches/regex-with-operator-module
[approach-string-list-and-dict-methods]: https://exercism.org/tracks/python/exercises/wordy/approaches/string-list-and-dict-methods
[approach-tuple-and-index]: https://exercism.org/tracks/python/exercises/wordy/approaches/tuple-and-index
[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
Expand All @@ -460,7 +522,6 @@ For more detail on this solution, take a look at the [dunder methods with `__get
[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
Expand All @@ -473,10 +534,12 @@ For more detail on this solution, take a look at the [dunder methods with `__get
[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-index-method]: https://docs.python.org/3/library/stdtypes.html#sequence.index
[sequence-operations]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations
[special-methods]: https://docs.python.org/3/reference/datamodel.html#specialnames
[split]: https://docs.python.org/3.9/library/stdtypes.html#str.split
[startswith]: https://docs.python.org/3.9/library/stdtypes.html#str.startswith
[strip]: https://docs.python.org/3.9/library/stdtypes.html#str.strip
[str-index]: https://docs.python.org/3.9/library/stdtypes.html#str.index
[str-methods]: https://docs.python.org/3/library/stdtypes.html#string-methods
[value-error]: https://docs.python.org/3.11/library/exceptions.html#ValueError
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ The major difference here is the use of [`lambda expressions`][lambdas] in place

`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.
`lambda`s 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 [`lambda`s][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`:

Expand Down
81 changes: 81 additions & 0 deletions exercises/practice/wordy/.approaches/tuple-and-index/content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Tuples with `sequence.index()`

```python
OPERATOR_WORDS = ("plus", "minus", "multiplied", "divided")

EXTRA_OPERATOR_WORDS = (None, None, "by", "by")


def answer(question):
if not question.startswith("What is ") or not question.endswith("?"):
raise ValueError("syntax error")

words = question[:-1].split(" ")
result = str_to_int(words[2])
index = 3

while index < len(words):
try:
operator_index = OPERATOR_WORDS.index(words[index])
except ValueError:
str_to_int(words[index], "unknown operation")
raise ValueError("syntax error")

operand_index = index + 1
if EXTRA_OPERATOR_WORDS[operator_index] is not None:
if index + 1 >= len(words) or words[index + 1] != EXTRA_OPERATOR_WORDS[operator_index]:
raise ValueError("syntax error")
operand_index += 1

if operand_index >= len(words):
raise ValueError("syntax error")

operand = str_to_int(words[operand_index])
match operator_index:
case 0: result += operand
case 1: result -= operand
case 2: result *= operand
case 3: result //= operand

index = operand_index + 1

return result


def str_to_int(string, err_msg = "syntax error"):
try:
return int(string)
except ValueError:
raise ValueError(err_msg)
```

This approach starts with declaring the `OPERATOR_WORDS` tuple, which contains the first word of each phrase that indicates an operator.
The `EXTRA_OPERATOR_WORDS` tuple contains the second word of each phrase if it exists, and [`None`][none] otherwise.

In `answer()`, we start by checking that the question starts with "What is " and ends with "?".
(Unlike many other approaches, it does not rely on explicitly checking for "cubed".)
Next, the question is `split` into a `words` list, excluding the trailing "?".

Then we call the helper function `str_to_int()` to initialize `result` to the first number in the question, or raise a `ValueError` if the third "word" is not a number.
Next, we start the while loop with `index` set to `3`, indicating that we have finished parsing the first `3` words ("What", "is", and the number).

Inside the loop, we use [`OPERATOR_WORDS.index()`][sequence-index-method] to get the index of the operator at index `index` of `words`.
If `words[index]` is not an operator word, the raised `ValueError` will be caught by the [`try-except`][handling-exceptions] statement, and the `except` block will determine the correct error to raise instead.

Next, we need to get the second operand for the operator (we already have `result` for the first one), so we set `operand_index = index + 1`.
However, some operator phrases are multiple words long, so we need to check if `EXTRA_OPERATOR_WORDS[operator_index] is not None`.
If there *is* an extra operator word, then we need to check if it is present as the next word in `words`.
If it is not present, we raise a `ValueError`, else we increment `operand_index` by `1` to get the correct index.

Here we call the helper function again, setting `operand` to the number at index `operand_index`, and raising a `ValueError` if it is not a number.
After this, the approach uses [structural pattern matching][structural-pattern-matching] to modify `result` using `+=`, `-=`, `*=`, or `//=` depending on the `operator_index`.
However, this section could easily be modified to use a similar method to any of the other approaches, such as an `if-elif-else` block or a tuple of `lambda`s.

At the end of the loop, we set `index` to `operand_index + 1`, as we have finished processing everything up to and including `operand_index`.
Then the loop continues until `index` reaches or exceeds `len(words)` or something raises an uncaught error.
When the loop ends, we know that we have finished processing the whole string, and thus we return `result`.

[handling-exceptions]: https://docs.python.org/3.11/tutorial/errors.html#handling-exceptions
[none]: https://docs.python.org/3/library/constants.html#None
[sequence-index-method]: https://docs.python.org/3/library/stdtypes.html#sequence.index
[structural-pattern-matching]: https://peps.python.org/pep-0636/
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
OPERATOR_WORDS = ("plus", "minus", "multiplied", "divided")
EXTRA_OPERATOR_WORDS = (None, None, "by", "by")
...
try:
operator_index = OPERATOR_WORDS.index(words[index])
except ValueError:
str_to_int(words[index], "unknown operation")
raise ValueError("syntax error")
Loading