diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..d34982f --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +# A sample Gemfile +source "https://rubygems.org" + +gem "mocha" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..2ef099e --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,12 @@ +GEM + remote: https://rubygems.org/ + specs: + metaclass (0.0.4) + mocha (1.0.0) + metaclass (~> 0.0.1) + +PLATFORMS + ruby + +DEPENDENCIES + mocha diff --git a/lib/sudoku_validator.rb b/lib/sudoku_validator.rb new file mode 100644 index 0000000..7a3305a --- /dev/null +++ b/lib/sudoku_validator.rb @@ -0,0 +1,93 @@ +class SudokuFileParser + attr_reader :grid + + def initialize(filename) + contents = IO.read(filename) + stripped = strip_unnecessary(contents) + @grid = gridify(stripped) + end + + private + + def strip_unnecessary(contents) + contents.gsub(/[^\d\.]/, '') + end + + def gridify(stripped) + stripped.chars.each_slice(9).to_a + end +end + + +class RowValidator + def initialize(grid) + @stripped_grid = strip_dots(grid) + end + + def valid? + @stripped_grid.all? { |row| row.uniq.size == row.size } + end + + private + + def strip_dots(grid) + grid.map do |row| + row.reject { |e| e == '.' } + end + end +end + + +class ColumnValidator + def initialize(grid) + @validator = RowValidator.new(grid.transpose) + end + + def valid? + @validator.valid? + end +end + + +class SubgridValidator + def initialize(grid) + modified_grid = subgrids_to_rows(grid) + @validator = RowValidator.new(modified_grid) + end + + def valid? + @validator.valid? + end + + private + + def subgrids_to_rows(grid) + grid.each_slice(3).each_with_object([]) do |rows, new_grid| + new_grid.push(*row_of_subgrids(rows)) + end + end + + def row_of_subgrids(rows) + rows.flatten + .each_slice(3) # in triples + .group_by.with_index { |_, i| i % 3 } # group every third + .map { |i, e| e.flatten } # form a row from subgrid elements + end +end + + +class SudokuValidator + def initialize(filename) + @grid = SudokuFileParser.new(filename).grid + @complete = !@grid.flatten.any? { |e| e == '.' } + end + + def valid? + validators = [RowValidator, ColumnValidator, SubgridValidator] + validators.all? { |v| v.new(@grid).valid? } + end + + def complete? + @complete + end +end diff --git a/sudoku-validator b/sudoku-validator new file mode 100755 index 0000000..df1916c --- /dev/null +++ b/sudoku-validator @@ -0,0 +1,12 @@ +#!/usr/bin/env ruby + +require_relative 'lib/sudoku_validator' + +app = SudokuValidator.new(ARGV.first) + +if app.valid? + ending = app.complete? ? '.' : ', but incomplete.' + puts "This sudoku is valid#{ending}" +else + puts 'This sudoku is invalid.' +end diff --git a/invalid_complete.sudoku b/test/invalid_complete.sudoku similarity index 100% rename from invalid_complete.sudoku rename to test/invalid_complete.sudoku diff --git a/invalid_incomplete.sudoku b/test/invalid_incomplete.sudoku similarity index 100% rename from invalid_incomplete.sudoku rename to test/invalid_incomplete.sudoku diff --git a/test/sudoku_validator_test.rb b/test/sudoku_validator_test.rb new file mode 100644 index 0000000..a4b33df --- /dev/null +++ b/test/sudoku_validator_test.rb @@ -0,0 +1,130 @@ +require 'minitest/autorun' +require 'minitest/pride' +require 'mocha/mini_test' + +require_relative '../lib/sudoku_validator' + +class SudokuFileParserTest < MiniTest::Unit::TestCase + def test_reads_file_into_9x9_2d_array + parser = SudokuFileParser.new('test/valid_complete.sudoku') + + assert_equal(9, parser.grid.size) + parser.grid.each { |row| assert_equal(9, row.size) } + end + + def test_parses_as_strings + expected = + [%w{8 5 . . . 2 4 . .}, + %w{7 2 . . . . . . 9}, + %w{. . 4 . . . . . .}, + %w{. . . 1 . 7 . . 2}, + %w{3 . 5 . . . 9 . .}, + %w{. 4 . . . . . . .}, + %w{. . . . 8 . . 7 .}, + %w{. 1 7 . . . . . .}, + %w{. . . . 3 6 . 4 .}] + + parser = SudokuFileParser.new('test/valid_incomplete.sudoku') + assert_equal(expected, parser.grid) + end +end + +class RowValidatorTest < MiniTest::Unit::TestCase + def test_valid_when_unique_in_row + grid = [%w{1 2 3}, %w{3 2 1}] + + validator = RowValidator.new(grid) + + assert(validator.valid?) + end + + def test_invalid_when_duplicates_in_row + grid = [%w{1 2 3}, %w{3 2 3}] + + validator = RowValidator.new(grid) + + refute(validator.valid?) + end + + def test_ignore_dots + assert(RowValidator.new([['.', '.'], ['.', '.']]).valid?) + assert(RowValidator.new([['.', '.'], ['1', '.']]).valid?) + refute(RowValidator.new([['.', '.', '2'], ['1', '.', '1']]).valid?) + end +end + +class ColumnValidatorTest < MiniTest::Unit::TestCase + def test_is_implemented_in_terms_of_row_validator_for_transposed_grid + grid =[%w{1 2 3}, %w{4 5 6}] + row_validator = mock() + row_validator.expects(:valid?).returns(true) + RowValidator + .expects(:new) + .with([%w{1 4}, %w{2 5}, %w{3 6}]) + .returns(row_validator) + + ColumnValidator.new(grid).valid? + end +end + +class SubgridValidatorTest < MiniTest::Unit::TestCase + def test_is_implemented_in_terms_of_row_validator_for_modified_grid + grid = + [%w{8 5 . . . 2 4 . .}, + %w{7 2 . . . . . . 9}, + %w{. . 4 . . . . . .}, + %w{. . . 1 . 7 . . 2}, + %w{3 . 5 . . . 9 . .}, + %w{. 4 . . . . . . .}, + %w{. . . . 8 . . 7 .}, + %w{. 1 7 . . . . . .}, + %w{. . . . 3 6 . 4 .}] + + modified_grid = + [%w{8 5 . 7 2 . . . 4}, + %w{. . 2 . . . . . .}, + %w{4 . . . . 9 . . .}, + %w{. . . 3 . 5 . 4 .}, + %w{1 . 7 . . . . . .}, + %w{. . 2 9 . . . . .}, + %w{. . . . 1 7 . . .}, + %w{. 8 . . . . . 3 6}, + %w{. 7 . . . . . 4 .}] + + row_validator = mock() + row_validator.expects(:valid?).returns(true) + RowValidator + .expects(:new) + .with(modified_grid) + .returns(row_validator) + + SubgridValidator.new(grid).valid? + end +end + +class SudokuValidatorTest < MiniTest::Unit::TestCase + def test_valid_complete + app = SudokuValidator.new('test/valid_complete.sudoku') + assert(app.valid?) + assert(app.complete?) + end + + def test_valid_incomplete + app = SudokuValidator.new('test/valid_incomplete.sudoku') + assert(app.valid?) + refute(app.complete?) + end + + def test_invalid_complete + app = SudokuValidator.new('test/invalid_complete.sudoku') + refute(app.valid?) + assert(app.complete?) + end + + def test_invalid_incomplete + app = SudokuValidator.new('test/invalid_incomplete.sudoku') + refute(app.valid?) + refute(app.complete?) + end +end + diff --git a/valid_complete.sudoku b/test/valid_complete.sudoku similarity index 100% rename from valid_complete.sudoku rename to test/valid_complete.sudoku diff --git a/valid_incomplete.sudoku b/test/valid_incomplete.sudoku similarity index 100% rename from valid_incomplete.sudoku rename to test/valid_incomplete.sudoku