Java tutorial
/******************************************************************************* * 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(); } }