3
\$\begingroup\$

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

  1. 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.
  2. Do my javadoc comments make sense?
  3. 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.
  4. Is my logic sound? Do I allow any nonsensical Sudoku grids to be built?
  5. Any other critiques would be much appreciated. Thanks!
\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

Answers to a few questions and a few other notes:

  1. Method names and the builder interface/pattern looks fine. I would rename the finish to build as the Fluent builders generator for Eclipse calls it but the others are OK.

  2. Here I'd not allow any nonsensical Sudoku grids to be built. If it was a GUI application it would make sense to create (temporarily) nonsensical tables for easier editing but I don't see any reasons why should it be allowed here. Some method just ignore input errors on non-strict mode which could be really hard to debug.

  3. System.exit isn't a nicest way to stop an entire application, especially hidden in a catch block. I'd throw and exception and let callers to handle the errors.

  4. The getOrCreateAt method are always called with ArrayList.class. I would not use newInstance here.

  5. As a client I would not expect that the order of the methods are defined for a builder, so calling strict() at the end of the chain would be completely fine for me.

  6. I'd redesign the class hierarchy a little bit, I'd except a Sudoku (or Table) class which stores Cell objects and not simple Strings. I'd create a Cell wrapper class (which would contain a String only) for the cell values. It would improve type-safety and make the code easier to read.

  7. The Sudoku.cellAt() method does not implies good encapsulation and it results tight coupling. Clients should not know the internals of the Sudoku class.

  8. It's rather hard to understand the role of mRows, mCols, mBoxes fields. If I'm right the code stores every cell value in all of the three Lists. It does not smell good.

    I think Guava's Table class (or a similar custom implementation) could be a better choice here.

  9. The m field name prefix is unnecessary and rather uncommon in the Java world. Modern IDEs use different colors for local variables and fields. See Effective Java, 2nd edition, Item 56: Adhere to generally accepted naming conventions

\$\endgroup\$
1
  • 1
    \$\begingroup\$ This isn't the complete class, so #4 doesn't apply and #8 has a good reason. The cellAt method was designed as such to allow for extensions to nonstandard Sudokus (e.g. Sudoku Samurai, 3D Sudoku). It just returns the Sudoku cell in order of addition. I never thought of Sudoku as a table; this gives me so many ideas. All in all good suggestions. Thank you very much! \$\endgroup\$
    – Eva
    Jul 31, 2012 at 21:32
3
\$\begingroup\$

List<String> is not good way, use Set<Character> to store (return false if wildcard exists) your wildcard.

So you will avoid the "" + (very bad way) and use Character.ValueOf(char));

\$\endgroup\$
1
  • \$\begingroup\$ Thank you. A good suggestion. I thought that part smelled a bit. \$\endgroup\$
    – Eva
    Jul 31, 2012 at 21:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.