001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.text;
018
019import org.apache.commons.lang3.StringUtils;
020
021import java.util.HashSet;
022import java.util.Set;
023
024/**
025 * <p>Case manipulation operations on Strings that contain words.</p>
026 *
027 * <p>This class tries to handle <code>null</code> input gracefully.
028 * An exception will not be thrown for a <code>null</code> input.
029 * Each method documents its behaviour in more detail.</p>
030 *
031 * @since 1.2
032 */
033public class CaseUtils {
034
035    /**
036     * <p><code>CaseUtils</code> instances should NOT be constructed in
037     * standard programming. Instead, the class should be used as
038     * <code>CaseUtils.toCamelCase("foo bar", true, new char[]{'-'});</code>.</p>
039     *
040     * <p>This constructor is public to permit tools that require a JavaBean
041     * instance to operate.</p>
042     */
043    public CaseUtils() {
044        super();
045    }
046
047    /**
048     * <p>Converts all the delimiter separated words in a String into camelCase,
049     * that is each word is made up of a titlecase character and then a series of
050     * lowercase characters.</p>
051     *
052     * <p>The delimiters represent a set of characters understood to separate words.
053     * The first non-delimiter character after a delimiter will be capitalized. The first String
054     * character may or may not be capitalized and it's determined by the user input for capitalizeFirstLetter
055     * variable.</p>
056     *
057     * <p>A <code>null</code> input String returns <code>null</code>.
058     * Capitalization uses the Unicode title case, normally equivalent to
059     * upper case and cannot perform locale-sensitive mappings.</p>
060     *
061     * <pre>
062     * CaseUtils.toCamelCase(null, false)                                 = null
063     * CaseUtils.toCamelCase("", false, *)                                = ""
064     * CaseUtils.toCamelCase(*, false, null)                              = *
065     * CaseUtils.toCamelCase(*, true, new char[0])                        = *
066     * CaseUtils.toCamelCase("To.Camel.Case", false, new char[]{'.'})     = "toCamelCase"
067     * CaseUtils.toCamelCase(" to @ Camel case", true, new char[]{'@'})   = "ToCamelCase"
068     * CaseUtils.toCamelCase(" @to @ Camel case", false, new char[]{'@'}) = "toCamelCase"
069     * </pre>
070     *
071     * @param str  the String to be converted to camelCase, may be null
072     * @param capitalizeFirstLetter boolean that determines if the first character of first word should be title case.
073     * @param delimiters  set of characters to determine capitalization, null and/or empty array means whitespace
074     * @return camelCase of String, <code>null</code> if null String input
075     */
076    public static String toCamelCase(String str, final boolean capitalizeFirstLetter, final char... delimiters) {
077        if (StringUtils.isEmpty(str)) {
078            return str;
079        }
080        str = str.toLowerCase();
081        final int strLen = str.length();
082        final int[] newCodePoints = new int[strLen];
083        int outOffset = 0;
084        final Set<Integer> delimiterSet = generateDelimiterSet(delimiters);
085        boolean capitalizeNext = false;
086        if (capitalizeFirstLetter) {
087            capitalizeNext = true;
088        }
089        for (int index = 0; index < strLen;) {
090            final int codePoint = str.codePointAt(index);
091
092            if (delimiterSet.contains(codePoint)) {
093                capitalizeNext = true;
094                if (outOffset == 0) {
095                    capitalizeNext = false;
096                }
097                index += Character.charCount(codePoint);
098            } else if (capitalizeNext || outOffset == 0 && capitalizeFirstLetter) {
099                final int titleCaseCodePoint = Character.toTitleCase(codePoint);
100                newCodePoints[outOffset++] = titleCaseCodePoint;
101                index += Character.charCount(titleCaseCodePoint);
102                capitalizeNext = false;
103            } else {
104                newCodePoints[outOffset++] = codePoint;
105                index += Character.charCount(codePoint);
106            }
107        }
108        if (outOffset != 0) {
109            return new String(newCodePoints, 0, outOffset);
110        }
111        return str;
112    }
113
114    /**
115     * <p>Converts an array of delimiters to a hash set of code points. Code point of space(32) is added
116     * as the default value. The generated hash set provides O(1) lookup time.</p>
117     *
118     * @param delimiters  set of characters to determine capitalization, null means whitespace
119     * @return Set<Integer>
120     */
121    private static Set<Integer> generateDelimiterSet(final char[] delimiters) {
122        final Set<Integer> delimiterHashSet = new HashSet<>();
123        delimiterHashSet.add(Character.codePointAt(new char[]{' '}, 0));
124        if (delimiters == null || delimiters.length == 0) {
125            return delimiterHashSet;
126        }
127
128        for (int index = 0; index < delimiters.length; index++) {
129            delimiterHashSet.add(Character.codePointAt(delimiters, index));
130        }
131        return delimiterHashSet;
132    }
133}
134