Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Grid class #4

Merged
merged 9 commits into from
Dec 5, 2024
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- No unreleased changes!

## [0.0.4]
### Added
- Grid class for working with two-dimensional arrays of data
- AocInput updated with convenience method for creating a Grid from input
- AocInput#sections added, for splitting input into multiple sections

## [0.0.3]
### Added
- Characters `I` and `T` now supported by DotMatrix
Expand Down
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,21 @@ X X X X X X X X X X X X X
```
Into the string `CFLELOYFCS`.

### [Grid](https://rubydoc.info/github/pacso/aoc_rb_helpers/Grid)
Provides helper methods for manipulating end querying two-dimensional grids.

```ruby
grid = Grid.new([[0, 1], [2, 3]])
grid.rotate! # => #<Grid:0x0000ffff8f42f8f8 @grid=[[2, 0], [3, 1]]>
```

## Examples

Below are some examples of how you can use the features of this gem.

### Input manipulation

This example solution for 2024 Day 1 shows how the convenience methdos can be used to format the puzzle input:
This example solution for 2024 Day 1 shows how the convenience methods can be used to format the puzzle input:

```ruby
# frozen_string_literal: true
Expand Down Expand Up @@ -95,6 +103,14 @@ module Year2024
end
```

Where you have different sections of input which need to be handled differently, you can quickly split them into independent instances of `AocInput`, such as with the pussle from 2024, Day 5:

```ruby
page_rules, page_updates = aoc_input.sections
page_rules_data = page_rules.multiple_lines.columns_of_numbers("|").data
page_updates_data = page_updates.multiple_lines.columns_of_numbers(",").data
```

### Decoding printed text

```ruby
Expand Down
37 changes: 25 additions & 12 deletions lib/aoc_rb_helpers/aoc_input.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# frozen_string_literal: true

# Provides input manipulation helper methods.
# Methods are chainable, and directly modify the parsed view of the input data within the @data instance variable.
# Methods are chainable, and directly modify the parsed view of the input data within the +@data+ instance variable.
#
# Once manipulated as required, the input is accessable from #data
# Once manipulated as required, the input is accessable using the {#data} method.
class AocInput
# Returns a new AocInput initialized with the given puzzle input.
#
Expand All @@ -24,8 +24,8 @@ def data

# Splits the input string into an array of lines.
#
# This method processes `@data` by splitting the input string into multiple lines,
# removing trailing newline characters. It modifies `@data` directly and returns `self`
# This method processes +@data+ by splitting the input string into multiple lines,
# removing trailing newline characters. It modifies +@data+ directly and returns +self+
# to enable method chaining.
#
# @return [AocInput] self
Expand All @@ -38,11 +38,11 @@ def multiple_lines

# Splits each string in the data array into an array of numbers.
#
# This method processes `@data` by splitting each string in the array using the specified delimiter,
# then converting each resulting element to an integer. It modifies `@data` directly and enables
# chaining by returning `self`.
# This method processes +@data+ by splitting each string in the array using the specified delimiter,
# then converting each resulting element to an integer. It modifies +@data+ directly and enables
# chaining by returning +self+.
#
# @param delimiter [String, nil] the delimiter to be passed to `String#split`
# @param delimiter [String, nil] the delimiter to be passed to +String#split+
# @raise [RuntimeError] if {#multiple_lines} has not been called
# @return [AocInput] self
def columns_of_numbers(delimiter = nil)
Expand All @@ -57,7 +57,7 @@ def columns_of_numbers(delimiter = nil)
# Transposes the data array.
#
# This method can only be called after {columns_of_numbers}.
# It directly modifies `@data` by transposing it and returns `self` to allow method chaining.
# It directly modifies +@data+ by transposing it and returns +self+ to allow method chaining.
#
# @raise [RuntimeError] if {columns_of_numbers} has not been called.
# @return [AocInput] self
Expand All @@ -67,10 +67,10 @@ def transpose
self
end

# Sorts each array within the `@data` array.
# Sorts each array within the +@data+ array.
#
# This method processes `@data` by sorting each nested array in ascending order.
# It directly modifies `@data` and returns `self` to enable method chaining.
# This method processes +@data+ by sorting each nested array in ascending order.
# It directly modifies +@data+ and returns +self+ to enable method chaining.
#
# @raise [RuntimeError] if {#columns_of_numbers} has not been called
# @return [AocInput] self
Expand All @@ -80,6 +80,19 @@ def sort_arrays
self
end

# Returns a new +Grid+ object from the parsed input
# @return [Grid] the new grid
def to_grid
Grid.from_input(@raw_input)
end

# Returns a new +AocInput+ for each section of the raw input, split by the given delimiter.
# @param delimiter [String] the string used to split sections
# @return [Array<AocInput>] an array of new AocInput instances initialised with each section of the raw input from +self+
def sections(delimiter = "\n\n")
@sections = @raw_input.split(delimiter).map { |section_input| AocInput.new(section_input) }
end

private
def configure_method_access
@can_call = {
Expand Down
129 changes: 129 additions & 0 deletions lib/aoc_rb_helpers/grid.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# frozen_string_literal: true

# Provides helper methods for manipulating end querying two-dimensional grids.
class Grid
# Returns a new {Grid} initialized with the given input.
#
# @param input [String] the unprocessed input text containing the grid
def self.from_input(input)
self.new(input.lines(chomp: true).map(&:chars))
end

# Returns a new {Grid} initialized with the provided two-dimensional array.
#
# @param grid [Array<Array<Object>>] the grid in a two-dimensional array
def initialize(grid)
@grid = grid
end

# Returns the value stored at coordinates +(row, column)+ within the grid.
#
# Row and column numbers are zero-indexed.
#
# @param row [Integer] the row index of the desired cell
# @param column [Integer] the column index of the desired cell
def cell(row, column)
@grid[row][column]
end

# Updates the cell at coordinates +(row, column)+ with the object provided in +value+; returns the given object.
#
# @param row [Integer] the row index of the cell you wish to update
# @param column [Integer] the column index of the cell you wish to update
# @param value [Object] the object to assign to the selected grid cell
# @return [Object] the given +value+
def set_cell(row, column, value)
@grid[row][column] = value
end

# Returns +true+ if the other grid has the same content in the same orientation, +false+ otherwise.
#
# grid = Grid.new([1, 2], [3, 4])
# grid == Grid.new([1, 2], [3, 4]) # => true
# grid == Grid.new([0, 1], [2, 3]) # => false
# grid == "non-grid object" # => false
#
# @param other [Grid] the grid you wish to compare with
# @return [Boolean]
def ==(other)
return false unless other.is_a?(self.class)
@grid == other.instance_variable_get(:@grid)
end

# Returns +true+ if the other grid can be rotated into an orientation where it is equal to +self+, +false+ otherwise.
#
# grid = Grid.new([1, 2], [3, 4])
# grid == Grid.new([1, 2], [3, 4]) # => true
# grid == Grid.new([3, 1], [4, 2]) # => true
# grid == Grid.new([1, 2], [4, 3]) # => false
# grid == "non-grid object" # => false
#
# @param other [Grid] the grid you wish to compare with
def matches_with_rotations?(other)
other.all_rotations.any? { |rotated| self == rotated }
end

# Returns an array of {Grid} objects in all possible rotations, copied from +self+.
# @return [Array<Grid>] an array containing four {Grid} objects, one in each possible rotation
def all_rotations
rotations = []
current_grid = self.dup

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

rotations
end

# Returns a new {Grid} as a copy of self.
# @return [Grid] a copy of +self+
def dup
Grid.new(@grid)
end

# Updates +self+ with a rotated grid and returns +self+.
#
# Will rotate in a clockwise direction by default. Will rotate in an anticlockwise direction if passed
# a param which is not +:clockwise+.
#
# @param direction [Symbol]
# @return [self]
def rotate!(direction = :clockwise)
@grid = direction == :clockwise ? @grid.transpose.map(&:reverse) : @grid.map(&:reverse).transpose
self
end

# Calls the given block with each subgrid from +self+ with the size constraints provided; returns +self+.
#
# Returns an enumerator if no block is given
#
# @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)
return to_enum(__callee__, rows, columns) unless block_given?
@grid.each_cons(rows) do |rows|
rows[0].each_cons(columns).with_index do |_, col_index|
yield Grid.new(rows.map { |row| row[col_index, columns] })
end
end

self
end

# Returns an array containing all of the subgrids of the specified dimensions.
# @param rows [Integer] the number of rows each subgrid should contain.
# Must be greater than zero and less than or equal to the number of rows in the grid.
# @param columns [Integer] the number of columns each subgrid should contain.
# Must be greater than zero and less than or equal to the number of columns in the grid.
# @raise [ArgumentError] if the specified rows or columns are not {Integer} values, or exceed the grid's dimensions.
# @return [Array<Grid>]
def subgrids(rows, columns)
raise ArgumentError unless rows.is_a?(Integer) && rows > 0 && rows <= @grid.length
raise ArgumentError unless columns.is_a?(Integer) && columns > 0 && columns <= @grid.first.length
each_subgrid(rows, columns).to_a
end
end
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.3"
VERSION = "0.0.4"
end
36 changes: 36 additions & 0 deletions spec/lib/aoc_rb_helpers/aoc_input_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@
345 678
EOF
end
let(:grid_input) do
<<~EOF
abcde
fghij
klmno
pqrst
uvwxy
EOF
end

describe '#multiple_lines' do
subject(:method_chain) { described_class.new(multiline_input).multiple_lines }
Expand Down Expand Up @@ -71,6 +80,33 @@
end
end

describe "#to_grid" do
it "returns an instance of Grid" do
expect(described_class.new(grid_input).to_grid).to be_a Grid
end
end

describe "sections" do
let(:input_with_2_sections) do
<<~EOF
23|45
34|38
83|21

4,3,1,8
EOF
end

it "returns two AocInput instances when provided an input with 2 sections" do
sections = described_class.new(input_with_2_sections).sections
expect(sections).to be_an Array
expect(sections.length).to eq 2
sections.each do |section|
expect(section).to be_an AocInput
end
end
end

it "is important which order you call the methods in" do
expect(described_class.new(multiline_numbers).multiple_lines.columns_of_numbers.transpose.sort_arrays.data).to eq [[123, 345, 789], [12, 456, 678]]
expect(described_class.new(multiline_numbers).multiple_lines.columns_of_numbers.sort_arrays.transpose.data).to eq [[123, 12, 345], [456, 789, 678]]
Expand Down
Loading
Loading