def find_preemptive_sets(puzzle, n):
    """Find preemptive sets and remove them from the candidate lists of other cells in the unit.

    A preemptive set is a set of values, size 'n', that are the only possible values for a set of cells, size 'n',
    within the same unit.  Preemptive sets can be safely removed from any cell in the unit that could be a value besides
    those in the preemptive set.  Preemptive sets are often called naked sets.

    :param puzzle: Puzzle object
    :param n: size of preemptive sets to be found
    """
    for unit_type_id, unit_type in enumerate(
        [alg.ROW_ITER, alg.COL_ITER, alg.BLOCK_ITER]):
        for unit in unit_type:
            for preemptive_tup in itertools.combinations(range(1, 9 + 1), n):
                if len(preemptive_tup) > len(
                        set(preemptive_tup)):  # skip set if any values repeat
                    continue
                preemptive_set = ''.join(str(val) for val in preemptive_tup)
                cells = []
                for row, col in unit:
                    cell = puzzle.cell_array[row][col]

                    # check that all candidates are in preemptive_set
                    candidates_not_in_set = [
                        val for val in cell.candidates
                        if val not in preemptive_set
                    ]
                    if cell.is_solved(
                    ) or len(candidates_not_in_set
                             ) != 0:  # some candidates not in preemptive set
                        continue
                    if len(cells) < n:
                        cells.append(cell)
                    else:
                        cells.clear()
                        break
                if len(cells) == n:
                    unit_list = ['row', 'column', 'block']
                    unit_with_pair = unit_list[unit_type_id]
                    for cell in cells:
                        cell.dont_remove = preemptive_set
                    for cell, val in itertools.product(cells, preemptive_set):
                        alg.update_peers(puzzle, cell.POS[0], cell.POS[1], val,
                                         unit_with_pair)
                    for cell in cells:
                        cell.dont_remove = ''
def _col_sub_unit_exclusions(puzzle):
    """Private find_sub_unit_exclusions() function."""
    for col, val in itertools.product(range(9), alg.DIGITS):
        val_solved = False
        val_in_sub_units = []
        # A sub unit is rows [0-2], [3-5], or [6-8] of a column
        for sub_unit_index, sub_unit in enumerate(alg.BANDS):
            for row in sub_unit:
                cell = puzzle.cell_array[row][col]
                if val in cell.candidates:
                    # if value has been solved, break all loops until the outer val loop
                    if cell.is_solved():
                        val_solved = True
                        break
                    else:
                        # value is in this sub unit. Don't care how many times it shows up, so don't check the rest.
                        val_in_sub_units.append(sub_unit_index)
                        break
            if val_solved:
                break
        # If value is solved or appears in more than one sub unit, go to next value
        if val_solved or len(val_in_sub_units) != 1:
            break
        sub_unit_num = val_in_sub_units[0]
        rows, cols = [], []
        # Find which vertical band 'col' is in (vertical in regards to whole puzzle)
        for vertical_band in alg.BANDS:
            if col in vertical_band:
                cols = vertical_band[:]
                break
        cols.remove(col)
        rows = alg.BANDS[sub_unit_num]
        for row_num, col_num in itertools.product(rows, cols):
            cell = puzzle.cell_array[row_num][col_num]
            previously_solved = cell.is_solved()
            cell.remove_candidate(val)
            if cell.is_solved() and not previously_solved:
                alg.update_peers(puzzle, row_num, col_num,
                                 cell.last_candidate())
def _vertical_block_sub_unit_exclusions(puzzle):
    """Private find_sub_unit_exclusions() function."""
    for block_rows, block_cols, val in itertools.product(
            alg.BANDS, alg.BANDS, alg.DIGITS):
        val_solved = False
        val_in_cols = []
        for col_index, col in enumerate(block_cols):
            for row in block_rows:
                cell = puzzle.cell_array[row][col]
                if val in cell.candidates:
                    if cell.is_solved():
                        val_solved = True
                        break
                    else:
                        # value is in this col. Don't care how many times it shows up, so don't check the rest
                        val_in_cols.append(col_index)
                        break
            if val_solved:
                break
        # If value is solved or appears in more than one row, go to next value
        if val_solved or len(val_in_cols) != 1:
            break
        col_index = val_in_cols[0]
        col_to_update = block_cols[col_index]
        rows = []
        [[
            rows.append(row) for row in horizontal_band
            if horizontal_band != block_rows
        ] for horizontal_band in alg.BANDS]
        for row in rows:
            cell = puzzle.cell_array[row][col_to_update]
            previously_solved = cell.is_solved()
            cell.remove_candidate(val)
            if cell.is_solved() and not previously_solved:
                alg.update_peers(puzzle, row, col_to_update,
                                 cell.last_candidate())
def find_hidden_sets(puzzle, n):
    """Find hidden sets and remove other values from the cells' candidate lists.

    A hidden set is a set of values, size 'n', which only appear in the candidate lists of a set of cells, size 'n',
    within the same unit.  Every value that is not in the hidden set can be safely removed from that cell's
    candidate list.

    :param puzzle: Puzzle object
    :param n: size of hidden sets to be found
    """
    for unit_type in [alg.ROW_ITER, alg.COL_ITER, alg.BLOCK_ITER]:
        for unit in unit_type:
            for val_tup in itertools.combinations(range(1, 9 + 1), n):
                if len(val_tup) > len(
                        set(val_tup)):  # skip set if any values repeat
                    continue
                val_set = ''.join(str(val) for val in val_tup)
                val_set_matches = [set() for _ in range(n)]
                cells = []
                for row, col in unit:
                    cell = puzzle.cell_array[row][col]
                    cell_solved = cell.is_solved()

                    # Check if one of the values is already solved. If one is, break and look at next value set
                    if cell_solved and cell.last_candidate() in val_set:
                        cells.clear()
                        break

                    # If cell is solved or none of val_set appears in its candidate list, continue to next cell
                    set_union = [
                        val for val in val_set if val in cell.candidates
                    ]
                    if cell_solved or len(set_union) == 0:
                        continue

                    for val_index, val in enumerate(val_set):
                        if val in cell.candidates:
                            val_set_matches[val_index].add(cell)
                    # If a value appears in a greater number of cells than the size of the set, break
                    if any(len(matches) > n for matches in val_set_matches):
                        cells.clear()
                        break

                    # If more than n cells have matching values, this val_set is not hidden
                    if len(cells) >= n:
                        cells.clear()
                        break
                    else:
                        cells.append(cell)
                if len(cells) == n:
                    combined_candidates = []
                    for cell in cells:
                        new_vals = [
                            val for val in cell.candidates
                            if val not in combined_candidates
                        ]
                        combined_candidates += new_vals

                    # Check that found set is hidden, not naked
                    vals_not_in_set = [
                        val for val in combined_candidates
                        if val not in val_set
                    ]
                    if len(vals_not_in_set) == 0:
                        continue
                    for cell in cells:
                        cell.set_cell(val_set)
                        if cell.is_solved():
                            alg.update_peers(puzzle, cell.POS[0], cell.POS[1],
                                             cell.last_candidate())