Sudoku/sudoku_grid.py
2021-01-07 11:13:30 +01:00

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 ()