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

Release 0.0.5 #5

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

## [0.0.5]
### Added
- AocInput#process_each_line - Processes each line of the input data using the provided block
- Grid#includes_coords? - Returns `true` if the provided coordinates exist within the bounds of the grid
- Grid#beyond_grid? - Returns `true` if the provided coordinates exceed the bounds of the grid
- Grid#locate(value) - Returns the first coordinates within the grid containing the given value
- Grid#locate_all(value) - Returns an array of coordinates for any location within the grid containing the given value
- Grid#each_cell - Iterates over each cell in the grid

### Changed
- Grid#cell now returns `nil` if the provided coordinates to not exist within the grid
- Grid#set_cell nwo returns `nil` if the provided coordinates to not exist within the grid

### Fixed
- Grid#dup previously returned a new `Grid` instance with the same instance of the `@grid` array within it. Now `@grid` is a unique copy.

## [0.0.4]
### Added
- Grid class for working with two-dimensional arrays of data
Expand All @@ -29,6 +45,9 @@ Initial release.
### Added
- Created `AocInput` class with initial helper methods

[Unreleased]: https://github.com/pacso/aoc_rb_helpers/compare/v0.0.2...HEAD
[Unreleased]: https://github.com/pacso/aoc_rb_helpers/compare/v0.0.5...HEAD
[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
[0.0.3]: https://github.com/pacso/aoc_rb_helpers/compare/v0.0.2...v0.0.3
[0.0.2]: https://github.com/pacso/aoc_rb_helpers/compare/v0.0.1...v0.0.2
[0.0.1]: https://github.com/pacso/aoc_rb_helpers
20 changes: 20 additions & 0 deletions lib/aoc_rb_helpers/aoc_input.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,26 @@ def multiple_lines
self
end

# Processes each line of the data using the provided block.
#
# This method applies the given block to each line in the +@data+ array,
# replacing the original +@data+ with the results of the block. The method
# returns +self+ to allow method chaining.
#
# Returns an enumerator if no block is given.
#
# @yieldparam line [Object, Array<Object>] a single line of the data being processed
# @yieldreturn [Object, Array<Object>] the result of processing the line
# @return [self] the instance itself, for method chaining
# @return [Enumerator] if no block is given
def process_each_line
return to_enum(__callee__) unless block_given?
@data = @data.map do |line|
yield line
end
self
end

# 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,
Expand Down
139 changes: 138 additions & 1 deletion lib/aoc_rb_helpers/grid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,55 @@ def initialize(grid)
@grid = grid
end

# Returns +true+ if the provided coordinates exceed the bounds of the grid; +false+ otherwise.
#
# @param row [Integer] the row index to test
# @param column [Integer] the column index to test
# @return [Boolean]
# @see #includes_coords?
def beyond_grid?(row, column)
!includes_coords?(row, column)
end

# Returns +true+ if the provided coordinates exist within the bounds of the grid; +false+ otherwise.
#
# @param row [Integer] the row index to test
# @param column [Integer] the column index to test
# @return [Boolean]
# @see #beyond_grid?
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.
#
# Returns +nil+ if the provided coordinates do not exist 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
# @return [Object] the value at the given coordinates within the grid
# @return [nil] if the given coordinates do not exist within the grid
# @see #set_cell
def cell(row, column)
return nil unless includes_coords?(row, column)
@grid[row][column]
end

# Updates the cell at coordinates +(row, column)+ with the object provided in +value+; returns the given object.
#
# Returns +nil+ if the provided coordinates do not exist within the grid.
#
# @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+
# @return [nil] if the provided coordinates do not exist within the grid
# @see #cell
def set_cell(row, column, value)
return nil unless includes_coords?(row, column)
@grid[row][column] = value
end

Expand Down Expand Up @@ -80,7 +112,7 @@ def all_rotations
# Returns a new {Grid} as a copy of self.
# @return [Grid] a copy of +self+
def dup
Grid.new(@grid)
self.class.new(@grid.map { |row| row.map { |cell| cell } })
end

# Updates +self+ with a rotated grid and returns +self+.
Expand Down Expand Up @@ -126,4 +158,109 @@ def subgrids(rows, columns)
raise ArgumentError unless columns.is_a?(Integer) && columns > 0 && columns <= @grid.first.length
each_subgrid(rows, columns).to_a
end

# Returns the first coordinates within the grid containing the given value. Returns +nil+ if not found.
#
# If given an array of values, the first coordinate matching any of the given values will be returned.
#
# Searches the grid from top left (+[0, 0]+) to bottom right, by scanning each row.
#
# @param value [Object, Array<Object>] the value, or array of values, to search for.
# @return [Array<Integer>] if the value was located, its coordinates are returned in a 2-item array where:
# - The first item is the row index.
# - The second item is the column index.
# @return [nil] if the value was not located
def locate(value)
result = nil
if value.is_a? Array
value.each do |e|
result = locate(e)
break unless result.nil?
end
else
result = locate_value value
end
result
end

# Returns an array of coordinates for any location within the grid containing the given value.
#
# If given an array of values, the coordinates of any cell matching any of the given values will be returned.
#
# @param value [Object, Array<Object>] the value, or array of values, to search for.
# @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 locate_all(value)
locations = []

if value.is_a? Array
@grid.each_with_index.select { |row, _r_index| value.any? { |el| row.include?(el) } }.each do |row, r_index|
row.each_with_index do |cell, c_index|
locations << [r_index, c_index] if value.include?(cell)
end
end
else
@grid.each_with_index.select { |row, _r_index| row.include?(value) }.each do |row, r_index|
row.each_with_index do |cell, c_index|
locations << [r_index, c_index] if cell == value
end
end
end

locations
end

# Iterates over each cell in the grid.
#
# When a block is given, passes the coordinates and value of each cell to the block; returns +self+:
# g = Grid.new([
# ["a", "b"],
# ["c", "d"]
# ])
# g.each_cell { |coords, value| puts "#{coords.inspect} => #{value}" }
#
# Output:
# [0, 0] => a
# [0, 1] => b
# [1, 0] => c
# [1, 1] => d
#
# When no block is given, returns a new Enumerator:
# g = Grid.new([
# [:a, "b"],
# [3, true]
# ])
# e = g.each_cell
# e # => #<Enumerator: #<Grid: @grid=[[\"a\", \"b\"], [\"c\", \"d\"]]>:each_cell>
# g1 = e.each { |coords, value| puts "#{coords.inspect} => #{value.class}: #{value}" }
#
# Output:
# [0, 0] => Symbol: a
# [0, 1] => String: b
# [1, 0] => Integer: 3
# [1, 1] => TrueClass: true
# @yieldparam coords [Array<Integer>] the coordinates of the cell in a 2-item array where:
# # - 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]
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|
yield [[r_index, c_index], cell]
end
end
self
end

private

def locate_value(element)
row = @grid.index { |row| row.include?(element) }
return nil if row.nil?
column = @grid[row].index(element)
[row, column]
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.4"
VERSION = "0.0.5"
end
35 changes: 34 additions & 1 deletion spec/lib/aoc_rb_helpers/aoc_input_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
end
end

describe "sections" do
describe "#sections" do
let(:input_with_2_sections) do
<<~EOF
23|45
Expand All @@ -107,6 +107,39 @@
end
end

describe "#process_each_line" do
let(:multi_type_row_input) do
<<~EOF
abc: 123
def: 456
ghi: 789
EOF
end
subject(:processed_input) {
described_class
.new(multi_type_row_input)
.multiple_lines
.process_each_line do |line|
key, value = line.split(": ")
[key, value.to_i]
end
}

it "returns an instance of AocInput" do
expect(processed_input).to be_an AocInput
end

it "modifies @data correctly" do
expect(processed_input.data).to eq [["abc", 123], ["def", 456], ["ghi", 789]]
end

it "returns an enumerator if no block is given" do
enumerator = described_class.new(multi_type_row_input).multiple_lines.process_each_line
expect(enumerator).to be_an Enumerator
expect(enumerator.next).to eq "abc: 123"
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