/**
* StringManipulator.java
* Copyright 2007 - 2008 Zach Scrivena
* zachscrivena@gmail.com
* http://zs.freeshell.org/
*
* TERMS AND CONDITIONS:
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.freeshell.zs.common;
import java.text.NumberFormat;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.ArrayList;
import java.util.List;
/**
* Perform string manipulation.
*/
public class StringManipulator
{
/** default regex delimiter pattern */
private static final String DEFAULT_REGEX_DELIM = "[\\s]++";
/**
* Tokenize the given string, using the specified regex delimiter pattern.
*
* @param in
* String to be tokenized
* @param regexDelim
* Regex delimiter pattern
* @param includeDelim
* If true, then delimiter tokens are returned too;
* otherwise, only non-delimiter tokens are returned
* @return
* Tokens in an array of strings;
* null, if the string to be tokenized is null
*/
public static Token[] tokenize(
final String in,
final String regexDelim,
final boolean includeDelimiters)
{
/* null input string */
if (in == null)
{
return null;
}
/* regex matcher for delimiter */
final Matcher delimiterMatcher = (regexDelim == null) ?
Pattern.compile(DEFAULT_REGEX_DELIM).matcher(in) :
Pattern.compile(regexDelim).matcher(in);
/* return value */
final List<Token> tokens = new ArrayList<Token>();
/* initialize buffer string */
final StringBuilder buffer = new StringBuilder();
/* parse each character in input string */
for (int i = 0; i < in.length(); i++)
{
delimiterMatcher.region(i, in.length());
if (delimiterMatcher.lookingAt())
{
/* found delimiter match starting at this index */
/* add buffer string to tokens if nonempty */
final int bufferLength = buffer.length();
if (bufferLength > 0)
{
tokens.add(new Token(buffer.toString(), false));
buffer.delete(0, bufferLength);
}
if (includeDelimiters)
{
tokens.add(new Token(delimiterMatcher.group(), true));
}
/* advance index by length of the delimiter match */
i += (delimiterMatcher.group().length() - 1);
}
else
{
/* not a match at this index, so we add the char */
/* to the buffer string */
buffer.append(in.charAt(i));
}
}
/* flush buffer string if nonempty */
if (buffer.length() > 0)
{
tokens.add(new Token(buffer.toString(), false));
}
/* return value */
return tokens.toArray(new Token[tokens.size()]);
}
/**
* Extract a substring from a given string, according to the specified
* format.
*
* @param in
* String from which the substring is to be extracted
* @param format
* Format string describing the sequence of characters in the substring
* @param rangeChar
* Range character to be used
* @param delimChar
* Delimiter character to be used
* @return
* Substring of the given string
*/
public static String substring(
final String in,
final String format,
final char rangeChar,
final char delimChar)
{
/* length of input string */
final int len = in.length();
/* nothing to do for empty string */
if (len == 0)
{
return in;
}
/* tokenize format string by delimiter character, e.g. ',' */
final String[] tokens = format.split("[\\" + delimChar + "]++");
/* return value */
final StringBuilder out = new StringBuilder();
/* Regex pattern for nonzero integers */
final Pattern nonZeroIntegerPattern =
Pattern.compile("\\s*([\\+\\-]?[1-9][0-9]*)\\s*");
/* process each token */
for (int i = 0; i < tokens.length; i++)
{
/* split betwen range characters, e.g. ':' */
final String[] entries = tokens[i].split("[\\" + rangeChar + "]++");
int[] indices = new int[entries.length];
/* check if entries are all non-zero integers */
for (int j = 0; j < entries.length; j++)
{
final String entry = entries[j];
if (nonZeroIntegerPattern.matcher(entry).matches())
{
/* convert to int */
indices[j] = Integer.parseInt(entry);
}
else
{
/* error; not a nonzero integer */
return null;
}
}
if (indices.length == 1)
{
/* format "a" */
indices[0] = normalizeIndex(len, indices[0]);
out.append(in.charAt(indices[0] - 1));
}
else if (indices.length == 2)
{
/* format "a:b" */
indices[0] = normalizeIndex(len, indices[0]);
indices[1] = normalizeIndex(len, indices[1]);
final int delta = (indices[0] <= indices[1]) ? 1 : -1;
for (int k = indices[0]; ; k += delta)
{
if (delta > 0)
{
if (k > indices[1]) break;
}
else
{
if (k < indices[1]) break;
}
out.append(in.charAt(k - 1));
}
}
else if (indices.length == 3)
{
/* format "a:b:c" */
indices[0] = normalizeIndex(len, indices[0]);
indices[2] = normalizeIndex(len, indices[2]);
if ((indices[2] - indices[0]) * indices[1] < 0)
{
continue;
}
for (int k = indices[0]; ; k += indices[1])
{
if (indices[1] > 0)
{
if (k > indices[2]) break;
}
else
{
if (k < indices[2]) break;
}
out.append(in.charAt(k - 1));
}
}
else
{
/* invalid format string */
return null;
}
}
return out.toString();
}
/**
* Normalize user-specified index value.
*
* @param len
* Length of the source string
* @param index
* User-specified index value
* @return
* Normalized index value
*/
private static int normalizeIndex(
final int len,
final int index)
{
/* clip index to [1,len] */
int newIndex = index;
if (newIndex < 0)
{
newIndex += (len + 1);
}
if (newIndex < 1)
{
newIndex = 1;
}
if (newIndex > len)
{
newIndex = len;
}
return newIndex;
}
/**
* Return a formatted string representation of a given long number
* (format is locale-sensitive).
*
* @param n
* Long number to be formatted
* @return
* Formatted string representation of the given long number
*/
public static String formattedLong(
final long n)
{
final NumberFormat nf = NumberFormat.getNumberInstance();
nf.setGroupingUsed(true);
try
{
return nf.format(n);
}
catch (Exception e)
{
return (n + "");
}
}
/**
* Return a formatted string representation of a given double number
* (format is locale-sensitive).
*
* @param n
* Double number to be formatted
* @return
* Formatted string representation of the given double number
*/
public static String formattedDouble(
final double n)
{
final NumberFormat nf = NumberFormat.getNumberInstance();
nf.setGroupingUsed(true);
try
{
return nf.format(n);
}
catch (Exception e)
{
return (n + "");
}
}
/**
* Center-justify the string representation of a given object, padding with
* leading and trailing spaces so that its length is at least the specified
* width.
*
* @param o
* Object to be center-justified
* @param width
* Width of the resulting center-justified string
* @return
* Center-justified string representation
*/
public static String centerJustify(
final Object o,
final int width)
{
final String s = o + "";
final int len = s.length();
final int totalSpace = width - len;
if (totalSpace <= 0)
{
return s;
}
final StringBuilder t = new StringBuilder();
final int leadingSpace = totalSpace / 2;
for (int i = 0; i < leadingSpace; i++)
{
t.append(' ');
}
t.append(s);
for (int i = 0; i < totalSpace - leadingSpace; i++)
{
t.append(' ');
}
return t.toString();
}
/**
* Left-justify the string representation of a given object, padding with
* trailing spaces so that its length is at least the specified width.
*
* @param o
* Object for to be left-justified
* @param width
* Width of the resulting left-justified string
* @return
* Left-justified string representation
*/
public static String leftJustify(
final Object o,
final int width)
{
final String s = o + "";
final int len = s.length();
final int totalSpace = width - len;
if (totalSpace <= 0)
{
return s;
}
final StringBuilder t = new StringBuilder();
for (int i = 0; i < totalSpace; i++)
{
t.append(' ');
}
t.append(s);
return t.toString();
}
/**
* Right-justify the string representation of a given object, padding with
* leading spaces so that its length is at least the specified width.
*
* @param o
* Object to be right-justified
* @param width
* Width of the resulting right-justified string
* @return
* Right-justified string representation
*/
public static String rightJustify(
final Object o,
final int width)
{
final String s = o + "";
final int len = s.length();
final int totalSpace = width - len;
if (totalSpace <= 0)
{
return s;
}
final StringBuilder t = new StringBuilder(s);
for (int i = 0; i < totalSpace; i++)
{
t.append(' ');
}
return t.toString();
}
/**
* Repeat the string representation of a given object, a specified number
* of times.
*
* @param o
* Object to be repeated
* @param n
* Number of times to repeat
* @return
* Repeated string representation
*/
public static String repeat(
final Object o,
final int n)
{
final String s = o + "";
final StringBuilder t = new StringBuilder();
for (int i = 0; i < n; i++)
{
t.append(s);
}
return t.toString();
}
/**
* Convert a specified string to title case, by capitalizing only the
* first letter of each word.
*
* @param s
* Input string
* @return
* Output string
*/
public static String toTitleCase(
final String s)
{
final Token[] tokens = tokenize(s, "[\\s\\p{Punct}]++", true);
final StringBuilder t = new StringBuilder();
for (Token token : tokens)
{
if (token.val.isEmpty())
{
continue;
}
if (token.isDelimiter)
{
t.append(token.val);
}
else
{
t.append(Character.toUpperCase(token.val.charAt(0)));
t.append(token.val.substring(1).toLowerCase());
}
}
return t.toString();
}
/**
* Abbreviate a specified string, by keeping only the first letter
* of each word.
*
* @param s
* Input string
* @return
* Output string
*/
public static String abbreviate(
final String s)
{
final Token[] tokens = tokenize(s, "[\\s\\p{Punct}]++", true);
final StringBuilder t = new StringBuilder();
for (Token token : tokens)
{
if (token.val.isEmpty())
{
continue;
}
if (token.isDelimiter)
{
t.append(token.val);
}
else
{
t.append(token.val.charAt(0));
}
}
return t.toString();
}
/**
* Reverse the string.
*
* @param s
* Input string
* @return
* Output string
*/
public static String reverse(
final String s)
{
return (new StringBuilder(s)).reverse().toString();
}
/**
* Trim away whitespace on the left.
*
* @param s
* Input string
* @return
* Output string
*/
public static String leftTrim(
final String s)
{
final Matcher m = Pattern.compile("[\\s]++(.*)").matcher(s);
if (m.matches())
{
return m.group(1);
}
else
{
return s;
}
}
/**
* Trim away whitespace on the right.
*
* @param s
* Input string
* @return
* Output string
*/
public static String rightTrim(
final String s)
{
return reverse(leftTrim(reverse(s)));
}
/**
* Delete extra whitespace in a specified string, by replacing contiguous
* whitespace characters with a single space.
*
* @param s
* Input string
* @return
* Output string
*/
public static String deleteExtraWhitespace(
final String s)
{
final String[] tokens = s.split("[\\s]++");
final StringBuilder t = new StringBuilder();
for (int i = 0; i < tokens.length - 1; i++)
{
if (!tokens[i].isEmpty())
{
t.append(tokens[i]);
t.append(' ');
}
}
t.append(tokens[tokens.length - 1]);
return t.toString();
}
/**
* Delete whitespace in a specified string, by deleting all whitespace
* characters.
*
* @param s
* Input string
* @return
* Output string
*/
public static String deleteWhitespace(
final String s)
{
final String[] tokens = s.split("[\\s]++");
final StringBuilder t = new StringBuilder();
for (String token : tokens)
{
t.append(token);
}
return t.toString();
}
/**
* Delete punctuation in a specified string, by deleting all punctuation
* characters.
*
* @param s
* Input string
* @return
* Output string
*/
public static String deletePunctuation(
final String s)
{
final String[] tokens = s.split("[\\p{Punct}]++");
final StringBuilder t = new StringBuilder();
for (String token : tokens)
{
t.append(token);
}
return t.toString();
}
/**
* Space out words in a specified string, by inserting a single space
* between concatenated words.
*
* @param s
* Input string
* @return
* Output string
*/
public static String spaceOutWords(
final String s)
{
final StringBuilder t = new StringBuilder();
for (int i = 0; i < s.length(); i++)
{
final char c = s.charAt(i);
if (Character.isLowerCase(c) &&
(i + 1 < s.length()) &&
Character.isUpperCase(s.charAt(i + 1)))
{
t.append(c);
t.append(' ');
}
else if (Character.isUpperCase(c) &&
(i - 1 >= 0) &&
Character.isUpperCase(s.charAt(i - 1)) &&
(i + 1 < s.length()) &&
Character.isLowerCase(s.charAt(i + 1)))
{
t.append(' ');
t.append(c);
}
else
{
t.append(c);
}
}
return t.toString();
}
/**
* Swap the case of a specified string, by converting lower case
* characters to upper case and vice versa.
*
* @param s
* Input string
* @return
* Output string
*/
public static String swapCase(
final String s)
{
final StringBuilder t = new StringBuilder();
for (int i = 0; i < s.length(); i++)
{
final char c = s.charAt(i);
if (Character.isLowerCase(c))
{
t.append(Character.toUpperCase(c));
}
else if (Character.isUpperCase(c))
{
t.append(Character.toLowerCase(c));
}
else
{
t.append(c);
}
}
return t.toString();
}
/**
* Return a modified version of the input string with the specified
* strings trimmed off from its tail, if found. The returned string is
* guaranteed not to end with any of the specified strings.
*
* @param s
* Input string
* @param trimStrings
* Strings to be trimmed off from the end of the input string;
* empty strings are ignored
* @return
* Modified version of input string
*/
public static String trimTrailingStrings(
final String s,
final String[] trimStrings)
{
/* resultant string (to be trimmed) */
final StringBuilder result = new StringBuilder(s);
while (true)
{
boolean modified = false;
for (String trim : trimStrings)
{
final int trimLength = trim.length();
/* ignore empty strings */
if (trimLength == 0)
{
continue;
}
TrimString:
while (true)
{
final int resultLength = result.length();
if ((resultLength >= trimLength) &&
result.substring(resultLength - trimLength).equals(trim))
{
/* trim trailing string */
result.delete(resultLength - trimLength, resultLength);
modified = true;
}
else
{
break TrimString;
}
}
}
if (!modified)
{
break;
}
}
return result.toString();
}
/**
* Return a modified version of the input string with the specified
* strings trimmed off from its head, if found. The returned string is
* guaranteed not to start with any of the specified strings.
*
* @param s
* Input string
* @param trimStrings
* Strings to be trimmed off from the head of the input string;
* empty strings are ignored
* @return
* Modified version of input string
*/
public static String trimLeadingStrings(
final String s,
final String[] trimStrings)
{
/* resultant string (to be trimmed) */
final StringBuilder result = new StringBuilder(s);
while (true)
{
boolean modified = false;
for (String trim : trimStrings)
{
final int trimLength = trim.length();
/* ignore empty strings */
if (trimLength == 0)
{
continue;
}
TrimString:
while (true)
{
final int resultLength = result.length();
if ((resultLength >= trimLength) &&
result.substring(0, trimLength).equals(trim))
{
/* trim leading string */
result.delete(0, trimLength);
modified = true;
}
else
{
break TrimString;
}
}
}
if (!modified)
{
break;
}
}
return result.toString();
}
/**
* Return a modified version of the input string with the specified
* strings trimmed off from its head and tail, if found. The returned
* string is guaranteed not to start or end with any of the specified
* strings.
*
* @param s
* Input string
* @param trimStrings
* Strings to be trimmed off from the head and tail of the input
* string; empty strings are ignored
* @return
* Modified version of input string
*/
public static String trimStrings(
final String s,
final String[] trimStrings)
{
return trimLeadingStrings(trimTrailingStrings(s, trimStrings), trimStrings);
}
/**
* Parse the specified "key:value" string.
* The key string is the trimmed string before the first colon (:).
* The value string is the trimmed string after the first colon (:);
* if this is enclosed in double quotes ("), then the value string is the string between them.
* If the specified string does not contain a colon (:), then the key string is the entire
* string trimmed, and the value string is the empty string.
*
* @param s
* string to be parsed
* @return
* String array containing two elements: the first is the key string,
* the second is the value string
*/
public static String[] parseKeyValueString(
final String s)
{
final String[] t = new String[2];
final int k = s.indexOf(":");
if (k >= 0)
{
t[0] = s.substring(0, k).trim();
t[1] = s.substring(k + 1).trim();
final int leftQuote = t[1].indexOf("\"");
final int rightQuote = t[1].lastIndexOf("\"");
if ((leftQuote == 0) &&
(rightQuote == (t[1].length() - 1)) &&
(leftQuote < rightQuote))
{
t[1] = t[1].substring(1, rightQuote);
}
}
else
{
t[0] = s.trim();
t[1] = "";
}
return t;
}
/*****************
* INNER CLASSES *
***t**************/
/**
* Represent a string token.
*/
public static class Token
{
public String val;
public boolean isDelimiter;
public Token(String val, boolean isDelimiter)
{
this.val = val;
this.isDelimiter = isDelimiter;
}
}
}