Skip to content

Commit

Permalink
Add each_cell! and neighbours methods (#7)
Browse files Browse the repository at this point in the history
* Add format_cells and neighbours methods

* Update grid documentation

* Allow selection of just diagonal points

* Update documentation and rename method

* Update dup method

* Update changelog and version
  • Loading branch information
pacso authored Dec 10, 2024
1 parent 21bda58 commit 017024c
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 9 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- No unreleased changes!

## [0.0.7]
### Added
- Grid#each_cell! - Iterates over each cell in the grid, calling the provided block with each cell and updating the cell with the returned value
- Grid#neighbours - Accessor method to easily fetch the coordinates of cardinal and/or ordinal locations from the given coordinates

## [0.0.6]
### Added
- AocInput#single_line - Strips newline characters from single line puzzle inputs
Expand Down Expand Up @@ -49,7 +54,8 @@ Initial release.
### Added
- Created `AocInput` class with initial helper methods

[Unreleased]: https://github.com/pacso/aoc_rb_helpers/compare/v0.0.6...HEAD
[Unreleased]: https://github.com/pacso/aoc_rb_helpers/compare/v0.0.7...HEAD
[0.0.7]: https://github.com/pacso/aoc_rb_helpers/compare/v0.0.6...v0.0.7
[0.0.6]: https://github.com/pacso/aoc_rb_helpers/compare/v0.0.5...v0.0.6
[0.0.5]: https://github.com/pacso/aoc_rb_helpers/compare/v0.0.4...v0.0.5
[0.0.4]: https://github.com/pacso/aoc_rb_helpers/compare/v0.0.3...v0.0.4
Expand Down
87 changes: 81 additions & 6 deletions lib/aoc_rb_helpers/grid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def beyond_grid?(row, column)
def includes_coords?(row, column)
row >= 0 && column >= 0 && row < @grid.length && column < @grid.first.length
end

alias_method(:within_grid?, :includes_coords?)

# Returns the value stored at coordinates +(row, column)+ within the grid.
Expand Down Expand Up @@ -99,11 +100,11 @@ def matches_with_rotations?(other)
# @return [Array<Grid>] an array containing four {Grid} objects, one in each possible rotation
def all_rotations
rotations = []
current_grid = self.dup
grid = self.dup

4.times do
rotations << current_grid.dup
current_grid.rotate!
rotations << grid.dup
grid.rotate!
end

rotations
Expand All @@ -112,7 +113,7 @@ def all_rotations
# Returns a new {Grid} as a copy of self.
# @return [Grid] a copy of +self+
def dup
self.class.new(@grid.map { |row| row.map { |cell| cell } })
self.class.new Marshal.load(Marshal.dump(@grid))
end

# Updates +self+ with a rotated grid and returns +self+.
Expand All @@ -131,8 +132,8 @@ def rotate!(direction = :clockwise)
#
# Returns an enumerator if no block is given
#
# @return [Grid] if given a block, returns +self+ after calling block for each subgrid
# @return [Enumerator] if no block is given.
# @return [self] after processing the provided block
# @yield [subgrid] calls the provided block with each subgrid as a new {Grid} object
# @yieldparam subgrid [Grid] a new {Grid} object containing a subgrid from the main grid
def each_subgrid(rows, columns)
Expand Down Expand Up @@ -244,7 +245,8 @@ def locate_all(value)
# # - The first item is the row index.
# # - The second item is the column index.
# @yieldparam value [Object] the value stored within the cell
# @return [self]
# @return [Grid] if given a block, returns +self+ after calling block for each cell
# @return [Enumerator] if no block is given
def each_cell
return to_enum(__callee__) unless block_given?
@grid.each_with_index do |row, r_index|
Expand All @@ -255,6 +257,79 @@ def each_cell
self
end

# Calls the block, if given, with each cell value; replaces the cell in the grid with the block's return value:
#
# Returns a new Enumerator if no block given
# @yieldparam value [Object] the value stored within the cell
# @yieldreturn new_value [Object] the updated value to replace cell with
# @return [Grid] if given a block, returns +self+ after calling block for each cell
# @return [Enumerator] if no block is given
def each_cell!
return to_enum(__callee__) unless block_given?
@grid.each_with_index do |row, r_index|
row.each_with_index do |cell, c_index|
@grid[r_index][c_index] = yield cell
end
end
self
end
alias_method :format_cells, :each_cell!

# For the given position indicated by the +row+ and +column+ provided, returns
# an array of coordinates which are direct neighbours. The returned coordinates are in
# clockwise order starting directly above the given cell:
# g = Grid.new([
# [0, 1, 2, 3],
# [4, 5, 6, 7],
# [8, 9, 10, 11]
# ])
# g.neighbours(1, 1) # => [[0, 1], [1, 2], [2, 1], [1, 0]]
#
# If the keyword argument +allow_diagonal: true+ is provided, diagonally accessible neighbours will also be included:
# g = Grid.new([
# [0, 1, 2, 3],
# [4, 5, 6, 7],
# [8, 9, 10, 11]
# ])
# g.neighbours(1, 1) # => [[0, 1], [0, 2], [1, 2], [2, 2], [2, 1], [2, 0], [1, 0], [0, 0]]
#
# If provided a block, each neighbour's cell value is yielded to the block, and only those neighbours for which the block
# returns a truthy value will be returned in the results:
# g = Grid.new([
# [0, 1, 2, 3],
# [4, 5, 6, 7],
# [8, 9, 10, 11]
# ])
# g.neighbours(1, 2) { |cell| cell.even? } # => [[0, 2], [2, 2]]
# g.neighbours(1, 2, allow_diagonal: true) { |cell| cell <= 5 } # => [[0, 2], [0, 3], [1, 1], [0, 1]]
#
# @param row [Integer] the row index of the starting cell
# @param column [Integer] the column index of the starting cell
# @param cardinal [Boolean] permits the direct north/east/south/west directions
# @param ordinal [Boolean] permits diagonal north-east/south-east/south-west/north-west directions
# @return [Array<Array<Integer>>] an array of coordinates. Each coordinate is a 2-item array where:
# - The first item is the row index.
# - The second item is the column index.
def neighbours(row, column, cardinal: true, ordinal: false)
possible_neighbours = []
possible_neighbours << [row - 1, column] if cardinal
possible_neighbours << [row - 1, column + 1] if ordinal
possible_neighbours << [row, column + 1] if cardinal
possible_neighbours << [row + 1, column + 1] if ordinal
possible_neighbours << [row + 1, column] if cardinal
possible_neighbours << [row + 1, column - 1] if ordinal
possible_neighbours << [row, column - 1] if cardinal
possible_neighbours << [row - 1, column - 1] if ordinal

valid_neighbours = possible_neighbours.select { |r, c| includes_coords?(r, c) }

if block_given?
valid_neighbours.select { |r, c| yield cell(r, c) }
else
valid_neighbours
end
end

private

def locate_value(element)
Expand Down
2 changes: 1 addition & 1 deletion lib/aoc_rb_helpers/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module AocRbHelpers
VERSION = "0.0.6"
VERSION = "0.0.7"
end
39 changes: 39 additions & 0 deletions spec/lib/aoc_rb_helpers/grid_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -306,4 +306,43 @@
expect(returned_values).to eq expected_values
end
end

describe "#neighbours" do
let(:grid) { described_class.new([
[7, 4, 2, 4, 7],
[4, 2, 1, 2, 4],
[2, 1, 0, 1, 2],
[4, 2, 1, 2, 4],
[7, 4, 2, 4, 7]
]) }

it "returns the surrounding neighbour coords when no block is given" do
expect(grid.neighbours(*[2, 2])).to eq [[1, 2], [2, 3], [3, 2], [2, 1]]
end

it "returns diagonal neighbour coords when requested" do
expect(grid.neighbours(*[2, 2], ordinal: true)).to eq [[1, 2], [1, 3], [2, 3], [3, 3], [3, 2], [3, 1], [2, 1], [1, 1]]
end

it "returns only diagonal neighbours when requested" do
expect(grid.neighbours(*[2, 2], cardinal: false, ordinal: true)).to eq [[1, 3], [3, 3], [3, 1], [1, 1]]
end

it "excludes coordinates outside the grid" do
expect(grid.neighbours(*[0,0])).to eq [[0, 1], [1, 0]]
expect(grid.neighbours(*[1,0])).to eq [[0, 0], [1, 1], [2, 0]]
expect(grid.neighbours(*[0,1])).to eq [[0, 2], [1, 1], [0, 0]]
end

it "accepts a block to filter neighbours based on each cell value" do
expect(grid.neighbours(*[1, 1]) { true }).to eq [[0, 1], [1, 2], [2, 1], [1, 0]]
expect(grid.neighbours(*[1, 1]) { |cell| cell.even? }).to eq [[0, 1], [1, 0]]
expect(grid.neighbours(*[1, 1]) { |cell| cell.odd? }).to eq [[1, 2], [2, 1]]
expect(grid.neighbours(*[1, 1]) { false }).to eq []
end

it "filters diagonal neighbours also" do
expect(grid.neighbours(*[1, 1], ordinal: true) { |cell| cell.even? }).to eq [[0, 1], [0, 2], [2, 2], [2, 0], [1, 0]]
end
end
end
2 changes: 1 addition & 1 deletion spec/lib/aoc_rb_helpers_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@

RSpec.describe AocRbHelpers do
it "has the expected version number" do
expect(AocRbHelpers::VERSION).to eq "0.0.6"
expect(AocRbHelpers::VERSION).to eq "0.0.7"
end
end

0 comments on commit 017024c

Please sign in to comment.