From 47592ca0e2c95be84d361c0c789202b90a694d90 Mon Sep 17 00:00:00 2001 From: Colin Leach Date: Wed, 25 Jun 2025 14:17:13 -0700 Subject: [PATCH 1/3] [Flower Field] draft approaches doc --- .../flower-field/.approaches/config.json | 8 + .../flower-field/.approaches/introduction.md | 194 ++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 exercises/practice/flower-field/.approaches/config.json create mode 100644 exercises/practice/flower-field/.approaches/introduction.md diff --git a/exercises/practice/flower-field/.approaches/config.json b/exercises/practice/flower-field/.approaches/config.json new file mode 100644 index 00000000000..cf5b9a7b872 --- /dev/null +++ b/exercises/practice/flower-field/.approaches/config.json @@ -0,0 +1,8 @@ +{ + "introduction": { + "authors": [ + "colinleach", + "BethanyG" + ] + } +} diff --git a/exercises/practice/flower-field/.approaches/introduction.md b/exercises/practice/flower-field/.approaches/introduction.md new file mode 100644 index 00000000000..117054df5a9 --- /dev/null +++ b/exercises/practice/flower-field/.approaches/introduction.md @@ -0,0 +1,194 @@ + +# Introduction + +This exercise tests iteration, logic and error handling. + +## General considerations + +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. + +- Is the board valid? +- Is the current square a flower? +- What are the valid neighboring squares, and how many of them contain flowers? + +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. + +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. + +Helpfully, Python can iterate over strings exactly like lists. + +## Valid boards + +The board must be rectangular: essentially, all rows must be the same length as the first row. + +Perhaps surprisingly, the row and column lengths can be zero, so an apparently non-existent board is valid and needs special handling. + +```python + rows = len(garden) + if rows > 0: + cols = len(garden[0]) + else: + return [] + if any([len(row) != cols for row in garden]): + raise ValueError('The board is invalid with current input.') +``` + +Additionally, the only valid entries are a space `' '` or an asterisk `'*'`. All other characters should raise an error. + +Some solutions use regular expressions for this test, but there are simpler options: + +```python + if garden[row][col] not in (' ', '*'): + # raise error +``` + +Depending on how the code is structured, it may be possible to combine the tests. + +More commonly, the board dimensions are checked at the beginning. +Invalid characters are then detected while iterating through the board. + +## Processing squares + +Squares containing a flower are easy: just copy `'*'` to the corresponding square in the result. + +For empty squares, the challenge is to count how many flowers are in the adjacent squares. + +*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. + +### 1. Nested `if..elif` statements + +This can be made to work, but quickly becomes very verbose. + +### 2. Explicit coordinates + +```python + def count_adjacent(r, c): + adj_squares = ( + (r-1, c-1), (r-1, c), (r-1, c+1), + (r, c-1), (r, c+1), + (r+1, c-1), (r+1, c), (r+1, c+1), + ) + + # which are on the board? + neighbors = [garden[r][c] for r, c in adj_squares + if 0 <= r < rows and 0 <= c < cols] + # how many contain flowers? + return len([adj for adj in neighbors if adj == '*']) +``` + +Slightly better, this lists all the possibilities then filters out any that fall outside the board. + +Note that we only want a _count_ of nearby flowers. +Their precise _location_ is irrelevant. + +### 3. Use a comprehension or generator + +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. + +```python + squares = ((row + row_diff, col + col_diff) + for row_diff in (-1, 0, 1) + for col_diff in (-1, 0, 1)) +``` + +We can then filter and count as in the previous code. + +### 4. Use complex numbers + +A particularly elegant solution is to treat the board as a portion of the complex plane. + +In Python, [complex numbers][complex-numbers] are a standard numeric type, alongside integers and floats. + +*This is less widely known than it deserves to be.* + +```python +def neighbors(cell: complex) -> Generator[complex, None, None]: + """Yield all eight neighboring cells.""" + for x in (-1, 0, 1): + for y in (-1, 0, 1): + if offset := x + y * 1j: + yield cell + offset +``` + +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. + +There are two properties of complex numbers that help us in this case: + +- The real and imaginary parts act independently under addition. +- The value `complex(0, 0)` is the complex zero, which like integer zero is treated as False in Python conditionals. + +A tuple of integers would not work as a substitute, because `+` behaves as the concatenation operator for tuples: + +```python +>>> complex(1, 2) + complex(3, 4) +(4+6j) +>>> (1, 2) + (3, 4) +(1, 2, 3, 4) +``` + +Note also the use of the ["walrus" operator][walrus-operator] `:=` in the definition of `offset` above. + +This relatively recent addition to Python simplifies variable assignment within the limited scope of an if statement or a comprehension. + +## Putting it all together + +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. + +All validation checks are done in the object constructor. + +```python +"""Flower Garden.""" + +# The import is only needed for type annotation, so can be considered optional. +from typing import Generator + + +def neighbors(cell: complex) -> Generator[complex, None, None]: + """Yield all eight neighboring cells.""" + for x in (-1, 0, 1): + for y in (-1, 0, 1): + if offset := x + y * 1j: + yield cell + offset + + +class Garden: + """garden helper.""" + + def __init__(self, data: list[str]): + """Initialize.""" + self.height = len(data) + self.width = len(data[0]) if data else 0 + + if not all(len(row) == self.width for row in data): + raise ValueError("The board is invalid with current input.") + + self.data = {} + for y, line in enumerate(data): + for x, val in enumerate(line): + self.data[x + y * 1j] = val + if not all(v in (" ", "*") for v in self.data.values()): + raise ValueError("The board is invalid with current input.") + + def val(self, x: int, y: int) -> str: + """Return the value for one square.""" + cur = x + y * 1j + if self.data[cur] == "*": + return "*" + count = sum(self.data.get(neighbor, "") == "*" for neighbor in neighbors(cur)) + return str(count) if count else " " + + def convert(self) -> list[str]: + """Convert the garden.""" + return [ + "".join(self.val(x, y) for x in range(self.width)) + for y in range(self.height) + ] + + +def annotate(garden: list[str]) -> list[str]: + """Annotate a garden.""" + return Garden(garden).convert() +``` + +[complex-numbers]: https://exercism.org/tracks/python/concepts/complex-numbers +[walrus-operator]: https://peps.python.org/pep-0572/ From 3fd51d03b63d2123fe8444427ad6c85a3191e888 Mon Sep 17 00:00:00 2001 From: Colin Leach Date: Wed, 25 Jun 2025 14:36:10 -0700 Subject: [PATCH 2/3] minor edits --- .../practice/flower-field/.approaches/introduction.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/exercises/practice/flower-field/.approaches/introduction.md b/exercises/practice/flower-field/.approaches/introduction.md index 117054df5a9..9f9dfdb6003 100644 --- a/exercises/practice/flower-field/.approaches/introduction.md +++ b/exercises/practice/flower-field/.approaches/introduction.md @@ -13,7 +13,8 @@ It is possible to break the exercise down into a series of sub-tasks, with plent 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. -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. +Thus, the input board and the final result are implemented as lists of strings. +Intermediate processing is likely to use lists of lists, plus a final `''.join()` for each row in the `return` statement. Helpfully, Python can iterate over strings exactly like lists. @@ -76,7 +77,7 @@ This can be made to work, but quickly becomes very verbose. return len([adj for adj in neighbors if adj == '*']) ``` -Slightly better, this lists all the possibilities then filters out any that fall outside the board. +This lists all the possibilities, then filters out any squares that fall outside the board. Note that we only want a _count_ of nearby flowers. Their precise _location_ is irrelevant. @@ -137,7 +138,7 @@ The example below is an object-oriented approach using complex numbers, included All validation checks are done in the object constructor. ```python -"""Flower Garden.""" +"""Flower Field.""" # The import is only needed for type annotation, so can be considered optional. from typing import Generator From f30e4f05a9bf8f6e8d6b10d346615c3c6763d525 Mon Sep 17 00:00:00 2001 From: BethanyG Date: Thu, 1 Jan 2026 15:22:32 -0800 Subject: [PATCH 3/3] Suggestions and edits for flower-field approaches intro doc. --- .../flower-field/.approaches/introduction.md | 272 +++++++++++------- 1 file changed, 167 insertions(+), 105 deletions(-) diff --git a/exercises/practice/flower-field/.approaches/introduction.md b/exercises/practice/flower-field/.approaches/introduction.md index 9f9dfdb6003..bafa01678eb 100644 --- a/exercises/practice/flower-field/.approaches/introduction.md +++ b/exercises/practice/flower-field/.approaches/introduction.md @@ -1,28 +1,33 @@ # Introduction -This exercise tests iteration, logic and error handling. +The Flower Field exercise is designed to practice iteration, boolean logic and raising errors with error messages. +It also provides ample opportunities for working with `lists`, `list-indexing`, `comprehensions`, `tuples`, and `generator-expressions`. -## General considerations -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. +## General considerations and guidance for the exercise + +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: - Is the board valid? - Is the current square a flower? - What are the valid neighboring squares, and how many of them contain flowers? -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. +Core Python does not support matrices nor N-dimensional arrays, though these are at the heart of many third-party packages such as NumPy. +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. + + +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. +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. -Thus, the input board and the final result are implemented as lists of strings. -Intermediate processing is likely to use lists of lists, plus a final `''.join()` for each row in the `return` statement. -Helpfully, Python can iterate over strings exactly like lists. +## Validating boards -## Valid boards +The "board" or "field" must be rectangular: essentially, all rows must be the same length as the first row. +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_). -The board must be rectangular: essentially, all rows must be the same length as the first row. +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: -Perhaps surprisingly, the row and column lengths can be zero, so an apparently non-existent board is valid and needs special handling. ```python rows = len(garden) @@ -30,121 +35,140 @@ Perhaps surprisingly, the row and column lengths can be zero, so an apparently n cols = len(garden[0]) else: return [] + if any([len(row) != cols for row in garden]): raise ValueError('The board is invalid with current input.') ``` -Additionally, the only valid entries are a space `' '` or an asterisk `'*'`. All other characters should raise an error. +Additionally, the only valid entries for the board/field are a space `' '` (_position empty_) or an asterisk `'*'` (_flower in position_). + All other characters are _invalid_ and should `raise` an error with an appropriate error message. + The exercise [tests][flower-field-tests] check for specific error messages including punctuation, so should be read or copied carefully. + +Some solutions use regular expressions for these checks, but there are simpler (_and more performant_) options: -Some solutions use regular expressions for this test, but there are simpler options: ```python if garden[row][col] not in (' ', '*'): # raise error ``` -Depending on how the code is structured, it may be possible to combine the tests. - -More commonly, the board dimensions are checked at the beginning. -Invalid characters are then detected while iterating through the board. - -## Processing squares +Depending on how the code is structured, it may be possible to combine the checks for row length with the checks for valid characters. +More commonly, board/field dimensions are checked at the beginning. +Invalid characters are then detected while iterating through the rows of the board/field. -Squares containing a flower are easy: just copy `'*'` to the corresponding square in the result. -For empty squares, the challenge is to count how many flowers are in the adjacent squares. +## Processing squares and finding occupied neighbors -*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. +Squares containing a flower are straightforward: you can copy `'*'` to the corresponding square in the results `list`. -### 1. Nested `if..elif` statements +Empty squares present a challenge: count how many flowers are in all the squares _adjacent_ to it. +But *How many squares are adjacent to the current position?* +In the middle of a reasonably large board there will be 8 adjacent squares, but this is reduced for squares at edges or corners. -This can be made to work, but quickly becomes very verbose. -### 2. Explicit coordinates - -```python - def count_adjacent(r, c): - adj_squares = ( - (r-1, c-1), (r-1, c), (r-1, c+1), - (r, c-1), (r, c+1), - (r+1, c-1), (r+1, c), (r+1, c+1), - ) - - # which are on the board? - neighbors = [garden[r][c] for r, c in adj_squares - if 0 <= r < rows and 0 <= c < cols] - # how many contain flowers? - return len([adj for adj in neighbors if adj == '*']) -``` - -This lists all the possibilities, then filters out any squares that fall outside the board. +### Some square processing methods Note that we only want a _count_ of nearby flowers. Their precise _location_ is irrelevant. -### 3. Use a comprehension or generator -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. - -```python - squares = ((row + row_diff, col + col_diff) - for row_diff in (-1, 0, 1) - for col_diff in (-1, 0, 1)) -``` - -We can then filter and count as in the previous code. - -### 4. Use complex numbers - -A particularly elegant solution is to treat the board as a portion of the complex plane. - -In Python, [complex numbers][complex-numbers] are a standard numeric type, alongside integers and floats. - -*This is less widely known than it deserves to be.* - -```python -def neighbors(cell: complex) -> Generator[complex, None, None]: - """Yield all eight neighboring cells.""" - for x in (-1, 0, 1): - for y in (-1, 0, 1): - if offset := x + y * 1j: - yield cell + offset -``` - -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. - -There are two properties of complex numbers that help us in this case: - -- The real and imaginary parts act independently under addition. -- The value `complex(0, 0)` is the complex zero, which like integer zero is treated as False in Python conditionals. - -A tuple of integers would not work as a substitute, because `+` behaves as the concatenation operator for tuples: - -```python ->>> complex(1, 2) + complex(3, 4) -(4+6j) ->>> (1, 2) + (3, 4) -(1, 2, 3, 4) -``` - -Note also the use of the ["walrus" operator][walrus-operator] `:=` in the definition of `offset` above. - -This relatively recent addition to Python simplifies variable assignment within the limited scope of an if statement or a comprehension. - -## Putting it all together - -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. +1. Nested `if..elif` statements + + This can be made to work, but can quickly become very verbose or confusing if not thought out carefully: + + ```python + for index_i, _ in enumerate(flowerfield): + temp_row = "" + for index_j in range(column_count): + if flowerfield[index_i][index_j].isspace(): + temp_row += count_flowers(flowerfield, index_i, index_j) + elif flowerfield[index_i][index_j] == "*": + temp_row += "*" + else: + raise ValueError("The board is invalid with current input.") + flowerfield[index_i] = temp_row + ``` + +2. Explicit coordinates + + List all the possibilities then filter out any squares that fall outside the board: + + ```python + def count_adjacent(row, col): + adj_squares = ( + (row-1, col-1), (row-1, col), (row-1, col+1), + (row, col-1), (row, col+1), + (row+1, col-1), (row+1, col), (row+1, col+1), + ) + + # which are on the board? + neighbors = [garden[row][col] for row, col in adj_squares + if 0 <= row < rows and 0 <= col < cols] + # how many contain flowers? + return len([adj for adj in neighbors if adj == '*']) + ``` + +3. Using a comprehension or generator expression + + ```python + # Using a list comprehension + squares = [(row + row_diff, col + col_diff) + for row_diff in (-1, 0, 1) + for col_diff in (-1, 0, 1)] + + # Using a generator expression + squares = ((row + row_diff, col + col_diff) + for row_diff in (-1, 0, 1) + for col_diff in (-1, 0, 1)) + ``` + + 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. + We can then filter and count as in the `count_adjacent` function in the previous code. + +4. Using complex numbers + + ```python + def neighbors(cell): + """Yield all eight neighboring cells.""" + for x in (-1, 0, 1): + for y in (-1, 0, 1): + if offset := x + y * 1j: + yield cell + offset + ``` + + A particularly elegant solution is to treat the board/field as a portion of the complex plane. + In Python, [complex numbers][complex-numbers] are a standard numeric type, alongside integers and floats. + *This is less widely known than it deserves to be.* + + 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. + + There are two properties of complex numbers that help us in this case: + - The real and imaginary parts act independently under addition. + - The value `complex(0, 0)` is the complex zero, which like integer zero is treated as False in Python conditionals. + + A tuple of integers would not work as a substitute, because `+` behaves as the concatenation operator for tuples: + + ```python + >>> complex(1, 2) + complex(3, 4) + (4+6j) + >>> (1, 2) + (3, 4) + (1, 2, 3, 4) + ``` + + Note also the use of the ["walrus" operator][walrus-operator] `:=` in the definition of `offset` above. + This relatively recent addition to Python simplifies variable assignment within the limited scope of an if statement or a comprehension. + + +## Ways of putting it all together + +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. All validation checks are done in the object constructor. ```python """Flower Field.""" -# The import is only needed for type annotation, so can be considered optional. -from typing import Generator - - -def neighbors(cell: complex) -> Generator[complex, None, None]: +def neighbors(cell): """Yield all eight neighboring cells.""" for x in (-1, 0, 1): for y in (-1, 0, 1): @@ -155,7 +179,7 @@ def neighbors(cell: complex) -> Generator[complex, None, None]: class Garden: """garden helper.""" - def __init__(self, data: list[str]): + def __init__(self, data): """Initialize.""" self.height = len(data) self.width = len(data[0]) if data else 0 @@ -170,7 +194,7 @@ class Garden: if not all(v in (" ", "*") for v in self.data.values()): raise ValueError("The board is invalid with current input.") - def val(self, x: int, y: int) -> str: + def val(self, x, y): """Return the value for one square.""" cur = x + y * 1j if self.data[cur] == "*": @@ -178,18 +202,56 @@ class Garden: count = sum(self.data.get(neighbor, "") == "*" for neighbor in neighbors(cur)) return str(count) if count else " " - def convert(self) -> list[str]: + def convert(self): """Convert the garden.""" - return [ - "".join(self.val(x, y) for x in range(self.width)) - for y in range(self.height) - ] + return ["".join(self.val(x, y) + for x in range(self.width)) + for y in range(self.height)] -def annotate(garden: list[str]) -> list[str]: +def annotate(garden): """Annotate a garden.""" return Garden(garden).convert() ``` +The example below takes an opposite strategy, using a single function, `list comprehensions`, and nested `if-elif` statements": + +```python +def annotate(garden): + grid = [[0 for _ in row] for row in garden] + positions = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)] + + for col, row in enumerate(garden): + # Checking that the board/field is rectangular up front. + if len(row) != len(grid[0]): + raise ValueError("The board is invalid with current input.") + + # Validating square content. + for index, square in enumerate(row): + if square == " ": + continue + elif square != "*": + raise ValueError("The board is invalid with current input.") + grid[col][index] = "*" + + for dr, dc in positions: + dr += col + if dr < 0 or dr >= len(grid): + continue + + dc += index + if dc < 0 or dc >= len(grid[dr]): + continue + + if grid[dr][dc] != "*": + grid[dr][dc] += 1 + + return ["".join(" " if square == 0 else str(square) for square in row) for row in grid] +``` + +[common-sequence-operations]: https://docs.python.org/3.13/library/stdtypes.html#common-sequence-operations [complex-numbers]: https://exercism.org/tracks/python/concepts/complex-numbers +[flower-field-tests]: https://github.com/exercism/python/blob/main/exercises/practice/flower-field/flower_field_test.py +[ordered-sequences]: https://docs.python.org/3.13/library/stdtypes.html#sequence-types-list-tuple-range +[text-sequences]: https://docs.python.org/3.13/library/stdtypes.html#text-sequence-type-str [walrus-operator]: https://peps.python.org/pep-0572/