Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Doding dojo JS edition - Sudoku solver Kata. Artur Wodarz solution #1

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ Return of the program should be a string as well but with all numbers filled out
## Running tests

npm test


2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"scripts": {
"test": "mocha *.test.js"
"test": "mocha test/*.test.js"
},
"dependencies": {
"chai": "^4.2.0",
Expand Down
5 changes: 0 additions & 5 deletions solver.js

This file was deleted.

13 changes: 0 additions & 13 deletions solver.test.js

This file was deleted.

68 changes: 68 additions & 0 deletions src/helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
function inRange(low, high, num) {
return num >= low && num <= high ? true : false;
}

function createMatrix(rows) {
return Array.from(Array(rows), row => []);
}

function extractBlock(x, y, matrix, blockSize = 3) {
if (!inRange(0, matrix.length - 1, x) || y < 0) throw new RangeError();
const blockStart = { row: x - (x % blockSize), column: y - (y % blockSize) };
let block = [];
for (let i = blockStart.row; i < blockStart.row + blockSize; i++) {
if (y >= matrix[i].length) throw new RangeError();
for (let j = blockStart.column; j < blockStart.column + blockSize; j++) {
block.push(matrix[i][j]);
}
}
return block;
}

function extractRow(rowIndex, matrix) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You missed unit tests for this function.

let row = [];
if (!inRange(0, matrix.length - 1, rowIndex)) throw new RangeError();
for (let i = 0; i < matrix.length; i++) {
row.push(matrix[rowIndex][i]);
}
return row;
}

function extractColumn(columnIndex, matrix) {
let column = [];
if (!inRange(0, matrix.length - 1, columnIndex)) throw new RangeError();
for (let i = 0; i < matrix.length; i++) {
column.push(matrix[i][columnIndex]);
}
return column;
}

function getEmptyCells(matrix) {
let emptyCells = [];
matrix.forEach((arr, rowIndex) => {
arr.forEach((value, colIndex) => {
if (value === null) emptyCells.push({row: rowIndex, col: colIndex});
});
});
return emptyCells;
}


function findUniqueInt(min, max, array){
const values = new Set(array);
for (let i = min; i <= max; i++){
if (!values.has(i)){
return i;
}
}
return null;
}
module.exports = {
createMatrix,
inRange,
extractBlock,
extractRow,
extractColumn,
getEmptyCells,
findUniqueInt
};
39 changes: 39 additions & 0 deletions src/solver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use strict";

const parser = require("./sudokuParser");
const helper = require("./helper");

function getRelatedCells(cell, sudokuMatrix) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When reading body of solve function my initial though when reached line with getRelatedCells was to look into helper module. I was wrong 😄 Maybe you should move it there.

return new Array()
.concat(helper.extractRow(cell.row, sudokuMatrix))
.concat(helper.extractColumn(cell.col, sudokuMatrix))
.concat(helper.extractBlock(cell.row, cell.col, sudokuMatrix, 3));
}

function solve(sudokuString) {
let sudokuMatrix = parser.parseToArray(sudokuString);
const modifiableCells = helper.getEmptyCells(sudokuMatrix);
let nowModified = 0;

while (helper.inRange(0, modifiableCells.length - 1, nowModified)) {
let x = modifiableCells[nowModified].row;
let y = modifiableCells[nowModified].col;
if (sudokuMatrix[x][y] == null) sudokuMatrix[x][y] = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you only set the cell to 0 so helper.findUniqueInt can work. You should move this if into helper function.
Separate functions should not be aware of such details.

let newUnique = helper.findUniqueInt(
sudokuMatrix[x][y],
9,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a magic number. It also can be retrieved from parsed matrix dimension.

getRelatedCells(modifiableCells[nowModified], sudokuMatrix)
);

if (newUnique == null) {
sudokuMatrix[x][y] = null;
nowModified--;
} else {
sudokuMatrix[x][y] = newUnique;
nowModified++;
}
}
if (nowModified < 0) return "Unsolvable sudoku";
else return sudokuMatrix.join().replace(/,/g, "");
}
exports.solve = solve;
17 changes: 17 additions & 0 deletions src/sudokuParser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"use strict";

const helper = require("./helper");

function parseToArray(sudokuAsString) {
let sudokuArray = helper.createMatrix(9);
sudokuAsString.split("").forEach((el, index) => {
sudokuArray[Math.floor(index / 9)][index % 9] = el != "_" ? parseInt(el) : null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Math.floor(index / 9)[index %9]is clever 😃
but it also is not tested. Since this is the smartest part of the whole function I'd move this calculation into a separate function that returns a tuple.

function arrayPosition(index: number): [number, number];

});
return sudokuArray;
}



module.exports = {
parseToArray
};
119 changes: 119 additions & 0 deletions test/helper.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
'use strict';

const chai = require('chai');
require('mocha');
const helper = require('../src/helper');

describe('Create matrix', () => {
it('Should return array', () => chai.assert.isArray(helper.createMatrix()));
it('Should return array contains provided number of elements', () => {
chai.assert.equal(helper.createMatrix(5).length, 5);
});
});

describe('Block extraction', () => {
const n = null;
const sudokuMatrix = [
[n, n, n, 2, 6, n, 7, n, 1],
[6, 8, n, n, 7, n, n, 9, n],
[1, 9, n, n, n, 4, 5, n, n],
[8, 2, n, 1, n, n, n, 4, n],
[n, n, 4, 6, n, 2, 9, n, n],
[n, 5, n, n, n, 3, n, 2, 8],
[n, n, 9, 3, n, n, n, 7, 4],
[n, 4, n, n, 5, n, n, 3, 6],
[7, n, 3, n, 1, 8, n, n, n]
];
const blocks = {
'0/0': [n, n, n, 6, 8, n, 1, 9, n],
'0/2': [n, n, n, 6, 8, n, 1, 9, n],
'3/5': [1, n, n, 6, n, 2, n, n, 3],
'7/8': [n, 7, 4, n, 3, 6, n, n, n]
}

it('Should return 9 elements array', () => {
chai.assert.isArray(helper.extractBlock(1, 2, sudokuMatrix));
chai.assert.equal(helper.extractBlock(1, 2, sudokuMatrix).length, 9);
});
it('Should return array of all elements in block', () => {
chai.assert.deepEqual(helper.extractBlock(0, 0, sudokuMatrix), blocks['0/0']);
chai.assert.deepEqual(helper.extractBlock(0, 2, sudokuMatrix), blocks['0/2']);
chai.assert.deepEqual(helper.extractBlock(4, 5, sudokuMatrix), blocks['3/5']);
chai.assert.deepEqual(helper.extractBlock(7, 8, sudokuMatrix), blocks['7/8']);
})
it('Should throw RangeError if provided coords are out of matrix range', () => {
chai.expect(() => helper.extractBlock(-1, -1, sudokuMatrix)).to.throw(RangeError);
chai.expect(() => helper.extractBlock(9, 5, sudokuMatrix)).to.throw(RangeError);
})
});

describe('Column extraction', () => {
const n = null;
const sudokuMatrix = [
[n, n, n, 2, 6, n, 7, n, 1],
[6, 8, n, n, 7, n, n, 9, n],
[1, 9, n, n, n, 4, 5, n, n],
[8, 2, n, 1, n, n, n, 4, n],
[n, n, 4, 6, n, 2, 9, n, n],
[n, 5, n, n, n, 3, n, 2, 8],
[n, n, 9, 3, n, n, n, 7, 4],
[n, 4, n, n, 5, n, n, 3, 6],
[7, n, 3, n, 1, 8, n, n, n]
];

const columns = {
0: [n, 6, 1, 8, n, n, n, n, 7],
4: [6, 7, n, n, n, n, n, 5, 1],
8: [1, n, n, n, n, 8, 4, 6, n]
};
it('Should return 9 elements array', () => {
chai.assert.isArray(helper.extractColumn(0, sudokuMatrix));
chai.assert.equal(helper.extractColumn(0, sudokuMatrix).length, 9);
});
it('Should return array of all elements in Column', () => {
chai.assert.deepEqual(helper.extractColumn(0, sudokuMatrix), columns[0]);
chai.assert.deepEqual(helper.extractColumn(4, sudokuMatrix), columns[4]);
chai.assert.deepEqual(helper.extractColumn(8, sudokuMatrix), columns[8]);
})
it('Should throw RangeError if provided coords are out of matrix range', () => {
chai.expect(() => helper.extractColumn(-1, sudokuMatrix)).to.throw(RangeError);
chai.expect(() => helper.extractColumn(9, sudokuMatrix)).to.throw(RangeError);
})
});

describe('Finding nulls in matrix', () => {
const n = null;
const sudokuMatrix = [
[n, n, n, 2, 6, n, 7, n, 1],
[6, 8, n, n, 7, n, n, 9, n],
[1, 9, n, n, n, 4, 5, n, n],
[8, 2, n, 1, n, n, n, 4, n],
[n, n, 4, 6, n, 2, 9, n, n],
[n, 5, n, n, n, 3, n, 2, 8],
[n, n, 9, 3, n, n, n, 7, 4],
[n, 4, n, n, 5, n, n, 3, 6],
[7, n, 3, n, 1, 8, n, n, n]
];

it('Should return 35 elements array', () => {
chai.assert.isArray(helper.getEmptyCells(sudokuMatrix));
chai.assert.equal(helper.getEmptyCells(sudokuMatrix).length, 45);
});
it('Every element should contains row and col keys', () => {
helper.getEmptyCells(sudokuMatrix).forEach(el => chai.expect(el).to.have.all.keys('row', 'col'));
});
});

describe('Finding first integer within given range that does not exists in providen array', () => {
it ('Should return first unique number within prividen range', () => {
const arrWithUniques = [2, 3, 4, 8, 9.2 ,10];
chai.assert.equal(helper.findUniqueInt(1, 11, arrWithUniques), 1, 'Testing min as ununique number');
chai.assert.equal(helper.findUniqueInt(8, 11, arrWithUniques), 9, 'Function should ignore float numbers');
});
it ('Should return null if could not find any unique number', () => {
const arrWithoutUniques = [1, 2, 3, 4, 5];
chai.assert.equal(helper.findUniqueInt(1, 5, arrWithoutUniques), null)
});
});


18 changes: 18 additions & 0 deletions test/solver.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use strict";

const chai = require("chai");
require("mocha");
const solver = require('../src/solver');

describe("Sudoku one", function() {
it("should be solved", function() {
var unsolved_sudoku = "___26_7_168__7__9_19___45__82_1___4___46_29___5___3_28__93___74_4__5__367_3_18___";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a small inconsistency. Using linter and formatter may help you with these.

var solved = "435269781682571493197834562826195347374682915951743628519326874248957136763418259";
chai.assert.equal(solver.solve(unsolved_sudoku), solved);
});
it("should return unsolvabe sudoku", function() {
const unsolved_sudoku = "___21_7_168__7__9_19___45__82_1___4___46_29___5___3_28__93___74_4__5__367_3_18___";
const solved = "Unsolvable sudoku";
chai.assert.equal(solver.solve(unsolved_sudoku), solved);
});
});
25 changes: 25 additions & 0 deletions test/sudokuParser.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use strict";

const chai = require("chai");
require("mocha");
const parser = require("../src/sudokuParser");

describe("Sudoku string parser", () => {
const n = null;
const unparsedSudoku = "___26_7_168__7__9_19___45__82_1___4___46_29___5___3_28__93___74_4__5__367_3_18___";
const parsed = [
[n, n, n, 2, 6, n, 7, n, 1],
[6, 8, n, n, 7, n, n, 9, n],
[1, 9, n, n, n, 4, 5, n, n],
[8, 2, n, 1, n, n, n, 4, n],
[n, n, 4, 6, n, 2, 9, n, n],
[n, 5, n, n, n, 3, n, 2, 8],
[n, n, 9, 3, n, n, n, 7, 4],
[n, 4, n, n, 5, n, n, 3, 6],
[7, n, 3, n, 1, 8, n, n, n]
];

it("Should return matrix from provided string", () => {
chai.assert.deepEqual(parser.parseToArray(unparsedSudoku), parsed);
});
});