diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cad2309 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/tmp \ No newline at end of file diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..5052887 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--color \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..20effee --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ + +gem 'rspec' +gem 'guard-rspec' \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..60f9b9a --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,48 @@ +GEM + specs: + celluloid (0.15.2) + timers (~> 1.1.0) + coderay (1.0.9) + diff-lcs (1.2.4) + ffi (1.9.0) + formatador (0.2.4) + guard (2.0.3) + formatador (>= 0.2.4) + listen (~> 2.0) + lumberjack (~> 1.0) + pry (>= 0.9.12) + thor (>= 0.18.1) + guard-rspec (3.1.0) + guard (>= 1.8) + rspec (~> 2.13) + listen (2.0.1) + celluloid (>= 0.15.2) + rb-fsevent (>= 0.9.3) + rb-inotify (>= 0.9) + lumberjack (1.0.4) + method_source (0.8.2) + pry (0.9.12.2) + coderay (~> 1.0.5) + method_source (~> 0.8) + slop (~> 3.4) + rb-fsevent (0.9.3) + rb-inotify (0.9.2) + ffi (>= 0.5.0) + rspec (2.14.1) + rspec-core (~> 2.14.0) + rspec-expectations (~> 2.14.0) + rspec-mocks (~> 2.14.0) + rspec-core (2.14.5) + rspec-expectations (2.14.3) + diff-lcs (>= 1.1.3, < 2.0) + rspec-mocks (2.14.3) + slop (3.4.6) + thor (0.18.1) + timers (1.1.0) + +PLATFORMS + ruby + +DEPENDENCIES + guard-rspec + rspec diff --git a/Guardfile b/Guardfile new file mode 100644 index 0000000..b70bc58 --- /dev/null +++ b/Guardfile @@ -0,0 +1,7 @@ + +guard :rspec, cli: "--tag ~@external" do + watch(%r{^spec/.+_spec\.rb$}) + watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } + watch('spec/spec_helper.rb') { "spec" } + watch(%r{^spec/support/.+\.rb$}) { "spec" } +end diff --git a/lib/sudoku_reader.rb b/lib/sudoku_reader.rb new file mode 100644 index 0000000..af0cb9d --- /dev/null +++ b/lib/sudoku_reader.rb @@ -0,0 +1,30 @@ + +class SudokuReader + + def self.reader_from_file(file_path) + reader = SudokuReader.new + reader.read(file_path) + reader + end + + attr_reader :grid + + def initialize + @grid = [] + end + + def read(file_path) + open(file_path).each do |line| + parsed_line = parse_line line + @grid << parsed_line if parsed_line + end + end + + def parse_line(line) + line.split(/[^\d\.]+/) unless separator_line? line + end + + def separator_line?(line) + line =~/^-+/ + end +end \ No newline at end of file diff --git a/lib/sudoku_validator.rb b/lib/sudoku_validator.rb new file mode 100644 index 0000000..1b1ace3 --- /dev/null +++ b/lib/sudoku_validator.rb @@ -0,0 +1,119 @@ +class SudokuValidator + + attr_reader :grid + attr_reader :errors + + def initialize(file_path) + @grid = SudokuReader.reader_from_file(file_path).grid + @errors = { + invalid: { + column: [], + row: [], + subgrid: [] + }, + incomplete: { + column: [], + row: [], + subgrid: [] + } + } + end + + def valid_array?(array) + numbers_only(array).uniq.length == numbers_only(array).length + end + + def complete_array?(array) + numbers_only(array).length == 9 + end + + def numbers_only(array) + array.reject { |el| el == "." } + end + + def array_is?(validation_type, array) + if validation_type == :invalid + not valid_array? array + else + not complete_array? array + end + end + + def row(i) + grid[i-1] + end + + def column(i) + grid.map { |row| row[i-1] } + end + + # subgrid is laid out as below + # 1-based 0-based + # 1 2 3 0 1 2 <- grid_index for column + # 1 - 3 4 - 6 7 - 9 0 - 2 3 - 5 6 - 8 + # 1 . . . | . . . | . . . 0 . . . | . . . | . . . + # 1 | . 1 . | . 2 . | . 3 . 0 | . 0 . | . 1 . | . 2 . + # 3 . . . | . . . | . . . 2 . . . | . . . | . . . + # +-----+-------+-----+ +-----+-------+-----+ + # 4 . . . | . . . | . . . 3 . . . | . . . | . . . + # 2 | . 4 . | . 5 . | . 6 . 1 | . 3 . | . 4 . | . 5 . + # 6 . . . | . . . | . . . 5 . . . | . . . | . . . + # +-----+-------+-----+ +-----+-------+-----+ + # 7 . . . | . . . | . . . 6 . . . | . . . | . . . + # 3 | . 7 . | . 8 . | . 9 . 2 | . 6 . | . 7 . | . 8 . + # 9 . . . | . . . | . . . 8 . . . | . . . | . . . + # + # ^ + # grid_index for row + # + def subgrid(i) + grid[row_starts_for_grid(i), 3].map do |row| + row[column_starts_for_grid(i), 3] + end.flatten + end + + def row_starts_for_grid(i) + zero_based(i) / 3 * 3 + end + + def column_starts_for_grid(i) + zero_based(i) % 3 * 3 + end + + def zero_based(i) + i - 1 + end + + def validate! + reset_errors! + [:invalid, :incomplete].each do |error_type| + [:row, :column, :subgrid].each do |array_type| + (1..9).each do |i| + errors[error_type][array_type] << i if array_is? error_type, send(array_type, i) + end + end + end + end + + def valid? + [:row, :column, :subgrid].all? { |array_type| errors[:invalid][array_type].none? } + end + + def complete? + [:row, :column, :subgrid].all? { |array_type| errors[:incomplete][array_type].none? } + end + + def reset_errors! + [:invalid, :incomplete].each do |error_type| + [:row, :column, :subgrid].each do |array_type| + errors[error_type][array_type].clear + end + end + end + + def error_messages(type) + errors[type].map do |key, vals| + "#{type} #{key}s: #{vals.join(", ")}" if vals.any? + end.compact.sort + end +end \ No newline at end of file diff --git a/spec/fixtures/invalid_complete.sudoku b/spec/fixtures/invalid_complete.sudoku new file mode 100644 index 0000000..ba5fb31 --- /dev/null +++ b/spec/fixtures/invalid_complete.sudoku @@ -0,0 +1,11 @@ +8 5 9 |6 1 2 |4 3 7 +7 2 3 |8 5 4 |1 6 9 +1 6 4 |3 7 9 |5 2 8 +------+------+------ +9 8 6 |1 4 7 |3 5 2 +3 7 5 |8 6 2 |9 1 4 +2 4 1 |5 9 3 |7 8 6 +------+------+------ +4 3 2 |9 8 1 |6 7 5 +6 1 7 |4 2 5 |8 9 3 +5 9 8 |7 3 6 |2 4 1 diff --git a/spec/fixtures/invalid_incomplete.sudoku b/spec/fixtures/invalid_incomplete.sudoku new file mode 100644 index 0000000..7e5b7f4 --- /dev/null +++ b/spec/fixtures/invalid_incomplete.sudoku @@ -0,0 +1,11 @@ +8 5 . |. . 2 |4 . . +7 2 . |. 8 . |. . 9 +. . 4 |. . . |. . . +------+------+------ +. . . |1 . 7 |. . 2 +3 . 5 |. . . |9 . . +. 4 . |. . . |. . . +------+------+------ +. 5 . |. 8 . |. 7 . +. 1 7 |. . . |. . . +. . . |. 3 6 |. 4 . diff --git a/spec/fixtures/valid_complete.sudoku b/spec/fixtures/valid_complete.sudoku new file mode 100644 index 0000000..ec5075c --- /dev/null +++ b/spec/fixtures/valid_complete.sudoku @@ -0,0 +1,11 @@ +8 5 9 |6 1 2 |4 3 7 +7 2 3 |8 5 4 |1 6 9 +1 6 4 |3 7 9 |5 2 8 +------+------+------ +9 8 6 |1 4 7 |3 5 2 +3 7 5 |2 6 8 |9 1 4 +2 4 1 |5 9 3 |7 8 6 +------+------+------ +4 3 2 |9 8 1 |6 7 5 +6 1 7 |4 2 5 |8 9 3 +5 9 8 |7 3 6 |2 4 1 diff --git a/spec/fixtures/valid_incomplete.sudoku b/spec/fixtures/valid_incomplete.sudoku new file mode 100644 index 0000000..bba06a1 --- /dev/null +++ b/spec/fixtures/valid_incomplete.sudoku @@ -0,0 +1,11 @@ +8 5 . |. . 2 |4 . . +7 2 . |. . . |. . 9 +. . 4 |. . . |. . . +------+------+------ +. . . |1 . 7 |. . 2 +3 . 5 |. . . |9 . . +. 4 . |. . . |. . . +------+------+------ +. . . |. 8 . |. 7 . +. 1 7 |. . . |. . . +. . . |. 3 6 |. 4 . diff --git a/spec/lib/sudoku_reader_spec.rb b/spec/lib/sudoku_reader_spec.rb new file mode 100644 index 0000000..7e68945 --- /dev/null +++ b/spec/lib/sudoku_reader_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe SudokuReader do + + let(:reader) { SudokuReader.new } + + describe '#parse_line' do + it 'returns an array' do + parsed_line = reader.parse_line "8 5 9 |6 1 2 |4 3 7 " + + expect(parsed_line).to eq ["8", "5", "9", "6", "1", "2", "4", "3", "7"] + + parsed_line = reader.parse_line ". . . |. 8 . |. 7 . " + + expect(parsed_line).to eq [".", ".", ".", ".", "8", ".", ".", "7", "."] + + parsed_line = reader.parse_line "------+------+------" + + expect(parsed_line).to be_nil + end + end + + describe '#separator_line?' do + it 'returns true if the line is -----' do + line = "------+------+------" + + expect(reader.separator_line?(line)).to be_true + + line = "8 5 9 |6 1 2 |4 3 7 " + + expect(reader.separator_line?(line)).to be_false + + line = ". . . |. 8 . |. 7 . " + + expect(reader.separator_line?(line)).to be_false + end + end + + describe '#read' do + it 'reads and stores the file into an array' do + reader.read fixture_path('valid_incomplete.sudoku') + + expect(reader.grid.length).to eq 9 # rows + reader.grid.each do |row| + expect(row.length).to eq 9 # columns + end + expect(reader.grid[0][4]).to eq "." + expect(reader.grid[2][2]).to eq "4" + end + end + + describe '#reader_from_file' do + it 'creates an instance of reader with a grid generated from file' do + reader = SudokuReader.reader_from_file(fixture_path('valid_incomplete.sudoku')) + + expect(reader.grid.length).to eq 9 + expect(reader.grid[0][4]).to eq "." + expect(reader.grid[2][2]).to eq "4" + end + end +end \ No newline at end of file diff --git a/spec/lib/sudoku_validator_spec.rb b/spec/lib/sudoku_validator_spec.rb new file mode 100644 index 0000000..e45b5d8 --- /dev/null +++ b/spec/lib/sudoku_validator_spec.rb @@ -0,0 +1,176 @@ +require 'spec_helper' +require 'yaml' + +describe SudokuValidator do + + def create_validator(file_path=fixture_path('valid_complete.sudoku')) + SudokuValidator.new(file_path) + end + + describe '#valid_array?' do + it 'returns false if there are repeats excluding "."' do + validator = create_validator + expect(validator.valid_array?(["1", "2", "3"])).to be_true + expect(validator.valid_array?(["1", "2", "3", "3"])).to be_false + expect(validator.valid_array?(["1", "2", ".", "."])).to be_true + end + end + + describe '#complete_array?' do + it 'returns false if the number elements have count of 9 excluding "."' do + validator = create_validator + expect(validator.complete_array?(["1", "2", ".", "."])).to be_false + expect(validator.complete_array?(["1", "2", "3", "4", "5", "6", "7", "8", "."])).to be_false + expect(validator.complete_array?(["1", "2", "3", "4", "5", "6", "7", "8", "8"])).to be_true + end + end + + describe '#array_ok?' do + it 'calls valid_array? if :invalid is given and returns the opposite' do + validator = create_validator + some_array = [1,2,3] + + expect(validator).to receive(:valid_array?).with(some_array).once.and_return(false) + expect(validator.array_is?(:invalid, some_array)).to be_true + expect(validator).to receive(:valid_array?).with(some_array).once.and_return(true) + expect(validator.array_is?(:invalid, some_array)).to be_false + end + + it 'calls complete_array? if :incomplete is given and returns the opposite' do + validator = create_validator + some_array = [1,2,3] + + expect(validator).to receive(:complete_array?).with(some_array).once.and_return(false) + expect(validator.array_is?(:incomplete, some_array)).to be_true + expect(validator).to receive(:complete_array?).with(some_array).once.and_return(true) + expect(validator.array_is?(:incomplete, some_array)).to be_false + end + end + + describe '#initialize' do + it 'takes a filepath and stores the grid' do + reader = SudokuReader.new + reader.read(fixture_path('valid_incomplete.sudoku')) + validator = SudokuValidator.new fixture_path('valid_incomplete.sudoku') + + expect(validator.grid).to eq reader.grid + end + end + + describe '#row' do + it 'retrieves a row as an array' do + validator = SudokuValidator.new fixture_path('valid_incomplete.sudoku') + + expect(validator.row(1)).to eq ["8", "5", ".", ".", ".", "2", "4", ".", "."] + expect(validator.row(5)).to eq ["3", ".", "5", ".", ".", ".", "9", ".", "."] + end + end + + describe '#column' do + it 'retrieves a column as an array' do + validator = SudokuValidator.new fixture_path('valid_incomplete.sudoku') + + expect(validator.column(2)).to eq ["5", "2", ".", ".", ".", "4", ".", "1", "."] + expect(validator.column(4)).to eq [".", ".", ".", "1", ".", ".", ".", ".", "."] + end + end + + describe '#subgrid' do + # subgrid is laid out as below + # 1 2 3 <- grid_column_index + # 1 - 3 4 - 6 7 - 9 + # 1 . . . | . . . | . . . + # 1 | . 1 . | . 2 . | . 3 . + # 3 . . . | . . . | . . . + # +-----+-------+-----+ + # 4 . . . | . . . | . . . + # 2 | . 4 . | . 5 . | . 6 . + # 6 . . . | . . . | . . . + # +-----+-------+-----+ + # 7 . . . | . . . | . . . + # 3 | . 7 . | . 8 . | . 9 . + # 9 . . . | . . . | . . . + # + # ^ + # grid_row_index + # + + it 'retrieves a subgrid as an array' do + validator = create_validator('valid_incomplete.sudoku') + + expect(validator.subgrid(3)).to eq ["4", ".", ".", ".", ".", "9", ".", ".", "."] + expect(validator.subgrid(7)).to eq [".", ".", ".", ".", "1", "7", ".", ".", "."] + end + end + + describe '#validate!' do + it 'runs finds and logs all the errors' do + validator = create_validator('invalid_incomplete.sudoku') + validator.validate! + expect(validator.errors[:invalid][:row]).to eq [] + expect(validator.errors[:invalid][:column]).to eq [2, 5] + expect(validator.errors[:invalid][:subgrid]).to eq [] + expect(validator.errors[:incomplete][:row]).to eq [1, 2, 3, 4, 5, 6, 7, 8, 9] + expect(validator.errors[:incomplete][:column]).to eq [1, 2, 3, 4, 5, 6, 7, 8, 9] + expect(validator.errors[:incomplete][:subgrid]).to eq [1, 2, 3, 4, 5, 6, 7, 8, 9] + end + end + + describe '#error_messages' do + it 'ouputs the correct error messages' do + validator = create_validator('invalid_incomplete.sudoku') + validator.validate! + + expect(validator.error_messages(:invalid)).to eq [ + "invalid columns: 2, 5" + ] + expect(validator.error_messages(:incomplete)).to eq [ + "incomplete columns: 1, 2, 3, 4, 5, 6, 7, 8, 9", + "incomplete rows: 1, 2, 3, 4, 5, 6, 7, 8, 9", + "incomplete subgrids: 1, 2, 3, 4, 5, 6, 7, 8, 9" + ] + end + end + + describe '#valid?' do + it 'validates all 3 array checks' do + validator = create_validator('valid_complete.sudoku') + validator.validate! + expect(validator.valid?).to be_true + + validator = create_validator('valid_incomplete.sudoku') + validator.validate! + expect(validator.valid?).to be_true + + validator = create_validator('invalid_complete.sudoku') + validator.validate! + expect(validator.valid?).to be_false + + validator = create_validator('invalid_incomplete.sudoku') + validator.validate! + expect(validator.valid?).to be_false + end + end + + describe '#complete?' do + it 'validates all 3 array checks' do + validator = create_validator('valid_complete.sudoku') + validator.validate! + expect(validator.complete?).to be_true + + validator = create_validator('valid_incomplete.sudoku') + validator.validate! + expect(validator.complete?).to be_false + + validator = create_validator('invalid_incomplete.sudoku') + validator.validate! + expect(validator.complete?).to be_false + + validator = create_validator('invalid_complete.sudoku') + validator.validate! + expect(validator.complete?).to be_true + end + end + + +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..8ed0794 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,10 @@ +require_relative '../lib/sudoku_reader' +require_relative '../lib/sudoku_validator' +require 'support/fixture_path' + + +RSpec.configure do |config| + config.treat_symbols_as_metadata_keys_with_true_values = true + config.filter_run focus: true + config.run_all_when_everything_filtered = true +end \ No newline at end of file diff --git a/spec/support/fixture_path.rb b/spec/support/fixture_path.rb new file mode 100644 index 0000000..83afb62 --- /dev/null +++ b/spec/support/fixture_path.rb @@ -0,0 +1,9 @@ +module FixturePath + def fixture_path(file_name) + File.join("spec/fixtures", file_name) + end +end + +RSpec.configure do |config| + config.include FixturePath +end \ No newline at end of file diff --git a/sudoku-validator b/sudoku-validator new file mode 100755 index 0000000..2598f7d --- /dev/null +++ b/sudoku-validator @@ -0,0 +1,53 @@ +#!/usr/bin/env ruby + +require_relative 'lib/sudoku_reader' +require_relative 'lib/sudoku_validator' +require 'json' + +path = ARGV[0] + +validator = SudokuValidator.new path +validator.validate! + +GRID_LOCATION = < subgrid + 6 . . . | . . . | . . . | + +-----+-------+-----+ + 7 . . . | . . . | . . . + 8 . 7 . | . 8 . | . 9 . + 9 . . . | . . . | . . . + + +********************************** +TEXT + +if validator.valid? + if validator.complete? + puts "This sudoku is valid." + else + puts "This sudoku is valid, but incomplete." + puts "" + validator.error_messages(:incomplete).each { |msg| puts msg } + puts GRID_LOCATION + end +else + puts 'This sudoku is invalid.' + puts "" + validator.error_messages(:invalid).each { |msg| puts msg } + validator.error_messages(:incomplete).each { |msg| puts msg } + puts GRID_LOCATION +end \ No newline at end of file