#!/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[3, 2] '9' >>> grid.get_row(2) '080900040' >>> grid.get_col(1) '048003500' >>> grid.get_square(4, 7) '003002870' >>> grid[1, 5] '3' >>> grid.is_empty_cell(1, 5) False >>> grid.is_empty_cell(4, 6) True >>> grid[4, 6] = '9' >>> grid.pretty_print() +-------+-------+-------+ | 9 . 6 | . 2 8 | . . 3 | | . 4 . | 5 . . | . . 1 | | . 8 . | 9 . . | . 4 . | +-------+-------+-------+ | 6 . . | . . . | . 7 . | | . . 8 | 2 . 6 | 9 . . | | . 3 . | . . . | . . 5 | +-------+-------+-------+ | . 5 . | . 9 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: col, row = 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: col, row = 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[col, row] 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[col, row] for row in range(9)]) def get_square(self, col, row): """ :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[firstcol + j, firstrow + i] for i in range(3) for j in range(3)) def is_empty_cell(grid, col, row): """ :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[col, row] == 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 ()