Code
Sudoku.java
public class Sudoku implements ISudoku {
/**
* Generates wildcard values in case of conflict with the keys of the
* Sudoku. Ensures all characters are alphanumeric, does not generate the
* same wildcard twice, and tries to keep the wildcard as short as
* possible.
*/
protected static class WildcardGenerator {
/**
* Used to build the next wildcard.
*/
protected static final List<String> SINGLE_CHARACTER_WILDCARDS =
createSingleCharacterWildcards();
/**
* Returns a wildcard whose generation is based on wildcard so that
* the same wildcard will not be generated twice by the same
* WildcardGenerator.
* @param wildcard The current wildcard.
* @return The next wildcard generated.
*/
public String nextWildcard(String wildcard) {
String last = wildcard.substring(wildcard.length() - 1);
int index = SINGLE_CHARACTER_WILDCARDS.indexOf(last);
if (index == SINGLE_CHARACTER_WILDCARDS.size() - 1) {
return wildcard + SINGLE_CHARACTER_WILDCARDS.get(0);
}
String replacement = SINGLE_CHARACTER_WILDCARDS.get(index + 1);
return wildcard.replace(last, replacement);
}
/**
* Creates and returns the single character wildcards used in the
* WildcardGenerators.
* @return The single character wildcards created.
*/
protected static List<String> createSingleCharacterWildcards() {
List<String> result = new ArrayList<String>();
for (char ch = '0'; ch <= '9'; ++ch) {
result.add("" + ch);
}
for (char ch = 'A'; ch <= 'Z'; ++ch) {
result.add("" + ch);
}
return result;
}
}
/**
* Used to build Sudokus. All methods within this class barring
* {@link Builder#finish()} (which must be called last) can be called in
* any order for any number of times, but side effects may vary. Default
* values are subject to change. It is recommended that inexperienced
* clients simply call all the methods in this order:
* <ol>
* <li>strict</li>
* <li>withWildcard</li>
* <li>withBoxWidth</li>
* <li>withAddedRow</li>
* <li>finish</li>
* </ol>
*/
public static class Builder {
/**
* A regex that matches a sequence of non-alphanumeric characters in a
* String.
*/
protected static final String DELIMITERS = "[\\W_]+";
/**
* The Sudoku built.
*/
protected Sudoku mSudoku;
/**
* True if strict mode is on.
*/
protected boolean mIsStrict;
/**
* Creates a new Builder for a Sudoku with key key.
* @param key The key.
* @throws IllegalArgumentException If any String in key contains
* non-alphanumeric characters.
* @see ISudoku#getKey()
*/
protected Builder(List<String> key) {
for (String symbol : key) {
if (hasDelimiter(symbol)) {
throw new IllegalArgumentException("Symbol " + symbol +
" in key contains a delimiter.");
}
}
init(key);
}
/**
* Creates a new Builder for a Sudoku with key parsed from key.
* @param key Used to differentiate between constant and variable
* cells. Constant cells have symbols parsed from the key. Also
* determines the number of cells per row.
*/
protected Builder(String key) {
Collection<String> parsedKey = new LinkedHashSet<String>();
Collections.addAll(parsedKey, key.split(DELIMITERS));
// A split String can contain the empty String, but that doesn't
// make much sense for a key value
parsedKey.remove("");
init(new ArrayList<String>(parsedKey));
}
/**
* Sets creation mode to strict, so that the subsequent method calls
* must conform to a standard otherwise they will throw an exception.
* Subsequent calls have no affect.
* @return A reference to this Builder.
*/
public Builder strict() {
mIsStrict = true;
return this;
}
/**
* Sets the wildcard to wildcard. Only the final call to this method
* has any affect. If not in strict mode, a null wildcard or a
* wildcard that is contained in the key will have no affect.
* @return A reference to this Builder
* @throws NullPointerException If in strict mode and wildcard is null.
* @throws IllegalArgumentException If in strict mode and the key
* contains wildcard. In addition, the wildcard must contain only
* alphanumeric characters in strict mode.
*/
public Builder withWildcard(String wildcard) {
if (wildcard == null) {
if (mIsStrict) {
throw new NullPointerException("Strict Mode: Null " +
"parameter.");
} else {
return this;
}
}
if (mSudoku.mKey.contains(wildcard)) {
if (mIsStrict) {
throw new IllegalArgumentException("Strict Mode: Key " +
"contains wildcard.");
} else {
return this;
}
}
if (mIsStrict && hasDelimiter(wildcard)) {
throw new IllegalArgumentException("Strict Mode: Wilcard " +
"contains non-alphanumeric characters.");
}
mSudoku.mWildcard = wildcard;
return this;
}
/**
* Sets the width of a box in the Sudoku to boxWidth. When not in
* strict mode, widths that are not positive integers that go evenly
* into the width of the Sudoku (determined by the number of keys)
* will be discarded. For example, if the width of the Sudoku is 9,
* valid box widths are 3 and 9. Entering any other number will be
* discarded, or, in strict mode, throw an exception. Subsequent calls
* will override previous calls to this method.
* @param boxWidth The width of a box in the Sudoku.
* @return A reference to this Builder.
* @throws IllegalArgumentException If boxWidth is not positive or
* does not evenly go into the width of the Sudoku during strict mode.
*/
public Builder withBoxWidth(int boxWidth) {
if (boxWidth > 0 && mSudoku.getWidth() % boxWidth == 0) {
mSudoku.mBoxWidth = boxWidth;
mSudoku.mBoxHeight = mSudoku.getWidth() / mSudoku.mBoxWidth;
} else if (mIsStrict) {
throw new IllegalArgumentException("Strict Mode: Invalid " +
"box width.");
}
return this;
}
/**
* Parses and appends row to the rows in the Sudoku. Parsing uses
* non-alphabetic characters as delimiters. Has no affect if row is
* null and not in strict mode. If the parsed length is not equal to
* the width and it is not strict mode, the row will be filled out
* with wildcards or truncated to the right length. If never called
* with the proper arguments, the Sudoku will be empty. (Note: Strict
* mode does not allow empty Sudokus.)
* @param row The row added.
* @return A reference to this Builder.
* @throws NullPointerException If row is null in strict mode.
* @throws IllegalArgumentException If in strict mode and the parsed
* length is not equal to the width of the Sudoku (determined by the
* number of elements in the key) or an element parsed is neither in
* the key nor equal to the wildcard.
*/
public Builder withAddedRow(String row) {
if (row == null) {
if (mIsStrict) {
throw new NullPointerException("Strict Mode: Null " +
"parameter.");
} else {
return this;
}
}
fillColumns();
String[] cells = row.split(DELIMITERS);
int nAddedCells = 0;
for (int index = 0; index < cells.length; ++index) {
if (nAddedCells == mSudoku.getWidth()) {
if (mIsStrict) {
throw new IllegalArgumentException("Strict Mode: " +
"Too many symbols in " + row);
} else {
break;
}
}
String cell = cells[index];
if ("".equals(cell)) continue;
++nAddedCells;
withAddedCell(cell);
}
if (mIsStrict && nAddedCells < mSudoku.getWidth()) {
throw new IllegalArgumentException("Strict Mode: Too few " +
"symbols in " + row);
}
while (nAddedCells++ < mSudoku.getWidth()) {
mSudoku.mCells.add("");
}
return this;
}
/**
* Appends cell to the cells in the Sudoku. Has no affect if cell is
* null and not in strict mode.
* @param cell The cell added.
* @return A reference to this Builder.
* @throws NullPointerException If cell is null in strict mode.
* @throws IllegalArgumentException If in strict mode and cell is
* neither in the key nor equal to the wildcard.
*/
public Builder withAddedCell(String cell) {
// Test for null first, otherwise strict mode will throw an
// IllegalArgumentException on null
if (cell == null) {
if (mIsStrict) {
throw new NullPointerException("Strict Mode: Null " +
"parameter.");
} else {
return this;
}
}
if (mSudoku.mKey.contains(cell)) {
mSudoku.mCells.add(cell);
} else if (mIsStrict && !mSudoku.getWildcard().equals(cell)) {
throw new IllegalArgumentException("Strict mode: " +
"Invalid symbol " + cell);
} else {
mSudoku.mCells.add("");
}
return this;
}
/**
* Returns a Sudoku ready for use. If not in strict mode and the cells
* do not fill the last row, extra blank cells will be added. If not
* in strict mode and the box height does not go into the height
* evenly, extra blank rows will be added.
* @return A Sudoku ready for use.
* @throws IllegalStateException If in strict mode and the Sudoku is
* empty, the cells do not fills the last row, or the box height does
* not go into the height evenly.
*/
public Sudoku finish() {
if (mSudoku.getSize() == 0) {
if (mIsStrict) {
throw new IllegalStateException("Strict Mode: Sudoku " +
"is empty.");
} else {
mSudoku.mKey = new ArrayList<String>();
}
}
fillColumns();
adjustRows();
if (mSudoku.mBoxWidth == 0) withBoxWidth(1);
buildDimensionLists();
return mSudoku;
}
/**
* A common function called by the Builder constructors to avoid code
* duplication.
* @param key The key.
* @see ISudoku#getKey()
*/
protected void init(List<String> key) {
mSudoku = new Sudoku(key);
WildcardGenerator wildcardGenerator = new WildcardGenerator();
do {
mSudoku.mWildcard =
wildcardGenerator.nextWildcard(mSudoku.getWildcard());
} while (key.contains(mSudoku.getWildcard()));
}
/**
* Returns true if symbol contains a delimiter.
* @param symbol Checked for whether it contains a delimiter.
* @return True if symbol contains a delimiter.
*/
protected boolean hasDelimiter(String symbol) {
return Pattern.compile(DELIMITERS).matcher(symbol).find();
}
/**
* If not is strict mode, fills the columns of the last row. If in
* strict mode, validates that the columns do not need to be filled.
* @throws IllegalStateException If in strict mode and the rows need
* to be filled.
*/
protected void fillColumns() {
int nAddedCells = (mSudoku.getWidth() == 0) ? 0:
(mSudoku.getWidth() - mSudoku.getSize() % mSudoku.getWidth())
% mSudoku.getWidth();
if (mIsStrict && nAddedCells != 0) {
throw new IllegalStateException("Strict Mode: Cells do not " +
"fill all rows.");
}
for (int count = 0; count < nAddedCells; ++count) {
withAddedCell(mSudoku.getWildcard());
}
}
/**
* If not in strict mode, adjusts the number of rows so that the width
* of the Sudoku equals the height. If in strict mode, validates that
* the rows do not need to be adjusted.
* @throws IllegalStateException If in strict mode and the width does
* not match the height.
*/
protected void adjustRows() {
int rowDiff = mSudoku.getWidth() - mSudoku.getHeight();
if (mIsStrict && rowDiff != 0) {
throw new IllegalStateException("Strict Mode: Width does " +
"not equal height.");
}
if (rowDiff > 0) {
String wildcardRow = createWildcardRow();
for (int count = 0; count < rowDiff; ++count) {
withAddedRow(wildcardRow);
}
} else if (rowDiff < 0) {
int nCells = mSudoku.getWidth() * mSudoku.getWidth();
mSudoku.mCells = mSudoku.mCells.subList(0, nCells);
}
}
/**
* Builds the lists of rows, columns, and boxes.
*/
protected void buildDimensionLists() {
mSudoku.mRows = new ArrayList<List<String>>();
mSudoku.mCols = new ArrayList<List<String>>();
mSudoku.mBoxes = new ArrayList<List<String>>();
for (int cellNo = 0; cellNo < mSudoku.mCells.size(); ++cellNo) {
int rowNo = cellNo / mSudoku.getWidth();
int colNo = cellNo % mSudoku.getWidth();
int boxNo = mSudoku.getBoxNo(rowNo, colNo);
List<String> row = mSudoku.getOrCreateAt(rowNo, mSudoku.mRows,
ArrayList.class);
List<String> col = mSudoku.getOrCreateAt(colNo, mSudoku.mCols,
ArrayList.class);
List<String> box = mSudoku.getOrCreateAt(boxNo,
mSudoku.mBoxes, ArrayList.class);
String cell = mSudoku.cellAt(cellNo);
if (cell.isEmpty()) cell = mSudoku.getWildcard();
row.add(cell);
col.add(cell);
box.add(cell);
}
}
/**
* Creates and returns a row full of wildcard symbols.
* @return A row full of wildcard symbols.
*/
protected String createWildcardRow() {
StringBuilder wildcardRow = new StringBuilder();
for (int count = 0; count < mSudoku.getWidth(); ++count) {
wildcardRow.append(mSudoku.getWildcard());
}
return wildcardRow.toString();
}
}
/**
* The cells organized by order of insertion into the Sudoku.
*/
protected List<String> mCells = new ArrayList<String>();
/**
* The cells organized by dimensions.
*/
protected List<List<String>> mRows, mCols, mBoxes;
/**
* Contains the possible symbols of the ConstantCells.
*/
protected List<String> mKey;
/**
* The representation of the VariableCells.
*/
protected String mWildcard = " "; // Setting the initial wildcard to
// contain a single non-alphanumeric
// character will ensure that the
// WildcardGenerator will generate an
// appropriate default value for it
/**
* The dimensions of a box.
*/
protected int mBoxWidth, mBoxHeight;
/**
* Creates a new, empty Sudoku. The Builder should set its fields.
*/
protected Sudoku(List<String> key) {
mKey = key;
}
/**
* Initiates building a Sudoku with key key.
* @param key The key.
* @return The Builder that will create this Sudoku.
* @throws IllegalArgumentException If any String in key contains
* non-alphanumeric characters.
* @see ISudoku#getKey()
*/
public static Builder make(List<String> key) {
return new Builder(key);
}
/**
* Initiates building a Sudoku with key parsed from key using
* non-alphanumeric characters as delimiters.
* @param key The key.
* @return The Builder that will create this Sudoku.
* @see ISudoku#getKey()
*/
public static Builder make(String key) {
return new Builder(key);
}
/**
* Returns the box number given the row number and the column number.
* @param rowNo The row number.
* @param colNo The column number.
* @return The box number.
*/
protected int getBoxNo(int rowNo, int colNo) {
int nBoxesAtRowStart = rowNo / mBoxHeight;
int nBoxesPerRow = getWidth() / mBoxWidth;
int nBoxesInRow = colNo / mBoxWidth;
return nBoxesAtRowStart * nBoxesPerRow + nBoxesInRow;
}
/**
* Adds elements of class elementClass to list until index is a valid
* index and returns the element at index.
* @param index The index of the element returned.
* @param list Contains the element returned.
* @param elementClass The class of the elements in list.
* @return The element of list at index index.
* @throws IndexOutOfBoundsException If index is negative.
*/
@SuppressWarnings("unchecked")
protected <T> T getOrCreateAt(int index, List<T> list, Class<?> elementClass) {
while (list.size() < index + 1) {
try {
list.add((T) elementClass.newInstance());
} catch (InstantiationException e) {
e.printStackTrace();
System.exit(1);
} catch (IllegalAccessException e) {
e.printStackTrace();
System.exit(1);
}
}
return list.get(index);
}
}
Example Usage
ISudoku sudoku = Sudoku.make("A B C D").strict()
.withWildcard("Q").withBoxWidth(2)
.withAddedRow("Q B C D")
.withAddedRow("D C Q A")
.withAddedRow("B A D Q")
.withAddedRow("C Q A B").finish();
Questions
- Is it appropriate to use the Fluent Builder pattern for a Sudoku program? I know that it's easy to overuse the pattern, so I'm wondering if I should go a different route here.
- Do my javadoc comments make sense?
- Are my method names ok? I searched, but I couldn't find tips on Fluent Builder naming conventions. I understand that its supposed to be "readable", but that's really vague.
- Is my logic sound? Do I allow any nonsensical Sudoku grids to be built?
- Any other critiques would be much appreciated. Thanks!