From c61fe60b2f5b0dc8ea60ba63ca556862091e066b Mon Sep 17 00:00:00 2001
From: Jared Davis
Date: Sat, 25 Jul 2026 12:14:48 -0400
Subject: [PATCH 01/17] 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/17] 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/17] 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/17] 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 extends Operator> 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/17] 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/17] 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/17] 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/17] 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