de.koethnig.sudoku.State.java Source code

Java tutorial

Introduction

Here is the source code for de.koethnig.sudoku.State.java

Source

/*******************************************************************************
 * Copyright  2012 Ivo Kthnig. All rights reserved.
 *
 * Author: Ivo Kthnig
 *
 * This file is part of Sudoku.
 *
 * Sudoku is free software: you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 *
 * Sudoku is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Components 4 Java. If not, see <http://www.gnu.org/licenses/>.
 *******************************************************************************/
package de.koethnig.sudoku;

import static de.koethnig.sudoku.State.Area.COLUMN;
import static de.koethnig.sudoku.SudokuUtils.getBoxIndex;
import static de.koethnig.sudoku.SudokuUtils.getCellIndexOfItsBox;
import static java.lang.String.format;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Represents a current state of the solving process for a sudoku.
 */
public final class State {

    /** The logger for this class. */
    private static final Logger LOGGER = LoggerFactory.getLogger(State.class);

    /**
     * Number of bits per byte.
     */
    private static final int NUM_BITS_PER_BYTE = 8;

    /**
     * Enumeration of all area types.
     */
    public enum Area {
        COLUMN, ROW, H_BOX, V_BOX
    }

    /**
     * The size of the sudoku. Usually 3.
     */
    public final SudokuSize size;

    /**
     * The squared size of the sudoku. Usually 9.
     */
    public final int qsize;

    /**
     * Stores all cells of the sudoku. The first index of the 2-dimensional
     * array addresses the the column of the cell and the second index
     * addresses the row of the cell.
     */
    private final Cell[][] columnArea;

    /**
     * Stores all cells of the sudoku. The first index of the 2-dimensional
     * array addresses the the row of the cell and the second index
     * addresses the column of the cell.
     */
    private final Cell[][] rowArea;

    /**
     * Stores all cells of the sudoku. The first index of the 2-dimensional
     * array addresses the the box of the cell and the second index
     * addresses the cell in the box.<br/><br/>
     *
     * The numbering scheme for boxes and cells in boxes count the cells
     * first from left to right ([H]orizontally) and secondly from top to down
     * (vertically). For example for sudokus of size 3 (e.g. 9 different
     * values) it is:<br/>
     * <pre>
     * 0 1 2<br/>
     * 3 4 5<br/>
     * 6 7 8<br/>
     * </pre>
     */
    private final Cell[][] boxAreaH;

    /**
     * Stores all cells of the sudoku. The first index of the 2-dimensional
     * array addresses the the box of the cell and the second index
     * addresses the cell in the box.<br/><br/>
     *
     * The numbering scheme for boxes and cells in boxes counts the cells
     * first from top to down ([V]ertically) and secondly from left to right
     * (horizontally). For example for sudokus of size 3 (e.g. 9 different
     * values) it is:<br/>
     * <pre>
     * 0 3 6<br/>
     * 1 4 7<br/>
     * 2 5 8<br/>
     * </pre>
     */
    private final Cell[][] boxAreaV;

    /**
     * Creates a new empty state.
     * <br/><br/>
     *
     * @param size
     *         The size of the sudoku.
     *
     * @return a state with the given partial solution.
     */
    public static State createEmptyState(final SudokuSize size) {
        return new State(size);
    }

    /**
     * Creates a new state and initialize it with a given partial solution.
     * <br/><br/>
     *
     * Each char of the field <code>values</code> will be interpreted to fill
     * the next cell or leave it empty if it is the last char of
     * <code>values</code>. All other chars in the <code>String</code> will be
     * ignored.<br/><br/>
     *
     * @param size
     *         The size of the sudoku.
     * @param solution
     *         The initial solution of the cells.
     * @param restrictionOnly
     *         Whether to set the restriction only or also set the solution of
     *         the cells.
     *
     * @return a state with the given partial solution.
     */
    public static State createStateBySolution(final SudokuSize size, final String solution,
            final boolean restrictionOnly) {
        final State result = new State(size);
        try {
            result.setSolution(solution, restrictionOnly);
        } catch (final ExcludeException e) {
            // should not occur since the state is fresh
            throw new RuntimeException("Unexpectedly could not init partial solution.", e);
        }
        return result;
    }

    /**
     * Creates a new state and initialize it with a given restrictions for the
     * cells.<br/><br/>
     *
     * Each char of the field <code>values</code> will be interpreted to be a
     * candidate to be kept. The chars will be interpreted as coordinated in a
     * size x size box and each size x size box will be mapped to a cell.
     *
     * @param size
     *         The size of the sudoku.
     * @param restriction
     *         The initial restrictions of the cells.
     *
     * @return a state with the given restrictions.
     *
     * @throws ExcludeException
     *         if the restriction contains cells without candidates left.
     */
    public static State createStateByRestriction(final SudokuSize size, final String restriction)
            throws ExcludeException {
        final State result = new State(size);
        result.setRestriction(restriction);
        return result;
    }

    /**
     * Creates a new state and initializes the restrictions of the cell using
     * the given Base64 encoded string.<br/><br/>
     *
     * @param size
     *         The size of the sudoku.
     * @param base64
     *         The Base64 encoded state.
     *
     * @return a state with the given Base64 encoded restrictions.
     *
     * @throws ExcludeException
     *         if the restrictions described by the base64-string contains
     *         cells without candidates left.
     *
     * @throws IndexOutOfBoundsException
     *         if the Base64 string encodes a wrong byte size.
     */
    public static State createStateFromBase64(final SudokuSize size, final String base64) throws ExcludeException {
        final int ssize = size.size;
        final int qsize = ssize * ssize;
        final int bits = qsize * qsize * qsize;
        final int bytes = (bits + NUM_BITS_PER_BYTE - 1) / NUM_BITS_PER_BYTE;
        final byte[] byteArray = Base64.decodeBase64(base64);
        if (byteArray.length != bytes)
            throw new IndexOutOfBoundsException("Invalid size of Base64 string.");
        final State result = new State(size);
        int byteIndex = 0;
        int bitIndex = NUM_BITS_PER_BYTE - 1;
        for (final Cell cell : result.getCells()) {
            for (int val = 0; val < qsize; val++) {
                if ((byteArray[byteIndex] & 1 << bitIndex) == 0)
                    cell.exclude(val);
                bitIndex--;
                if (bitIndex == -1) {
                    bitIndex = NUM_BITS_PER_BYTE - 1;
                    byteIndex++;
                }
            }
        }
        return result;
    }

    /**
     * Creates a copy of a state.<br/><br/>
     *
     * @param state
     *         The state to copy.
     *
     * @return a copy of the given state.
     */
    public static State createCopy(final State state) {
        final State result = new State(state.size);
        for (final Cell cell : state.getCells()) {
            final Cell resultCell = result.getCell(COLUMN, cell.getColumn(), cell.getRow());
            final int solution = cell.getSolution();
            if (solution == -1) {
                for (int val = 0; val < result.qsize; val++) {
                    if (!cell.isPossible(val))
                        try {
                            resultCell.exclude(val);
                        } catch (final ExcludeException e) {
                            // should not occur since original cell would
                            // have thrown this exception too and can not
                            // be invalid
                            throw new RuntimeException("Unexpected exlude exception while " + "copying state.", e);
                        }
                }
            } else {
                try {
                    resultCell.setSolution(solution);
                } catch (final ExcludeException e) {
                    // should not occur since original cell would have
                    // thrown this exception too and can not be invalid
                    throw new RuntimeException("Unexpectedly could not copy cell.", e);
                }
            }
        }
        return result;
    }

    /**
     * Creates a new state with no restriction.<br/><br/>
     *
     * @param size
     *         The simple size of the state.
     */
    private State(final SudokuSize size) {
        this.size = size;
        qsize = size.size * size.size;
        columnArea = new Cell[qsize][qsize];
        rowArea = new Cell[qsize][qsize];
        boxAreaH = new Cell[qsize][qsize];
        boxAreaV = new Cell[qsize][qsize];
        for (int column = 0; column < qsize; column++) {
            for (int row = 0; row < qsize; row++) {
                final Cell cell = new Cell(size.size, column, row);
                columnArea[column][row] = cell;
                rowArea[row][column] = cell;
                boxAreaH[getBoxIndex(size.size, column, row)][getCellIndexOfItsBox(size.size, column, row)] = cell;
                // yes we exchange row and column here!
                boxAreaV[getBoxIndex(size.size, row, column)][getCellIndexOfItsBox(size.size, row, column)] = cell;
            }
        }
    }

    /**
     * Initializes the state with the given partial solution for the
     * cells.<br/><br/>
     *
     * Each char in the field <code>values</code> will be interpreted to fill
     * the next cell or leave it empty if it is the last char of
     * <code>values</code>. All other chars in the <code>String</code> will be
     * ignored.<br/><br/>
     *
     * The cells will filled first from left to right and secondly from top to
     * bottom.
     *
     * @param solution
     *         The initial partial solution for the cells.
     * @param restrictionOnly
     *         Whether to set the restrictions only or also set the solution
     *         of the cells.
     * @throws ExcludeException
     *         if the state was not fresh and some solutions does not match.
     */
    private void setSolution(final String solution, final boolean restrictionOnly) throws ExcludeException {
        // the current index of the cell
        int index = 0;
        // the current position when reading the handicap
        int position = 0;
        while (index < qsize * qsize && position < solution.length()) {
            final int value = size.values.indexOf(solution.charAt(position));
            if (value != -1) {
                if (value != size.values.length() - 1) {
                    final int column = index % qsize;
                    final int row = index / qsize;
                    if (restrictionOnly)
                        columnArea[column][row].restrictToCandidate(value);
                    else
                        columnArea[column][row].setSolution(value);
                }
                index++;
            }
            position++;
        }
    }

    /**
     * Initializes the state with the given restrictions for the
     * cells.<br/><br/>
     *
     * Each char in the field <code>values</code> will be interpreted to be a
     * candidate to be kept. The chars will be interpreted as coordinated in a
     * size x size box and each size x size box will be mapped to a cell.
     *
     * @param restriction
     *         The initial restrictions for the cells.
     *
     * @throws ExcludeException
     *         if the restrictions contains cells without candidates left.
     */
    private void setRestriction(final String restriction) throws ExcludeException {
        final boolean[][][] b = new boolean[qsize][qsize][qsize];

        // the current index of the candidate
        int index = 0;
        // the current position when reading the handicap
        int position = 0;
        while (index < qsize * qsize * qsize && position < restriction.length()) {
            final int value = size.values.indexOf(restriction.charAt(position));
            if (value != -1) {
                if (value != size.values.length() - 1) {
                    final int column = (index % (qsize * size.size)) / size.size;
                    final int row = index / qsize / qsize;
                    b[column][row][value] = true;
                }
                index++;
            }
            position++;
        }
        for (final Cell cell : getCells()) {
            for (int value = 0; value < qsize; value++) {
                if (!b[cell.getColumn()][cell.getRow()][value]) {
                    cell.exclude(value);
                }
            }
        }
    }

    /**
     * Returns the cell specified by the area and the two index arguments.
     *
     * @param area
     *         The area.
     * @param arg1
     *         First index of the area.
     * @param arg2
     *         Second index of the area.
     *
     * @return the desired cell.
     */
    public Cell getCell(final Area area, final int arg1, final int arg2) {
        switch (area) {
        case COLUMN:
            return columnArea[arg1][arg2];
        case ROW:
            return rowArea[arg1][arg2];
        case H_BOX:
            return boxAreaH[arg1][arg2];
        case V_BOX:
            return boxAreaV[arg1][arg2];
        default:
            // should not occur since all types are handled
            throw new RuntimeException("Unknown area type.");
        }
    }

    /**
     * The restrictions of the state encoded as Base64 string.
     *
     * @return the restrictions of the state encoded as Base64 string.
     */
    public String getAsBase64() {
        int byteIndex = 0;
        int bitIndex = NUM_BITS_PER_BYTE - 1;
        final int bits = qsize * qsize * qsize;
        final int bytes = (bits + NUM_BITS_PER_BYTE - 1) / NUM_BITS_PER_BYTE;
        final byte[] byteArray = new byte[bytes];
        for (final Cell cell : getCells()) {
            for (int val = 0; val < qsize; val++) {
                if (cell.isPossible(val)) {
                    byteArray[byteIndex] |= 1 << bitIndex;
                }
                bitIndex--;
                if (bitIndex == -1) {
                    bitIndex = NUM_BITS_PER_BYTE - 1;
                    byteIndex++;
                }
            }
        }
        return StringUtils.newStringUtf8(Base64.encodeBase64(byteArray, false));
    }

    /**
     * Checks whether the state is a (possibly invalid) solution or not.
     * The state is a solution if all cells contain a solution. The solution
     * must not be valid.
     *
     * @return <code>true</code> iff the state is a (possibly invalid)
     *         solution.
     */
    public boolean isSolved() {
        for (final Cell cell : getCells()) {
            final int solution = cell.getSolution();
            if (solution == -1) {
                LOGGER.debug(format("Missing solution at (%d, %d).", cell.getColumn() + 1, cell.getRow() + 1));
                return false;
            }
        }
        return true;
    }

    /**
     * Checks whether the state is a valid solution or not. The state is a
     * solution if all cells contains a solution and the sudoku conditions are
     * fullfilled.
     *
     * @return <code>true</code> iff the state is valid solution.
     */
    public boolean isCorrectlySolved() {
        final List<Set<Integer>> columns = new ArrayList<Set<Integer>>(qsize);
        final List<Set<Integer>> rows = new ArrayList<Set<Integer>>(qsize);
        final List<Set<Integer>> boxes = new ArrayList<Set<Integer>>(qsize);
        for (int index = 0; index < qsize; index++) {
            final Set<Integer> column = new HashSet<Integer>();
            final Set<Integer> row = new HashSet<Integer>();
            final Set<Integer> box = new HashSet<Integer>();
            columns.add(column);
            rows.add(row);
            boxes.add(box);
        }
        for (final Cell cell : getCells()) {
            final int column = cell.getColumn();
            final int row = cell.getRow();
            final int solution = cell.getSolution();
            if (solution == -1) {
                LOGGER.debug(format("Missing solution at (%d, %d).", column + 1, row + 1));
                return false;
            }
            if (columns.get(column).contains(solution)) {
                LOGGER.debug(format("Column %d contains %d at least twice.", column + 1, solution + 1));
                return false;
            }
            columns.get(column).add(solution);

            if (rows.get(row).contains(solution)) {
                LOGGER.debug(format("Row %d contains %d at least twice.", row + 1, solution + 1));
                return false;
            }
            rows.get(row).add(solution);

            final int box = getBoxIndex(size.size, column, row);
            if (boxes.get(box).contains(solution)) {
                LOGGER.debug(format("Box %d contains %d at least twice.", box + 1, solution + 1));
                return false;
            }
            boxes.get(box).add(solution);
        }
        return true;
    }

    /**
     * Stores an <code>Iterable</code> over all cells of the state.
     */
    private final Iterable<Cell> cells = new Iterable<Cell>() {
        @Override
        public Iterator<Cell> iterator() {
            return new Iterator<Cell>() {
                int column = 0;
                int row = 0;

                @Override
                public boolean hasNext() {
                    return column < qsize;
                }

                @Override
                public Cell next() {
                    if (!hasNext())
                        throw new NoSuchElementException("Iterator over cells of state has no more " + "elements.");
                    final Cell result = getCell(COLUMN, column, row);
                    row++;
                    if (row == qsize) {
                        row = 0;
                        column++;
                    }
                    return result;
                }

                @Override
                public void remove() {
                    throw new UnsupportedOperationException(
                            "This Iterator over cells does not support " + "remove().");
                }
            };
        }
    };

    /**
     * Return an <code>Iterable</code> over all cells of the state.
     *
     * @return an <code>Iterable</code> over all cells of the state.
     */
    public Iterable<Cell> getCells() {
        return cells;
    }

    /**
     * Returns all possible candidates for each unsolved cell of the state.
     *
     * @return the candidates of all unsolved cells.
     */
    public List<Candidate> getCandidates() {
        final List<Candidate> result = new ArrayList<Candidate>();
        for (int column = 0; column < qsize; column++) {
            for (int row = 0; row < qsize; row++) {
                final Cell cell = getCell(COLUMN, column, row);
                if (cell.getSolution() == -1)
                    for (int value = 0; value < qsize; value++)
                        if (cell.isPossible(value))
                            result.add(new Candidate(column, row, value));
            }
        }
        return result;
    }

    /**
     * Helper method for <code>printState()</code> to print simple rows.
     *
     * @param printer
     *            printStream used for output
     * @param leftBorder
     *            char for the left border of the row
     * @param rightBorder
     *            char for the right border of the row
     * @param boxSeparator
     *            char for box separators
     * @param cellSeparator
     *            char for cell separators
     * @param fill
     *            char to fill space
     */
    private void printRow(final PrintWriter printer, final char leftBorder, final char rightBorder,
            final char boxSeparator, final char cellSeparator, final char fill) {
        final StringBuilder builder = new StringBuilder();
        for (int box = 0; box < size.size; box++) {
            builder.append(box == 0 ? leftBorder : boxSeparator);
            for (int cell = 0; cell < size.size; cell++) {
                builder.append(cellSeparator);
                for (int value = 0; value < size.size; value++)
                    builder.append(fill);
            }
            builder.append(cellSeparator);
        }

        builder.append(rightBorder);
        printer.println(builder.toString());
    }

    /**
     * Prints the state with all possible candidates to a
     * <code>PrintWriter</code>.
     *
     * @param printer
     *         The PrintWriter.
     */
    public void printCandidates(final PrintWriter printer) {
        printRow(printer, ' ', ' ', '-', '-', '-');

        for (int i = 0; i < qsize * size.size; i++) {
            if (i % size.size == 0 && i != 0)
                if (i % qsize == 0)
                    printRow(printer, '|', '|', '-', '-', '-');
                else
                    printRow(printer, '|', '|', '|', ' ', ' ');
            final StringBuilder builder = new StringBuilder();
            builder.append("| ");
            for (int j = 0; j < qsize * size.size; j++) {
                if (j % size.size == 0 && j != 0)
                    if (j % qsize == 0)
                        builder.append(" | ");
                    else
                        builder.append(" ");

                final int value = columnArea[j / size.size][i / size.size].getSolution();
                if (value == -1)
                    if (columnArea[j / size.size][i / size.size]
                            .isPossible(i % size.size * size.size + j % size.size))
                        builder.append(size.values.charAt(i % size.size * size.size + j % size.size));
                    else
                        builder.append(size.values.charAt(qsize));
                else if (i % size.size == (size.size - 1) / 2 && j % size.size == (size.size - 1) / 2)
                    builder.append(size.values.charAt(value));
                else
                    builder.append(size.values.charAt(qsize));
            }
            builder.append(" |");
            printer.println(builder.toString());
        }

        printRow(printer, ' ', ' ', '-', '-', '-');
    }

    /**
     * Prints the current solution to a <code>PrintWriter</code>.
     *
     * @param printer
     *         The PrintWriter.
     */
    public void printSolution(final PrintWriter printer) {
        for (int row = 0; row < qsize; row++) {
            if (row > 0 && row % size.size == 0) {
                for (int index = 0; index < qsize + size.size - 1; index++)
                    printer.print("-");
                printer.println();
            }
            for (int column = 0; column < qsize; column++) {
                if (column > 0 && column % size.size == 0)
                    printer.print('|');
                final Cell cell = columnArea[column][row];
                if (cell.getSolution() == -1)
                    printer.print(size.values.charAt(size.values.length() - 1));
                else
                    printer.print(size.values.charAt(cell.getSolution()));
            }
            printer.println();
        }
    }

    /**
     * Returns the current solution of the state as <code>String</code>.
     *
     * @return the current solution.
     */
    public String toSolutionString() {
        final StringWriter out = new StringWriter();
        final PrintWriter writer = new PrintWriter(out);
        printSolution(writer);
        writer.close();
        return out.toString();
    }

    /**
     * Returns the state with all possible candidates as <code>String</code>.
     *
     * @return the current state
     */
    public String toCandidatesString() {
        final StringWriter out = new StringWriter();
        final PrintWriter writer = new PrintWriter(out);
        printCandidates(writer);
        writer.close();
        return out.toString();
    }

}