From c61fe60b2f5b0dc8ea60ba63ca556862091e066b Mon Sep 17 00:00:00 2001 From: Jared Davis Date: Sat, 25 Jul 2026 12:14:48 -0400 Subject: [PATCH 01/12] eliminate regular expressions. Interim comments --- .../java/org/scijava/parsington/Literals.java | 379 +--------- .../org/scijava/parsington/ParseNumber.java | 686 ++++++++++++++++++ 2 files changed, 692 insertions(+), 373 deletions(-) create mode 100644 src/main/java/org/scijava/parsington/ParseNumber.java diff --git a/src/main/java/org/scijava/parsington/Literals.java b/src/main/java/org/scijava/parsington/Literals.java index f5997bd..674fe89 100644 --- a/src/main/java/org/scijava/parsington/Literals.java +++ b/src/main/java/org/scijava/parsington/Literals.java @@ -29,11 +29,6 @@ package org.scijava.parsington; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - /** * Utility methods for parsing literals from strings. These methods largely * conform to the Java specification's ideas of what constitutes a numeric or @@ -43,19 +38,6 @@ */ public final class Literals { - private static final Pattern HEX = Pattern.compile( - "(([-+]?)0[Xx]([0-9a-fA-F]+)" + - "([Ll]|(\\.[0-9a-fA-F]*)?[Pp]([-+]?)([0-9]+)([DdFf]|)|)).*"); - - private static final Pattern BINARY = Pattern.compile( - "(([-+]?)0[Bb]([01]+)([Ll]?)).*"); - - private static final Pattern OCTAL = Pattern.compile( - "(([-+]?)0([0-7]+)([Ll]?)).*"); - - private static final Pattern DECIMAL = Pattern.compile( - "(([-+]?[0-9]+(\\.[0-9]*)?([Ee][-+]?[0-9]+)?)([DdFfLl])?).*"); - private Literals() { // NB: Prevent instantiation of utility class. } @@ -90,83 +72,6 @@ public static String parseString(final CharSequence s) { return parseString(s, new Position()); } - /** - * Parses a hexidecimal literal. Both hexadecimal integer (e.g., - * {@code 0xfedcba9876543210}) and hexidecimal floating point (e.g., - * {@code 0xfedcba.98765432p10f}) are supported. - * - * @param s The string from which the numeric literal should be parsed. - * @return The parsed numeric value. For hexidecimal integers, returns an - * {@link Integer} if sufficiently small; or a {@link Long} if needed - * or if the {@code L} suffix is given; or a {@link BigInteger} if the - * value is too large even for {@code long}. For hexidecimal floating - * point, returns a {@link Float} if sufficiently small and the - * {@code F} suffix is given; or a {@link Double} otherwise (the - * {@code D} suffix is optional). - */ - public static Number parseHex(final CharSequence s) { - return parseHex(s, new Position()); - } - - /** - * Parses a binary literal (e.g., {@code 0b010101000011}). - * - * @param s The string from which the numeric literal should be parsed. - * @return The parsed numeric value—an {@link Integer} if sufficiently - * small, or a {@link Long} if needed or if the {@code L} suffix is - * given; or a {@link BigInteger} if the value is too large even for - * {@code long}. - */ - public static Number parseBinary(final CharSequence s) { - return parseBinary(s, new Position()); - } - - /** - * Parses an octal literal (e.g., {@code 01234567}). - * - * @param s The string from which the numeric literal should be parsed. - * @return The parsed numeric value—an {@link Integer} if sufficiently - * small, or a {@link Long} if needed or if the {@code L} suffix is - * given; or a {@link BigInteger} if the value is too large even for - * {@code long}. - */ - public static Number parseOctal(final CharSequence s) { - return parseOctal(s, new Position()); - } - - /** - * Parses a decimal literal (integer or otherwise; e.g., {@code 1234567890}, - * {@code 1234.0987} or {@code 1.2e34}). - * - * @param s The string from which the numeric literal should be parsed. - * @return The parsed numeric value, of a type consistent with Java's support - * for numeric primitives—or for values outside the normal range - * of Java primitives, {@link BigInteger} or {@link BigDecimal} as - * appropriate. Returns null if the string does not begin with the - * numeric literal telltale of a 0-9 digit with optional leading sign. - */ - public static Number parseDecimal(final CharSequence s) { - return parseDecimal(s, new Position()); - } - - /** - * Parses a numeric literal of any known type. - *

- * This parsing mechanism is intended to be as close as possible to the - * numeric literals supported by the Java programming language itself. - *

- * - * @param s The string from which the numeric literal should be parsed. - * @return The parsed numeric value, of a type consistent with Java's support - * for numeric primitives—or for values outside the normal range - * of Java primitives, {@link BigInteger} or {@link BigDecimal} as - * appropriate. Returns null if the string does not begin with the - * numeric literal telltale of a 0-9 digit with optional leading sign. - */ - public static Number parseNumber(final CharSequence s) { - return parseNumber(s, new Position()); - } - /** * Parses a literal of any known type (booleans, strings and numbers). * @@ -177,9 +82,9 @@ public static Number parseNumber(final CharSequence s) { * not match the syntax of a known literal. * @see #parseBoolean(CharSequence) * @see #parseString(CharSequence) - * @see #parseNumber(CharSequence) + * @see ParseNumber#parseAllNumbers(String) */ - public static Object parseLiteral(final CharSequence s) { + public static Object parseLiteral(final String s) { return parseLiteral(s, new Position()); } @@ -195,7 +100,6 @@ public static Object parseLiteral(final CharSequence s) { * with a boolean literal. */ public static Boolean parseBoolean(final CharSequence s, final Position pos) { - if (isWord(s, pos, "true")) { pos.inc(4); return Boolean.TRUE; @@ -294,162 +198,6 @@ public static String parseString(final CharSequence s, final Position pos) { return sb.toString(); } - /** - * Parses a hexidecimal literal. Both hexadecimal integer (e.g., - * {@code 0xfedcba9876543210}) and hexidecimal floating point (e.g., - * {@code 0xfedcba.98765432p10f}) are supported. - * - * @param s The string from which the numeric literal should be parsed. - * @param pos The offset from which the literal should be parsed. If parsing - * is successful, the position will be advanced to the next index - * after the parsed literal. - * @return The parsed numeric value. For hexidecimal integers, returns an - * {@link Integer} if sufficiently small; or a {@link Long} if needed - * or if the {@code L} suffix is given; or a {@link BigInteger} if the - * value is too large even for {@code long}. For hexidecimal floating - * point, returns a {@link Float} if sufficiently small and the - * {@code F} suffix is given; or a {@link Double} otherwise (the - * {@code D} suffix is optional). In either case, returns {@code null} - * if the string does not begin with the numeric literal telltale of a - * 0-9 digit with optional leading sign. - */ - public static Number parseHex(final CharSequence s, final Position pos) { - if (!isNumberSyntax(s, pos)) return null; - - final Matcher m = matcher(HEX, s, pos); - if (!m.matches()) return null; - final String sign = m.group(2); // + or - or nothing - final String integer = m.group(3); // hex digits before decimal point - final String suffix = m.group(4); // L or floating point expression - final boolean forceLong = "L".equalsIgnoreCase(suffix); - - final Number result; - if (forceLong || suffix.isEmpty()) { - // Integer notation. - final String number = sign + integer; - result = parseInteger(number, forceLong, 16); - } - else { - // Floating point notation. - final String token = m.group(1); // entire matched literal - //final String mantissa = m.group(5); // dot & hex digits after decimal point - //final String expSign = m.group(6); // + or - or nothing - //final String exp = m.group(7); // decimal exponent - final String expSuffix = m.group(8); // f or d or nothing - final boolean forceFloat = "F".equalsIgnoreCase(expSuffix); - final boolean forceDouble = "D".equalsIgnoreCase(expSuffix); - // NB: The BigDecimal code does not understand floating point - // hex strings, so the following invocation will never produce - // a larger-than-double-precision floating point BigDecimal. - // It's a convenient way to support float and double precision, - // but for BigDecimal support, we would need to process the - // matched groups above, converting hex to base 10 first. - result = parseDecimal(token, forceFloat, forceDouble); - } - return verifyResult(result, m, pos); - } - - /** - * Parses a binary literal (e.g., {@code 0b010101000011}). - * - * @param s The string from which the numeric literal should be parsed. - * @param pos The offset from which the literal should be parsed. If parsing - * is successful, the position will be advanced to the next index - * after the parsed literal. - * @return The parsed numeric value—an {@link Integer} if sufficiently - * small, or a {@link Long} if needed or if the {@code L} suffix is - * given; or a {@link BigInteger} if the value is too large even for - * {@code long}; or {@code null} if the string does not begin with the - * numeric literal telltale of a 0-9 digit with optional leading sign. - */ - public static Number parseBinary(final CharSequence s, final Position pos) { - return parseInteger(BINARY, s, pos, 2); - } - - /** - * Parses an octal literal (e.g., {@code 01234567}). - * - * @param s The string from which the numeric literal should be parsed. - * @param pos The offset from which the literal should be parsed. If parsing - * is successful, the position will be advanced to the next index - * after the parsed literal. - * @return The parsed numeric value—an {@link Integer} if sufficiently - * small, or a {@link Long} if needed or if the {@code L} suffix is - * given; or a {@link BigInteger} if the value is too large even for - * {@code long}; or {@code null} if the string does not begin with the - * numeric literal telltale of a 0-9 digit with optional leading sign. - */ - public static Number parseOctal(final CharSequence s, final Position pos) { - return parseInteger(OCTAL, s, pos, 8); - } - - /** - * Parses a decimal literal (e.g., {@code 1234.0987} or {@code 1.2e34}). - * - * @param s The string from which the numeric literal should be parsed. - * @param pos The offset from which the literal should be parsed. If parsing - * is successful, the position will be advanced to the next index - * after the parsed literal. - * @return The parsed numeric value, of a type consistent with Java's support - * for numeric primitives—or for values outside the normal range - * of Java primitives, {@link BigInteger} or {@link BigDecimal} as - * appropriate. Returns null if the string does not begin with the - * numeric literal telltale of a 0-9 digit with optional leading sign. - */ - public static Number parseDecimal(final CharSequence s, final Position pos) { - if (!isNumberSyntax(s, pos)) return null; - - final Matcher m = matcher(DECIMAL, s, pos); - if (!m.matches()) return null; - final String number = m.group(2); - final String force = m.group(5); - final boolean forceLong = "l".equalsIgnoreCase(force); - final boolean forceFloat = "f".equalsIgnoreCase(force); - final boolean forceDouble = "d".equalsIgnoreCase(force); - Number result = null; - if (m.start(3) < 0 && m.start(4) < 0 && !forceFloat && !forceDouble) { - // No decimal point and no exponent part. So this *might* be an integer! - result = parseInteger(number, forceLong, 10); - } - if (result == null && !forceLong) { - result = parseDecimal(number, forceFloat, forceDouble); - } - return verifyResult(result, m, pos); - } - - /** - * Parses a numeric literal of any known type. - *

- * This parsing mechanism is intended to be as close as possible to the - * numeric literals supported by the Java programming language itself. - *

- * - * @param s The string from which the numeric literal should be parsed. - * @param pos The offset from which the literal should be parsed. If parsing - * is successful, the position will be advanced to the next index - * after the parsed literal. - * @return The parsed numeric value, of a type consistent with Java's support - * for numeric primitives—or for values outside the normal range - * of Java primitives, {@link BigInteger} or {@link BigDecimal} as - * appropriate. Returns null if the string does not begin with the - * numeric literal telltale of a 0-9 digit with optional leading sign. - */ - public static Number parseNumber(final CharSequence s, final Position pos) { - final Number hex = parseHex(s, pos); - if (hex != null) return hex; - - final Number binary = parseBinary(s, pos); - if (binary != null) return binary; - - final Number octal = parseOctal(s, pos); - if (octal != null) return octal; - - final Number decimal = parseDecimal(s, pos); - if (decimal != null) return decimal; - - return null; - } - /** * Parses a literal of any known type (booleans, strings and numbers). * @@ -463,19 +211,16 @@ public static Number parseNumber(final CharSequence s, final Position pos) { * not match the syntax of a known literal. * @see #parseBoolean(CharSequence, Position) * @see #parseString(CharSequence, Position) - * @see #parseNumber(CharSequence, Position) + * @see ParseNumber#parseAllNumbers(String, Position) */ - public static Object parseLiteral(final CharSequence s, final Position pos) { + public static Object parseLiteral(final String s, final Position pos) { final Boolean bool = parseBoolean(s, pos); if (bool != null) return bool; final String str = parseString(s, pos); if (str != null) return str; - final Number num = parseNumber(s, pos); - if (num != null) return num; - - return null; + return ParseNumber.parseAllNumbers(s,pos); } // -- Helper methods -- @@ -495,122 +240,10 @@ private static char hex(final CharSequence s, final Position pos, return '\0'; // NB: Unreachable. } - private static boolean - isNumberSyntax(final CharSequence s, final Position pos) - { - final int i = pos.get(); - final boolean sign = s.charAt(i) == '-' || s.charAt(i) == '+'; - final int digitIndex = sign ? i + 1 : i; - if (digitIndex >= s.length()) return false; // at end of string - final char digit = s.charAt(digitIndex); - return digit >= '0' && digit <= '9'; - } - - private static Number parseInteger(final Pattern p, final CharSequence s, - final Position pos, final int base) - { - if (!isNumberSyntax(s, pos)) return null; - - final Matcher m = matcher(p, s, pos); - if (!m.matches()) return null; - final String sign = m.group(2); - final String number = sign + m.group(3); - final boolean forceLong = !m.group(4).isEmpty(); - final Number result = parseInteger(number, forceLong, base); - return verifyResult(result, m, pos); - } - - private static Number parseInteger(final String number, - final boolean forceLong, final int base) - { - if (!forceLong) { - // Try to fit it into an int. - try { - return Integer.parseInt(number, base); - } - catch (final NumberFormatException exc) { - // NB: No action needed. - } - } - - // Try to fit it into a long. - try { - return Long.parseLong(number, base); - } - catch (final NumberFormatException exc) { - // NB: No action needed. - } - - if (!forceLong) { - // Try to treat it as a BigInteger. - try { - return new BigInteger(number, base); - } - catch (final NumberFormatException exc) { - // NB: No action needed. - } - } - - return null; - } - - private static Number parseDecimal(final String number, - final boolean forceFloat, final boolean forceDouble) - { - if (forceFloat) { - // Try to fit it into a flaot. - try { - return Float.parseFloat(number); - } - catch (final NumberFormatException exc) { - // NB: No action needed. - } - } - else { - // Try to fit it into a double. - try { - return Double.parseDouble(number); - } - catch (final NumberFormatException exc) { - // NB: No action needed. - } - } - - if (!forceDouble && !forceFloat) { - // Try to treat it as a BigDecimal. - try { - return new BigDecimal(number); - } - catch (final NumberFormatException exc) { - // NB: No action needed. - } - } - - return null; - } - - private static Matcher matcher(final Pattern p, final CharSequence s, - final Position pos) - { - return p.matcher(sub(s, pos)); - } - - private static CharSequence sub(final CharSequence s, final Position pos) { - return pos.get() == 0 ? s : new SubSequence(s, pos.get()); - } - - private static Number verifyResult(final Number result, final Matcher m, - final Position pos) - { - if (result == null) pos.die("Illegal numeric literal"); - pos.inc(m.group(1).length()); - return result; - } - private static boolean isWord(final CharSequence s, final Position pos, final String word) { - if (s.length() - pos.get() < word.length()) return false; + // not needed since pos.ch will return 0 for out of bounds requests. if (s.length() - pos.get() < word.length()) return false; for (int i=0; i + * This parsing mechanism is intended to be as close as possible to the + * numeric literals supported by the Java programming language itself. + *

+ * + * @param s The string from which the numeric literal should be parsed. + * @return The parsed numeric value, of a type consistent with Java's support + * for numeric primitives—or for values outside the normal range + * of Java primitives, {@link BigInteger} or {@link BigDecimal} as + * appropriate. Returns null if a numeric literal is not detected. + */ + public static Number parseAllNumbers(String s) { + return parseAllNumbers(s, new Position()); + } + + /** + * Parses a numeric literal of any known type. + *

+ * This parsing mechanism is intended to be as close as possible to the + * numeric literals supported by the Java programming language itself. + *

+ * + * @param s The string from which the numeric literal should be parsed. + * @param pos The offset from which the literal should be parsed. If parsing + * is successful, the position will be advanced to the next index + * after the parsed literal. + * @return The parsed numeric value, of a type consistent with Java's support + * for numeric primitives—or for values outside the normal range + * of Java primitives, {@link BigInteger} or {@link BigDecimal} as + * appropriate. Returns null if a numeric literal is not detected. + */ + + public static Number parseAllNumbers(String s, Position pos) { + ParseNumberResults result = ParseNumber.identifyNumber(s, pos.get()); + ParseNumber.processNumber(s, result); + if (result.number != null) { + pos.inc(result.getLength()); + } + return result.number; + } + + + // identify + + private static ParseNumberResults identifyNumber(String s, int startingPosition) { + ParseNumberResults results = new ParseNumberResults(); + results.beginGroup[1] = startingPosition; + if (s == null || s.isEmpty()) { + results.numberType = NumberType.NOT_A_NUMBER; + return results; + } + int len = s.length(); + if (startingPosition >= len) { + results.numberType = NumberType.NOT_A_NUMBER; + return results; + } + int start = startingPosition; + + // Handle optional leading sign + char first = s.charAt(startingPosition); + if (first == '-' || first == '+') { + results.ndxSign = startingPosition; + start++; + if (len == 1) { + results.numberType = NumberType.NOT_A_NUMBER; + return results; + } + } + + // Check for Hexadecimal prefix (0x or 0X) + if (start + 2 < len && s.charAt(start) == '0' && + (s.charAt(start + 1) == 'x' || s.charAt(start + 1) == 'X')) { + results.numberType = NumberType.HEXADECIMAL; + extractHexNumber(s, start + 2, len, results); + return results; + } + + if (start + 2 < len && s.charAt(start) == '0' && + (s.charAt(start + 1) == 'b' || s.charAt(start + 1) == 'B')) { + results.numberType = NumberType.BINARY; + extractBinaryNumber(s, start + 2, len, results); + return results; + } + + // try decimal, internally checks for octal + extractDecimalNumber(s, start, len, results); + return results; + } + + // extract + +/** + * Attempts to parse a decimal literal (integer or otherwise; e.g., {@code 1234567890}, + * {@code 1234.0987} or {@code 1.2e34} or {@code 01234}). Parsing supports base 10 and base 8. + * + * + * @param s The string from which the numeric literal should be parsed. + * @param start The index of the string to start the parsing after the sign and 0[Xx] prefix. + * @param end The index of the last char to parse. + * @param results contains the parsing details calculated in this method. + * @return true if a potential decimal literal is found, false otherwise + * + * + * Based on regular expression (([-+]?[0-9]+(\.[0-9]*)?([Ee][-+]?[0-9]+)?)([DdFfLl])?).* + * group 1 = entire matching string. NB: re has .* postfix but this method does not include .* in this group. + * group 2 is from start of string including optional groups 3 and 4 + * group 3 is \. then [0-9]* + * group 4 is [Ee][-+]?[0-9]+ + * group 5 is [DdFfLl]? + * + * group 6 is not in re. [-+]? at start of string + * group 7 is not in re. [0-9]+ after sign, before group 3 + * group 8 is not in re. Leads to group 3 or 4 or 5 or end + * + * Octal encoding detection + * If we have (no group 3) and (no group 4) then do a scan for an octal value in group 2 + * Octal IFF + * group 7 starts with 0; does not contain 8 or 9; is longer than 1 + * group 5 == empty or Ll + */ + + private static boolean extractDecimalNumber(String s, int start, int end, ParseNumberResults results) { + char c; + + results.setSignGroup(6); + + boolean havedot = false; + boolean haveSign = false; + boolean haveFour = false; + + int group = 7; + results.beginGroup[group] = start; + + bigWhile: + while (start <= end) { + if (start == end) { + results.endGroup[group] = start; + break; + } + c = s.charAt(start); + switch (group) { + case 7: // [0-9]+ digits after sign and before decimal point + if (isDecimalDigit(c)) { + start++; + } else { + if (results.beginGroup[group] == start) { + // need at least 1 digit + results.numberType = NumberType.NOT_A_NUMBER; + break bigWhile; + } + results.endGroup[group] = start; + group = 8; + } + break; + + case 8: // transition to group 3, 4, 5 or end + if (c == '.') { + if (havedot) { // only one . allowed + results.numberType = NumberType.NOT_A_NUMBER; + break bigWhile; + } + havedot = true; + group = 3; + results.beginGroup[group] = start++; + break; + } + if (isE(c)) { + if (haveFour) { // one group 4 allowed + results.numberType = NumberType.NOT_A_NUMBER; + break bigWhile; + } + haveFour = true; + group = 4; + results.beginGroup[group] = start++; + break; + } + if (isADoubleOrFloatSuffix(c) || isL(c)) { + group = 5; + results.beginGroup[group] = start++; + results.endGroup[group] = start; + results.numberType = isL(c) ? NumberType.INTEGER: NumberType.DOUBLE; + // break bigWhile fall through below + } + break bigWhile; + case 3: // [0-9] 0..n times + if (isDecimalDigit(c)) { + start++; + } else { + results.endGroup[group] = start; + group = 8; + } + break; + case 4: // just [-+]?[0-9]+ of group 4. [Ee] done in 8 + if (isASign(c)) { + if (haveSign) { // only one sign allowed + results.numberType = NumberType.NOT_A_NUMBER; + break bigWhile; + } + haveSign = true; + start++; + } else if (isDecimalDigit(c)) { + start++; + } else { + results.endGroup[group] = start; + group = 8; // back to 8 for group 5 + } + break; + } // switch group + } // bigWhile + results.endGroup[1] = start; + + if (results.endGroup[7] - results.beginGroup[7] == 0) + results.numberType = NumberType.NOT_A_NUMBER; // need at least 1 digit in number before decimal + + if (results.beginGroup[4] != -1) { // we got an E + int minLength = (haveSign) ? 3 : 2; + if (results.endGroup[4] - results.beginGroup[4] < minLength) + results.numberType = NumberType.NOT_A_NUMBER; // need at least 1 digit in exponent + } + + if (results.numberType == null) { + if ((results.beginGroup[4] != -1) || (results.endGroup[3] != -1)) { + results.numberType = NumberType.DOUBLE; + } else { + results.numberType = NumberType.INTEGER; + } + } + + if (results.numberType != NumberType.NOT_A_NUMBER) { + // group 2 length = group 1 length - group 5 length + results.beginGroup[2] = results.beginGroup[1]; + results.endGroup[2] = results.endGroup[1]; + if (results.beginGroup[5] != -1) { + results.endGroup[2]--; + } + } + + // octal encoding ? + if (results.numberType == NumberType.INTEGER) { + if (s.charAt(results.beginGroup[7]) == '0') { + boolean octal = (results.endGroup[7] - results.beginGroup[7] > 1); + if (octal) { + for (int i = results.beginGroup[7] + 1; i < results.endGroup[7]; i++) { + if (!isOctalDigit(s.charAt(i))) { + octal = false; + break; + } + } + } + if (octal) { + if (results.beginGroup[5] != -1) { + if (!isL(s.charAt(results.beginGroup[5]))) { + octal = false; + } + } + } + if (octal) { + results.numberType = NumberType.OCTAL; + } + } + } + + return (results.numberType != NumberType.NOT_A_NUMBER); + } + + + + /** + * Parses a binary literal (e.g., {@code 0b010101000011}). + * + * @param s The string from which the numeric literal should be parsed. + * @param start The index of the string to start the parsing after the sign and 0[Bb] prefix. + * @param end The index of the last char to parse. + * @param results contains the parsing details calculated in this method. + * @return true if a potential binary literal is found, false otherwise + + * based on Regular Expression (([-+]?)0[Bb]([01]+)([Ll]?)).* + * group 1 = entire matching string. NB: re has .* postfix but this method does not include .* in this group. + * group 2 [-+]? This is processed by the caller and placed into ParseNumberResults ndxSign. + * group none 0[Bb] must exist. This is processed by the caller. + * group 3 [01]+ + * group 4 [lL]? + */ + + private static boolean extractBinaryNumber(String s, int start, int end, ParseNumberResults results) { + char c; + results.setSignGroup(2); + + int group = 3; + results.beginGroup[group] = start; + + bigWhile: + while (start <= end) { + if (start == end) { + results.endGroup[group] = start; + break; + } + c = s.charAt(start); + switch (group) { + case 3: // [01]+ + if (isBinaryDigit(c)) { + start++; + } else { + results.endGroup[group] = start; + group = 4; + } + break; + case 4: // [Ll]? + if (isL(c)) { + results.beginGroup[group] = start; + results.endGroup[group] = ++start; + } + // ok - we are done + break bigWhile; + } // switch group + } // while bigWhile + results.endGroup[1] = start; + // need at least 1 digit in group 3 + if (results.endGroup[3] - results.beginGroup[3] == 0) + results.numberType = NumberType.NOT_A_NUMBER; + return (results.numberType == NumberType.BINARY); + } + + /** + * Parses a hexidecimal literal. Both hexadecimal integer (e.g., + * {@code 0xfedcba9876543210}) and hexidecimal floating point (e.g., + * {@code 0xfedcba.98765432p10f}) are supported. + * + * @param s The string from which the numeric literal should be parsed. + * @param start The index of the string to start the parsing after the sign and 0[Xx] prefix. + * @param end The index of the last char to parse. + * @param results contains the parsing details calculated in this method. + * @return true if a potential hexidecimal literal is found, false otherwise + * + * + * based on Regular Expression (([-+]?)0[Xx]([0-9a-fA-F]+)([Ll]|(\\.[0-9a-fA-F]*)?[Pp]([-+]?)([0-9]+)([DdFf]|)|)).* + * group 1 = entire matching string. NB: re has .* postfix but this method does not include .* in this group. + * group 2 [-+]? This is processed by the caller and placed into ParseNumberResults ndxSign. + * group none 0[Xx] This is processed by the caller. + * group 3 [0-9a-fA-F]+ repeat. + * group 4 includes groups 5, 6, 7 and 8 or [lL]? + * group 5 (\\.[0-9a-fA-F]*)? Entire group is optional + * group 6 requires a [Pp] prefix which is not part of the group. [-+]? + * group 7 [0-9]+ + * group 8 [DdFf]? + * group 9, not in re, is [Pp] which is required before groups 6, 7. + */ + + private static boolean extractHexNumber(String s, int start, int end, ParseNumberResults results) { + char c; + + results.setSignGroup(2); + + int group = 3; + results.beginGroup[3] = start; + bigWhile: + while (start <= end) { + if (start == end) { + results.endGroup[group] = start; + break; + } + c = s.charAt(start); // start may not change when group changes + switch (group) { + case 3: // [0-9a-fA-F]+ + if (isHexDigit(c)) { + start++; + } else { + results.endGroup[group] = start; + group = 4; + results.beginGroup[group] = start; + } + break; + case 4: // [Ll]? group end is adjusted after switch to include groups 5-8. + if (isL(c)) { + results.endGroup[group] = ++start; + break bigWhile; // end of re. No need to continue parsing chars + } + if (c == '.') { + group = 5; + results.beginGroup[group] = start++; + break; + } + if (isP(c)) { + start++; + group = 9; + break; + } + // we are done, there is no group 4 + results.beginGroup[group] = -1; + break bigWhile; + case 5: // \.[0-9a-fA-F]* 0..n + if (isHexDigit(c)) { + start++; + } else { + results.endGroup[group] = start; + if (isP(c)) { + group = 9; + results.beginGroup[group] = start; + results.endGroup[group] = ++start; + } + } + break; + case 9: // start -1 == p or P + if (isASign(c)) { + group = 6; + results.beginGroup[group] = start; + results.endGroup[group] = ++start; + } + group = 7; + break; + case 7: // [0-9]+ enforce at least 1 char in this group + if (isDecimalDigit(c)) { + if (results.beginGroup[group] == -1) results.beginGroup[group] = start; + start++; + } else { + if (results.beginGroup[group] == -1) { + // need at least 1 digit to be valid + results.numberType = NumberType.NOT_A_NUMBER; + break bigWhile; + } else { + results.endGroup[group] = start; + group = 8; + } + } + break; + case 8: // [DdFf]? + if (isADoubleOrFloatSuffix(c)) { + results.beginGroup[8] = start; + results.endGroup[8] = ++start; + } + break bigWhile; + } // switch group + } // while bigWhile + + results.endGroup[1] = start; + results.endGroup[4] = Math.max(results.endGroup[4], Math.max(Math.max(results.endGroup[5], results.endGroup[6]), Math.max(results.endGroup[7], results.endGroup[8]))); + // P (in group 9) requires at least one entry in group 7 + if ((results.beginGroup[9] != -1) && (results.beginGroup[7] == -1)) + results.numberType = NumberType.NOT_A_NUMBER; + // group 3 must have at least 1 digit + if (results.endGroup[3] - results.beginGroup[3] == 0) + results.numberType = NumberType.NOT_A_NUMBER; + return (results.numberType == NumberType.HEXADECIMAL); + } + + // process extracted results + + private static void processNumber(String in, ParseNumberResults results) { + switch (results.numberType) { + case INTEGER: + case DOUBLE: + results.number = processDecimal(in, results); + break; + case HEXADECIMAL: + results.number = processHex(in, results); + break; + case BINARY: + results.number = processBinary(in, results); + break; + case OCTAL: + results.number = processOctal(in, results); + break; + } + } + + private static Number processHex(final String s, ParseNumberResults results) { + final String sign = results.getGroup(s, 2); // + or - or "" + final String integer = results.getGroup(s, 3); // hex digits before decimal point + final String suffix = results.getGroup(s, 4); // L or floating point expression + final boolean forceLong = "L".equalsIgnoreCase(String.valueOf(suffix)); + + final Number result; + if (forceLong || suffix.isEmpty()) { + // Integer notation. + final String number = sign + integer; + result = parseIntegerToNumber(number, forceLong, 16); + } else { + // Floating point notation. + final String token = results.getGroup(s, 1); // entire matched literal + final String expSuffix = results.getGroup(s, 8); // f or d or nothing + final boolean forceFloat = "F".equalsIgnoreCase(expSuffix); + final boolean forceDouble = "D".equalsIgnoreCase(expSuffix); + // NB: The BigDecimal code does not understand floating point + // hex strings, so the following invocation will never produce + // a larger-than-double-precision floating point BigDecimal. + // It's a convenient way to support float and double precision, + // but for BigDecimal support, we would need to process the + // matched groups above, converting hex to base 10 first. + result = parseDecimalToNumber(token, forceFloat, forceDouble); + } + return result;//verifyResult(result, m, pos); + } + + private static Number processBinary(String s, ParseNumberResults results) { + final String sign = results.getGroup(s, 2); + final String number = sign + results.getGroup(s, 3); + + final boolean forceLong = !results.getGroup(s, 4).isEmpty(); + final Number result = parseIntegerToNumber(number, forceLong, 2); + return result; + } + + // NB: uses decimal regular expression group numbers + private static Number processOctal(String s, ParseNumberResults results) { + final String number = results.getGroup(s, 2); + final String expSuffix = results.getGroup(s, 5); // Ll nothing + final boolean forceLong = "L".equalsIgnoreCase(expSuffix); + final Number result = parseIntegerToNumber(number, forceLong, 8); + return result; + } + + private static Number processDecimal(String s, ParseNumberResults results) { + final String numberStr = results.getGroup(s, 2); + final String force = results.getGroup(s, 5); + final boolean forceLong = "l".equalsIgnoreCase(force); + final boolean forceFloat = "f".equalsIgnoreCase(force); + final boolean forceDouble = "d".equalsIgnoreCase(force); + Number result = null; + if (results.numberType == NumberType.INTEGER) { + // No decimal point and no exponent part. So this *might* be an integer! + result = parseIntegerToNumber(numberStr, forceLong, 10); + } + if (result == null && !forceLong) { + result = parseDecimalToNumber(numberStr, forceFloat, forceDouble); + } + return result; + } + + + // parse resulting Strings and flags to Number + + /** + * Parses an Integer literal (e.g., {@code 1234}). + * Attempts to minimize swallowed Exceptions by parsing as a long first. + * + * @param number The string from which the numeric literal should be parsed. + * @param forceLong boolean flag to require a long Number be returned + * @param base radix for conversion + * @return The parsed numeric value—an {@link Integer} if sufficiently + * small, or a {@link Long} if needed or if the {@code L} suffix is + * given; or a {@link BigInteger} if the value is too large even for + * {@code long} or null. + */ + + private static Number parseIntegerToNumber(final String number, + final boolean forceLong, final int base) { + + // parse to long, convert to int if in range and not forced to a long + long result ; + try { + result = Long.parseLong(number, base); + if (forceLong || result < Integer.MIN_VALUE || result > Integer.MAX_VALUE) { + return result; + } + return (int) result; // NB: range check above to avoid silent loss + } catch (NumberFormatException e) { + // will not fit in long or bad text + ignoredExceptions++; // not thread safe, ok for rough metric + if (!forceLong) { + try { + return new BigInteger(number, base); + } catch (final NumberFormatException exc) { + ignoredExceptions++; + } + } + } + return null; + } + + + /** + * Parses a Decimal literal (e.g., {@code 1234.1}). + * + * @param number The string from which the numeric literal should be parsed. + * @param forceFloat boolean flag to require a float Number be returned + * @param forceDouble boolean flag to require a double Number be returned + * @return The parsed numeric value—an {@link Double}, {@link Float} or {@link BigDecimal} or null + */ + private static Number parseDecimalToNumber(final String number, + final boolean forceFloat, final boolean forceDouble) { + if (forceFloat) { + // Try to fit it into a float. + try { + return Float.parseFloat(number); + } catch (final NumberFormatException exc) { + // NB: No action needed. + ignoredExceptions++; + } + } else { + // Try to fit it into a double. + try { + return Double.parseDouble(number); + } catch (final NumberFormatException exc) { + // NB: No action needed. + ignoredExceptions++; + } + } + + if (!forceDouble && !forceFloat) { + // Try to treat it as a BigDecimal. + try { + return new BigDecimal(number); + } catch (final NumberFormatException exc) { + // NB: No action needed. + ignoredExceptions++; + } + } + + return null; + } + + // Helpers + + private static boolean isHexDigit(char c) { + return ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')); + } + + private static boolean isOctalDigit(char c) { + return (c >= '0' && c <= '7'); + } + + private static boolean isL(char c) { + return (c == 'L' || c == 'l'); + } + + private static boolean isP(char c) { + return (c == 'P' || c == 'p'); + } + + private static boolean isE(char c) { + return (c == 'E' || c == 'e'); + } + + private static boolean isDecimalDigit(char c) { + return (c >= '0' && c <= '9'); + } + + private static boolean isBinaryDigit(char c) { + return (c == '0' || c == '1'); + } + + private static boolean isASign(char c) { + return (c == '+' || c == '-'); + } + + private static boolean isADoubleOrFloatSuffix(char c) { + return (c == 'D' || c == 'd' || c == 'F' || c == 'f'); + } + + public enum NumberType { + NOT_A_NUMBER, + INTEGER, // or LONG or BIGINT + DOUBLE, // or FLOAT + HEXADECIMAL, + OCTAL, // INTEGER or LONG + BINARY, // INTEGER, DOUBLE, FLOAT + } + + +} From 1236d22ae4134c38713d3bf1dbad0bff7fb2b7da Mon Sep 17 00:00:00 2001 From: Jared Davis Date: Sat, 25 Jul 2026 10:14:18 -0400 Subject: [PATCH 02/12] Eliminate regular expressions --- .../parsington/ParseNumberResults.java | 33 +++ .../eval/AbstractStandardEvaluator.java | 7 +- .../org/scijava/parsington/LiteralsTest.java | 156 ++++++------ .../scijava/parsington/ParseNumberTest.java | 222 ++++++++++++++++++ 4 files changed, 335 insertions(+), 83 deletions(-) create mode 100644 src/main/java/org/scijava/parsington/ParseNumberResults.java create mode 100644 src/test/java/org/scijava/parsington/ParseNumberTest.java diff --git a/src/main/java/org/scijava/parsington/ParseNumberResults.java b/src/main/java/org/scijava/parsington/ParseNumberResults.java new file mode 100644 index 0000000..8e0f17f --- /dev/null +++ b/src/main/java/org/scijava/parsington/ParseNumberResults.java @@ -0,0 +1,33 @@ +package org.scijava.parsington; + +public class ParseNumberResults { + int ndxSign = -1; + ParseNumber.NumberType numberType; + int[] beginGroup = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; + int[] endGroup = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; + Number number =null; + + void setSignGroup(int signGroup) { + if (ndxSign != -1) { + beginGroup[signGroup] = ndxSign; + endGroup[signGroup] = beginGroup[signGroup] + 1; + } + } + + String getGroup(String in, int group) { + if ((beginGroup[group] != -1) && (endGroup[group] != -1)) { + return in.substring( beginGroup[group], endGroup[group]); + } else { + return ""; + } + } + + int getLength() { + final int ONE = 1; + if ((beginGroup[ONE] != -1) && (endGroup[ONE] != -1)) { + return endGroup[ONE] - beginGroup[ONE]; + } + return 0; + } + +} diff --git a/src/main/java/org/scijava/parsington/eval/AbstractStandardEvaluator.java b/src/main/java/org/scijava/parsington/eval/AbstractStandardEvaluator.java index b676a5a..251de5a 100644 --- a/src/main/java/org/scijava/parsington/eval/AbstractStandardEvaluator.java +++ b/src/main/java/org/scijava/parsington/eval/AbstractStandardEvaluator.java @@ -35,10 +35,7 @@ import java.util.List; import java.util.Objects; -import org.scijava.parsington.ExpressionParser; -import org.scijava.parsington.Literals; -import org.scijava.parsington.Tokens; -import org.scijava.parsington.Variable; +import org.scijava.parsington.*; /** * Base class for {@link StandardEvaluator} implementations on common @@ -582,7 +579,7 @@ private String str(final Object v) { /** Coerces the given value to a number. */ private Number num(final Object v) { final Number n = cast(v, Number.class); - return n != null ? n : Literals.parseNumber(v.toString()); + return n != null ? n : ParseNumber.parseAllNumbers(v.toString()); } private int i(final Object v) { return num(v).intValue(); } diff --git a/src/test/java/org/scijava/parsington/LiteralsTest.java b/src/test/java/org/scijava/parsington/LiteralsTest.java index ea9b9a1..58d6a19 100644 --- a/src/test/java/org/scijava/parsington/LiteralsTest.java +++ b/src/test/java/org/scijava/parsington/LiteralsTest.java @@ -109,195 +109,195 @@ public void testParseStringInvalid() { @Test public void testParseHexInteger() { - assertNumber(0x123, Literals.parseHex("0x123")); + assertNumber(0x123, ParseNumber.parseAllNumbers("0x123")); // Test explicit long. - assertNumber(0x123L, Literals.parseHex("0x123L")); + assertNumber(0x123L, ParseNumber.parseAllNumbers("0x123L")); // Test implicit long. - assertNumber(0x123456789abcdefL, Literals.parseHex("0x123456789abcdef")); + assertNumber(0x123456789abcdefL, ParseNumber.parseAllNumbers("0x123456789abcdef")); // Test BigInteger. final String big = "123456789abcdeffedcba987654321"; - final Number bigNum = Literals.parseHex("0x" + big); + final Number bigNum = ParseNumber.parseAllNumbers("0x" + big); assertNumber(new BigInteger(big, 16), bigNum); } @Test public void testParseHexNegativeInteger() { - assertNumber(-0x123, Literals.parseHex("-0x123")); + assertNumber(-0x123, ParseNumber.parseAllNumbers("-0x123")); // Test explicit long. - assertNumber(-0x123L, Literals.parseHex("-0x123L")); + assertNumber(-0x123L, ParseNumber.parseAllNumbers("-0x123L")); // Test implicit long. - assertNumber(-0x123456789abcdefL, Literals.parseHex("-0x123456789abcdef")); + assertNumber(-0x123456789abcdefL, ParseNumber.parseAllNumbers("-0x123456789abcdef")); // Test BigInteger. final String big = "123456789abcdeffedcba987654321"; - final Number bigNum = Literals.parseHex("-0x" + big); + final Number bigNum = ParseNumber.parseAllNumbers("-0x" + big); assertNumber(new BigInteger("-" + big, 16), bigNum); } @Test public void testParseHexFloat() { assertNumber(0xfedcba.98765432P-10f, // - Literals.parseHex("0xfedcba.98765432P-10f")); + ParseNumber.parseAllNumbers("0xfedcba.98765432P-10f")); assertNumber(0x1.fffffffffffffP+1023, // - Literals.parseHex("0x1.fffffffffffffP+1023")); - assertNumber(0xff.fP0F, Literals.parseHex("0xff.fP0F")); - assertNumber(0x1P+1023, Literals.parseHex("0x1P+1023")); - assertNumber(0xfP102, Literals.parseHex("0xfP102")); - assertNumber(0xfP-102, Literals.parseHex("0xfP-102")); - assertNumber(0xffP-102, Literals.parseHex("0xffP-102")); - assertNumber(0x123.456P1, Literals.parseHex("0x123.456P1")); - assertNumber(0x123.456P0f, Literals.parseHex("0x123.456P0f")); - assertNumber(0x123.456P0d, Literals.parseHex("0x123.456P0d")); - assertNumber(0x123.456P-10d, Literals.parseHex("0x123.456P-10d")); - assertNumber(0x123.456P+009, Literals.parseHex("0x123.456P+009")); + ParseNumber.parseAllNumbers("0x1.fffffffffffffP+1023")); + assertNumber(0xff.fP0F, ParseNumber.parseAllNumbers("0xff.fP0F")); + assertNumber(0x1P+1023, ParseNumber.parseAllNumbers("0x1P+1023")); + assertNumber(0xfP102, ParseNumber.parseAllNumbers("0xfP102")); + assertNumber(0xfP-102, ParseNumber.parseAllNumbers("0xfP-102")); + assertNumber(0xffP-102, ParseNumber.parseAllNumbers("0xffP-102")); + assertNumber(0x123.456P1, ParseNumber.parseAllNumbers("0x123.456P1")); + assertNumber(0x123.456P0f, ParseNumber.parseAllNumbers("0x123.456P0f")); + assertNumber(0x123.456P0d, ParseNumber.parseAllNumbers("0x123.456P0d")); + assertNumber(0x123.456P-10d, ParseNumber.parseAllNumbers("0x123.456P-10d")); + assertNumber(0x123.456P+009, ParseNumber.parseAllNumbers("0x123.456P+009")); } @Test public void testParseHexNegativeFloat() { assertNumber(-0xfedcba.98765432P-10f, // - Literals.parseHex("-0xfedcba.98765432P-10f")); + ParseNumber.parseAllNumbers("-0xfedcba.98765432P-10f")); assertNumber(-0x1.fffffffffffffP+1023, // - Literals.parseHex("-0x1.fffffffffffffP+1023")); - assertNumber(-0xff.fP0F, Literals.parseHex("-0xff.fP0F")); - assertNumber(-0x1P+1023, Literals.parseHex("-0x1P+1023")); - assertNumber(-0xfP102, Literals.parseHex("-0xfP102")); - assertNumber(-0xfP-102, Literals.parseHex("-0xfP-102")); - assertNumber(-0xffP-102, Literals.parseHex("-0xffP-102")); - assertNumber(-0x123.456P1, Literals.parseHex("-0x123.456P1")); - assertNumber(-0x123.456P0f, Literals.parseHex("-0x123.456P0f")); - assertNumber(-0x123.456P0d, Literals.parseHex("-0x123.456P0d")); - assertNumber(-0x123.456P-10d, Literals.parseHex("-0x123.456P-10d")); - assertNumber(-0x123.456P+009, Literals.parseHex("-0x123.456P+009")); + ParseNumber.parseAllNumbers("-0x1.fffffffffffffP+1023")); + assertNumber(-0xff.fP0F, ParseNumber.parseAllNumbers("-0xff.fP0F")); + assertNumber(-0x1P+1023, ParseNumber.parseAllNumbers("-0x1P+1023")); + assertNumber(-0xfP102, ParseNumber.parseAllNumbers("-0xfP102")); + assertNumber(-0xfP-102, ParseNumber.parseAllNumbers("-0xfP-102")); + assertNumber(-0xffP-102, ParseNumber.parseAllNumbers("-0xffP-102")); + assertNumber(-0x123.456P1, ParseNumber.parseAllNumbers("-0x123.456P1")); + assertNumber(-0x123.456P0f, ParseNumber.parseAllNumbers("-0x123.456P0f")); + assertNumber(-0x123.456P0d, ParseNumber.parseAllNumbers("-0x123.456P0d")); + assertNumber(-0x123.456P-10d, ParseNumber.parseAllNumbers("-0x123.456P-10d")); + assertNumber(-0x123.456P+009, ParseNumber.parseAllNumbers("-0x123.456P+009")); } @Test public void testParseBinary() { // NB: "0b..." syntax is only supported starting with Java 7. - assertNumber(33, Literals.parseBinary("0b100001")); + assertNumber(33, ParseNumber.parseAllNumbers("0b100001")); // Test explicit long. - assertNumber(33L, Literals.parseBinary("0b100001L")); + assertNumber(33L, ParseNumber.parseAllNumbers("0b100001L")); // Test implicit long. - assertNumber(194588677707L, Literals.parseBinary( + assertNumber(194588677707L, ParseNumber.parseAllNumbers( "0b10110101001110011000111001011001001011")); // Test BigInteger. final String big = "10110011100011110000111110000011111100000011111110000000" + "111111110000000011111111100000000011111111110000000000"; - final Number bigNum = Literals.parseBinary("0b" + big); + final Number bigNum = ParseNumber.parseAllNumbers("0b" + big); assertNumber(new BigInteger(big, 2), bigNum); } @Test public void testParseBinaryNegative() { // NB: "0b..." syntax is only supported starting with Java 7. - assertNumber(-33, Literals.parseBinary("-0b100001")); + assertNumber(-33, ParseNumber.parseAllNumbers("-0b100001")); // Test explicit long. - assertNumber(-33L, Literals.parseBinary("-0b100001L")); + assertNumber(-33L, ParseNumber.parseAllNumbers("-0b100001L")); // Test implicit long. - assertNumber(-194588677707L, Literals.parseBinary( + assertNumber(-194588677707L, ParseNumber.parseAllNumbers( "-0b10110101001110011000111001011001001011")); // Test BigInteger. final String big = "10110011100011110000111110000011111100000011111110000000" + "111111110000000011111111100000000011111111110000000000"; - final Number bigNum = Literals.parseBinary("-0b" + big); + final Number bigNum = ParseNumber.parseAllNumbers("-0b" + big); assertNumber(new BigInteger("-" + big, 2), bigNum); } @Test public void testParseOctal() { - assertNumber(01234567, Literals.parseOctal("01234567")); + assertNumber(01234567, ParseNumber.parseAllNumbers("01234567")); // Test explicit long. - assertNumber(01234567L, Literals.parseOctal("01234567L")); + assertNumber(01234567L, ParseNumber.parseAllNumbers("01234567L")); // Test implicit long. - assertNumber(012345677654321L, Literals.parseOctal("012345677654321")); + assertNumber(012345677654321L, ParseNumber.parseAllNumbers("012345677654321")); // Test BigInteger. final String big = "1234567765432112345677654321"; - final Number bigNum = Literals.parseOctal("0" + big); + final Number bigNum = ParseNumber.parseAllNumbers("0" + big); assertNumber(new BigInteger(big, 8), bigNum); } @Test public void testParseOctalNegative() { - assertNumber(-01234567, Literals.parseOctal("-01234567")); + assertNumber(-01234567, ParseNumber.parseAllNumbers("-01234567")); // Test explicit long. - assertNumber(-01234567L, Literals.parseOctal("-01234567L")); + assertNumber(-01234567L, ParseNumber.parseAllNumbers("-01234567L")); // Test implicit long. - assertNumber(-012345677654321L, Literals.parseOctal("-012345677654321")); + assertNumber(-012345677654321L, ParseNumber.parseAllNumbers("-012345677654321")); // Test BigInteger. final String big = "1234567765432112345677654321"; - final Number bigNum = Literals.parseOctal("-0" + big); + final Number bigNum = ParseNumber.parseAllNumbers("-0" + big); assertNumber(new BigInteger("-" + big, 8), bigNum); } @Test public void testParseDecimal() { - assertNumber(123456789, Literals.parseDecimal("123456789")); + assertNumber(123456789, ParseNumber.parseAllNumbers("123456789")); // Test explicit long. - assertNumber(123456789L, Literals.parseDecimal("123456789L")); + assertNumber(123456789L, ParseNumber.parseAllNumbers("123456789L")); // Test implicit long. - assertNumber(123456787654321L, Literals.parseDecimal("123456787654321")); + assertNumber(123456787654321L, ParseNumber.parseAllNumbers("123456787654321")); // Test BigInteger. final String bigI = "1234567898765432123456789"; - final Number bigInt = Literals.parseDecimal(bigI); + final Number bigInt = ParseNumber.parseAllNumbers(bigI); assertNumber(new BigInteger(bigI), bigInt); // Test explicit float. - assertNumber(1f, Literals.parseDecimal("1f")); + assertNumber(1f, ParseNumber.parseAllNumbers("1f")); // Test explicit double. - assertNumber(1d, Literals.parseDecimal("1d")); + assertNumber(1d, ParseNumber.parseAllNumbers("1d")); // Test implicit double. - assertNumber(1.0, Literals.parseDecimal("1.0")); - assertNumber(1., Literals.parseDecimal("1.")); + assertNumber(1.0, ParseNumber.parseAllNumbers("1.0")); + assertNumber(1., ParseNumber.parseAllNumbers("1.")); // Test scientific notation. - assertNumber(1e2, Literals.parseDecimal("1e2")); - assertNumber(1.2e3, Literals.parseDecimal("1.2e3")); - assertNumber(4.5e-6, Literals.parseDecimal("4.5e-6")); - assertNumber(1.2e3f, Literals.parseDecimal("1.2e3f")); - assertNumber(4.5e-6f, Literals.parseDecimal("4.5e-6f")); + assertNumber(1e2, ParseNumber.parseAllNumbers("1e2")); + assertNumber(1.2e3, ParseNumber.parseAllNumbers("1.2e3")); + assertNumber(4.5e-6, ParseNumber.parseAllNumbers("4.5e-6")); + assertNumber(1.2e3f, ParseNumber.parseAllNumbers("1.2e3f")); + assertNumber(4.5e-6f, ParseNumber.parseAllNumbers("4.5e-6f")); } @Test public void testParseDecimalNegative() { - assertNumber(-123456789, Literals.parseDecimal("-123456789")); + assertNumber(-123456789, ParseNumber.parseAllNumbers("-123456789")); // Test explicit long. - assertNumber(-123456789L, Literals.parseDecimal("-123456789L")); + assertNumber(-123456789L, ParseNumber.parseAllNumbers("-123456789L")); // Test implicit long. - assertNumber(-123456787654321L, Literals.parseDecimal("-123456787654321")); + assertNumber(-123456787654321L, ParseNumber.parseAllNumbers("-123456787654321")); // Test BigInteger. final String bigI = "-1234567898765432123456789"; - final Number bigInt = Literals.parseDecimal(bigI); + final Number bigInt = ParseNumber.parseAllNumbers(bigI); assertNumber(new BigInteger(bigI), bigInt); // Test explicit float. - assertNumber(-1f, Literals.parseDecimal("-1f")); + assertNumber(-1f, ParseNumber.parseAllNumbers("-1f")); // Test explicit double. - assertNumber(-1d, Literals.parseDecimal("-1d")); + assertNumber(-1d, ParseNumber.parseAllNumbers("-1d")); // Test implicit double. - assertNumber(-1.0, Literals.parseDecimal("-1.0")); - assertNumber(-1., Literals.parseDecimal("-1.")); + assertNumber(-1.0, ParseNumber.parseAllNumbers("-1.0")); + assertNumber(-1., ParseNumber.parseAllNumbers("-1.")); // Test scientific notation. - assertNumber(-1e2, Literals.parseDecimal("-1e2")); - assertNumber(-1.2e3, Literals.parseDecimal("-1.2e3")); - assertNumber(-4.5e-6, Literals.parseDecimal("-4.5e-6")); - assertNumber(-1.2e3f, Literals.parseDecimal("-1.2e3f")); - assertNumber(-4.5e-6f, Literals.parseDecimal("-4.5e-6f")); + assertNumber(-1e2, ParseNumber.parseAllNumbers("-1e2")); + assertNumber(-1.2e3, ParseNumber.parseAllNumbers("-1.2e3")); + assertNumber(-4.5e-6, ParseNumber.parseAllNumbers("-4.5e-6")); + assertNumber(-1.2e3f, ParseNumber.parseAllNumbers("-1.2e3f")); + assertNumber(-4.5e-6f, ParseNumber.parseAllNumbers("-4.5e-6f")); } @Test public void testParseNumber() { final Position pos = new Position(); - assertNumber(0, Literals.parseNumber("0", pos)); + assertNumber(0, ParseNumber.parseAllNumbers("0", pos)); assertEquals(1, pos.get()); pos.set(1); - assertNumber(5.7, Literals.parseNumber("a5.7a", pos)); + assertNumber(5.7, ParseNumber.parseAllNumbers("a5.7a", pos)); assertEquals(4, pos.get()); pos.set(2); - assertNumber(-11, Literals.parseNumber("bb-11bb", pos)); + assertNumber(-11, ParseNumber.parseAllNumbers("bb-11bb", pos)); assertEquals(5, pos.get()); pos.set(3); - assertNumber(0x123L, Literals.parseNumber("ccc0x123Lccc", pos)); + assertNumber(0x123L, ParseNumber.parseAllNumbers("ccc0x123Lccc", pos)); assertEquals(9, pos.get()); } diff --git a/src/test/java/org/scijava/parsington/ParseNumberTest.java b/src/test/java/org/scijava/parsington/ParseNumberTest.java new file mode 100644 index 0000000..7c5726c --- /dev/null +++ b/src/test/java/org/scijava/parsington/ParseNumberTest.java @@ -0,0 +1,222 @@ +/* + * #%L + * Parsington: the SciJava mathematical expression parser. + * %% + * Copyright (C) 2015 - 2026 Parsington developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.parsington; + +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; + + +/** + * Tests {@link Literals}. + * + * @author Curtis Rueden + */ +public class ParseNumberTest extends AbstractTest { + + + @Test + public void testParseHexInteger() { + assertNumber(0x123, ParseNumber.parseAllNumbers("0x123")); + // Test explicit long. + assertNumber(0x123L, ParseNumber.parseAllNumbers("0x123L")); + // Test implicit long. + assertNumber(0x123456789abcdefL, ParseNumber.parseAllNumbers("0x123456789abcdef")); + // Test BigInteger. + final String big = "123456789abcdeffedcba987654321"; + final Number bigNum = ParseNumber.parseAllNumbers("0x" + big); + assertNumber(new BigInteger(big, 16), bigNum); + } + + @Test + public void testParseHexNegativeInteger() { + assertNumber(-0x123, ParseNumber.parseAllNumbers("-0x123")); + // Test explicit long. + assertNumber(-0x123L, ParseNumber.parseAllNumbers("-0x123L")); + // Test implicit long. + assertNumber(-0x123456789abcdefL, ParseNumber.parseAllNumbers("-0x123456789abcdef")); + // Test BigInteger. + final String big = "123456789abcdeffedcba987654321"; + final Number bigNum = ParseNumber.parseAllNumbers("-0x" + big); + assertNumber(new BigInteger("-" + big, 16), bigNum); + } + + @Test + public void testParseHexFloat() { + assertNumber(0xfedcba.98765432P-10f, // + ParseNumber.parseAllNumbers("0xfedcba.98765432P-10f")); + assertNumber(0x1.fffffffffffffP+1023, // + ParseNumber.parseAllNumbers("0x1.fffffffffffffP+1023")); + assertNumber(0xff.fP0F, ParseNumber.parseAllNumbers("0xff.fP0F")); + assertNumber(0x1P+1023, ParseNumber.parseAllNumbers("0x1P+1023")); + assertNumber(0xfP102, ParseNumber.parseAllNumbers("0xfP102")); + assertNumber(0xfP-102, ParseNumber.parseAllNumbers("0xfP-102")); + assertNumber(0xffP-102, ParseNumber.parseAllNumbers("0xffP-102")); + assertNumber(0x123.456P1, ParseNumber.parseAllNumbers("0x123.456P1")); + assertNumber(0x123.456P0f, ParseNumber.parseAllNumbers("0x123.456P0f")); + assertNumber(0x123.456P0d, ParseNumber.parseAllNumbers("0x123.456P0d")); + assertNumber(0x123.456P-10d, ParseNumber.parseAllNumbers("0x123.456P-10d")); + assertNumber(0x123.456P+009, ParseNumber.parseAllNumbers("0x123.456P+009")); + } + + @Test + public void testParseHexNegativeFloat() { + assertNumber(-0xfedcba.98765432P-10f, // + ParseNumber.parseAllNumbers("-0xfedcba.98765432P-10f")); + assertNumber(-0x1.fffffffffffffP+1023, // + ParseNumber.parseAllNumbers("-0x1.fffffffffffffP+1023")); + assertNumber(-0xff.fP0F, ParseNumber.parseAllNumbers("-0xff.fP0F")); + assertNumber(-0x1P+1023, ParseNumber.parseAllNumbers("-0x1P+1023")); + assertNumber(-0xfP102, ParseNumber.parseAllNumbers("-0xfP102")); + assertNumber(-0xfP-102, ParseNumber.parseAllNumbers("-0xfP-102")); + assertNumber(-0xffP-102, ParseNumber.parseAllNumbers("-0xffP-102")); + assertNumber(-0x123.456P1, ParseNumber.parseAllNumbers("-0x123.456P1")); + assertNumber(-0x123.456P0f, ParseNumber.parseAllNumbers("-0x123.456P0f")); + assertNumber(-0x123.456P0d, ParseNumber.parseAllNumbers("-0x123.456P0d")); + assertNumber(-0x123.456P-10d, ParseNumber.parseAllNumbers("-0x123.456P-10d")); + assertNumber(-0x123.456P+009, ParseNumber.parseAllNumbers("-0x123.456P+009")); + } + + @Test + public void testParseBinary() { + // NB: "0b..." syntax is only supported starting with Java 7. + assertNumber(33, ParseNumber.parseAllNumbers("0b100001")); + // Test explicit long. + assertNumber(33L, ParseNumber.parseAllNumbers("0b100001L")); + // Test implicit long. + assertNumber(194588677707L, ParseNumber.parseAllNumbers( + "0b10110101001110011000111001011001001011")); + // Test BigInteger. + final String big = + "10110011100011110000111110000011111100000011111110000000" + + "111111110000000011111111100000000011111111110000000000"; + final Number bigNum = ParseNumber.parseAllNumbers("0b" + big); + assertNumber(new BigInteger(big, 2), bigNum); + } + + @Test + public void testParseBinaryNegative() { + // NB: "0b..." syntax is only supported starting with Java 7. + assertNumber(-33, ParseNumber.parseAllNumbers("-0b100001")); + // Test explicit long. + assertNumber(-33L, ParseNumber.parseAllNumbers("-0b100001L")); + // Test implicit long. + assertNumber(-194588677707L, ParseNumber.parseAllNumbers( + "-0b10110101001110011000111001011001001011")); + // Test BigInteger. + final String big = + "10110011100011110000111110000011111100000011111110000000" + + "111111110000000011111111100000000011111111110000000000"; + final Number bigNum = ParseNumber.parseAllNumbers("-0b" + big); + assertNumber(new BigInteger("-" + big, 2), bigNum); + } + + @Test + public void testParseOctal() { + assertNumber(01234567, ParseNumber.parseAllNumbers("01234567")); + // Test explicit long. + assertNumber(01234567L, ParseNumber.parseAllNumbers("01234567L")); + // Test implicit long. + assertNumber(012345677654321L, ParseNumber.parseAllNumbers("012345677654321")); + // Test BigInteger. + final String big = "1234567765432112345677654321"; + final Number bigNum = ParseNumber.parseAllNumbers("0" + big); + assertNumber(new BigInteger(big, 8), bigNum); + } + + @Test + public void testParseOctalNegative() { + assertNumber(-01234567, ParseNumber.parseAllNumbers("-01234567")); + // Test explicit long. + assertNumber(-01234567L, ParseNumber.parseAllNumbers("-01234567L")); + // Test implicit long. + assertNumber(-012345677654321L, ParseNumber.parseAllNumbers("-012345677654321")); + // Test BigInteger. + final String big = "1234567765432112345677654321"; + final Number bigNum = ParseNumber.parseAllNumbers("-0" + big); + assertNumber(new BigInteger("-" + big, 8), bigNum); + } + + @Test + public void testParseDecimal() { + assertNumber(1, ParseNumber.parseAllNumbers("1+2")); + + assertNumber(123456789, ParseNumber.parseAllNumbers("123456789")); + // Test explicit long. + assertNumber(123456789L, ParseNumber.parseAllNumbers("123456789L")); + // Test implicit long. + assertNumber(123456787654321L, ParseNumber.parseAllNumbers("123456787654321")); + // Test BigInteger. + final String bigI = "1234567898765432123456789"; + final Number bigInt = ParseNumber.parseAllNumbers(bigI); + assertNumber(new BigInteger(bigI), bigInt); + // Test explicit float. + assertNumber(1f, ParseNumber.parseAllNumbers("1f")); + // Test explicit double. + assertNumber(1d, ParseNumber.parseAllNumbers("1d")); + // Test implicit double. + assertNumber(1.0, ParseNumber.parseAllNumbers("1.0")); + assertNumber(1., ParseNumber.parseAllNumbers("1.")); + // Test scientific notation. + assertNumber(1e2, ParseNumber.parseAllNumbers("1e2")); + assertNumber(1.2e3, ParseNumber.parseAllNumbers("1.2e3")); + assertNumber(4.5e-6, ParseNumber.parseAllNumbers("4.5e-6")); + assertNumber(1.2e3f, ParseNumber.parseAllNumbers("1.2e3f")); + assertNumber(4.5e-6f, ParseNumber.parseAllNumbers("4.5e-6f")); + } + + @Test + public void testParseDecimalNegative() { + assertNumber(-123456789, ParseNumber.parseAllNumbers("-123456789")); + // Test explicit long. + assertNumber(-123456789L, ParseNumber.parseAllNumbers("-123456789L")); + // Test implicit long. + assertNumber(-123456787654321L, ParseNumber.parseAllNumbers("-123456787654321")); + // Test BigInteger. + final String bigI = "-1234567898765432123456789"; + final Number bigInt = ParseNumber.parseAllNumbers(bigI); + assertNumber(new BigInteger(bigI), bigInt); + // Test explicit float. + assertNumber(-1f, ParseNumber.parseAllNumbers("-1f")); + // Test explicit double. + assertNumber(-1d, ParseNumber.parseAllNumbers("-1d")); + // Test implicit double. + assertNumber(-1.0, ParseNumber.parseAllNumbers("-1.0")); + assertNumber(-1., ParseNumber.parseAllNumbers("-1.")); + // Test scientific notation. + assertNumber(-1e2, ParseNumber.parseAllNumbers("-1e2")); + assertNumber(-1.2e3, ParseNumber.parseAllNumbers("-1.2e3")); + assertNumber(-4.5e-6, ParseNumber.parseAllNumbers("-4.5e-6")); + assertNumber(-1.2e3f, ParseNumber.parseAllNumbers("-1.2e3f")); + assertNumber(-4.5e-6f, ParseNumber.parseAllNumbers("-4.5e-6f")); + } + + +} From da54509d1b49212ff796a39da19142e2fa41fd50 Mon Sep 17 00:00:00 2001 From: Jared Davis Date: Sat, 25 Jul 2026 23:15:45 -0400 Subject: [PATCH 03/12] Adjust return logic in isWord --- src/main/java/org/scijava/parsington/Literals.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/scijava/parsington/Literals.java b/src/main/java/org/scijava/parsington/Literals.java index 674fe89..9282616 100644 --- a/src/main/java/org/scijava/parsington/Literals.java +++ b/src/main/java/org/scijava/parsington/Literals.java @@ -251,8 +251,7 @@ private static boolean isWord(final CharSequence s, final Position pos, if (next >= 'a' && next <= 'z') return false; if (next >= 'A' && next <= 'Z') return false; if (next >= '0' && next <= '9') return false; - if (next == '_') return false; - return true; - } + return next != '_'; + } } From ec50921adc6dd92f77afd341497e993e1b4d9bbf Mon Sep 17 00:00:00 2001 From: Jared Davis Date: Sun, 26 Jul 2026 10:40:06 -0400 Subject: [PATCH 04/12] Change Collections.sort -> list.sort --- src/main/java/org/scijava/parsington/ExpressionParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/scijava/parsington/ExpressionParser.java b/src/main/java/org/scijava/parsington/ExpressionParser.java index eeea692..d6d7774 100644 --- a/src/main/java/org/scijava/parsington/ExpressionParser.java +++ b/src/main/java/org/scijava/parsington/ExpressionParser.java @@ -151,7 +151,7 @@ public ExpressionParser(final Collection operators, // NB: Ensure operators with longer symbols come first. // This prevents e.g. '-' from being matched before '-=' and '--'. - Collections.sort(operatorsList, (o1, o2) -> { + operatorsList.sort((o1, o2) -> { final String t1 = o1.getToken(); final String t2 = o2.getToken(); final int len1 = t1.length(); From 0f7c76512b7cd7f24e6310b6a533d76f1de99cac Mon Sep 17 00:00:00 2001 From: Jared Davis Date: Sun, 26 Jul 2026 11:22:11 -0400 Subject: [PATCH 05/12] Adjusted text cases. Altered handling of integer part before decimal point to match java numeric parsing. --- .../org/scijava/parsington/ParseNumber.java | 19 +- .../org/scijava/parsington/LiteralsTest.java | 176 ------------------ .../scijava/parsington/ParseNumberTest.java | 23 +++ 3 files changed, 36 insertions(+), 182 deletions(-) diff --git a/src/main/java/org/scijava/parsington/ParseNumber.java b/src/main/java/org/scijava/parsington/ParseNumber.java index fec577e..600ecac 100644 --- a/src/main/java/org/scijava/parsington/ParseNumber.java +++ b/src/main/java/org/scijava/parsington/ParseNumber.java @@ -127,7 +127,7 @@ private static ParseNumberResults identifyNumber(String s, int startingPosition) * @return true if a potential decimal literal is found, false otherwise * * - * Based on regular expression (([-+]?[0-9]+(\.[0-9]*)?([Ee][-+]?[0-9]+)?)([DdFfLl])?).* + * Based on regular expression (but not exact) (([-+]?[0-9]+(\.[0-9]*)?([Ee][-+]?[0-9]+)?)([DdFfLl])?).* * group 1 = entire matching string. NB: re has .* postfix but this method does not include .* in this group. * group 2 is from start of string including optional groups 3 and 4 * group 3 is \. then [0-9]* @@ -135,7 +135,7 @@ private static ParseNumberResults identifyNumber(String s, int startingPosition) * group 5 is [DdFfLl]? * * group 6 is not in re. [-+]? at start of string - * group 7 is not in re. [0-9]+ after sign, before group 3 + * group 7 is not in re. Digits after sign, before group 3. re has [0-9]+ but changed to [0-9]* to match java numeric parsing * group 8 is not in re. Leads to group 3 or 4 or 5 or end * * Octal encoding detection @@ -169,11 +169,15 @@ private static boolean extractDecimalNumber(String s, int start, int end, ParseN if (isDecimalDigit(c)) { start++; } else { + // removed this to match java - it is now a little slower + /* + // this does not follow java - eg parsing +.2 is valid in java. if (results.beginGroup[group] == start) { // need at least 1 digit results.numberType = NumberType.NOT_A_NUMBER; break bigWhile; } + */ results.endGroup[group] = start; group = 8; } @@ -235,8 +239,11 @@ private static boolean extractDecimalNumber(String s, int start, int end, ParseN } // bigWhile results.endGroup[1] = start; - if (results.endGroup[7] - results.beginGroup[7] == 0) - results.numberType = NumberType.NOT_A_NUMBER; // need at least 1 digit in number before decimal + int sevenLength = results.endGroup[7] - results.beginGroup[7]; + int threeLength = results.endGroup[3] - results.beginGroup[3]; + + if ((sevenLength == 0) && (threeLength < 2)) + results.numberType = NumberType.NOT_A_NUMBER; // need at least 1 digit in number after decimal if (results.beginGroup[4] != -1) { // we got an E int minLength = (haveSign) ? 3 : 2; @@ -245,7 +252,7 @@ private static boolean extractDecimalNumber(String s, int start, int end, ParseN } if (results.numberType == null) { - if ((results.beginGroup[4] != -1) || (results.endGroup[3] != -1)) { + if ((results.beginGroup[4] != -1) || (threeLength > 0)) { results.numberType = NumberType.DOUBLE; } else { results.numberType = NumberType.INTEGER; @@ -264,7 +271,7 @@ private static boolean extractDecimalNumber(String s, int start, int end, ParseN // octal encoding ? if (results.numberType == NumberType.INTEGER) { if (s.charAt(results.beginGroup[7]) == '0') { - boolean octal = (results.endGroup[7] - results.beginGroup[7] > 1); + boolean octal = (sevenLength > 1); if (octal) { for (int i = results.beginGroup[7] + 1; i < results.endGroup[7]; i++) { if (!isOctalDigit(s.charAt(i))) { diff --git a/src/test/java/org/scijava/parsington/LiteralsTest.java b/src/test/java/org/scijava/parsington/LiteralsTest.java index 58d6a19..bea1897 100644 --- a/src/test/java/org/scijava/parsington/LiteralsTest.java +++ b/src/test/java/org/scijava/parsington/LiteralsTest.java @@ -34,8 +34,6 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.fail; -import java.math.BigInteger; - import org.junit.jupiter.api.Test; /** @@ -107,180 +105,6 @@ public void testParseStringInvalid() { } } - @Test - public void testParseHexInteger() { - assertNumber(0x123, ParseNumber.parseAllNumbers("0x123")); - // Test explicit long. - assertNumber(0x123L, ParseNumber.parseAllNumbers("0x123L")); - // Test implicit long. - assertNumber(0x123456789abcdefL, ParseNumber.parseAllNumbers("0x123456789abcdef")); - // Test BigInteger. - final String big = "123456789abcdeffedcba987654321"; - final Number bigNum = ParseNumber.parseAllNumbers("0x" + big); - assertNumber(new BigInteger(big, 16), bigNum); - } - - @Test - public void testParseHexNegativeInteger() { - assertNumber(-0x123, ParseNumber.parseAllNumbers("-0x123")); - // Test explicit long. - assertNumber(-0x123L, ParseNumber.parseAllNumbers("-0x123L")); - // Test implicit long. - assertNumber(-0x123456789abcdefL, ParseNumber.parseAllNumbers("-0x123456789abcdef")); - // Test BigInteger. - final String big = "123456789abcdeffedcba987654321"; - final Number bigNum = ParseNumber.parseAllNumbers("-0x" + big); - assertNumber(new BigInteger("-" + big, 16), bigNum); - } - - @Test - public void testParseHexFloat() { - assertNumber(0xfedcba.98765432P-10f, // - ParseNumber.parseAllNumbers("0xfedcba.98765432P-10f")); - assertNumber(0x1.fffffffffffffP+1023, // - ParseNumber.parseAllNumbers("0x1.fffffffffffffP+1023")); - assertNumber(0xff.fP0F, ParseNumber.parseAllNumbers("0xff.fP0F")); - assertNumber(0x1P+1023, ParseNumber.parseAllNumbers("0x1P+1023")); - assertNumber(0xfP102, ParseNumber.parseAllNumbers("0xfP102")); - assertNumber(0xfP-102, ParseNumber.parseAllNumbers("0xfP-102")); - assertNumber(0xffP-102, ParseNumber.parseAllNumbers("0xffP-102")); - assertNumber(0x123.456P1, ParseNumber.parseAllNumbers("0x123.456P1")); - assertNumber(0x123.456P0f, ParseNumber.parseAllNumbers("0x123.456P0f")); - assertNumber(0x123.456P0d, ParseNumber.parseAllNumbers("0x123.456P0d")); - assertNumber(0x123.456P-10d, ParseNumber.parseAllNumbers("0x123.456P-10d")); - assertNumber(0x123.456P+009, ParseNumber.parseAllNumbers("0x123.456P+009")); - } - - @Test - public void testParseHexNegativeFloat() { - assertNumber(-0xfedcba.98765432P-10f, // - ParseNumber.parseAllNumbers("-0xfedcba.98765432P-10f")); - assertNumber(-0x1.fffffffffffffP+1023, // - ParseNumber.parseAllNumbers("-0x1.fffffffffffffP+1023")); - assertNumber(-0xff.fP0F, ParseNumber.parseAllNumbers("-0xff.fP0F")); - assertNumber(-0x1P+1023, ParseNumber.parseAllNumbers("-0x1P+1023")); - assertNumber(-0xfP102, ParseNumber.parseAllNumbers("-0xfP102")); - assertNumber(-0xfP-102, ParseNumber.parseAllNumbers("-0xfP-102")); - assertNumber(-0xffP-102, ParseNumber.parseAllNumbers("-0xffP-102")); - assertNumber(-0x123.456P1, ParseNumber.parseAllNumbers("-0x123.456P1")); - assertNumber(-0x123.456P0f, ParseNumber.parseAllNumbers("-0x123.456P0f")); - assertNumber(-0x123.456P0d, ParseNumber.parseAllNumbers("-0x123.456P0d")); - assertNumber(-0x123.456P-10d, ParseNumber.parseAllNumbers("-0x123.456P-10d")); - assertNumber(-0x123.456P+009, ParseNumber.parseAllNumbers("-0x123.456P+009")); - } - - @Test - public void testParseBinary() { - // NB: "0b..." syntax is only supported starting with Java 7. - assertNumber(33, ParseNumber.parseAllNumbers("0b100001")); - // Test explicit long. - assertNumber(33L, ParseNumber.parseAllNumbers("0b100001L")); - // Test implicit long. - assertNumber(194588677707L, ParseNumber.parseAllNumbers( - "0b10110101001110011000111001011001001011")); - // Test BigInteger. - final String big = - "10110011100011110000111110000011111100000011111110000000" - + "111111110000000011111111100000000011111111110000000000"; - final Number bigNum = ParseNumber.parseAllNumbers("0b" + big); - assertNumber(new BigInteger(big, 2), bigNum); - } - - @Test - public void testParseBinaryNegative() { - // NB: "0b..." syntax is only supported starting with Java 7. - assertNumber(-33, ParseNumber.parseAllNumbers("-0b100001")); - // Test explicit long. - assertNumber(-33L, ParseNumber.parseAllNumbers("-0b100001L")); - // Test implicit long. - assertNumber(-194588677707L, ParseNumber.parseAllNumbers( - "-0b10110101001110011000111001011001001011")); - // Test BigInteger. - final String big = - "10110011100011110000111110000011111100000011111110000000" - + "111111110000000011111111100000000011111111110000000000"; - final Number bigNum = ParseNumber.parseAllNumbers("-0b" + big); - assertNumber(new BigInteger("-" + big, 2), bigNum); - } - - @Test - public void testParseOctal() { - assertNumber(01234567, ParseNumber.parseAllNumbers("01234567")); - // Test explicit long. - assertNumber(01234567L, ParseNumber.parseAllNumbers("01234567L")); - // Test implicit long. - assertNumber(012345677654321L, ParseNumber.parseAllNumbers("012345677654321")); - // Test BigInteger. - final String big = "1234567765432112345677654321"; - final Number bigNum = ParseNumber.parseAllNumbers("0" + big); - assertNumber(new BigInteger(big, 8), bigNum); - } - - @Test - public void testParseOctalNegative() { - assertNumber(-01234567, ParseNumber.parseAllNumbers("-01234567")); - // Test explicit long. - assertNumber(-01234567L, ParseNumber.parseAllNumbers("-01234567L")); - // Test implicit long. - assertNumber(-012345677654321L, ParseNumber.parseAllNumbers("-012345677654321")); - // Test BigInteger. - final String big = "1234567765432112345677654321"; - final Number bigNum = ParseNumber.parseAllNumbers("-0" + big); - assertNumber(new BigInteger("-" + big, 8), bigNum); - } - - @Test - public void testParseDecimal() { - assertNumber(123456789, ParseNumber.parseAllNumbers("123456789")); - // Test explicit long. - assertNumber(123456789L, ParseNumber.parseAllNumbers("123456789L")); - // Test implicit long. - assertNumber(123456787654321L, ParseNumber.parseAllNumbers("123456787654321")); - // Test BigInteger. - final String bigI = "1234567898765432123456789"; - final Number bigInt = ParseNumber.parseAllNumbers(bigI); - assertNumber(new BigInteger(bigI), bigInt); - // Test explicit float. - assertNumber(1f, ParseNumber.parseAllNumbers("1f")); - // Test explicit double. - assertNumber(1d, ParseNumber.parseAllNumbers("1d")); - // Test implicit double. - assertNumber(1.0, ParseNumber.parseAllNumbers("1.0")); - assertNumber(1., ParseNumber.parseAllNumbers("1.")); - // Test scientific notation. - assertNumber(1e2, ParseNumber.parseAllNumbers("1e2")); - assertNumber(1.2e3, ParseNumber.parseAllNumbers("1.2e3")); - assertNumber(4.5e-6, ParseNumber.parseAllNumbers("4.5e-6")); - assertNumber(1.2e3f, ParseNumber.parseAllNumbers("1.2e3f")); - assertNumber(4.5e-6f, ParseNumber.parseAllNumbers("4.5e-6f")); - } - - @Test - public void testParseDecimalNegative() { - assertNumber(-123456789, ParseNumber.parseAllNumbers("-123456789")); - // Test explicit long. - assertNumber(-123456789L, ParseNumber.parseAllNumbers("-123456789L")); - // Test implicit long. - assertNumber(-123456787654321L, ParseNumber.parseAllNumbers("-123456787654321")); - // Test BigInteger. - final String bigI = "-1234567898765432123456789"; - final Number bigInt = ParseNumber.parseAllNumbers(bigI); - assertNumber(new BigInteger(bigI), bigInt); - // Test explicit float. - assertNumber(-1f, ParseNumber.parseAllNumbers("-1f")); - // Test explicit double. - assertNumber(-1d, ParseNumber.parseAllNumbers("-1d")); - // Test implicit double. - assertNumber(-1.0, ParseNumber.parseAllNumbers("-1.0")); - assertNumber(-1., ParseNumber.parseAllNumbers("-1.")); - // Test scientific notation. - assertNumber(-1e2, ParseNumber.parseAllNumbers("-1e2")); - assertNumber(-1.2e3, ParseNumber.parseAllNumbers("-1.2e3")); - assertNumber(-4.5e-6, ParseNumber.parseAllNumbers("-4.5e-6")); - assertNumber(-1.2e3f, ParseNumber.parseAllNumbers("-1.2e3f")); - assertNumber(-4.5e-6f, ParseNumber.parseAllNumbers("-4.5e-6f")); - } - @Test public void testParseNumber() { final Position pos = new Position(); diff --git a/src/test/java/org/scijava/parsington/ParseNumberTest.java b/src/test/java/org/scijava/parsington/ParseNumberTest.java index 7c5726c..97d7dae 100644 --- a/src/test/java/org/scijava/parsington/ParseNumberTest.java +++ b/src/test/java/org/scijava/parsington/ParseNumberTest.java @@ -33,6 +33,8 @@ import java.math.BigInteger; +import static org.junit.jupiter.api.Assertions.assertNull; + /** * Tests {@link Literals}. @@ -167,6 +169,27 @@ public void testParseOctalNegative() { @Test public void testParseDecimal() { assertNumber(1, ParseNumber.parseAllNumbers("1+2")); + assertNumber(01.01, ParseNumber.parseAllNumbers("01.01")); + assertNull(ParseNumber.parseAllNumbers("x01.01 yada")); // x + assertNull(ParseNumber.parseAllNumbers("01.01E yada")); // needs digit after E + assertNull(ParseNumber.parseAllNumbers("01.01EE yada")); // double E + assertNull(ParseNumber.parseAllNumbers("01.01E1.2 yada")); // decimal after E + assertNumber(01.01E1, ParseNumber.parseAllNumbers("01.01E1 ")); + assertNumber(01.01E+1, ParseNumber.parseAllNumbers("01.01E+1 ")); + assertNumber(01.01E-1, ParseNumber.parseAllNumbers("01.01E-1 ")); + assertNull(ParseNumber.parseAllNumbers("01.01E++1l ")); + assertNull(ParseNumber.parseAllNumbers("01.01E--1l ")); + assertNumber(01.01E-13, ParseNumber.parseAllNumbers("01.01E-13 ")); + assertNumber(01.01E-13f, ParseNumber.parseAllNumbers("01.01E-13f ")); + assertNumber(01.01E-13d, ParseNumber.parseAllNumbers("01.01E-13d ")); + assertNull(ParseNumber.parseAllNumbers("01.01E-13l ")); // will not fit in long + assertNull(ParseNumber.parseAllNumbers("+ ya")); + assertNull(ParseNumber.parseAllNumbers("+. ya")); + // This seems to be valid in java but does not match re since a digit is required before the decimal. + // assertNull(ParseNumber.parseAllNumbers("+.2 ya")); + assertNumber(+.2, ParseNumber.parseAllNumbers("+.2 ya")); + + assertNumber(123456789, ParseNumber.parseAllNumbers("123456789")); // Test explicit long. From 6056db4b57509536d8af2658e17fc4ef08e37b88 Mon Sep 17 00:00:00 2001 From: Jared Davis Date: Sun, 26 Jul 2026 11:45:30 -0400 Subject: [PATCH 06/12] javadoc, text and return cleanup, ParseNumberResults cleanup --- .../org/scijava/parsington/ParseNumber.java | 148 +++++++++--------- .../parsington/ParseNumberResults.java | 31 +++- 2 files changed, 96 insertions(+), 83 deletions(-) diff --git a/src/main/java/org/scijava/parsington/ParseNumber.java b/src/main/java/org/scijava/parsington/ParseNumber.java index 600ecac..f587f75 100644 --- a/src/main/java/org/scijava/parsington/ParseNumber.java +++ b/src/main/java/org/scijava/parsington/ParseNumber.java @@ -7,14 +7,11 @@ /** * Utility methods for parsing numeric literals from strings. These methods largely * conform to the Java specification's ideas of what constitutes a numeric literal. - * * Based on a regular expression implementation. - * - * initial source from Google Gemini on July 23, 2026 query + * Initial source from Google Gemini on July 23, 2026 query * "java fast method to identify integers, doubles, hex and octal numbers in a string" * and existing code in Literals * - * * * @author Curtis Rueden, Jared Davis */ @@ -59,10 +56,10 @@ public static Number parseAllNumbers(String s) { public static Number parseAllNumbers(String s, Position pos) { ParseNumberResults result = ParseNumber.identifyNumber(s, pos.get()); ParseNumber.processNumber(s, result); - if (result.number != null) { + if (result.getNumber() != null) { pos.inc(result.getLength()); } - return result.number; + return result.getNumber(); } @@ -70,7 +67,7 @@ public static Number parseAllNumbers(String s, Position pos) { private static ParseNumberResults identifyNumber(String s, int startingPosition) { ParseNumberResults results = new ParseNumberResults(); - results.beginGroup[1] = startingPosition; + results.getBeginGroup()[1] = startingPosition; if (s == null || s.isEmpty()) { results.numberType = NumberType.NOT_A_NUMBER; return results; @@ -85,7 +82,7 @@ private static ParseNumberResults identifyNumber(String s, int startingPosition) // Handle optional leading sign char first = s.charAt(startingPosition); if (first == '-' || first == '+') { - results.ndxSign = startingPosition; + results.setSignIndex(startingPosition); start++; if (len == 1) { results.numberType = NumberType.NOT_A_NUMBER; @@ -127,18 +124,17 @@ private static ParseNumberResults identifyNumber(String s, int startingPosition) * @return true if a potential decimal literal is found, false otherwise * * - * Based on regular expression (but not exact) (([-+]?[0-9]+(\.[0-9]*)?([Ee][-+]?[0-9]+)?)([DdFfLl])?).* + *

Based on regular expression (but not exact) (([-+]?[0-9]+(\.[0-9]*)?([Ee][-+]?[0-9]+)?)([DdFfLl])?).* * group 1 = entire matching string. NB: re has .* postfix but this method does not include .* in this group. * group 2 is from start of string including optional groups 3 and 4 * group 3 is \. then [0-9]* * group 4 is [Ee][-+]?[0-9]+ * group 5 is [DdFfLl]? - * * group 6 is not in re. [-+]? at start of string * group 7 is not in re. Digits after sign, before group 3. re has [0-9]+ but changed to [0-9]* to match java numeric parsing * group 8 is not in re. Leads to group 3 or 4 or 5 or end * - * Octal encoding detection + *

Octal encoding detection * If we have (no group 3) and (no group 4) then do a scan for an octal value in group 2 * Octal IFF * group 7 starts with 0; does not contain 8 or 9; is longer than 1 @@ -155,12 +151,12 @@ private static boolean extractDecimalNumber(String s, int start, int end, ParseN boolean haveFour = false; int group = 7; - results.beginGroup[group] = start; + results.getBeginGroup()[group] = start; bigWhile: while (start <= end) { if (start == end) { - results.endGroup[group] = start; + results.getEndGroup()[group] = start; break; } c = s.charAt(start); @@ -171,14 +167,14 @@ private static boolean extractDecimalNumber(String s, int start, int end, ParseN } else { // removed this to match java - it is now a little slower /* - // this does not follow java - eg parsing +.2 is valid in java. + // this does not follow java - e.g. parsing +.2 is valid in java. if (results.beginGroup[group] == start) { // need at least 1 digit results.numberType = NumberType.NOT_A_NUMBER; break bigWhile; } */ - results.endGroup[group] = start; + results.getEndGroup()[group] = start; group = 8; } break; @@ -191,7 +187,7 @@ private static boolean extractDecimalNumber(String s, int start, int end, ParseN } havedot = true; group = 3; - results.beginGroup[group] = start++; + results.getBeginGroup()[group] = start++; break; } if (isE(c)) { @@ -201,13 +197,13 @@ private static boolean extractDecimalNumber(String s, int start, int end, ParseN } haveFour = true; group = 4; - results.beginGroup[group] = start++; + results.getBeginGroup()[group] = start++; break; } if (isADoubleOrFloatSuffix(c) || isL(c)) { group = 5; - results.beginGroup[group] = start++; - results.endGroup[group] = start; + results.getBeginGroup()[group] = start++; + results.getEndGroup()[group] = start; results.numberType = isL(c) ? NumberType.INTEGER: NumberType.DOUBLE; // break bigWhile fall through below } @@ -216,7 +212,7 @@ private static boolean extractDecimalNumber(String s, int start, int end, ParseN if (isDecimalDigit(c)) { start++; } else { - results.endGroup[group] = start; + results.getEndGroup()[group] = start; group = 8; } break; @@ -231,28 +227,28 @@ private static boolean extractDecimalNumber(String s, int start, int end, ParseN } else if (isDecimalDigit(c)) { start++; } else { - results.endGroup[group] = start; + results.getEndGroup()[group] = start; group = 8; // back to 8 for group 5 } break; } // switch group } // bigWhile - results.endGroup[1] = start; + results.getEndGroup()[1] = start; - int sevenLength = results.endGroup[7] - results.beginGroup[7]; - int threeLength = results.endGroup[3] - results.beginGroup[3]; + int sevenLength = results.getEndGroup()[7] - results.getBeginGroup()[7]; + int threeLength = results.getEndGroup()[3] - results.getBeginGroup()[3]; if ((sevenLength == 0) && (threeLength < 2)) results.numberType = NumberType.NOT_A_NUMBER; // need at least 1 digit in number after decimal - if (results.beginGroup[4] != -1) { // we got an E + if (results.getBeginGroup()[4] != -1) { // we got an E int minLength = (haveSign) ? 3 : 2; - if (results.endGroup[4] - results.beginGroup[4] < minLength) + if (results.getEndGroup()[4] - results.getBeginGroup()[4] < minLength) results.numberType = NumberType.NOT_A_NUMBER; // need at least 1 digit in exponent } if (results.numberType == null) { - if ((results.beginGroup[4] != -1) || (threeLength > 0)) { + if ((results.getBeginGroup()[4] != -1) || (threeLength > 0)) { results.numberType = NumberType.DOUBLE; } else { results.numberType = NumberType.INTEGER; @@ -261,19 +257,19 @@ private static boolean extractDecimalNumber(String s, int start, int end, ParseN if (results.numberType != NumberType.NOT_A_NUMBER) { // group 2 length = group 1 length - group 5 length - results.beginGroup[2] = results.beginGroup[1]; - results.endGroup[2] = results.endGroup[1]; - if (results.beginGroup[5] != -1) { - results.endGroup[2]--; + results.getBeginGroup()[2] = results.getBeginGroup()[1]; + results.getEndGroup()[2] = results.getEndGroup()[1]; + if (results.getBeginGroup()[5] != -1) { + results.getEndGroup()[2]--; } } // octal encoding ? if (results.numberType == NumberType.INTEGER) { - if (s.charAt(results.beginGroup[7]) == '0') { + if (s.charAt(results.getBeginGroup()[7]) == '0') { boolean octal = (sevenLength > 1); if (octal) { - for (int i = results.beginGroup[7] + 1; i < results.endGroup[7]; i++) { + for (int i = results.getBeginGroup()[7] + 1; i < results.getEndGroup()[7]; i++) { if (!isOctalDigit(s.charAt(i))) { octal = false; break; @@ -281,8 +277,8 @@ private static boolean extractDecimalNumber(String s, int start, int end, ParseN } } if (octal) { - if (results.beginGroup[5] != -1) { - if (!isL(s.charAt(results.beginGroup[5]))) { + if (results.getBeginGroup()[5] != -1) { + if (!isL(s.charAt(results.getBeginGroup()[5]))) { octal = false; } } @@ -306,8 +302,8 @@ private static boolean extractDecimalNumber(String s, int start, int end, ParseN * @param end The index of the last char to parse. * @param results contains the parsing details calculated in this method. * @return true if a potential binary literal is found, false otherwise - - * based on Regular Expression (([-+]?)0[Bb]([01]+)([Ll]?)).* + * + *

based on Regular Expression (([-+]?)0[Bb]([01]+)([Ll]?)).* * group 1 = entire matching string. NB: re has .* postfix but this method does not include .* in this group. * group 2 [-+]? This is processed by the caller and placed into ParseNumberResults ndxSign. * group none 0[Bb] must exist. This is processed by the caller. @@ -320,12 +316,12 @@ private static boolean extractBinaryNumber(String s, int start, int end, ParseNu results.setSignGroup(2); int group = 3; - results.beginGroup[group] = start; + results.getBeginGroup()[group] = start; bigWhile: while (start <= end) { if (start == end) { - results.endGroup[group] = start; + results.getEndGroup()[group] = start; break; } c = s.charAt(start); @@ -334,39 +330,39 @@ private static boolean extractBinaryNumber(String s, int start, int end, ParseNu if (isBinaryDigit(c)) { start++; } else { - results.endGroup[group] = start; + results.getEndGroup()[group] = start; group = 4; } break; case 4: // [Ll]? if (isL(c)) { - results.beginGroup[group] = start; - results.endGroup[group] = ++start; + results.getBeginGroup()[group] = start; + results.getEndGroup()[group] = ++start; } // ok - we are done break bigWhile; } // switch group } // while bigWhile - results.endGroup[1] = start; + results.getEndGroup()[1] = start; // need at least 1 digit in group 3 - if (results.endGroup[3] - results.beginGroup[3] == 0) + if (results.getEndGroup()[3] - results.getBeginGroup()[3] == 0) results.numberType = NumberType.NOT_A_NUMBER; return (results.numberType == NumberType.BINARY); } /** - * Parses a hexidecimal literal. Both hexadecimal integer (e.g., - * {@code 0xfedcba9876543210}) and hexidecimal floating point (e.g., + * Parses a hexadecimal literal. Both hexadecimal integer (e.g., + * {@code 0xfedcba9876543210}) and hexadecimal floating point (e.g., * {@code 0xfedcba.98765432p10f}) are supported. * * @param s The string from which the numeric literal should be parsed. * @param start The index of the string to start the parsing after the sign and 0[Xx] prefix. * @param end The index of the last char to parse. * @param results contains the parsing details calculated in this method. - * @return true if a potential hexidecimal literal is found, false otherwise + * @return true if a potential hexadecimal literal is found, false otherwise * * - * based on Regular Expression (([-+]?)0[Xx]([0-9a-fA-F]+)([Ll]|(\\.[0-9a-fA-F]*)?[Pp]([-+]?)([0-9]+)([DdFf]|)|)).* + *

based on Regular Expression (([-+]?)0[Xx]([0-9a-fA-F]+)([Ll]|(\\.[0-9a-fA-F]*)?[Pp]([-+]?)([0-9]+)([DdFf]|)|)).* * group 1 = entire matching string. NB: re has .* postfix but this method does not include .* in this group. * group 2 [-+]? This is processed by the caller and placed into ParseNumberResults ndxSign. * group none 0[Xx] This is processed by the caller. @@ -385,11 +381,11 @@ private static boolean extractHexNumber(String s, int start, int end, ParseNumbe results.setSignGroup(2); int group = 3; - results.beginGroup[3] = start; + results.getBeginGroup()[3] = start; bigWhile: while (start <= end) { if (start == end) { - results.endGroup[group] = start; + results.getEndGroup()[group] = start; break; } c = s.charAt(start); // start may not change when group changes @@ -398,19 +394,19 @@ private static boolean extractHexNumber(String s, int start, int end, ParseNumbe if (isHexDigit(c)) { start++; } else { - results.endGroup[group] = start; + results.getEndGroup()[group] = start; group = 4; - results.beginGroup[group] = start; + results.getBeginGroup()[group] = start; } break; case 4: // [Ll]? group end is adjusted after switch to include groups 5-8. if (isL(c)) { - results.endGroup[group] = ++start; + results.getEndGroup()[group] = ++start; break bigWhile; // end of re. No need to continue parsing chars } if (c == '.') { group = 5; - results.beginGroup[group] = start++; + results.getBeginGroup()[group] = start++; break; } if (isP(c)) { @@ -419,59 +415,59 @@ private static boolean extractHexNumber(String s, int start, int end, ParseNumbe break; } // we are done, there is no group 4 - results.beginGroup[group] = -1; + results.getBeginGroup()[group] = -1; break bigWhile; case 5: // \.[0-9a-fA-F]* 0..n if (isHexDigit(c)) { start++; } else { - results.endGroup[group] = start; + results.getEndGroup()[group] = start; if (isP(c)) { group = 9; - results.beginGroup[group] = start; - results.endGroup[group] = ++start; + results.getBeginGroup()[group] = start; + results.getEndGroup()[group] = ++start; } } break; case 9: // start -1 == p or P if (isASign(c)) { group = 6; - results.beginGroup[group] = start; - results.endGroup[group] = ++start; + results.getBeginGroup()[group] = start; + results.getEndGroup()[group] = ++start; } group = 7; break; case 7: // [0-9]+ enforce at least 1 char in this group if (isDecimalDigit(c)) { - if (results.beginGroup[group] == -1) results.beginGroup[group] = start; + if (results.getBeginGroup()[group] == -1) results.getBeginGroup()[group] = start; start++; } else { - if (results.beginGroup[group] == -1) { + if (results.getBeginGroup()[group] == -1) { // need at least 1 digit to be valid results.numberType = NumberType.NOT_A_NUMBER; break bigWhile; } else { - results.endGroup[group] = start; + results.getEndGroup()[group] = start; group = 8; } } break; case 8: // [DdFf]? if (isADoubleOrFloatSuffix(c)) { - results.beginGroup[8] = start; - results.endGroup[8] = ++start; + results.getBeginGroup()[8] = start; + results.getEndGroup()[8] = ++start; } break bigWhile; } // switch group } // while bigWhile - results.endGroup[1] = start; - results.endGroup[4] = Math.max(results.endGroup[4], Math.max(Math.max(results.endGroup[5], results.endGroup[6]), Math.max(results.endGroup[7], results.endGroup[8]))); + results.getEndGroup()[1] = start; + results.getEndGroup()[4] = Math.max(results.getEndGroup()[4], Math.max(Math.max(results.getEndGroup()[5], results.getEndGroup()[6]), Math.max(results.getEndGroup()[7], results.getEndGroup()[8]))); // P (in group 9) requires at least one entry in group 7 - if ((results.beginGroup[9] != -1) && (results.beginGroup[7] == -1)) + if ((results.getBeginGroup()[9] != -1) && (results.getBeginGroup()[7] == -1)) results.numberType = NumberType.NOT_A_NUMBER; // group 3 must have at least 1 digit - if (results.endGroup[3] - results.beginGroup[3] == 0) + if (results.getEndGroup()[3] - results.getBeginGroup()[3] == 0) results.numberType = NumberType.NOT_A_NUMBER; return (results.numberType == NumberType.HEXADECIMAL); } @@ -482,16 +478,16 @@ private static void processNumber(String in, ParseNumberResults results) { switch (results.numberType) { case INTEGER: case DOUBLE: - results.number = processDecimal(in, results); + results.setNumber(processDecimal(in, results)); break; case HEXADECIMAL: - results.number = processHex(in, results); + results.setNumber(processHex(in, results)); break; case BINARY: - results.number = processBinary(in, results); + results.setNumber(processBinary(in, results)); break; case OCTAL: - results.number = processOctal(in, results); + results.setNumber(processOctal(in, results)); break; } } @@ -529,8 +525,7 @@ private static Number processBinary(String s, ParseNumberResults results) { final String number = sign + results.getGroup(s, 3); final boolean forceLong = !results.getGroup(s, 4).isEmpty(); - final Number result = parseIntegerToNumber(number, forceLong, 2); - return result; + return parseIntegerToNumber(number, forceLong, 2); } // NB: uses decimal regular expression group numbers @@ -538,8 +533,7 @@ private static Number processOctal(String s, ParseNumberResults results) { final String number = results.getGroup(s, 2); final String expSuffix = results.getGroup(s, 5); // Ll nothing final boolean forceLong = "L".equalsIgnoreCase(expSuffix); - final Number result = parseIntegerToNumber(number, forceLong, 8); - return result; + return parseIntegerToNumber(number, forceLong, 8); } private static Number processDecimal(String s, ParseNumberResults results) { diff --git a/src/main/java/org/scijava/parsington/ParseNumberResults.java b/src/main/java/org/scijava/parsington/ParseNumberResults.java index 8e0f17f..783216f 100644 --- a/src/main/java/org/scijava/parsington/ParseNumberResults.java +++ b/src/main/java/org/scijava/parsington/ParseNumberResults.java @@ -1,15 +1,20 @@ package org.scijava.parsington; public class ParseNumberResults { - int ndxSign = -1; + private int signNdx = -1; ParseNumber.NumberType numberType; - int[] beginGroup = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; - int[] endGroup = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; - Number number =null; + private final int[] beginGroup = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; + private final int[] endGroup = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; + private Number number = null; + + + void setSignIndex(int signNdx) { + this.signNdx = signNdx; + } void setSignGroup(int signGroup) { - if (ndxSign != -1) { - beginGroup[signGroup] = ndxSign; + if (signNdx != -1) { + beginGroup[signGroup] = signNdx; endGroup[signGroup] = beginGroup[signGroup] + 1; } } @@ -30,4 +35,18 @@ int getLength() { return 0; } + Number getNumber() { + return number; + } + void setNumber(Number number) { + this.number = number; + } + + public int[] getBeginGroup() { + return beginGroup; + } + + public int[] getEndGroup() { + return endGroup; + } } From 6f3faad4a7799b07fc3dc6dd579970f43f2f413d Mon Sep 17 00:00:00 2001 From: Jared Davis Date: Sun, 26 Jul 2026 13:43:55 -0400 Subject: [PATCH 07/12] Attempt to reject a non number faster --- src/main/java/org/scijava/parsington/ParseNumber.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/scijava/parsington/ParseNumber.java b/src/main/java/org/scijava/parsington/ParseNumber.java index f587f75..af74eaf 100644 --- a/src/main/java/org/scijava/parsington/ParseNumber.java +++ b/src/main/java/org/scijava/parsington/ParseNumber.java @@ -105,8 +105,12 @@ private static ParseNumberResults identifyNumber(String s, int startingPosition) return results; } - // try decimal, internally checks for octal - extractDecimalNumber(s, start, len, results); + if (start < len && (isDecimalDigit(s.charAt(start)) || s.charAt(start) == '.')) { + // try decimal, internally checks for octal + extractDecimalNumber(s, start, len, results); + } else { + results.numberType = NumberType.NOT_A_NUMBER; + } return results; } From 1dc247161ad742433d91993541e6eca5a2363c86 Mon Sep 17 00:00:00 2001 From: Jared Davis Date: Sun, 26 Jul 2026 15:27:20 -0400 Subject: [PATCH 08/12] Add test for flat sum --- .../parsington/ExpressionParserTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/test/java/org/scijava/parsington/ExpressionParserTest.java b/src/test/java/org/scijava/parsington/ExpressionParserTest.java index 25faec5..d3e3702 100644 --- a/src/test/java/org/scijava/parsington/ExpressionParserTest.java +++ b/src/test/java/org/scijava/parsington/ExpressionParserTest.java @@ -300,6 +300,29 @@ public void testLogicOperators() { assertSame(Operators.LOGICAL_OR, queue.pop()); } + @Test + public void testFlatSum() { + final ExpressionParser parser = new ExpressionParser(); + final LinkedList queue = parser.parsePostfix("a0+a1+a2+a3+a4+a5+a6+a7"); + assertNotNull(queue); + assertEquals(15, queue.size()); + assertVariable("a0", queue.pop()); + assertVariable("a1", queue.pop()); + assertSame(Operators.ADD, queue.pop()); + assertVariable("a2", queue.pop()); + assertSame(Operators.ADD, queue.pop()); + assertVariable("a3", queue.pop()); + assertSame(Operators.ADD, queue.pop()); + assertVariable("a4", queue.pop()); + assertSame(Operators.ADD, queue.pop()); + assertVariable("a5", queue.pop()); + assertSame(Operators.ADD, queue.pop()); + assertVariable("a6", queue.pop()); + assertSame(Operators.ADD, queue.pop()); + assertVariable("a7", queue.pop()); + assertSame(Operators.ADD, queue.pop()); + } + @Test public void testUnaryOperators1() { final ExpressionParser parser = new ExpressionParser(); From 704acf1ea9385f0317ee0c98f5759283c7074e4b Mon Sep 17 00:00:00 2001 From: Jared Davis Date: Sun, 26 Jul 2026 16:45:03 -0400 Subject: [PATCH 09/12] Fail faster on non numbers --- .../org/scijava/parsington/ParseNumber.java | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/scijava/parsington/ParseNumber.java b/src/main/java/org/scijava/parsington/ParseNumber.java index af74eaf..dd32153 100644 --- a/src/main/java/org/scijava/parsington/ParseNumber.java +++ b/src/main/java/org/scijava/parsington/ParseNumber.java @@ -55,6 +55,7 @@ public static Number parseAllNumbers(String s) { public static Number parseAllNumbers(String s, Position pos) { ParseNumberResults result = ParseNumber.identifyNumber(s, pos.get()); + if (result == null) return null; ParseNumber.processNumber(s, result); if (result.getNumber() != null) { pos.inc(result.getLength()); @@ -65,28 +66,24 @@ public static Number parseAllNumbers(String s, Position pos) { // identify + // return null if not a number private static ParseNumberResults identifyNumber(String s, int startingPosition) { + int len = s.length(); + if (startingPosition >= len) return null; + final char first = s.charAt(startingPosition); + // quick fail if first char not in this list += 0-9 + if ("+-0123456789".indexOf(first) == -1) return null; + ParseNumberResults results = new ParseNumberResults(); results.getBeginGroup()[1] = startingPosition; - if (s == null || s.isEmpty()) { - results.numberType = NumberType.NOT_A_NUMBER; - return results; - } - int len = s.length(); - if (startingPosition >= len) { - results.numberType = NumberType.NOT_A_NUMBER; - return results; - } int start = startingPosition; // Handle optional leading sign - char first = s.charAt(startingPosition); if (first == '-' || first == '+') { results.setSignIndex(startingPosition); start++; if (len == 1) { - results.numberType = NumberType.NOT_A_NUMBER; - return results; + return null; } } @@ -105,12 +102,8 @@ private static ParseNumberResults identifyNumber(String s, int startingPosition) return results; } - if (start < len && (isDecimalDigit(s.charAt(start)) || s.charAt(start) == '.')) { - // try decimal, internally checks for octal - extractDecimalNumber(s, start, len, results); - } else { - results.numberType = NumberType.NOT_A_NUMBER; - } + // try decimal, internally checks for octal + extractDecimalNumber(s, start, len, results); return results; } From 16bd3ce839dab91819ec2b8224640cf5bb1e189b Mon Sep 17 00:00:00 2001 From: Jared Davis Date: Sun, 26 Jul 2026 17:22:42 -0400 Subject: [PATCH 10/12] Fail faster on non numbers, take 2 --- .../org/scijava/parsington/ParseNumber.java | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/scijava/parsington/ParseNumber.java b/src/main/java/org/scijava/parsington/ParseNumber.java index dd32153..98dbe36 100644 --- a/src/main/java/org/scijava/parsington/ParseNumber.java +++ b/src/main/java/org/scijava/parsington/ParseNumber.java @@ -69,22 +69,27 @@ public static Number parseAllNumbers(String s, Position pos) { // return null if not a number private static ParseNumberResults identifyNumber(String s, int startingPosition) { int len = s.length(); - if (startingPosition >= len) return null; final char first = s.charAt(startingPosition); - // quick fail if first char not in this list += 0-9 - if ("+-0123456789".indexOf(first) == -1) return null; + boolean hasSign=false; + // quick fail if first char not in not 0-9 or a +- + if (first < '0' || first > '9') { + if (first == '-' || first == '+') { + hasSign = true; + if (len == 1) { + // fail on just a sign + return null; + } + } else { + return null; // first is not [-+0-9] + } + } ParseNumberResults results = new ParseNumberResults(); results.getBeginGroup()[1] = startingPosition; int start = startingPosition; - - // Handle optional leading sign - if (first == '-' || first == '+') { + if (hasSign) { results.setSignIndex(startingPosition); start++; - if (len == 1) { - return null; - } } // Check for Hexadecimal prefix (0x or 0X) From 55119068d7dd42ef8825b970ecd4e6a1e0de6f10 Mon Sep 17 00:00:00 2001 From: Jared Davis Date: Sun, 26 Jul 2026 18:03:33 -0400 Subject: [PATCH 11/12] Parse .2 correctly --- src/main/java/org/scijava/parsington/ParseNumber.java | 2 +- src/test/java/org/scijava/parsington/ParseNumberTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/scijava/parsington/ParseNumber.java b/src/main/java/org/scijava/parsington/ParseNumber.java index 98dbe36..dbbab25 100644 --- a/src/main/java/org/scijava/parsington/ParseNumber.java +++ b/src/main/java/org/scijava/parsington/ParseNumber.java @@ -79,7 +79,7 @@ private static ParseNumberResults identifyNumber(String s, int startingPosition) // fail on just a sign return null; } - } else { + } else if (first != '.' ){ return null; // first is not [-+0-9] } } diff --git a/src/test/java/org/scijava/parsington/ParseNumberTest.java b/src/test/java/org/scijava/parsington/ParseNumberTest.java index 97d7dae..ec163d7 100644 --- a/src/test/java/org/scijava/parsington/ParseNumberTest.java +++ b/src/test/java/org/scijava/parsington/ParseNumberTest.java @@ -188,7 +188,7 @@ public void testParseDecimal() { // This seems to be valid in java but does not match re since a digit is required before the decimal. // assertNull(ParseNumber.parseAllNumbers("+.2 ya")); assertNumber(+.2, ParseNumber.parseAllNumbers("+.2 ya")); - + assertNumber(.2, ParseNumber.parseAllNumbers(".2 ya")); assertNumber(123456789, ParseNumber.parseAllNumbers("123456789")); From 563b04be9b2fb364e94ee4f5bba077dbcac6fcea Mon Sep 17 00:00:00 2001 From: Jared Davis Date: Mon, 27 Jul 2026 13:13:14 -0400 Subject: [PATCH 12/12] Encapsulate ParseNumberResults. Execution is slightly slower. Fewer lines of code. --- .../org/scijava/parsington/ParseNumber.java | 259 ++++++++---------- .../parsington/ParseNumberResults.java | 130 +++++++-- 2 files changed, 222 insertions(+), 167 deletions(-) diff --git a/src/main/java/org/scijava/parsington/ParseNumber.java b/src/main/java/org/scijava/parsington/ParseNumber.java index dbbab25..41b379d 100644 --- a/src/main/java/org/scijava/parsington/ParseNumber.java +++ b/src/main/java/org/scijava/parsington/ParseNumber.java @@ -3,6 +3,8 @@ import java.math.BigDecimal; import java.math.BigInteger; +import static org.scijava.parsington.ParseNumberResults.NumberType.*; + /** * Utility methods for parsing numeric literals from strings. These methods largely @@ -71,7 +73,7 @@ private static ParseNumberResults identifyNumber(String s, int startingPosition) int len = s.length(); final char first = s.charAt(startingPosition); boolean hasSign=false; - // quick fail if first char not in not 0-9 or a +- + // quick fail if first char not in not 0-9 or a +- or a . if (first < '0' || first > '9') { if (first == '-' || first == '+') { hasSign = true; @@ -80,50 +82,46 @@ private static ParseNumberResults identifyNumber(String s, int startingPosition) return null; } } else if (first != '.' ){ - return null; // first is not [-+0-9] + return null; // first is not [-+0-9\.] } } ParseNumberResults results = new ParseNumberResults(); - results.getBeginGroup()[1] = startingPosition; + results.setBegin(startingPosition); int start = startingPosition; if (hasSign) { results.setSignIndex(startingPosition); start++; } - // Check for Hexadecimal prefix (0x or 0X) - if (start + 2 < len && s.charAt(start) == '0' && - (s.charAt(start + 1) == 'x' || s.charAt(start + 1) == 'X')) { - results.numberType = NumberType.HEXADECIMAL; - extractHexNumber(s, start + 2, len, results); - return results; - } - if (start + 2 < len && s.charAt(start) == '0' && - (s.charAt(start + 1) == 'b' || s.charAt(start + 1) == 'B')) { - results.numberType = NumberType.BINARY; - extractBinaryNumber(s, start + 2, len, results); - return results; + if (start + 2 < len && s.charAt(start) == '0') { + final char afterZero = s.charAt(start + 1); + if (afterZero == 'x' || afterZero == 'X') { + extractHexNumber(s, start + 2, len, results); + return results; + } + if (afterZero == 'b' || afterZero == 'B') { + extractBinaryNumber(s, start + 2, len, results); + return results; + } } - // try decimal, internally checks for octal - extractDecimalNumber(s, start, len, results); + extractDecimalOrOctalNumber(s, start, len, results); return results; } // extract /** - * Attempts to parse a decimal literal (integer or otherwise; e.g., {@code 1234567890}, - * {@code 1234.0987} or {@code 1.2e34} or {@code 01234}). Parsing supports base 10 and base 8. + * Attempts to parse a decimal or octal literal (integer or otherwise; e.g., {@code 1234567890}, + * {@code 1234.0987} or {@code 1.2e34} or {@code 01234}). * * * @param s The string from which the numeric literal should be parsed. - * @param start The index of the string to start the parsing after the sign and 0[Xx] prefix. + * @param start The index of the string to start the parsing after the optional sign. * @param end The index of the last char to parse. * @param results contains the parsing details calculated in this method. - * @return true if a potential decimal literal is found, false otherwise * * *

Based on regular expression (but not exact) (([-+]?[0-9]+(\.[0-9]*)?([Ee][-+]?[0-9]+)?)([DdFfLl])?).* @@ -143,7 +141,7 @@ private static ParseNumberResults identifyNumber(String s, int startingPosition) * group 5 == empty or Ll */ - private static boolean extractDecimalNumber(String s, int start, int end, ParseNumberResults results) { + private static void extractDecimalOrOctalNumber(String s, int start, int end, ParseNumberResults results) { char c; results.setSignGroup(6); @@ -152,31 +150,21 @@ private static boolean extractDecimalNumber(String s, int start, int end, ParseN boolean haveSign = false; boolean haveFour = false; - int group = 7; - results.getBeginGroup()[group] = start; + int group = results.setBeginGroup(7, start); bigWhile: while (start <= end) { if (start == end) { - results.getEndGroup()[group] = start; + results.setEndGroup(group, start); break; } c = s.charAt(start); switch (group) { - case 7: // [0-9]+ digits after sign and before decimal point + case 7: // [0-9]* digits after sign and before decimal point if (isDecimalDigit(c)) { start++; } else { - // removed this to match java - it is now a little slower - /* - // this does not follow java - e.g. parsing +.2 is valid in java. - if (results.beginGroup[group] == start) { - // need at least 1 digit - results.numberType = NumberType.NOT_A_NUMBER; - break bigWhile; - } - */ - results.getEndGroup()[group] = start; + results.setEndGroup(group, start); group = 8; } break; @@ -184,29 +172,25 @@ private static boolean extractDecimalNumber(String s, int start, int end, ParseN case 8: // transition to group 3, 4, 5 or end if (c == '.') { if (havedot) { // only one . allowed - results.numberType = NumberType.NOT_A_NUMBER; + results.setNumberType(NOT_A_NUMBER); break bigWhile; } havedot = true; - group = 3; - results.getBeginGroup()[group] = start++; + group = results.setBeginGroup(3, start++); break; } if (isE(c)) { if (haveFour) { // one group 4 allowed - results.numberType = NumberType.NOT_A_NUMBER; + results.setNumberType(NOT_A_NUMBER); break bigWhile; } haveFour = true; - group = 4; - results.getBeginGroup()[group] = start++; + group = results.setBeginGroup(4, start++); break; } if (isADoubleOrFloatSuffix(c) || isL(c)) { - group = 5; - results.getBeginGroup()[group] = start++; - results.getEndGroup()[group] = start; - results.numberType = isL(c) ? NumberType.INTEGER: NumberType.DOUBLE; + results.setGroup(5, start, ++start); + results.setNumberType(isL(c) ? INTEGER : DOUBLE); // break bigWhile fall through below } break bigWhile; @@ -214,14 +198,14 @@ private static boolean extractDecimalNumber(String s, int start, int end, ParseN if (isDecimalDigit(c)) { start++; } else { - results.getEndGroup()[group] = start; + results.setEndGroup(group, start); group = 8; } break; case 4: // just [-+]?[0-9]+ of group 4. [Ee] done in 8 if (isASign(c)) { if (haveSign) { // only one sign allowed - results.numberType = NumberType.NOT_A_NUMBER; + results.setNumberType(NOT_A_NUMBER); break bigWhile; } haveSign = true; @@ -229,69 +213,67 @@ private static boolean extractDecimalNumber(String s, int start, int end, ParseN } else if (isDecimalDigit(c)) { start++; } else { - results.getEndGroup()[group] = start; + results.setEndGroup(group, start); group = 8; // back to 8 for group 5 } break; } // switch group } // bigWhile - results.getEndGroup()[1] = start; + results.setEnd(start); - int sevenLength = results.getEndGroup()[7] - results.getBeginGroup()[7]; - int threeLength = results.getEndGroup()[3] - results.getBeginGroup()[3]; + int sevenLength = results.getGroupLength(7); + int threeLength = results.getGroupLength(3); - if ((sevenLength == 0) && (threeLength < 2)) - results.numberType = NumberType.NOT_A_NUMBER; // need at least 1 digit in number after decimal - - if (results.getBeginGroup()[4] != -1) { // we got an E - int minLength = (haveSign) ? 3 : 2; - if (results.getEndGroup()[4] - results.getBeginGroup()[4] < minLength) - results.numberType = NumberType.NOT_A_NUMBER; // need at least 1 digit in exponent - } + if ((sevenLength == 0) && (threeLength < 2)) { + results.setNumberType(NOT_A_NUMBER); // need at least 1 digit in number after decimal + } else { + if (results.hasGroupBegin(4)) { // we got an E + int minLength = (haveSign) ? 3 : 2; + if (results.getGroupLength(4) < minLength) + results.setNumberType(NOT_A_NUMBER); // need at least 1 digit in exponent + } - if (results.numberType == null) { - if ((results.getBeginGroup()[4] != -1) || (threeLength > 0)) { - results.numberType = NumberType.DOUBLE; - } else { - results.numberType = NumberType.INTEGER; + if (results.getNumberType() == null) { + if ((results.hasGroupBegin(4)) || (threeLength > 0)) { + results.setNumberType(DOUBLE); + } else { + results.setNumberType(INTEGER); + } } - } - if (results.numberType != NumberType.NOT_A_NUMBER) { - // group 2 length = group 1 length - group 5 length - results.getBeginGroup()[2] = results.getBeginGroup()[1]; - results.getEndGroup()[2] = results.getEndGroup()[1]; - if (results.getBeginGroup()[5] != -1) { - results.getEndGroup()[2]--; + if (results.getNumberType() != NOT_A_NUMBER) { + // group 2 length = group 1 length - group 5 length + results.copyGroup(2, 1); + if (results.hasGroupBegin(5)) { + results.offsetEndGroup(2, -1); + } } - } - // octal encoding ? - if (results.numberType == NumberType.INTEGER) { - if (s.charAt(results.getBeginGroup()[7]) == '0') { - boolean octal = (sevenLength > 1); - if (octal) { - for (int i = results.getBeginGroup()[7] + 1; i < results.getEndGroup()[7]; i++) { - if (!isOctalDigit(s.charAt(i))) { - octal = false; - break; + // octal encoding ? + if (results.getNumberType() == INTEGER) { + if (s.charAt(results.getBeginGroup(7)) == '0') { + boolean octal = (sevenLength > 1); + if (octal) { + for (int i = results.getBeginGroup(7) + 1; i < results.getEndGroup(7); i++) { + if (!isOctalDigit(s.charAt(i))) { + octal = false; + break; + } } } - } - if (octal) { - if (results.getBeginGroup()[5] != -1) { - if (!isL(s.charAt(results.getBeginGroup()[5]))) { - octal = false; + if (octal) { + if (results.hasGroupBegin(5)) { + if (!isL(s.charAt(results.getBeginGroup(5)))) { + octal = false; + } } } - } - if (octal) { - results.numberType = NumberType.OCTAL; + if (octal) { + results.setNumberType(OCTAL); + } } } } - - return (results.numberType != NumberType.NOT_A_NUMBER); } @@ -303,7 +285,6 @@ private static boolean extractDecimalNumber(String s, int start, int end, ParseN * @param start The index of the string to start the parsing after the sign and 0[Bb] prefix. * @param end The index of the last char to parse. * @param results contains the parsing details calculated in this method. - * @return true if a potential binary literal is found, false otherwise * *

based on Regular Expression (([-+]?)0[Bb]([01]+)([Ll]?)).* * group 1 = entire matching string. NB: re has .* postfix but this method does not include .* in this group. @@ -313,17 +294,16 @@ private static boolean extractDecimalNumber(String s, int start, int end, ParseN * group 4 [lL]? */ - private static boolean extractBinaryNumber(String s, int start, int end, ParseNumberResults results) { + private static void extractBinaryNumber(String s, int start, int end, ParseNumberResults results) { char c; + results.setNumberType(BINARY); results.setSignGroup(2); - - int group = 3; - results.getBeginGroup()[group] = start; + int group = results.setBeginGroup(3, start); bigWhile: while (start <= end) { if (start == end) { - results.getEndGroup()[group] = start; + results.setEndGroup(group, start); break; } c = s.charAt(start); @@ -332,24 +312,22 @@ private static boolean extractBinaryNumber(String s, int start, int end, ParseNu if (isBinaryDigit(c)) { start++; } else { - results.getEndGroup()[group] = start; + results.setEndGroup(group, start); group = 4; } break; case 4: // [Ll]? if (isL(c)) { - results.getBeginGroup()[group] = start; - results.getEndGroup()[group] = ++start; + results.setGroup(group, start, ++start); } // ok - we are done break bigWhile; } // switch group } // while bigWhile - results.getEndGroup()[1] = start; + results.setEnd(start); // need at least 1 digit in group 3 - if (results.getEndGroup()[3] - results.getBeginGroup()[3] == 0) - results.numberType = NumberType.NOT_A_NUMBER; - return (results.numberType == NumberType.BINARY); + if (results.getGroupLength(3) == 0) + results.setNumberType(NOT_A_NUMBER); } /** @@ -361,7 +339,6 @@ private static boolean extractBinaryNumber(String s, int start, int end, ParseNu * @param start The index of the string to start the parsing after the sign and 0[Xx] prefix. * @param end The index of the last char to parse. * @param results contains the parsing details calculated in this method. - * @return true if a potential hexadecimal literal is found, false otherwise * * *

based on Regular Expression (([-+]?)0[Xx]([0-9a-fA-F]+)([Ll]|(\\.[0-9a-fA-F]*)?[Pp]([-+]?)([0-9]+)([DdFf]|)|)).* @@ -377,17 +354,15 @@ private static boolean extractBinaryNumber(String s, int start, int end, ParseNu * group 9, not in re, is [Pp] which is required before groups 6, 7. */ - private static boolean extractHexNumber(String s, int start, int end, ParseNumberResults results) { + private static void extractHexNumber(String s, int start, int end, ParseNumberResults results) { char c; - + results.setNumberType(HEXADECIMAL); results.setSignGroup(2); - - int group = 3; - results.getBeginGroup()[3] = start; + int group = results.setBeginGroup(3, start); bigWhile: while (start <= end) { if (start == end) { - results.getEndGroup()[group] = start; + results.setEndGroup(group, start); break; } c = s.charAt(start); // start may not change when group changes @@ -396,19 +371,16 @@ private static boolean extractHexNumber(String s, int start, int end, ParseNumbe if (isHexDigit(c)) { start++; } else { - results.getEndGroup()[group] = start; - group = 4; - results.getBeginGroup()[group] = start; + group = results.endGroupBeginGroup(group,4, start); } break; case 4: // [Ll]? group end is adjusted after switch to include groups 5-8. if (isL(c)) { - results.getEndGroup()[group] = ++start; + results.setEndGroup(group, ++start); break bigWhile; // end of re. No need to continue parsing chars } if (c == '.') { - group = 5; - results.getBeginGroup()[group] = start++; + group = results.setBeginGroup(5, start++); break; } if (isP(c)) { @@ -417,67 +389,64 @@ private static boolean extractHexNumber(String s, int start, int end, ParseNumbe break; } // we are done, there is no group 4 - results.getBeginGroup()[group] = -1; + results.resetGroup(group); break bigWhile; case 5: // \.[0-9a-fA-F]* 0..n if (isHexDigit(c)) { start++; } else { - results.getEndGroup()[group] = start; + results.setEndGroup(group, start); if (isP(c)) { - group = 9; - results.getBeginGroup()[group] = start; - results.getEndGroup()[group] = ++start; + group = results.setGroup(9, start, ++start); } } break; case 9: // start -1 == p or P if (isASign(c)) { - group = 6; - results.getBeginGroup()[group] = start; - results.getEndGroup()[group] = ++start; + results.setGroup(6, start, ++start); } group = 7; break; case 7: // [0-9]+ enforce at least 1 char in this group if (isDecimalDigit(c)) { - if (results.getBeginGroup()[group] == -1) results.getBeginGroup()[group] = start; + results.setGroupBeginIfNotSet(group, start); start++; } else { - if (results.getBeginGroup()[group] == -1) { + if (results.groupIsEmpty(group)) { // need at least 1 digit to be valid - results.numberType = NumberType.NOT_A_NUMBER; + results.setNumberType(NOT_A_NUMBER); break bigWhile; } else { - results.getEndGroup()[group] = start; + results.setEndGroup(group, start); group = 8; } } break; case 8: // [DdFf]? if (isADoubleOrFloatSuffix(c)) { - results.getBeginGroup()[8] = start; - results.getEndGroup()[8] = ++start; + results.setGroup(8,start, ++start); } break bigWhile; } // switch group } // while bigWhile - results.getEndGroup()[1] = start; - results.getEndGroup()[4] = Math.max(results.getEndGroup()[4], Math.max(Math.max(results.getEndGroup()[5], results.getEndGroup()[6]), Math.max(results.getEndGroup()[7], results.getEndGroup()[8]))); + results.setEnd(start); + + // group 4 end = max of groups 4,5,6,7,8 + results.setEndGroupMaxOfRange(4, 4, 8); + // P (in group 9) requires at least one entry in group 7 - if ((results.getBeginGroup()[9] != -1) && (results.getBeginGroup()[7] == -1)) - results.numberType = NumberType.NOT_A_NUMBER; + if (results.hasGroupBegin(9) && results.groupIsEmpty(7)) + results.setNumberType(NOT_A_NUMBER); // group 3 must have at least 1 digit - if (results.getEndGroup()[3] - results.getBeginGroup()[3] == 0) - results.numberType = NumberType.NOT_A_NUMBER; - return (results.numberType == NumberType.HEXADECIMAL); + if (results.getGroupLength(3) == 0) + results.setNumberType(NOT_A_NUMBER); } // process extracted results private static void processNumber(String in, ParseNumberResults results) { - switch (results.numberType) { + switch (results.getNumberType()) { case INTEGER: case DOUBLE: results.setNumber(processDecimal(in, results)); @@ -545,7 +514,7 @@ private static Number processDecimal(String s, ParseNumberResults results) { final boolean forceFloat = "f".equalsIgnoreCase(force); final boolean forceDouble = "d".equalsIgnoreCase(force); Number result = null; - if (results.numberType == NumberType.INTEGER) { + if (results.getNumberType() == INTEGER) { // No decimal point and no exponent part. So this *might* be an integer! result = parseIntegerToNumber(numberStr, forceLong, 10); } @@ -676,14 +645,6 @@ private static boolean isADoubleOrFloatSuffix(char c) { return (c == 'D' || c == 'd' || c == 'F' || c == 'f'); } - public enum NumberType { - NOT_A_NUMBER, - INTEGER, // or LONG or BIGINT - DOUBLE, // or FLOAT - HEXADECIMAL, - OCTAL, // INTEGER or LONG - BINARY, // INTEGER, DOUBLE, FLOAT - } } diff --git a/src/main/java/org/scijava/parsington/ParseNumberResults.java b/src/main/java/org/scijava/parsington/ParseNumberResults.java index 783216f..0e52df8 100644 --- a/src/main/java/org/scijava/parsington/ParseNumberResults.java +++ b/src/main/java/org/scijava/parsington/ParseNumberResults.java @@ -1,52 +1,146 @@ package org.scijava.parsington; public class ParseNumberResults { - private int signNdx = -1; - ParseNumber.NumberType numberType; - private final int[] beginGroup = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; - private final int[] endGroup = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; - private Number number = null; + private final int EMPTY = -1; + private final int GROUPALL = 1; + private final int[] beginGroup = {EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY}; + private final int[] endGroup = {EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY}; + private int signNdx = EMPTY; + private NumberType numberType; + private Number number = null; void setSignIndex(int signNdx) { this.signNdx = signNdx; } void setSignGroup(int signGroup) { - if (signNdx != -1) { + if (signNdx != EMPTY) { beginGroup[signGroup] = signNdx; - endGroup[signGroup] = beginGroup[signGroup] + 1; + endGroup[signGroup] = signNdx + 1; } } String getGroup(String in, int group) { - if ((beginGroup[group] != -1) && (endGroup[group] != -1)) { - return in.substring( beginGroup[group], endGroup[group]); + if ((beginGroup[group] != EMPTY) && (endGroup[group] != EMPTY)) { + return in.substring(beginGroup[group], endGroup[group]); } else { return ""; } } int getLength() { - final int ONE = 1; - if ((beginGroup[ONE] != -1) && (endGroup[ONE] != -1)) { - return endGroup[ONE] - beginGroup[ONE]; - } - return 0; + return getGroupLength(GROUPALL); } Number getNumber() { return number; } + void setNumber(Number number) { this.number = number; } - public int[] getBeginGroup() { - return beginGroup; + void setBegin(int ndx) { + setBeginGroup(GROUPALL, ndx); + } + + void setEnd(int ndx) { + setEndGroup(GROUPALL, ndx); + } + + int setBeginGroup(int group, int ndx) { + beginGroup[group] = ndx; + return group; + } + + int getBeginGroup(int group) { + return beginGroup[group]; + } + + void setEndGroup(int group, int ndx) { + endGroup[group] = ndx; + } + + int getEndGroup(int group) { + return endGroup[group]; + } + + int offsetBeginGroup(int group, int offset) { + return beginGroup[group] += offset; + } + + void offsetEndGroup(int group, int offset) { + endGroup[group] += offset; + } + + int getGroupLength(int group) { + if ((beginGroup[group] != EMPTY) && (endGroup[group] != EMPTY)) { + return endGroup[group] - beginGroup[group]; + } + return 0; + } + + boolean hasGroupBegin(int group) { + return beginGroup[group] != EMPTY; + } + + boolean groupIsEmpty(int group) { + return beginGroup[group] == EMPTY; + } + + void resetGroup(int group) { + beginGroup[group] = EMPTY; + endGroup[group] = EMPTY; } - public int[] getEndGroup() { - return endGroup; + int setGroup(int group, int start, int stop) { + beginGroup[group] = start; + endGroup[group] = stop; + return group; } + + // NB destination first + void copyGroup(int dest, int source) { + beginGroup[dest] = beginGroup[source]; + endGroup[dest] = endGroup[source]; + } + + void setEndGroupMaxOfRange(int destination, int sourceStart, int sourceStop) { + int maxEnd = endGroup[sourceStart]; + for (int g = sourceStart + 1; g <= sourceStop; g++) { + if (endGroup[g] > maxEnd) { + maxEnd = endGroup[g]; + } + } + endGroup[destination] = maxEnd; + } + + void setGroupBeginIfNotSet(int group, int start) { + if (beginGroup[group] == EMPTY) { + beginGroup[group] = start; + } + } + + int endGroupBeginGroup(int groupEnd, int groupBegin, int startEnd) { + endGroup[groupEnd] = startEnd; + beginGroup[groupBegin] = startEnd; + return groupBegin; + } + + public NumberType getNumberType() { + return numberType; + } + + public void setNumberType(NumberType numberType) { + this.numberType = numberType; + } + + public enum NumberType { + NOT_A_NUMBER, INTEGER, // or LONG or BIGINT + DOUBLE, // or FLOAT + HEXADECIMAL, OCTAL, // INTEGER or LONG + BINARY, // INTEGER, DOUBLE, FLOAT + } + }