4
\$\begingroup\$

Given a two-dimensional array, Sudoku class can create NxN Sudoku where N = size^2. Assume that the user gives only valid NxN matrixes for the constructor.

  • Method is_correct? should return true if Sudoku is solved
  • Method print / to_s should write on screen Sudoku.
  • Method set should set specified position in Sudoku for a given value.

Is there any solution that does not involve so many iterations to validate? Is it good to override initialize method? Will this validation pass through every Sudoku? How can I improve my code to be more readable?

sudoku.rb

class Sudoku
attr_accessor :board, :size
def initialize (starting_board, size = 3)
 #default size is 3
 @board = starting_board
 @size = size
end
def is_correct?
 #valid all not null, then valid rows columns and squares sizeXsize
 valid_not_null? && valid_rows? && valid_columns? && valid_squares?
end 
def set (x, y , value)
 #set value of sudoku as something (no validation of value)
 if( x.between?(1, 9) && y.between?(1, 9))
 @board[y - 1 ][x - 1] = value
 end
end
def to_s 
 #o = 1 2 3
 # 4 5 6
 # 7 8 9
 #o | o | o
 #- - - - - 
 #o | o | o
 #- - - - -
 #o | o | o
 nums = Array.new(@size)
 rows = Array.new(@size)
 multi_rows = Array.new(@size)
 sqr_multi_rows = Array.new(@size)
 for s in 0..@size -1
 for k in 0..@size -1 
 for j in 0..@size - 1
 # set 1 2 3 | 3 4 5 | 6 7 8 
 for i in 0..@size -1
 # set 1 2 3 
 nums[i] = ( board[3*s+ k ][3*j + i] || " " )
 end
 rows[j] = (nums.join(" "))
 end
 multi_rows[k] = (rows.join(" | "))
 end
 sqr_multi_rows[s] = multi_rows.join("\n")
 end
 line = "- " * (@size**2 + @size - 1)
 sqr_multi_rows.join("\n#{line}\n")
 #brings everything together
end
alias print to_s
 private
 def valid_rows?
 row = 0
 valid = true
 mask = Array.new(@size ** 2) {|e| e = e+1}
 while (valid && row < @size **2 )
 current_row = @board[row]
 #check if contains all from 1 to 9
 valid = mask == current_row.sort
 row += 1
 end
 valid
 end
 def valid_columns?
 column = row= 0
 valid = true
 mask = Array.new(@size ** 2) {|e| e = e+1}
 current_col = Array.new(@size ** 2)
 while (valid && column < @size ** 2)
 while (valid && row < @size ** 2 )
 current_col[row] = @board[column][row]
 row += 1
 end
 #create col
 #check if contains all from 1 to 9
 valid = mask == current_col.sort
 column += 1
 end
 valid
 end
 def valid_squares?
 current_sqr = Array.new(@size**2)
 mask = Array.new(@size ** 2) {|e| e = e+1}
 valid = true
 mask = Array.new(@size ** 2) {|e| e = e+1}
 i = 0
 for col in 0...@size
 for row in 0...@size
 for j in 0...@size
 for i in 0...@size
 current_sqr[@size*i+j] = @board[3*col + j][3*row + i]
 end
 end
 #create square
 #check if contains all from 1 to 9
 valid = mask == current_sqr.sort
 end 
 end
 valid
 end
 def valid_not_null?
 valid = true
 i = j = 0
 #for each element check if is not null
 while valid && j < @size
 while valid && i <@size
 valid = valid != nil ? true : false
 i+= 1
 end
 j+=1
 end
 valid
 end 
end #end of Sudoku

test_sudoku.rb

require "test/unit"
require_relative "sudoku.rb"
class TestSudoku <Test::Unit::TestCase
def setup
 @goodSudoku1 = Sudoku.new([
 [7,8,4, 1,5,9, 3,2,6],
 [5,3,9, 6,7,2, 8,4,1],
 [6,1,2, 4,3,8, 7,5,9],
 [9,2,8, 7,1,5, 4,6,3],
 [3,5,7, 8,4,6, 1,9,2],
 [4,6,1, 9,2,3, 5,8,7],
 [8,7,6, 3,9,4, 2,1,5],
 [2,4,3, 5,6,1, 9,7,8],
 [1,9,5, 2,8,7, 6,3,4]
 ])
 @goodSudoku2 = Sudoku.new([
 [2,9,5, 7,4,3, 8,6,1],
 [4,3,1, 8,6,5, 9,2,7],
 [8,7,6, 1,9,2, 5,4,3],
 [3,8,7, 4,5,9, 2,1,6],
 [6,1,2, 3,8,7, 4,9,5],
 [5,4,9, 2,1,6, 7,3,8],
 [7,6,3, 5,2,4, 1,8,9],
 [9,2,8, 6,7,1, 3,5,4],
 [1,5,4, 9,3,8, 6,7,2]
 ])
 @badSudoku1 = Sudoku.new([
 [1,2,3, 4,5,6, 7,8,9],
 [1,2,3, 4,5,6, 7,8,9],
 [1,2,3, 4,5,6, 7,8,9],
 [1,2,3, 4,5,6, 7,8,9],
 [1,2,3, 4,5,6, 7,8,9],
 [1,2,3, 4,5,6, 7,8,9],
 [1,2,3, 4,5,6, 7,8,9],
 [1,2,3, 4,5,6, 7,8,9],
 [1,2,3, 4,5,6, 7,8,9]
 ])
 @badSudoku2 = Sudoku.new([
 [1,2,3, 4,5,6, 7,8,9],
 [2,3,4, 5,6,7, 8,9,1],
 [3,4,5, 6,7,8, 9,1,2],
 [4,5,6, 7,8,9, 1,2,3],
 [5,6,7, 8,9,1, 2,3,4],
 [6,7,8, 9,1,2, 3,4,5],
 [7,8,9, 1,2,3, 4,5,6],
 [8,9,1, 2,3,4, 5,6,7],
 [9,1,2, 3,4,5, 6,7,8]
 ])
end
def test_success
 assert_equal(@goodSudoku1.is_correct?, true)
 assert_equal(@goodSudoku2.is_correct?, true)
end
def test_failure
 assert_equal(@badSudoku1.is_correct?, false)
 assert_equal(@badSudoku2.is_correct?, false)
end
end
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked May 24, 2017 at 14:03
\$\endgroup\$
2
  • \$\begingroup\$ A quick comment because I think it's too short to be an answer. I don't believe comments like in this line end #end of Sudoku are needed. As long as everything is properly indented you shouldn't need stuff like that. I don't know Ruby though, so maybe it's a standard there. \$\endgroup\$ Commented May 24, 2017 at 14:58
  • \$\begingroup\$ you may find this helpful - other data structures possible to represent the sudoku grid: stackoverflow.com/questions/1391381/… - btw this looks like a very interesting problem \$\endgroup\$ Commented May 30, 2017 at 3:11

1 Answer 1

3
\$\begingroup\$

A couple comments:

Your is_correct? method is good as it expresses the logic and delegates the details to helper methods. (I wouldn't name it with the prefix is_ because that's not a ruby convention, but I realize you were given the test case so you have no choice.). The only thing that strikes me as a little odd about it is the valid_not_null? method. It shouldn't be necessary: if you check for valid rows, valid columns, and valid sub-squares, then there will be no nulls.

Now for the bad: Notice how you are creating new arrays everywhere? Your to_s method is creating several new arrays. Your validation methods each create new arrays. This is lots of extra code and a sign that you haven't properly modeled the "board". In fact, I think you're relying completely on the input format, when you probably should have an internal representation that makes everything else easier. One way to do that is to have a separate class representing the board so you can ask it to return its rows, e.g. board.rows, etc. If you were going to extend this Sudoku class to do anything else (e.g. generate, play, or solve puzzles) then I would definitely recommend splitting into multiple classes to avoid complexity. In this case, you can probably work within this single class, but I'd add helper methods like .rows and .columns etc.

By the way, it is convention to indent Ruby code 2 spaces, no tabs. But the main thing that stands out is that your methods are very long, partly due to the problems I mentioned above. It's best to try to keep them short, no more than 5 lines. How can this be accomplished? What I like to do is write code that assumes the use of other classes that I wish existed, then I write those classes. I'll provide an example. Here's a refactoring of your code that depends on a Board class, not yet written:

sudoku.rb

require './board.rb'
class Sudoku
 attr_accessor :board
 def initialize (board_spec)
 @board = Board.new(board_spec)
 end
 def is_correct?
 valid_rows? && valid_columns? && valid_squares?
 end
 def valid_rows?
 #check that the digits, when sorted, are 1 through 9
 board.rows.all? {|r| r.sort == (1..9).to_a}
 end
 def valid_columns?
 board.columns.all? {|c| c.sort == (1..9).to_a}
 end
 def valid_squares?
 board.squares.all? {|s| s.sort == (1..9).to_a}
 end
end

That simplifies things greatly, but it pushes all the complexity to the Board class, right? Those long methods have to be somewhere, right? As it turns out, you can keep applying this method until all your code is short. The Rails codebase is an amazing example, check it out sometime.

Let's implement a Board class that has the rows, columns, and squares methods. As I was doing this I realized we can use Matrix which has a way to retrieve columns (though you can also use transpose on an AoA) and a way to retrieve sub-matrices to get the 3x3 squares.

board.rb

require 'matrix'
class Board
 attr_accessor :board
 def initialize(board_spec)
 @board = Matrix[*board_spec]
 end
 def rows
 @board.row_vectors
 end
 def columns
 @board.column_vectors
 end
 def squares
 # The `Matrix#minor` method extracts sub-matrices specified by
 # index ranges. These are the 3x3 squares in Sudoku.
 [
 [0..2, 0..2], [3..5, 0..2], [6..8, 0..2],
 [0..2, 3..5], [3..5, 3..5], [6..8, 3..5],
 [0..2, 6..8], [3..5, 6..8], [6..8, 6..8]
 ].map {|sq| @board.minor(*sq)}
 end
end

So there you have it. This is a working implementation of a standard 9x9 board. I left as an exercise handling an nxn board and the to_s method on Board, but it should be simple to do (hint: use in_groups_of(n) which is a method on Array).

answered May 29, 2017 at 19:56
\$\endgroup\$
2
  • 2
    \$\begingroup\$ quite simply brilliant and very readable. am envious of your application of matrix and enumerable methods to make it look so easy. i do not follow what the board.squares method does - btw should there be a third element in each array? thx for your post. i don't think i can emulate this \$\endgroup\$ Commented May 30, 2017 at 8:25
  • \$\begingroup\$ @BKSpurgeon Thanks for the kind words. I wasn't familiar with the minor method before answering this question, but figured there had to be a way to specify a submatrix. According to the docs, for each submatrix you can either specify four params (start_row, nrows, start_col, ncols) or two ranges (row_start..row_end, col_start..col_end). I chose the latter. \$\endgroup\$ Commented May 30, 2017 at 14:41

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.