/*
 * @(#)Strings.java    2.5.0   12 September 2006
 *
 * Copyright 2006
 * College of Computer and Information Science
 * Northeastern University
 * Boston, MA  02115
 *
 * The Java Power Tools software may be used for educational
 * purposes as long as this copyright notice is retained intact
 * at the top of all source files.
 *
 * To discuss possible commercial use of this software, 
 * contact Richard Rasala at Northeastern University, 
 * College of Computer and Information Science,
 * 617-373-2462 or rasala@ccs.neu.edu.
 *
 * The Java Power Tools software has been designed and built
 * in collaboration with Viera Proulx and Jeff Raab.
 *
 * Should this software be modified, the words "Modified from 
 * Original" must be included as a comment below this notice.
 *
 * All publication rights are retained.  This software or its 
 * documentation may not be published in any media either
 * in whole or in part without explicit permission.
 *
 * This software was created with support from Northeastern 
 * University and from NSF grant DUE-9950829.
 */

package edu.neu.ccs;

import edu.neu.ccs.codec.*;

import java.util.*;
import java.text.ParseException;

/**
 * <p>Class <code>Strings</code> contains a set of static methods
 * to provide fast and human-readable encoding of an array of
 * <code>String</code> data together with corresponding tools
 * for decoding such data.</p>
 * 
 * <p>Class <code>Strings</code> also contains miscellaneous
 * <code>String</code> utilities.</p>
 *
 * <p>An encoded string will have one of the following forms that
 * we will refer to as a <i>string group</i>:
 *
 * <ul>
 *   <li><code>[s0;s1;...]</code></li>
 *   <li><code>(s0,s1,...)</code></li>
 *   <li><code>{s0|s1|...}</code></li>
 * </ul>
 *
 * <p>A <i>string group</i> enclosed in <code>[]</code> uses the
 * character <code>;</code> as its string separator.</p>
 *
 * <p>A <i>string group</i> enclosed in <code>()</code> uses the
 * character <code>,</code> as its string separator.</p>
 *
 * <p>A <i>string group</i> enclosed in <code>{}</code> uses the
 * character <code>|</code> as its string separator.</p>
 *
 * <p>The encoding methods in this class are <b>NOT</b> 100% robust
 * since the decoding process will fail if any encoded string contains
 * the separator character used for the encoding.  To guarantee robust
 * encoding, use the encoding methods in <code>CodecUtilities</code>.</p>
 *
 * <p>Despite this consideration, these methods of encoding and
 * decoding are quite valuable in the situations where what is
 * to be encoded refers to data that should not contain the
 * particular separator being used.  It is for this reason that
 * three forms of encoding are provided with three separators.</p>
 *
 * <p>The method <code>decodeStringGroup</code> directly looks for the
 * type of encoding performed by the encoding methods in this class.
 * The more general method <code>decode</code> tries 3 algorithms in
 * turn including <code>CodecUtilities.decode</code> to see if the
 * given string has been encoded in any of 3 possible ways.  It is
 * recommended that the method <code>decode</code> in this class be
 * used for general decoding of a string into a string array.</p>
 *
 * <p>As a helpful support tool, this class contains a method
 * <code>tokenize</code> that uses a <code>StringTokenizer</code> to
 * break a string into a string array of tokens according to a given
 * list of delimiters.  Optionally, the <code>trim</code> method may
 * be invoked to trim the tokens.  There are parallel methods that
 * make delimited strings.</p>
 *
 * <p>As of 2.5.0, this class also contains methods that will split
 * strings corresponding to a character delimiter and will not make
 * adjacent delimiters be treated as a single delimiter.  These
 * methods begin with <code>exactSplit</code> in their name.  There
 * are also parallel methods with <code>exactMake</code> in their
 * name that make delimited strings.</p>
 * 
 * <p>This class contains methods to build name-value string pairs
 * of the form <i>name</i>=<i>value</i> and to decompose such pairs.</p>
 *
 * <p>This class contains utility methods to convert between
 * arrays of string and arrays of long or double.</p>
 *
 * <p>This class contains utility methods to pad strings and to
 * combine string pairs with a blank separator.</p>
 *
 * <p>Finally, this class contains a simple utility to convert a
 * string to the equivalent HTML string with ampersand, less than,
 * greater than, and quote encoded.</p>
 *
 * <p>Class <code>Strings</code> cannot be instantiated.</p>
 *
 * @author  Richard Rasala
 * @version 2.5.0
 * @since   2.4.0
 */
public class Strings {

    /** Prevent instantiation. */
    private Strings() {}
    
    
    /** The start character for encodings <code>[s0;s1;etc]</code>. */
    public static final char BRACKETS = '[';
    
    
    /** The start character for encodings <code>(s0,s1,etc)</code>. */
    public static final char PARENS = '(';
    
    
    /** The start character for encodings <code>{s0|s1|etc}</code>. */
    public static final char BRACES = '{';
    
    
    /** The end character for encodings <code>[s0;s1;etc]</code>. */
    public static final char BRACKETS_END = ']';
    
    
    /** The end character for encodings <code>(s0,s1,etc)</code>. */
    public static final char PARENS_END = ')';
    
    
    /** The end character for encodings <code>{s0|s1|etc}</code>. */
    public static final char BRACES_END = '}';
    
    
    /**
     * The separator character for encodings <code>[s0;s1;etc]</code>
     * as a string.
     */
    public static final char BRACKETS_SEP = ';';
    
    
    /**
     * The separator character for encodings <code>(s0,s1,etc)</code>
     * as a string.
     */
    public static final char PARENS_SEP = ',';
    
    
    /**
     * The separator character for encodings <code>{s0|s1|etc}</code>
     * as a string.
     */
    public static final char BRACES_SEP = '|';
    
    
    /** The equals character. */
    private static final char EQUALS = '=';
    
    
    /**
     * <p>Returns the encoded <code>String</code> array as
     * <code>[s0;s1;etc]</code>
     * where s0, s1, etc represent the array elements.</p>
     *
     * <p>Equivalent to <code>encodeWithBrackets</code>.</p>
     *
     * @param data the data to encode
     * @see #encodeStringGroup(char, String[])
     */
    public static String encode(String[] data) {
        return encodeStringGroup(BRACKETS, data);
    }
    
    
    /**
     * <p>Returns the encoded <code>String</code> array as
     * <code>[s0;s1;etc]</code>
     * where s0, s1, etc represent the array elements.</p>
     *
     * <p>Equivalent to <code>encode</code>.</p>
     *
     * @param data the data to encode
     * @see #encodeStringGroup(char, String[])
     */
    public static String encodeWithBrackets(String[] data) {
        return encodeStringGroup(BRACKETS, data);
    }
    
    
    /**
     * <p>Returns the encoded <code>String</code> array as
     * <code>(s0,s1,etc)</code>
     * where s0, s1, etc represent the array elements.</p>
     *
     * @param data the data to encode
     * @see #encodeStringGroup(char, String[])
     */
    public static String encodeWithParens(String[] data) {
        return encodeStringGroup(PARENS, data);
    }
    
    
    /**
     * <p>Returns the encoded <code>String</code> array as
     * <code>{s0|s1|etc}</code>
     * where s0, s1, etc represent the array elements.</p>
     *
     * @param data the data to encode
     * @see #encodeStringGroup(char, String[])
     */
    public static String encodeWithBraces(String[] data) {
        return encodeStringGroup(BRACES, data);
    }
    
    
    /**
     * <p>Returns the encoded <code>String</code> array as
     * a sequence enclosed with the given start character,
     * its matching end character, and its associated
     * separator character.</p>
     *
     * <p>If the given data array is <code>null</code> then
     * returns <code>null</code> to signal failure.</p>
     *
     * <p>Assume the array elements are s0, s1, etc.  Then
     * the elements are encoded in one of the following ways.</p>
     *
     * <ul>
     *   <li><code>[s0;s1;...]</code></li>
     *   <li><code>(s0,s1,...)</code></li>
     *   <li><code>{s0|s1|...}</code></li>
     * </ul>
     *
     * <p>depending on whether the start character is
     * <code>[</code>, <code>(</code>, or <code>{</code>.</p>
     *
     * <p>If the start character is none of the three characters
     * required then returns <code>null</code> to signal failure.</p>
     *
     * <p>Each valid start character is paired with a corresponding
     * separator character for the encoding.</p>
     *
     * <ul>
     *  <li><code>[</code> is paired with the separator <code>;</code></li>
     *  <li><code>(</code> is paired with the separator <code>,</code></li>
     *  <li><code>{</code> is paired with the separator <code>|</code></li>
     * </ul>
     *
     * <p>As of 2.5.0, the encoding-decoding algorithms were changed
     * to avoid the use of the Java class <code>StringTokenizer</code>.
     * In fact, the portion of the output string between the start and
     * end characters is computed by the following call:</p>
     * 
     * <pre>    exactMakeSeparatorList(data,separator,false)</pre>
     * 
     * <p>This call ignores <code>null</code> strings in the data
     * array but makes an entry in the output string for all data
     * strings that are non-<code>null</code> even for those of
     * length 0.  The parameter false means that no trimming is
     * done on the data strings.</p>
     *
     * <p>To be robust and decodable, the encoded strings must not
     * contain the separator character used in the encoding.</p>
     *
     * <p>For a fully robust but more complex encoding, see the
     * classes in the <code>codec</code> package especially
     * <code>CodecUtilities</code>.</p>
     *
     * @param start the start character for the encoding which must
     *        be either <code>[</code>, <code>(</code>, or
     *        <code>{</code>
     * @param data the array data to encode
     */
    public static String encodeStringGroup
        (char start, String[] data)
    {
        if (data == null)
            return null;
        
        char end;
        char sep;
        
        if (start == BRACKETS) {
            end = BRACKETS_END;
            sep = BRACKETS_SEP;
        }
        else if (start == PARENS) {
            end = PARENS_END;
            sep = PARENS_SEP;
        }
        else if (start == BRACES) {
            end = BRACES_END;
            sep = BRACES_SEP;
        }
        else
            return null;
        
        StringBuffer buffer = new StringBuffer();
        
        buffer.append(start);
        buffer.append(exactMakeSeparatorList(data,sep,false));
        buffer.append(end);
        
        return buffer.toString();
    }
    
    
    /**
     * <p>Attempts several algorithms to decode the given string into
     * an array of strings and returns this array if successful and
     * returns <code>null</code> if unsuccessful with all attempts.</p>
     *
     * <p>Algorithm 1 uses <code>CodecUtilities.decode</code> to
     * attempt to parse the given string as a codec-encoded string.</p>
     *
     * <p>Algorithm 2 calls <code>splitIn2</code> and then calls
     * <code>decodeStringGroup</code> to parse the suffix as a
     * <i>string group</i> if possible.</p>
     *
     * <p>Algorithm 3 calls <code>splitIn3</code> and then calls
     * <code>decodeStringGroup</code> to parse the internal
     * <i>string group</i> if this string group is non-empty.</p>
     *
     * <p>In particular, since this <code>decode</code> method calls the
     * <code>decode</code> method of <code>CodecUtilities</code>,
     * it is more general and is recommended as the preferred method for
     * decoding a string into an array of strings.</p>
     *
     * @param string the string to decode
     */
    public static String[] decode(String string) {
        if (string == null)
            return null;
        
        String[] result = null;
        
        try {
            result = CodecUtilities.decode(string);
        }
        catch (Throwable ex) {
            result = null;
        }
        
        if (result != null)
            return result;
        
        String[] splits = null;
        
        splits = splitIn2(string);
        
        result = decodeStringGroup(splits[1]);
        
        if (result != null)
            return result;
        
        splits = splitIn3(string);
        
        result = decodeStringGroup(splits[1]);
        
        return result;
    }
    
    
    /**
     * <p>Returns a <code>String</code> array by decoding the given
     * string assuming it is encoded by the encoding conventions of
     * this class.</p>
     *
     * <p>The given string should be what we call a <i>string group</i>,
     * that is, it should have one of the following forms.</p>
     *
     * <ul>
     *   <li><code>[s0;s1;...]</code></li>
     *   <li><code>(s0,s1,...)</code></li>
     *   <li><code>{s0|s1|...}</code></li>
     * </ul>
     *
     * <p>The string array returned contains <code>s0 s1 ...</code>.
     * As of 2.5.0, even empty (zero length) strings will be included
     * in the array returned.  The method <code>exactSplitList</code>
     * is used to split the string within the start and end grouping
     * characters.</p>
     *
     * <p>To avoid issues with leading and trailing whitespace, the
     * given string is trimmed prior to processing.</p>
     *
     * <p>Returns <code>null</code> if.</p>
     *
     * <ul>
     *   <li>The given string is <code>null</code>.</li>
     *   <li>After being trimmed, the given string does not start and
     *       end with one of the following pairs of grouping characters:
     *       <code>[]</code>, <code>()</code>, or <code>{}</code>.</li>
     * </ul>
     *
     * @param string the <i>string group</i> to decode
     */
    public static String[] decodeStringGroup(String string) {
        if (string == null)
            return null;
        
        string = string.trim();
        
        int last = string.length() - 1;
        
        if (last < 1)
            return null;
        
        char sep;
        
        if ((string.charAt(0) == BRACKETS)
                && (string.charAt(last) == BRACKETS_END)) {
            sep = BRACKETS_SEP;
        }
        else
        if ((string.charAt(0) == PARENS)
                && (string.charAt(last) == PARENS_END)) {
            sep = PARENS_SEP;
        }
        else
        if ((string.charAt(0) == BRACES)
                && (string.charAt(last) == BRACES_END)) {
            sep = BRACES_SEP;
        }
        else
            return null;
        
        string = string.substring(1, last);
        
        return exactSplitList(string, sep, false);
    }
    
    
    /**
     * <p>Returns true if the given string starts and ends with the given
     * pair of distinct start and end characters.</p>
     *
     * @param string the string to test
     * @param start  the start character
     * @param end    the end   character
     */
    public static boolean isStartEnd(String string, char start, char end) {
        if (string == null)
            return false;
        
        int last = string.length() - 1;
        
        if (last < 1)
            return false;
        
        return ((string.charAt(0) == start) && (string.charAt(last) == end));
    }
    
    
    /**
     * <p>Splits the given string into 2 parts: a prefix and a suffix
     * and returns these 2 parts in a string array.</p>
     *
     * <p>The prefix consists of the longest substring of the given string
     * starting at the beginning that does not contain one of the following
     * start characters: <code>[</code>, <code>(</code>, or <code>{</code>.</p>
     *
     * <p>If the given string does not contain any of the start characters,
     * then the prefix is the given string and the suffix is the empty
     * string.</p>
     *
     * <p>Otherwise, the prefix is the substring up to but not including the
     * first start character and the suffix is the rest of the string.</p>
     *
     * <p>This algorithm does not guarantee that the suffix is a
     * <i>string group</i> but if it is then it is the largest possible
     * <i>string group</i> contained in the original string.</p>
     *
     * <p>Returns <code>null</code> if the given string is <code>null</code>.
     * If the given string is non-<code>null</code>, then both strings in the
     * return array will be non-<code>null</code>.</p>
     *
     * @param string the string to split
     * @return an array with the prefix and suffix
     */
    public static String[] splitIn2(String string) {
        if (string == null)
            return null;
        
        String prefix = "";
        String suffix = "";
        
        int length = string.length();
        
        if (length == 0)
            return new String[] { prefix, suffix };
        
        int first = length;
        int index;
        
        index = string.indexOf(BRACKETS);
        
        if ((index >= 0) && (index < first)) {
            first = index;
        }
        
        index = string.indexOf(PARENS);
        
        if ((index >= 0) && (index < first)) {
            first = index;
        }
        
        index = string.indexOf(BRACES);
        
        if ((index >= 0) && (index < first)) {
            first = index;
        }
        
        if (first == 0) {
            suffix = string;
            
            return new String[] { prefix, suffix };
        }
        
        if (first == length) {
            prefix = string;
            
            return new String[] { prefix, suffix };
        }
        
        prefix = string.substring(0, first);
        suffix = string.substring(first, length);
        
        return new String[] { prefix, suffix };
    }
    
    
    /**
     * <p>Splits the given string into 3 parts:
     * a prefix, a <i>string group</i>, and a suffix
     * and returns these 3 parts in a string array.</p>
     *
     * <p>The prefix consists of the longest substring of the given string
     * starting at the beginning that does not contain one of the following
     * start characters: <code>[</code>, <code>(</code>, or <code>{</code>.</p>
     *
     * <p>If the given string does not contain any of the start characters,
     * then the prefix is the given string and the other two parts are empty
     * strings.</p>
     *
     * <p>If a start character exists, then a search is made for the nearest
     * matching end character that follows the start character.  If such an
     * end character is found then this defines the <i>string group</i> and
     * the suffix is what is left.  If no matching end character is found
     * then the <i>string group</i> is empty and the suffix is the rest of
     * the string including the unmatched start character.</p>
     *
     * <p>With this specification, it is possible for the suffix to contain
     * additional string groups that could be peeled off by repeated use of
     * this method.</p>
     *
     * <p>Returns <code>null</code> if the given string is <code>null</code>.
     * If the given string is non-<code>null</code>, then all three strings
     * in the return array will be non-<code>null</code>.</p>
     *
     * @param string the string to split
     * @return an array with the prefix, <i>string group</i>, and suffix
     */
    public static String[] splitIn3(String string) {
        if (string == null)
            return null;
        
        String prefix = "";
        String middle = "";
        String suffix = "";
        
        int length = string.length();
        
        if (length == 0)
            return new String[] { prefix, middle, suffix };
        
        int first = length;
        int index;
        
        char end = '\0';
        
        index = string.indexOf(BRACKETS);
        
        if ((index >= 0) && (index < first)) {
            first = index;
            end = BRACKETS_END;
        }
        
        index = string.indexOf(PARENS);
        
        if ((index >= 0) && (index < first)) {
            first = index;
            end = PARENS_END;
        }
        
        index = string.indexOf(BRACES);
        
        if ((index >= 0) && (index < first)) {
            first = index;
            end = BRACES_END;
        }
        
        if (first == length) {
            prefix = string;
            
            return new String[] { prefix, middle, suffix };
        }
        
        prefix = string.substring(0, first);
        
        index = string.indexOf(end, first);
        
        if (index == -1) {
            suffix = string.substring(first, length);
            
            return new String[] { prefix, middle, suffix };
        }
        
        index++;
        
        middle = string.substring(first, index);
        suffix = string.substring(index, length);
        
        return new String[] { prefix, middle, suffix };
    }
    
    
    /**
     * <p>Given a pair of strings <i>name</i> and <i>value</i>, return the
     * string <i>name</i>=<i>value</i>.</p>
     *
     * <p>If either string is <code>null</code>, return <code>null</code>.</p>
     *
     * @param name the name
     * @param value the value
     */
    public static String makeNameValuePair(String name, String value) {
        if ((name == null) || (value == null))
            return null;
        
        return name + EQUALS + value;
    }
    
    
    /**
     * <p>Given a string of the form <i>name</i>=<i>value</i>, return the
     * <i>name</i> portion.</p>
     *
     * <p>Return <code>null</code> if the given string is <code>null</code>.</p>
     *
     * <p>Return an empty string if the given string does not contain an
     * equals sign.</p>
     *
     * @param string the name-value string
     */
    public static String getName(String string) {
        if (string == null)
            return null;
        
        int index = string.indexOf(EQUALS);
        
        if (index == -1)
            return "";
        
        return string.substring(0, index);
    }
    
    
    /**
     * <p>Given a string of the form <i>name</i>=<i>value</i>, return the
     * <i>value</i> portion.</p>
     *
     * <p>Return <code>null</code> if the given string is <code>null</code>.</p>
     *
     * <p>Return the given string if the given string does not contain an
     * equals sign.</p>
     *
     * @param string the name-value string
     */
    public static String getValue(String string) {
        if (string == null)
            return null;
        
        int index = string.indexOf(EQUALS);
        
        if (index == -1)
            return string;
        
        return string.substring(index + 1, string.length());
    }
    
    
    /**
     * <p>Given an array of names and an array of values, constructs
     * and returns an array of pairs of the form: <i>name</i>=<i>value</i>.</p>
     *
     * <p>Returns <code>null</code> to signal failure if any of the
     * following situations takes place.</p>
     *
     * <ul>
     *   <li>Either data array is <code>null</code>.</li>
     *   <li>The name and value arrays do not have the same length.</li>
     *   <li>The name and value arrays contain <code>null</code> entries.</li>
     * </ul>
     *
     * @param names  the array of names
     * @param values the array of values
     */
    public static String[] makeNameValuePairs(String[] names, String[] values) {
        if ((names == null) || (values == null))
            return null;
        
        int length = names.length;
        
        if (values.length != length)
            return null;
        
        String[] result = new String[length];
        
        for (int i = 0; i < length; i++) {
            result[i] = makeNameValuePair(names[i], values[i]);
            
            if (result[i] == null)
                return null;
        }
        
        return result;
    }
    
    
    /**
     * <p>Given an array of name-value pairs, returns the array of names.</p>
     *
     * <p>Returns <code>null</code> if the given array is
     * <code>null</code>.</p>
     *
     * <p>Returns <code>null</code> if any entry in the given array is
     * <code>null</code>.</p>
     *
     * @param pairs the array of name-value pairs
     */
    public static String[] getNames(String[] pairs) {
        if (pairs == null)
            return null;
        
        int length = pairs.length;
        
        String[] result = new String[length];
        
        for (int i = 0; i < length; i++) {
            result[i] = getName(pairs[i]);
            
            if (result[i] == null)
                return null;
        }
        
        return result;
    }
    
    
    /**
     * <p>Given an array of name-value pairs, returns the array of values.</p>
     *
     * <p>Returns <code>null</code> if the given array is
     * <code>null</code>.</p>
     *
     * <p>Returns <code>null</code> if any entry in the given array is
     * <code>null</code>.</p>
     *
     * @param pairs the array of name-value pairs
     */
    public static String[] getValues(String[] pairs) {
        if (pairs == null)
            return null;
        
        int length = pairs.length;
        
        String[] result = new String[length];
        
        for (int i = 0; i < length; i++) {
            result[i] = getValue(pairs[i]);
            
            if (result[i] == null)
                return null;
        }
        
        return result;
    }
    
    
    /**
     * <p>Returns an array of strings
     * corresponding to the given array of <code>long</code>s.</p>
     *
     * <p>Returns <code>null</code> if the given data is <code>null</code>.</p>
     *
     * @param data the data to convert
     * @return the converted data
     */
    public static String[] longsToStrings(long[] data) {
        if (data == null)
            return null;
        
        int length = data.length;
        
        String[] result = new String[length];
        
        for (int i = 0; i < length; i++)
            result[i] = java.lang.Long.toString(data[i]);
        
        return result;
    }
    
    
    /**
     * <p>Returns an array of strings
     * corresponding to the given array of <code>double</code>.</p>
     *
     * <p>Returns <code>null</code> if the given data is <code>null</code>.</p>
     *
     * @param data the data to convert
     * @return the converted data
     */
    public static String[] doublesToStrings(double[] data) {
        if (data == null)
            return null;
        
        int length = data.length;
        
        String[] result = new String[length];
        
        for (int i = 0; i < length; i++)
            result[i] = java.lang.Double.toString(data[i]);
        
        return result;
    }
    
    
    /**
     * <p>Returns an array of strings
     * corresponding to the given array of <code>XComplex</code>.</p>
     *
     * <p>Each <code>XComplex</code> is converted by the static method
     * <code>XComplex.toStringData</code>.</p>
     *
     * <p>Returns <code>null</code> if the given data is <code>null</code>.</p>
     *
     * @param data the data to convert
     * @return the converted data
     */
    public static String[] XComplexValuesToStrings(XComplex[] data) {
        if (data == null)
            return null;
        
        int length = data.length;
        
        String[] result = new String[length];
        
        for (int i = 0; i < length; i++)
            result[i] = XComplex.toStringData(data[i]);
        
        return result;
    }
    
    
    /**
     * <p>Returns an array of <code>long</code>s
     * corresponding to the given array of strings.</p>
     *
     * <p>Returns <code>null</code> if the given data is <code>null</code>.</p>
     *
     * <p>Trims the data strings prior to the numeric conversion.</p>
     *
     * <p>In converting the i-th data item of the array to a <code>long</code>,
     * the following conventions and procedures are followed.</p>
     *
     * <ul>
     *   <li>If the i-th data entry is <code>null</code> or of length 0,
     *       then the corresponding <code>long</code> is set to zero.</li>
     *   <li>Otherwise, the i-th data entry is parsed using the method
     *       <code>fromStringData</code> of <code>XLong</code> so that
     *       arithmetic expressions will be evaluated.  If this method
     *       throws a <code>ParseException</code> then the exception is
     *       rethrown with the following items prepended to the message
     *       to make precise where in the array the error occured.</li>
     *       <ul>
     *         <li>The string <code>i + ": "</code></li>
     *         <li>The string contents of the i-th data entry</li>
     *         <li>A newline</li>
     *       </ul>
     * </ul>
     *
     * @param data the data to convert
     * @return the converted data
     */
    public static long[] stringsToLongs(String[] data)
        throws ParseException
    {
        if (data == null)
            return null;
        
        data = trim(data);
        
        int length = data.length;
        
        XLong xlong = new XLong();
        
        long[] result = new long[length];
        
        for (int i = 0; i < length; i++) {
            if ((data[i] == null) || (data[i].length() == 0))
                result[i] = 0;
            else {
                try {
                    xlong.fromStringData(data[i]);
                    result[i] = xlong.getValue();
                }
                catch (ParseException ex) {
                    String message = ex.getMessage();
                    int offset = ex.getErrorOffset();
                    
                    message = i + ": " + data[i] + "\n" + message;
                    
                    throw new ParseException(message, offset);
                }
            }
        }
        
        return result;
    }
    
    
    /**
     * <p>Returns an array of <code>double</code>s
     * corresponding to the given array of strings.</p>
     *
     * <p>Returns <code>null</code> if the given data is <code>null</code>.</p>
     *
     * <p>Trims the data strings prior to the numeric conversion.</p>
     *
     * <p>In converting the i-th data item of the array to a <code>double</code>,
     * the following conventions and procedures are followed.</p>
     *
     * <ul>
     *   <li>If the i-th data entry is <code>null</code> or of length 0,
     *       then the corresponding <code>double</code> is set to zero.</li>
     *   <li>Otherwise, the i-th data entry is parsed using the method
     *       <code>fromStringData</code> of <code>XDouble</code> so that
     *       arithmetic expressions will be evaluated.  If this method
     *       throws a <code>ParseException</code> then the exception is
     *       rethrown with the following items prepended to the message
     *       to make precise where in the array the error occured.</li>
     *       <ul>
     *         <li>The string <code>i + ": "</code></li>
     *         <li>The string contents of the i-th data entry</li>
     *         <li>A newline</li>
     *       </ul>
     * </ul>
     *
     * @param data the data to convert
     * @return the converted data
     */
    public static double[] stringsToDoubles(String[] data)
        throws ParseException
    {
        if (data == null)
            return null;
        
        data = trim(data);
        
        int length = data.length;
        
        XDouble xdouble = new XDouble();
        
        double[] result = new double[length];
        
        for (int i = 0; i < length; i++) {
            if ((data[i] == null) || (data[i].length() == 0))
                result[i] = 0;
            else {
                try {
                    xdouble.fromStringData(data[i]);
                    result[i] = xdouble.getValue();
                }
                catch (ParseException ex) {
                    String message = ex.getMessage();
                    int offset = ex.getErrorOffset();
                    
                    message = i + ": " + data[i] + "\n" + message;
                    
                    throw new ParseException(message, offset);
                }
            }
        }
        
        return result;
    }
    
    
    /**
     * <p>Returns an array of <code>XComplex</code> values
     * corresponding to the given array of strings.</p>
     *
     * <p>Returns <code>null</code> if the given data is <code>null</code>.</p>
     *
     * <p>Trims the data strings prior to the numeric conversion.</p>
     *
     * <p>In converting the i-th data item of the array to an <code>XComplex</code>,
     * the following conventions and procedures are followed.</p>
     *
     * <ul>
     *   <li>If the i-th data entry is <code>null</code> or of length 0,
     *       then the corresponding <code>XComplex</code> is set to zero.</li>
     *   <li>Otherwise, the i-th data entry is parsed using the method
     *       <code>fromStringData</code> of <code>XComplex</code>.  If
     *       this method throws a <code>ParseException</code> then the
     *       exception is rethrown with the following items prepended to
     *       the message to make precise where in the array the error
     *       occured.</li>
     *       <ul>
     *         <li>The string <code>i + ": "</code></li>
     *         <li>The string contents of the i-th data entry</li>
     *         <li>A newline</li>
     *       </ul>
     * </ul>
     *
     * @param data the data to convert
     * @return the converted data
     */
    public static XComplex[] stringsToXComplexValues(String[] data)
        throws ParseException
    {
        if (data == null)
            return null;
        
        data = trim(data);
        
        int length = data.length;
        
        XComplex[] result = new XComplex[length];
        
        for (int i = 0; i < length; i++) {
            if ((data[i] == null) || (data[i].length() == 0))
                result[i] = new XComplex();
            else {
                try {
                    result[i] = new XComplex(data[i]);
                }
                catch (ParseException ex) {
                    String message = ex.getMessage();
                    int offset = ex.getErrorOffset();
                    
                    message = i + ": " + data[i] + "\n" + message;
                    
                    throw new ParseException(message, offset);
                }
            }
        }
        
        return result;
    }
    
    
    /**
     * <p>Returns the array index that was prepended to an
     * error message thrown by <code>stringsToLongs</code>
     * or <code>stringsToDoubles</code>.</p>
     *
     * <p>This is a hack required since there is no extra
     * data field in a <code>ParseException</code> to hold
     * the array index directly.</p>
     *
     * <p>Returns 0 if the message has no prepended value.</p>
     *
     * @param message the error message
     */
    public static int getIndexValue(String message) {
        if (message == null)
            return 0;
        
        int length = message.length();
        
        int result = 0;
        
        for (int i = 0; i < length; i++) {
            char c = message.charAt(i);
            
            if (Character.isDigit(c)) {
                int v = Character.digit(c, 10);
                result = 10 * result + v;
            }
            else
                break;
        }
        
        return result;
    }
    
    
    /**
     * <p>Returns the error message with its prepended index
     * value removed.</p>
     *
     * <p>Assumes that the original error message was created
     * by <code>stringsToLongs</code> or
     * <code>stringsToDoubles</code>.</p>
     *
     * <p>This is a hack required since there is no extra
     * data field in a <code>ParseException</code> to hold
     * the array index directly.</p>
     *
     * <p>Returns the message if the message has no prepended
     * index value.</p>
     *
     * @param message the error message
     */
    public static String removeIndexValue(String message) {
        if (message == null)
            return null;
        
        int length = message.length();
        
        int i = 0;
        
        for (i = 0; i < length; i++) {
            char c = message.charAt(i);
            
            if (! Character.isDigit(c))
                break;
        }
        
        return message.substring(i);
    }
    
    
    /**
     * <p>Returns a new <code>ParseException</code> based on
     * the given <code>ParseException</code> but adjusted to
     * include information from the given type name and the
     * given field names.</p>
     *
     * <p>Assumes that the given <code>ParseException</code>
     * was thrown by <code>stringsToLongs</code> or
     * <code>stringsToDoubles</code> while parsing the given
     * type.</p>
     *
     * <p>The type name is used to make the user aware of the
     * specific type whose data was being parsed.</p>
     *
     * <p>The names array is used to replace the array index
     * in the original message with a more understandable
     * field name.</p>
     *
     * <p>This is a helper method for the methods
     * <code>fromStringData</code> in
     * assorted <code>Stringable</code> classes.</p>
     *
     * @param ex the original <code>ParseException</code>
     * @param type the type being parsed
     * @param names the field names that replace indices
     */
    public static ParseException makeAdjustedParseException
        (ParseException ex, String type, String[] names)
    {
        String message = ex.getMessage();
        int offset = ex.getErrorOffset();
        
        int index = Strings.getIndexValue(message);
        String replace = Strings.removeIndexValue(message);
        
        message =
            "\n"
            + type
            + " Error in\n"
            + names[index]
            + " field"
            + replace
            + "\nat offset "
            + offset
            + "\n";
        
        return new ParseException(message, -1); 
    }
    
    
    /**
     * <p>Returns a string array with all strings in the original
     * array trimmed using <code>String.trim</code>.</p>
     *
     * <p><code>null</code> data strings are returned as is.</p>
     *
     * <p>Returns <code>null</code> if the given data array is
     * <code>null</code>.</p>
     * 
     * <p>Returns a newly constructed array but strings that were
     * already trimmed in the original array will be returned
     * as is.</p>
     * 
     * @param data the data to trim
     */
    public static String[] trim(String[] data) {
        if (data == null)
            return null;
        
        int length = data.length;
        
        String[] result = new String[length];
        
        for (int i = 0; i < length; i++) {
            result[i] = (data[i] == null) ? null : data[i].trim();
        }
        
        return result;
    }
    
    
    /**
     * <p>Returns true if the given character is whitespace
     * but is non-blank.</p>
     * 
     * @param c the character to test
     */
    public static boolean isNonTrivialWhitespace(char c) {
        if (c == ' ')
            return false;
        
        return Character.isWhitespace(c);
    }
    
    
    /**
     * <p>Returns a string with all whitespace but non-blank
     * characters replaced by blanks.</p>
     * 
     * <p>This method is called flatten because it will turn
     * a string with line feeds and carriage returns into
     * one that may be viewed on a single line.</p>
     * 
     * <p>Returns the given string if it does not contain a
     * character that is whitespace but non-blank.</p>
     * 
     * @param data the string to flatten
     */
    public static String flatten(String data) {
        if (data == null)
            return null;
        
        int length = data.length();
        
        boolean ok = true;
        
        for (int i = 0; i < length; i++) {
            if (isNonTrivialWhitespace(data.charAt(i))) {
                ok = false;
                break;
            }
        }
        
        if (ok)
            return data;
        
        StringBuffer buffer = new StringBuffer(data);
        
        for (int i = 0; i < length; i++) {
            if (isNonTrivialWhitespace(data.charAt(i))) {
                buffer.setCharAt(i, ' ');
            }
        }
        
        return buffer.toString();
    }
    
    
    /**
     * <p>Returns a string array with all strings in the original
     * array flattened using <code>Strings.flatten</code>.</p>
     *
     * <p><code>null</code> data strings are returned as is.</p>
     *
     * <p>Returns <code>null</code> if the given data array is
     * <code>null</code>.</p>
     *
     * <p>Returns a newly constructed array but strings that were
     * already flattened in the original array will be returned
     * as is.</p>
     * 
     * @param data the data to flatten
     */
    public static String[] flatten(String[] data) {
        if (data == null)
            return null;
        
        int length = data.length;
        
        String[] result = new String[length];
        
        for (int i = 0; i < length; i++) {
            result[i] = (data[i] == null) ? null : flatten(data[i]);
        }
        
        return result;
    }
    
    
    /**
     * <p>Returns a string array contains the tokens obtained by
     * using a <code>StringTokenizer</code> to split the string
     * according to the given delimiters; if the given trim is
     * true then the tokens are trimmed before being returned.</p>
     *
     * <p>If the given string is <code>null</code>, returns
     * <code>null</code>.</p>
     *
     * <p>If the given delimiters string is <code>null</code> or
     * of length 0, return a string array with the given string
     * as its only element (possibly trimmed).</p>
     *
     * @param string the string to tokenize
     * @param delimiters the delimiter characters
     * @param trim whether or not to trim the tokens
     */
    public static String[] tokenize(String string, String delimiters, boolean trim) {
        String[] result = null;
        
        if (string == null)
            return result;
        
        if ((delimiters == null) || (delimiters.length() == 0)) {
            result = new String[] { string };
        }
        else {
            StringTokenizer tokenizer =
                new StringTokenizer(string, delimiters);
            
            int length = tokenizer.countTokens();
            
            result = new String[length];
            
            for (int i = 0; i < length; i++) {
                result[i] = tokenizer.nextToken();
            }
        }
        
        if (trim)
            result = trim(result);
        
        return result;
    }
    
    
    /**
     * <p>Extract the non-empty strings from a comma separated list.</p>
     *
     * <p>Return a zero length array if the list is <code>null</code>
     * or has length 0 after being trimmed.</p>
     *
     * <p>This is a frequently used special case of the method
     * <code>tokenize</code> and so is explicitly given its own name.</p>
     *
     * @param commalist a comma separated list of strings
     */
    public static String[] splitCommaList(String commalist) {
        if (commalist == null)
            return new String[0];
        
        commalist = commalist.trim();
        
        if (commalist.length() == 0)
            return new String[0];
        
        return Strings.tokenize(commalist, ",", true);
    }
    
    
    /**
     * <p>Creates a separated list by trimming the data in the given
     * String array and concatenating with the given separator.</p>
     *
     * <p>The strings that are <code>null</code> or that trim to
     * length 0 strings will be omitted from the separator list.</p>
     * 
     * @param arrayList the array of strings to concatenate
     * @param separator the separator character
     */
    public static String makeSeparatorList
        (String[] arrayList, char separator)
    {
        if (arrayList == null)
            return "";
        
        int N = arrayList.length;
        
        arrayList = trim(arrayList);
        
        // push non-trivial data items down in the list
        // loop invariant M = number of non-trivial data items found so far
        
        int M = 0;
        
        for (int i = 0; i < N; i++)
            if (arrayList[i] != null)
                if (arrayList[i].length() != 0) {
                    arrayList[M] = arrayList[i];
                    M++;   
                }
        
        // now make the separated list
        
        StringBuffer buffer = new StringBuffer();
        
        int L = M - 1;
        
        for (int i = 0; i <= L; i++) {
            buffer.append(arrayList[i]);
            
            if (i < L)
                buffer.append(separator);
        }
        
        return buffer.toString();
    }
    
    
    /**
     * <p>Creates a comma separated list by trimming the data in the
     * given String array and concatenating with commas.</p>
     *
     * <p>The strings that are <code>null</code> or that trim to
     * length 0 strings will be omitted from the separator list.</p>
     * 
     * <p>This is a frequently used special case of the method
     * <code>makeSeparatorList</code> and so is explicitly given its
     * own name.</p>
     *
     * @param arrayList the array of strings to concatenate
     */
    public static String makeCommaList(String[] arrayList) {
        return makeSeparatorList(arrayList, ',');
    }
    
    
    /**
     * <p>Returns an array of strings obtained by splitting the
     * given string at every occurence of the given delimiter;
     * if trim is true then also trims the array returned.</p>
     * 
     * <p>If the delimiter occurs N times in the given string
     * then the array returned will have size (N+1).</p>
     * 
     * <p>If the delimiter occurs in adjacent positions in the
     * string then the corresponding split will have length 0.</p>
     * 
     * <p>This method avoids <code>StringTokenizer</code> since
     * that class groups adjacent delimiters.</p>
     * 
     * <p>If the given string is <code>null</code>, returns
     * <code>null</code>.</p>
     *
     * @param string the string to split
     * @param delimiter the delimiter that defines the split
     * @param trim whether or not to trim the array returned
     */
    public static String[] exactSplitList
        (String string, char delimiter, boolean trim)
    {
        if (string == null)
            return null;
        
        int L = string.length();
        
        int N = 0;      // number of delimiters
        
        for (int i = 0; i < L; i++)
            if (string.charAt(i) == delimiter)
                N++;
        
        String[] result = new String[N+1];
        
        int M = 0;      // current delimiter to seek
        
        int s = 0;      // substring start inclusive
        int t = 0;      // substring end   exclusive
        
        while (M < N) {
            while (string.charAt(t) != delimiter)
                t++;
            
            result[M] = string.substring(s, t);
            
            M++;
            t++;
            s = t;
        }
        
        t = L;
        
        result[N] = string.substring(s, t);
        
        if (trim)
            result = trim(result);
        
        return result;
    }
    
    
    /**
     * <p>Returns an array of strings obtained by splitting the
     * given string at every occurence of the given delimiter;
     * automatically trims the array returned.</p>
     * 
     * <p>If the delimiter occurs N times in the given string
     * then the array returned will have size (N+1).</p>
     * 
     * <p>If the delimiter occurs in adjacent positions in the
     * string then the corresponding split will have length 0.</p>
     * 
     * <p>This method avoids <code>StringTokenizer</code> since
     * that class groups adjacent delimiters.</p>
     * 
     * <p>If the given string is <code>null</code>, returns
     * <code>null</code>.</p>
     *
     * @param string the string to split
     * @param delimiter the delimiter that defines the split
     */
    public static String[] exactSplitList
        (String string, char delimiter)
    {
        return exactSplitList(string, delimiter, true);
    }
    
    
    /**
     * <p>Returns an array of strings obtained by splitting the
     * given string at every occurence of the comma character;
     * automatically trims the array returned.</p>
     * 
     * <p>If comma occurs N times in the given string
     * then the array returned will have size (N+1).</p>
     * 
     * <p>If comma occurs in adjacent positions in the
     * string then the corresponding split will have length 0.</p>
     * 
     * <p>This method avoids <code>StringTokenizer</code> since
     * that class groups adjacent delimiters.</p>
     * 
     * <p>If the given string is <code>null</code>, returns
     * <code>null</code>.</p>
     *
     * @param string the string to split
     */
    public static String[] exactSplitCommaList
        (String string)
    {
        return exactSplitList(string, ',', true);
    }
    
    
    /**
     * <p>Returns an array of strings obtained by splitting the
     * given string at every occurence of the newline character;
     * automatically trims the array returned.</p>
     * 
     * <p>If newline occurs N times in the given string
     * then the array returned will have size (N+1).</p>
     * 
     * <p>If newline occurs in adjacent positions in the
     * string then the corresponding split will have length 0.</p>
     * 
     * <p>This method avoids <code>StringTokenizer</code> since
     * that class groups adjacent delimiters.</p>
     * 
     * <p>If the given string is <code>null</code>, returns
     * <code>null</code>.</p>
     *
     * @param string the string to split
     */
    public static String[] exactSplitNewlineList
        (String string)
    {
        return exactSplitList(string, '\n', true);
    }
    
    
    /**
     * <p>Uses the array of strings to build a string that
     * separates each item with the given delimiter;
     * if trim is true then trims the array before the build.</p>
     * 
     * <p>If the given data is <code>null</code>, returns
     * <code>null</code>.</p>
     * 
     * <p>If the given data is non-<code>null</code>, then
     * the string returned is non-<code>null</code>.</p>
     * 
     * <p>If an item in the data array is <code>null</code>
     * then that item is skipped.</p>
     * 
     * <p>If M is the number of non-<code>null</code> items
     * in the data array then the delimiter will be placed
     * (M-1) times into the returned string to separate the
     * non-<code>null</code> items.</p>
     * 
     * <p>This method does not check whether or not the
     * delimiter occurs within any item in the data array.</p>
     * 
     * @param data the string array to build into a string
     * @param delimiter the delimiter to use as a separator
     * @param trim whether or not to trim before the build
     */
    public static String exactMakeSeparatorList
        (String[] data, char delimiter, boolean trim)
    {
        if (data == null)
            return null;
        
        if (trim)
            data = trim(data);
        
        StringBuffer buffer = new StringBuffer();
        
        int L = data.length;
        
        int N = 0;      // number of non-null data items
        
        for (int i = 0; i < L; i++)
            if (data[i] != null)
                N++;
        
        int M = 0;      // current non-null data item to seek
        int k = 0;      // current search index
        
        while (M < N) {
            while (data[k] == null)
                k++;
            
            buffer.append(data[k]);
            
            if (M != (N-1))
                buffer.append(delimiter);
            
            M++;
            k++;
        }
        
        return buffer.toString();
    }
    
    
    /**
     * <p>Uses the array of strings to build a string that
     * separates each item with the given delimiter;
     * automatically trims the array before the build.</p>
     * 
     * <p>If the given data is <code>null</code>, returns
     * <code>null</code>.</p>
     * 
     * <p>If the given data is non-<code>null</code>, then
     * the string returned is non-<code>null</code>.</p>
     * 
     * <p>If an item in the data array is <code>null</code>
     * then that item is skipped.</p>
     * 
     * <p>If M is the number of non-<code>null</code> items
     * in the data array then the delimiter will be placed
     * (M-1) times into the returned string to separate the
     * non-<code>null</code> items.</p>
     * 
     * <p>This method does not check whether or not the
     * delimiter occurs within any item in the data array.</p>
     * 
     * @param data the string array to build into a string
     * @param delimiter the delimiter to use as a separator
     */
    public static String exactMakeSeparatorList
        (String[] data, char delimiter)
    {
        return exactMakeSeparatorList(data, delimiter, true);
    }
    
    
    /**
     * <p>Uses the array of strings to build a string that
     * separates each item with a comma;
     * automatically trims the array before the build.</p>
     * 
     * <p>If the given data is <code>null</code>, returns
     * <code>null</code>.</p>
     * 
     * <p>If the given data is non-<code>null</code>, then
     * the string returned is non-<code>null</code>.</p>
     * 
     * <p>If an item in the data array is <code>null</code>
     * then that item is skipped.</p>
     * 
     * <p>If M is the number of non-<code>null</code> items
     * in the data array then the comma will be placed
     * (M-1) times into the returned string to separate the
     * non-<code>null</code> items.</p>
     * 
     * <p>This method does not check whether or not a
     * comma occurs within any item in the data array.</p>
     * 
     * @param data the string array to build into a string
     */
    public static String exactMakeCommaList
        (String[] data)
    {
        return exactMakeSeparatorList(data, ',', true);
    }
    
    
    /**
     * <p>Uses the array of strings to build a string that
     * separates each item with a newline;
     * automatically trims the array before the build.</p>
     * 
     * <p>If the given data is <code>null</code>, returns
     * <code>null</code>.</p>
     * 
     * <p>If the given data is non-<code>null</code>, then
     * the string returned is non-<code>null</code>.</p>
     * 
     * <p>If an item in the data array is <code>null</code>
     * then that item is skipped.</p>
     * 
     * <p>If M is the number of non-<code>null</code> items
     * in the data array then the newline will be placed
     * (M-1) times into the returned string to separate the
     * non-<code>null</code> items.</p>
     * 
     * <p>This method does not check whether or not a
     * newline occurs within any item in the data array.</p>
     * 
     * @param data the string array to build into a string
     */
    public static String exactMakeNewlineList
        (String[] data)
    {
        return exactMakeSeparatorList(data, '\n', true);
    }
    
    
    /**
     * <p>Returns the given string if it is non-<code>null</code>
     * and has length at least 1; otherwise returns " ".</p>
     *
     * @param s the string to pad to length at least 1
     */
    public static String pad(String s) {
        if ((s == null) || (s.length() == 0))
            return " ";
        
        return s;
    }
    
    
    /**
     * <p>Returns a string array with all strings in the original
     * array padded using <code>Strings.pad</code>.</p>
     *
     * <p>Returns <code>null</code> if the given data array is
     * <code>null</code>.</p>
     *
     * @param data the data to pad
     */
    public static String[] pad(String[] data) {
        if (data == null)
            return null;
        
        int length = data.length;
        
        String[] result = new String[length];
        
        for (int i = 0; i < length; i++) {
            result[i] = pad(data[i]);
        }
        
        return result;
    }
    
    
    /**
     * <p>Returns a string with count instances of the
     * given character c prefixed to the given string.</p>
     * 
     * <p>If string is <code>null</code>, it is set to
     * the empty string.</p>
     * 
     * <p>If count is negative it is set to 0.</p>
     * 
     * @param string the string to prefix
     * @param c the character to repeat
     * @param count the number of repeats
     */
    public static String prefixRepeatChar
        (String string, char c, int count)
    {
        if (string == null)
            string = "";
        
        if (count < 0)
            count = 0;
        
        StringBuffer buffer = new StringBuffer(count + string.length());
        
        for (int i = 0; i < count; i++)
            buffer.append(c);
        
        buffer.append(string);
        
        return buffer.toString();
    }
    
    
    /**
     * <p>Returns a string with count instances of the
     * given character c suffixed to the given string.</p>
     * 
     * <p>If string is <code>null</code>, it is set to
     * the empty string.</p>
     * 
     * <p>If count is negative it is set to 0.</p>
     * 
     * @param string the string to suffix
     * @param c the character to repeat
     * @param count the number of repeats
     */
    public static String suffixRepeatChar
        (String string, char c, int count)
    {
        if (string == null)
            string = "";
        
        if (count < 0)
            count = 0;
        
        StringBuffer buffer = new StringBuffer(count + string.length());
        
        buffer.append(string);
        
        for (int i = 0; i < count; i++)
            buffer.append(c);
        
        return buffer.toString();
    }
    
    
    /**
     * <p>Return the string s padded with blanks on the left.</p>
     * 
     * <p>First, if s is <code>null</code>, replace s with an
     * empty string.</p>
     * 
     * <p>Then, if s has length greater than or equal to width,
     * return s.</p>
     * 
     * <p>Otherwise, return a string constructed with s on the
     * right and (width-s.length()) blanks on the left.</p>
     * 
     * @param s the string to pad
     * @param width the minimum width of the return string
     */
    public static String padOnLeft(String s, int width) {
        if (s == null)
            s = "";
        
        int n = s.length();
        
        if (n >= width)
            return s;
        
        return prefixRepeatChar(s, ' ', width - n);
    }
    
    
    /**
     * <p>Returns a string array with all strings in the original
     * array padded using <code>Strings.padOnLeft</code>.</p>
     *
     * <p>Returns <code>null</code> if the given data array is
     * <code>null</code>.</p>
     *
     * @param data the data to pad
     * @param width the minimum width of each return string
     */
    public static String[] padOnLeft(String[] data, int width) {
        if (data == null)
            return null;
        
        int length = data.length;
        
        String[] result = new String[length];
        
        for (int i = 0; i < length; i++) {
            result[i] = padOnLeft(data[i], width);
        }
        
        return result;
    }
    
    
    /**
     * <p>Return the string s padded with blanks on the right.</p>
     * 
     * <p>First, if s is <code>null</code>, replace s with an
     * empty string.</p>
     * 
     * <p>Then, if s has length greater than or equal to width,
     * return s.</p>
     * 
     * <p>Otherwise, return a string constructed with s on the
     * left and (width-s.length()) blanks on the right.</p>
     * 
     * @param s the string to pad
     * @param width the minimum width of the return string
     */
    public static String padOnRight(String s, int width) {
        if (s == null)
            s = "";
        
        int n = s.length();
        
        if (n >= width)
            return s;
        
        return suffixRepeatChar(s, ' ', width - n);
    }
    
    
    /**
     * <p>Returns a string array with all strings in the original
     * array padded using <code>Strings.padOnRight</code>.</p>
     *
     * <p>Returns <code>null</code> if the given data array is
     * <code>null</code>.</p>
     *
     * @param data the data to pad
     * @param width the minimum width of each return string
     */
    public static String[] padOnRight(String[] data, int width) {
        if (data == null)
            return null;
        
        int length = data.length;
        
        String[] result = new String[length];
        
        for (int i = 0; i < length; i++) {
            result[i] = padOnRight(data[i], width);
        }
        
        return result;
    }
    
    
    /**
     * <p>If s and t are both non-<code>null</code> then return
     * (s+blank+t).</p>
     * 
     * <p>Otherwise, if one string is <code>null</code>, return
     * the other.</p>
     * 
     * @param s string 1 to join
     * @param t string 2 to join
     */
    public static String joinWithSpace(String s, String t) {
        if (s == null)
            return t;
        
        if (t == null)
            return s;
        
        int width = s.length() + 1 + t.length();
        
        StringBuffer buffer = new StringBuffer(width);
        
        buffer.append(s);
        buffer.append(' ');
        buffer.append(t);
        
        return buffer.toString();
    }
    
    
    /**
     * <p>Returns an HTML safe string built from the given string
     * by replacing ampersand, less than, greater than, and quote
     * with their HTML encodings;
     * replaces tabs with spaces assuming a tab size of 4.</p>
     *
     * @param text the string to convert to HTML
     */
    public static String textToSafeHTML(String text) {
        return textToSafeHTML(text, 4);
    }
    
    
    /**
     * <p>Returns an HTML safe string built from the given string
     * by replacing ampersand, less than, greater than, and quote
     * with their HTML encodings;
     * replaces tabs with spaces assuming the given tab size.</p>
     *
     * @param text the string to convert to HTML
     * @param tabsize the tabsize for conversion of tabs to spaces
     */
    public static String textToSafeHTML(String text, int tabsize) {
        if (text == null)
            return null;
        
        int length = text.length();
        
        if (length == 0)
            return text;
        
        StringBuffer buffer = new StringBuffer(1024);
        
        int position = 0;
        
        if (tabsize <= 0)
            tabsize = 4;
        
        for (int i = 0; i < length; i++) {
            char c = text.charAt(i);
            
            if (c == '\n') {
                buffer.append(c);
                
                position = 0;
            }
            else if (c == '\t') {
                int spaces = tabsize - (position % tabsize);
                
                for (int k = 0; k < spaces; k++)
                    buffer.append(' ');

                position += spaces;
            }
            else {
                if (c == '&')
                    buffer.append("&amp;");
                else if (c == '<')
                    buffer.append("&lt;");
                else if (c == '>')
                    buffer.append("&gt;");
                else if (c == '\"')
                    buffer.append("&quot;");
                else
                    buffer.append(c);

                position++;
            }
        }
        
        return buffer.toString();
    }
}

