    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 java.util.HashSet;
    020import java.util.Set;
    021
    022import org.apache.commons.lang3.ArrayUtils;
    023import org.apache.commons.lang3.StringUtils;
    024
    025/**
    026 * <p>Case manipulation operations on Strings that contain words.</p>
    027 *
    028 * <p>This class tries to handle {@code null} input gracefully.
    029 * An exception will not be thrown for a {@code null} input.
    030 * Each method documents its behavior in more detail.</p>
    031 *
    032 * @since 1.2
    033 */
    034public class CaseUtils {
    035
    036    /**
    037     * <p>{@code CaseUtils} instances should NOT be constructed in
    038     * standard programming. Instead, the class should be used as
    039     * {@code CaseUtils.toCamelCase("foo bar", true, new char[]{'-'});}.</p>
    040     *
    041     * <p>This constructor is public to permit tools that require a JavaBean
    042     * instance to operate.</p>
    043     */
    044    public CaseUtils() {
    045        super();
    046    }
    047
    048    /**
    049     * <p>Converts all the delimiter separated words in a String into camelCase,
    050     * that is each word is made up of a title case character and then a series of
    051     * lowercase characters.</p>
    052     *
    053     * <p>The delimiters represent a set of characters understood to separate words.
    054     * The first non-delimiter character after a delimiter will be capitalized. The first String
    055     * character may or may not be capitalized and it's determined by the user input for capitalizeFirstLetter
    056     * variable.</p>
    057     *
    058     * <p>A {@code null} input String returns {@code null}.
    059     * Capitalization uses the Unicode title case, normally equivalent to
    060     * upper case and cannot perform locale-sensitive mappings.</p>
    061     *
    062     * <pre>
    063     * CaseUtils.toCamelCase(null, false)                                 = null
    064     * CaseUtils.toCamelCase("", false, *)                                = ""
    065     * CaseUtils.toCamelCase(*, false, null)                              = *
    066     * CaseUtils.toCamelCase(*, true, new char[0])                        = *
    067     * CaseUtils.toCamelCase("To.Camel.Case", false, new char[]{'.'})     = "toCamelCase"
    068     * CaseUtils.toCamelCase(" to @ Camel case", true, new char[]{'@'})   = "ToCamelCase"
    069     * CaseUtils.toCamelCase(" @to @ Camel case", false, new char[]{'@'}) = "toCamelCase"
    070     * </pre>
    071     *
    072     * @param str  the String to be converted to camelCase, may be null
    073     * @param capitalizeFirstLetter boolean that determines if the first character of first word should be title case.
    074     * @param delimiters  set of characters to determine capitalization, null and/or empty array means whitespace
    075     * @return camelCase of String, {@code null} if null String input
    076     */
    077    public static String toCamelCase(String str, final boolean capitalizeFirstLetter, final char... delimiters) {
    078        if (StringUtils.isEmpty(str)) {
    079            return str;
    080        }
    081        str = str.toLowerCase();
    082        final int strLen = str.length();
    083        final int[] newCodePoints = new int[strLen];
    084        int outOffset = 0;
    085        final Set<Integer> delimiterSet = generateDelimiterSet(delimiters);
    086        boolean capitalizeNext = false;
    087        if (capitalizeFirstLetter) {
    088            capitalizeNext = true;
    089        }
    090        for (int index = 0; index < strLen;) {
    091            final int codePoint = str.codePointAt(index);
    092
    093            if (delimiterSet.contains(codePoint)) {
    094                capitalizeNext = outOffset != 0;
    095                index += Character.charCount(codePoint);
    096            } else if (capitalizeNext || outOffset == 0 && capitalizeFirstLetter) {
    097                final int titleCaseCodePoint = Character.toTitleCase(codePoint);
    098                newCodePoints[outOffset++] = titleCaseCodePoint;
    099                index += Character.charCount(titleCaseCodePoint);
    100                capitalizeNext = false;
    101            } else {
    102                newCodePoints[outOffset++] = codePoint;
    103                index += Character.charCount(codePoint);
    104            }
    105        }
    106        if (outOffset != 0) {
    107            return new String(newCodePoints, 0, outOffset);
    108        }
    109        return str;
    110    }
    111
    112    /**
    113     * <p>Converts an array of delimiters to a hash set of code points. Code point of space(32) is added
    114     * as the default value. The generated hash set provides O(1) lookup time.</p>
    115     *
    116     * @param delimiters  set of characters to determine capitalization, null means whitespace
    117     * @return Set<Integer>
    118     */
    119    private static Set<Integer> generateDelimiterSet(final char[] delimiters) {
    120        final Set<Integer> delimiterHashSet = new HashSet<>();
    121        delimiterHashSet.add(Character.codePointAt(new char[]{' '}, 0));
    122        if (ArrayUtils.isEmpty(delimiters)) {
    123            return delimiterHashSet;
    124        }
    125
    126        for (int index = 0; index < delimiters.length; index++) {
    127            delimiterHashSet.add(Character.codePointAt(delimiters, index));
    128        }
    129        return delimiterHashSet;
    130    }
    131}
    132