From 1c390adc61b95fb0a69ecbea95fc33983b163e86 Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Tue, 3 Dec 2013 01:32:27 +0000 Subject: [PATCH 01/24] Project setup. Adds first passing scenario. --- .gitignore | 1 + .rspec | 2 ++ Gemfile | 2 ++ Gemfile.lock | 45 +++++++++++++++++++++++++++++++ Rakefile | 9 +++++++ bin/sudoku-validator | 3 +++ features/sudoku_validator.feature | 5 ++++ features/support/env.rb | 1 + sudoku_validator.gemspec | 14 ++++++++++ 9 files changed, 82 insertions(+) create mode 100644 .gitignore create mode 100644 .rspec create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 Rakefile create mode 100755 bin/sudoku-validator create mode 100644 features/sudoku_validator.feature create mode 100644 features/support/env.rb create mode 100644 sudoku_validator.gemspec diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3fec32c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +tmp/ diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..16f9cdb --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--color +--format documentation diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..851fabc --- /dev/null +++ b/Gemfile @@ -0,0 +1,2 @@ +source 'https://rubygems.org' +gemspec diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..c9bd8d1 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,45 @@ +PATH + remote: . + specs: + sudoku_validator (0.0.1) + +GEM + remote: https://rubygems.org/ + specs: + aruba (0.5.3) + childprocess (>= 0.3.6) + cucumber (>= 1.1.1) + rspec-expectations (>= 2.7.0) + builder (3.2.2) + childprocess (0.3.9) + ffi (~> 1.0, >= 1.0.11) + cucumber (1.3.10) + builder (>= 2.1.2) + diff-lcs (>= 1.1.3) + gherkin (~> 2.12) + multi_json (>= 1.7.5, < 2.0) + multi_test (>= 0.0.2) + diff-lcs (1.2.5) + ffi (1.9.3) + gherkin (2.12.2) + multi_json (~> 1.3) + multi_json (1.8.2) + multi_test (0.0.2) + rake (10.1.0) + rspec (2.14.1) + rspec-core (~> 2.14.0) + rspec-expectations (~> 2.14.0) + rspec-mocks (~> 2.14.0) + rspec-core (2.14.7) + rspec-expectations (2.14.4) + diff-lcs (>= 1.1.3, < 2.0) + rspec-mocks (2.14.4) + +PLATFORMS + ruby + +DEPENDENCIES + aruba + rake + rspec + sudoku_validator! diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..c92204b --- /dev/null +++ b/Rakefile @@ -0,0 +1,9 @@ +require 'cucumber' +require 'cucumber/rake/task' + +Cucumber::Rake::Task.new(:features) do |t| + t.cucumber_opts = "features --format pretty -x" + t.fork = false +end + +task :default => :features diff --git a/bin/sudoku-validator b/bin/sudoku-validator new file mode 100755 index 0000000..9b20113 --- /dev/null +++ b/bin/sudoku-validator @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby + +puts "This sudoku is valid." diff --git a/features/sudoku_validator.feature b/features/sudoku_validator.feature new file mode 100644 index 0000000..5db2d88 --- /dev/null +++ b/features/sudoku_validator.feature @@ -0,0 +1,5 @@ +Feature: Validating .sudoku files + + Scenario: A valid, complete sudoku + When I successfully run `sudoku-validator ./valid_complete.sudoku` + Then the output should contain "This sudoku is valid." diff --git a/features/support/env.rb b/features/support/env.rb new file mode 100644 index 0000000..fb0a661 --- /dev/null +++ b/features/support/env.rb @@ -0,0 +1 @@ +require 'aruba/cucumber' diff --git a/sudoku_validator.gemspec b/sudoku_validator.gemspec new file mode 100644 index 0000000..a954cb4 --- /dev/null +++ b/sudoku_validator.gemspec @@ -0,0 +1,14 @@ +Gem::Specification.new do |s| + s.name = 'sudoku_validator' + s.version = '0.0.1' + s.summary = 'Sudoku Validator exercise from Thoughtbot' + s.author = 'Dan Scotton' + s.email = 'danscotton@gmail.com' + + s.add_development_dependency 'aruba' + s.add_development_dependency 'rake' + s.add_development_dependency 'rspec' + + s.executables << 'sudoku-validator' + s.require_path = "lib" +end From faad0b58685148274544ff6b492ae85b7575a045 Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Tue, 3 Dec 2013 09:16:08 +0000 Subject: [PATCH 02/24] Adds new failing scenario. Started to flesh out some objects and their interfaces. --- bin/sudoku-validator | 57 ++++++++++++++++++++++++++++++- features/sudoku_validator.feature | 4 +++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/bin/sudoku-validator b/bin/sudoku-validator index 9b20113..5a5ffca 100755 --- a/bin/sudoku-validator +++ b/bin/sudoku-validator @@ -1,3 +1,58 @@ #!/usr/bin/env ruby -puts "This sudoku is valid." +filename = ARGV[0] + +if File.exist?(filename) + file = File.read(filename) +end + +module Sudoku + class FileParser + def initialize(file) + + end + + def parse_rows + + end + end +end + +module Sudoku + class Puzzle + def initialize(rows) + + end + + def complete? + true + end + end +end + +module Sudoku + class Validator + def initialize(puzzle) + + end + + def valid? + true + end + end +end + +rows = Sudoku::FileParser.new(file).parse_rows +puzzle = Sudoku::Puzzle.new(rows) +validator = Sudoku::Validator.new(puzzle) + +case [validator.valid?, puzzle.complete?] + when [true, true] + puts "This sudoku is valid." + when [true, false] + puts "This sudoku is valid, but incomplete." + when [false, true] + puts "This sudoku is invalid." + when [false, false] + puts "This sudoku is invalid." +end diff --git a/features/sudoku_validator.feature b/features/sudoku_validator.feature index 5db2d88..f212f54 100644 --- a/features/sudoku_validator.feature +++ b/features/sudoku_validator.feature @@ -3,3 +3,7 @@ Feature: Validating .sudoku files Scenario: A valid, complete sudoku When I successfully run `sudoku-validator ./valid_complete.sudoku` Then the output should contain "This sudoku is valid." + + Scenario: A valid, incomplete sudoku + When I successfully run `sudoku-validator ./valid_incomplete.sudoko` + Then the output should contain "This sudoku is valid, but incomplete." From 060ed2597a0a0a238c24ccf199a131da8ab747f7 Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Tue, 3 Dec 2013 15:50:47 +0000 Subject: [PATCH 03/24] Adds Sudoku::FileParser with spec. --- .rspec | 1 + lib/sudoku_fileparser.rb | 30 +++++++++++++++++++++++ spec/lib/sudoku_fileparser_spec.rb | 38 ++++++++++++++++++++++++++++++ spec/spec_helper.rb | 17 +++++++++++++ 4 files changed, 86 insertions(+) create mode 100644 lib/sudoku_fileparser.rb create mode 100644 spec/lib/sudoku_fileparser_spec.rb create mode 100644 spec/spec_helper.rb diff --git a/.rspec b/.rspec index 16f9cdb..b83d9b7 100644 --- a/.rspec +++ b/.rspec @@ -1,2 +1,3 @@ --color --format documentation +--require spec_helper diff --git a/lib/sudoku_fileparser.rb b/lib/sudoku_fileparser.rb new file mode 100644 index 0000000..9f8ab80 --- /dev/null +++ b/lib/sudoku_fileparser.rb @@ -0,0 +1,30 @@ +module Sudoku + class FileParser + attr_reader :file + + def initialize(file = nil) + raise(ArgumentError, "A file is required.") if file.nil? + @file = file + end + + def parse_rows + file.strip.each_line.map do |line| + parse_line(line) unless separator?(line) + end.compact + end + + private + + def parse_line(line) + parse_empties(line).delete("\n|").split(" ") + end + + def parse_empties(line) + line.gsub(".", "0") + end + + def separator?(line) + line.start_with?('-') + end + end +end diff --git a/spec/lib/sudoku_fileparser_spec.rb b/spec/lib/sudoku_fileparser_spec.rb new file mode 100644 index 0000000..c236e5d --- /dev/null +++ b/spec/lib/sudoku_fileparser_spec.rb @@ -0,0 +1,38 @@ +require 'sudoku_fileparser' + +describe Sudoku::FileParser do + it "throws an ArgumentError if instantiated without a file" do + expect { Sudoku::FileParser.new }.to raise_error(ArgumentError) + end + + it "can parse rows out of the file into a simple array format" do + parser = Sudoku::FileParser.new(example_file) + parser.parse_rows.should eq [ + %w{ 0 5 9 6 1 2 4 3 0 }, + %w{ 7 0 3 8 5 4 1 0 9 }, + %w{ 1 6 0 3 7 9 0 2 8 }, + %w{ 9 8 6 0 4 0 3 5 2 }, + %w{ 3 7 5 2 0 8 9 1 4 }, + %w{ 2 4 1 0 9 0 7 8 6 }, + %w{ 4 3 0 9 8 1 0 7 5 }, + %w{ 6 0 7 4 2 5 8 0 3 }, + %w{ 0 9 8 7 3 6 2 4 0 } + ] + end + + def example_file + %Q{ +. 5 9 |6 1 2 |4 3 . +7 . 3 |8 5 4 |1 . 9 +1 6 . |3 7 9 |. 2 8 +------+------+------ +9 8 6 |. 4 . |3 5 2 +3 7 5 |2 . 8 |9 1 4 +2 4 1 |. 9 . |7 8 6 +------+------+------ +4 3 . |9 8 1 |. 7 5 +6 . 7 |4 2 5 |8 . 3 +. 9 8 |7 3 6 |2 4 . + } + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..dbc4f1a --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,17 @@ +# This file was generated by the `rspec --init` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# Require this file using `require "spec_helper"` to ensure that it is only +# loaded once. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + config.treat_symbols_as_metadata_keys_with_true_values = true + config.run_all_when_everything_filtered = true + config.filter_run :focus + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = 'random' +end From 37e1af68e77cd51a1253745554cbfa8fe30230d9 Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Thu, 5 Dec 2013 19:42:31 +0000 Subject: [PATCH 04/24] Adds Sudoku::Puzzle and Sudoku::Row specs with simple implementations inside the spec files. Also left a load of comments in to show my ideas of how I might express the errors. --- bin/sudoku-validator | 19 ++----- features/sudoku_validator.feature | 2 +- features/support/env.rb | 4 ++ spec/lib/sudoku_puzzle_spec.rb | 94 +++++++++++++++++++++++++++++++ spec/lib/sudoku_row_spec.rb | 83 +++++++++++++++++++++++++++ 5 files changed, 187 insertions(+), 15 deletions(-) create mode 100644 spec/lib/sudoku_puzzle_spec.rb create mode 100644 spec/lib/sudoku_row_spec.rb diff --git a/bin/sudoku-validator b/bin/sudoku-validator index 5a5ffca..004b907 100755 --- a/bin/sudoku-validator +++ b/bin/sudoku-validator @@ -1,21 +1,12 @@ #!/usr/bin/env ruby -filename = ARGV[0] - -if File.exist?(filename) - file = File.read(filename) -end - -module Sudoku - class FileParser - def initialize(file) +require_relative '../lib/sudoku_fileparser' - end - - def parse_rows +filename = ARGV[0] - end - end +filepath = File.expand_path(filename) +if File.exist?(filepath) + file = File.read(filepath) end module Sudoku diff --git a/features/sudoku_validator.feature b/features/sudoku_validator.feature index f212f54..8a80680 100644 --- a/features/sudoku_validator.feature +++ b/features/sudoku_validator.feature @@ -5,5 +5,5 @@ Feature: Validating .sudoku files Then the output should contain "This sudoku is valid." Scenario: A valid, incomplete sudoku - When I successfully run `sudoku-validator ./valid_incomplete.sudoko` + When I successfully run `sudoku-validator ./valid_incomplete.sudoku` Then the output should contain "This sudoku is valid, but incomplete." diff --git a/features/support/env.rb b/features/support/env.rb index fb0a661..e52a59f 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -1 +1,5 @@ require 'aruba/cucumber' + +Before do + @dirs = ["."] +end diff --git a/spec/lib/sudoku_puzzle_spec.rb b/spec/lib/sudoku_puzzle_spec.rb new file mode 100644 index 0000000..b6885cd --- /dev/null +++ b/spec/lib/sudoku_puzzle_spec.rb @@ -0,0 +1,94 @@ +module Sudoku + class Puzzle + def initialize(rows = []) + raise(ArgumentError, "An array of rows is required to create a puzzle.") if rows.empty? + @rows = rows + end + + def eql?(other) + hash == other.hash + end + alias_method :==, :eql? + + def hash + @rows.hash + end + end +end + +# "This sudoku is invalid." +# "- Row 1 contains a duplicate 5 in squares 1 and 3." +# "- Column 4 contains a duplicate 3 in squares 3 and 9." +# "- Box 5 contains a duplicate 11 in squares 1 and 2." + +# [ +# { +# :type => :row, +# :number => "5", +# :positions => [1, 3] +# } +# ] + +# errors = { +# :row => [ +# { +# :number => 1, +# :value => 5, +# :squares => [1, 3] +# } +# ], +# +# :row => { +# 1 => [ +# { +# :number => 5, +# :squares => [1, 3] +# }, +# { +# :number => 1, +# :squares => [2, 4] +# } +# ] +# } +# } + +# { +# :rows => { +# "1" => {"5" => [1, 3]} +# }, +# :columns => { +# "4" => {"3" => [3, 9]} +# }, +# :boxes => { +# "5" => {"9" => [1, 2]} +# } +# } + +describe Sudoku::Puzzle do + it "throws an ArgumentError if not passed an array of rows" do + expect { Sudoku::Puzzle.new }.to raise_error(ArgumentError) + end + + it "is a value object (two puzzles with the same numbers are equal)" do + p1 = Sudoku::Puzzle.new(input_rows) + p2 = Sudoku::Puzzle.new(input_rows) + + expect(p1).to eq p2 + end + + it "responds to :complete?" + + def input_rows + [ + %w{ 0 5 9 6 1 2 4 3 0 }, + %w{ 7 0 3 8 5 4 1 0 9 }, + %w{ 1 6 0 3 7 9 0 2 8 }, + %w{ 9 8 6 0 4 0 3 5 2 }, + %w{ 3 7 5 2 0 8 9 1 4 }, + %w{ 2 4 1 0 9 0 7 8 6 }, + %w{ 4 3 0 9 8 1 0 7 5 }, + %w{ 6 0 7 4 2 5 8 0 3 }, + %w{ 0 9 8 7 3 6 2 4 0 } + ] + end +end diff --git a/spec/lib/sudoku_row_spec.rb b/spec/lib/sudoku_row_spec.rb new file mode 100644 index 0000000..9b60f6a --- /dev/null +++ b/spec/lib/sudoku_row_spec.rb @@ -0,0 +1,83 @@ +require 'matrix' + +module Sudoku + class Row + def self.[](*values) + new(Vector.elements(values)) + end + + def positions + to_a.each.with_index(1).inject ({}) do |h, (number, position)| + h[number] = h[number] ? [h[number]].flatten << position : position + h + end + end + + def to_a + @row.to_a + end + + def complete? + to_a.reject {|n| n === 0}.uniq.length === to_a.length + end + + def eql?(other) + hash === other.hash + end + alias_method :==, :eql? + + def hash + @row.hash + end + + private + + def initialize(row) + @row = row + end + end +end + +describe Sudoku::Row do + + it "is a value object" do + r1 = Sudoku::Row[1, 2, 3] + r2 = Sudoku::Row[1, 2, 3] + + expect(r1).to eq r2 + end + + it "can be converted back to an array" do + r1 = Sudoku::Row[1, 2, 3] + expect(r1.to_a).to eq [1, 2, 3] + end + + it "can be complete" do + r1 = Sudoku::Row[3, 7, 5, 2, 6, 8, 9, 1, 4] + + expect(r1).to be_complete + end + + it "can be incomplete" do + r1 = Sudoku::Row[3, 7, 5, 2, 0, 8, 9, 1, 4] + + expect(r1).not_to be_complete + end + + it "can list each number and its position" do + r1 = Sudoku::Row[3, 7, 5, 2, 0, 8, 3, 1, 4] + + output = { + 0 => 5, + 1 => 8, + 2 => 4, + 3 => [1, 7], + 4 => 9, + 5 => 3, + 7 => 2, + 8 => 6 + } + expect(r1.positions).to eq output + end + +end From 8f3f42a9a0474efdcea75bcf2b960e09a1611c67 Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Fri, 6 Dec 2013 00:29:07 +0000 Subject: [PATCH 05/24] Move Puzzle and Row out to their own files. Refactor the specs a little. Be clear to use numbers, not strings. --- lib/sudoku_puzzle.rb | 25 +++++++++++++++ lib/sudoku_row.rb | 39 +++++++++++++++++++++++ spec/lib/sudoku_puzzle_spec.rb | 58 ++++++++++++---------------------- spec/lib/sudoku_row_spec.rb | 40 +---------------------- 4 files changed, 85 insertions(+), 77 deletions(-) create mode 100644 lib/sudoku_puzzle.rb create mode 100644 lib/sudoku_row.rb diff --git a/lib/sudoku_puzzle.rb b/lib/sudoku_puzzle.rb new file mode 100644 index 0000000..9fb50e1 --- /dev/null +++ b/lib/sudoku_puzzle.rb @@ -0,0 +1,25 @@ +require_relative 'sudoku_row' + +module Sudoku + class Puzzle + def initialize(rows = []) + raise(ArgumentError, "An array of rows is required to create a puzzle.") if rows.empty? + @rows = rows + end + + def rows + @rows.dup.map do |row| + Sudoku::Row[*row] + end + end + + def eql?(other) + hash == other.hash + end + alias_method :==, :eql? + + def hash + @rows.hash + end + end +end diff --git a/lib/sudoku_row.rb b/lib/sudoku_row.rb new file mode 100644 index 0000000..4de505b --- /dev/null +++ b/lib/sudoku_row.rb @@ -0,0 +1,39 @@ +require 'matrix' + +module Sudoku + class Row + def self.[](*values) + new(Vector.elements(values)) + end + + def positions + to_a.each.with_index(1).inject ({}) do |h, (number, position)| + h[number] = h[number] ? [h[number]].flatten << position : position + h + end + end + + def to_a + @row.to_a + end + + def complete? + to_a.reject {|n| n === 0}.uniq.length === to_a.length + end + + def eql?(other) + hash === other.hash + end + alias_method :==, :eql? + + def hash + @row.hash + end + + private + + def initialize(row) + @row = row + end + end +end diff --git a/spec/lib/sudoku_puzzle_spec.rb b/spec/lib/sudoku_puzzle_spec.rb index b6885cd..5fa92ec 100644 --- a/spec/lib/sudoku_puzzle_spec.rb +++ b/spec/lib/sudoku_puzzle_spec.rb @@ -1,20 +1,4 @@ -module Sudoku - class Puzzle - def initialize(rows = []) - raise(ArgumentError, "An array of rows is required to create a puzzle.") if rows.empty? - @rows = rows - end - - def eql?(other) - hash == other.hash - end - alias_method :==, :eql? - - def hash - @rows.hash - end - end -end +require 'sudoku_puzzle' # "This sudoku is invalid." # "- Row 1 contains a duplicate 5 in squares 1 and 3." @@ -52,18 +36,6 @@ def hash # } # } -# { -# :rows => { -# "1" => {"5" => [1, 3]} -# }, -# :columns => { -# "4" => {"3" => [3, 9]} -# }, -# :boxes => { -# "5" => {"9" => [1, 2]} -# } -# } - describe Sudoku::Puzzle do it "throws an ArgumentError if not passed an array of rows" do expect { Sudoku::Puzzle.new }.to raise_error(ArgumentError) @@ -76,19 +48,29 @@ def hash expect(p1).to eq p2 end + describe ":rows" do + it "returns an array of Row objects" do + puzzle = Sudoku::Puzzle.new(input_rows) + + expect(puzzle.rows.count).to eq 9 + expect(puzzle.rows.first).to eq Sudoku::Row[0, 5, 9, 6, 1, 2, 4, 3, 0] + expect(puzzle.rows.last).to eq Sudoku::Row[0, 9, 8, 7, 3, 6, 2, 4, 0] + end + end + it "responds to :complete?" def input_rows [ - %w{ 0 5 9 6 1 2 4 3 0 }, - %w{ 7 0 3 8 5 4 1 0 9 }, - %w{ 1 6 0 3 7 9 0 2 8 }, - %w{ 9 8 6 0 4 0 3 5 2 }, - %w{ 3 7 5 2 0 8 9 1 4 }, - %w{ 2 4 1 0 9 0 7 8 6 }, - %w{ 4 3 0 9 8 1 0 7 5 }, - %w{ 6 0 7 4 2 5 8 0 3 }, - %w{ 0 9 8 7 3 6 2 4 0 } + [0, 5, 9, 6, 1, 2, 4, 3, 0], + [7, 0, 3, 8, 5, 4, 1, 0, 9], + [1, 6, 0, 3, 7, 9, 0, 2, 8], + [9, 8, 6, 0, 4, 0, 3, 5, 2], + [3, 7, 5, 2, 0, 8, 9, 1, 4], + [2, 4, 1, 0, 9, 0, 7, 8, 6], + [4, 3, 0, 9, 8, 1, 0, 7, 5], + [6, 0, 7, 4, 2, 5, 8, 0, 3], + [0, 9, 8, 7, 3, 6, 2, 4, 0] ] end end diff --git a/spec/lib/sudoku_row_spec.rb b/spec/lib/sudoku_row_spec.rb index 9b60f6a..8033698 100644 --- a/spec/lib/sudoku_row_spec.rb +++ b/spec/lib/sudoku_row_spec.rb @@ -1,42 +1,4 @@ -require 'matrix' - -module Sudoku - class Row - def self.[](*values) - new(Vector.elements(values)) - end - - def positions - to_a.each.with_index(1).inject ({}) do |h, (number, position)| - h[number] = h[number] ? [h[number]].flatten << position : position - h - end - end - - def to_a - @row.to_a - end - - def complete? - to_a.reject {|n| n === 0}.uniq.length === to_a.length - end - - def eql?(other) - hash === other.hash - end - alias_method :==, :eql? - - def hash - @row.hash - end - - private - - def initialize(row) - @row = row - end - end -end +require 'sudoku_row' describe Sudoku::Row do From df3a0dc23e4df85f31bbb56d51850e5c318ad8d9 Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Fri, 6 Dec 2013 00:44:21 +0000 Subject: [PATCH 06/24] Adds Column class along with :columns message for Puzzle. Utilises Matrix inside Puzzle. --- lib/sudoku_column.rb | 24 ++++++++++++++++++++++++ lib/sudoku_puzzle.rb | 19 ++++++++++++++----- spec/lib/sudoku_column_spec.rb | 12 ++++++++++++ spec/lib/sudoku_puzzle_spec.rb | 9 +++++++++ 4 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 lib/sudoku_column.rb create mode 100644 spec/lib/sudoku_column_spec.rb diff --git a/lib/sudoku_column.rb b/lib/sudoku_column.rb new file mode 100644 index 0000000..d51d775 --- /dev/null +++ b/lib/sudoku_column.rb @@ -0,0 +1,24 @@ +require 'matrix' + +module Sudoku + class Column + def self.[](*values) + new(Vector.elements(values)) + end + + def eql?(other) + hash === other.hash + end + alias_method :==, :eql? + + def hash + @column.hash + end + + private + + def initialize(column) + @column = column + end + end +end diff --git a/lib/sudoku_puzzle.rb b/lib/sudoku_puzzle.rb index 9fb50e1..6bd19c3 100644 --- a/lib/sudoku_puzzle.rb +++ b/lib/sudoku_puzzle.rb @@ -1,16 +1,19 @@ require_relative 'sudoku_row' +require_relative 'sudoku_column' module Sudoku class Puzzle def initialize(rows = []) raise(ArgumentError, "An array of rows is required to create a puzzle.") if rows.empty? - @rows = rows + @grid = create_grid(rows) end def rows - @rows.dup.map do |row| - Sudoku::Row[*row] - end + @grid.row_vectors.map { |row| Sudoku::Row[*row] } + end + + def columns + @grid.column_vectors.map { |column| Sudoku::Column[*column] } end def eql?(other) @@ -19,7 +22,13 @@ def eql?(other) alias_method :==, :eql? def hash - @rows.hash + @grid.hash + end + + private + + def create_grid(input_rows) + Matrix[*input_rows] end end end diff --git a/spec/lib/sudoku_column_spec.rb b/spec/lib/sudoku_column_spec.rb new file mode 100644 index 0000000..ab950fc --- /dev/null +++ b/spec/lib/sudoku_column_spec.rb @@ -0,0 +1,12 @@ +require 'sudoku_column' + +describe Sudoku::Column do + + it "is a value object" do + c1 = Sudoku::Column[4, 5, 6] + c2 = Sudoku::Column[4, 5, 6] + + expect(c1).to eq c2 + end + +end diff --git a/spec/lib/sudoku_puzzle_spec.rb b/spec/lib/sudoku_puzzle_spec.rb index 5fa92ec..ac00aa8 100644 --- a/spec/lib/sudoku_puzzle_spec.rb +++ b/spec/lib/sudoku_puzzle_spec.rb @@ -58,6 +58,15 @@ end end + describe ":columns" do + it "returns an array of Column objects" do + puzzle = Sudoku::Puzzle.new(input_rows) + + expect(puzzle.columns.count).to eq 9 + expect(puzzle.columns.first).to eq Sudoku::Column[0, 7, 1, 9, 3, 2, 4, 6, 0] + end + end + it "responds to :complete?" def input_rows From cf3a99dc6050ad6b3b4226bf84ca949b4bd60d42 Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Sun, 8 Dec 2013 01:59:13 +0000 Subject: [PATCH 07/24] Refactored Row and Column objects to utilise new Unit module. Refactored specs into single Unit spec. Added new Box object that also is a Unit. --- lib/sudoku_column.rb | 24 ------------- lib/sudoku_puzzle.rb | 21 +++++++++--- lib/sudoku_row.rb | 39 ---------------------- lib/sudoku_unit.rb | 61 ++++++++++++++++++++++++++++++++++ spec/lib/sudoku_column_spec.rb | 12 ------- spec/lib/sudoku_puzzle_spec.rb | 46 ++++++------------------- spec/lib/sudoku_row_spec.rb | 45 ------------------------- spec/lib/sudoku_unit_spec.rb | 60 +++++++++++++++++++++++++++++++++ 8 files changed, 147 insertions(+), 161 deletions(-) delete mode 100644 lib/sudoku_column.rb delete mode 100644 lib/sudoku_row.rb create mode 100644 lib/sudoku_unit.rb delete mode 100644 spec/lib/sudoku_column_spec.rb delete mode 100644 spec/lib/sudoku_row_spec.rb create mode 100644 spec/lib/sudoku_unit_spec.rb diff --git a/lib/sudoku_column.rb b/lib/sudoku_column.rb deleted file mode 100644 index d51d775..0000000 --- a/lib/sudoku_column.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'matrix' - -module Sudoku - class Column - def self.[](*values) - new(Vector.elements(values)) - end - - def eql?(other) - hash === other.hash - end - alias_method :==, :eql? - - def hash - @column.hash - end - - private - - def initialize(column) - @column = column - end - end -end diff --git a/lib/sudoku_puzzle.rb b/lib/sudoku_puzzle.rb index 6bd19c3..105fa50 100644 --- a/lib/sudoku_puzzle.rb +++ b/lib/sudoku_puzzle.rb @@ -1,5 +1,5 @@ -require_relative 'sudoku_row' -require_relative 'sudoku_column' +require_relative 'sudoku_unit' +require 'matrix' module Sudoku class Puzzle @@ -9,22 +9,33 @@ def initialize(rows = []) end def rows - @grid.row_vectors.map { |row| Sudoku::Row[*row] } + @grid.row_vectors.map do |row| + Sudoku::Row[*row] + end end def columns - @grid.column_vectors.map { |column| Sudoku::Column[*column] } + @grid.column_vectors.map do |column| + Sudoku::Column[*column] + end + end + + def boxes + [0, 3, 6].repeated_permutation(2).map do |x, y| + Sudoku::Box[*@grid.minor(x, 3, y, 3).to_a.flatten] + end end def eql?(other) hash == other.hash end - alias_method :==, :eql? def hash @grid.hash end + alias_method :==, :eql? + private def create_grid(input_rows) diff --git a/lib/sudoku_row.rb b/lib/sudoku_row.rb deleted file mode 100644 index 4de505b..0000000 --- a/lib/sudoku_row.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'matrix' - -module Sudoku - class Row - def self.[](*values) - new(Vector.elements(values)) - end - - def positions - to_a.each.with_index(1).inject ({}) do |h, (number, position)| - h[number] = h[number] ? [h[number]].flatten << position : position - h - end - end - - def to_a - @row.to_a - end - - def complete? - to_a.reject {|n| n === 0}.uniq.length === to_a.length - end - - def eql?(other) - hash === other.hash - end - alias_method :==, :eql? - - def hash - @row.hash - end - - private - - def initialize(row) - @row = row - end - end -end diff --git a/lib/sudoku_unit.rb b/lib/sudoku_unit.rb new file mode 100644 index 0000000..f236a74 --- /dev/null +++ b/lib/sudoku_unit.rb @@ -0,0 +1,61 @@ +module Sudoku + module Unit + def positions + squares.each.with_index(1).inject ({}) do |h, (number, position)| + h[number] = h[number] ? [h[number]].flatten << position : position + h + end + end + + def valid? + non_empty_squares.uniq.count === non_empty_squares.count + end + + def complete? + squares.none? { |square| empty?(square) } + end + + private + + def squares + raise("Please set @squares in the object that implements this interface.") unless defined?(@squares) + @squares + end + + def non_empty_squares + squares.reject { |square| empty?(square) } + end + + def empty?(square) + square === 0 + end + end +end + +module Sudoku + class BaseUnit + include Unit + + def self.[](*values) + new(values) + end + + def initialize(values) + @squares = values + end + + def eql?(other) + self.class == other.class && hash == other.hash + end + + def hash + squares.hash + end + + alias_method :==, :eql? + end + + class Row < BaseUnit; end + class Column < BaseUnit; end + class Box < BaseUnit; end +end diff --git a/spec/lib/sudoku_column_spec.rb b/spec/lib/sudoku_column_spec.rb deleted file mode 100644 index ab950fc..0000000 --- a/spec/lib/sudoku_column_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'sudoku_column' - -describe Sudoku::Column do - - it "is a value object" do - c1 = Sudoku::Column[4, 5, 6] - c2 = Sudoku::Column[4, 5, 6] - - expect(c1).to eq c2 - end - -end diff --git a/spec/lib/sudoku_puzzle_spec.rb b/spec/lib/sudoku_puzzle_spec.rb index ac00aa8..66a3d9c 100644 --- a/spec/lib/sudoku_puzzle_spec.rb +++ b/spec/lib/sudoku_puzzle_spec.rb @@ -1,41 +1,5 @@ require 'sudoku_puzzle' -# "This sudoku is invalid." -# "- Row 1 contains a duplicate 5 in squares 1 and 3." -# "- Column 4 contains a duplicate 3 in squares 3 and 9." -# "- Box 5 contains a duplicate 11 in squares 1 and 2." - -# [ -# { -# :type => :row, -# :number => "5", -# :positions => [1, 3] -# } -# ] - -# errors = { -# :row => [ -# { -# :number => 1, -# :value => 5, -# :squares => [1, 3] -# } -# ], -# -# :row => { -# 1 => [ -# { -# :number => 5, -# :squares => [1, 3] -# }, -# { -# :number => 1, -# :squares => [2, 4] -# } -# ] -# } -# } - describe Sudoku::Puzzle do it "throws an ArgumentError if not passed an array of rows" do expect { Sudoku::Puzzle.new }.to raise_error(ArgumentError) @@ -64,6 +28,16 @@ expect(puzzle.columns.count).to eq 9 expect(puzzle.columns.first).to eq Sudoku::Column[0, 7, 1, 9, 3, 2, 4, 6, 0] + expect(puzzle.columns.last).to eq Sudoku::Column[0, 9, 8, 2, 4, 6, 5, 3, 0] + end + end + + describe ":boxes" do + it "returns an array of Box objects" do + puzzle = Sudoku::Puzzle.new(input_rows) + + expect(puzzle.boxes.count).to eq 9 + expect(puzzle.boxes.first).to eq Sudoku::Box[0, 5, 9, 7, 0, 3, 1, 6, 0] end end diff --git a/spec/lib/sudoku_row_spec.rb b/spec/lib/sudoku_row_spec.rb deleted file mode 100644 index 8033698..0000000 --- a/spec/lib/sudoku_row_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -require 'sudoku_row' - -describe Sudoku::Row do - - it "is a value object" do - r1 = Sudoku::Row[1, 2, 3] - r2 = Sudoku::Row[1, 2, 3] - - expect(r1).to eq r2 - end - - it "can be converted back to an array" do - r1 = Sudoku::Row[1, 2, 3] - expect(r1.to_a).to eq [1, 2, 3] - end - - it "can be complete" do - r1 = Sudoku::Row[3, 7, 5, 2, 6, 8, 9, 1, 4] - - expect(r1).to be_complete - end - - it "can be incomplete" do - r1 = Sudoku::Row[3, 7, 5, 2, 0, 8, 9, 1, 4] - - expect(r1).not_to be_complete - end - - it "can list each number and its position" do - r1 = Sudoku::Row[3, 7, 5, 2, 0, 8, 3, 1, 4] - - output = { - 0 => 5, - 1 => 8, - 2 => 4, - 3 => [1, 7], - 4 => 9, - 5 => 3, - 7 => 2, - 8 => 6 - } - expect(r1.positions).to eq output - end - -end diff --git a/spec/lib/sudoku_unit_spec.rb b/spec/lib/sudoku_unit_spec.rb new file mode 100644 index 0000000..98b01b7 --- /dev/null +++ b/spec/lib/sudoku_unit_spec.rb @@ -0,0 +1,60 @@ +require 'sudoku_unit' + +shared_examples "a Unit" do + context "completeness" do + specify "a unit is incomplete if it contains empty squares" do + expect(described_class[0, 5, 9, 6, 1, 2, 4, 3, 8]).not_to be_complete + end + + specify "a unit is complete if all squares are filled in" do + expect(described_class[7, 5, 9, 6, 1, 2, 4, 3, 8]).to be_complete + end + end + + context "validity" do + specify "a unit is invalid if it contains duplicates (excludes empty squares)" do + expect(described_class[0, 0, 9, 6, 1, 0, 9, 3, 8]).not_to be_valid + end + + specify "a unit is valid if it contains no duplicates" do + expect(described_class[0, 0, 2, 6, 1, 0, 9, 3, 8]).to be_valid + end + end + + describe "listing square values against their positions in the unit" do + specify ":positions can list empty squares" do + output = { + 0 => 8, 1 => 5, 2 => 6, + 4 => 7, 5 => 2, 6 => 4, + 7 => 1, 8 => 9, 9 => 3 + } + expect(described_class[7, 5, 9, 6, 1, 2, 4, 0, 8].positions).to eq output + end + + specify ":positions can list duplicate squares as an array" do + output = { + 0 => [6, 8], 1 => 5, 4 => 7, 5 => 2, + 7 => [1, 4], 8 => 9, 9 => 3 + } + expect(described_class[7, 5, 9, 7, 1, 0, 4, 0, 8].positions).to eq output + end + end + + context "immutability (value objects)" do + specify "two units of the same class with the same values are equal" do + expect(described_class[7, 5, 9, 6, 1, 2, 4, 3, 8]).to eq described_class[7, 5, 9, 6, 1, 2, 4, 3, 8] + end + end +end + +describe Sudoku::Row do + it_behaves_like "a Unit" +end + +describe Sudoku::Column do + it_behaves_like "a Unit" +end + +describe Sudoku::Box do + it_behaves_like "a Unit" +end From 17dea6db4c584f27d4f681b6d959026f637d5f52 Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Sun, 8 Dec 2013 02:04:07 +0000 Subject: [PATCH 08/24] Adds complete? method to Puzzle. --- lib/sudoku_puzzle.rb | 4 ++++ spec/lib/sudoku_puzzle_spec.rb | 12 +++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/sudoku_puzzle.rb b/lib/sudoku_puzzle.rb index 105fa50..c0e032b 100644 --- a/lib/sudoku_puzzle.rb +++ b/lib/sudoku_puzzle.rb @@ -26,6 +26,10 @@ def boxes end end + def complete? + rows.any? { |row| row.complete? } + end + def eql?(other) hash == other.hash end diff --git a/spec/lib/sudoku_puzzle_spec.rb b/spec/lib/sudoku_puzzle_spec.rb index 66a3d9c..573cc2b 100644 --- a/spec/lib/sudoku_puzzle_spec.rb +++ b/spec/lib/sudoku_puzzle_spec.rb @@ -41,7 +41,17 @@ end end - it "responds to :complete?" + describe "completeness" do + specify "a puzzle is complete if all of its rows are complete" do + puzzle = Sudoku::Puzzle.new([[1, 2, 3], [4, 9, 8], [5, 7, 6]]) + expect(puzzle).to be_complete + end + + specify "a puzzle is incomplete if any of its rows are incomplete" do + puzzle = Sudoku::Puzzle.new(input_rows) + expect(puzzle).not_to be_complete + end + end def input_rows [ From d0f951f53e88dc88eec630fd87e23f7eb2e14a64 Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Sun, 8 Dec 2013 02:11:01 +0000 Subject: [PATCH 09/24] Amend FileParser to convert strings into numbers. --- lib/sudoku_fileparser.rb | 2 +- spec/lib/sudoku_fileparser_spec.rb | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/sudoku_fileparser.rb b/lib/sudoku_fileparser.rb index 9f8ab80..82fb471 100644 --- a/lib/sudoku_fileparser.rb +++ b/lib/sudoku_fileparser.rb @@ -16,7 +16,7 @@ def parse_rows private def parse_line(line) - parse_empties(line).delete("\n|").split(" ") + parse_empties(line).delete("\n|").split(" ").map(&:to_i) end def parse_empties(line) diff --git a/spec/lib/sudoku_fileparser_spec.rb b/spec/lib/sudoku_fileparser_spec.rb index c236e5d..b0cd28a 100644 --- a/spec/lib/sudoku_fileparser_spec.rb +++ b/spec/lib/sudoku_fileparser_spec.rb @@ -5,18 +5,18 @@ expect { Sudoku::FileParser.new }.to raise_error(ArgumentError) end - it "can parse rows out of the file into a simple array format" do + it "can parse rows out of the file into a simple array format (strings are converted to numbers)" do parser = Sudoku::FileParser.new(example_file) parser.parse_rows.should eq [ - %w{ 0 5 9 6 1 2 4 3 0 }, - %w{ 7 0 3 8 5 4 1 0 9 }, - %w{ 1 6 0 3 7 9 0 2 8 }, - %w{ 9 8 6 0 4 0 3 5 2 }, - %w{ 3 7 5 2 0 8 9 1 4 }, - %w{ 2 4 1 0 9 0 7 8 6 }, - %w{ 4 3 0 9 8 1 0 7 5 }, - %w{ 6 0 7 4 2 5 8 0 3 }, - %w{ 0 9 8 7 3 6 2 4 0 } + [0, 5, 9, 6, 1, 2, 4, 3, 0], + [7, 0, 3, 8, 5, 4, 1, 0, 9], + [1, 6, 0, 3, 7, 9, 0, 2, 8], + [9, 8, 6, 0, 4, 0, 3, 5, 2], + [3, 7, 5, 2, 0, 8, 9, 1, 4], + [2, 4, 1, 0, 9, 0, 7, 8, 6], + [4, 3, 0, 9, 8, 1, 0, 7, 5], + [6, 0, 7, 4, 2, 5, 8, 0, 3], + [0, 9, 8, 7, 3, 6, 2, 4, 0] ] end From 18bd9c8ab1667e9836090320a4879d30e2149a6f Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Sun, 8 Dec 2013 02:11:49 +0000 Subject: [PATCH 10/24] =?UTF-8?q?Passing=20second=20scenario=20=E2=80=93?= =?UTF-8?q?=20replaced=20stub=20Puzzle=20with=20real=20implementation.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/sudoku-validator | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/bin/sudoku-validator b/bin/sudoku-validator index 004b907..56c8ed1 100755 --- a/bin/sudoku-validator +++ b/bin/sudoku-validator @@ -1,6 +1,7 @@ #!/usr/bin/env ruby require_relative '../lib/sudoku_fileparser' +require_relative '../lib/sudoku_puzzle' filename = ARGV[0] @@ -9,18 +10,6 @@ if File.exist?(filepath) file = File.read(filepath) end -module Sudoku - class Puzzle - def initialize(rows) - - end - - def complete? - true - end - end -end - module Sudoku class Validator def initialize(puzzle) From a260fd7fff2f2965056e15dc4348d0cb0aef324b Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Sun, 8 Dec 2013 11:51:53 +0000 Subject: [PATCH 11/24] Finalise feature with two remaining scenarios passing. Removed the SudokuValidator stub, as the Puzzle is now aware of its own validity. --- bin/sudoku-validator | 18 ++---------------- features/sudoku_validator.feature | 8 ++++++++ lib/sudoku_puzzle.rb | 10 ++++++++++ spec/lib/sudoku_puzzle_spec.rb | 28 +++++++++++++++++++++++++++- 4 files changed, 47 insertions(+), 17 deletions(-) diff --git a/bin/sudoku-validator b/bin/sudoku-validator index 56c8ed1..26d4ca2 100755 --- a/bin/sudoku-validator +++ b/bin/sudoku-validator @@ -10,23 +10,9 @@ if File.exist?(filepath) file = File.read(filepath) end -module Sudoku - class Validator - def initialize(puzzle) +puzzle = Sudoku::Puzzle.new(Sudoku::FileParser.new(file).parse_rows) - end - - def valid? - true - end - end -end - -rows = Sudoku::FileParser.new(file).parse_rows -puzzle = Sudoku::Puzzle.new(rows) -validator = Sudoku::Validator.new(puzzle) - -case [validator.valid?, puzzle.complete?] +case [puzzle.valid?, puzzle.complete?] when [true, true] puts "This sudoku is valid." when [true, false] diff --git a/features/sudoku_validator.feature b/features/sudoku_validator.feature index 8a80680..c5a264e 100644 --- a/features/sudoku_validator.feature +++ b/features/sudoku_validator.feature @@ -7,3 +7,11 @@ Feature: Validating .sudoku files Scenario: A valid, incomplete sudoku When I successfully run `sudoku-validator ./valid_incomplete.sudoku` Then the output should contain "This sudoku is valid, but incomplete." + + Scenario: An invalid, complete sudoku + When I successfully run `sudoku-validator ./invalid_complete.sudoku` + Then the output should contain "This sudoku is invalid." + + Scenario: An invalid, incomplete sudoku + When I successfully run `sudoku-validator ./invalid_incomplete.sudoku` + Then the output should contain "This sudoku is invalid." diff --git a/lib/sudoku_puzzle.rb b/lib/sudoku_puzzle.rb index c0e032b..c03cecb 100644 --- a/lib/sudoku_puzzle.rb +++ b/lib/sudoku_puzzle.rb @@ -30,6 +30,10 @@ def complete? rows.any? { |row| row.complete? } end + def valid? + all_units.all? { |unit| unit.valid? } + end + def eql?(other) hash == other.hash end @@ -45,5 +49,11 @@ def hash def create_grid(input_rows) Matrix[*input_rows] end + + def all_units + [:rows, :columns, :boxes].map do |message| + send(message) + end.flatten + end end end diff --git a/spec/lib/sudoku_puzzle_spec.rb b/spec/lib/sudoku_puzzle_spec.rb index 573cc2b..392ad7e 100644 --- a/spec/lib/sudoku_puzzle_spec.rb +++ b/spec/lib/sudoku_puzzle_spec.rb @@ -53,10 +53,22 @@ end end + describe "validity" do + specify "a puzzle is invalid if any of its rows, columns or boxes are invalid" do + puzzle = Sudoku::Puzzle.new(input_rows) + expect(puzzle).not_to be_valid + end + + specify "a puzzle is valid if all of its rows, columns and boxes are valid" do + puzzle = Sudoku::Puzzle.new(valid_incomplete) + expect(puzzle).to be_valid + end + end + def input_rows [ [0, 5, 9, 6, 1, 2, 4, 3, 0], - [7, 0, 3, 8, 5, 4, 1, 0, 9], + [7, 0, 3, 8, 5, 1, 1, 0, 9], [1, 6, 0, 3, 7, 9, 0, 2, 8], [9, 8, 6, 0, 4, 0, 3, 5, 2], [3, 7, 5, 2, 0, 8, 9, 1, 4], @@ -66,4 +78,18 @@ def input_rows [0, 9, 8, 7, 3, 6, 2, 4, 0] ] end + + def valid_incomplete + [ + [8, 5, 0, 0, 0, 2, 4, 0, 0], + [7, 2, 0, 0, 0, 0, 0, 0, 9], + [0, 0, 4, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 7, 0, 0, 2], + [3, 0, 5, 0, 0, 0, 9, 0, 0], + [0, 4, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 8, 0, 0, 7, 0], + [0, 1, 7, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 3, 6, 0, 4, 0] + ] + end end From ce92e3d48fea0c1b358303a4fee206a23db59810 Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Sun, 8 Dec 2013 20:06:31 +0000 Subject: [PATCH 12/24] Refactor to remove overuse of Sudoku module. Done the same in the specs to make the code easier to read. --- lib/sudoku_puzzle.rb | 6 +- spec/lib/sudoku_fileparser_spec.rb | 40 ++++---- spec/lib/sudoku_puzzle_spec.rb | 146 +++++++++++++++-------------- spec/lib/sudoku_unit_spec.rb | 18 ++-- 4 files changed, 108 insertions(+), 102 deletions(-) diff --git a/lib/sudoku_puzzle.rb b/lib/sudoku_puzzle.rb index c03cecb..ad93640 100644 --- a/lib/sudoku_puzzle.rb +++ b/lib/sudoku_puzzle.rb @@ -10,19 +10,19 @@ def initialize(rows = []) def rows @grid.row_vectors.map do |row| - Sudoku::Row[*row] + Row[*row] end end def columns @grid.column_vectors.map do |column| - Sudoku::Column[*column] + Column[*column] end end def boxes [0, 3, 6].repeated_permutation(2).map do |x, y| - Sudoku::Box[*@grid.minor(x, 3, y, 3).to_a.flatten] + Box[*@grid.minor(x, 3, y, 3).to_a.flatten] end end diff --git a/spec/lib/sudoku_fileparser_spec.rb b/spec/lib/sudoku_fileparser_spec.rb index b0cd28a..7ff6451 100644 --- a/spec/lib/sudoku_fileparser_spec.rb +++ b/spec/lib/sudoku_fileparser_spec.rb @@ -1,26 +1,27 @@ require 'sudoku_fileparser' -describe Sudoku::FileParser do - it "throws an ArgumentError if instantiated without a file" do - expect { Sudoku::FileParser.new }.to raise_error(ArgumentError) - end +module Sudoku + describe FileParser do + it "throws an ArgumentError if instantiated without a file" do + expect { FileParser.new }.to raise_error(ArgumentError) + end - it "can parse rows out of the file into a simple array format (strings are converted to numbers)" do - parser = Sudoku::FileParser.new(example_file) - parser.parse_rows.should eq [ - [0, 5, 9, 6, 1, 2, 4, 3, 0], - [7, 0, 3, 8, 5, 4, 1, 0, 9], - [1, 6, 0, 3, 7, 9, 0, 2, 8], - [9, 8, 6, 0, 4, 0, 3, 5, 2], - [3, 7, 5, 2, 0, 8, 9, 1, 4], - [2, 4, 1, 0, 9, 0, 7, 8, 6], - [4, 3, 0, 9, 8, 1, 0, 7, 5], - [6, 0, 7, 4, 2, 5, 8, 0, 3], - [0, 9, 8, 7, 3, 6, 2, 4, 0] - ] - end + it "can parse rows out of the file into a simple array format (strings are converted to numbers)" do + parser = FileParser.new(example_file) + parser.parse_rows.should eq [ + [0, 5, 9, 6, 1, 2, 4, 3, 0], + [7, 0, 3, 8, 5, 4, 1, 0, 9], + [1, 6, 0, 3, 7, 9, 0, 2, 8], + [9, 8, 6, 0, 4, 0, 3, 5, 2], + [3, 7, 5, 2, 0, 8, 9, 1, 4], + [2, 4, 1, 0, 9, 0, 7, 8, 6], + [4, 3, 0, 9, 8, 1, 0, 7, 5], + [6, 0, 7, 4, 2, 5, 8, 0, 3], + [0, 9, 8, 7, 3, 6, 2, 4, 0] + ] + end - def example_file + def example_file %Q{ . 5 9 |6 1 2 |4 3 . 7 . 3 |8 5 4 |1 . 9 @@ -34,5 +35,6 @@ def example_file 6 . 7 |4 2 5 |8 . 3 . 9 8 |7 3 6 |2 4 . } + end end end diff --git a/spec/lib/sudoku_puzzle_spec.rb b/spec/lib/sudoku_puzzle_spec.rb index 392ad7e..dfbaabc 100644 --- a/spec/lib/sudoku_puzzle_spec.rb +++ b/spec/lib/sudoku_puzzle_spec.rb @@ -1,95 +1,97 @@ require 'sudoku_puzzle' -describe Sudoku::Puzzle do - it "throws an ArgumentError if not passed an array of rows" do - expect { Sudoku::Puzzle.new }.to raise_error(ArgumentError) - end +module Sudoku + describe Puzzle do + it "throws an ArgumentError if not passed an array of rows" do + expect { Puzzle.new }.to raise_error(ArgumentError) + end - it "is a value object (two puzzles with the same numbers are equal)" do - p1 = Sudoku::Puzzle.new(input_rows) - p2 = Sudoku::Puzzle.new(input_rows) + it "is a value object (two puzzles with the same numbers are equal)" do + p1 = Puzzle.new(input_rows) + p2 = Puzzle.new(input_rows) - expect(p1).to eq p2 - end + expect(p1).to eq p2 + end - describe ":rows" do - it "returns an array of Row objects" do - puzzle = Sudoku::Puzzle.new(input_rows) + describe ":rows" do + it "returns an array of Row objects" do + puzzle = Puzzle.new(input_rows) - expect(puzzle.rows.count).to eq 9 - expect(puzzle.rows.first).to eq Sudoku::Row[0, 5, 9, 6, 1, 2, 4, 3, 0] - expect(puzzle.rows.last).to eq Sudoku::Row[0, 9, 8, 7, 3, 6, 2, 4, 0] + expect(puzzle.rows.count).to eq 9 + expect(puzzle.rows.first).to eq Row[0, 5, 9, 6, 1, 2, 4, 3, 0] + expect(puzzle.rows.last).to eq Row[0, 9, 8, 7, 3, 6, 2, 4, 0] + end end - end - describe ":columns" do - it "returns an array of Column objects" do - puzzle = Sudoku::Puzzle.new(input_rows) + describe ":columns" do + it "returns an array of Column objects" do + puzzle = Puzzle.new(input_rows) - expect(puzzle.columns.count).to eq 9 - expect(puzzle.columns.first).to eq Sudoku::Column[0, 7, 1, 9, 3, 2, 4, 6, 0] - expect(puzzle.columns.last).to eq Sudoku::Column[0, 9, 8, 2, 4, 6, 5, 3, 0] + expect(puzzle.columns.count).to eq 9 + expect(puzzle.columns.first).to eq Column[0, 7, 1, 9, 3, 2, 4, 6, 0] + expect(puzzle.columns.last).to eq Column[0, 9, 8, 2, 4, 6, 5, 3, 0] + end end - end - describe ":boxes" do - it "returns an array of Box objects" do - puzzle = Sudoku::Puzzle.new(input_rows) + describe ":boxes" do + it "returns an array of Box objects" do + puzzle = Puzzle.new(input_rows) - expect(puzzle.boxes.count).to eq 9 - expect(puzzle.boxes.first).to eq Sudoku::Box[0, 5, 9, 7, 0, 3, 1, 6, 0] + expect(puzzle.boxes.count).to eq 9 + expect(puzzle.boxes.first).to eq Box[0, 5, 9, 7, 0, 3, 1, 6, 0] + end end - end - describe "completeness" do - specify "a puzzle is complete if all of its rows are complete" do - puzzle = Sudoku::Puzzle.new([[1, 2, 3], [4, 9, 8], [5, 7, 6]]) - expect(puzzle).to be_complete - end + describe "completeness" do + specify "a puzzle is complete if all of its rows are complete" do + puzzle = Puzzle.new([[1, 2, 3], [4, 9, 8], [5, 7, 6]]) + expect(puzzle).to be_complete + end - specify "a puzzle is incomplete if any of its rows are incomplete" do - puzzle = Sudoku::Puzzle.new(input_rows) - expect(puzzle).not_to be_complete + specify "a puzzle is incomplete if any of its rows are incomplete" do + puzzle = Puzzle.new(input_rows) + expect(puzzle).not_to be_complete + end end - end - describe "validity" do - specify "a puzzle is invalid if any of its rows, columns or boxes are invalid" do - puzzle = Sudoku::Puzzle.new(input_rows) - expect(puzzle).not_to be_valid - end + describe "validity" do + specify "a puzzle is invalid if any of its rows, columns or boxes are invalid" do + puzzle = Puzzle.new(input_rows) + expect(puzzle).not_to be_valid + end - specify "a puzzle is valid if all of its rows, columns and boxes are valid" do - puzzle = Sudoku::Puzzle.new(valid_incomplete) - expect(puzzle).to be_valid + specify "a puzzle is valid if all of its rows, columns and boxes are valid" do + puzzle = Puzzle.new(valid_incomplete) + expect(puzzle).to be_valid + end end - end - def input_rows - [ - [0, 5, 9, 6, 1, 2, 4, 3, 0], - [7, 0, 3, 8, 5, 1, 1, 0, 9], - [1, 6, 0, 3, 7, 9, 0, 2, 8], - [9, 8, 6, 0, 4, 0, 3, 5, 2], - [3, 7, 5, 2, 0, 8, 9, 1, 4], - [2, 4, 1, 0, 9, 0, 7, 8, 6], - [4, 3, 0, 9, 8, 1, 0, 7, 5], - [6, 0, 7, 4, 2, 5, 8, 0, 3], - [0, 9, 8, 7, 3, 6, 2, 4, 0] - ] - end + def input_rows + [ + [0, 5, 9, 6, 1, 2, 4, 3, 0], + [7, 0, 3, 8, 5, 1, 1, 0, 9], + [1, 6, 0, 3, 7, 9, 0, 2, 8], + [9, 8, 6, 0, 4, 0, 3, 5, 2], + [3, 7, 5, 2, 0, 8, 9, 1, 4], + [2, 4, 1, 0, 9, 0, 7, 8, 6], + [4, 3, 0, 9, 8, 1, 0, 7, 5], + [6, 0, 7, 4, 2, 5, 8, 0, 3], + [0, 9, 8, 7, 3, 6, 2, 4, 0] + ] + end - def valid_incomplete - [ - [8, 5, 0, 0, 0, 2, 4, 0, 0], - [7, 2, 0, 0, 0, 0, 0, 0, 9], - [0, 0, 4, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 1, 0, 7, 0, 0, 2], - [3, 0, 5, 0, 0, 0, 9, 0, 0], - [0, 4, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 8, 0, 0, 7, 0], - [0, 1, 7, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 3, 6, 0, 4, 0] - ] + def valid_incomplete + [ + [8, 5, 0, 0, 0, 2, 4, 0, 0], + [7, 2, 0, 0, 0, 0, 0, 0, 9], + [0, 0, 4, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 7, 0, 0, 2], + [3, 0, 5, 0, 0, 0, 9, 0, 0], + [0, 4, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 8, 0, 0, 7, 0], + [0, 1, 7, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 3, 6, 0, 4, 0] + ] + end end end diff --git a/spec/lib/sudoku_unit_spec.rb b/spec/lib/sudoku_unit_spec.rb index 98b01b7..cd77439 100644 --- a/spec/lib/sudoku_unit_spec.rb +++ b/spec/lib/sudoku_unit_spec.rb @@ -47,14 +47,16 @@ end end -describe Sudoku::Row do - it_behaves_like "a Unit" -end +module Sudoku + describe Row do + it_behaves_like "a Unit" + end -describe Sudoku::Column do - it_behaves_like "a Unit" -end + describe Column do + it_behaves_like "a Unit" + end -describe Sudoku::Box do - it_behaves_like "a Unit" + describe Box do + it_behaves_like "a Unit" + end end From c1797a1eb2d3c28e2ab2d96fdc4f1c3a468c0fb7 Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Mon, 9 Dec 2013 16:57:32 +0000 Subject: [PATCH 13/24] Adds new error listing requirements to the feature. Refactored out a new Validator class. Previous scenarios green, but new steps failing. --- bin/sudoku-validator | 18 ++++++------ features/sudoku_validator.feature | 2 ++ lib/sudoku_validator.rb | 23 ++++++++++++++++ spec/lib/sudoku_sudoku_validator_spec.rb | 35 ++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 lib/sudoku_validator.rb create mode 100644 spec/lib/sudoku_sudoku_validator_spec.rb diff --git a/bin/sudoku-validator b/bin/sudoku-validator index 26d4ca2..ff7b506 100755 --- a/bin/sudoku-validator +++ b/bin/sudoku-validator @@ -2,6 +2,7 @@ require_relative '../lib/sudoku_fileparser' require_relative '../lib/sudoku_puzzle' +require_relative '../lib/sudoku_validator' filename = ARGV[0] @@ -11,14 +12,13 @@ if File.exist?(filepath) end puzzle = Sudoku::Puzzle.new(Sudoku::FileParser.new(file).parse_rows) +validator = Sudoku::Validator.new(puzzle) -case [puzzle.valid?, puzzle.complete?] - when [true, true] - puts "This sudoku is valid." - when [true, false] - puts "This sudoku is valid, but incomplete." - when [false, true] - puts "This sudoku is invalid." - when [false, false] - puts "This sudoku is invalid." +puts validator.status +if validator.errors + puts "Errors:" + puts "" + validator.errors.each.with_index(1) do |error, index| + puts " #{index}) #{error}" + end end diff --git a/features/sudoku_validator.feature b/features/sudoku_validator.feature index c5a264e..717c893 100644 --- a/features/sudoku_validator.feature +++ b/features/sudoku_validator.feature @@ -11,6 +11,8 @@ Feature: Validating .sudoku files Scenario: An invalid, complete sudoku When I successfully run `sudoku-validator ./invalid_complete.sudoku` Then the output should contain "This sudoku is invalid." + Then the output should contain "Column 4 contains a duplicate 8 in squares 2 and 5." + Then the output should contain "Column 6 contains a duplicate 2 in squares 1 and 5." Scenario: An invalid, incomplete sudoku When I successfully run `sudoku-validator ./invalid_incomplete.sudoku` diff --git a/lib/sudoku_validator.rb b/lib/sudoku_validator.rb new file mode 100644 index 0000000..d274e24 --- /dev/null +++ b/lib/sudoku_validator.rb @@ -0,0 +1,23 @@ +module Sudoku + class Validator + attr_reader :puzzle + + def initialize(puzzle = nil) + raise(ArgumentError, "A Puzzle is required.") if puzzle.nil? + @puzzle = puzzle + end + + def status + case [puzzle.valid?, puzzle.complete?] + when [true, true] then "This sudoku is valid." + when [true, false] then "This sudoku is valid, but incomplete." + + else "This sudoku is invalid." + end + end + + def errors + + end + end +end diff --git a/spec/lib/sudoku_sudoku_validator_spec.rb b/spec/lib/sudoku_sudoku_validator_spec.rb new file mode 100644 index 0000000..9d81f61 --- /dev/null +++ b/spec/lib/sudoku_sudoku_validator_spec.rb @@ -0,0 +1,35 @@ +require 'sudoku_validator' + +module Sudoku + describe Validator do + it "throws an ArgumentError if a puzzle is not passed in" do + expect { Validator.new }.to raise_error(ArgumentError) + end + + describe "status" do + it "returns 'This sudoku is valid.' if the puzzle is valid and complete" do + puzzle = double(:valid? => true, :complete? => true) + expect(Validator.new(puzzle).status).to eq "This sudoku is valid." + end + + it "returns 'This sudoku is valid, but incomplete.' if the puzzle is valid, but incomplete" do + puzzle = double(:valid? => true, :complete? => false) + expect(Validator.new(puzzle).status).to eq "This sudoku is valid, but incomplete." + end + + it "returns 'This sudoku is invalid.' if the puzzle is invalid, but complete." do + puzzle = double(:valid? => false, :complete? => true) + expect(Validator.new(puzzle).status).to eq "This sudoku is invalid." + end + + it "returns 'This sudoku is invalid.' if the puzzle is invalid and incomplete." do + puzzle = double(:valid? => false, :complete? => false) + expect(Validator.new(puzzle).status).to eq "This sudoku is invalid." + end + end + + it "responds to :errors" do + expect(Validator.new(double())).to respond_to(:errors) + end + end +end From dd4a140cbd828271acbdf3c2cf9b1faf5471e641 Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Mon, 9 Dec 2013 18:04:38 +0000 Subject: [PATCH 14/24] Adds new spec describing error output for rows. Feature still failing. Basic implementation, needs refactoring. --- lib/sudoku_validator.rb | 20 +++++++++++++++++ spec/lib/sudoku_sudoku_validator_spec.rb | 28 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/lib/sudoku_validator.rb b/lib/sudoku_validator.rb index d274e24..e387b0b 100644 --- a/lib/sudoku_validator.rb +++ b/lib/sudoku_validator.rb @@ -17,7 +17,27 @@ def status end def errors + return [] if puzzle.valid? + puzzle.rows.map do |row_number, row| + x = row.positions.select { |value, position| position.is_a?(Array) } + x.empty? ? nil : x.map {|number, squares| DuplicateError.new(row_number, number, squares).to_s } + end.compact.flatten + end + + class DuplicateError < Struct.new(:row_number, :number, :squares) + def to_s + "#{unit_type} #{row_number} contains a duplicate #{number} in squares #{list(squares)}" + end + + private + def unit_type + "Row" + end + + def list(squares) + "#{squares.first(squares.count-1).join(', ')} and #{squares.last}" + end end end end diff --git a/spec/lib/sudoku_sudoku_validator_spec.rb b/spec/lib/sudoku_sudoku_validator_spec.rb index 9d81f61..655d9cf 100644 --- a/spec/lib/sudoku_sudoku_validator_spec.rb +++ b/spec/lib/sudoku_sudoku_validator_spec.rb @@ -1,4 +1,5 @@ require 'sudoku_validator' +require 'sudoku_unit' module Sudoku describe Validator do @@ -31,5 +32,32 @@ module Sudoku it "responds to :errors" do expect(Validator.new(double())).to respond_to(:errors) end + + describe "errors" do + context "a valid puzzle" do + specify "contains no errors" do + puzzle = double(:valid? => true, :complete? => false) + allow(puzzle).to receive(:rows).and_return({}) + expect(Validator.new(puzzle).errors).to be_empty + end + end + + context "an invalid puzzle" do + context "with a row duplicate" do + it "contains a detailed error" do + puzzle = double(:valid? => false, :complete? => true, + :rows => { + 1 => Row[1, 8, 2, 3, 4, 5, 7, 7, 8], + 2 => Row[1, 2, 3, 4, 5, 6, 7, 8, 9], + 9 => Row[1, 8, 8, 3, 4, 5, 6, 7, 8] + }) + + errors = Validator.new(puzzle).errors + expect(errors).to include "Row 1 contains a duplicate 8 in squares 2 and 9" + expect(errors).to include "Row 9 contains a duplicate 8 in squares 2, 3 and 9" + end + end + end + end end end From 21c2e697d5e23fb20ee421e65ccaeb86e0a38bf6 Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Mon, 9 Dec 2013 18:21:43 +0000 Subject: [PATCH 15/24] Puzzle's :rows, :columns and :boxes return hashes indexed from 1, instead of an array. --- lib/sudoku_puzzle.rb | 22 +++++++++++----------- spec/lib/sudoku_puzzle_spec.rb | 12 ++++++------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/sudoku_puzzle.rb b/lib/sudoku_puzzle.rb index ad93640..fa750bc 100644 --- a/lib/sudoku_puzzle.rb +++ b/lib/sudoku_puzzle.rb @@ -9,25 +9,25 @@ def initialize(rows = []) end def rows - @grid.row_vectors.map do |row| - Row[*row] - end + Hash[@grid.row_vectors.map.with_index(1) do |row, index| + [index, Row[*row]] + end] end def columns - @grid.column_vectors.map do |column| - Column[*column] - end + Hash[@grid.column_vectors.map.with_index(1) do |column, index| + [index, Column[*column]] + end] end def boxes - [0, 3, 6].repeated_permutation(2).map do |x, y| - Box[*@grid.minor(x, 3, y, 3).to_a.flatten] - end + Hash[[0, 3, 6].repeated_permutation(2).map.with_index(1) do |(x, y), index| + [index, Box[*@grid.minor(x, 3, y, 3).to_a.flatten]] + end] end def complete? - rows.any? { |row| row.complete? } + rows.any? { |index, row| row.complete? } end def valid? @@ -52,7 +52,7 @@ def create_grid(input_rows) def all_units [:rows, :columns, :boxes].map do |message| - send(message) + send(message).map {|index, unit| unit} end.flatten end end diff --git a/spec/lib/sudoku_puzzle_spec.rb b/spec/lib/sudoku_puzzle_spec.rb index dfbaabc..d0ec353 100644 --- a/spec/lib/sudoku_puzzle_spec.rb +++ b/spec/lib/sudoku_puzzle_spec.rb @@ -14,12 +14,12 @@ module Sudoku end describe ":rows" do - it "returns an array of Row objects" do + it "returns a hash of Row objects indexed" do puzzle = Puzzle.new(input_rows) expect(puzzle.rows.count).to eq 9 - expect(puzzle.rows.first).to eq Row[0, 5, 9, 6, 1, 2, 4, 3, 0] - expect(puzzle.rows.last).to eq Row[0, 9, 8, 7, 3, 6, 2, 4, 0] + expect(puzzle.rows[1]).to eq Row[0, 5, 9, 6, 1, 2, 4, 3, 0] + expect(puzzle.rows[9]).to eq Row[0, 9, 8, 7, 3, 6, 2, 4, 0] end end @@ -28,8 +28,8 @@ module Sudoku puzzle = Puzzle.new(input_rows) expect(puzzle.columns.count).to eq 9 - expect(puzzle.columns.first).to eq Column[0, 7, 1, 9, 3, 2, 4, 6, 0] - expect(puzzle.columns.last).to eq Column[0, 9, 8, 2, 4, 6, 5, 3, 0] + expect(puzzle.columns[1]).to eq Column[0, 7, 1, 9, 3, 2, 4, 6, 0] + expect(puzzle.columns[9]).to eq Column[0, 9, 8, 2, 4, 6, 5, 3, 0] end end @@ -38,7 +38,7 @@ module Sudoku puzzle = Puzzle.new(input_rows) expect(puzzle.boxes.count).to eq 9 - expect(puzzle.boxes.first).to eq Box[0, 5, 9, 7, 0, 3, 1, 6, 0] + expect(puzzle.boxes[1]).to eq Box[0, 5, 9, 7, 0, 3, 1, 6, 0] end end From 47f941c9895bceffb8fca81f5a5f27b6e4d7f815 Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Tue, 10 Dec 2013 00:23:14 +0000 Subject: [PATCH 16/24] Complete feature with error listings and their positions. --- bin/sudoku-validator | 1 - features/sudoku_validator.feature | 6 ++-- lib/sudoku_unit.rb | 22 +++++++++--- lib/sudoku_validator.rb | 44 ++++++++++++++++-------- spec/lib/sudoku_sudoku_validator_spec.rb | 39 ++++++++++++--------- spec/lib/sudoku_unit_spec.rb | 12 ++++--- 6 files changed, 81 insertions(+), 43 deletions(-) diff --git a/bin/sudoku-validator b/bin/sudoku-validator index ff7b506..c8b65a9 100755 --- a/bin/sudoku-validator +++ b/bin/sudoku-validator @@ -17,7 +17,6 @@ validator = Sudoku::Validator.new(puzzle) puts validator.status if validator.errors puts "Errors:" - puts "" validator.errors.each.with_index(1) do |error, index| puts " #{index}) #{error}" end diff --git a/features/sudoku_validator.feature b/features/sudoku_validator.feature index 717c893..d78ac35 100644 --- a/features/sudoku_validator.feature +++ b/features/sudoku_validator.feature @@ -11,9 +11,11 @@ Feature: Validating .sudoku files Scenario: An invalid, complete sudoku When I successfully run `sudoku-validator ./invalid_complete.sudoku` Then the output should contain "This sudoku is invalid." - Then the output should contain "Column 4 contains a duplicate 8 in squares 2 and 5." - Then the output should contain "Column 6 contains a duplicate 2 in squares 1 and 5." + Then the output should contain "Column 4 contains a duplicate 8 in squares 2 and 5" + Then the output should contain "Column 6 contains a duplicate 2 in squares 1 and 5" Scenario: An invalid, incomplete sudoku When I successfully run `sudoku-validator ./invalid_incomplete.sudoku` Then the output should contain "This sudoku is invalid." + Then the output should contain "Column 2 contains a duplicate 5 in squares 1 and 7" + Then the output should contain "Column 5 contains a duplicate 8 in squares 2 and 7" diff --git a/lib/sudoku_unit.rb b/lib/sudoku_unit.rb index f236a74..5e6f0ea 100644 --- a/lib/sudoku_unit.rb +++ b/lib/sudoku_unit.rb @@ -30,9 +30,7 @@ def empty?(square) square === 0 end end -end -module Sudoku class BaseUnit include Unit @@ -55,7 +53,21 @@ def hash alias_method :==, :eql? end - class Row < BaseUnit; end - class Column < BaseUnit; end - class Box < BaseUnit; end + class Row < BaseUnit + def type + "Row" + end + end + + class Column < BaseUnit + def type + "Column" + end + end + + class Box < BaseUnit + def type + "Box" + end + end end diff --git a/lib/sudoku_validator.rb b/lib/sudoku_validator.rb index e387b0b..04327cb 100644 --- a/lib/sudoku_validator.rb +++ b/lib/sudoku_validator.rb @@ -18,26 +18,42 @@ def status def errors return [] if puzzle.valid? - puzzle.rows.map do |row_number, row| - x = row.positions.select { |value, position| position.is_a?(Array) } - x.empty? ? nil : x.map {|number, squares| DuplicateError.new(row_number, number, squares).to_s } - end.compact.flatten + + [:rows, :columns, :boxes].map do |message| + puzzle.send(message).map do |index, unit| + duplicates(unit).map do |number, positions| + DuplicateError.new(unit.type, index, number, positions) + end + end + end.flatten end - class DuplicateError < Struct.new(:row_number, :number, :squares) - def to_s - "#{unit_type} #{row_number} contains a duplicate #{number} in squares #{list(squares)}" + private + + def duplicates(unit) + unit.positions.select do |number, position| + non_empty?(number) && duplicate?(position) end + end - private + def non_empty?(number) + number != 0 + end - def unit_type - "Row" - end + def duplicate?(position) + position.is_a?(Array) + end + end - def list(squares) - "#{squares.first(squares.count-1).join(', ')} and #{squares.last}" - end + class DuplicateError < Struct.new(:type, :index, :number, :positions) + def to_s + "#{type} #{index} contains a duplicate #{number} in squares #{list(positions)}" + end + + private + + def list(positions) + "#{positions[0...-1].join(', ')} and #{positions.last}" end end end diff --git a/spec/lib/sudoku_sudoku_validator_spec.rb b/spec/lib/sudoku_sudoku_validator_spec.rb index 655d9cf..034aa6b 100644 --- a/spec/lib/sudoku_sudoku_validator_spec.rb +++ b/spec/lib/sudoku_sudoku_validator_spec.rb @@ -29,34 +29,39 @@ module Sudoku end end - it "responds to :errors" do - expect(Validator.new(double())).to respond_to(:errors) - end - describe "errors" do context "a valid puzzle" do specify "contains no errors" do puzzle = double(:valid? => true, :complete? => false) - allow(puzzle).to receive(:rows).and_return({}) expect(Validator.new(puzzle).errors).to be_empty end end context "an invalid puzzle" do - context "with a row duplicate" do - it "contains a detailed error" do - puzzle = double(:valid? => false, :complete? => true, - :rows => { - 1 => Row[1, 8, 2, 3, 4, 5, 7, 7, 8], - 2 => Row[1, 2, 3, 4, 5, 6, 7, 8, 9], - 9 => Row[1, 8, 8, 3, 4, 5, 6, 7, 8] - }) - - errors = Validator.new(puzzle).errors - expect(errors).to include "Row 1 contains a duplicate 8 in squares 2 and 9" - expect(errors).to include "Row 9 contains a duplicate 8 in squares 2, 3 and 9" + let(:puzzle) { double(:valid? => false, :complete? => false) } + let(:errors) { Validator.new(puzzle).errors.map(&:to_s) } + + before(:each) do + [:rows, :columns, :boxes].each do |message| + allow(puzzle).to receive(message).and_return({}) end end + + it "returns an error for a unit that contains a duplicate number" do + puzzle.stub(:rows).and_return({ 1 => Row[1, 8, 2, 3, 4, 5, 6, 7, 8] }) + expect(errors).to include "Row 1 contains a duplicate 8 in squares 2 and 9" + end + + it "ignores duplicate 0s as they are empty squares" do + puzzle.stub(:columns).and_return({ 4 => Column[0, 1, 0, 2, 0, 3, 0, 4, 0] }) + expect(errors).to be_empty + end + + it "returns errors for a unit that contains multiple duplicate numbers" do + puzzle.stub(:boxes).and_return({ 7 => Box[2, 3, 1, 6, 2, 7, 3, 3, 0] }) + expect(errors).to include "Box 7 contains a duplicate 2 in squares 1 and 5" + expect(errors).to include "Box 7 contains a duplicate 3 in squares 2, 7 and 8" + end end end end diff --git a/spec/lib/sudoku_unit_spec.rb b/spec/lib/sudoku_unit_spec.rb index cd77439..f530d27 100644 --- a/spec/lib/sudoku_unit_spec.rb +++ b/spec/lib/sudoku_unit_spec.rb @@ -1,6 +1,6 @@ require 'sudoku_unit' -shared_examples "a Unit" do +shared_examples "a Unit" do |type| context "completeness" do specify "a unit is incomplete if it contains empty squares" do expect(described_class[0, 5, 9, 6, 1, 2, 4, 3, 8]).not_to be_complete @@ -45,18 +45,22 @@ expect(described_class[7, 5, 9, 6, 1, 2, 4, 3, 8]).to eq described_class[7, 5, 9, 6, 1, 2, 4, 3, 8] end end + + it "has the right type" do + expect(described_class[1, 2, 3, 4, 5, 6, 7, 8, 9].type).to eq type + end end module Sudoku describe Row do - it_behaves_like "a Unit" + it_behaves_like "a Unit", "Row" end describe Column do - it_behaves_like "a Unit" + it_behaves_like "a Unit", "Column" end describe Box do - it_behaves_like "a Unit" + it_behaves_like "a Unit", "Box" end end From bfc9f7db3723a3fcfeedf1b9d04072d78643d329 Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Tue, 10 Dec 2013 00:44:24 +0000 Subject: [PATCH 17/24] Some small tinkerings and refactorings. Bit more checking around the command line tool. --- bin/sudoku-validator | 23 ++++++++++++----------- lib/sudoku_puzzle.rb | 2 +- lib/sudoku_unit.rb | 3 +-- spec/lib/sudoku_puzzle_spec.rb | 11 ++++++----- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/bin/sudoku-validator b/bin/sudoku-validator index c8b65a9..984736f 100755 --- a/bin/sudoku-validator +++ b/bin/sudoku-validator @@ -4,20 +4,21 @@ require_relative '../lib/sudoku_fileparser' require_relative '../lib/sudoku_puzzle' require_relative '../lib/sudoku_validator' -filename = ARGV[0] +exit unless filename = ARGV[0] filepath = File.expand_path(filename) if File.exist?(filepath) - file = File.read(filepath) -end - -puzzle = Sudoku::Puzzle.new(Sudoku::FileParser.new(file).parse_rows) -validator = Sudoku::Validator.new(puzzle) + parser = Sudoku::FileParser.new(File.read(filepath)) + puzzle = Sudoku::Puzzle.new(parser.parse_rows) + validator = Sudoku::Validator.new(puzzle) -puts validator.status -if validator.errors - puts "Errors:" - validator.errors.each.with_index(1) do |error, index| - puts " #{index}) #{error}" + puts validator.status + if validator.errors + puts "Errors:" + validator.errors.each.with_index(1) do |error, index| + puts " #{index}) #{error}" + end end +else + puts "Could not find file #{filename}" end diff --git a/lib/sudoku_puzzle.rb b/lib/sudoku_puzzle.rb index fa750bc..4fc58c4 100644 --- a/lib/sudoku_puzzle.rb +++ b/lib/sudoku_puzzle.rb @@ -52,7 +52,7 @@ def create_grid(input_rows) def all_units [:rows, :columns, :boxes].map do |message| - send(message).map {|index, unit| unit} + send(message).map { |index, unit| unit } end.flatten end end diff --git a/lib/sudoku_unit.rb b/lib/sudoku_unit.rb index 5e6f0ea..1bd24e2 100644 --- a/lib/sudoku_unit.rb +++ b/lib/sudoku_unit.rb @@ -2,8 +2,7 @@ module Sudoku module Unit def positions squares.each.with_index(1).inject ({}) do |h, (number, position)| - h[number] = h[number] ? [h[number]].flatten << position : position - h + h.tap { |h| h[number] = h[number] ? [h[number]].flatten << position : position } end end diff --git a/spec/lib/sudoku_puzzle_spec.rb b/spec/lib/sudoku_puzzle_spec.rb index d0ec353..baca4f3 100644 --- a/spec/lib/sudoku_puzzle_spec.rb +++ b/spec/lib/sudoku_puzzle_spec.rb @@ -24,21 +24,22 @@ module Sudoku end describe ":columns" do - it "returns an array of Column objects" do + it "returns an array of Column objects indexed" do puzzle = Puzzle.new(input_rows) expect(puzzle.columns.count).to eq 9 - expect(puzzle.columns[1]).to eq Column[0, 7, 1, 9, 3, 2, 4, 6, 0] - expect(puzzle.columns[9]).to eq Column[0, 9, 8, 2, 4, 6, 5, 3, 0] + expect(puzzle.columns[2]).to eq Column[5, 0, 6, 8, 7, 4, 3, 0, 9] + expect(puzzle.columns[8]).to eq Column[3, 0, 2, 5, 1, 8, 7, 0, 4] end end describe ":boxes" do - it "returns an array of Box objects" do + it "returns an array of Box objects indexed" do puzzle = Puzzle.new(input_rows) expect(puzzle.boxes.count).to eq 9 - expect(puzzle.boxes[1]).to eq Box[0, 5, 9, 7, 0, 3, 1, 6, 0] + expect(puzzle.boxes[3]).to eq Box[4, 3, 0, 1, 0, 9, 0, 2, 8] + expect(puzzle.boxes[7]).to eq Box[4, 3, 0, 6, 0, 7, 0, 9, 8] end end From 9b821e734bf70b587dca8394220abcb0e7f99c43 Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Thu, 12 Dec 2013 15:57:46 +0000 Subject: [PATCH 18/24] Fix truthy errors bug as suggested by JoelQ. --- bin/sudoku-validator | 2 +- features/sudoku_validator.feature | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/sudoku-validator b/bin/sudoku-validator index 984736f..e884ffe 100755 --- a/bin/sudoku-validator +++ b/bin/sudoku-validator @@ -13,7 +13,7 @@ if File.exist?(filepath) validator = Sudoku::Validator.new(puzzle) puts validator.status - if validator.errors + if validator.errors.any? puts "Errors:" validator.errors.each.with_index(1) do |error, index| puts " #{index}) #{error}" diff --git a/features/sudoku_validator.feature b/features/sudoku_validator.feature index d78ac35..59496b1 100644 --- a/features/sudoku_validator.feature +++ b/features/sudoku_validator.feature @@ -3,10 +3,12 @@ Feature: Validating .sudoku files Scenario: A valid, complete sudoku When I successfully run `sudoku-validator ./valid_complete.sudoku` Then the output should contain "This sudoku is valid." + Then the output should not contain "Errors:" Scenario: A valid, incomplete sudoku When I successfully run `sudoku-validator ./valid_incomplete.sudoku` Then the output should contain "This sudoku is valid, but incomplete." + Then the output should not contain "Errors:" Scenario: An invalid, complete sudoku When I successfully run `sudoku-validator ./invalid_complete.sudoku` From 3ac7e3c8e9c6c117265bce8935c29e209b23b0aa Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Wed, 18 Dec 2013 12:12:17 +0000 Subject: [PATCH 19/24] Remove :file accessor from FileParser. --- lib/sudoku_fileparser.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/sudoku_fileparser.rb b/lib/sudoku_fileparser.rb index 82fb471..238ccc1 100644 --- a/lib/sudoku_fileparser.rb +++ b/lib/sudoku_fileparser.rb @@ -1,14 +1,12 @@ module Sudoku class FileParser - attr_reader :file - def initialize(file = nil) raise(ArgumentError, "A file is required.") if file.nil? @file = file end def parse_rows - file.strip.each_line.map do |line| + @file.strip.each_line.map do |line| parse_line(line) unless separator?(line) end.compact end From 68e512692271d32c0e585f321f930bfc0d0ccac9 Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Sun, 22 Dec 2013 10:02:04 +0000 Subject: [PATCH 20/24] Refactor FileParser to use nice :split method with regex as suggested by joelq --- lib/sudoku_fileparser.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sudoku_fileparser.rb b/lib/sudoku_fileparser.rb index 238ccc1..9322abc 100644 --- a/lib/sudoku_fileparser.rb +++ b/lib/sudoku_fileparser.rb @@ -14,7 +14,7 @@ def parse_rows private def parse_line(line) - parse_empties(line).delete("\n|").split(" ").map(&:to_i) + parse_empties(line).split(/[^\d]+/).map(&:to_i) end def parse_empties(line) From e17fd1f41e7dc69059c52a0b7f1a1e50009c04ce Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Sun, 22 Dec 2013 10:07:42 +0000 Subject: [PATCH 21/24] =?UTF-8?q?Small=20refactor=20symbol=20to=20proc=20?= =?UTF-8?q?=E2=80=93=20lovely=20stuff.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/sudoku_puzzle.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/sudoku_puzzle.rb b/lib/sudoku_puzzle.rb index 4fc58c4..7969f0c 100644 --- a/lib/sudoku_puzzle.rb +++ b/lib/sudoku_puzzle.rb @@ -27,11 +27,11 @@ def boxes end def complete? - rows.any? { |index, row| row.complete? } + rows.all? { |index, row| row.complete? } end def valid? - all_units.all? { |unit| unit.valid? } + all_units.all?(&:valid?) end def eql?(other) From 013686d1353c7e9b37c8b5c589033904e28333bd Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Sun, 22 Dec 2013 10:10:22 +0000 Subject: [PATCH 22/24] Great refactoring from joelq, simplifying Puzzle's :all_units method. --- lib/sudoku_puzzle.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/sudoku_puzzle.rb b/lib/sudoku_puzzle.rb index 7969f0c..795f14a 100644 --- a/lib/sudoku_puzzle.rb +++ b/lib/sudoku_puzzle.rb @@ -51,9 +51,7 @@ def create_grid(input_rows) end def all_units - [:rows, :columns, :boxes].map do |message| - send(message).map { |index, unit| unit } - end.flatten + rows.values + columns.values + boxes.values end end end From 604ac80d10c4ef24d23e678e4247694f9164c687 Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Sun, 22 Dec 2013 10:19:16 +0000 Subject: [PATCH 23/24] Couple of small refactorings in Unit from joelq --- lib/sudoku_unit.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/sudoku_unit.rb b/lib/sudoku_unit.rb index 1bd24e2..8b445a4 100644 --- a/lib/sudoku_unit.rb +++ b/lib/sudoku_unit.rb @@ -11,7 +11,7 @@ def valid? end def complete? - squares.none? { |square| empty?(square) } + squares.none?(&:zero?) end private @@ -22,11 +22,7 @@ def squares end def non_empty_squares - squares.reject { |square| empty?(square) } - end - - def empty?(square) - square === 0 + squares.reject(&:zero?) end end From 60440157299580c870248c104496646b6489942d Mon Sep 17 00:00:00 2001 From: Dan Scotton Date: Sun, 22 Dec 2013 10:44:39 +0000 Subject: [PATCH 24/24] Simplify Unit by removing module and BaseUnit class and just keeping Unit. Good suggestion from joelq. --- lib/sudoku_unit.rb | 51 +++++++++++++++++++--------------------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/lib/sudoku_unit.rb b/lib/sudoku_unit.rb index 8b445a4..07e5ebf 100644 --- a/lib/sudoku_unit.rb +++ b/lib/sudoku_unit.rb @@ -1,7 +1,15 @@ module Sudoku - module Unit + class Unit + def self.[](*values) + new(values) + end + + def initialize(values) + @squares = values + end + def positions - squares.each.with_index(1).inject ({}) do |h, (number, position)| + @squares.each.with_index(1).inject ({}) do |h, (number, position)| h.tap { |h| h[number] = h[number] ? [h[number]].flatten << position : position } end end @@ -11,30 +19,7 @@ def valid? end def complete? - squares.none?(&:zero?) - end - - private - - def squares - raise("Please set @squares in the object that implements this interface.") unless defined?(@squares) - @squares - end - - def non_empty_squares - squares.reject(&:zero?) - end - end - - class BaseUnit - include Unit - - def self.[](*values) - new(values) - end - - def initialize(values) - @squares = values + @squares.none?(&:zero?) end def eql?(other) @@ -42,25 +27,31 @@ def eql?(other) end def hash - squares.hash + @squares.hash end alias_method :==, :eql? + + private + + def non_empty_squares + @squares.reject(&:zero?) + end end - class Row < BaseUnit + class Row < Unit def type "Row" end end - class Column < BaseUnit + class Column < Unit def type "Column" end end - class Box < BaseUnit + class Box < Unit def type "Box" end