diff --git a/CHANGELOG.md b/CHANGELOG.md index 032fdae..6d69b98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/lib/aoc_rb_helpers/aoc_input.rb b/lib/aoc_rb_helpers/aoc_input.rb index 31c8f9f..3e6eca2 100644 --- a/lib/aoc_rb_helpers/aoc_input.rb +++ b/lib/aoc_rb_helpers/aoc_input.rb @@ -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] a single line of the data being processed + # @yieldreturn [Object, Array] 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, diff --git a/lib/aoc_rb_helpers/grid.rb b/lib/aoc_rb_helpers/grid.rb index 1caff9d..f357476 100644 --- a/lib/aoc_rb_helpers/grid.rb +++ b/lib/aoc_rb_helpers/grid.rb @@ -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 @@ -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+. @@ -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] the value, or array of values, to search for. + # @return [Array] 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] the value, or array of values, to search for. + # @return [Array>] 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 # => #: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] 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 diff --git a/lib/aoc_rb_helpers/version.rb b/lib/aoc_rb_helpers/version.rb index 0912593..030691a 100644 --- a/lib/aoc_rb_helpers/version.rb +++ b/lib/aoc_rb_helpers/version.rb @@ -1,3 +1,3 @@ module AocRbHelpers - VERSION = "0.0.4" + VERSION = "0.0.5" end diff --git a/spec/lib/aoc_rb_helpers/aoc_input_spec.rb b/spec/lib/aoc_rb_helpers/aoc_input_spec.rb index 0bddba0..b39a1c6 100644 --- a/spec/lib/aoc_rb_helpers/aoc_input_spec.rb +++ b/spec/lib/aoc_rb_helpers/aoc_input_spec.rb @@ -86,7 +86,7 @@ end end - describe "sections" do + describe "#sections" do let(:input_with_2_sections) do <<~EOF 23|45 @@ -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]] diff --git a/spec/lib/aoc_rb_helpers/grid_spec.rb b/spec/lib/aoc_rb_helpers/grid_spec.rb index 15e16eb..171e13a 100644 --- a/spec/lib/aoc_rb_helpers/grid_spec.rb +++ b/spec/lib/aoc_rb_helpers/grid_spec.rb @@ -21,17 +21,31 @@ end describe "#cell(y, x)" do + let(:grid) { described_class.from_input(input_text) } + it "returns the cell at the given coordinates" do - grid = described_class.from_input(input_text) expect(grid.cell(0, 0)).to eq "a" end + + it "returns nil if the coords are out of bounds" do + expect(grid.cell(-1, -1)).to be_nil + end end describe "#set_cell(y, x, value)" do + let(:grid) { described_class.from_input(input_text) } + it "sets the cell at the given coordinates" do - grid = described_class.from_input(input_text) - grid.set_cell(0, 0, "z") - expect(grid.cell(0, 0)).to eq "z" + expect { grid.set_cell(0, 0, "z") } + .to change { grid.cell(0, 0) }.from("a").to("z") + end + + it "returns nil if the coordinates are out of bounds" do + expect(grid.set_cell(-1, 0, "z")).to be_nil + end + + it "does not modify the grid if the coordinates are out of bounds" do + expect { grid.set_cell(-1, 0, "z") }.not_to change { grid.instance_variable_get("@grid") } end end @@ -169,4 +183,127 @@ expect(grid).to eq Grid.new([[0, 1], [2, 3]]) end end + + describe "#includes_coords?(row, column)" do + let(:grid) { described_class.new([[0, 1], [2, 3]]) } + + it "returns true if the given row and column are in the grid" do + [[0, 0], [0, 1], [1, 0], [1, 1]].each do |coords| + expect(grid.includes_coords?(*coords)).to be true + end + end + + it "returns false if the given row and column are not in the grid" do + [[-1, 0], [0, -1], [0, 2], [2, 0]].each do |coords| + expect(grid.includes_coords?(*coords)).to be false + end + end + end + + describe "#beyond_grid?(row, column)" do + let(:grid) { described_class.new([[0, 1], [2, 3]]) } + + it "returns false if the given row and column are in the grid" do + [[0, 0], [0, 1], [1, 0], [1, 1]].each do |coords| + expect(grid.beyond_grid?(*coords)).to be false + end + end + + it "returns true if the given row and column are not in the grid" do + [[-1, 0], [0, -1], [0, 2], [2, 0]].each do |coords| + expect(grid.beyond_grid?(*coords)).to be true + end + end + end + + describe "#dup" do + let(:grid) { described_class.new([[0, 1], [2, 3]]) } + + it "returns a new Grid instance" do + expect(grid.dup).to be_an_instance_of Grid + end + + it "returns a new Grid with a matching @grid" do + expect(grid.dup.instance_variable_get(:@grid)).to eq grid.instance_variable_get(:@grid) + end + + it "does not create a Grid with the same instance of @grid" do + expect(grid.dup.instance_variable_get(:@grid)).not_to be grid.instance_variable_get(:@grid) + end + + it "isolates changes in the new grid from the original" do + other = grid.dup + expect { other.set_cell(0, 0, "9") }.not_to change { grid.cell(0, 0) }.from(0) + expect(other.cell(0, 0)).to eq "9" + end + end + + describe "#locate" do + let(:grid) { described_class.new([%w[a b], %w[c d]]) } + + it "returns the coordinates of the given value" do + expect(grid.locate("a")).to eq [0, 0] + expect(grid.locate("b")).to eq [0, 1] + expect(grid.locate("c")).to eq [1, 0] + expect(grid.locate("d")).to eq [1, 1] + end + + it "returns nil if the given value is not in the grid" do + expect(grid.locate("missing")).to be_nil + end + + context "with a grid contining duplicate values" do + let(:grid) { described_class.new([%w[a b], %w[b a]]) } + + it "returns the first coordinate of the given value, searching from the top-left by row" do + expect(grid.locate("a")).to eq [0, 0] + expect(grid.locate("b")).to eq [0, 1] + end + end + end + + describe "#locate_all" do + let(:grid) { described_class.new([%w[a b c a], %w[c d e b], %w[f a g h]]) } + + it "returns all coordinates of the given value" do + expect(grid.locate_all("a")).to eq [[0, 0], [0, 3], [2, 1]] + end + + it "returns an empty array if the given value is not in the grid" do + expect(grid.locate_all("missing")).to eq [] + end + end + + describe "#each_cell" do + let(:grid) { described_class.new([%w[a b], %w[c d]]) } + + it "returns an enumerator when no block is given" do + e = grid.each_cell + expect(e).to be_an Enumerator + expect(e.next).to eq [[0, 0], "a"] + end + + it "allows the grid to be modified during iteration" do + grid.each_cell { |coords, value| grid.set_cell(*coords, value * 2) } + expect(grid.cell(0, 0)).to eq "aa" + end + + it "returns self when given a block" do + expect(grid.each_cell { |_, value| value * 2 }).to eq grid + end + + it "yields the coordinates and values of each cell in the grid" do + expected_values = [ + [[0, 0], "a"], + [[0, 1], "b"], + [[1, 0], "c"], + [[1, 1], "d"], + ] + returned_values = [] + grid.each_cell do |coords, value| + returned_values << [coords, value] + end + expect(returned_values).to eq expected_values + end + end end diff --git a/spec/lib/aoc_rb_helpers_spec.rb b/spec/lib/aoc_rb_helpers_spec.rb index 87e0fab..b61e03d 100644 --- a/spec/lib/aoc_rb_helpers_spec.rb +++ b/spec/lib/aoc_rb_helpers_spec.rb @@ -4,6 +4,6 @@ RSpec.describe AocRbHelpers do it "has the expected version number" do - expect(AocRbHelpers::VERSION).to eq "0.0.4" + expect(AocRbHelpers::VERSION).to eq "0.0.5" end end