// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.collide.shared.util;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.json.shared.JsonIntegerMap;
import javax.annotation.Nonnull;
/**
* Utility methods for string operations.
*
*/
public class StringUtils {
/**
* Map [N] -> string of N spaces. Used by {@link #getSpaces} to cache strings
* of spaces.
*/
private static final JsonIntegerMap<String> cachedSpaces = JsonCollections.createIntegerMap();
/**
* Interface that defines string utility methods used by shared code but have
* differing client and server implementations.
*/
public interface Implementation {
JsonArray<String> split(String string, String separator);
}
private static class PureJavaImplementation implements Implementation {
@Override
public JsonArray<String> split(String string, String separator) {
JsonArray<String> result = JsonCollections.createArray();
int sepLength = separator.length();
if (sepLength == 0) {
for (int i = 0, n = string.length(); i < n; i++) {
result.add(string.substring(i, i + 1));
}
return result;
}
int position = 0;
while (true) {
int index = string.indexOf(separator, position);
if (index == -1) {
result.add(string.substring(position));
return result;
}
result.add(string.substring(position, index));
position = index + sepLength;
}
}
}
/**
* By default, this is a pure java implementation, but can be set to a more
* optimized version by the client
*/
private static Implementation implementation = new PureJavaImplementation();
/**
* Sets the implementation for methods
*/
public static void setImplementation(Implementation implementation) {
StringUtils.implementation = implementation;
}
/**
* @return largest n such that
* {@code string1.substring(0, n).equals(string2.substring(0, n))}
*/
public static int findCommonPrefixLength(String string1, String string2) {
int limit = Math.min(string1.length(), string2.length());
int result = 0;
while (result < limit) {
if (string2.charAt(result) != string1.charAt(result)) {
break;
}
result++;
}
return result;
}
public static int countNumberOfOccurrences(String s, String pattern) {
int count = 0;
int i = 0;
while ((i = s.indexOf(pattern, i)) >= 0) {
count++;
i += pattern.length();
}
return count;
}
/**
* Check if a String ends with a specified suffix, ignoring case
*
* @param s the String to check, may be null
* @param suffix the suffix to find, may be null
* @return true if s ends with suffix or both s and suffix are null, false
* otherwise.
*/
public static boolean endsWithIgnoreCase(String s, String suffix) {
if (s == null || suffix == null) {
return (s == null && suffix == null);
}
if (suffix.length() > s.length()) {
return false;
}
return s.regionMatches(true, s.length() - suffix.length(), suffix, 0, suffix.length());
}
public static boolean looksLikeImage(String path) {
String lowercase = path.toLowerCase();
return lowercase.endsWith(".jpg") || lowercase.endsWith(".jpeg") || lowercase.endsWith(".ico")
|| lowercase.endsWith(".png") || lowercase.endsWith(".gif");
}
public static <T> String join(T[] items, String separator) {
StringBuilder s = new StringBuilder();
for (int i = 0; i < items.length; i++) {
s.append(items[i]).append(separator);
}
s.setLength(s.length() - separator.length());
return s.toString();
}
public static <T> String join(JsonArray<T> items, String separator) {
StringBuilder s = new StringBuilder();
for (int i = 0; i < items.size(); i++) {
s.append(items.get(i)).append(separator);
}
s.setLength(s.length() - separator.length());
return s.toString();
}
/**
* Check that string starts with specified prefix.
*
* <p>If {@code caseInsensitive == false} this check is equivalent
* to {@link String#startsWith(String)}.
*
* <p>Otherwise {@code prefix} should be lower-case and check ignores
* case of {@code string}.
*/
public static boolean startsWith(
@Nonnull String prefix, @Nonnull String string, boolean caseInsensitive) {
if (caseInsensitive) {
int prefixLength = prefix.length();
if (string.length() < prefixLength) {
return false;
}
return prefix.equals(string.substring(0, prefixLength).toLowerCase());
} else {
return string.startsWith(prefix);
}
}
/**
* @return the length of the starting whitespace for the line, or the string
* length if it is all whitespace
*/
public static int lengthOfStartingWhitespace(String s) {
int n = s.length();
for (int i = 0; i < n; i++) {
char c = s.charAt(i);
// TODO: This currently only deals with ASCII whitespace.
// Read until a non-space
if (c != ' ' && c != '\t') {
return i;
}
}
return n;
}
/**
* @return first character in the string that is not a whitespace, or
* {@code 0} if there is no such characters
*/
public static char firstNonWhitespaceCharacter(String s) {
for (int i = 0, n = s.length(); i < n; ++i) {
char c = s.charAt(i);
if (!isWhitespace(c)) {
return c;
}
}
return 0;
}
/**
* @return last character in the string that is not a whitespace, or
* {@code 0} if there is no such characters
*/
public static char lastNonWhitespaceCharacter(String s) {
for (int i = s.length() - 1; i >= 0; --i) {
char c = s.charAt(i);
if (!isWhitespace(c)) {
return c;
}
}
return 0;
}
public static String nullToEmpty(String s) {
return s == null ? "" : s;
}
public static boolean isNullOrEmpty(String s) {
return s == null || "".equals(s);
}
public static boolean isNullOrWhitespace(String s) {
return s == null || "".equals(s.trim());
}
public static String trimNullToEmpty(String s) {
return s == null ? "" : s.trim();
}
public static String ensureNotEmpty(String s, String defaultStr) {
return isNullOrEmpty(s) ? defaultStr : s;
}
public static boolean isWhitespace(char ch) {
return (ch <= ' ');
}
public static boolean isAlpha(char ch) {
return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z');
}
public static boolean isNumeric(char ch) {
return ('0' <= ch && ch <= '9');
}
public static boolean isQuote(char ch) {
return ch == '\'' || ch == '\"';
}
public static boolean isAlphaNumOrUnderscore(char ch) {
return isAlpha(ch) || isNumeric(ch) || ch == '_';
}
public static long toLong(String longStr) {
return longStr == null ? 0 : Long.parseLong(longStr);
}
/**
* @return true, if both strings are empty or {@code null}, or if they are
* equal
*/
public static boolean equalStringsOrEmpty(String a, String b) {
return nullToEmpty(a).equals(nullToEmpty(b));
}
/**
* @return true, if the strings are not empty or {@code null}, and equal
*/
public static boolean equalNonEmptyStrings(String a, String b) {
return !isNullOrEmpty(a) && a.equals(b);
}
/**
* Splits with the contract of the contract of the JavaScript String.split().
*
* <p>More specifically: empty segments will be produced for adjacent and
* trailing separators.
*
* <p>If an empty string is used as the separator, the string is split
* between each character.
*
* <p>Examples:<ul>
* <li>{@code split("a", "a")} should produce {@code ["", ""]}
* <li>{@code split("a\n", "\n")} should produce {@code ["a", ""]}
* <li>{@code split("ab", "")} should produce {@code ["a", "b"]}
* </ul>
*/
public static JsonArray<String> split(String s, String separator) {
return implementation.split(s, separator);
}
/**
* @return the number of editor lines this text would take up
*/
public static int countNumberOfVisibleLines(String text) {
return countNumberOfOccurrences(text, "\n") + (text.endsWith("\n") ? 0 : 1);
}
public static String capitalizeFirstLetter(String s) {
if (!isNullOrEmpty(s)) {
s = s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase();
}
return s;
}
public static String ensureStartsWith(String s, String startText) {
if (isNullOrEmpty(s)) {
return startText;
} else if (!s.startsWith(startText)) {
return startText + s;
} else {
return s;
}
}
/**
* Like {@link String#substring(int)} but allows for the {@code count}
* parameter to extend past the string's bounds.
*/
public static String substringGuarded(String s, int position, int count) {
int sLength = s.length();
if (sLength - position <= count) {
return position == 0 ? s : s.substring(position);
} else {
return s.substring(position, position + count);
}
}
public static String repeatString(String s, int count) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < count; i++) {
builder.append(s);
}
return builder.toString();
}
/**
* Gets a {@link String} consisting of the given number of spaces.
*
* <p>NB: The result is cached in {@link #cachedSpaces}.
*
* @param size the number of spaces
* @return a {@link String} consisting of {@code size} spaces
*/
public static String getSpaces(int size) {
if (cachedSpaces.hasKey(size)) {
return cachedSpaces.get(size);
}
char[] fill = new char[size];
for (int i = 0; i < size; i++) {
fill[i] = ' ';
}
String spaces = new String(fill);
cachedSpaces.put(size, spaces);
return spaces;
}
/**
* If this given string is of length {@code maxLength} or less, it will
* be returned as-is.
* Otherwise it will be trucated to {@code maxLength}, regardless of whether
* there are any space characters in the String. If an ellipsis is requested
* to be appended to the truncated String, the String will be truncated so
* that the ellipsis will also fit within maxLength.
* If no truncation was necessary, no ellipsis will be added.
* @param source the String to truncate if necessary
* @param maxLength the maximum number of characters to keep
* @param addEllipsis if true, and if the String had to be truncated,
* add "..." to the end of the String before returning. Additionally,
* the ellipsis will only be added if maxLength is greater than 3.
* @return the original string if it's length is less than or equal to
* maxLength, otherwise a truncated string as mentioned above
*/
public static String truncateAtMaxLength(String source, int maxLength,
boolean addEllipsis) {
if (source.length() <= maxLength) {
return source;
}
if (addEllipsis && maxLength > 3) {
return unicodePreservingSubstring(source, 0, maxLength - 3) + "...";
}
return unicodePreservingSubstring(source, 0, maxLength);
}
/**
* Normalizes {@code index} such that it respects Unicode character
* boundaries in {@code str}.
*
* <p>If {@code index} is the low surrogate of a unicode character,
* the method returns {@code index - 1}. Otherwise, {@code index} is
* returned.
*
* <p>In the case in which {@code index} falls in an invalid surrogate pair
* (e.g. consecutive low surrogates, consecutive high surrogates), or if
* if it is not a valid index into {@code str}, the original value of
* {@code index} is returned.
*
* @param str the String
* @param index the index to be normalized
* @return a normalized index that does not split a Unicode character
*/
private static int unicodePreservingIndex(String str, int index) {
if (index > 0 && index < str.length()) {
if (Character.isHighSurrogate(str.charAt(index - 1)) &&
Character.isLowSurrogate(str.charAt(index))) {
return index - 1;
}
}
return index;
}
/**
* Returns a substring of {@code str} that respects Unicode character
* boundaries.
*
* <p>The string will never be split between a [high, low] surrogate pair,
* as defined by {@link Character#isHighSurrogate} and
* {@link Character#isLowSurrogate}.
*
* <p>If {@code begin} or {@code end} are the low surrogate of a unicode
* character, it will be offset by -1.
*
* <p>This behavior guarantees that
* {@code str.equals(StringUtil.unicodePreservingSubstring(str, 0, n) +
* StringUtil.unicodePreservingSubstring(str, n, str.length())) } is
* true for all {@code n}.
* </pre>
*
* <p>This means that unlike {@link String#substring(int, int)}, the length of
* the returned substring may not necessarily be equivalent to
* {@code end - begin}.
*
* @param str the original String
* @param begin the beginning index, inclusive
* @param end the ending index, exclusive
* @return the specified substring, possibly adjusted in order to not
* split unicode surrogate pairs
* @throws IndexOutOfBoundsException if the {@code begin} is negative,
* or {@code end} is larger than the length of {@code str}, or
* {@code begin} is larger than {@code end}
*/
private static String unicodePreservingSubstring(
String str, int begin, int end) {
return str.substring(unicodePreservingIndex(str, begin),
unicodePreservingIndex(str, end));
}
}