227 lines
6.7 KiB
Python
227 lines
6.7 KiB
Python
#!/usr/bin/python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
:mod:`sudoku_grid` module
|
|
:author: `Éric W`
|
|
:date: 2016, october
|
|
|
|
Module for internal representation of Sudoku grids.
|
|
|
|
:Provides:
|
|
|
|
* a class SudokuGrid
|
|
|
|
:Examples: with the sudoku grid
|
|
|
|
.. code-block:: text
|
|
|
|
+-------+-------+-------+
|
|
| 9 . 6 | . 2 8 | . . 3 |
|
|
| . 4 . | 5 . . | . . 1 |
|
|
| . 8 . | 9 . . | . 4 . |
|
|
+-------+-------+-------+
|
|
| 6 . . | . . . | . 7 . |
|
|
| . . 8 | 2 . 6 | 9 . . |
|
|
| . 3 . | . . . | . . 5 |
|
|
+-------+-------+-------+
|
|
| . 5 . | . . 3 | . 6 . |
|
|
| 1 . . | . . 2 | . 8 . |
|
|
| 2 . . | 8 7 . | 5 . . |
|
|
+-------+-------+-------+
|
|
|
|
|
|
>>> strgrid = '906028003040500001080900040600000070008206900030000005050003060100002080200870500'
|
|
>>> grid = SudokuGrid(strgrid)
|
|
>>> str(grid) == strgrid
|
|
True
|
|
>>> grid[4, 6]
|
|
'9'
|
|
>>> grid.get_row(2)
|
|
'080900040'
|
|
>>> grid.get_col(1)
|
|
'048003500'
|
|
>>> grid.get_square(4, 7)
|
|
'070900005'
|
|
>>> grid[1, 5]
|
|
'0'
|
|
>>> grid.is_empty_cell(1, 5)
|
|
True
|
|
>>> grid.is_empty_cell(4, 6)
|
|
False
|
|
>>> grid[1, 5] = '7'
|
|
>>> grid.pretty_print()
|
|
+-------+-------+-------+
|
|
| 9 . 6 | . 2 8 | . . 3 |
|
|
| . 4 . | 5 . 7 | . . 1 |
|
|
| . 8 . | 9 . . | . 4 . |
|
|
+-------+-------+-------+
|
|
| 6 . . | . . . | . 7 . |
|
|
| . . 8 | 2 . 6 | 9 . . |
|
|
| . 3 . | . . . | . . 5 |
|
|
+-------+-------+-------+
|
|
| . 5 . | . . 3 | . 6 . |
|
|
| 1 . . | . . 2 | . 8 . |
|
|
| 2 . . | 8 7 . | 5 . . |
|
|
+-------+-------+-------+
|
|
"""
|
|
|
|
_SYMBOLS_SET = set('0123456789')
|
|
|
|
EMPTY_SYMBOL = '0'
|
|
|
|
_LINE_SEP = ('+' + '-' * 7) * 3 + '+'
|
|
_LINE_TO_FILL = '| {} {} {} ' * 3 + '|'
|
|
|
|
|
|
class SudokuGridError(Exception):
|
|
"""
|
|
Exception for bad description of sudokus grids
|
|
"""
|
|
def __init__(self, msg):
|
|
self.message = msg
|
|
|
|
class SudokuGrid():
|
|
|
|
def _is_valid(block):
|
|
"""
|
|
predicate for block validation
|
|
"""
|
|
return all(block.count(c) <= 1 for c in _SYMBOLS_SET if c != EMPTY_SYMBOL)
|
|
|
|
def __init__(self, grid):
|
|
"""
|
|
:param grid: a string of 81 characters in ['0'-'9'] describing a sudoku.
|
|
'0' for an empty cell of the grid.
|
|
:type grid: str
|
|
:UC: grid must be of length 81 and contains only characters in ['0'-'9'].
|
|
"""
|
|
if not type(grid) in (str, tuple, list):
|
|
raise SudokuGridError('grid must be a str or tuple or list')
|
|
if len(grid) != 81:
|
|
raise SudokuGridError('grid must be of length 81')
|
|
if not set(grid).issubset(_SYMBOLS_SET):
|
|
raise SudokuGridError("grid must contain only characters in ['0'-'9']")
|
|
if not all(SudokuGrid._is_valid(grid[9*r:9*(r+1)]) for r in range(9)):
|
|
raise SudokuGridError('a row is not valid')
|
|
if not all(SudokuGrid._is_valid(''.join(grid[c + 9*r] for c in range(9))) for r in range(9)):
|
|
raise SudokuGridError('a col is not valid')
|
|
if not all(SudokuGrid._is_valid(''.join(grid[9*(3*blockrow + r) + 3*blockcol + c]
|
|
for r in range(3) for c in range(3)))
|
|
for blockrow in range(3) for blockcol in range(3)):
|
|
raise SudokuGridError('a square block is not valid')
|
|
self._grid = list(c for c in grid)
|
|
|
|
|
|
def __str__(self):
|
|
"""
|
|
:return: a string representation of self
|
|
"""
|
|
return ''.join(c for c in self._grid)
|
|
|
|
def __getitem__(self, coord):
|
|
"""
|
|
:return: value of cell of coordinate coord=(row, col)
|
|
:rtype: str
|
|
:UC: coord must be a tuple of two valid coordinates
|
|
"""
|
|
try:
|
|
row, col = coord
|
|
assert 0 <= row < 9
|
|
assert 0 <= col < 9
|
|
return self._grid[9 * row + col]
|
|
except:
|
|
raise SudokuGridError('coord must be a tuple of two valid coordinates')
|
|
|
|
def __setitem__(self, coord, value):
|
|
"""
|
|
:return: value of cell of coordinate coord=(row, col)
|
|
:rtype: str
|
|
:UC: coord must be a tuple of two valid coordinates
|
|
"""
|
|
if not value in _SYMBOLS_SET:
|
|
raise SudokuGridError("new value must be in ['0'..'9']")
|
|
try:
|
|
row, col = coord
|
|
assert 0 <= row < 9
|
|
assert 0 <= col < 9
|
|
old_value = self._grid[9 * row + col]
|
|
self._grid[9 * row + col] = value
|
|
except:
|
|
raise SudokuGridError('coord must be a tuple of two valid coordinates')
|
|
if not (SudokuGrid._is_valid(self.get_row(row)) and
|
|
SudokuGrid._is_valid(self.get_col(col)) and
|
|
SudokuGrid._is_valid(self.get_square(row, col))):
|
|
self._grid[9 * row + col] = old_value
|
|
raise SudokuGridError("value violate the grid's validity")
|
|
|
|
def get_row(self, row):
|
|
"""
|
|
:param row: row number to select
|
|
:type row: int
|
|
:return: value of the row `row` of `grid`
|
|
:rtype: str
|
|
:UC: 0 <= row < 9
|
|
"""
|
|
if not (0 <= row < 9):
|
|
raise SudokuGridError('row must be in [0, 9[')
|
|
return ''.join(self[row, col] for col in range(9))
|
|
|
|
def get_col(self, col):
|
|
"""
|
|
:param col: col number to select
|
|
:type col: int
|
|
:return: value of the col `col` of `grid`
|
|
:rtype: str
|
|
:UC: 0 <= col < 9
|
|
"""
|
|
if not (0 <= col < 9):
|
|
raise SudokuGridError('col must be in [0, 9[')
|
|
return "".join([self[row, col] for row in range(9)])
|
|
|
|
def get_square(self, row, col):
|
|
"""
|
|
:param row: row number of the cell in the square_block to select
|
|
:type row: int
|
|
:param col: col number of the cell in the block to select
|
|
:type col: int
|
|
:return: value of block in containing the cell (`row`, `col`)
|
|
:rtype: str
|
|
:UC: 0 <= row < 9, 0<= col < 9
|
|
"""
|
|
if not (0 <= row < 9):
|
|
raise SudokuGridError('row must be in [0, 9[')
|
|
if not (0 <= col < 9):
|
|
raise SudokuGridError('col must be in [0, 9[')
|
|
firstrow = 3 * (row // 3)
|
|
firstcol = 3 * (col // 3)
|
|
return "".join(self[firstrow + i, firstcol + j]
|
|
for i in range(3) for j in range(3))
|
|
|
|
def is_empty_cell(grid, row, col):
|
|
"""
|
|
:param row: row number of the cell to test
|
|
:type row: int
|
|
:param col: col number of the cell to test
|
|
:type col: int
|
|
:return: True if cell (`row`, `col`) is empty, False otherwise
|
|
:rtype: bool
|
|
:UC: 0 <= row < 9, 0<= col < 9
|
|
"""
|
|
return grid[row, col] == EMPTY_SYMBOL
|
|
|
|
def pretty_print(self):
|
|
for row in range(9):
|
|
if row % 3 == 0: print(_LINE_SEP)
|
|
print(_LINE_TO_FILL.format(*self.get_row(row).replace(EMPTY_SYMBOL, '.')))
|
|
print(_LINE_SEP)
|
|
|
|
def copy(self):
|
|
return SudokuGrid(str(self))
|
|
|
|
if __name__ == '__main__':
|
|
import doctest
|
|
doctest.testmod ()
|
|
|
|
|