Skip to content

Commit f30e4f0

Browse files
committed
Suggestions and edits for flower-field approaches intro doc.
1 parent 3fd51d0 commit f30e4f0

File tree

1 file changed

+167
-105
lines changed

1 file changed

+167
-105
lines changed
Lines changed: 167 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,150 +1,174 @@
11

22
# Introduction
33

4-
This exercise tests iteration, logic and error handling.
4+
The Flower Field exercise is designed to practice iteration, boolean logic and raising errors with error messages.
5+
It also provides ample opportunities for working with `lists`, `list-indexing`, `comprehensions`, `tuples`, and `generator-expressions`.
56

6-
## General considerations
77

8-
It is possible to break the exercise down into a series of sub-tasks, with plenty of scope to mix and match approaches within these.
8+
## General considerations and guidance for the exercise
9+
10+
It is possible (_and potentially easier_) to break the problem down into a series of sub-tasks, with plenty of scope to mix and match strategies within these sections:
911

1012
- Is the board valid?
1113
- Is the current square a flower?
1214
- What are the valid neighboring squares, and how many of them contain flowers?
1315

14-
Core Python does not support matrices, nor N-dimensional arrays more generally, though these are at the heart of many third-party packages such as NumPy.
16+
Core Python does not support matrices nor N-dimensional arrays, though these are at the heart of many third-party packages such as NumPy.
17+
Due to this limitation, the input board and final result for this exercise are implemented in the tests as a `list` of strings; one string per "row" of the board.
18+
19+
20+
Intermediate processing for the problem is likely to use lists of lists with a final `''.join()` for each "row" in the returned single `list`, although other strategies could be employed.
21+
Helpfully, Python considers both [lists][ordered-sequences] and [strings][text-sequences] as [sequence types][common-sequence-operations], and can iterate over/index into both in the same fashion.
1522

16-
Thus, the input board and the final result are implemented as lists of strings.
17-
Intermediate processing is likely to use lists of lists, plus a final `''.join()` for each row in the `return` statement.
1823

19-
Helpfully, Python can iterate over strings exactly like lists.
24+
## Validating boards
2025

21-
## Valid boards
26+
The "board" or "field" must be rectangular: essentially, all rows must be the same length as the first row.
27+
This means that any board can be invalidated using the built-ins `all()` or `any()` to check for equal lengths of the strings in the `list` (_see an example below_).
2228

23-
The board must be rectangular: essentially, all rows must be the same length as the first row.
29+
Perhaps surprisingly, both row and column lengths **can be zero/empty**, so an apparently "non-existent board or field" is considered valid and needs special handling:
2430

25-
Perhaps surprisingly, the row and column lengths can be zero, so an apparently non-existent board is valid and needs special handling.
2631

2732
```python
2833
rows = len(garden)
2934
if rows > 0:
3035
cols = len(garden[0])
3136
else:
3237
return []
38+
3339
if any([len(row) != cols for row in garden]):
3440
raise ValueError('The board is invalid with current input.')
3541
```
3642

37-
Additionally, the only valid entries are a space `' '` or an asterisk `'*'`. All other characters should raise an error.
43+
Additionally, the only valid entries for the board/field are a space `' '` (_position empty_) or an asterisk `'*'` (_flower in position_).
44+
All other characters are _invalid_ and should `raise` an error with an appropriate error message.
45+
The exercise [tests][flower-field-tests] check for specific error messages including punctuation, so should be read or copied carefully.
46+
47+
Some solutions use regular expressions for these checks, but there are simpler (_and more performant_) options:
3848

39-
Some solutions use regular expressions for this test, but there are simpler options:
4049

4150
```python
4251
if garden[row][col] not in (' ', '*'):
4352
# raise error
4453
```
4554

46-
Depending on how the code is structured, it may be possible to combine the tests.
47-
48-
More commonly, the board dimensions are checked at the beginning.
49-
Invalid characters are then detected while iterating through the board.
50-
51-
## Processing squares
55+
Depending on how the code is structured, it may be possible to combine the checks for row length with the checks for valid characters.
56+
More commonly, board/field dimensions are checked at the beginning.
57+
Invalid characters are then detected while iterating through the rows of the board/field.
5258

53-
Squares containing a flower are easy: just copy `'*'` to the corresponding square in the result.
5459

55-
For empty squares, the challenge is to count how many flowers are in the adjacent squares.
60+
## Processing squares and finding occupied neighbors
5661

57-
*How many squares are adjacent?* In the middle of a reasonably large board there will be 8, but this is reduced for squares at the edges or corners.
62+
Squares containing a flower are straightforward: you can copy `'*'` to the corresponding square in the results `list`.
5863

59-
### 1. Nested `if..elif` statements
64+
Empty squares present a challenge: count how many flowers are in all the squares _adjacent_ to it.
65+
But *How many squares are adjacent to the current position?*
66+
In the middle of a reasonably large board there will be 8 adjacent squares, but this is reduced for squares at edges or corners.
6067

61-
This can be made to work, but quickly becomes very verbose.
6268

63-
### 2. Explicit coordinates
64-
65-
```python
66-
def count_adjacent(r, c):
67-
adj_squares = (
68-
(r-1, c-1), (r-1, c), (r-1, c+1),
69-
(r, c-1), (r, c+1),
70-
(r+1, c-1), (r+1, c), (r+1, c+1),
71-
)
72-
73-
# which are on the board?
74-
neighbors = [garden[r][c] for r, c in adj_squares
75-
if 0 <= r < rows and 0 <= c < cols]
76-
# how many contain flowers?
77-
return len([adj for adj in neighbors if adj == '*'])
78-
```
79-
80-
This lists all the possibilities, then filters out any squares that fall outside the board.
69+
### Some square processing methods
8170

8271
Note that we only want a _count_ of nearby flowers.
8372
Their precise _location_ is irrelevant.
8473

85-
### 3. Use a comprehension or generator
8674

87-
A key insight is that we can work on a 3x3 block of cells, because we already ensured that the central cell does *not* contain a flower that would affect our count.
88-
89-
```python
90-
squares = ((row + row_diff, col + col_diff)
91-
for row_diff in (-1, 0, 1)
92-
for col_diff in (-1, 0, 1))
93-
```
94-
95-
We can then filter and count as in the previous code.
96-
97-
### 4. Use complex numbers
98-
99-
A particularly elegant solution is to treat the board as a portion of the complex plane.
100-
101-
In Python, [complex numbers][complex-numbers] are a standard numeric type, alongside integers and floats.
102-
103-
*This is less widely known than it deserves to be.*
104-
105-
```python
106-
def neighbors(cell: complex) -> Generator[complex, None, None]:
107-
"""Yield all eight neighboring cells."""
108-
for x in (-1, 0, 1):
109-
for y in (-1, 0, 1):
110-
if offset := x + y * 1j:
111-
yield cell + offset
112-
```
113-
114-
The constructor for a complex number is `complex(x, y)` or (as here) `x + y * 1j`, where `x` and `y` are the real and imaginary parts, respectively.
115-
116-
There are two properties of complex numbers that help us in this case:
117-
118-
- The real and imaginary parts act independently under addition.
119-
- The value `complex(0, 0)` is the complex zero, which like integer zero is treated as False in Python conditionals.
120-
121-
A tuple of integers would not work as a substitute, because `+` behaves as the concatenation operator for tuples:
122-
123-
```python
124-
>>> complex(1, 2) + complex(3, 4)
125-
(4+6j)
126-
>>> (1, 2) + (3, 4)
127-
(1, 2, 3, 4)
128-
```
129-
130-
Note also the use of the ["walrus" operator][walrus-operator] `:=` in the definition of `offset` above.
131-
132-
This relatively recent addition to Python simplifies variable assignment within the limited scope of an if statement or a comprehension.
133-
134-
## Putting it all together
135-
136-
The example below is an object-oriented approach using complex numbers, included because it is a particularly clear illustration of the various topics discussed above.
75+
1. Nested `if..elif` statements
76+
77+
This can be made to work, but can quickly become very verbose or confusing if not thought out carefully:
78+
79+
```python
80+
for index_i, _ in enumerate(flowerfield):
81+
temp_row = ""
82+
for index_j in range(column_count):
83+
if flowerfield[index_i][index_j].isspace():
84+
temp_row += count_flowers(flowerfield, index_i, index_j)
85+
elif flowerfield[index_i][index_j] == "*":
86+
temp_row += "*"
87+
else:
88+
raise ValueError("The board is invalid with current input.")
89+
flowerfield[index_i] = temp_row
90+
```
91+
92+
2. Explicit coordinates
93+
94+
List all the possibilities then filter out any squares that fall outside the board:
95+
96+
```python
97+
def count_adjacent(row, col):
98+
adj_squares = (
99+
(row-1, col-1), (row-1, col), (row-1, col+1),
100+
(row, col-1), (row, col+1),
101+
(row+1, col-1), (row+1, col), (row+1, col+1),
102+
)
103+
104+
# which are on the board?
105+
neighbors = [garden[row][col] for row, col in adj_squares
106+
if 0 <= row < rows and 0 <= col < cols]
107+
# how many contain flowers?
108+
return len([adj for adj in neighbors if adj == '*'])
109+
```
110+
111+
3. Using a comprehension or generator expression
112+
113+
```python
114+
# Using a list comprehension
115+
squares = [(row + row_diff, col + col_diff)
116+
for row_diff in (-1, 0, 1)
117+
for col_diff in (-1, 0, 1)]
118+
119+
# Using a generator expression
120+
squares = ((row + row_diff, col + col_diff)
121+
for row_diff in (-1, 0, 1)
122+
for col_diff in (-1, 0, 1))
123+
```
124+
125+
A key insight here is that we can work on a 3x3 block of cells: we already ensured that the central cell does *not* contain a flower that would affect our count.
126+
We can then filter and count as in the `count_adjacent` function in the previous code.
127+
128+
4. Using complex numbers
129+
130+
```python
131+
def neighbors(cell):
132+
"""Yield all eight neighboring cells."""
133+
for x in (-1, 0, 1):
134+
for y in (-1, 0, 1):
135+
if offset := x + y * 1j:
136+
yield cell + offset
137+
```
138+
139+
A particularly elegant solution is to treat the board/field as a portion of the complex plane.
140+
In Python, [complex numbers][complex-numbers] are a standard numeric type, alongside integers and floats.
141+
*This is less widely known than it deserves to be.*
142+
143+
The constructor for a complex number is `complex(x, y)` or (as here) `x + y * 1j`, where `x` and `y` are the real and imaginary parts, respectively.
144+
145+
There are two properties of complex numbers that help us in this case:
146+
- The real and imaginary parts act independently under addition.
147+
- The value `complex(0, 0)` is the complex zero, which like integer zero is treated as False in Python conditionals.
148+
149+
A tuple of integers would not work as a substitute, because `+` behaves as the concatenation operator for tuples:
150+
151+
```python
152+
>>> complex(1, 2) + complex(3, 4)
153+
(4+6j)
154+
>>> (1, 2) + (3, 4)
155+
(1, 2, 3, 4)
156+
```
157+
158+
Note also the use of the ["walrus" operator][walrus-operator] `:=` in the definition of `offset` above.
159+
This relatively recent addition to Python simplifies variable assignment within the limited scope of an if statement or a comprehension.
160+
161+
162+
## Ways of putting it all together
163+
164+
The example below takes an object-oriented approach using complex numbers, included because it is a particularly clear illustration of the various topics discussed above.
137165

138166
All validation checks are done in the object constructor.
139167

140168
```python
141169
"""Flower Field."""
142170

143-
# The import is only needed for type annotation, so can be considered optional.
144-
from typing import Generator
145-
146-
147-
def neighbors(cell: complex) -> Generator[complex, None, None]:
171+
def neighbors(cell):
148172
"""Yield all eight neighboring cells."""
149173
for x in (-1, 0, 1):
150174
for y in (-1, 0, 1):
@@ -155,7 +179,7 @@ def neighbors(cell: complex) -> Generator[complex, None, None]:
155179
class Garden:
156180
"""garden helper."""
157181

158-
def __init__(self, data: list[str]):
182+
def __init__(self, data):
159183
"""Initialize."""
160184
self.height = len(data)
161185
self.width = len(data[0]) if data else 0
@@ -170,26 +194,64 @@ class Garden:
170194
if not all(v in (" ", "*") for v in self.data.values()):
171195
raise ValueError("The board is invalid with current input.")
172196

173-
def val(self, x: int, y: int) -> str:
197+
def val(self, x, y):
174198
"""Return the value for one square."""
175199
cur = x + y * 1j
176200
if self.data[cur] == "*":
177201
return "*"
178202
count = sum(self.data.get(neighbor, "") == "*" for neighbor in neighbors(cur))
179203
return str(count) if count else " "
180204

181-
def convert(self) -> list[str]:
205+
def convert(self):
182206
"""Convert the garden."""
183-
return [
184-
"".join(self.val(x, y) for x in range(self.width))
185-
for y in range(self.height)
186-
]
207+
return ["".join(self.val(x, y)
208+
for x in range(self.width))
209+
for y in range(self.height)]
187210

188211

189-
def annotate(garden: list[str]) -> list[str]:
212+
def annotate(garden):
190213
"""Annotate a garden."""
191214
return Garden(garden).convert()
192215
```
193216

217+
The example below takes an opposite strategy, using a single function, `list comprehensions`, and nested `if-elif` statements":
218+
219+
```python
220+
def annotate(garden):
221+
grid = [[0 for _ in row] for row in garden]
222+
positions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
223+
224+
for col, row in enumerate(garden):
225+
# Checking that the board/field is rectangular up front.
226+
if len(row) != len(grid[0]):
227+
raise ValueError("The board is invalid with current input.")
228+
229+
# Validating square content.
230+
for index, square in enumerate(row):
231+
if square == " ":
232+
continue
233+
elif square != "*":
234+
raise ValueError("The board is invalid with current input.")
235+
grid[col][index] = "*"
236+
237+
for dr, dc in positions:
238+
dr += col
239+
if dr < 0 or dr >= len(grid):
240+
continue
241+
242+
dc += index
243+
if dc < 0 or dc >= len(grid[dr]):
244+
continue
245+
246+
if grid[dr][dc] != "*":
247+
grid[dr][dc] += 1
248+
249+
return ["".join(" " if square == 0 else str(square) for square in row) for row in grid]
250+
```
251+
252+
[common-sequence-operations]: https://docs.python.org/3.13/library/stdtypes.html#common-sequence-operations
194253
[complex-numbers]: https://exercism.org/tracks/python/concepts/complex-numbers
254+
[flower-field-tests]: https://github.com/exercism/python/blob/main/exercises/practice/flower-field/flower_field_test.py
255+
[ordered-sequences]: https://docs.python.org/3.13/library/stdtypes.html#sequence-types-list-tuple-range
256+
[text-sequences]: https://docs.python.org/3.13/library/stdtypes.html#text-sequence-type-str
195257
[walrus-operator]: https://peps.python.org/pep-0572/

0 commit comments

Comments
 (0)