commit ea695a3055fac6a90612823d7624f80df96a5fd3 Author: Ray Miller Date: Tue Aug 13 16:56:53 2019 +0100 Initial check-in diff --git a/grid.go b/grid.go new file mode 100644 index 0000000..14d23ef --- /dev/null +++ b/grid.go @@ -0,0 +1,120 @@ +package main + +import ( + "fmt" + "strings" +) + +type Grid [81]int + +func (g Grid) ElementAt(row, col int) int { + return g[col+row*9] +} + +func (g Grid) WithElementAt(row, col int, val int) Grid { + g[col+row*9] = val + return g +} + +func (g Grid) String() string { + var sb strings.Builder + for r := 0; r < 9; r++ { + fmt.Fprintf(&sb, "%d %d %d | %d %d %d | %d %d %d\n", + g.ElementAt(r, 0), g.ElementAt(r, 1), g.ElementAt(r, 2), + g.ElementAt(r, 3), g.ElementAt(r, 4), g.ElementAt(r, 5), + g.ElementAt(r, 6), g.ElementAt(r, 7), g.ElementAt(r, 8)) + if r == 2 || r == 5 { + sb.WriteString("------+-------+------\n") + } + } + return sb.String() +} + +func (g Grid) Neighbours(row, col int) IntSet { + neighbours := IntSet{} + + // Neighbouring row + for c := 0; c < 9; c++ { + if v := g.ElementAt(row, c); v != 0 { + neighbours.Insert(v) + } + } + + // Neighbouring col + for r := 0; r < 9; r++ { + if v := g.ElementAt(r, col); v != 0 { + neighbours.Insert(v) + } + } + + // Top-left corner of the 3x3 subgrid + tlRow := (row / 3) * 3 + tlCol := (col / 3) * 3 + + // Neighbouring subgrid + for r := tlRow; r < tlRow+3; r++ { + for c := tlCol; c < tlCol+3; c++ { + if v := g.ElementAt(r, c); v != 0 { + neighbours.Insert(v) + } + } + } + + return neighbours +} + +func (g Grid) FirstEmptyCell() int { + for i, v := range g { + if v == 0 { + return i + } + } + return -1 +} + +func (g Grid) Candidates(row, col int) []int { + neighbours := g.Neighbours(row, col) + var candidates []int + for _, v := range []int{1, 2, 3, 4, 5, 6, 7, 8, 9} { + if !neighbours.Contains(v) { + candidates = append(candidates, v) + } + } + return candidates +} + +func (g Grid) Solve() (Grid, error) { + i := g.FirstEmptyCell() + if i == -1 { + // No unfilled cells: we have found a solution! + return g, nil + } + row := i / 9 + col := i % 9 + candidates := g.Candidates(row, col) + // Try each of the candidates in turn + for _, v := range candidates { + result, err := g.WithElementAt(row, col, v).Solve() + if err == nil { + return result, nil + } + } + // There were no valid candidates + return g, fmt.Errorf("No solutions found") +} + +func ParseGrid(data []byte) (Grid, error) { + G := Grid{} + i := 0 + for _, x := range data { + x := int(x) - int('0') + if x >= 0 && x <= 9 { + G[i] = x + i++ + } + } + if i < len(G) { + return G, fmt.Errorf("Invalid input: found %d elements, expected %d", i, len(G)) + } + return G, nil +} diff --git a/grid_test.go b/grid_test.go new file mode 100644 index 0000000..c5dc5ba --- /dev/null +++ b/grid_test.go @@ -0,0 +1,190 @@ +package main + +import ( + "fmt" + "math/rand" + "testing" +) + +func TestElementAt(t *testing.T) { + G := Grid{} // an empty grid + for row := 0; row < 9; row++ { + for col := 0; col < 9; col++ { + v := G.ElementAt(row, col) + if v != 0 { + t.Errorf("ElementAt(%d, %d) = %d, expected 0", row, col, v) + } + } + } +} + +func TestElementAtPanic(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("ElementAt did not panic when out of bounds") + } + }() + G := Grid{} + G.ElementAt(10, 10) +} + +func TestWithElementAt(t *testing.T) { + G := Grid{} + for i := 0; i < 10; i++ { + row := rand.Intn(9) + col := rand.Intn(9) + val := rand.Intn(10) + G = G.WithElementAt(row, col, val) + if G.ElementAt(row, col) != val { + t.Errorf("WithElementAt(%d, %d, %d) did not return expected value", row, col, val) + } + } +} + +func TestParseGrid(t *testing.T) { + data := []byte(`010020706 +700913040 +380004001 +000007010 +500109003 +090500000 +200300094 +040762005 +105090070`) + G, err := ParseGrid(data) + if err != nil { + t.Errorf("Unexpected error parsing grid: %v", err) + } + // Check that row 2 is as expected + row := 2 + expected := []int{3, 8, 0, 0, 0, 4, 0, 0, 1} + for col, want := range expected { + got := G.ElementAt(row, col) + if got != want { + t.Errorf("ElementAt(%d,%d) = %d, expected %d", row, col, got, want) + } + } + fmt.Println(G) +} + +func TestParseGridError(t *testing.T) { + data := []byte(`01002070 +700913040 +380004001 +000007010 +500109003 +090500000 +200300094 +040762005 +105090070`) + _, err := ParseGrid(data) + if err == nil { + t.Errorf("ParseGrid() did not return an expected error") + } +} + +var tests = []struct { + data []byte + grid Grid + firstEmptyCell int + neighbours []int + candidates []int +}{ + { + []byte("010020706700913040380004001000007010500109003090500000200300094040762005105090070"), + Grid{}, + 0, + []int{1, 2, 3, 5, 6, 7, 8}, + []int{4, 9}, + }, + { + []byte("000000800785090006040700002068300051400000007570009320900002060800060719006000000"), + Grid{}, + 0, + []int{4, 5, 7, 8, 9}, + []int{1, 2, 3, 6}, + }, + { + []byte("320000809400600027005000040000401000002000900000502000080000100640003008107000032"), + Grid{}, + 2, + []int{2, 3, 4, 5, 7, 8, 9}, + []int{1, 6}, + }, + { + []byte("000008000040100390000900561006094000003000200000320800372001000095003020000200000"), + Grid{}, + 0, + []int{3, 4, 8}, + []int{1, 2, 5, 6, 7, 9}, + }, +} + +func init() { + for i := 0; i < len(tests); i++ { + grid, err := ParseGrid(tests[i].data) + if err != nil { + panic(err) + } + tests[i].grid = grid + } +} + +func TestFirstEmptyCell(t *testing.T) { + for i, x := range tests { + got := tests[i].grid.FirstEmptyCell() + if got != x.firstEmptyCell { + t.Errorf("FirstEmptyCell(grid[%d]) = %d, expected %d", i, got, x.firstEmptyCell) + } + } +} + +func TestNeighbours(t *testing.T) { + for i, x := range tests { + r := x.firstEmptyCell / 9 + c := x.firstEmptyCell % 9 + got := x.grid.Neighbours(r, c) + if len(got) != len(x.neighbours) { + t.Errorf("len(Neighbours(grid[%d]) = %d, expected %d", i, len(got), len(x.neighbours)) + } + for _, v := range x.neighbours { + if !got.Contains(v) { + t.Errorf("Neighbours(grid[%d]) did not contain %d", i, v) + } + } + } +} + +func TestCandidates(t *testing.T) { + for i, x := range tests { + r := x.firstEmptyCell / 9 + c := x.firstEmptyCell % 9 + got := x.grid.Candidates(r, c) + if len(got) != len(x.candidates) { + t.Errorf("len(Candidates(grid[%d]) = %d, expected %d", i, len(got), len(x.candidates)) + } + for _, v := range x.candidates { + found := false + for _, w := range got { + if v == w { + found = true + break + } + } + if !found { + t.Errorf("Candidates(grid[%d]) did not contain %d", i, v) + } + } + } +} + +func TestSolve(t *testing.T) { + for i, x := range tests { + res, err := x.grid.Solve() + if err != nil { + t.Errorf("Could not solve grid %d", i) + } else { + fmt.Println(res) + } + } +} diff --git a/intset.go b/intset.go new file mode 100644 index 0000000..232b6c8 --- /dev/null +++ b/intset.go @@ -0,0 +1,21 @@ +package main + +type IntSet []int + +// Contains returns true if x is in the set, otherwise false +func (s *IntSet) Contains(x int) bool { + for _, v := range *s { + if v == x { + return true + } + } + return false +} + +// Insert adds x to the set if it is not already present +func (s *IntSet) Insert(x int) { + if s.Contains(x) { + return + } + *s = append(*s, x) +} diff --git a/intset_test.go b/intset_test.go new file mode 100644 index 0000000..dae575c --- /dev/null +++ b/intset_test.go @@ -0,0 +1,41 @@ +package main + +import "testing" + +func TestContains(t *testing.T) { + s := IntSet{} + if s.Contains(0) { + t.Errorf("The empty set should not contain 0") + } + xs := []int{1, 2, 3} + s = IntSet(xs) + for _, x := range xs { + if !s.Contains(x) { + t.Errorf("Expected set to contain %d", x) + } + } + if s.Contains(0) { + t.Errorf("The set {1,2,3} should not contain 0") + } +} + +func TestInsert(t *testing.T) { + s := IntSet{} + xs := []int{1, 2, 3} + for _, x := range xs { + if s.Contains(x) { + t.Errorf("%d has not yet been added to the set", x) + } + s.Insert(x) + if !s.Contains(x) { + t.Errorf("Expected the set to contain %d", x) + } + } + if len(s) != len(xs) { + t.Errorf("The set should contain %d elements", len(xs)) + } + s.Insert(1) + if len(s) != len(xs) { + t.Errorf("Inserting an element twice should not affect size of the set") + } +} diff --git a/sudoku.go b/sudoku.go new file mode 100644 index 0000000..d45499c --- /dev/null +++ b/sudoku.go @@ -0,0 +1,31 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" +) + +func main() { + if len(os.Args) != 2 { + fmt.Fprintln(os.Stderr, "Usage: sudoku FILENAME") + os.Exit(2) + } + filename := os.Args[1] + data, err := ioutil.ReadFile(filename) + if err != nil { + fmt.Fprintf(os.Stderr, "Error reading puzzle file: %v\n", err) + os.Exit(3) + } + grid, err := ParseGrid(data) + if err != nil { + fmt.Fprintf(os.Stderr, "Error parsing grid: %v\n", err) + os.Exit(3) + } + solution, err := grid.Solve() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(3) + } + fmt.Println(solution) +}