diff --git a/7-quality-assurance/4-sudoku-solver/controllers/puzzle-strings.js b/7-quality-assurance/4-sudoku-solver/controllers/puzzle-strings.js index 9d5e49c..f48aa3d 100644 --- a/7-quality-assurance/4-sudoku-solver/controllers/puzzle-strings.js +++ b/7-quality-assurance/4-sudoku-solver/controllers/puzzle-strings.js @@ -29,8 +29,8 @@ export const invalidStrings = [ ]; export const unsolvableStrings = [ - "115..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.420..8..1..16....926969.37.", - "4206942069.......................................................................", + "115..2.84..63.12.7.2..5.....9..1....8.2.3674.3.7.2..9.421..8..1..16....926969.37.", + "4216942169.......................................................................", "111111111111111111111111111111111111111111111111111111111111111111111111111111111", ]; diff --git a/7-quality-assurance/4-sudoku-solver/controllers/sudoku-solver.js b/7-quality-assurance/4-sudoku-solver/controllers/sudoku-solver.js index b1fc386..9e2e3fc 100644 --- a/7-quality-assurance/4-sudoku-solver/controllers/sudoku-solver.js +++ b/7-quality-assurance/4-sudoku-solver/controllers/sudoku-solver.js @@ -1,47 +1,127 @@ -const grid = [ - [1, 2, 3, 4, 5, 6, 7, 8, 9], - [10, 11, 12, 13, 14, 15, 16, 17, 18], - [19, 20, 21, 22, 23, 24, 25, 26, 27], - [28, 29, 30, 31, 32, 33, 34, 35, 36], - [37, 38, 39, 40, 41, 42, 43, 44, 45], - [46, 47, 48, 49, 50, 51, 52, 53, 54], - [55, 56, 57, 58, 59, 60, 61, 62, 63], - [64, 65, 66, 67, 68, 69, 70, 71, 72], - [73, 74, 75, 76, 77, 78, 79, 80, 81], -]; - +let currentPuzzle = ""; class SudokuSolver { getPuzzleArray(puzzleString) { - const puzzleArray = []; - for (let i = 1; i <= 81; i++) { - let value = puzzleString[i - 1]; + const puzzleArray = [[], [], [], [], [], [], [], [], []]; + for (let i = 0; i <= 80; i++) { + let value = puzzleString[i]; value === "." || !value - ? (puzzleArray[Math.ceil(i / 9)][(i % 9) + 1] = " ") - : (puzzleArray[Math.ceil(i / 9)][(i % 9) + 1] = value); + ? (puzzleArray[Math.floor(i / 9)][i % 9] = " ") + : (puzzleArray[Math.floor(i / 9)][i % 9] = value); } + return puzzleArray; + } + + getRow(puzzle, row) { + return puzzle[row]; + } + + getColumn(puzzle, row, column) { + let col = []; + for (let i in puzzle) { + col.push(puzzle[i][column]); + } + return col; + } + + getRegion(puzzle, row, column) { + let region = []; + for (let r = 0; r < 3; r++) { + for (let c = 0; c < 3; c++) { + region.push( + puzzle[Math.floor(row / 3) * 3 + r][Math.floor(column / 3) * 3 + c] + ); + } + } + return region; } validate(puzzleString) { - if (puzzleString.length != 81) { + if (puzzleString.length !== 81) { return { - status: "error", error: "Expected puzzle to be 81 characters long", }; - } else if (!/^[1-9\.]+$/.test(puzzleString)) { - return { status: "error", error: "Invalid characters in puzzle" }; + } else if (!/^[1-9.]+$/.test(puzzleString)) { + return { error: "Invalid characters in puzzle" }; } else { + for (let i in puzzleString) { + let val = puzzleString[i]; + let row = Math.floor(i / 9); + let column = i % 9; + if (val !== ".") { + let temp = puzzleString.split(""); + temp[i] = "."; + if ( + this.checkRowPlacement(temp, row, column, val) && + this.checkColPlacement(temp, row, column, val) && + this.checkRegionPlacement(temp, row, column, val) + ) { + temp[i] = val; + puzzleString = temp.join(""); + } else { + return { error: "Puzzle cannot be solved" }; + } + } + } return true; } } - checkRowPlacement(puzzleString, row, column, value) {} + checkRowPlacement(puzzleString, row, column, value) { + let puzzle = this.getPuzzleArray(puzzleString); + let puzzleRow = this.getRow(puzzle, row, column); + return !( + puzzleRow.includes(value.toString()) && + puzzleRow[column] !== value.toString() + ); + } - checkColPlacement(puzzleString, row, column, value) {} + checkColPlacement(puzzleString, row, column, value) { + let puzzle = this.getPuzzleArray(puzzleString); + let puzzleCol = this.getColumn(puzzle, row, column); + return !( + puzzleCol.includes(value.toString()) && + puzzleCol[row] !== value.toString() + ); + } - checkRegionPlacement(puzzleString, row, column, value) {} + checkRegionPlacement(puzzleString, row, column, value) { + let puzzle = this.getPuzzleArray(puzzleString); + let puzzleRegion = this.getRegion(puzzle, row, column); + return !( + puzzleRegion.includes(value.toString()) && + puzzle[row][column] !== value.toString() + ); + } solve(puzzleString) { - return { error: "test" }; + if (puzzleString) currentPuzzle = puzzleString; + let valid = this.validate(currentPuzzle); + if (valid !== true) { + return valid; + } + let empty = currentPuzzle.indexOf("."); + let row = Math.floor(empty / 9); + let column = empty % 9; + if (empty !== -1) { + for (let i = 1; i <= 9; i++) { + if ( + this.checkRowPlacement(currentPuzzle, row, column, i) && + this.checkColPlacement(currentPuzzle, row, column, i) && + this.checkRegionPlacement(currentPuzzle, row, column, i) + ) { + let temp = currentPuzzle.split(""); + temp[empty] = i; + currentPuzzle = temp.join(""); + this.solve(); + } + } + } + if (currentPuzzle.indexOf(".") !== -1) { + let temp = currentPuzzle.split(""); + temp[empty] = "."; + currentPuzzle = temp.join(""); + } + return { solution: currentPuzzle }; } } diff --git a/7-quality-assurance/4-sudoku-solver/routes/api.js b/7-quality-assurance/4-sudoku-solver/routes/api.js index 9cece27..d662131 100644 --- a/7-quality-assurance/4-sudoku-solver/routes/api.js +++ b/7-quality-assurance/4-sudoku-solver/routes/api.js @@ -1,18 +1,47 @@ -'use strict'; +"use strict"; -const SudokuSolver = require('../controllers/sudoku-solver.js'); +const SudokuSolver = require("../controllers/sudoku-solver.js"); module.exports = function (app) { - let solver = new SudokuSolver(); - app.route('/api/check') - .post((req, res) => { + app.route("/api/check").post((req, res) => { + if (!req.body.puzzle || !req.body.coordinate || !req.body.value) { + return res.json({ error: "Required field(s) missing" }); + } + if (!(req.body.value > 0) || !(req.body.value < 10)) { + return res.json({ error: "Invalid value" }); + } + if (!/^[A-I][1-9]$/.test(req.body.coordinate)) { + return res.json({ error: "Invalid coordinate" }); + } + let validString = solver.validate(req.body.puzzle); + if (validString !== true) { + return res.json(validString); + } + let row = req.body.coordinate[0].charCodeAt(0) - 65; + let col = req.body.coordinate[1] - 1; + let conflict = []; + let valid; + if (!solver.checkRowPlacement(req.body.puzzle, row, col, req.body.value)) { + conflict.push("row"); + } + if (!solver.checkColPlacement(req.body.puzzle, row, col, req.body.value)) { + conflict.push("column"); + } + if ( + !solver.checkRegionPlacement(req.body.puzzle, row, col, req.body.value) + ) { + conflict.push("region"); + } + conflict.length > 0 ? (valid = false) : (valid = true); + res.json({ valid: valid, conflict: conflict }); + }); - }); - - app.route('/api/solve') - .post((req, res) => { - - }); + app.route("/api/solve").post((req, res) => { + if (!req.body.puzzle) { + return res.json({ error: "Required field missing" }); + } + res.json(solver.solve(req.body.puzzle)); + }); }; diff --git a/7-quality-assurance/4-sudoku-solver/tests/1_unit-tests.js b/7-quality-assurance/4-sudoku-solver/tests/1_unit-tests.js index 3aab681..be8c596 100644 --- a/7-quality-assurance/4-sudoku-solver/tests/1_unit-tests.js +++ b/7-quality-assurance/4-sudoku-solver/tests/1_unit-tests.js @@ -3,7 +3,6 @@ const assert = chai.assert; import { puzzlesAndSolutions, invalidStrings, - unsolvableStrings, wrongLengthStrings, } from "../controllers/puzzle-strings"; @@ -36,11 +35,11 @@ suite("Unit Tests", () => { }); }); suite("Placement Checks", () => { - test("Login handles a valid row placement", () => { + test("Logic handles a valid row placement", () => { const checks = [ - solver.checkRowPlacement(puzzlesAndSolutions[0][0], "A", 2, 3), - solver.checkRowPlacement(puzzlesAndSolutions[0][1], "A", 2, 6), - solver.checkRowPlacement(puzzlesAndSolutions[0][2], "A", 1, 2), + solver.checkRowPlacement(puzzlesAndSolutions[0][0], 0, 3, 3), + solver.checkRowPlacement(puzzlesAndSolutions[1][0], 0, 3, 6), + solver.checkRowPlacement(puzzlesAndSolutions[2][0], 0, 2, 2), ]; for (let i in checks) { assert.isTrue(checks[i]); @@ -48,9 +47,9 @@ suite("Unit Tests", () => { }); test("Logic handles an invalid row placement", () => { const checks = [ - solver.checkRowPlacement(puzzlesAndSolutions[0][0], "A", 2, 8), - solver.checkRowPlacement(puzzlesAndSolutions[0][1], "A", 2, 1), - solver.checkRowPlacement(puzzlesAndSolutions[0][2], "A", 1, 3), + solver.checkRowPlacement(puzzlesAndSolutions[0][0], 0, 3, 5), + solver.checkRowPlacement(puzzlesAndSolutions[1][0], 0, 3, 1), + solver.checkRowPlacement(puzzlesAndSolutions[2][0], 0, 2, 3), ]; for (let i in checks) { assert.isFalse(checks[i]); @@ -58,9 +57,9 @@ suite("Unit Tests", () => { }); test("Login handles a valid column placement", () => { const checks = [ - solver.checkColPlacement(puzzlesAndSolutions[0][0], "A", 2, 3), - solver.checkColPlacement(puzzlesAndSolutions[0][1], "A", 2, 6), - solver.checkColPlacement(puzzlesAndSolutions[0][2], "A", 1, 2), + solver.checkColPlacement(puzzlesAndSolutions[0][0], 0, 1, 3), + solver.checkColPlacement(puzzlesAndSolutions[1][0], 0, 1, 6), + solver.checkColPlacement(puzzlesAndSolutions[2][0], 0, 0, 2), ]; for (let i in checks) { assert.isTrue(checks[i]); @@ -68,9 +67,9 @@ suite("Unit Tests", () => { }); test("Logic handles an invalid column placement", () => { const checks = [ - solver.checkColPlacement(puzzlesAndSolutions[0][0], "B", 2, 9), - solver.checkColPlacement(puzzlesAndSolutions[0][1], "B", 2, 8), - solver.checkColPlacement(puzzlesAndSolutions[0][2], "C", 3, 2), + solver.checkColPlacement(puzzlesAndSolutions[0][0], 1, 1, 9), + solver.checkColPlacement(puzzlesAndSolutions[1][0], 1, 1, 8), + solver.checkColPlacement(puzzlesAndSolutions[2][0], 2, 2, 2), ]; for (let i in checks) { assert.isFalse(checks[i]); @@ -78,9 +77,9 @@ suite("Unit Tests", () => { }); test("Logic handles a valid region (3x3 grid) placement", () => { const checks = [ - solver.checkRegionPlacement(puzzlesAndSolutions[0][0], "A", 2, 3), - solver.checkRegionPlacement(puzzlesAndSolutions[0][1], "A", 2, 6), - solver.checkRegionPlacement(puzzlesAndSolutions[0][2], "A", 1, 2), + solver.checkRegionPlacement(puzzlesAndSolutions[0][0], 0, 1, 3), + solver.checkRegionPlacement(puzzlesAndSolutions[1][0], 0, 1, 6), + solver.checkRegionPlacement(puzzlesAndSolutions[2][0], 0, 0, 2), ]; for (let i in checks) { assert.isTrue(checks[i]); @@ -88,9 +87,9 @@ suite("Unit Tests", () => { }); test("Logic handles an invalid region (3x3 grid) placement", () => { const checks = [ - solver.checkRegionPlacement(puzzlesAndSolutions[0][0], "C", 3, 5), - solver.checkRegionPlacement(puzzlesAndSolutions[0][1], "C", 3, 5), - solver.checkRegionPlacement(puzzlesAndSolutions[0][2], "C", 3, 5), + solver.checkRegionPlacement(puzzlesAndSolutions[0][0], 2, 2, 5), + solver.checkRegionPlacement(puzzlesAndSolutions[1][0], 2, 2, 5), + solver.checkRegionPlacement(puzzlesAndSolutions[2][0], 2, 2, 5), ]; for (let i in checks) { assert.isFalse(checks[i]); @@ -101,7 +100,7 @@ suite("Unit Tests", () => { test("Valid puzzle strings pass the solver", () => { for (let i in puzzlesAndSolutions) { assert.equal( - solver.solve(puzzlesAndSolutions[i][1]), + solver.solve(puzzlesAndSolutions[i][0]).solution, puzzlesAndSolutions[i][1] ); } @@ -109,8 +108,8 @@ suite("Unit Tests", () => { test("Invalid puzzle strings fail the solver", () => { for (let i in invalidStrings) { assert.equal( - solver.solve(invalidStrings[i]), - { error: "Invalid characters in puzzle" }, + solver.solve(invalidStrings[i]).error, + "Invalid characters in puzzle", "invalid characters" ); } @@ -118,9 +117,8 @@ suite("Unit Tests", () => { test("Solver returns the expected solution for an incomplete puzzle", () => { for (let i in puzzlesAndSolutions) { assert.equal( - solver.solve(puzzlesAndSolutions[i][0]), - puzzlesAndSolutions[i][1], - puzzlesAndSolutions[i] + solver.solve(puzzlesAndSolutions[i][0]).solution, + puzzlesAndSolutions[i][1] ); } }); diff --git a/7-quality-assurance/4-sudoku-solver/tests/2_functional-tests.js b/7-quality-assurance/4-sudoku-solver/tests/2_functional-tests.js index f63e914..8ab1665 100644 --- a/7-quality-assurance/4-sudoku-solver/tests/2_functional-tests.js +++ b/7-quality-assurance/4-sudoku-solver/tests/2_functional-tests.js @@ -22,22 +22,20 @@ suite("Functional Tests", () => { .end((err, res) => { assert.equal(res.status, 200); assert.equal(res.body.solution, puzzlesAndSolutions[i][1]); - done(); }); } + done(); }); test("Solve a puzzle with missing puzzle string: POST request to /api/solve", (done) => { - for (let i in puzzlesAndSolutions) { - chai - .request(server) - .post("/api/solve") - .send({}) - .end((err, res) => { - assert.equal(res.status, 200); - assert.equal(res.body.error, "Required field missing"); - done(); - }); - } + chai + .request(server) + .post("/api/solve") + .send({}) + .end((err, res) => { + assert.equal(res.status, 200); + assert.equal(res.body.error, "Required field missing"); + done(); + }); }); test("Solve a puzzle with invalid characters: POST request to /api/solve", (done) => { for (let i in invalidStrings) { @@ -48,9 +46,9 @@ suite("Functional Tests", () => { .end((err, res) => { assert.equal(res.status, 200); assert.equal(res.body.error, "Invalid characters in puzzle"); - done(); }); } + done(); }); test("Solve a puzzle with incorrect length: POST request to /api/solve", (done) => { for (let i in wrongLengthStrings) { @@ -62,11 +60,11 @@ suite("Functional Tests", () => { assert.equal(res.status, 200); assert.equal( res.body.error, - "Expected puzzle to be 81 charcters long" + "Expected puzzle to be 81 characters long" ); - done(); }); } + done(); }); test("Solve a puzzle that cannot be solved: POST request to /api/solve", (done) => { for (let i in unsolvableStrings) { @@ -77,11 +75,12 @@ suite("Functional Tests", () => { .end((err, res) => { assert.equal(res.status, 200); assert.equal(res.body.error, "Puzzle cannot be solved"); - done(); }); } + done(); }); }); + suite("Placement Checks", () => { test("Check a puzzle placement with all fields: POST request to /api/check", (done) => { chai