package basics.utl;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import basics.testing.Test;
import basics.testing.Units;
import basics.unexpected.Failure;

/**
 * @author kst
 */
public class SUtl
{
public static final String EMPTYSTRING = "";

/**
 * Compares to strings using simplified regex (?, *). String are converted into lower case so that
 * comparison is case-insensitive.
 * @param pattern - the input-pattern as string, may be null, or contain *, ?.
 * @param test - the tested string, may be null.
 * @return
 */
public static boolean matches(String pattern, String test)
{
   pattern = pattern == null ? "" : pattern.toLowerCase();
   test = test == null ? "" : test.toLowerCase();
   pattern = replaceAll(pattern, "*", ".*");
   pattern = replaceAll(pattern, "?", ".?");
   Pattern p = Pattern.compile(pattern);
   return p.matcher(test).matches();
}

/**
 * Replaces all occurrences of a searched string in a text.  
 * No regex. Exact matching.
 * This is much faster then java.util.String which uses regex.
 * @param text buffer to be searched
 * @param search string searched for
 * @param replace replacement
 * @return new string
 */
public static synchronized String replaceAll(String text, String search, String replace)
{
   if(search != null)
   {
      text = text == null ? "" : text;
      replace = replace == null ? "" : replace;
      for(int pos = text.indexOf(search), replaceLength = replace.length(); text.length() > 0
         && search.length() > 0 && pos > -1;)
      {
         String s1 = text.substring(0, pos);
         String s2 = text.substring(pos + search.length());
         int nextpos = s1.length() + replaceLength;
         text = new StringBuffer(s1).append(replace).append(s2).toString();
         pos = text.indexOf(search, nextpos);
      }
   }
   return text;
}

public static synchronized List<String> listTaggedToken(String text, String startTag, String endTag)
{
   List<String> list = new ArrayList<String>();
   if(text != null)
   {
      int x1 = 0, x2 = -1;
      while(true)
      {
         x1 = text.indexOf(startTag, x2 + 1) + 1;
         if(x1 == 0)
         {
            break;
         }
         x2 = text.indexOf(endTag, x1);
         if(x2 == -1)
         {
            SysUtl.trace(null, "Error in '%': closing tag '%' is missing, start tag was '%'.",
               text, endTag, startTag);
            x2 = text.length();
         }
         list.add(text.substring(x1, x2));
      }
   }
   return list;
}

/**
 * Replaces the first occurrence of a searched string in a text.  
 * No regex. Exact matching.
 * This is much faster then java.util.String which uses regex.
 * @param text buffer to be searched
 * @param search string searched for
 * @param replace replacement
 * @return new string
 */
public static synchronized String replaceFirst(String text, String search, String replace)
{
   if(search != null)
   {
      text = text == null ? "" : text;
      replace = replace == null ? "" : replace;
      int pos = text.indexOf(search), replaceLength = replace.length();
      if(text.length() > 0 && search.length() > 0 && pos > -1)
      {
         String s1 = text.substring(0, pos);
         String s2 = text.substring(pos + search.length());
         int nextpos = s1.length() + replaceLength;
         text = new StringBuffer(s1).append(replace).append(s2).toString();
         pos = text.indexOf(search, nextpos);
      }
   }
   return text;
}

/**
 * Convert a string into a boolean.
 * Understands true/false/yes/no. 
 * For any other input the default value dv will be returned.
 * String is trimmed, comparison is case insensitive.
 * @param input
 * @param dv
 * @return
 */
public static boolean toBoolean(Object input, boolean dv)
{
   boolean result = dv;
   if(input != null)
   {
      String s = input.toString().trim().toLowerCase();
      if(s.equalsIgnoreCase("true"))
         result = true;
      else
         if(s.equalsIgnoreCase("yes"))
            result = true;
         else
            if(s.equalsIgnoreCase("false"))
               result = false;
            else
               if(s.equalsIgnoreCase("no"))
                  result = false;
   }
   return result;
}

/**
 * Converts a string into a double. 
 * Returns default value if conversion fails.
 */
public static double toDouble(Object input, double dv)
{
   int i = 0;
   int sign = 1;
   double r = 0; // integer part
   // double f = 0; // fractional part
   double p = 1; // exponent of fractional part
   int state = 0; // 0 = int part, 1 = frac part
   String s = "";
   try
   {
      s = input.toString().trim().toLowerCase();
   }
   catch(Exception e)
   {
      return dv;
   }
   try
   {
      while(i < s.length() && Character.isWhitespace(s.charAt(i)))
         i++;
      if(i < s.length() && s.charAt(i) == '-')
      {
         sign = -1;
         i++;
      }
      else
         if(i < s.length() && s.charAt(i) == '+')
         {
            i++;
         }
      while(i < s.length())
      {
         char ch = s.charAt(i);
         if('0' <= ch && ch <= '9')
         {
            if(state == 0)
               r = r * 10 + ch - '0';
            else
               if(state == 1)
               {
                  p = p / 10;
                  r = r + p * (ch - '0');
               }
         }
         else
            if(ch == '.')
            {
               if(state == 0)
                  state = 1;
               else
                  return sign * r;
            }
            else
               if(ch == 'e' || ch == 'E')
               {
                  long e = (int)parseLong(s.substring(i + 1), 10);
                  return sign * r * Math.pow(10, e);
               }
               else
                  return sign * r;
         i++;
      }
      return sign * r;
   }
   catch(Exception e)
   {
      return dv;
   }
}

/**
 * Converts a string into a int. 
 * Returns default value if conversion fails.
 */
public static int toInt(Object s, int dv)
{
   long l = toLong(s, (long)dv);
   return (int)l;
}

/**
 * Converts a string of digits (decimal, octal or hex) to a long integer
 * @param s a string
 * @return the numeric value of the prefix of s representing a base 10 integer
 */
public static long toLong(Object v, long dv)
{
   int i = 0;
   try
   {
      String s = v.toString();
      while(i < s.length() && Character.isWhitespace(s.charAt(i)))
         i++;
      if(i < s.length() && s.charAt(i) == '0')
      {
         if(i + 1 < s.length() && (s.charAt(i + 1) == 'x' || s.charAt(i + 1) == 'X'))
            return parseLong(s.substring(i + 2), 16);
         else
            return parseLong(s, 8);
      }
      else
         return parseLong(s, 10);
   }
   catch(Exception e)
   {
      return dv;
   }
}

private static long parseLong(String s, int base)
{
   int i = 0;
   int sign = 1;
   long r = 0;
   while(i < s.length() && Character.isWhitespace(s.charAt(i)))
      i++;
   if(i < s.length() && s.charAt(i) == '-')
   {
      sign = -1;
      i++;
   }
   else
      if(i < s.length() && s.charAt(i) == '+')
      {
         i++;
      }
   while(i < s.length())
   {
      char ch = s.charAt(i);
      if('0' <= ch && ch < '0' + base)
         r = r * base + ch - '0';
      else
         if('A' <= ch && ch < 'A' + base - 10)
            r = r * base + ch - 'A' + 10;
         else
            if('a' <= ch && ch < 'a' + base - 10)
               r = r * base + ch - 'a' + 10;
            else
               return r * sign;
      i++;
   }
   return r * sign;
}

/**
 * Converts an object into a string. 
 * Returns default value if conversion fails.
 */
public static String toString(Object value, String dv)
{
   if(value == null)
      return dv;
   else
      return value.toString();
}

/**
 * Serialize a map into a string, ignore keys listed in hiddenValueKeys (which is
 * a comma separated list). Optionally skip empty map entries.
 */
@SuppressWarnings("unchecked")
public static String toString(Map map, String hiddenValueKeys, boolean hideEmpty)
{
   StringBuilder s = new StringBuilder();
   if(map != null)
   {
      List hidden = Arrays.asList(split(hiddenValueKeys, ",", true));
      boolean first = true;
      for(Object k : map.keySet())
      {
         Object o = map.get(k);
         String v = o == null ? "" : o.toString();
         if(!hideEmpty || !Empty.is(v))
         {
            if(!first)
               s.append(",");
            s.append(k);
            if(!hidden.contains(k))
            {
               s.append(":");
               s.append(v);
            }
            if(first)
               first = false;
         }
      }
      if(!first)
         s.append(";");
   }
   return s.toString();
}

/**
 * Prevent Null string by converting Null into EMPTYSTRING.
 */
public static String $(String s)
{
   return s != null ? s : EMPTYSTRING;
}

/**
 * Prevent Null string by converting Null into EMPTYSTRING.
 */
public static String $(Object o)
{
   return o != null ? o.toString() : EMPTYSTRING;
}

/**
 * Split string into an array, search for delimiter delim in the string to find 
 * array element borders.
 * No regex.
 */
public static synchronized String [] split(String s, String delim)
{
   if(s == null)
      return new String [0];
   int pos, lastpos, count = 0, delimLen = delim.length();
   for(lastpos = 0, pos = s.indexOf(delim, lastpos); pos > -1; pos = s.indexOf(delim, lastpos))
   {
      count++;
      lastpos = pos + delimLen;
   }
   String [] r = new String [count + 1];
   for(count = 0, lastpos = 0, pos = s.indexOf(delim, lastpos); pos > -1; pos = s.indexOf(delim,
      lastpos))
   {
      r[count++] = s.substring(lastpos, pos);
      lastpos = pos + delimLen;
   }
   r[count] = s.substring(lastpos);
   return r;
}

/**
 * Split string into an array, search for delimiter delim in the string to find 
 * array element borders. Elements may be trimmed, to avoid additional 
 * spaces "a, b, c" -> {"a", "b", "c"} instead of {"a", " b", " c"}.
 * No regex. 
 */
public static synchronized String [] split(String s, String delim, boolean trimElements)
{
   if(s == null)
      return new String [0];
   int pos, lastpos, count = 0, delimLen = delim.length();
   for(lastpos = 0, pos = s.indexOf(delim, lastpos); pos > -1; pos = s.indexOf(delim, lastpos))
   {
      count++;
      lastpos = pos + delimLen;
   }
   String [] r = new String [count + 1];
   if(trimElements)
   {
      for(count = 0, lastpos = 0, pos = s.indexOf(delim, lastpos); pos > -1; pos = s.indexOf(delim,
         lastpos))
      {
         r[count++] = s.substring(lastpos, pos).trim();
         lastpos = pos + delimLen;
      }
      r[count] = s.substring(lastpos).trim();
   }
   else
   {
      for(count = 0, lastpos = 0, pos = s.indexOf(delim, lastpos); pos > -1; pos = s.indexOf(delim,
         lastpos))
      {
         r[count++] = s.substring(lastpos, pos);
         lastpos = pos + delimLen;
      }
      r[count] = s.substring(lastpos);
   }
   return r;
}

/**
 * comma separated string array, elements are trimmed, empty or null elements a skipped.
 * @param commaSeparatedString comma separated string
 * @return string array with trimmed elements, never null
 */
public static synchronized String [] split(String commaSeparatedString)
{
   if(commaSeparatedString == null)
      return new String [] { };
   String [] s = split(commaSeparatedString, ",", true);
   int j = 0, i = 0;
   for(; i < s.length; i++)
      if(!Empty.is(s[i]))
         s[j++] = s[i];
   if(j == i)
      return s;
   String [] r = new String [j];
   System.arraycopy(s, 0, r, 0, j);
   return r;
}

/**
 * String compare made easy.
 */
public static boolean equals(String s, String t, boolean compareNull, boolean doTrim)
{
   if(s == null || t == null)
   {
      if(compareNull)
         return(s == null && t == null);
      else
         return false;
   }
   if(doTrim)
      return s.trim().equals(t.trim());
   else
      return s.equals(t);
}

/**
 * String compare made easy.
 */
public static boolean equalsIgnoreCase(String s, String t, boolean compareNull, boolean doTrim)
{
   if(s == null || t == null)
   {
      if(compareNull)
         return(s == null && t == null);
      else
         return false;
   }
   if(doTrim)
      return s.trim().equalsIgnoreCase(t.trim());
   else
      return s.equalsIgnoreCase(t);
}

public static boolean isGreater(String s, String t)
{
   return s.compareTo(t) > 0;
}

public static boolean isSmaller(String s, String t)
{
   return s.compareTo(t) < 0;
}

/*
 * Join a collection, build a string by concatenation of elements, separate elements by delimiter.
 */
public static synchronized String join(String [] m, String delim)
{
   StringBuilder sb = new StringBuilder();
   if(m != null && m.length > 0)
   {
      sb.append(m[0]);
      for(int i = 1; i < m.length; i++)
      {
         sb.append(delim);
         sb.append(m[i]);
      }
   }
   String r = sb.toString();
   if(r.endsWith(delim))
   {
      r = r.substring(0, r.length() - delim.length());
   }
   return(r);
}
/*
 * Join starts at pos, uses delim
 */
public static synchronized String join(int pos, String [] m, String delim)
{
   StringBuilder sb = new StringBuilder();
   if(m != null && m.length > 1)
   {
      sb.append(m[1]);
      for(int i = 2; i < m.length; i++)
      {
         sb.append(delim);
         sb.append(m[i]);
      }
   }
   String r = sb.toString();
//   if(r.endsWith(delim))
//   {
//      r = r.substring(0, r.length() - delim.length());
//   }
   return(r);
}

/*
 * Join a collection, build a string by concatenation of elements, separate elements by delimiter.
 */
public static synchronized String join(Object...m)
{
   String delim = ";";
   StringBuilder sb = new StringBuilder();
   if(m != null && m.length > 0)
   {
      sb.append(m[0].toString());
      for(int i = 1; i < m.length; i++)
      {
         sb.append(delim);
         sb.append(m[i].toString());
      }
   }
   String r = sb.toString();
   if(r.endsWith(delim))
   {
      r = r.substring(0, r.length() - delim.length());
   }
   return(r);
}

public static synchronized String join(Map<String, ?> m)
{
   String delim = ";";
   StringBuilder sb = new StringBuilder();
   Iterator<String> keys = m.keySet().iterator();
   while(keys.hasNext())
   {
      String k = keys.next();
      String v = m.get(k).toString();
      sb.append(k);
      sb.append("=");
      sb.append(v);
      sb.append(delim);
   }
   String r = sb.toString();
   if(r.endsWith(delim))
   {
      r = r.substring(0, r.length() - delim.length());
   }
   return(r);
}

/*
 * Join a collection, build a string by concatenation of elements, separate elements by delimiter.
 */
public static synchronized String join(List<?> s, String delim)
{
   StringBuilder sb = new StringBuilder();
   Object [] m = s.toArray();
   if(m != null && m.length > 0)
   {
      sb.append(m[0]);
      for(int i = 1; i < m.length; i++)
      {
         sb.append(delim);
         sb.append(m[i]);
      }
   }
   String r = sb.toString();
   if(r.endsWith(delim))
   {
      r = r.substring(0, r.length() - delim.length());
   }
   return(r);
}

/*
 * Join a collection, build a string by concatenation of elements, separate elements by delimiter.
 */
public static synchronized String join(Vector<?> s, String delim)
{
   StringBuilder sb = new StringBuilder();
   Object [] m = s.toArray();
   if(m != null && m.length > 0)
   {
      sb.append(m[0]);
      for(int i = 1; i < m.length; i++)
      {
         sb.append(delim);
         sb.append(m[i]);
      }
   }
   String r = sb.toString();
   if(r.endsWith(delim))
   {
      r = r.substring(0, r.length() - delim.length());
   }
   return(r);
}

/*
 * Join a collection, build a string by concatenation of elements, separate elements by delimiter.
 */
public static synchronized String join(Collection<?> s, String delim)
{
   StringBuilder sb = new StringBuilder();
   Object [] m = s.toArray();
   if(m != null && m.length > 0)
   {
      sb.append(m[0]);
      for(int i = 1; i < m.length; i++)
      {
         sb.append(delim);
         sb.append(m[i]);
      }
   }
   String r = sb.toString();
   if(r.endsWith(delim))
   {
      r = r.substring(0, r.length() - delim.length());
   }
   return(r);
}

/*
 * Join a collection, build a string by concatenation of elements, separate elements by delimiter.
 */
public static synchronized String join(double [] m, String delim)
{
   StringBuilder sb = new StringBuilder();
   if(m != null && m.length > 0)
   {
      sb.append(m[0]);
      for(int i = 1; i < m.length; i++)
      {
         sb.append(delim);
         sb.append(m[i]);
      }
   }
   String r = sb.toString();
   if(r.endsWith(delim))
   {
      r = r.substring(0, r.length() - delim.length());
   }
   return(r);
}

/*
 * Join a collection, build a string by concatenation of elements, separate elements by delimiter.
 */
public static synchronized String join(double [] m, String delim, int precision)
{
   StringBuilder sb = new StringBuilder();
   if(m != null && m.length > 0)
   {
      sb.append(NumUtl.round(m[0], 1));
      for(int i = 1; i < m.length; i++)
      {
         sb.append(delim);
         sb.append(NumUtl.round(m[i], precision));
      }
   }
   String r = sb.toString();
   if(r.endsWith(delim))
   {
      r = r.substring(0, r.length() - delim.length());
   }
   return(r);
}

/**
 * Shorten a string to a given length if the string exceeds the length.
 */
public static String prune(String input, int len)
{
   if(input != null && input.length() > len)
      input = input.substring(0, len);
   return input;
}

/**
 * Take any kind of object and return a string (never null).
 * The return string will not contain any whitespace characters.
 */
public static String xtrim(Object input)
{
   String output = $(input);
   output = output.replaceAll("\\s", " ");
   output = output.replaceAll(" {2,}", " ");
   return output.trim();
}

/**
 * Removes spaces on the left side of a token.
 */
public static String ltrim(String input)
{
   if(input == null)
      return null;
   while(input.length() > 0 && input.charAt(0) == ' ')
      input = input.substring(1);
   return input;
}

/**
 * Check if a string may be a decimal number 
 * by checking that it contains only +|-<digits>.<digits>.
 * This may no work for all locales.
 */
public static boolean isDigitsOnly(String input)
{
   int points = 0;
   if(input == null)
      return false;
   int k = 0;
   for(int i = 0; i < input.length(); i++)
   {
      Character ch = input.charAt(i);
      if(Character.isDigit(ch))
      {
         k++;
         continue;
      }
      if(k == 0 && (ch == '+' || ch == '-'))
      {
         k++;
         continue;
      }
      if(ch == '.' && 0 == points++)
      {
         k++;
         continue;
      }
      return false;
   }
   return true;
}

/**
 * Extract all digits, a leading -, and a decimal point from a string.
 * Run isDigitsOnly before this! 
 * May fail for some locales.
 */
public static String extractDigits(String input)
{
   int x = count(input, ".");
   boolean usepoint = x == 1;
   if(input == null)
      return "";
   int k = 0;
   StringBuilder s = new StringBuilder();
   for(int i = 0; i < input.length(); i++)
   {
      Character ch = input.charAt(i);
      if(Character.isDigit(ch))
      {
         k++;
         s.append(ch);
      }
      if(k == 0 && ch == '-')
      {
         k++;
         s.append(ch);
      }
      if(usepoint && ch == '.')
      {
         k++;
         s.append(ch);
      }
   }
   return s.toString();
}

/**
 * Removes a suffix from a string.
 * @param input may be null
 * @param suffix 
 * @return may be null
 */
public static String removeSuffix(String input, String suffix)
{
   if(input != null && input.endsWith(suffix))
      return input.substring(0, input.length() - suffix.length());
   else
      return input;
}

public static String removeSuffixIgnoreCase(String input, String suffix)
{
   if(input != null && input.toLowerCase().endsWith(suffix.toLowerCase()))
      return input.substring(0, input.length() - suffix.length());
   else
      return input;
}

/**
 * Removes a prefix from a string.
 * @param input may be null
 * @param prefix 
 * @return may be null
 */
public static String removePrefix(String input, String prefix)
{
   if(input != null && input.startsWith(prefix))
      return input.substring(prefix.length());
   else
      return input;
}

public static String removePrefixIgnoreCase(String input, String prefix)
{
   if(input != null && input.toLowerCase().startsWith(prefix.toLowerCase()))
      return input.substring(prefix.length());
   else
      return input;
}

/**
 * Extincts a sequence out of a string.
 * The removed sequence starts at an offset an has a given length.
 * Offset is corrected to 0 if input was negative.
 * Length is pruned if it exceeds the buffer.
 * @param text
 * @param offset
 * @param length
 * @return null if input was null or a string which may be empty
 */
public static String extinct(String text, int offset, int length)
{
   if(text == null)
      return null;
   int size = text.length();
   if(size == 0)
      return "";
   if(offset >= size)
      return text;
   offset = offset < 0 ? 0 : offset;
   length = offset + length > size ? size - offset : length;
   StringBuilder s = new StringBuilder();
   s.append(text.substring(0, offset));
   s.append(text.substring(offset + length));
   return s.toString();
}

/**
 * Extracts a sequence out of a string.
 * The extracted sequence starts at an offset an has a given length.
 * Offset is corrected to 0 if input was negative.
 * Length is pruned if it exceeds the buffer.
 * @param text
 * @param offset
 * @param length
 * @return null if input was null or a string which may be empty
 */
public static String extract(String text, int offset, int length)
{
   if(text == null)
      return null;
   int size = text.length();
   offset = offset < 0 ? 0 : offset;
   offset = offset > size - 1 ? size - 1 : offset;
   length = offset + length > size ? size - offset : length;
   return text.substring(offset, offset + length);
}

/**
 * Check if first buffer contains second string.
 * Ignores case. Null/Empty contains nothing
 * and is not contained anywhere.
 */
public static boolean contains(String buffer, String searchstring)
{
   if(Empty.is(buffer) || Empty.is(searchstring))
      return false;
   return buffer.toLowerCase().indexOf(searchstring.toLowerCase()) > -1;
}

/**
 * Check is an array of strings contains the second arg's string.
 * Always false, if array is null. But does find a null entry in the 
 * array if the search string c is null.
 * @param array
 * @param searchstring
 * @return
 */
public static boolean contains(String [] array, String searchstring)
{
   if(array == null)
      return false;
   if(searchstring == null)
   {
      for(String q : array)
         if(q == null)
            return true;
   }
   else
   {
      for(String q : array)
         if(q != null && q.equalsIgnoreCase(searchstring))
            return true;
   }
   return false;
}

/**
 * Cut off anything at the last occurrence of a given token. 
 */
public static String cutRestOffAtLast(String input, String token)
{
   if(input != null)
   {
      int x = input.lastIndexOf(token);
      if(x > -1)
         input = input.substring(0, x);
   }
   return input;
}

/**
 * Cut off anything at the last occurrence of a given token. 
 */
public static String cutRestOffAfterLast(String input, String token)
{
   if(input != null)
   {
      int x = input.lastIndexOf(token);
      if(x > -1)
         input = input.substring(0, x + token.length());
   }
   return input;
}

/**
 * Cut off anything at the first occurrence of a given token. 
 */
public static String cutRestOffAtFirst(String input, String token)
{
   if(input != null)
   {
      int x = input.indexOf(token);
      if(x > -1)
         input = input.substring(0, x);
   }
   return input;
}

/**
 * Cut off anything at the first occurrence of a given token. 
 */
public static String cutRestOffAfterFirst(String input, String token)
{
   if(input != null)
   {
      int x = input.indexOf(token);
      if(x > -1)
         input = input.substring(0, x + token.length());
   }
   return input;
}

/**
 * Counts the number of occurrences of a given token.
 */
public static int count(String buffer, String token)
{
   if(Empty.is(buffer) || Empty.is(token))
      return 0;
   int x = 0;
   int pos = 0;
   int offset = token.length();
   while(true)
   {
      pos = buffer.indexOf(token, pos);
      if(pos == -1)
         break;
      x++;
      pos += offset;
   }
   return x;
}

/**
 * Return a concatenation of x signs. Never null, may be empty.
 */
public static String repeat(String sign, int x)
{
   if(x == 0 || Empty.is(sign))
      return "";
   StringBuilder res = new StringBuilder();
   for(int i = 0; i < x; i++)
      res.append(sign);
   return res.toString();
}

public static String sprint(String msg, Object...values)
{
   if(Empty.is(msg))
      return "";
   boolean b = msg.contains("\\%");
   if(b)
      msg = replaceAll(msg, "\\%", "(percent)");
   for(Object v : values)
   {
      int x = msg.indexOf("%");
      if(x < 0)
         break;
      try
      {
         msg = replaceFirst(msg, "%", "" + v);
      }
      catch(Exception e)
      {
         SysUtl.terror(e, null);
      }
   }
   return b ? replaceAll(msg, "(percent)", "%") : msg;
}

/**
 * Looks for all matches of (regex+suffix) and replaces the suffix by given suffixReplacement.
 * Example: input is "a- aa- aaa- -bb- cc -z."
 * regex  : \\w
 * suffix : -
 * replace: +
 * RESULT: "a+ aa+ aaa+ -bb- cc -z."
 * @return never null, may be "".
 */
public static String matchAllReplaceSuffix(String input, String regex, String suffix,
   String suffixReplacement)
{
   if(Empty.is(input))
      return "";
   Pattern rule = Pattern.compile(regex + suffix);
   int n = suffix.length();
   Matcher m = rule.matcher(input);
   while(m.find())
   {
      int x = m.end();
      StringBuilder t = new StringBuilder();
      t.append(input.substring(0, x - n));
      t.append(suffixReplacement);
      t.append(input.substring(x + n - 1));
      input = t.toString();
      m = rule.matcher(input);
   }
   return input;
}

/**
 * Looks for all matches of (prefix+regex) and replaces the prefix by given prefixReplacement.
 * Example: input is "a- aa- aaa- -bb- cc -z."
 * regex  : \\w
 * prefix : -
 * replace: +
 * RESULT: "a- aa- aaa- +bb- cc +z."
 * @return never null, may be "".
 */
public static String matchAllReplacePrefix(String input, String regex, String prefix,
   String prefixReplacement)
{
   if(Empty.is(input))
      return "";
   Pattern rule = Pattern.compile(prefix + regex);
   int n = prefix.length();
   Matcher m = rule.matcher(input);
   while(m.find())
   {
      int x = m.start();
      StringBuilder t = new StringBuilder();
      t.append(input.substring(0, x));
      t.append(prefixReplacement);
      t.append(input.substring(m.end() - n));
      input = t.toString();
      m = rule.matcher(input);
   }
   return input;
}

public static String tag(String start, String content, String end)
{
   StringBuilder s = new StringBuilder();
   s.append(start);
   s.append(content);
   s.append(end);
   return s.toString();
}

/**
 * Looks for all matches of (prefix+regex) and replaces the prefix by given prefixReplacement.
 * Example: input is "a- aa- aaa- -bb- cc -z."
 * regex  : \\w
 * prefix : -
 * replace: +
 * RESULT: "a- aa- aaa- +bb- cc +z."
 * @return never null, may be "".
 */
public static String matchAllReplaceInfix(String input, String regexLeft, String infix,
   String regexRight, String infixReplacement)
{
   if(Empty.is(input))
      return "";
   Pattern rule = Pattern.compile(regexLeft + infix + regexRight);
   Pattern leftmatch = Pattern.compile(regexLeft);
   int n = infix.length();
   Matcher m = rule.matcher(input);
   while(m.find())
   {
      StringBuilder t = new StringBuilder();
      t.append(input.substring(0, m.start()));
      String match = input.substring(m.start(), m.end());
      Matcher i = leftmatch.matcher(match); // find end of regexLeft (w/o infix).
      if(!i.find())
         throw new Failure("Infix match, inner left match failed for " + match);
      t.append(match.substring(0, i.end()));
      t.append(infixReplacement);
      t.append(match.substring(i.end() + n));
      t.append(input.substring(m.end()));
      input = t.toString();
      m = rule.matcher(input);
   }
   return input;
}

public static long timespanToMillis(long timeSpan, String unit)
{
   long factor = 1;
   unit = unit == null ? "ms" : unit.trim().toLowerCase();
   if(unit.length() < 1)
      unit = "ms";
   if(unit.equals("t"))
      factor = 8640000;
   else
      if(unit.equals("h"))
         factor = 360000;
      else
         if(unit.equals("m"))
            factor = 60000;
         else
            if(unit.equals("s"))
               factor = 1000;
   return timeSpan * factor;
}

public static void unittest()
{
   Test.assertEqualsTrue(EMPTYSTRING, "", "empty string should be empty");
   Test.assertNotNull($((Object)null), "$(Object) is null");
   Test.assertNotNull($((String)null), "$(String) is null");
   Units.assertEqualsTrue(ltrim(" a "), "a ", "ltrim failed");
   Units.assertEqualsTrue(xtrim(" a \n,\t b "), "a , b", "xtrim failed: '" + xtrim(" a \n,\t b ")
      + "'");
   Units.assertEqualsTrue(prune(" abc ", 3), " ab", "prune failed");
   Units.assertEqualsTrue(repeat("x", 3), "xxx", "repeat failed");
   Units.assertTrue(count("1.23", ".") == 1, "1 . expected");
   Units.assertTrue(count("1.2.3", ".") == 2, "2 . expected");
   Units.assertTrue(count("1.aa2aa.3", "a") == 4, "4 a expected");
   Units.assertTrue(contains("abcdef", "bc"), "bc is in abcdef");
   Units.assertTrue(contains("abcdef", "BC"), "BC is in abcdef");
   Units.assertTrue(contains("ABCDEF", "bc"), "bc is in ABCDEF");
   Units.assertTrue(contains("abcdef", "ab"), "ab is in abcdef");
   Units.assertTrue(contains("abcdef", "ef"), "ef is in abcdef");
   Units.assertFalse(contains("abcdef", "exf"), "exf is not in abcdef");
   Units.assertTrue(contains(new String [] { "abcdef", "", null, "xx" }, "xx"),
      "xx is in the array");
   Units.assertTrue(isDigitsOnly("-123.04"), "negative number not recognized");
   Units.assertTrue(isDigitsOnly("+123.04"), "positive number not recognized");
   Units.assertFalse(isDigitsOnly("123.04."), "false number recognized");
   Units.assertEqualsTrue(extractDigits("12300"), "12300", "expected 12300 not "
      + extractDigits("12300"));
   Units.assertEqualsTrue(extractDigits("is-123.04$"), "-123.04", "expected -123.04 not "
      + extractDigits("is-123.04$"));
   Units.assertEqualsTrue(extractDigits("is+123.04$"), "123.04", "expected 123.04 not "
      + extractDigits("is+123.04$"));
   Units.assertFalse(contains(new String [] { "abcdef", "", null, "xx" }, "xy"),
      "xy is not in the array");
   Units.assertEqualsTrue(cutRestOffAtFirst("ababcababx", "abc"), "ab", "1: ab expected not "
      + cutRestOffAtFirst("ababcababx", "abc"));
   Units.assertEqualsTrue(cutRestOffAtFirst("ababEnd.    ab  ab x ", "End."), "abab",
      "2: abab expected not " + cutRestOffAfterFirst("ababEnd.    ab  ab x ", "End."));
   Units.assertEqualsTrue(cutRestOffAfterFirst("ababababx", "ab"), "ab", "3: ab expected not "
      + cutRestOffAtLast("ababababx", "ab"));
   Units.assertEqualsTrue(cutRestOffAfterFirst("ababEnd.    ab  ab x ", "End."), "ababEnd.",
      "4: ababEnd. expected not " + cutRestOffAfterFirst("ababEnd.    ab  ab x ", "End."));
   Units.assertEqualsTrue(cutRestOffAtLast("ababababx", "ab"), "ababab", "5: ababab expected not "
      + cutRestOffAtLast("ababababx", "ab"));
   Units.assertEqualsTrue(cutRestOffAtLast("ababab abx", " "), "ababab", "6: ababab expected not "
      + cutRestOffAtLast("ababab abx", " "));
   Units.assertEqualsTrue(cutRestOffAfterLast("ababababx", "ab"), "abababab",
      "7: abababab expected not " + cutRestOffAfterLast("ababababx", "ab"));
   Units.assertEqualsTrue(cutRestOffAfterLast("abababcacx", "ab"), "ababab",
      "8: ababab expected not " + cutRestOffAfterLast("abababcacx", "ab"));
   Units.assertTrue(matches("*a?b*c", "XXXXaXbXXXXc"), "*a?b*c should match XXXXaXbXXXXc");
   Units.assertTrue(matches("*a?b*c", "XXXXAXBXXXXC"), "*a?b*c should match XXXXAXBXXXXC");
   Units.assertFalse(matches("*a?b*c", "XXXXaXYbXXXXc"), "*a?b*c should not match XXXXaXYbXXXXc");
   Units.assertFalse(matches("*a?b*c", "XXXXnXbXXXXc"), "*a?b*c should not match XXXXnXbXXXXc");
   Units.assertTrue(equals(" a ", "a", true, true), "equals failed, 1");
   Units.assertFalse(equals(" a ", "a", true, false), "equals failed, 2");
   Units.assertFalse(equals(" a ", null, true, false), "equals failed, 3");
   Units.assertFalse(equals(null, "a", true, false), "equals failed, 4");
   Units.assertTrue(equals(null, null, true, false), "equals failed, 5");
   Units.assertTrue(equalsIgnoreCase(" A ", "a", true, true), "equals failed, i1");
   Units.assertFalse(equalsIgnoreCase(" a ", "a", true, false), "equals failed, i2");
   Units.assertFalse(equalsIgnoreCase(" a ", null, true, false), "equals failed, i3");
   Units.assertFalse(equalsIgnoreCase(null, "a", true, false), "equals failed, i4");
   Units.assertTrue(equalsIgnoreCase(null, null, true, false), "equals failed, i5");
   Units.assertEqualsTrue(replaceAll("aabbcc", "aa", "xx"), "xxbbcc", "replace failed");
   Units.assertEqualsTrue(replaceAll("aabbcc", "bb", "xx"), "aaxxcc", "replace failed");
   Units.assertTrue(toBoolean("true", false) == true, "convertion failed: bool");
   Units.assertTrue(toBoolean("yes", false) == true, "convertion failed: bool");
   Units.assertTrue(toBoolean("no", true) == false, "convertion failed: bool");
   Units.assertTrue(toBoolean("false", true) == false, "convertion failed: bool");
   Units.assertTrue(toDouble("1.12", 0D) == 1.12D, "conversion failed: double");
   Units.assertTrue(toDouble("-1.12", 0D) == -1.12D, "conversion failed: double");
   Units.assertTrue(toInt("3", 0) == 3, "conversion failed: int");
   Units.assertTrue(toInt("-3", 0) == -3, "conversion failed: int");
   Units.assertTrue(toLong("3", 0) == 3L, "conversion failed: long");
   Units.assertTrue(toLong("-3", 0) == -3L, "conversion failed: long");
   Units.assertTrue(split("a,b,c", ",").length == 3, "split failed");
   Units.assertEqualsTrue(split("a,b,c", ",")[0], "a", "split failed");
   Units.assertEqualsTrue(split("a,b,c", ",")[2], "c", "split failed");
   Units.assertTrue(equals(" abc ", "abc", true, true), "equals failed1");
   Units.assertTrue(equals("abc", "abc", false, false), "equals failed2");
   Units.assertTrue(equals(null, null, true, true), "equals failed 3");
   Units.assertFalse(equals("", null, false, false), "equals failed 4");
   Units.assertFalse(equals("", null, true, false), "equals failed 5");
   Units.assertTrue(equalsIgnoreCase(" ABC ", "abc", true, true), "equalsIgnoreCase failed1");
   Units.assertTrue(equalsIgnoreCase("abc", "ABC", false, false), "equalsIgnoreCase failed2");
   Units.assertTrue(equalsIgnoreCase(null, null, true, true), "equalsIgnoreCase failed 3");
   Units.assertFalse(equalsIgnoreCase("", null, false, false), "equalsIgnoreCase failed 4");
   Units.assertFalse(equalsIgnoreCase("", null, true, false), "equalsIgnoreCase failed 5");
   Units.assertEqualsTrue(removeSuffix(" abc zzz", "zzz"), " abc ", "removeSuffix failed");
   Units.assertEqualsTrue(removePrefix(" abc zzz", " abc "), "zzz", "removePrefix failed");
   Units.assertEqualsTrue(removeSuffixIgnoreCase(" abc zZz", "ZzZ"), " abc ",
      "removeSuffixIgnoreCase failed");
   Units.assertEqualsTrue(removePrefixIgnoreCase(" aBc zzz", " AbC "), "zzz",
      "removePrefixIgnoreCase failed");
   Units.assertEqualsTrue(extract("01234567", 1, 2), "12", "5 expected 12 not "
      + extract("01234567", 1, 2));
   Units.assertEqualsTrue(extract("01234567", -1, 20), "01234567", "6 expected 01234567 not "
      + extract("01234567", -1, 20));
   Units.assertEqualsTrue(extract("01234567", 0, 0), "", "7 expected '' not "
      + extract("01234567", 0, 0));
   Units.assertEqualsTrue(extinct("01234567", 0, 0), "01234567", "7 expected '01234567' not "
      + extinct("01234567", 0, 0));
   Units.assertEqualsTrue(sprint("%km/h %PS %\\%", 120, 75, 80), "120km/h 75PS 80%",
      "expected '120km/h 75PS 80%' not " + sprint("%km/h %PS %\\%", 120, 75, 80));
   Test.assertEqualsTrue(matchAllReplaceSuffix("-b a- c", "\\w", "-", ""), "-b a c",
      "1, not -b a c: '" + matchAllReplaceSuffix("-b a- c", "\\w", "-", "") + "'");
   Test.assertEqualsTrue(matchAllReplaceSuffix("b a- -c", "\\w", "-", ""), "b a -c",
      "2, not b a -c: '" + matchAllReplaceSuffix("b a- -c", "\\w", "-", "") + "'");
   Test.assertEqualsTrue(matchAllReplaceSuffix("b a c", "\\w", "-", ""), "b a c", "3, not b a c: "
      + matchAllReplaceSuffix("b a c", "\\w", "-", ""));
   Test.assertEqualsTrue(matchAllReplacePrefix("-b a- c", "\\w", "-", ""), "b a- c",
      "1, not b a- c: '" + matchAllReplacePrefix("-b a- c", "\\w", "-", "") + "'");
   Test.assertEqualsTrue(matchAllReplacePrefix("-b -a- c", "\\w", "-", ""), "b a- c",
      "1, not b a- c: '" + matchAllReplacePrefix("-b -a- c", "\\w", "-", "") + "'");
   String r = matchAllReplaceInfix("aa-bb bb cc dd-ee", "\\w", "-", "\\w", "+");
   String ex = "aa+bb bb cc dd+ee";
   Test.assertEqualsTrue(r, ex, "'" + ex + "' expected, not '" + r + "'");
   Units.assertEqualsTrue(extinct("", 0, 0), "", "expected '' not " + extinct("", 0, 0));
   Units.assertEqualsTrue(extinct("a", 0, 0), "a", "expected 'a' not " + extinct("a", 0, 0));
   Units.assertEqualsTrue(extinct("a", 1, 0), "a", "expected 'a' not " + extinct("a", 1, 0));
   Units.assertEqualsTrue(extinct("a", 1, 1), "a", "expected 'a' not " + extinct("a", 1, 1));
   Units.assertEqualsTrue(extinct("01234567", 1, 2), "034567", "1 expected 034567 not "
      + extinct("01234567", 1, 2));
   Units.assertEqualsTrue(extinct("01234567", 0, 3), "34567", "2 expected 34567 not "
      + extinct("01234567", 0, 3));
   Units.assertEqualsTrue(extinct("01234567", -1, 3), "34567", "3 expected 34567 not "
      + extinct("01234567", -1, 3));
   Units.assertEqualsTrue(extinct("01234567", 1, 20), "0", "4 expected 0 not "
      + extinct("01234567", 1, 20));
}
}
