Merge branch 'master' of https://source.jalview.org/git/jalviewjs.git
[jalviewjs.git] / src / javajs / util / PT.java
index fa9cbe3..e6a27b4 100644 (file)
-/* $RCSfile$\r
- * $Author: hansonr $\r
- * $Date: 2007-04-26 16:57:51 -0500 (Thu, 26 Apr 2007) $\r
- * $Revision: 7502 $\r
- *\r
- * Copyright (C) 2005  The Jmol Development Team\r
- *\r
- * Contact: jmol-developers@lists.sf.net\r
- *\r
- *  This library is free software; you can redistribute it and/or\r
- *  modify it under the terms of the GNU Lesser General Public\r
- *  License as published by the Free Software Foundation; either\r
- *  version 2.1 of the License, or (at your option) any later version.\r
- *\r
- *  This library is distributed in the hope that it will be useful,\r
- *  but WITHOUT ANY WARRANTY; without even the implied warranty of\r
- *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\r
- *  Lesser General Public License for more details.\r
- *\r
- *  You should have received a copy of the GNU Lesser General Public\r
- *  License along with this library; if not, write to the Free Software\r
- *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.\r
- */\r
-\r
-package javajs.util;\r
-\r
-import java.lang.reflect.Array;\r
-import java.util.Map;\r
-import java.util.Map.Entry;\r
-\r
-import javajs.J2SIgnoreImport;\r
-import javajs.api.JSONEncodable;\r
-\r
-/**\r
- * a combination of Parsing and Text-related utility classes\r
- * \r
- * @author hansonr\r
- * \r
- */\r
-\r
-@J2SIgnoreImport(value = { java.lang.reflect.Array.class })\r
-public class PT {\r
-\r
-  public static int parseInt(String str) {\r
-    return parseIntNext(str, new int[] {0});\r
-  }\r
-\r
-  public static int parseIntNext(String str, int[] next) {\r
-    int cch = str.length();\r
-    if (next[0] < 0 || next[0] >= cch)\r
-      return Integer.MIN_VALUE;\r
-    return parseIntChecked(str, cch, next);\r
-  }\r
-\r
-  public static int parseIntChecked(String str, int ichMax, int[] next) {\r
-    boolean digitSeen = false;\r
-    int value = 0;\r
-    int ich = next[0];\r
-    if (ich < 0)\r
-      return Integer.MIN_VALUE;\r
-    int ch;\r
-    while (ich < ichMax && isWhiteSpace(str, ich))\r
-      ++ich;\r
-    boolean negative = false;\r
-    if (ich < ichMax && str.charAt(ich) == 45) { //"-"\r
-      negative = true;\r
-      ++ich;\r
-    }\r
-    while (ich < ichMax && (ch = str.charAt(ich)) >= 48 && ch <= 57) {\r
-      value = value * 10 + (ch - 48);\r
-      digitSeen = true;\r
-      ++ich;\r
-    }\r
-    if (!digitSeen)// || !checkTrailingText(str, ich, ichMax))\r
-      value = Integer.MIN_VALUE;\r
-    else if (negative)\r
-      value = -value;\r
-    next[0] = ich;\r
-    return value;\r
-  }\r
-\r
-  public static boolean isWhiteSpace(String str, int ich) {\r
-    char ch;\r
-    return (ich >= 0 && ((ch = str.charAt(ich)) == ' ' || ch == '\t' || ch == '\n'));\r
-  }\r
-\r
-  /**\r
-   * A float parser that is 30% faster than Float.parseFloat(x) and also accepts\r
-   * x.yD+-n\r
-   * \r
-   * @param str\r
-   * @param ichMax\r
-   * @param next\r
-   *        pointer; incremented\r
-   * @param isStrict\r
-   * @return value or Float.NaN\r
-   */\r
-  public static float parseFloatChecked(String str, int ichMax, int[] next,\r
-                                         boolean isStrict) {\r
-    boolean digitSeen = false;\r
-    int ich = next[0];\r
-    if (isStrict && str.indexOf('\n') != str.lastIndexOf('\n'))\r
-      return Float.NaN;\r
-    while (ich < ichMax && isWhiteSpace(str, ich))\r
-      ++ich;\r
-    boolean negative = false;\r
-    if (ich < ichMax && str.charAt(ich) == '-') {\r
-      ++ich;\r
-      negative = true;\r
-    }\r
-    // looks crazy, but if we don't do this, Google Closure Compiler will \r
-    // write code that Safari will misinterpret in a VERY nasty way -- \r
-    // getting totally confused as to long integers and double values\r
-    \r
-    // This is Safari figuring out the values of the numbers on the line (x, y, then z):\r
-  \r
-    //  ATOM 1241 CD1 LEU A 64 -2.206 36.532 31.576 1.00 60.60 C\r
-    //  e=1408749273\r
-    //  -e =-1408749273\r
-    //  ATOM 1241 CD1 LEU A 64 -2.206 36.532 31.576 1.00 60.60 C\r
-    //  e=-1821066134\r
-    //  e=36.532\r
-    //  ATOM 1241 CD1 LEU A 64 -2.206 36.532 31.576 1.00 60.60 C\r
-    //  e=-1133871366\r
-    //  e=31.576\r
-    //\r
-    //  "e" values are just before and after the "value = -value" statement.\r
-    \r
-    int ch = 0;\r
-    float ival = 0f;\r
-    float ival2 = 0f;\r
-    while (ich < ichMax && (ch = str.charAt(ich)) >= 48 && ch <= 57) {\r
-      ival = (ival * 10f) + (ch - 48)*1f;\r
-      ++ich;\r
-      digitSeen = true;\r
-    }\r
-    boolean isDecimal = false;\r
-    int iscale = 0;\r
-    int nzero = (ival == 0 ? -1 : 0);\r
-    if (ch == '.') {\r
-      isDecimal = true;\r
-      while (++ich < ichMax && (ch = str.charAt(ich)) >= 48 && ch <= 57) {\r
-        digitSeen = true;\r
-        if (nzero < 0) {\r
-          if (ch == 48) { \r
-            nzero--;\r
-            continue;\r
-          }\r
-          nzero = -nzero;\r
-        } \r
-        if (iscale  < decimalScale.length) {\r
-          ival2 = (ival2 * 10f) + (ch - 48)*1f;\r
-          iscale++;\r
-        }\r
-      }\r
-    }\r
-    float value;\r
-    \r
-    // Safari breaks here intermittently converting integers to floats \r
-    \r
-    if (!digitSeen) {\r
-      value = Float.NaN;\r
-    } else if (ival2 > 0) {\r
-      value = ival2 * decimalScale[iscale - 1];\r
-      if (nzero > 1) {\r
-        if (nzero - 2 < decimalScale.length) {\r
-          value *= decimalScale[nzero - 2];\r
-        } else {\r
-          value *= Math.pow(10, 1 - nzero);\r
-        }\r
-      } else {\r
-        value += ival;\r
-      }\r
-    } else {\r
-      value = ival;\r
-    }\r
-    boolean isExponent = false;\r
-    if (ich < ichMax && (ch == 69 || ch == 101 || ch == 68)) { // E e D\r
-      isExponent = true;\r
-      if (++ich >= ichMax)\r
-        return Float.NaN;\r
-      ch = str.charAt(ich);\r
-      if ((ch == '+') && (++ich >= ichMax))\r
-        return Float.NaN;\r
-      next[0] = ich;\r
-      int exponent = parseIntChecked(str, ichMax, next);\r
-      if (exponent == Integer.MIN_VALUE)\r
-        return Float.NaN;\r
-      if (exponent > 0 && exponent <= tensScale.length)\r
-        value *= tensScale[exponent - 1];\r
-      else if (exponent < 0 && -exponent <= decimalScale.length)\r
-        value *= decimalScale[-exponent - 1];\r
-      else if (exponent != 0)\r
-        value *= Math.pow(10, exponent);\r
-    } else {\r
-      next[0] = ich; // the exponent code finds its own ichNextParse\r
-    }\r
-    // believe it or not, Safari reports the long-equivalent of the \r
-    // float value here, then later the float value, after no operation!\r
-    if (negative)\r
-      value = -value;\r
-    if (value == Float.POSITIVE_INFINITY)\r
-      value = Float.MAX_VALUE;\r
-    return (!isStrict || (!isExponent || isDecimal)\r
-        && checkTrailingText(str, next[0], ichMax) ? value : Float.NaN);\r
-  }\r
-\r
-  public final static float[] tensScale = { 10f, 100f, 1000f, 10000f, 100000f, 1000000f };\r
-  public final static float[] decimalScale = { \r
-  0.1f, \r
-  0.01f, \r
-  0.001f, \r
-  0.0001f, \r
-  0.00001f,\r
-  0.000001f, \r
-  0.0000001f, \r
-  0.00000001f, \r
-  0.000000001f\r
-  };\r
-  public static boolean checkTrailingText(String str, int ich, int ichMax) {\r
-    //number must be pure -- no additional characters other than white space or ;\r
-    char ch;\r
-    while (ich < ichMax && (isWhitespace(ch = str.charAt(ich)) || ch == ';'))\r
-      ++ich;\r
-    return (ich == ichMax);\r
-  }\r
-\r
-  public static float[] parseFloatArray(String str) {\r
-    return parseFloatArrayNext(str, new int[1], null, null, null);\r
-  }\r
-\r
-  public static int parseFloatArrayInfested(String[] tokens, float[] data) {\r
-    int len = data.length;\r
-    int nTokens = tokens.length;\r
-    int n = 0;\r
-    int max = 0;\r
-    for (int i = 0; i >= 0 && i < len && n < nTokens; i++) {\r
-      float f;\r
-      while (Float.isNaN(f = parseFloat(tokens[n++])) \r
-          && n < nTokens) {\r
-      }\r
-      if (!Float.isNaN(f))\r
-        data[(max = i)] = f;\r
-      if (n == nTokens)\r
-        break;\r
-    }\r
-    return max + 1;\r
-  }\r
-\r
-  /**\r
-   * @param str\r
-   * @param next\r
-   * @param f\r
-   * @param strStart or null\r
-   * @param strEnd   or null\r
-   * @return array of float values\r
-   * \r
-   */\r
-  public static float[] parseFloatArrayNext(String str, int[] next, float[] f,\r
-                                            String strStart, String strEnd) {\r
-    int n = 0;\r
-    int pt = next[0];\r
-    if (pt >= 0) {\r
-      if (strStart != null) {\r
-        int p = str.indexOf(strStart, pt);\r
-        if (p >= 0)\r
-          next[0] = p + strStart.length();\r
-      }\r
-      str = str.substring(next[0]);\r
-      pt = (strEnd == null ? -1 : str.indexOf(strEnd));\r
-      if (pt < 0)\r
-        pt = str.length();\r
-      else\r
-        str = str.substring(0, pt);\r
-      next[0] += pt + 1;\r
-      String[] tokens = getTokens(str);\r
-      if (f == null)\r
-        f = new float[tokens.length];\r
-      n = parseFloatArrayInfested(tokens, f);\r
-    }\r
-    if (f == null)\r
-      return new float[0];\r
-    for (int i = n; i < f.length; i++)\r
-      f[i] = Float.NaN;\r
-    return f;\r
-  }\r
-\r
-  public static float parseFloatRange(String str, int ichMax, int[] next) {\r
-    int cch = str.length();\r
-    if (ichMax > cch)\r
-      ichMax = cch;\r
-    if (next[0] < 0 || next[0] >= ichMax)\r
-      return Float.NaN;\r
-    return parseFloatChecked(str, ichMax, next, false);\r
-  }\r
-\r
-  public static float parseFloatNext(String str, int[] next) {\r
-    int cch = (str == null ? -1 : str.length());\r
-    return (next[0] < 0 || next[0] >= cch ? Float.NaN : parseFloatChecked(str, cch, next, false));\r
-  }\r
-\r
-  public static float parseFloatStrict(String str) {\r
-    // checks trailing characters and does not allow "1E35" to be float\r
-    int cch = str.length();\r
-    if (cch == 0)\r
-      return Float.NaN;\r
-    return parseFloatChecked(str, cch, new int[] {0}, true);\r
-  }\r
-\r
-  public static float parseFloat(String str) {\r
-    return parseFloatNext(str, new int[] {0});\r
-  }\r
-\r
-  public static int parseIntRadix(String s, int i) throws NumberFormatException {\r
-    /**\r
-     * \r
-     * JavaScript uses parseIntRadix\r
-     * \r
-     * @j2sNative\r
-     * \r
-     *    return Integer.parseIntRadix(s, i);\r
-     *    \r
-     */\r
-    {\r
-      return Integer.parseInt(s, i);\r
-    }\r
-  }\r
-\r
-  public static String[] getTokens(String line) {\r
-    return getTokensAt(line, 0);\r
-  }\r
-\r
-  public static String parseToken(String str) {\r
-    return parseTokenNext(str, new int[] {0});\r
-  }\r
-\r
-  public static String parseTrimmed(String str) {\r
-    return parseTrimmedRange(str, 0, str.length());\r
-  }\r
-\r
-  public static String parseTrimmedAt(String str, int ichStart) {\r
-    return parseTrimmedRange(str, ichStart, str.length());\r
-  }\r
-\r
-  public static String parseTrimmedRange(String str, int ichStart, int ichMax) {\r
-    int cch = str.length();\r
-    if (ichMax < cch)\r
-      cch = ichMax;\r
-    if (cch < ichStart)\r
-      return "";\r
-    return parseTrimmedChecked(str, ichStart, cch);\r
-  }\r
-\r
-  public static String[] getTokensAt(String line, int ich) {\r
-    if (line == null)\r
-      return null;\r
-    int cchLine = line.length();\r
-    if (ich < 0 || ich > cchLine)\r
-      return null;\r
-    int tokenCount = countTokens(line, ich);\r
-    String[] tokens = new String[tokenCount];\r
-    int[] next = new int[1];\r
-    next[0] = ich;\r
-    for (int i = 0; i < tokenCount; ++i)\r
-      tokens[i] = parseTokenChecked(line, cchLine, next);\r
-    return tokens;\r
-  }\r
-\r
-  public static int countChar(String line, char c) {\r
-    int tokenCount = 0;\r
-    int pt = -1;\r
-    while ((pt = line.indexOf(c, pt + 1)) >= 0)\r
-      tokenCount++;\r
-    return  tokenCount;\r
-  }\r
-  \r
-  public static int countTokens(String line, int ich) {\r
-    int tokenCount = 0;\r
-    if (line != null) {\r
-      int ichMax = line.length();\r
-      while (true) {\r
-        while (ich < ichMax && isWhiteSpace(line, ich))\r
-          ++ich;\r
-        if (ich == ichMax)\r
-          break;\r
-        ++tokenCount;\r
-        do {\r
-          ++ich;\r
-        } while (ich < ichMax && !isWhiteSpace(line, ich));\r
-      }\r
-    }\r
-    return tokenCount;\r
-  }\r
-\r
-  public static String parseTokenNext(String str, int[] next) {\r
-    int cch = str.length();\r
-    return (next[0] < 0 || next[0] >= cch ? null : parseTokenChecked(str, cch, next));\r
-  }\r
-\r
-  public static String parseTokenRange(String str, int ichMax, int[] next) {\r
-    int cch = str.length();\r
-    if (ichMax > cch)\r
-      ichMax = cch;\r
-    return (next[0] < 0 || next[0] >= ichMax ? null : parseTokenChecked(str, ichMax, next));\r
-  }\r
-\r
-  public static String parseTokenChecked(String str, int ichMax, int[] next) {\r
-    int ich = next[0];\r
-    while (ich < ichMax && isWhiteSpace(str, ich))\r
-      ++ich;\r
-    int ichNonWhite = ich;\r
-    while (ich < ichMax && !isWhiteSpace(str, ich))\r
-      ++ich;\r
-    next[0] = ich;\r
-    return (ichNonWhite == ich ? null : str.substring(ichNonWhite, ich));\r
-  }\r
-\r
-  public static String parseTrimmedChecked(String str, int ich, int ichMax) {\r
-    while (ich < ichMax && isWhiteSpace(str, ich))\r
-      ++ich;\r
-    int ichLast = ichMax - 1;\r
-    while (ichLast >= ich && isWhiteSpace(str, ichLast))\r
-      --ichLast;\r
-    return (ichLast < ich ? "" : str.substring(ich, ichLast + 1));\r
-  }\r
-\r
-  public static double dVal(String s) throws NumberFormatException {\r
-    /**\r
-     * @j2sNative\r
-     * \r
-     * if(s==null)\r
-     *   throw new NumberFormatException("null");\r
-     * var d=parseFloat(s);\r
-     * if(isNaN(d))\r
-     *  throw new NumberFormatException("Not a Number : "+s);\r
-     * return d \r
-     * \r
-     */\r
-    {\r
-      return Double.valueOf(s).doubleValue();\r
-    }\r
-  }\r
-\r
-  public static float fVal(String s) throws NumberFormatException {\r
-    /**\r
-     * @j2sNative\r
-     * \r
-     * return this.dVal(s);\r
-     */\r
-    {\r
-      return Float.parseFloat(s);\r
-    }\r
-  }\r
-\r
-  public static int parseIntRange(String str, int ichMax, int[] next) {\r
-    int cch = str.length();\r
-    if (ichMax > cch)\r
-      ichMax = cch;\r
-    return (next[0] < 0 || next[0] >= ichMax ? Integer.MIN_VALUE : parseIntChecked(str, ichMax, next));\r
-  }\r
-\r
-  /**\r
-   * parses a string array for floats. Returns NaN for nonfloats.\r
-   * \r
-   *  @param tokens  the strings to parse\r
-   *  @param data    the array to fill\r
-   */\r
-  public static void parseFloatArrayData(String[] tokens, float[] data) {\r
-    parseFloatArrayDataN(tokens, data, data.length);\r
-  }\r
-\r
-  /**\r
-   * parses a string array for floats. Returns NaN for nonfloats or missing data.\r
-   * \r
-   *  @param tokens  the strings to parse\r
-   *  @param data    the array to fill\r
-   *  @param nData   the number of elements\r
-   */\r
-  public static void parseFloatArrayDataN(String[] tokens, float[] data, int nData) {\r
-    for (int i = nData; --i >= 0;)\r
-      data[i] = (i >= tokens.length ? Float.NaN : parseFloat(tokens[i]));\r
-  }\r
-\r
-  /**\r
-   * \r
-   *  proper splitting, even for Java 1.3 -- if the text ends in the run,\r
-   *  no new line is appended.\r
-   * \r
-   * @param text\r
-   * @param run\r
-   * @return  String array\r
-   */\r
-  public static String[] split(String text, String run) {\r
-    if (text.length() == 0)\r
-      return new String[0];\r
-    int n = 1;\r
-    int i = text.indexOf(run);\r
-    String[] lines;\r
-    int runLen = run.length();\r
-    if (i < 0 || runLen == 0) {\r
-      lines = new String[1];\r
-      lines[0] = text;\r
-      return lines;\r
-    }\r
-    int len = text.length() - runLen;\r
-    for (; i >= 0 && i < len; n++)\r
-      i = text.indexOf(run, i + runLen);\r
-    lines = new String[n];\r
-    i = 0;\r
-    int ipt = 0;\r
-    int pt = 0;\r
-    for (; (ipt = text.indexOf(run, i)) >= 0 && pt + 1 < n;) {\r
-      lines[pt++] = text.substring(i, ipt);\r
-      i = ipt + runLen;\r
-    }\r
-    if (text.indexOf(run, len) != len)\r
-      len += runLen;\r
-    lines[pt] = text.substring(i, len);\r
-    return lines;\r
-  }\r
-\r
-  public final static float FLOAT_MIN_SAFE = 2E-45f; \r
-  // Float.MIN_VALUE (1.45E-45) is not reliable with JavaScript because of the float/double difference there\r
-  \r
-  /// general static string-parsing class ///\r
-\r
-  // next[0] tracks the pointer within the string so these can all be static.\r
-  // but the methods parseFloat, parseInt, parseToken, parseTrimmed, and getTokens do not require this.\r
-\r
-//  public static String concatTokens(String[] tokens, int iFirst, int iEnd) {\r
-//    String str = "";\r
-//    String sep = "";\r
-//    for (int i = iFirst; i < iEnd; i++) {\r
-//      if (i < tokens.length) {\r
-//        str += sep + tokens[i];\r
-//        sep = " ";\r
-//      }\r
-//    }\r
-//    return str;\r
-//  }\r
-  \r
-  public static String getQuotedStringAt(String line, int ipt0) {\r
-    int[] next = new int[] { ipt0 };\r
-    return getQuotedStringNext(line, next);\r
-  }\r
-  \r
-  /**\r
-   * \r
-   * @param line\r
-   * @param next passes [current pointer]\r
-   * @return quoted string -- does NOT unescape characters\r
-   */\r
-  public static String getQuotedStringNext(String line, int[] next) {\r
-    int i = next[0];\r
-    if (i < 0 || (i = line.indexOf("\"", i)) < 0)\r
-      return "";\r
-    int pt = i + 1;\r
-    int len = line.length();\r
-    while (++i < len && line.charAt(i) != '"')\r
-      if (line.charAt(i) == '\\')\r
-        i++;\r
-    next[0] = i + 1;\r
-    return line.substring(pt, i);\r
-  }\r
-  \r
-  /**\r
-   * CSV format -- escaped quote is "" WITHIN "..."\r
-   *\r
-   * \r
-   * @param line\r
-   * @param next int[2] filled with [ptrQuote1, ptrAfterQuote2]\r
-   *            next[1] will be -1 if unmatched quotes are found (continuation on next line)\r
-   * @return unescaped string or null\r
-   */\r
-  public static String getCSVString(String line, int[] next) {\r
-    int i = next[1];\r
-    if (i < 0 || (i = line.indexOf("\"", i)) < 0)\r
-      return null;\r
-    int pt = next[0] = i;\r
-    int len = line.length();\r
-    boolean escaped = false;\r
-    boolean haveEscape = false;\r
-    while (++i < len \r
-        && (line.charAt(i) != '"' || (escaped = (i + 1 < len && line.charAt(i + 1) == '"'))))\r
-      if (escaped) {\r
-        escaped = false;\r
-        haveEscape = true;\r
-        i++;\r
-      }\r
-    if (i >= len) {\r
-      next[1] = -1;\r
-      return null; // unmatched\r
-    }\r
-    next[1] = i + 1;\r
-    String s = line.substring(pt + 1, i);\r
-    return (haveEscape ? rep(rep(s, "\"\"", "\0"), "\0","\"") : s);\r
-  }\r
-  \r
-  public static boolean isOneOf(String key, String semiList) {\r
-    if (semiList.length() == 0)\r
-      return false;\r
-    if (semiList.charAt(0) != ';')\r
-      semiList = ";" + semiList + ";";\r
-    return key.indexOf(";") < 0  && semiList.indexOf(';' + key + ';') >= 0;\r
-  }\r
-\r
-  public static String getQuotedAttribute(String info, String name) {\r
-    int i = info.indexOf(name + "=");\r
-    return (i < 0 ? null : getQuotedStringAt(info, i));\r
-  }\r
-\r
-  public static float approx(float f, float n) {\r
-    return Math.round (f * n) / n;\r
-  }\r
-\r
-  /**\r
-   * Does a clean ITERATIVE replace of strFrom in str with strTo. \r
-   * Thus, rep("Testttt", "tt","t") becomes "Test".\r
-   * \r
-   * @param str\r
-   * @param strFrom\r
-   * @param strTo\r
-   * @return replaced string\r
-   */\r
-  public static String rep(String str, String strFrom, String strTo) {\r
-    if (str == null || strFrom.length() == 0 || str.indexOf(strFrom) < 0)\r
-      return str;\r
-    boolean isOnce = (strTo.indexOf(strFrom) >= 0);\r
-    do {\r
-      str = str.replace(strFrom, strTo);\r
-    } while (!isOnce && str.indexOf(strFrom) >= 0);\r
-    return str;\r
-  }\r
-\r
-  public static String formatF(float value, int width, int precision,\r
-                              boolean alignLeft, boolean zeroPad) {\r
-    return formatS(DF.formatDecimal(value, precision), width, 0, alignLeft, zeroPad);\r
-  }\r
-\r
-  /**\r
-   * \r
-   * @param value\r
-   * @param width\r
-   * @param precision\r
-   * @param alignLeft\r
-   * @param zeroPad\r
-   * @param allowOverflow IGNORED\r
-   * @return formatted string\r
-   */\r
-  public static String formatD(double value, int width, int precision,\r
-                              boolean alignLeft, boolean zeroPad, boolean allowOverflow) {\r
-    return formatS(DF.formatDecimal((float)value, -1 - precision), width, 0, alignLeft, zeroPad);\r
-  }\r
-\r
-  /**\r
-   * \r
-   * @param value       \r
-   * @param width       number of columns\r
-   * @param precision   precision > 0 ==> precision = number of characters max from left\r
-   *                    precision < 0 ==> -1 - precision = number of char. max from right\r
-   * @param alignLeft\r
-   * @param zeroPad     generally for numbers turned strings\r
-   * @return            formatted string\r
-   */\r
-  public static String formatS(String value, int width, int precision,\r
-                              boolean alignLeft, boolean zeroPad) {\r
-    if (value == null)\r
-      return "";\r
-    int len = value.length();\r
-    if (precision != Integer.MAX_VALUE && precision > 0\r
-        && precision < len)\r
-      value = value.substring(0, precision);\r
-    else if (precision < 0 && len + precision >= 0)\r
-      value = value.substring(len + precision + 1);\r
-  \r
-    int padLength = width - value.length();\r
-    if (padLength <= 0)\r
-      return value;\r
-    boolean isNeg = (zeroPad && !alignLeft && value.charAt(0) == '-');\r
-    char padChar = (zeroPad ? '0' : ' ');\r
-    char padChar0 = (isNeg ? '-' : padChar);\r
-  \r
-    SB sb = new SB();\r
-    if (alignLeft)\r
-      sb.append(value);\r
-    sb.appendC(padChar0);\r
-    for (int i = padLength; --i > 0;)\r
-      // this is correct, not >= 0\r
-      sb.appendC(padChar);\r
-    if (!alignLeft)\r
-      sb.append(isNeg ? padChar + value.substring(1) : value);\r
-    return sb.toString();\r
-  }\r
-\r
-  /**\r
-   * Does a clean replace of any of the characters in str with chrTo\r
-   * If strTo contains strFrom, then only a single pass is done.\r
-   * Otherwise, multiple passes are made until no more replacements can be made.\r
-   * \r
-   * @param str\r
-   * @param strFrom\r
-   * @param chTo\r
-   * @return  replaced string\r
-   */\r
-  public static String replaceWithCharacter(String str, String strFrom,\r
-                                            char chTo) {\r
-    if (str == null)\r
-      return null;\r
-    for (int i = strFrom.length(); --i >= 0;)\r
-      str = str.replace(strFrom.charAt(i), chTo);\r
-    return str;\r
-  }\r
-\r
-  /**\r
-   * Does a clean replace of any of the characters in str with strTo\r
-   * If strTo contains strFrom, then only a single pass is done.\r
-   * Otherwise, multiple passes are made until no more replacements can be made.\r
-   * \r
-   * @param str\r
-   * @param strFrom\r
-   * @param strTo\r
-   * @return  replaced string\r
-   */\r
-  public static String replaceAllCharacters(String str, String strFrom,\r
-                                            String strTo) {\r
-    for (int i = strFrom.length(); --i >= 0;) {\r
-      String chFrom = strFrom.substring(i, i + 1);\r
-      str = rep(str, chFrom, strTo);\r
-    }\r
-    return str;\r
-  }\r
-\r
-  public static String trim(String str, String chars) {\r
-    if (str == null || str.length() == 0)\r
-      return str;\r
-    if (chars.length() == 0)\r
-      return str.trim();\r
-    int len = str.length();\r
-    int k = 0;\r
-    while (k < len && chars.indexOf(str.charAt(k)) >= 0)\r
-      k++;\r
-    int m = str.length() - 1;\r
-    while (m > k && chars.indexOf(str.charAt(m)) >= 0)\r
-      m--;\r
-    return str.substring(k, m + 1);\r
-  }\r
-\r
-  public static String trimQuotes(String value) {\r
-    return (value != null && value.length() > 1 && value.startsWith("\"")\r
-        && value.endsWith("\"") ? value.substring(1, value.length() - 1)\r
-        : value);\r
-  }\r
-\r
-  public static boolean isNonStringPrimitive(Object info) {\r
-    // note that we don't use Double, Float, or Integer here\r
-    // because in JavaScript those would be false for unwrapped primitives\r
-    // coming from equivalent of Array.get()\r
-    // Strings will need their own escaped processing\r
-    \r
-    return info instanceof Number || info instanceof Boolean;\r
-  }\r
-\r
-  private static Object arrayGet(Object info, int i) {\r
-    /**\r
-     * \r
-     * Note that info will be a primitive in JavaScript\r
-     * but a wrapped primitive in Java.\r
-     * \r
-     * @j2sNative\r
-     * \r
-     *            return info[i];\r
-     */\r
-    {\r
-      return Array.get(info, i);\r
-    }\r
-  }\r
-  \r
-  @SuppressWarnings("unchecked")\r
-  public static String toJSON(String infoType, Object info) {\r
-    if (info == null)\r
-      return packageJSON(infoType, null);\r
-    if (isNonStringPrimitive(info))\r
-      return packageJSON(infoType, info.toString());\r
-    String s = null;\r
-    SB sb = null;\r
-    while (true) {\r
-      if (info instanceof String) {\r
-        s = (String) info;\r
-        /**\r
-         * @j2sNative\r
-         * \r
-         * if (typeof s == "undefined") s = "null"\r
-         * \r
-         */\r
-        {}\r
-        if (s.indexOf("{\"") != 0) {\r
-          //don't doubly fix JSON strings when retrieving status\r
-          s = rep(s, "\"", "\\\"");\r
-          s = rep(s, "\n", "\\n");\r
-          s = "\"" + s + "\"";\r
-        }\r
-        break;\r
-      }\r
-      if (info instanceof JSONEncodable) {\r
-        // includes javajs.util.BS, org.jmol.script.SV\r
-        if ((s = ((JSONEncodable) info).toJSON()) == null)\r
-          s = "null"; // perhaps a list has a null value (group3List, for example)\r
-        break;\r
-      }\r
-      sb = new SB();\r
-      if (info instanceof Map) {\r
-        sb.append("{ ");\r
-        String sep = "";\r
-        for (String key : ((Map<String, ?>) info).keySet()) {\r
-          sb.append(sep).append(\r
-              packageJSON(key, toJSON(null, ((Map<?, ?>) info).get(key))));\r
-          sep = ",";\r
-        }\r
-        sb.append(" }");\r
-        break;\r
-      }\r
-      if (info instanceof Lst) {\r
-        sb.append("[ ");\r
-        int n = ((Lst<?>) info).size();\r
-        for (int i = 0; i < n; i++) {\r
-          if (i > 0)\r
-            sb.appendC(',');\r
-          sb.append(toJSON(null, ((Lst<?>) info).get(i)));\r
-        }\r
-        sb.append(" ]");\r
-        break;\r
-      }\r
-      if (info instanceof M34) {\r
-        // M4 extends M3\r
-        int len = (info instanceof M4 ? 4 : 3);\r
-        float[] x = new float[len];\r
-        M34 m = (M34) info;\r
-        sb.appendC('[');\r
-        for (int i = 0; i < len; i++) {\r
-          if (i > 0)\r
-            sb.appendC(',');\r
-          m.getRow(i, x);\r
-          sb.append(toJSON(null, x));\r
-        }\r
-        sb.appendC(']');\r
-        break;\r
-      }\r
-      s = nonArrayString(info);\r
-      if (s == null) {\r
-        sb.append("[");\r
-        int n = AU.getLength(info);\r
-        for (int i = 0; i < n; i++) {\r
-          if (i > 0)\r
-            sb.appendC(',');\r
-          sb.append(toJSON(null, arrayGet(info, i)));\r
-        }\r
-        sb.append("]");\r
-        break;\r
-      }\r
-      info = info.toString();\r
-    }\r
-    return packageJSON(infoType, (s == null ? sb.toString() : s));\r
-  }\r
-\r
-  /**\r
-   * Checks to see if an object is an array, and if it is, returns null;\r
-   * otherwise it returns the string equivalent of that object.\r
-   * \r
-   * @param x\r
-   * @return String or null\r
-   */\r
-  public static String nonArrayString(Object x) {\r
-    /**\r
-     * @j2sNative\r
-     * \r
-     *            var s = x.toString(); return (s.startsWith("[object") &&\r
-     *            s.endsWith("Array]") ? null : s);\r
-     * \r
-     */\r
-    {\r
-      try {\r
-        Array.getLength(x);\r
-        return null;\r
-      } catch (Exception e) {\r
-        return x.toString();\r
-      }\r
-    }\r
-  }\r
-\r
-  public static String byteArrayToJSON(byte[] data) {\r
-    SB sb = new SB();\r
-    sb.append("[");\r
-    int n = data.length;\r
-    for (int i = 0; i < n; i++) {\r
-      if (i > 0)\r
-        sb.appendC(',');\r
-      sb.appendI(data[i] & 0xFF);\r
-    }\r
-    sb.append("]");\r
-    return sb.toString();\r
-  }\r
-  \r
-  public static String packageJSON(String infoType, String info) {\r
-    return (infoType == null ? info : "\"" + infoType + "\": " + info);\r
-  }\r
-\r
-  public static String escapeUrl(String url) {\r
-    url = rep(url, "\n", "");\r
-    url = rep(url, "%", "%25");\r
-    url = rep(url, "#", "%23");\r
-    url = rep(url, "[", "%5B");\r
-    url = rep(url, "]", "%5D");\r
-    url = rep(url, " ", "%20");\r
-    return url;\r
-  }\r
-\r
-  private final static String escapable = "\\\\\tt\rr\nn\"\""; \r
-\r
-  public static String esc(String str) {\r
-    if (str == null || str.length() == 0)\r
-      return "\"\"";\r
-    boolean haveEscape = false;\r
-    int i = 0;\r
-    for (; i < escapable.length(); i += 2)\r
-      if (str.indexOf(escapable.charAt(i)) >= 0) {\r
-        haveEscape = true;\r
-        break;\r
-      }\r
-    if (haveEscape)\r
-      while (i < escapable.length()) {\r
-        int pt = -1;\r
-        char ch = escapable.charAt(i++);\r
-        char ch2 = escapable.charAt(i++);\r
-        SB sb = new SB();\r
-        int pt0 = 0;\r
-        while ((pt = str.indexOf(ch, pt + 1)) >= 0) {\r
-          sb.append(str.substring(pt0, pt)).appendC('\\').appendC(ch2);\r
-          pt0 = pt + 1;\r
-        }\r
-        sb.append(str.substring(pt0, str.length()));\r
-        str = sb.toString();\r
-      }    \r
-    return "\"" + escUnicode(str) + "\"";\r
-  }\r
-\r
-  public static String escUnicode(String str) {\r
-    for (int i = str.length(); --i >= 0;)\r
-      if (str.charAt(i) > 0x7F) {\r
-        String s = "0000" + Integer.toHexString(str.charAt(i));\r
-        str = str.substring(0, i) + "\\u" + s.substring(s.length() - 4)\r
-            + str.substring(i + 1);\r
-      }\r
-    return str;\r
-  }\r
-\r
-  /**\r
-   * ensures that a float turned to string has a decimal point\r
-   * \r
-   * @param f\r
-   * @return string version of float\r
-   */\r
-  public static String escF(float f) {\r
-    String sf = "" + f;\r
-    /**\r
-     * @j2sNative\r
-     * \r
-     * if (sf.indexOf(".") < 0 && sf.indexOf("e") < 0)\r
-     *   sf += ".0";\r
-     */\r
-    {\r
-    }\r
-    return sf;\r
-  }\r
-  public static String join(String[] s, char c, int i0) {\r
-    if (s.length < i0)\r
-      return null;\r
-    SB sb = new SB();\r
-    sb.append(s[i0++]);\r
-    for (int i = i0; i < s.length; i++)\r
-      sb.appendC(c).append(s[i]);\r
-    return sb.toString();\r
-  }\r
-\r
-  /**\r
-   * a LIKE "x"    a is a string and equals x\r
-   * \r
-   * a LIKE "*x"   a is a string and ends with x\r
-   * \r
-   * a LIKE "x*"   a is a string and starts with x\r
-   * \r
-   * a LIKE "*x*"  a is a string and contains x\r
-   *  \r
-   * @param a\r
-   * @param b\r
-   * @return  a LIKE b\r
-   */\r
-  public static boolean isLike(String a, String b) {\r
-    boolean areEqual = a.equals(b);\r
-    if (areEqual)\r
-      return true;\r
-    boolean isStart = b.startsWith("*");\r
-    boolean isEnd = b.endsWith("*");\r
-    return (!isStart && !isEnd) ? areEqual\r
-        : isStart && isEnd ? b.length() == 1 || a.contains(b.substring(1, b.length() - 1))\r
-        : isStart ? a.endsWith(b.substring(1))\r
-        : a.startsWith(b.substring(0, b.length() - 1));\r
-  }\r
-\r
-  public static Object getMapValueNoCase(Map<String, ?> h, String key) {\r
-    if ("this".equals(key))\r
-      return h;\r
-    Object val = h.get(key);\r
-    if (val == null)\r
-      for (Entry<String, ?> e : h.entrySet())\r
-        if (e.getKey().equalsIgnoreCase(key))\r
-          return e.getValue();\r
-    return val;\r
-  }\r
-\r
-  public static String clean(String s) {\r
-    return rep(replaceAllCharacters(s, " \t\n\r", " "), "  ", " ").trim();\r
-  }\r
-\r
-  /**\r
-   * \r
-   * fdup      duplicates p or q formats for formatCheck\r
-   *           and the format() function.\r
-   * \r
-   * @param f\r
-   * @param pt\r
-   * @param n\r
-   * @return     %3.5q%3.5q%3.5q%3.5q or %3.5p%3.5p%3.5p\r
-   */\r
-  public static String fdup(String f, int pt, int n) {\r
-    char ch;\r
-    int count = 0;\r
-    for (int i = pt; --i >= 1; ) {\r
-      if (isDigit(ch = f.charAt(i)))\r
-        continue;\r
-      switch (ch) {\r
-      case '.':\r
-        if (count++ != 0)\r
-          return f;\r
-        continue;\r
-      case '-':\r
-        if (i != 1 && f.charAt(i - 1) != '.')\r
-          return f;\r
-        continue;\r
-      default:\r
-        return f;\r
-      }\r
-    }\r
-    String s = f.substring(0, pt + 1);\r
-    SB sb = new SB();\r
-    for (int i = 0; i < n; i++)\r
-      sb.append(s);\r
-    sb.append(f.substring(pt + 1));\r
-    return sb.toString();\r
-  }\r
-\r
-  /**\r
-   * generic string formatter  based on formatLabel in Atom\r
-   * \r
-   * \r
-   * @param strFormat   .... %width.precisionKEY....\r
-   * @param key      any string to match\r
-   * @param strT     replacement string or null\r
-   * @param floatT   replacement float or Float.NaN\r
-   * @param doubleT  replacement double or Double.NaN -- for exponential\r
-   * @param doOne    mimic sprintf    \r
-   * @return         formatted string\r
-   */\r
-  \r
-  public static String formatString(String strFormat, String key, String strT,\r
-                                    float floatT, double doubleT, boolean doOne) {\r
-    if (strFormat == null)\r
-      return null;\r
-    if ("".equals(strFormat))\r
-      return "";\r
-    int len = key.length();\r
-    if (strFormat.indexOf("%") < 0 || len == 0 || strFormat.indexOf(key) < 0)\r
-      return strFormat;\r
-  \r
-    String strLabel = "";\r
-    int ich, ichPercent, ichKey;\r
-    for (ich = 0; (ichPercent = strFormat.indexOf('%', ich)) >= 0\r
-        && (ichKey = strFormat.indexOf(key, ichPercent + 1)) >= 0;) {\r
-      if (ich != ichPercent)\r
-        strLabel += strFormat.substring(ich, ichPercent);\r
-      ich = ichPercent + 1;\r
-      if (ichKey > ichPercent + 6) {\r
-        strLabel += '%';\r
-        continue;//%12.10x\r
-      }\r
-      try {\r
-        boolean alignLeft = false;\r
-        if (strFormat.charAt(ich) == '-') {\r
-          alignLeft = true;\r
-          ++ich;\r
-        }\r
-        boolean zeroPad = false;\r
-        if (strFormat.charAt(ich) == '0') {\r
-          zeroPad = true;\r
-          ++ich;\r
-        }\r
-        char ch;\r
-        int width = 0;\r
-        while ((ch = strFormat.charAt(ich)) >= '0' && (ch <= '9')) {\r
-          width = (10 * width) + (ch - '0');\r
-          ++ich;\r
-        }\r
-        int precision = Integer.MAX_VALUE;\r
-        boolean isExponential = false;\r
-        if (strFormat.charAt(ich) == '.') {\r
-          ++ich;\r
-          if ((ch = strFormat.charAt(ich)) == '-') {\r
-            isExponential = true;\r
-            ++ich;\r
-          } \r
-          if ((ch = strFormat.charAt(ich)) >= '0' && ch <= '9') {\r
-            precision = ch - '0';\r
-            ++ich;\r
-          }\r
-          if (isExponential)\r
-            precision = -precision - (strT == null ? 1 : 0);\r
-        }\r
-        String st = strFormat.substring(ich, ich + len);\r
-        if (!st.equals(key)) {\r
-          ich = ichPercent + 1;\r
-          strLabel += '%';\r
-          continue;\r
-        }\r
-        ich += len;\r
-        if (!Float.isNaN(floatT))\r
-          strLabel += formatF(floatT, width, precision, alignLeft,\r
-              zeroPad);\r
-        else if (strT != null)\r
-          strLabel += formatS(strT, width, precision, alignLeft,\r
-              zeroPad);\r
-        else if (!Double.isNaN(doubleT))\r
-          strLabel += formatD(doubleT, width, precision, alignLeft,\r
-              zeroPad, true);\r
-        if (doOne)\r
-          break;\r
-      } catch (IndexOutOfBoundsException ioobe) {\r
-        ich = ichPercent;\r
-        break;\r
-      }\r
-    }\r
-    strLabel += strFormat.substring(ich);\r
-    //if (strLabel.length() == 0)\r
-      //return null;\r
-    return strLabel;\r
-  }\r
-\r
-  public static String formatStringS(String strFormat, String key, String strT) {\r
-    return formatString(strFormat, key, strT, Float.NaN, Double.NaN, false);\r
-  }\r
-\r
-  public static String formatStringF(String strFormat, String key, float floatT) {\r
-    return formatString(strFormat, key, null, floatT, Double.NaN, false);\r
-  }\r
-\r
-  public static String formatStringI(String strFormat, String key, int intT) {\r
-    return formatString(strFormat, key, "" + intT, Float.NaN, Double.NaN, false);\r
-  }\r
-\r
-  /**\r
-   * sprintf emulation uses (almost) c++ standard string formats\r
-   * \r
-   * 's' string 'i' or 'd' integer, 'e' double, 'f' float, 'p' point3f 'q'\r
-   * quaternion/plane/axisangle with added "i" (equal to the insipid "d" --\r
-   * digits?)\r
-   * \r
-   * @param strFormat\r
-   * @param list\r
-   *        a listing of what sort of data will be found in Object[] values, in\r
-   *        order: s string, f float, i integer, d double, p point3f, q\r
-   *        quaternion/point4f, S String[], F float[], I int[], and D double[]\r
-   * @param values\r
-   *        Object[] containing above types\r
-   * @return formatted string\r
-   */\r
-  public static String sprintf(String strFormat, String list, Object[] values) {\r
-    if (values == null)\r
-      return strFormat;\r
-    int n = list.length();\r
-    if (n == values.length)\r
-      try {\r
-        for (int o = 0; o < n; o++) {\r
-          if (values[o] == null)\r
-            continue;\r
-          switch (list.charAt(o)) {\r
-          case 's':\r
-            strFormat = formatString(strFormat, "s", (String) values[o],\r
-                Float.NaN, Double.NaN, true);\r
-            break;\r
-          case 'f':\r
-            strFormat = formatString(strFormat, "f", null, ((Float) values[o])\r
-                .floatValue(), Double.NaN, true);\r
-            break;\r
-          case 'i':\r
-            strFormat = formatString(strFormat, "d", "" + values[o], Float.NaN,\r
-                Double.NaN, true);\r
-            strFormat = formatString(strFormat, "i", "" + values[o], Float.NaN,\r
-                Double.NaN, true);\r
-            break;\r
-          case 'd':\r
-            strFormat = formatString(strFormat, "e", null, Float.NaN,\r
-                ((Double) values[o]).doubleValue(), true);\r
-            break;\r
-          case 'p':\r
-            T3 pVal = (T3) values[o];\r
-            strFormat = formatString(strFormat, "p", null, pVal.x, Double.NaN,\r
-                true);\r
-            strFormat = formatString(strFormat, "p", null, pVal.y, Double.NaN,\r
-                true);\r
-            strFormat = formatString(strFormat, "p", null, pVal.z, Double.NaN,\r
-                true);\r
-            break;\r
-          case 'q':\r
-            T4 qVal = (T4) values[o];\r
-            strFormat = formatString(strFormat, "q", null, qVal.x, Double.NaN,\r
-                true);\r
-            strFormat = formatString(strFormat, "q", null, qVal.y, Double.NaN,\r
-                true);\r
-            strFormat = formatString(strFormat, "q", null, qVal.z, Double.NaN,\r
-                true);\r
-            strFormat = formatString(strFormat, "q", null, qVal.w, Double.NaN,\r
-                true);\r
-            break;\r
-          case 'S':\r
-            String[] sVal = (String[]) values[o];\r
-            for (int i = 0; i < sVal.length; i++)\r
-              strFormat = formatString(strFormat, "s", sVal[i], Float.NaN,\r
-                  Double.NaN, true);\r
-            break;\r
-          case 'F':\r
-            float[] fVal = (float[]) values[o];\r
-            for (int i = 0; i < fVal.length; i++)\r
-              strFormat = formatString(strFormat, "f", null, fVal[i],\r
-                  Double.NaN, true);\r
-            break;\r
-          case 'I':\r
-            int[] iVal = (int[]) values[o];\r
-            for (int i = 0; i < iVal.length; i++)\r
-              strFormat = formatString(strFormat, "d", "" + iVal[i], Float.NaN,\r
-                  Double.NaN, true);\r
-            for (int i = 0; i < iVal.length; i++)\r
-              strFormat = formatString(strFormat, "i", "" + iVal[i], Float.NaN,\r
-                  Double.NaN, true);\r
-            break;\r
-          case 'D':\r
-            double[] dVal = (double[]) values[o];\r
-            for (int i = 0; i < dVal.length; i++)\r
-              strFormat = formatString(strFormat, "e", null, Float.NaN,\r
-                  dVal[i], true);\r
-          }\r
-  \r
-        }\r
-        return rep(strFormat, "%%", "%");\r
-      } catch (Exception e) {\r
-        //\r
-      }\r
-    System.out.println("TextFormat.sprintf error " + list + " " + strFormat);\r
-    return rep(strFormat, "%", "?");\r
-  }\r
-\r
-  /**\r
-   * \r
-   * formatCheck   checks p and q formats and duplicates if necessary\r
-   *               "%10.5p xxxx" ==> "%10.5p%10.5p%10.5p xxxx" \r
-   * \r
-   * @param strFormat\r
-   * @return    f or dupicated format\r
-   */\r
-  public static String formatCheck(String strFormat) {\r
-    if (strFormat == null || strFormat.indexOf('p') < 0 && strFormat.indexOf('q') < 0)\r
-      return strFormat;\r
-    strFormat = rep(strFormat, "%%", "\1");\r
-    strFormat = rep(strFormat, "%p", "%6.2p");\r
-    strFormat = rep(strFormat, "%q", "%6.2q");\r
-    String[] format = split(strFormat, "%");\r
-    SB sb = new SB();\r
-    sb.append(format[0]);\r
-    for (int i = 1; i < format.length; i++) {\r
-      String f = "%" + format[i];\r
-      int pt;\r
-      if (f.length() >= 3) {\r
-        if ((pt = f.indexOf('p')) >= 0)\r
-          f = fdup(f, pt, 3);\r
-        if ((pt = f.indexOf('q')) >= 0)\r
-          f = fdup(f, pt, 4);\r
-      }\r
-      sb.append(f);\r
-    }\r
-    return sb.toString().replace('\1', '%');\r
-  }\r
-\r
-  public static void leftJustify(SB s, String s1, String s2) {\r
-    s.append(s2);\r
-    int n = s1.length() - s2.length();\r
-    if (n > 0)\r
-      s.append(s1.substring(0, n));\r
-  }\r
-\r
-  public static void rightJustify(SB s, String s1, String s2) {\r
-    int n = s1.length() - s2.length();\r
-    if (n > 0)\r
-      s.append(s1.substring(0, n));\r
-    s.append(s2);\r
-  }\r
-\r
-  public static String safeTruncate(float f, int n) {\r
-    if (f > -0.001 && f < 0.001)\r
-      f = 0;\r
-    return (f + "         ").substring(0,n);\r
-  }\r
-\r
-  public static boolean isWild(String s) {\r
-    return s != null && (s.indexOf("*") >= 0 || s.indexOf("?") >= 0);\r
-  }\r
-\r
-  /**\r
-   * A general non-regex (for performance) text matcher that utilizes ? and *.\r
-   * \r
-   * ??? means "at most three" characters if at beginning or end; \r
-   *   "exactly three" otherwise\r
-   * \1 in search is a stand-in for actual ?\r
-   * \r
-   * @param search\r
-   *        the string to search\r
-   * @param match\r
-   *        the match string\r
-   * @param checkStar\r
-   * @param allowInitialStar\r
-   * @return true if found\r
-   */\r
-  public static boolean isMatch(String search, String match, boolean checkStar,\r
-                                boolean allowInitialStar) {\r
-    // search == match --> true\r
-    if (search.equals(match))\r
-      return true;\r
-    int mLen = match.length();\r
-    // match == ""  --> false\r
-    if (mLen == 0)\r
-      return false;\r
-    boolean isStar0 = (checkStar && allowInitialStar ? match.charAt(0) == '*'\r
-        : false);\r
-    // match == "*" --> true\r
-    if (mLen == 1 && isStar0)\r
-      return true;\r
-    boolean isStar1 = (checkStar && match.endsWith("*"));\r
-    boolean haveQ = (match.indexOf('?') >= 0);\r
-    // match == "**" --> true\r
-    // match == "*xxx*" --> search contains "xxx"\r
-    // match == "*xxx" --> search ends with "xxx"\r
-    // match == "xxx*" --> search starts with "xxx"\r
-    if (!haveQ) {\r
-      if (isStar0)\r
-        return (isStar1 ? (mLen < 3 || search.indexOf(match.substring(1,\r
-            mLen - 1)) >= 0) : search.endsWith(match.substring(1)));\r
-      else if (isStar1)\r
-        return search.startsWith(match.substring(0, mLen - 1));\r
-    }\r
-    int sLen = search.length();\r
-    // pad match with "?" -- same as *\r
-    String qqqq = "????";\r
-    int nq = 4;\r
-    while (nq < sLen) {\r
-      qqqq += qqqq;\r
-      nq += 4;\r
-    }\r
-    if (checkStar) {\r
-      if (isStar0) {\r
-        match = qqqq + match.substring(1);\r
-        mLen += nq - 1;\r
-      }\r
-      if (isStar1) {\r
-        match = match.substring(0, mLen - 1) + qqqq;\r
-        mLen += nq - 1;\r
-      }\r
-    }\r
-    // length of match < length of search --> false \r
-    if (mLen < sLen)\r
-      return false;\r
-  \r
-    // -- each ? matches ONE character if not at end\r
-    // -- extra ? at end ignored\r
-  \r
-    // (allowInitialStar == true)\r
-    // -- extra ? at beginning reduced to match length\r
-  \r
-    int ich = 0;\r
-    while (mLen > sLen) {\r
-      if (allowInitialStar && match.charAt(ich) == '?') {\r
-        ++ich;\r
-      } else if (match.charAt(ich + mLen - 1) != '?') {\r
-        return false;\r
-      }\r
-      --mLen;\r
-    }\r
-  \r
-    // both are effectively same length now.\r
-    // \1 is stand-in for "?"\r
-  \r
-    for (int i = sLen; --i >= 0;) {\r
-      char chm = match.charAt(ich + i);\r
-      if (chm == '?')\r
-        continue;\r
-      char chs = search.charAt(i);\r
-      if (chm != chs && (chm != '\1' || chs != '?'))\r
-        return false;\r
-    }\r
-    return true;\r
-  }\r
-\r
-  public static String replaceQuotedStrings(String s, Lst<String> list,\r
-                                            Lst<String> newList) {\r
-    int n = list.size();\r
-    for (int i = 0; i < n; i++) {\r
-      String name = list.get(i);\r
-      String newName = newList.get(i);\r
-      if (!newName.equals(name))\r
-        s = rep(s, "\"" + name + "\"", "\"" + newName\r
-            + "\"");\r
-    }\r
-    return s;\r
-  }\r
-\r
-  public static String replaceStrings(String s, Lst<String> list,\r
-                                      Lst<String> newList) {\r
-    int n = list.size();\r
-    for (int i = 0; i < n; i++) {\r
-      String name = list.get(i);\r
-      String newName = newList.get(i);\r
-      if (!newName.equals(name))\r
-        s = rep(s, name, newName);\r
-    }\r
-    return s;\r
-  }\r
-\r
-  public static boolean isDigit(char ch) {\r
-    // just way simpler code than  Character.isDigit(ch);\r
-    int c = ch;\r
-    return (48 <= c && c <= 57);\r
-  }\r
-\r
-  public static boolean isUpperCase(char ch) {\r
-    int c = ch;\r
-    return (65 <= c && c <= 90);\r
-  }\r
-\r
-  public static boolean isLowerCase(char ch) {\r
-    int c = ch;\r
-    return (97 <= c && c <= 122);\r
-  }\r
-\r
-  public static boolean isLetter(char ch) {\r
-    // just way simpler code than     Character.isLetter(ch);\r
-    int c = ch;\r
-    return (65 <= c && c <= 90 || 97 <= c && c <= 122);\r
-  }\r
-\r
-  public static boolean isLetterOrDigit(char ch) {\r
-    // just way simpler code than     Character.isLetterOrDigit(ch);\r
-    int c = ch;\r
-    return (65 <= c && c <= 90 || 97 <= c && c <= 122 || 48 <= c && c <= 57);\r
-  }\r
-\r
-  public static boolean isWhitespace(char ch) {\r
-    int c = ch;\r
-    return (c >= 0x1c && c <= 0x20 || c >= 0x9 && c <= 0xd);\r
-  }\r
-\r
-  public static final float FRACTIONAL_PRECISION = 100000f;\r
-  public static final float CARTESIAN_PRECISION =  10000f;\r
-\r
-  public static void fixPtFloats(T3 pt, float f) {\r
-    //this will equate float and double as long as -256 <= x <= 256\r
-    pt.x = Math.round(pt.x * f) / f;\r
-    pt.y = Math.round(pt.y * f) / f;\r
-    pt.z = Math.round(pt.z * f) / f;\r
-  }\r
-  \r
-  public static double fixDouble(double d, double f) {\r
-    return Math.round(d * f) / f;\r
-  }\r
-\r
-  /**\r
-   * parse a float or "float/float"\r
-   * @param s\r
-   * @return a/b\r
-   */\r
-  public static float parseFloatFraction(String s) {\r
-      int pt = s.indexOf("/");\r
-      return (pt < 0 ? parseFloat(s) : parseFloat(s.substring(0, pt))\r
-          / parseFloat(s.substring(pt + 1)));\r
-  }\r
-\r
-//static {\r
-//    \r
-//  double d = 790.8999998888;\r
-//  float x  = 790.8999998888f;\r
-//  for (int i = 0; i < 50; i++) {\r
-//  System.out.println(x + " " + d);\r
-//  System.out.println(Math.round(x * 100000) / 100000f);\r
-//  System.out.println(Math.round(d * 100000) / 100000.);\r
-//  System.out.println(Math.round(x * 10000) / 10000f);\r
-//  System.out.println(Math.round(d * 10000) / 10000.);\r
-//  x+=1; \r
-//  d+=1;\r
-//  }\r
-//  System.out.println(100.123456789f);\r
-//}\r
-\r
-//  static {\r
-//    long t;\r
-//    char c = '0';\r
-//    t = System.currentTimeMillis();\r
-//    for (int i = 0; i < 10000000; i++) {\r
-//      boolean b = PT.isUpperCase(c);\r
-//    }\r
-//    System.out.println(System.currentTimeMillis() - t);\r
-//\r
-//    t = System.currentTimeMillis();\r
-//    for (int i = 0; i < 10000000; i++) {\r
-//      boolean b = Character.isUpperCase(c);\r
-//    }\r
-//    System.out.println(System.currentTimeMillis() - t);\r
-//    \r
-//    t = System.currentTimeMillis();\r
-//    for (int i = 0; i < 10000000; i++) {\r
-//      boolean b = PT.isUpperCase(c);\r
-//    }\r
-//    System.out.println(System.currentTimeMillis() - t);\r
-//\r
-//    System.out.println("PT test");\r
-//  }\r
-}\r
+/* $RCSfile$
+ * $Author: hansonr $
+ * $Date: 2007-04-26 16:57:51 -0500 (Thu, 26 Apr 2007) $
+ * $Revision: 7502 $
+ *
+ * Copyright (C) 2005  The Jmol Development Team
+ *
+ * Contact: jmol-developers@lists.sf.net
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.1 of the License, or (at your option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+package javajs.util;
+
+import java.lang.reflect.Array;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javajs.J2SIgnoreImport;
+import javajs.api.JSONEncodable;
+
+/**
+ * a combination of Parsing and Text-related utility classes
+ * 
+ * @author hansonr
+ * 
+ */
+
+@J2SIgnoreImport(value = { java.lang.reflect.Array.class })
+public class PT {
+
+  public static int parseInt(String str) {
+    return parseIntNext(str, new int[] {0});
+  }
+
+  public static int parseIntNext(String str, int[] next) {
+    int cch = str.length();
+    if (next[0] < 0 || next[0] >= cch)
+      return Integer.MIN_VALUE;
+    return parseIntChecked(str, cch, next);
+  }
+
+  public static int parseIntChecked(String str, int ichMax, int[] next) {
+    boolean digitSeen = false;
+    int value = 0;
+    int ich = next[0];
+    if (ich < 0)
+      return Integer.MIN_VALUE;
+    int ch;
+    while (ich < ichMax && isWhiteSpace(str, ich))
+      ++ich;
+    boolean negative = false;
+    if (ich < ichMax && str.charAt(ich) == 45) { //"-"
+      negative = true;
+      ++ich;
+    }
+    while (ich < ichMax && (ch = str.charAt(ich)) >= 48 && ch <= 57) {
+      value = value * 10 + (ch - 48);
+      digitSeen = true;
+      ++ich;
+    }
+    if (!digitSeen)// || !checkTrailingText(str, ich, ichMax))
+      value = Integer.MIN_VALUE;
+    else if (negative)
+      value = -value;
+    next[0] = ich;
+    return value;
+  }
+
+  public static boolean isWhiteSpace(String str, int ich) {
+    char ch;
+    return (ich >= 0 && ((ch = str.charAt(ich)) == ' ' || ch == '\t' || ch == '\n'));
+  }
+
+  /**
+   * A float parser that is 30% faster than Float.parseFloat(x) and also accepts
+   * x.yD+-n
+   * 
+   * @param str
+   * @param ichMax
+   * @param next
+   *        pointer; incremented
+   * @param isStrict
+   * @return value or Float.NaN
+   */
+  public static float parseFloatChecked(String str, int ichMax, int[] next,
+                                         boolean isStrict) {
+    boolean digitSeen = false;
+    int ich = next[0];
+    if (isStrict && str.indexOf('\n') != str.lastIndexOf('\n'))
+      return Float.NaN;
+    while (ich < ichMax && isWhiteSpace(str, ich))
+      ++ich;
+    boolean negative = false;
+    if (ich < ichMax && str.charAt(ich) == '-') {
+      ++ich;
+      negative = true;
+    }
+    // looks crazy, but if we don't do this, Google Closure Compiler will 
+    // write code that Safari will misinterpret in a VERY nasty way -- 
+    // getting totally confused as to long integers and double values
+    
+    // This is Safari figuring out the values of the numbers on the line (x, y, then z):
+  
+    //  ATOM 1241 CD1 LEU A 64 -2.206 36.532 31.576 1.00 60.60 C
+    //  e=1408749273
+    //  -e =-1408749273
+    //  ATOM 1241 CD1 LEU A 64 -2.206 36.532 31.576 1.00 60.60 C
+    //  e=-1821066134
+    //  e=36.532
+    //  ATOM 1241 CD1 LEU A 64 -2.206 36.532 31.576 1.00 60.60 C
+    //  e=-1133871366
+    //  e=31.576
+    //
+    //  "e" values are just before and after the "value = -value" statement.
+    
+    int ch = 0;
+    float ival = 0f;
+    float ival2 = 0f;
+    while (ich < ichMax && (ch = str.charAt(ich)) >= 48 && ch <= 57) {
+      ival = (ival * 10f) + (ch - 48)*1f;
+      ++ich;
+      digitSeen = true;
+    }
+    boolean isDecimal = false;
+    int iscale = 0;
+    int nzero = (ival == 0 ? -1 : 0);
+    if (ch == '.') {
+      isDecimal = true;
+      while (++ich < ichMax && (ch = str.charAt(ich)) >= 48 && ch <= 57) {
+        digitSeen = true;
+        if (nzero < 0) {
+          if (ch == 48) { 
+            nzero--;
+            continue;
+          }
+          nzero = -nzero;
+        } 
+        if (iscale  < decimalScale.length) {
+          ival2 = (ival2 * 10f) + (ch - 48)*1f;
+          iscale++;
+        }
+      }
+    }
+    float value;
+    
+    // Safari breaks here intermittently converting integers to floats 
+    
+    if (!digitSeen) {
+      value = Float.NaN;
+    } else if (ival2 > 0) {
+      value = ival2 * decimalScale[iscale - 1];
+      if (nzero > 1) {
+        if (nzero - 2 < decimalScale.length) {
+          value *= decimalScale[nzero - 2];
+        } else {
+          value *= Math.pow(10, 1 - nzero);
+        }
+      } else {
+        value += ival;
+      }
+    } else {
+      value = ival;
+    }
+    boolean isExponent = false;
+    if (ich < ichMax && (ch == 69 || ch == 101 || ch == 68)) { // E e D
+      isExponent = true;
+      if (++ich >= ichMax)
+        return Float.NaN;
+      ch = str.charAt(ich);
+      if ((ch == '+') && (++ich >= ichMax))
+        return Float.NaN;
+      next[0] = ich;
+      int exponent = parseIntChecked(str, ichMax, next);
+      if (exponent == Integer.MIN_VALUE)
+        return Float.NaN;
+      if (exponent > 0 && exponent <= tensScale.length)
+        value *= tensScale[exponent - 1];
+      else if (exponent < 0 && -exponent <= decimalScale.length)
+        value *= decimalScale[-exponent - 1];
+      else if (exponent != 0)
+        value *= Math.pow(10, exponent);
+    } else {
+      next[0] = ich; // the exponent code finds its own ichNextParse
+    }
+    // believe it or not, Safari reports the long-equivalent of the 
+    // float value here, then later the float value, after no operation!
+    if (negative)
+      value = -value;
+    if (value == Float.POSITIVE_INFINITY)
+      value = Float.MAX_VALUE;
+    return (!isStrict || (!isExponent || isDecimal)
+        && checkTrailingText(str, next[0], ichMax) ? value : Float.NaN);
+  }
+
+  public final static float[] tensScale = { 10f, 100f, 1000f, 10000f, 100000f, 1000000f };
+  public final static float[] decimalScale = { 
+  0.1f, 
+  0.01f, 
+  0.001f, 
+  0.0001f, 
+  0.00001f,
+  0.000001f, 
+  0.0000001f, 
+  0.00000001f, 
+  0.000000001f
+  };
+  public static boolean checkTrailingText(String str, int ich, int ichMax) {
+    //number must be pure -- no additional characters other than white space or ;
+    char ch;
+    while (ich < ichMax && (isWhitespace(ch = str.charAt(ich)) || ch == ';'))
+      ++ich;
+    return (ich == ichMax);
+  }
+
+  public static float[] parseFloatArray(String str) {
+    return parseFloatArrayNext(str, new int[1], null, null, null);
+  }
+
+  public static int parseFloatArrayInfested(String[] tokens, float[] data) {
+    int len = data.length;
+    int nTokens = tokens.length;
+    int n = 0;
+    int max = 0;
+    for (int i = 0; i >= 0 && i < len && n < nTokens; i++) {
+      float f;
+      while (Float.isNaN(f = parseFloat(tokens[n++])) 
+          && n < nTokens) {
+      }
+      if (!Float.isNaN(f))
+        data[(max = i)] = f;
+      if (n == nTokens)
+        break;
+    }
+    return max + 1;
+  }
+
+  /**
+   * @param str
+   * @param next
+   * @param f
+   * @param strStart or null
+   * @param strEnd   or null
+   * @return array of float values
+   * 
+   */
+  public static float[] parseFloatArrayNext(String str, int[] next, float[] f,
+                                            String strStart, String strEnd) {
+    int n = 0;
+    int pt = next[0];
+    if (pt >= 0) {
+      if (strStart != null) {
+        int p = str.indexOf(strStart, pt);
+        if (p >= 0)
+          next[0] = p + strStart.length();
+      }
+      str = str.substring(next[0]);
+      pt = (strEnd == null ? -1 : str.indexOf(strEnd));
+      if (pt < 0)
+        pt = str.length();
+      else
+        str = str.substring(0, pt);
+      next[0] += pt + 1;
+      String[] tokens = getTokens(str);
+      if (f == null)
+        f = new float[tokens.length];
+      n = parseFloatArrayInfested(tokens, f);
+    }
+    if (f == null)
+      return new float[0];
+    for (int i = n; i < f.length; i++)
+      f[i] = Float.NaN;
+    return f;
+  }
+
+  public static float parseFloatRange(String str, int ichMax, int[] next) {
+    int cch = str.length();
+    if (ichMax > cch)
+      ichMax = cch;
+    if (next[0] < 0 || next[0] >= ichMax)
+      return Float.NaN;
+    return parseFloatChecked(str, ichMax, next, false);
+  }
+
+  public static float parseFloatNext(String str, int[] next) {
+    int cch = (str == null ? -1 : str.length());
+    return (next[0] < 0 || next[0] >= cch ? Float.NaN : parseFloatChecked(str, cch, next, false));
+  }
+
+  public static float parseFloatStrict(String str) {
+    // checks trailing characters and does not allow "1E35" to be float
+    int cch = str.length();
+    if (cch == 0)
+      return Float.NaN;
+    return parseFloatChecked(str, cch, new int[] {0}, true);
+  }
+
+  public static float parseFloat(String str) {
+    return parseFloatNext(str, new int[] {0});
+  }
+
+  public static int parseIntRadix(String s, int i) throws NumberFormatException {
+    /**
+     * 
+     * JavaScript uses parseIntRadix
+     * 
+     * @j2sNative
+     * 
+     *    return Integer.parseIntRadix(s, i);
+     *    
+     */
+    {
+      return Integer.parseInt(s, i);
+    }
+  }
+
+  public static String[] getTokens(String line) {
+    return getTokensAt(line, 0);
+  }
+
+  public static String parseToken(String str) {
+    return parseTokenNext(str, new int[] {0});
+  }
+
+  public static String parseTrimmed(String str) {
+    return parseTrimmedRange(str, 0, str.length());
+  }
+
+  public static String parseTrimmedAt(String str, int ichStart) {
+    return parseTrimmedRange(str, ichStart, str.length());
+  }
+
+  public static String parseTrimmedRange(String str, int ichStart, int ichMax) {
+    int cch = str.length();
+    if (ichMax < cch)
+      cch = ichMax;
+    if (cch < ichStart)
+      return "";
+    return parseTrimmedChecked(str, ichStart, cch);
+  }
+
+  public static String[] getTokensAt(String line, int ich) {
+    if (line == null)
+      return null;
+    int cchLine = line.length();
+    if (ich < 0 || ich > cchLine)
+      return null;
+    int tokenCount = countTokens(line, ich);
+    String[] tokens = new String[tokenCount];
+    int[] next = new int[1];
+    next[0] = ich;
+    for (int i = 0; i < tokenCount; ++i)
+      tokens[i] = parseTokenChecked(line, cchLine, next);
+    return tokens;
+  }
+
+  public static int countChar(String line, char c) {
+    int tokenCount = 0;
+    int pt = -1;
+    while ((pt = line.indexOf(c, pt + 1)) >= 0)
+      tokenCount++;
+    return  tokenCount;
+  }
+  
+  public static int countTokens(String line, int ich) {
+    int tokenCount = 0;
+    if (line != null) {
+      int ichMax = line.length();
+      while (true) {
+        while (ich < ichMax && isWhiteSpace(line, ich))
+          ++ich;
+        if (ich == ichMax)
+          break;
+        ++tokenCount;
+        do {
+          ++ich;
+        } while (ich < ichMax && !isWhiteSpace(line, ich));
+      }
+    }
+    return tokenCount;
+  }
+
+  public static String parseTokenNext(String str, int[] next) {
+    int cch = str.length();
+    return (next[0] < 0 || next[0] >= cch ? null : parseTokenChecked(str, cch, next));
+  }
+
+  public static String parseTokenRange(String str, int ichMax, int[] next) {
+    int cch = str.length();
+    if (ichMax > cch)
+      ichMax = cch;
+    return (next[0] < 0 || next[0] >= ichMax ? null : parseTokenChecked(str, ichMax, next));
+  }
+
+  public static String parseTokenChecked(String str, int ichMax, int[] next) {
+    int ich = next[0];
+    while (ich < ichMax && isWhiteSpace(str, ich))
+      ++ich;
+    int ichNonWhite = ich;
+    while (ich < ichMax && !isWhiteSpace(str, ich))
+      ++ich;
+    next[0] = ich;
+    return (ichNonWhite == ich ? null : str.substring(ichNonWhite, ich));
+  }
+
+  public static String parseTrimmedChecked(String str, int ich, int ichMax) {
+    while (ich < ichMax && isWhiteSpace(str, ich))
+      ++ich;
+    int ichLast = ichMax - 1;
+    while (ichLast >= ich && isWhiteSpace(str, ichLast))
+      --ichLast;
+    return (ichLast < ich ? "" : str.substring(ich, ichLast + 1));
+  }
+
+  public static double dVal(String s) throws NumberFormatException {
+    /**
+     * @j2sNative
+     * 
+     * if(s==null)
+     *   throw new NumberFormatException("null");
+     * var d=parseFloat(s);
+     * if(isNaN(d))
+     *  throw new NumberFormatException("Not a Number : "+s);
+     * return d 
+     * 
+     */
+    {
+      return Double.valueOf(s).doubleValue();
+    }
+  }
+
+  public static float fVal(String s) throws NumberFormatException {
+    /**
+     * @j2sNative
+     * 
+     * return this.dVal(s);
+     */
+    {
+      return Float.parseFloat(s);
+    }
+  }
+
+  public static int parseIntRange(String str, int ichMax, int[] next) {
+    int cch = str.length();
+    if (ichMax > cch)
+      ichMax = cch;
+    return (next[0] < 0 || next[0] >= ichMax ? Integer.MIN_VALUE : parseIntChecked(str, ichMax, next));
+  }
+
+  /**
+   * parses a string array for floats. Returns NaN for nonfloats.
+   * 
+   *  @param tokens  the strings to parse
+   *  @param data    the array to fill
+   */
+  public static void parseFloatArrayData(String[] tokens, float[] data) {
+    parseFloatArrayDataN(tokens, data, data.length);
+  }
+
+  /**
+   * parses a string array for floats. Returns NaN for nonfloats or missing data.
+   * 
+   *  @param tokens  the strings to parse
+   *  @param data    the array to fill
+   *  @param nData   the number of elements
+   */
+  public static void parseFloatArrayDataN(String[] tokens, float[] data, int nData) {
+    for (int i = nData; --i >= 0;)
+      data[i] = (i >= tokens.length ? Float.NaN : parseFloat(tokens[i]));
+  }
+
+  /**
+   * 
+   *  proper splitting, even for Java 1.3 -- if the text ends in the run,
+   *  no new line is appended.
+   * 
+   * @param text
+   * @param run
+   * @return  String array
+   */
+  public static String[] split(String text, String run) {
+    if (text.length() == 0)
+      return new String[0];
+    int n = 1;
+    int i = text.indexOf(run);
+    String[] lines;
+    int runLen = run.length();
+    if (i < 0 || runLen == 0) {
+      lines = new String[1];
+      lines[0] = text;
+      return lines;
+    }
+    int len = text.length() - runLen;
+    for (; i >= 0 && i < len; n++)
+      i = text.indexOf(run, i + runLen);
+    lines = new String[n];
+    i = 0;
+    int ipt = 0;
+    int pt = 0;
+    for (; (ipt = text.indexOf(run, i)) >= 0 && pt + 1 < n;) {
+      lines[pt++] = text.substring(i, ipt);
+      i = ipt + runLen;
+    }
+    if (text.indexOf(run, len) != len)
+      len += runLen;
+    lines[pt] = text.substring(i, len);
+    return lines;
+  }
+
+  public final static float FLOAT_MIN_SAFE = 2E-45f; 
+  // Float.MIN_VALUE (1.45E-45) is not reliable with JavaScript because of the float/double difference there
+  
+  /// general static string-parsing class ///
+
+  // next[0] tracks the pointer within the string so these can all be static.
+  // but the methods parseFloat, parseInt, parseToken, parseTrimmed, and getTokens do not require this.
+
+//  public static String concatTokens(String[] tokens, int iFirst, int iEnd) {
+//    String str = "";
+//    String sep = "";
+//    for (int i = iFirst; i < iEnd; i++) {
+//      if (i < tokens.length) {
+//        str += sep + tokens[i];
+//        sep = " ";
+//      }
+//    }
+//    return str;
+//  }
+  
+  public static String getQuotedStringAt(String line, int ipt0) {
+    int[] next = new int[] { ipt0 };
+    return getQuotedStringNext(line, next);
+  }
+  
+  /**
+   * 
+   * @param line
+   * @param next passes [current pointer]
+   * @return quoted string -- does NOT unescape characters
+   */
+  public static String getQuotedStringNext(String line, int[] next) {
+    int i = next[0];
+    if (i < 0 || (i = line.indexOf("\"", i)) < 0)
+      return "";
+    int pt = i + 1;
+    int len = line.length();
+    while (++i < len && line.charAt(i) != '"')
+      if (line.charAt(i) == '\\')
+        i++;
+    next[0] = i + 1;
+    return line.substring(pt, i);
+  }
+  
+  /**
+   * CSV format -- escaped quote is "" WITHIN "..."
+   *
+   * 
+   * @param line
+   * @param next int[2] filled with [ptrQuote1, ptrAfterQuote2]
+   *            next[1] will be -1 if unmatched quotes are found (continuation on next line)
+   * @return unescaped string or null
+   */
+  public static String getCSVString(String line, int[] next) {
+    int i = next[1];
+    if (i < 0 || (i = line.indexOf("\"", i)) < 0)
+      return null;
+    int pt = next[0] = i;
+    int len = line.length();
+    boolean escaped = false;
+    boolean haveEscape = false;
+    while (++i < len 
+        && (line.charAt(i) != '"' || (escaped = (i + 1 < len && line.charAt(i + 1) == '"'))))
+      if (escaped) {
+        escaped = false;
+        haveEscape = true;
+        i++;
+      }
+    if (i >= len) {
+      next[1] = -1;
+      return null; // unmatched
+    }
+    next[1] = i + 1;
+    String s = line.substring(pt + 1, i);
+    return (haveEscape ? rep(rep(s, "\"\"", "\0"), "\0","\"") : s);
+  }
+  
+  public static boolean isOneOf(String key, String semiList) {
+    if (semiList.length() == 0)
+      return false;
+    if (semiList.charAt(0) != ';')
+      semiList = ";" + semiList + ";";
+    return key.indexOf(";") < 0  && semiList.indexOf(';' + key + ';') >= 0;
+  }
+
+  public static String getQuotedAttribute(String info, String name) {
+    int i = info.indexOf(name + "=");
+    return (i < 0 ? null : getQuotedStringAt(info, i));
+  }
+
+  public static float approx(float f, float n) {
+    return Math.round (f * n) / n;
+  }
+
+  /**
+   * Does a clean ITERATIVE replace of strFrom in str with strTo. 
+   * Thus, rep("Testttt", "tt","t") becomes "Test".
+   * 
+   * @param str
+   * @param strFrom
+   * @param strTo
+   * @return replaced string
+   */
+  public static String rep(String str, String strFrom, String strTo) {
+    if (str == null || strFrom.length() == 0 || str.indexOf(strFrom) < 0)
+      return str;
+    boolean isOnce = (strTo.indexOf(strFrom) >= 0);
+    do {
+      str = str.replace(strFrom, strTo);
+    } while (!isOnce && str.indexOf(strFrom) >= 0);
+    return str;
+  }
+
+  public static String formatF(float value, int width, int precision,
+                              boolean alignLeft, boolean zeroPad) {
+    return formatS(DF.formatDecimal(value, precision), width, 0, alignLeft, zeroPad);
+  }
+
+  /**
+   * 
+   * @param value
+   * @param width
+   * @param precision
+   * @param alignLeft
+   * @param zeroPad
+   * @param allowOverflow IGNORED
+   * @return formatted string
+   */
+  public static String formatD(double value, int width, int precision,
+                              boolean alignLeft, boolean zeroPad, boolean allowOverflow) {
+    return formatS(DF.formatDecimal((float)value, -1 - precision), width, 0, alignLeft, zeroPad);
+  }
+
+  /**
+   * 
+   * @param value       
+   * @param width       number of columns
+   * @param precision   precision > 0 ==> precision = number of characters max from left
+   *                    precision < 0 ==> -1 - precision = number of char. max from right
+   * @param alignLeft
+   * @param zeroPad     generally for numbers turned strings
+   * @return            formatted string
+   */
+  public static String formatS(String value, int width, int precision,
+                              boolean alignLeft, boolean zeroPad) {
+    if (value == null)
+      return "";
+    int len = value.length();
+    if (precision != Integer.MAX_VALUE && precision > 0
+        && precision < len)
+      value = value.substring(0, precision);
+    else if (precision < 0 && len + precision >= 0)
+      value = value.substring(len + precision + 1);
+  
+    int padLength = width - value.length();
+    if (padLength <= 0)
+      return value;
+    boolean isNeg = (zeroPad && !alignLeft && value.charAt(0) == '-');
+    char padChar = (zeroPad ? '0' : ' ');
+    char padChar0 = (isNeg ? '-' : padChar);
+  
+    SB sb = new SB();
+    if (alignLeft)
+      sb.append(value);
+    sb.appendC(padChar0);
+    for (int i = padLength; --i > 0;)
+      // this is correct, not >= 0
+      sb.appendC(padChar);
+    if (!alignLeft)
+      sb.append(isNeg ? padChar + value.substring(1) : value);
+    return sb.toString();
+  }
+
+  /**
+   * Does a clean replace of any of the characters in str with chrTo
+   * If strTo contains strFrom, then only a single pass is done.
+   * Otherwise, multiple passes are made until no more replacements can be made.
+   * 
+   * @param str
+   * @param strFrom
+   * @param chTo
+   * @return  replaced string
+   */
+  public static String replaceWithCharacter(String str, String strFrom,
+                                            char chTo) {
+    if (str == null)
+      return null;
+    for (int i = strFrom.length(); --i >= 0;)
+      str = str.replace(strFrom.charAt(i), chTo);
+    return str;
+  }
+
+  /**
+   * Does a clean replace of any of the characters in str with strTo
+   * If strTo contains strFrom, then only a single pass is done.
+   * Otherwise, multiple passes are made until no more replacements can be made.
+   * 
+   * @param str
+   * @param strFrom
+   * @param strTo
+   * @return  replaced string
+   */
+  public static String replaceAllCharacters(String str, String strFrom,
+                                            String strTo) {
+    for (int i = strFrom.length(); --i >= 0;) {
+      String chFrom = strFrom.substring(i, i + 1);
+      str = rep(str, chFrom, strTo);
+    }
+    return str;
+  }
+
+  public static String trim(String str, String chars) {
+    if (str == null || str.length() == 0)
+      return str;
+    if (chars.length() == 0)
+      return str.trim();
+    int len = str.length();
+    int k = 0;
+    while (k < len && chars.indexOf(str.charAt(k)) >= 0)
+      k++;
+    int m = str.length() - 1;
+    while (m > k && chars.indexOf(str.charAt(m)) >= 0)
+      m--;
+    return str.substring(k, m + 1);
+  }
+
+  public static String trimQuotes(String value) {
+    return (value != null && value.length() > 1 && value.startsWith("\"")
+        && value.endsWith("\"") ? value.substring(1, value.length() - 1)
+        : value);
+  }
+
+  public static boolean isNonStringPrimitive(Object info) {
+    // note that we don't use Double, Float, or Integer here
+    // because in JavaScript those would be false for unwrapped primitives
+    // coming from equivalent of Array.get()
+    // Strings will need their own escaped processing
+    
+    return info instanceof Number || info instanceof Boolean;
+  }
+
+  private static Object arrayGet(Object info, int i) {
+    /**
+     * 
+     * Note that info will be a primitive in JavaScript
+     * but a wrapped primitive in Java.
+     * 
+     * @j2sNative
+     * 
+     *            return info[i];
+     */
+    {
+      return Array.get(info, i);
+    }
+  }
+  
+  @SuppressWarnings("unchecked")
+  public static String toJSON(String infoType, Object info) {
+    if (info == null)
+      return packageJSON(infoType, null);
+    if (isNonStringPrimitive(info))
+      return packageJSON(infoType, info.toString());
+    String s = null;
+    SB sb = null;
+    while (true) {
+      if (info instanceof String) {
+        s = (String) info;
+        /**
+         * @j2sNative
+         * 
+         * if (typeof s == "undefined") s = "null"
+         * 
+         */
+        {}
+        if (s.indexOf("{\"") != 0) {
+          //don't doubly fix JSON strings when retrieving status
+          s = rep(s, "\"", "\\\"");
+          s = rep(s, "\n", "\\n");
+          s = "\"" + s + "\"";
+        }
+        break;
+      }
+      if (info instanceof JSONEncodable) {
+        // includes javajs.util.BS, org.jmol.script.SV
+        if ((s = ((JSONEncodable) info).toJSON()) == null)
+          s = "null"; // perhaps a list has a null value (group3List, for example)
+        break;
+      }
+      sb = new SB();
+      if (info instanceof Map) {
+        sb.append("{ ");
+        String sep = "";
+        for (String key : ((Map<String, ?>) info).keySet()) {
+          sb.append(sep).append(
+              packageJSON(key, toJSON(null, ((Map<?, ?>) info).get(key))));
+          sep = ",";
+        }
+        sb.append(" }");
+        break;
+      }
+      if (info instanceof Lst) {
+        sb.append("[ ");
+        int n = ((Lst<?>) info).size();
+        for (int i = 0; i < n; i++) {
+          if (i > 0)
+            sb.appendC(',');
+          sb.append(toJSON(null, ((Lst<?>) info).get(i)));
+        }
+        sb.append(" ]");
+        break;
+      }
+      if (info instanceof M34) {
+        // M4 extends M3
+        int len = (info instanceof M4 ? 4 : 3);
+        float[] x = new float[len];
+        M34 m = (M34) info;
+        sb.appendC('[');
+        for (int i = 0; i < len; i++) {
+          if (i > 0)
+            sb.appendC(',');
+          m.getRow(i, x);
+          sb.append(toJSON(null, x));
+        }
+        sb.appendC(']');
+        break;
+      }
+      s = nonArrayString(info);
+      if (s == null) {
+        sb.append("[");
+        int n = AU.getLength(info);
+        for (int i = 0; i < n; i++) {
+          if (i > 0)
+            sb.appendC(',');
+          sb.append(toJSON(null, arrayGet(info, i)));
+        }
+        sb.append("]");
+        break;
+      }
+      info = info.toString();
+    }
+    return packageJSON(infoType, (s == null ? sb.toString() : s));
+  }
+
+  /**
+   * Checks to see if an object is an array, and if it is, returns null;
+   * otherwise it returns the string equivalent of that object.
+   * 
+   * @param x
+   * @return String or null
+   */
+  public static String nonArrayString(Object x) {
+    /**
+     * @j2sNative
+     * 
+     *            var s = x.toString(); return (s.startsWith("[object") &&
+     *            s.endsWith("Array]") ? null : s);
+     * 
+     */
+    {
+      try {
+        Array.getLength(x);
+        return null;
+      } catch (Exception e) {
+        return x.toString();
+      }
+    }
+  }
+
+  public static String byteArrayToJSON(byte[] data) {
+    SB sb = new SB();
+    sb.append("[");
+    int n = data.length;
+    for (int i = 0; i < n; i++) {
+      if (i > 0)
+        sb.appendC(',');
+      sb.appendI(data[i] & 0xFF);
+    }
+    sb.append("]");
+    return sb.toString();
+  }
+  
+  public static String packageJSON(String infoType, String info) {
+    return (infoType == null ? info : "\"" + infoType + "\": " + info);
+  }
+
+  public static String escapeUrl(String url) {
+    url = rep(url, "\n", "");
+    url = rep(url, "%", "%25");
+    url = rep(url, "#", "%23");
+    url = rep(url, "[", "%5B");
+    url = rep(url, "]", "%5D");
+    url = rep(url, " ", "%20");
+    return url;
+  }
+
+  private final static String escapable = "\\\\\tt\rr\nn\"\""; 
+
+  public static String esc(String str) {
+    if (str == null || str.length() == 0)
+      return "\"\"";
+    boolean haveEscape = false;
+    int i = 0;
+    for (; i < escapable.length(); i += 2)
+      if (str.indexOf(escapable.charAt(i)) >= 0) {
+        haveEscape = true;
+        break;
+      }
+    if (haveEscape)
+      while (i < escapable.length()) {
+        int pt = -1;
+        char ch = escapable.charAt(i++);
+        char ch2 = escapable.charAt(i++);
+        SB sb = new SB();
+        int pt0 = 0;
+        while ((pt = str.indexOf(ch, pt + 1)) >= 0) {
+          sb.append(str.substring(pt0, pt)).appendC('\\').appendC(ch2);
+          pt0 = pt + 1;
+        }
+        sb.append(str.substring(pt0, str.length()));
+        str = sb.toString();
+      }    
+    return "\"" + escUnicode(str) + "\"";
+  }
+
+  public static String escUnicode(String str) {
+    for (int i = str.length(); --i >= 0;)
+      if (str.charAt(i) > 0x7F) {
+        String s = "0000" + Integer.toHexString(str.charAt(i));
+        str = str.substring(0, i) + "\\u" + s.substring(s.length() - 4)
+            + str.substring(i + 1);
+      }
+    return str;
+  }
+
+  /**
+   * ensures that a float turned to string has a decimal point
+   * 
+   * @param f
+   * @return string version of float
+   */
+  public static String escF(float f) {
+    String sf = "" + f;
+    /**
+     * @j2sNative
+     * 
+     * if (sf.indexOf(".") < 0 && sf.indexOf("e") < 0)
+     *   sf += ".0";
+     */
+    {
+    }
+    return sf;
+  }
+  public static String join(String[] s, char c, int i0) {
+    if (s.length < i0)
+      return null;
+    SB sb = new SB();
+    sb.append(s[i0++]);
+    for (int i = i0; i < s.length; i++)
+      sb.appendC(c).append(s[i]);
+    return sb.toString();
+  }
+
+  /**
+   * a LIKE "x"    a is a string and equals x
+   * 
+   * a LIKE "*x"   a is a string and ends with x
+   * 
+   * a LIKE "x*"   a is a string and starts with x
+   * 
+   * a LIKE "*x*"  a is a string and contains x
+   *  
+   * @param a
+   * @param b
+   * @return  a LIKE b
+   */
+  public static boolean isLike(String a, String b) {
+    boolean areEqual = a.equals(b);
+    if (areEqual)
+      return true;
+    boolean isStart = b.startsWith("*");
+    boolean isEnd = b.endsWith("*");
+    return (!isStart && !isEnd) ? areEqual
+        : isStart && isEnd ? b.length() == 1 || a.contains(b.substring(1, b.length() - 1))
+        : isStart ? a.endsWith(b.substring(1))
+        : a.startsWith(b.substring(0, b.length() - 1));
+  }
+
+  public static Object getMapValueNoCase(Map<String, ?> h, String key) {
+    if ("this".equals(key))
+      return h;
+    Object val = h.get(key);
+    if (val == null)
+      for (Entry<String, ?> e : h.entrySet())
+        if (e.getKey().equalsIgnoreCase(key))
+          return e.getValue();
+    return val;
+  }
+
+  public static String clean(String s) {
+    return rep(replaceAllCharacters(s, " \t\n\r", " "), "  ", " ").trim();
+  }
+
+  /**
+   * 
+   * fdup      duplicates p or q formats for formatCheck
+   *           and the format() function.
+   * 
+   * @param f
+   * @param pt
+   * @param n
+   * @return     %3.5q%3.5q%3.5q%3.5q or %3.5p%3.5p%3.5p
+   */
+  public static String fdup(String f, int pt, int n) {
+    char ch;
+    int count = 0;
+    for (int i = pt; --i >= 1; ) {
+      if (isDigit(ch = f.charAt(i)))
+        continue;
+      switch (ch) {
+      case '.':
+        if (count++ != 0)
+          return f;
+        continue;
+      case '-':
+        if (i != 1 && f.charAt(i - 1) != '.')
+          return f;
+        continue;
+      default:
+        return f;
+      }
+    }
+    String s = f.substring(0, pt + 1);
+    SB sb = new SB();
+    for (int i = 0; i < n; i++)
+      sb.append(s);
+    sb.append(f.substring(pt + 1));
+    return sb.toString();
+  }
+
+  /**
+   * generic string formatter  based on formatLabel in Atom
+   * 
+   * 
+   * @param strFormat   .... %width.precisionKEY....
+   * @param key      any string to match
+   * @param strT     replacement string or null
+   * @param floatT   replacement float or Float.NaN
+   * @param doubleT  replacement double or Double.NaN -- for exponential
+   * @param doOne    mimic sprintf    
+   * @return         formatted string
+   */
+  
+  public static String formatString(String strFormat, String key, String strT,
+                                    float floatT, double doubleT, boolean doOne) {
+    if (strFormat == null)
+      return null;
+    if ("".equals(strFormat))
+      return "";
+    int len = key.length();
+    if (strFormat.indexOf("%") < 0 || len == 0 || strFormat.indexOf(key) < 0)
+      return strFormat;
+  
+    String strLabel = "";
+    int ich, ichPercent, ichKey;
+    for (ich = 0; (ichPercent = strFormat.indexOf('%', ich)) >= 0
+        && (ichKey = strFormat.indexOf(key, ichPercent + 1)) >= 0;) {
+      if (ich != ichPercent)
+        strLabel += strFormat.substring(ich, ichPercent);
+      ich = ichPercent + 1;
+      if (ichKey > ichPercent + 6) {
+        strLabel += '%';
+        continue;//%12.10x
+      }
+      try {
+        boolean alignLeft = false;
+        if (strFormat.charAt(ich) == '-') {
+          alignLeft = true;
+          ++ich;
+        }
+        boolean zeroPad = false;
+        if (strFormat.charAt(ich) == '0') {
+          zeroPad = true;
+          ++ich;
+        }
+        char ch;
+        int width = 0;
+        while ((ch = strFormat.charAt(ich)) >= '0' && (ch <= '9')) {
+          width = (10 * width) + (ch - '0');
+          ++ich;
+        }
+        int precision = Integer.MAX_VALUE;
+        boolean isExponential = false;
+        if (strFormat.charAt(ich) == '.') {
+          ++ich;
+          if ((ch = strFormat.charAt(ich)) == '-') {
+            isExponential = true;
+            ++ich;
+          } 
+          if ((ch = strFormat.charAt(ich)) >= '0' && ch <= '9') {
+            precision = ch - '0';
+            ++ich;
+          }
+          if (isExponential)
+            precision = -precision - (strT == null ? 1 : 0);
+        }
+        String st = strFormat.substring(ich, ich + len);
+        if (!st.equals(key)) {
+          ich = ichPercent + 1;
+          strLabel += '%';
+          continue;
+        }
+        ich += len;
+        if (!Float.isNaN(floatT))
+          strLabel += formatF(floatT, width, precision, alignLeft,
+              zeroPad);
+        else if (strT != null)
+          strLabel += formatS(strT, width, precision, alignLeft,
+              zeroPad);
+        else if (!Double.isNaN(doubleT))
+          strLabel += formatD(doubleT, width, precision, alignLeft,
+              zeroPad, true);
+        if (doOne)
+          break;
+      } catch (IndexOutOfBoundsException ioobe) {
+        ich = ichPercent;
+        break;
+      }
+    }
+    strLabel += strFormat.substring(ich);
+    //if (strLabel.length() == 0)
+      //return null;
+    return strLabel;
+  }
+
+  public static String formatStringS(String strFormat, String key, String strT) {
+    return formatString(strFormat, key, strT, Float.NaN, Double.NaN, false);
+  }
+
+  public static String formatStringF(String strFormat, String key, float floatT) {
+    return formatString(strFormat, key, null, floatT, Double.NaN, false);
+  }
+
+  public static String formatStringI(String strFormat, String key, int intT) {
+    return formatString(strFormat, key, "" + intT, Float.NaN, Double.NaN, false);
+  }
+
+  /**
+   * sprintf emulation uses (almost) c++ standard string formats
+   * 
+   * 's' string 'i' or 'd' integer, 'e' double, 'f' float, 'p' point3f 'q'
+   * quaternion/plane/axisangle with added "i" (equal to the insipid "d" --
+   * digits?)
+   * 
+   * @param strFormat
+   * @param list
+   *        a listing of what sort of data will be found in Object[] values, in
+   *        order: s string, f float, i integer, d double, p point3f, q
+   *        quaternion/point4f, S String[], F float[], I int[], and D double[]
+   * @param values
+   *        Object[] containing above types
+   * @return formatted string
+   */
+  public static String sprintf(String strFormat, String list, Object[] values) {
+    if (values == null)
+      return strFormat;
+    int n = list.length();
+    if (n == values.length)
+      try {
+        for (int o = 0; o < n; o++) {
+          if (values[o] == null)
+            continue;
+          switch (list.charAt(o)) {
+          case 's':
+            strFormat = formatString(strFormat, "s", (String) values[o],
+                Float.NaN, Double.NaN, true);
+            break;
+          case 'f':
+            strFormat = formatString(strFormat, "f", null, ((Float) values[o])
+                .floatValue(), Double.NaN, true);
+            break;
+          case 'i':
+            strFormat = formatString(strFormat, "d", "" + values[o], Float.NaN,
+                Double.NaN, true);
+            strFormat = formatString(strFormat, "i", "" + values[o], Float.NaN,
+                Double.NaN, true);
+            break;
+          case 'd':
+            strFormat = formatString(strFormat, "e", null, Float.NaN,
+                ((Double) values[o]).doubleValue(), true);
+            break;
+          case 'p':
+            T3 pVal = (T3) values[o];
+            strFormat = formatString(strFormat, "p", null, pVal.x, Double.NaN,
+                true);
+            strFormat = formatString(strFormat, "p", null, pVal.y, Double.NaN,
+                true);
+            strFormat = formatString(strFormat, "p", null, pVal.z, Double.NaN,
+                true);
+            break;
+          case 'q':
+            T4 qVal = (T4) values[o];
+            strFormat = formatString(strFormat, "q", null, qVal.x, Double.NaN,
+                true);
+            strFormat = formatString(strFormat, "q", null, qVal.y, Double.NaN,
+                true);
+            strFormat = formatString(strFormat, "q", null, qVal.z, Double.NaN,
+                true);
+            strFormat = formatString(strFormat, "q", null, qVal.w, Double.NaN,
+                true);
+            break;
+          case 'S':
+            String[] sVal = (String[]) values[o];
+            for (int i = 0; i < sVal.length; i++)
+              strFormat = formatString(strFormat, "s", sVal[i], Float.NaN,
+                  Double.NaN, true);
+            break;
+          case 'F':
+            float[] fVal = (float[]) values[o];
+            for (int i = 0; i < fVal.length; i++)
+              strFormat = formatString(strFormat, "f", null, fVal[i],
+                  Double.NaN, true);
+            break;
+          case 'I':
+            int[] iVal = (int[]) values[o];
+            for (int i = 0; i < iVal.length; i++)
+              strFormat = formatString(strFormat, "d", "" + iVal[i], Float.NaN,
+                  Double.NaN, true);
+            for (int i = 0; i < iVal.length; i++)
+              strFormat = formatString(strFormat, "i", "" + iVal[i], Float.NaN,
+                  Double.NaN, true);
+            break;
+          case 'D':
+            double[] dVal = (double[]) values[o];
+            for (int i = 0; i < dVal.length; i++)
+              strFormat = formatString(strFormat, "e", null, Float.NaN,
+                  dVal[i], true);
+          }
+  
+        }
+        return rep(strFormat, "%%", "%");
+      } catch (Exception e) {
+        //
+      }
+    System.out.println("TextFormat.sprintf error " + list + " " + strFormat);
+    return rep(strFormat, "%", "?");
+  }
+
+  /**
+   * 
+   * formatCheck   checks p and q formats and duplicates if necessary
+   *               "%10.5p xxxx" ==> "%10.5p%10.5p%10.5p xxxx" 
+   * 
+   * @param strFormat
+   * @return    f or dupicated format
+   */
+  public static String formatCheck(String strFormat) {
+    if (strFormat == null || strFormat.indexOf('p') < 0 && strFormat.indexOf('q') < 0)
+      return strFormat;
+    strFormat = rep(strFormat, "%%", "\1");
+    strFormat = rep(strFormat, "%p", "%6.2p");
+    strFormat = rep(strFormat, "%q", "%6.2q");
+    String[] format = split(strFormat, "%");
+    SB sb = new SB();
+    sb.append(format[0]);
+    for (int i = 1; i < format.length; i++) {
+      String f = "%" + format[i];
+      int pt;
+      if (f.length() >= 3) {
+        if ((pt = f.indexOf('p')) >= 0)
+          f = fdup(f, pt, 3);
+        if ((pt = f.indexOf('q')) >= 0)
+          f = fdup(f, pt, 4);
+      }
+      sb.append(f);
+    }
+    return sb.toString().replace('\1', '%');
+  }
+
+  public static void leftJustify(SB s, String s1, String s2) {
+    s.append(s2);
+    int n = s1.length() - s2.length();
+    if (n > 0)
+      s.append(s1.substring(0, n));
+  }
+
+  public static void rightJustify(SB s, String s1, String s2) {
+    int n = s1.length() - s2.length();
+    if (n > 0)
+      s.append(s1.substring(0, n));
+    s.append(s2);
+  }
+
+  public static String safeTruncate(float f, int n) {
+    if (f > -0.001 && f < 0.001)
+      f = 0;
+    return (f + "         ").substring(0,n);
+  }
+
+  public static boolean isWild(String s) {
+    return s != null && (s.indexOf("*") >= 0 || s.indexOf("?") >= 0);
+  }
+
+  /**
+   * A general non-regex (for performance) text matcher that utilizes ? and *.
+   * 
+   * ??? means "at most three" characters if at beginning or end; 
+   *   "exactly three" otherwise
+   * \1 in search is a stand-in for actual ?
+   * 
+   * @param search
+   *        the string to search
+   * @param match
+   *        the match string
+   * @param checkStar
+   * @param allowInitialStar
+   * @return true if found
+   */
+  public static boolean isMatch(String search, String match, boolean checkStar,
+                                boolean allowInitialStar) {
+    // search == match --> true
+    if (search.equals(match))
+      return true;
+    int mLen = match.length();
+    // match == ""  --> false
+    if (mLen == 0)
+      return false;
+    boolean isStar0 = (checkStar && allowInitialStar ? match.charAt(0) == '*'
+        : false);
+    // match == "*" --> true
+    if (mLen == 1 && isStar0)
+      return true;
+    boolean isStar1 = (checkStar && match.endsWith("*"));
+    boolean haveQ = (match.indexOf('?') >= 0);
+    // match == "**" --> true
+    // match == "*xxx*" --> search contains "xxx"
+    // match == "*xxx" --> search ends with "xxx"
+    // match == "xxx*" --> search starts with "xxx"
+    if (!haveQ) {
+      if (isStar0)
+        return (isStar1 ? (mLen < 3 || search.indexOf(match.substring(1,
+            mLen - 1)) >= 0) : search.endsWith(match.substring(1)));
+      else if (isStar1)
+        return search.startsWith(match.substring(0, mLen - 1));
+    }
+    int sLen = search.length();
+    // pad match with "?" -- same as *
+    String qqqq = "????";
+    int nq = 4;
+    while (nq < sLen) {
+      qqqq += qqqq;
+      nq += 4;
+    }
+    if (checkStar) {
+      if (isStar0) {
+        match = qqqq + match.substring(1);
+        mLen += nq - 1;
+      }
+      if (isStar1) {
+        match = match.substring(0, mLen - 1) + qqqq;
+        mLen += nq - 1;
+      }
+    }
+    // length of match < length of search --> false 
+    if (mLen < sLen)
+      return false;
+  
+    // -- each ? matches ONE character if not at end
+    // -- extra ? at end ignored
+  
+    // (allowInitialStar == true)
+    // -- extra ? at beginning reduced to match length
+  
+    int ich = 0;
+    while (mLen > sLen) {
+      if (allowInitialStar && match.charAt(ich) == '?') {
+        ++ich;
+      } else if (match.charAt(ich + mLen - 1) != '?') {
+        return false;
+      }
+      --mLen;
+    }
+  
+    // both are effectively same length now.
+    // \1 is stand-in for "?"
+  
+    for (int i = sLen; --i >= 0;) {
+      char chm = match.charAt(ich + i);
+      if (chm == '?')
+        continue;
+      char chs = search.charAt(i);
+      if (chm != chs && (chm != '\1' || chs != '?'))
+        return false;
+    }
+    return true;
+  }
+
+  public static String replaceQuotedStrings(String s, Lst<String> list,
+                                            Lst<String> newList) {
+    int n = list.size();
+    for (int i = 0; i < n; i++) {
+      String name = list.get(i);
+      String newName = newList.get(i);
+      if (!newName.equals(name))
+        s = rep(s, "\"" + name + "\"", "\"" + newName
+            + "\"");
+    }
+    return s;
+  }
+
+  public static String replaceStrings(String s, Lst<String> list,
+                                      Lst<String> newList) {
+    int n = list.size();
+    for (int i = 0; i < n; i++) {
+      String name = list.get(i);
+      String newName = newList.get(i);
+      if (!newName.equals(name))
+        s = rep(s, name, newName);
+    }
+    return s;
+  }
+
+  public static boolean isDigit(char ch) {
+    // just way simpler code than  Character.isDigit(ch);
+    int c = ch;
+    return (48 <= c && c <= 57);
+  }
+
+  public static boolean isUpperCase(char ch) {
+    int c = ch;
+    return (65 <= c && c <= 90);
+  }
+
+  public static boolean isLowerCase(char ch) {
+    int c = ch;
+    return (97 <= c && c <= 122);
+  }
+
+  public static boolean isLetter(char ch) {
+    // just way simpler code than     Character.isLetter(ch);
+    int c = ch;
+    return (65 <= c && c <= 90 || 97 <= c && c <= 122);
+  }
+
+  public static boolean isLetterOrDigit(char ch) {
+    // just way simpler code than     Character.isLetterOrDigit(ch);
+    int c = ch;
+    return (65 <= c && c <= 90 || 97 <= c && c <= 122 || 48 <= c && c <= 57);
+  }
+
+  public static boolean isWhitespace(char ch) {
+    int c = ch;
+    return (c >= 0x1c && c <= 0x20 || c >= 0x9 && c <= 0xd);
+  }
+
+  public static final float FRACTIONAL_PRECISION = 100000f;
+  public static final float CARTESIAN_PRECISION =  10000f;
+
+  public static void fixPtFloats(T3 pt, float f) {
+    //this will equate float and double as long as -256 <= x <= 256
+    pt.x = Math.round(pt.x * f) / f;
+    pt.y = Math.round(pt.y * f) / f;
+    pt.z = Math.round(pt.z * f) / f;
+  }
+  
+  public static double fixDouble(double d, double f) {
+    return Math.round(d * f) / f;
+  }
+
+  /**
+   * parse a float or "float/float"
+   * @param s
+   * @return a/b
+   */
+  public static float parseFloatFraction(String s) {
+      int pt = s.indexOf("/");
+      return (pt < 0 ? parseFloat(s) : parseFloat(s.substring(0, pt))
+          / parseFloat(s.substring(pt + 1)));
+  }
+
+//static {
+//    
+//  double d = 790.8999998888;
+//  float x  = 790.8999998888f;
+//  for (int i = 0; i < 50; i++) {
+//  System.out.println(x + " " + d);
+//  System.out.println(Math.round(x * 100000) / 100000f);
+//  System.out.println(Math.round(d * 100000) / 100000.);
+//  System.out.println(Math.round(x * 10000) / 10000f);
+//  System.out.println(Math.round(d * 10000) / 10000.);
+//  x+=1; 
+//  d+=1;
+//  }
+//  System.out.println(100.123456789f);
+//}
+
+//  static {
+//    long t;
+//    char c = '0';
+//    t = System.currentTimeMillis();
+//    for (int i = 0; i < 10000000; i++) {
+//      boolean b = PT.isUpperCase(c);
+//    }
+//    System.out.println(System.currentTimeMillis() - t);
+//
+//    t = System.currentTimeMillis();
+//    for (int i = 0; i < 10000000; i++) {
+//      boolean b = Character.isUpperCase(c);
+//    }
+//    System.out.println(System.currentTimeMillis() - t);
+//    
+//    t = System.currentTimeMillis();
+//    for (int i = 0; i < 10000000; i++) {
+//      boolean b = PT.isUpperCase(c);
+//    }
+//    System.out.println(System.currentTimeMillis() - t);
+//
+//    System.out.println("PT test");
+//  }
+}