diff --git a/src/sudoku.ts b/src/sudoku.ts index bc11d45..068fcf5 100644 --- a/src/sudoku.ts +++ b/src/sudoku.ts @@ -58,18 +58,28 @@ export class Sudoku { const s = new Sudoku(size); for (const [rowIndex, row] of cells.entries()) { + // Even though it would also throw in s.getCell + // this throws a more useful error + inRangeIncl(0, size - 1, rowIndex); + if (!row) { continue; } for (const [colIndex, content] of row.entries()) { + inRangeIncl(0, size - 1, colIndex); + // prettier-ignore const cellIndex = (rowIndex * size) + colIndex; const cell = s.getCell(cellIndex); if (isReadonlyArray(content)) { + for (const candidate of content) { + inRangeIncl(0, size - 1, candidate); + } + cell.candidates = new Set(content); } else if (content !== undefined) { - s.setContent(cellIndex, content); + s.setContent(cell, content); } } } @@ -104,6 +114,10 @@ export class Sudoku { throw new TypeError('Expected size to be a square of an integer.'); } + if (size <= 0) { + throw new TypeError(`Expected size (${size}) to be greater than 0.`); + } + this.blockWidth = blockWidth; const amountCells = size ** 2; @@ -116,14 +130,16 @@ export class Sudoku { if (typeof content === 'number') { if (Number.isInteger(content)) { + inRangeIncl(0, this.size - 1, content); + cell.setContent(content); } else { - cell.clear(); + throw new TypeError(`content was not an integer: ${content}`); } } else { const index = Sudoku.alphabet.indexOf(content.toUpperCase()); if (index === -1) { - cell.clear(); + throw new Error(`content was not in alphabet: "${content}"`); } else { cell.setContent(index); } diff --git a/test/plugins/naked-pairs.test.ts b/test/plugins/naked-pairs.test.ts index 87f699c..be6308a 100644 --- a/test/plugins/naked-pairs.test.ts +++ b/test/plugins/naked-pairs.test.ts @@ -24,7 +24,7 @@ test('nakedPairs should not change an empty sudoku.', t => { test('nakedPairs should correctly find the pairs of "1" and "6".', t => { const s = Sudoku.fromPrefilled( [ - [4, [1, 6], [1, 6], [7, 8], 3, 2, [1, 7, 8], 9, 5], + [4, [1, 6], [1, 6], [7, 8], 3, 2, [1, 7, 8], 0, 5], // ^ ^ , ^ ^ ^ Remove this ], 9, @@ -40,10 +40,10 @@ test('nakedPairs should not change anything upon finding ("1", "2", "5") across const candidates = [ [1, 2, 5], // #1 [3, 6, 7, 8], - [1, 4, 6, 9], + [1, 4, 6, 0], [1, 2, 5], // #2 [1, 2, 5, 6], - [4, 5, 9], + [4, 5, 0], [1, 5, 7, 8], [3, 5, 7], [1, 4, 8], @@ -69,13 +69,13 @@ test('nakedPairs should find an incomplete naked pair', t => { [ [1, 2, 4], // #1 [1, 2, 4], // #2 - [1, 2, 3, 4, 5, 6, 7, 8, 9], - [1, 2, 3, 4, 5, 6, 7, 8, 9], + [1, 2, 3, 4, 5, 6, 7, 8, 0], + [1, 2, 3, 4, 5, 6, 7, 8, 0], [1, 4], // #3, missing 2, though - [1, 2, 3, 4, 5, 6, 7, 8, 9], - [1, 2, 3, 4, 5, 6, 7, 8, 9], - [1, 2, 3, 4, 5, 6, 7, 8, 9], - [1, 2, 3, 4, 5, 6, 7, 8, 9], + [1, 2, 3, 4, 5, 6, 7, 8, 0], + [1, 2, 3, 4, 5, 6, 7, 8, 0], + [1, 2, 3, 4, 5, 6, 7, 8, 0], + [1, 2, 3, 4, 5, 6, 7, 8, 0], ], ], 9, @@ -88,13 +88,13 @@ test('nakedPairs should find an incomplete naked pair', t => { [ [1, 2, 4], // #1 [1, 2, 4], // #2 - [3, 5, 6, 7, 8, 9], - [3, 5, 6, 7, 8, 9], + [3, 5, 6, 7, 8, 0], + [3, 5, 6, 7, 8, 0], [1, 4], // #3 - [3, 5, 6, 7, 8, 9], - [3, 5, 6, 7, 8, 9], - [3, 5, 6, 7, 8, 9], - [3, 5, 6, 7, 8, 9], + [3, 5, 6, 7, 8, 0], + [3, 5, 6, 7, 8, 0], + [3, 5, 6, 7, 8, 0], + [3, 5, 6, 7, 8, 0], ], ); }); @@ -106,14 +106,14 @@ test('nakedPairs should find an incomplete naked pair with incomplete cell as fi [ [ [1, 4], // #1, missing 2, though - [1, 2, 3, 4, 5, 6, 7, 8, 9], + [1, 2, 3, 4, 5, 6, 7, 8, 0], [1, 2, 4], // #2 - [1, 2, 3, 4, 5, 6, 7, 8, 9], - [1, 2, 3, 4, 5, 6, 7, 8, 9], + [1, 2, 3, 4, 5, 6, 7, 8, 0], + [1, 2, 3, 4, 5, 6, 7, 8, 0], [1, 2, 4], // #3 - [1, 2, 3, 4, 5, 6, 7, 8, 9], - [1, 2, 3, 4, 5, 6, 7, 8, 9], - [1, 2, 3, 4, 5, 6, 7, 8, 9], + [1, 2, 3, 4, 5, 6, 7, 8, 0], + [1, 2, 3, 4, 5, 6, 7, 8, 0], + [1, 2, 3, 4, 5, 6, 7, 8, 0], ], ], 9, @@ -125,14 +125,14 @@ test('nakedPairs should find an incomplete naked pair with incomplete cell as fi s.getRow(0).map(({candidates}) => [...candidates]), [ [1, 4], // #1 - [3, 5, 6, 7, 8, 9], + [3, 5, 6, 7, 8, 0], [1, 2, 4], // #2 - [3, 5, 6, 7, 8, 9], - [3, 5, 6, 7, 8, 9], + [3, 5, 6, 7, 8, 0], + [3, 5, 6, 7, 8, 0], [1, 2, 4], // #3 - [3, 5, 6, 7, 8, 9], - [3, 5, 6, 7, 8, 9], - [3, 5, 6, 7, 8, 9], + [3, 5, 6, 7, 8, 0], + [3, 5, 6, 7, 8, 0], + [3, 5, 6, 7, 8, 0], ], ); }); @@ -144,14 +144,14 @@ test('nakedPairs should find an incomplete naked pair with multiple incomplete c [ [ [1, 4], // #1, missing 2 - [1, 2, 3, 4, 5, 6, 7, 8, 9], + [1, 2, 3, 4, 5, 6, 7, 8, 0], [1, 2], // #2, missing 4 - [1, 2, 3, 4, 5, 6, 7, 8, 9], - [1, 2, 3, 4, 5, 6, 7, 8, 9], + [1, 2, 3, 4, 5, 6, 7, 8, 0], + [1, 2, 3, 4, 5, 6, 7, 8, 0], [2, 4], // #3, missing 1 - [1, 2, 3, 4, 5, 6, 7, 8, 9], - [1, 2, 3, 4, 5, 6, 7, 8, 9], - [1, 2, 3, 4, 5, 6, 7, 8, 9], + [1, 2, 3, 4, 5, 6, 7, 8, 0], + [1, 2, 3, 4, 5, 6, 7, 8, 0], + [1, 2, 3, 4, 5, 6, 7, 8, 0], ], ], 9, @@ -163,14 +163,14 @@ test('nakedPairs should find an incomplete naked pair with multiple incomplete c s.getRow(0).map(({candidates}) => [...candidates]), [ [1, 4], // #1 - [3, 5, 6, 7, 8, 9], + [3, 5, 6, 7, 8, 0], [1, 2], // #2 - [3, 5, 6, 7, 8, 9], - [3, 5, 6, 7, 8, 9], + [3, 5, 6, 7, 8, 0], + [3, 5, 6, 7, 8, 0], [2, 4], // #3 - [3, 5, 6, 7, 8, 9], - [3, 5, 6, 7, 8, 9], - [3, 5, 6, 7, 8, 9], + [3, 5, 6, 7, 8, 0], + [3, 5, 6, 7, 8, 0], + [3, 5, 6, 7, 8, 0], ], ); }); @@ -182,14 +182,14 @@ test('nakedPairs with incomplete cells that do not overlap much', t => { [ [ [1, 4], // #1, missing 2, 3; doesn't overlap with #2 - [1, 2, 3, 4, 5, 6, 7, 8, 9], + [1, 2, 3, 4, 5, 6, 7, 8, 0], [2, 3], // #2, missing 1, 4 [1, 3], // #3, missing 2, 4 - [1, 2, 3, 4, 5, 6, 7, 8, 9], + [1, 2, 3, 4, 5, 6, 7, 8, 0], [2, 4], // #4, missing 1, 3 - [1, 2, 3, 4, 5, 6, 7, 8, 9], - [1, 2, 3, 4, 5, 6, 7, 8, 9], - [1, 2, 3, 4, 5, 6, 7, 8, 9], + [1, 2, 3, 4, 5, 6, 7, 8, 0], + [1, 2, 3, 4, 5, 6, 7, 8, 0], + [1, 2, 3, 4, 5, 6, 7, 8, 0], ], ], 9, @@ -200,14 +200,14 @@ test('nakedPairs with incomplete cells that do not overlap much', t => { s.getRow(0).map(({candidates}) => [...candidates]), [ [1, 4], // #1 - [5, 6, 7, 8, 9], + [5, 6, 7, 8, 0], [2, 3], // #2 [1, 3], // #3 - [5, 6, 7, 8, 9], + [5, 6, 7, 8, 0], [2, 4], // #4 - [5, 6, 7, 8, 9], - [5, 6, 7, 8, 9], - [5, 6, 7, 8, 9], + [5, 6, 7, 8, 0], + [5, 6, 7, 8, 0], + [5, 6, 7, 8, 0], ], ); }); diff --git a/test/plugins/pointing-arrows.test.ts b/test/plugins/pointing-arrows.test.ts index 6e3e56b..3682786 100644 --- a/test/plugins/pointing-arrows.test.ts +++ b/test/plugins/pointing-arrows.test.ts @@ -19,7 +19,7 @@ test('pointingArrows should not change an empty sudoku.', t => { test('pointingArrows should find a pointing arrow of 3s.', t => { const s = Sudoku.fromPrefilled( [ - [[2, 4, 5, 8], 1, 7, 9, [2, 4, 5], 3, 6, [4, 8], [2, 4, 8]], + [[2, 4, 5, 8], 1, 7, 0, [2, 4, 5], 3, 6, [4, 8], [2, 4, 8]], [ [2, 3, 4, 5, 6], // Remove "3" from here [2, 3, 4, 5], // And here @@ -27,11 +27,11 @@ test('pointingArrows should find a pointing arrow of 3s.', t => { [1, 2, 5, 7], 8, [5, 7], - [1, 3, 9], // Pointing arrow here - [1, 4, 9], - [1, 2, 3, 4, 9], // And here + [1, 3, 0], // Pointing arrow here + [1, 4, 0], + [1, 2, 3, 4, 0], // And here ], - [9, [2, 3, 4, 8], [3, 6, 8], [1, 2], [2, 4, 6], [4, 6], 5, [1, 4, 8], 7], + [0, [2, 3, 4, 8], [3, 6, 8], [1, 2], [2, 4, 6], [4, 6], 5, [1, 4, 8], 7], ], 9, ); @@ -53,27 +53,27 @@ test('pointingArrows should find a pointing arrow of 2s.', t => { [ [ 7, - [2, 3, 4, 8, 9], // Remove "2" from here + [2, 3, 4, 8, 0], // Remove "2" from here 1, [2, 8], // Pointing arrow here - [2, 4, 9], // And here - [4, 8, 9], - [3, 8, 9], + [2, 4, 0], // And here + [4, 8, 0], + [3, 8, 0], 6, 5, ], [ [2, 4, 6, 8], - [2, 4, 8, 9], - [6, 8, 9], + [2, 4, 8, 0], + [6, 8, 0], [5, 7], 3, [5, 7, 8], - [1, 8, 9], - [1, 4, 8, 9], - [1, 4, 8, 9], + [1, 8, 0], + [1, 4, 8, 0], + [1, 4, 8, 0], ], - [[3, 4, 8], [3, 4, 8, 9], 5, 6, [4, 9], 1, 7, 2, [3, 4, 8, 9]], + [[3, 4, 8], [3, 4, 8, 0], 5, 6, [4, 0], 1, 7, 2, [3, 4, 8, 0]], ], 9, ); @@ -81,5 +81,5 @@ test('pointingArrows should find a pointing arrow of 2s.', t => { pointingArrows(s); t.true(s.anyChanged); - t.deepEqual([...s.getCell(1).candidates], [3, 4, 8, 9]); + t.deepEqual([...s.getCell(1).candidates], [3, 4, 8, 0]); }); diff --git a/test/sudoku.test.ts b/test/sudoku.test.ts index ef3ebc9..57f3df9 100644 --- a/test/sudoku.test.ts +++ b/test/sudoku.test.ts @@ -10,11 +10,31 @@ test('Sudoku should be a class', t => { t.is(typeof new Sudoku(9), 'object'); }); -test('Sudoku#setContent', t => { +test('Sudoku#setContent valid content', t => { const s = new Sudoku(9); s.setContent(0, '4'); t.is(s.getContent(0), '4'); + + s.setContent(0, 4); // because '1' is 0, '2' is 1 ... + t.is(s.getContent(0), '5'); +}); + +test('Sudoku#setContent invalid content', t => { + const s = new Sudoku(9); + t.throws( + () => { + s.setContent(0, '.'); + }, + {message: /not in alphabet: "\."$/}, + ); + + t.throws( + () => { + s.setContent(0, 9); + }, + {message: /9/}, + ); }); test('Sudoku#getContent', t => { @@ -24,10 +44,6 @@ test('Sudoku#getContent', t => { s.setContent(8 * 9 + 8, '4'); t.is(s.getContent(8 * 9 + 8), '4'); - s.setContent(8 * 9 + 8, '.'); - t.is(s.getContent(8 * 9 + 8), undefined); - t.is(s.getCell(8 * 9 + 8).candidates.size, 16); - s.setContent(0, 'A'); t.is(s.getContent(0), 'A'); t.is(s.getCell(0).content, 10); @@ -442,7 +458,7 @@ test('Sudoku#cellsIndividuallyValidByStructure', t => { // ==== s = new Sudoku(9); - s.setContent(2, 'Hello there'); + s.getCell(2).setContent(9); t.is(s.getContent(2), undefined); t.true( s.cellsIndividuallyValidByStructure(), @@ -622,6 +638,64 @@ test('Sudoku#clone 16x16', t => { t.not(cloned, s); }); +test('Sudoku.fromPrefilled valid sudoku', t => { + const s = Sudoku.fromPrefilled( + [ + [0, 1, 2, 3], + [0, 1, 2, 3], + [0, 1, 2, 3], + [0, 1, 2, 3], + ], + 4, + ); + + for (let i = 0; i < s.size; ++i) { + const row = s.getRow(i); + t.deepEqual( + row.map(cell => cell.content), + [0, 1, 2, 3], + ); + } +}); + +test('Sudoku.fromPrefilled too many cols', t => { + t.throws( + () => { + Sudoku.fromPrefilled( + [ + [0, 1, 2, 3, 4], + [0, 1, 2, 3, 4], + [0, 1, 2, 3, 4], + ], + 4, + ); + }, + { + message: /4/, // 4 is index, not number + }, + ); +}); + +test('Sudoku.fromPrefilled too many rows', t => { + t.throws( + () => { + Sudoku.fromPrefilled( + [ + [0, 1, 2, 3], + [0, 1, 2, 3], + [0, 1, 2, 3], + [0, 1, 2, 3], + [0, 1, 2, 3], + ], + 4, + ); + }, + { + message: /4/, + }, + ); +}); + test('inRangeIncl', t => { t.throws( () => {