5
\$\begingroup\$

A of A method to capitalize the first letter of all words in a String.

To recap the requirements:

Please implement this method to capitalize all first letters of the words in the given String. All other symbols shall remain intact. If a word starts not with a letter, it shall remain intact too. Assume that the parameter String can only contain spaces and alphanumeric characters.

NOTE: please keep in mind that the words can be divided by single or multiple spaces.The spaces also can be found at the beginning or the end of the parameter string,and you need to preserve them.

And the code:

public class Main
{
    public static void main(String... args) {
        for(String arg : args) {
            System.out.println(titleize(arg));
        }
    }

    /**
     * Titleize a string. Takes a string and returns a new string where all words
     * have had their first letter title cased. If the letter is already title case
     * or is not a cased letter (like a number), it will be passed through.
     * Leading, trailing, and all other whitespace is preserved.
     *
     * This method is not as robust as titleize in Rails. It does not do any magic
     * like breaking up MashedTogetherWords or replacing_underscores.
     *
     * This will blow up if null is passed in.
     */
    public static String titleize(final String input) {
        // Initialize the output to the length of the input since we know it and the
        // output and input will be the same size.
        StringBuilder output = new StringBuilder(input.length());
        // This defaults to true because we assume the beginning of the
        // string also counts as whitespace. This is to make sure the
        // first word gets capitalized as well.
        boolean lastCharacterWasWhitespace = true;

        for(int i = 0; i < input.length(); i++) {
            char currentCharacter = input.charAt(i);

            if(lastCharacterWasWhitespace && Character.isLowerCase(currentCharacter)) {
                currentCharacter = Character.toTitleCase(currentCharacter);
            }

            output.append(currentCharacter);

            lastCharacterWasWhitespace = Character.isWhitespace(currentCharacter);
        }

        return output.toString();
    }
}

The original code tried to use regular expressions and splitting words to accomplish the task. This does neither. It was partially inspired by the accepted answer, by treating the String as a stream of characters.

This is Java 7. I'm sure there's some Java 8 stream fanciness that can do this in half the lines.

\$\endgroup\$

2 Answers 2

7
\$\begingroup\$

This is a cool little problem that needs Java8, and regex. ... promise.

First up, the Pattern.splitAsStream(...) method is new in Java8, and converts a string in to a stream of substrings, depending on the pattern, so, what's the pattern?

Well, the pattern is to match the beginning of any word that needs to be capitalized. Your code currently finds characters, after whitespace, but that is, as you admit, too naive. Words that need capitalization are things like:

  • This is 'quoted' -> This Is 'Quoted'
  • This has-joined-words -> This Has-Joined-Words
  • This should not 1800not1337 -> This Should Not 1800not1337

and so on.

So, your code would not deal well with the above examples.... but, how can it be better?

private static final Pattern bound = Pattern.compile("\\b(?=\\w)");

The above regular expression looks for a word-boundary that is followed by a character (where "a character" is a letter, digit, or underscore).

Then, you have a sequence of sentence parts that need captialization, and it can be streamed easily:

private static final Pattern bound = Pattern.compile("\\b(?=\\w)");

private static final String ucFirst(String input) {
    return input.substring(0,  1).toUpperCase() + input.substring(1);
};

public static String titleize(final String input) {
    return bound.splitAsStream(input)
          .map(StringCap::ucFirst)
          .collect(Collectors.joining());
}

You can see this running here: http://ideone.com/ilmBqi


Note: Using Java7 versions of Pattern matchers, it is also actually quite easy.... Consider this pattern, which stores the first-character as a pattern group, and then the "traditional" matching replacement loop:

private static final Pattern bound = Pattern.compile("\\b(\\w)");

public static String titleize(final String input) {
    StringBuffer sb = new StringBuffer(input.length());
    Matcher mat = bound.matcher(input);
    while (mat.find()) {
        mat.appendReplacement(sb, mat.group().toUpperCase());
    }
    mat.appendTail(sb);
    return sb.toString();
}

The above is able to capitalize the words in the same output as the Java8 version above.

\$\endgroup\$
1
  • \$\begingroup\$ I like how you made the method much more robust, even though the original requirements don't specify what happens under the conditions you describe (quoted and such). At the same time, it goes beyond those requirements so I can't actually decide if that makes it more or less correct. I was also right about the Java 8 potential for terseness :). I like that you also include the Java 7 version as well. I didn't realize the Matcher API allowed that level of manipulation. \$\endgroup\$
    – cbojar
    Oct 15, 2015 at 22:21
3
\$\begingroup\$
  1. You can iterate over the characters in the input directly, rather than using a for loop.
  2. Your in-line comments aren't helpful.
  3. Checking if the character is already TitleCase is probably more expensive than just calling TitleCase, and makes the code more complex.
  4. Don't check for LowerCase and then set to TitleCase. Check for not TitleCase and set to TitleCase. That protects against things which aren't currently TitleCase or LowerCase, but can be made into TitleCase.
  5. Your javadoc is conversational rather than professional. "This will blow up", "not as robust as titleize in Rails", etc.
  6. Throwing the NPE on a null input is reasonable, but returning null instead may also be reasonable, depending on whether null is a valid input or not.

Taken in sum,

/**
 * Returns a new copy of the given string where each character immediately after
 * a whitespace character is title-cased. Only works for characters in the
 * <a href="http://docs.oracle.com/javase/7/docs/api/java/lang/Character.html#unicode">
 * <i>Basic Multilingual Plane</i></a>.
 * @param input the string to be title-cased. May not be null.
 * @return a new copy of the given string where each character immediately after
 * a whitespace character is title-cased. Will never return null.
 * @throws NullPointerException if the input parameter is null.
 */
public static String titleize(final String input) {
    Objects.requireNonNull(input, "The input parameter may not be null.");

    final StringBuilder output = new StringBuilder(input.length());
    boolean lastCharacterWasWhitespace = true;

    for (final char currentCharacter : input.toCharArray()) {
        if (lastCharacterWasWhitespace) {
            output.append(Character.toTitleCase(currentCharacter));
        } else {
            output.append(currentCharacter);
        }
        lastCharacterWasWhitespace = Character.isWhitespace(currentCharacter);
    }
    return output.toString();
}
\$\endgroup\$
2
  • \$\begingroup\$ I agree with 3 and 4. I want to agree with 1, but toCharArray creates a defensive copy, thus increasing memory usage so that would be situation- and requirements-dependent. The original requirements don't specify what to do re. 6, so I can't actually answer that one. That's why I just let it break in that case. I like using Objects.requireNonNull there to guard though. I disagree with 2, especially the comments about lastCharacterWasWhitespace. It explains why it is default true in a way that the code cannot. \$\endgroup\$
    – cbojar
    Oct 15, 2015 at 22:27
  • \$\begingroup\$ Re. 5, my javadoc is conversational, and can use some help, but your suggested comments feel repetitive or state the obvious. They do offer some additional information that mine don't convey, and the additional javadoc tags documentation could help to make mine clearer, so I do see your point. \$\endgroup\$
    – cbojar
    Oct 15, 2015 at 22:34

Your Answer

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

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