Source code for sudokutools.sudoku

"""Parse, print and compare sudokus.

Classes defined here:
 * Sudoku: Represents a sudoku.

Functions defined here:
 * column_of(): Returns all coordinates in the column of a given field.
 * row_of(): Returns all coordinates in the row of a given field.
 * square_of(): Returns all coordinates in the square of a given field.
 * surrounding_of(): Returns all surrounding coordinates of a given field.
"""

from itertools import product
from string import whitespace

INDICES = (0, 1, 2, 3, 4, 5, 6, 7, 8)
NUMBERS = (1, 2, 3, 4, 5, 6, 7, 8, 9)


[docs]class Sudoku(object): """Represents a sudoku. This class provides methods to read, write, copy and display data to and from a sudoku as well as compare one sudoku with another. It stores 9x9 fields and the numbers and candidates within these fields. Coordinates for row and column access are values from 0 to 8 (including). Using other values will most likely raise an IndexError (even though negative numbers are supported, they should not be used). Overview: Data access (read, write): * __getitem__() * __setitem__() * get_candidates() * set_candidates() * remove_candidates() Copying: * copy() Comparing: * empty() * __len__() * __eq__() * diff() Printing: * __str__() * encode() Parsing: * decode() """ def __init__(self): """Create a new empty sudoku.""" self.__numbers = [[0] * 9 for _ in range(9)] self.__candidates = [[frozenset()] * 9 for _ in range(9)]
[docs] def empty(self): """Iterate through the coordinates of all empty fields. Yields: (int, int): row and column of the next empty field. """ for row, col in product(INDICES, repeat=2): if not self.__numbers[row][col]: yield row, col
[docs] def diff(self, other): """Iterate through coordinates with different values in other. Compares each field in other to the corresponding field in self and yields the coordinates, if the number within is different. Args: other (Sudoku): Most likely another :class:`Sudoku` instance, but any object, which can be accessed by other[row, col] is valid. Yields: (int, int): row and column of the next different field. """ for row, col in product(INDICES, repeat=2): if self[row, col] != other[row, col]: yield row, col
[docs] def copy(self, include_candidates=False): """Returns a copy of this sudoku. Args: include_candidates (bool): Whether to copy candidates as well. Returns: Sudoku: The new sudoku instance. """ sudoku = Sudoku() for row, col in product(INDICES, repeat=2): sudoku[row, col] = self[row, col] if include_candidates: for row, col in product(INDICES, repeat=2): c = self.get_candidates(row, col) sudoku.set_candidates(row, col, c) return sudoku
[docs] def __eq__(self, other): """Return if other is equal in all fields. Args: other (Sudoku): Most likely another :class:`Sudoku` instance, but any object, which can be accessed by other[row, col] is valid. Returns: bool: True, if all fields are equal and false if not or other is an incompatible type. """ try: for row, col in product(INDICES, repeat=2): if self[row, col] != other[row, col]: return False return True except (IndexError, KeyError, TypeError): return False
[docs] def __str__(self): """Return sudoku as a human-readable string. Returns: str: String representing the sudoku. """ s = "" for rc, row in enumerate(INDICES): col_str = [] for cc, col in enumerate(INDICES): val = str(self[row, col]) if val == "0": val = " " col_str.append(val) if cc % 3 == 2 and cc < 8: col_str.append("|") s += " ".join(col_str) if rc < 8: s += "\n" if rc % 3 == 2 and rc < 8: s += "------+-------+------\n" return s
[docs] def __getitem__(self, key): """Return the number in the field referenced by key. Args: key (int, int): row and column of the requested field. Must be in range(0, 9). Returns: int: The number in the given field, 0 representing an empty field. Raises: IndexError: if the given coordinates are not valid. """ row, col = key return self.__numbers[row][col]
[docs] def __setitem__(self, key, value): """Set the number in the field referenced by key to value. Args: key (int, int): row and column of the requested field. Must be in range(0, 9). value (int): The number to set the field to, 0 representing an empty field. Raises: IndexError: if the given coordinates are not valid. """ row, col = key self.__numbers[row][col] = value
[docs] def __len__(self): """Return the number of non-empty fields in this sudoku. Returns: int: The number of non-empty fields within this sudoku. """ count = 0 for row, col in product(INDICES, repeat=2): if self.__numbers[row][col]: count += 1 return count
[docs] def get_number(self, row, col): """Same as sudoku[row, col].""" return self[row, col]
[docs] def set_number(self, row, col, value): """Same as sudoku[row, col] = value.""" self[row, col] = value
[docs] def get_candidates(self, row, col): """Return the candidates of the field at (row, col). Args: row (int): The row of the field. col (int): The column of the field. Returns: frozenset: The candidates at (row, col). """ return self.__candidates[row][col]
[docs] def set_candidates(self, row, col, value): """Set the candidates of the field at (row, col) to value. Args: row (int): The row of the field. col (int): The column of the field. value (iterable): The candidates to set the field to. """ self.__candidates[row][col] = frozenset(value)
[docs] def remove_candidates(self, row, col, value): """Remove the given candidates in the field at (row, col). Ignores candidates, which are not present in the field. Args: row (int): The row of the field. col (int): The column of the field. value (iterable): The candidates to remove. """ self.__candidates[row][col] -= set(value)
[docs] def encode(self, row_sep="", col_sep="", include_candidates=False): """Return sudoku as a (machine-readable) string. This method is mainly provided to output sudokus in a machine-readable format, but can be used for creating nicely looking representations as well. Args: row_sep (str): Separator between rows. col_sep (str): Separator between columns. include_candidates: Returns: (str): String representing this sudoku. For examples of default output string see decode(). """ rows = [] for row in INDICES: numbers = [str(self[row, col]) for col in INDICES] rows.append(col_sep.join(numbers)) s = row_sep.join(rows) if include_candidates: all_candidates = [] for row, col in product(INDICES, repeat=2): clist = list(self.get_candidates(row, col)) cstr = "".join([str(i) for i in sorted(clist)]) all_candidates.append(cstr) s += "|" + ",".join(all_candidates) return s
[docs] @classmethod def decode(cls, s): """Create a new sudoku from the string s. Args: s (str): A string representing the sudoku (see below). Returns: Sudoku: The newly created :class:`Sudoku` instance. Examples for s: 000030000005009602008004013020060000703040106000080090210300800306800700000020000 000030000005009602008004013020060000703040106000080090210300800306800700000020000|124,235 Whitespace is ignored while parsing the string, so you can place newlines for better readability. Each number represents the value of a column. If a row is full, we continue in the next one. So the sudoku above looks like this:: | 3 | 5 | 9 | 6 2 8 | 4 | 1 3 ------+-------+------ 2 | 6 | 7 3 | 4 | 1 6 | 8 | 9 ------+-------+------ 2 1 | 3 | 8 3 6 | 8 | 7 | 2 | The second string additionally defines candidates. Each set of candidates is separated by ',' so the string above defines the candidates for (0, 0) to be 1, 2 and 4 and for (0, 1) to be 2, 3 and 5 This is the default format, which encode() uses and no other format is supported right now. """ if '|' in s: s, c_str = s.split('|', 1) else: c_str = "" # read sudoku fields sudoku = Sudoku() col = 0 row = 0 for item in s: if item in whitespace: continue sudoku[row, col] = int(item) col += 1 if col >= 9: row += 1 col = 0 if row >= 9: break # read candidates for c in whitespace: c_str = c_str.replace(c, '') c_str = c_str.split(',') col = 0 row = 0 for c in c_str: if c in whitespace: continue sudoku.set_candidates(row, col, set([int(item) for item in c])) col += 1 if col == 9: col = 0 row += 1 if row == 9: break return sudoku
[docs]def column_of(row, col, include=True): """Return all coordinates in the column of (col, row) as a list. Args: row (int): The row of the field. col (int): The column of the field. include (bool): Whether or not to include (row, col). Returns: list of (int, int): list of pairs (row, column) of all fields in the same column. """ return [(i, col) for i in INDICES if include or i != row]
[docs]def row_of(row, col, include=True): """Return all coordinates in the row of (col, row) as a list. Args: row (int): The row of the field. col (int): The column of the field. include (bool): Whether or not to include (row, col). Returns: list of (int, int): list of pairs (row, column) of all fields in the same row. """ return [(row, j) for j in INDICES if include or j != col]
[docs]def square_of(row, col, include=True): """Return all coordinates in the square of (col, row) as a list. Args: row (int): The row of the field. col (int): The column of the field. include (bool): Whether or not to include (row, col). Returns: list of (int, int): list of pairs (row, column) of all fields in the same square. """ grid_x = col - (col % 3) grid_y = row - (row % 3) coords = [(grid_y + i, grid_x + j) for i in range(3) for j in range(3)] if not include: coords.remove((row, col)) return coords
[docs]def surrounding_of(row, col, include=True): """Return all surrounding coordinates of (col, row) as a list. Args: row (int): The row of the field. col (int): The column of the field. include (bool): Whether or not to include (row, col). Returns: list of (int, int): list of pairs (row, column) of all fields in the same column, row or square. """ coords = row_of(row, col, include=include) coords.extend(column_of(row, col, include=include)) coords.extend(_quad_without_row_and_column_of(row, col)) # remove two items of (col, row) in coords (there are three) if include: coords.remove((row, col)) return coords
[docs]def _quad_without_row_and_column_of(row, col): """Return some coordinates in the square of (col, row) as a list. The coordinates in the same row and column are removed. This is an internal function and should not be used outside of the sudoku module. """ grid_x = col - (col % 3) grid_y = row - (row % 3) return [(grid_y + i, grid_x + j) for i, j in product( range(3), repeat=2) if grid_y + i != row and grid_x + j != col]