Code katas are so humbling
30 Apr 2017I have been working through, slowly, the ruby track on exercism. It’s a great site which I highly reccommend. Anyway, todays task was to identify saddle points. Here… I’ll print the readme
Saddle point README
# Saddle Points
Detect saddle points in a matrix.
So say you have a matrix like so:
0 1 2
|---------
0 | 9 8 7
1 | 5 3 2 <--- saddle point at (1,0)
2 | 6 6 7
It has a saddle point at (1, 0).
It's called a "saddle point" because it is greater than or equal to
every element in its row and the less than or equal to every element in
its column.
A matrix may have zero or more saddle points.
Your code should be able to provide the (possibly empty) list of all the
saddle points for any given matrix.
You are then given a spec file
#!/usr/bin/env ruby
gem 'minitest', '>= 5.0.0'
require 'minitest/autorun'
require_relative 'saddle_points'
class MatrixTest < Minitest::Test
def test_extract_a_row
matrix = Matrix.new("1 2\n10 20")
assert_equal [1, 2], matrix.rows[0]
end
def test_extract_same_row_again
matrix = Matrix.new("9 7\n8 6")
assert_equal [9, 7], matrix.rows[0]
end
def test_extract_other_row
matrix = Matrix.new("9 8 7\n19 18 17")
assert_equal [19, 18, 17], matrix.rows[1]
end
def test_extract_other_row_again
matrix = Matrix.new("1 4 9\n16 25 36")
assert_equal [16, 25, 36], matrix.rows[1]
end
def test_extract_a_column
matrix = Matrix.new("1 2 3\n4 5 6\n7 8 9\n 8 7 6")
assert_equal [1, 4, 7, 8], matrix.columns[0]
end
def test_extract_another_column
matrix = Matrix.new("89 1903 3\n18 3 1\n9 4 800")
assert_equal [1903, 3, 4], matrix.columns[1]
end
def test_no_saddle_point
matrix = Matrix.new("2 1\n1 2")
assert_equal [], matrix.saddle_points
end
def test_a_saddle_point
matrix = Matrix.new("1 2\n3 4")
assert_equal [[0, 1]], matrix.saddle_points
end
def test_another_saddle_point
matrix = Matrix.new("18 3 39 19 91\n38 10 8 77 320\n3 4 8 6 7")
assert_equal [[2, 2]], matrix.saddle_points
end
def test_multiple_saddle_points
matrix = Matrix.new("4 5 4\n3 5 5\n1 5 4")
assert_equal [[0, 1], [1, 1], [2, 1]], matrix.saddle_points
end
end
And then you have to give your own solution.
Mine was, frankly, horrible. I was slightly stymied as I had to keep the rows
and column
methods as they were. I ended up making a terribly verbose class, creating a Point
class etc.
I knew it was horrible too, the @rows
shadowing the rows
method (which returned values), it was horrible. If I’d been able to refactor the tests I think I could have cleared it up quite a bit more nicely. I did like my little Point
value object though, that was nice.
My solution
class Matrix
def initialize(matrix_string)
@rows = matrix_string
.lines
.map(&:strip)
.map(&:split)
.map { |row| row.map(&:to_i) }
@rows = @rows.map.with_index do |cols, row_index|
cols.map.with_index do |value, col_index|
Point.new(row_index, col_index, value)
end
end
@columns = @rows.transpose
end
def saddle_points
max_row_elements.select do |point|
col_for_point = @columns[point.col]
col_for_point.all? { |p| (point.value <= p.value) || p == point } ? point : nil
end.flatten.compact.map(&:to_coordinates)
end
def rows
@rows.map do |row|
row.map(&:value)
end
end
def columns
@columns.map do |column|
column.map(&:value)
end
end
private
def max_row_elements
@max_row_elements ||= begin
@rows.map do |row|
max_value_in_row = row.max.value
row.select {|p| p.value == max_value_in_row}
end
end.flatten
end
class Point < Struct.new(:row, :col, :value)
def <=>(other)
value <=> other.value
end
def ==(other)
row == other.row && col == other.col
end
def to_coordinates
[row, col]
end
end
end
Anyway, after looking at other peoples solutions, I saw that the proper way to do it was to just neatly iterate over all the positions in the Matrix, and that the easiest way to do that was to use the each_index
method for the rows & columns combined with the product
method.
Obvious in retrospect.. looks like this:
class Matrix
attr_reader :rows, :columns
def initialize(string)
@rows = string.each_line.map { |line| line.split.map(&:to_i) }
@columns = @rows.transpose
end
def saddle_points
all_coordinates.select do |i, j|
rows[i].max == rows[i][j] && columns[j].min == rows[i][j]
end
end
private
def all_coordinates
rows.each_index.to_a.product(columns.each_index.to_a)
end
end
Really quite a lot nicer, and infinitely shorter.
Well, I highly recommend exercism, it’s really good.