Skip to content

Commit 47592ca

Browse files
author
Colin Leach
committed
[Flower Field] draft approaches doc
1 parent b3cf79f commit 47592ca

File tree

2 files changed

+202
-0
lines changed

2 files changed

+202
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"introduction": {
3+
"authors": [
4+
"colinleach",
5+
"BethanyG"
6+
]
7+
}
8+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
2+
# Introduction
3+
4+
This exercise tests iteration, logic and error handling.
5+
6+
## General considerations
7+
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.
9+
10+
- Is the board valid?
11+
- Is the current square a flower?
12+
- What are the valid neighboring squares, and how many of them contain flowers?
13+
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.
15+
16+
Thus, the input board and the final result are implemented as lists of strings, though intermediate processing is likely to use lists of lists plus a final `''.join()` for each row in the `return` statement.
17+
18+
Helpfully, Python can iterate over strings exactly like lists.
19+
20+
## Valid boards
21+
22+
The board must be rectangular: essentially, all rows must be the same length as the first row.
23+
24+
Perhaps surprisingly, the row and column lengths can be zero, so an apparently non-existent board is valid and needs special handling.
25+
26+
```python
27+
rows = len(garden)
28+
if rows > 0:
29+
cols = len(garden[0])
30+
else:
31+
return []
32+
if any([len(row) != cols for row in garden]):
33+
raise ValueError('The board is invalid with current input.')
34+
```
35+
36+
Additionally, the only valid entries are a space `' '` or an asterisk `'*'`. All other characters should raise an error.
37+
38+
Some solutions use regular expressions for this test, but there are simpler options:
39+
40+
```python
41+
if garden[row][col] not in (' ', '*'):
42+
# raise error
43+
```
44+
45+
Depending on how the code is structured, it may be possible to combine the tests.
46+
47+
More commonly, the board dimensions are checked at the beginning.
48+
Invalid characters are then detected while iterating through the board.
49+
50+
## Processing squares
51+
52+
Squares containing a flower are easy: just copy `'*'` to the corresponding square in the result.
53+
54+
For empty squares, the challenge is to count how many flowers are in the adjacent squares.
55+
56+
*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.
57+
58+
### 1. Nested `if..elif` statements
59+
60+
This can be made to work, but quickly becomes very verbose.
61+
62+
### 2. Explicit coordinates
63+
64+
```python
65+
def count_adjacent(r, c):
66+
adj_squares = (
67+
(r-1, c-1), (r-1, c), (r-1, c+1),
68+
(r, c-1), (r, c+1),
69+
(r+1, c-1), (r+1, c), (r+1, c+1),
70+
)
71+
72+
# which are on the board?
73+
neighbors = [garden[r][c] for r, c in adj_squares
74+
if 0 <= r < rows and 0 <= c < cols]
75+
# how many contain flowers?
76+
return len([adj for adj in neighbors if adj == '*'])
77+
```
78+
79+
Slightly better, this lists all the possibilities then filters out any that fall outside the board.
80+
81+
Note that we only want a _count_ of nearby flowers.
82+
Their precise _location_ is irrelevant.
83+
84+
### 3. Use a comprehension or generator
85+
86+
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.
87+
88+
```python
89+
squares = ((row + row_diff, col + col_diff)
90+
for row_diff in (-1, 0, 1)
91+
for col_diff in (-1, 0, 1))
92+
```
93+
94+
We can then filter and count as in the previous code.
95+
96+
### 4. Use complex numbers
97+
98+
A particularly elegant solution is to treat the board as a portion of the complex plane.
99+
100+
In Python, [complex numbers][complex-numbers] are a standard numeric type, alongside integers and floats.
101+
102+
*This is less widely known than it deserves to be.*
103+
104+
```python
105+
def neighbors(cell: complex) -> Generator[complex, None, None]:
106+
"""Yield all eight neighboring cells."""
107+
for x in (-1, 0, 1):
108+
for y in (-1, 0, 1):
109+
if offset := x + y * 1j:
110+
yield cell + offset
111+
```
112+
113+
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.
114+
115+
There are two properties of complex numbers that help us in this case:
116+
117+
- The real and imaginary parts act independently under addition.
118+
- The value `complex(0, 0)` is the complex zero, which like integer zero is treated as False in Python conditionals.
119+
120+
A tuple of integers would not work as a substitute, because `+` behaves as the concatenation operator for tuples:
121+
122+
```python
123+
>>> complex(1, 2) + complex(3, 4)
124+
(4+6j)
125+
>>> (1, 2) + (3, 4)
126+
(1, 2, 3, 4)
127+
```
128+
129+
Note also the use of the ["walrus" operator][walrus-operator] `:=` in the definition of `offset` above.
130+
131+
This relatively recent addition to Python simplifies variable assignment within the limited scope of an if statement or a comprehension.
132+
133+
## Putting it all together
134+
135+
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.
136+
137+
All validation checks are done in the object constructor.
138+
139+
```python
140+
"""Flower Garden."""
141+
142+
# The import is only needed for type annotation, so can be considered optional.
143+
from typing import Generator
144+
145+
146+
def neighbors(cell: complex) -> Generator[complex, None, None]:
147+
"""Yield all eight neighboring cells."""
148+
for x in (-1, 0, 1):
149+
for y in (-1, 0, 1):
150+
if offset := x + y * 1j:
151+
yield cell + offset
152+
153+
154+
class Garden:
155+
"""garden helper."""
156+
157+
def __init__(self, data: list[str]):
158+
"""Initialize."""
159+
self.height = len(data)
160+
self.width = len(data[0]) if data else 0
161+
162+
if not all(len(row) == self.width for row in data):
163+
raise ValueError("The board is invalid with current input.")
164+
165+
self.data = {}
166+
for y, line in enumerate(data):
167+
for x, val in enumerate(line):
168+
self.data[x + y * 1j] = val
169+
if not all(v in (" ", "*") for v in self.data.values()):
170+
raise ValueError("The board is invalid with current input.")
171+
172+
def val(self, x: int, y: int) -> str:
173+
"""Return the value for one square."""
174+
cur = x + y * 1j
175+
if self.data[cur] == "*":
176+
return "*"
177+
count = sum(self.data.get(neighbor, "") == "*" for neighbor in neighbors(cur))
178+
return str(count) if count else " "
179+
180+
def convert(self) -> list[str]:
181+
"""Convert the garden."""
182+
return [
183+
"".join(self.val(x, y) for x in range(self.width))
184+
for y in range(self.height)
185+
]
186+
187+
188+
def annotate(garden: list[str]) -> list[str]:
189+
"""Annotate a garden."""
190+
return Garden(garden).convert()
191+
```
192+
193+
[complex-numbers]: https://exercism.org/tracks/python/concepts/complex-numbers
194+
[walrus-operator]: https://peps.python.org/pep-0572/

0 commit comments

Comments
 (0)