11
\$\begingroup\$

I'm developing a Java Helper Library which has useful methods developers don't want to type out over and over. I'm trying to think of the best way of checking whether a string is a valid number.

I've always just tried something like Integer.parseInt(iPromiseImANumber); and if it throws an exception then it's not a valid number (integer in this case). I'm thinking that trying to catch an exception isn't good practice. Is there a better way to do this? (I'm thinking a regular expression could do it, but it doesn't seem to be as reliable as the parse option).

Here's my method as it stands. What could be improved?

/**
* Does a try catch (Exception) on parsing the given string to the given type
*
* @param c the number type. Valid types are (wrappers included): double, int, float, long.
* @param numString number to check
* @return false if there is an exception, true otherwise
*/
public static boolean isValidNumber(Class c, String numString) {
  try {
    if (c == double.class || c == Double.class) {
      Double.parseDouble(numString);
    } else if (c == int.class || c == Integer.class) {
      Integer.parseInt(numString);
    } else if (c == float.class || c == Float.class) {
      Float.parseFloat(numString);
    } else if (c == long.class || c == Long.class) {
      Long.parseLong(numString);
    }
  } catch (Exception ex) {
    return false;
  }
  return true;
}
\$\endgroup\$
9
  • 1
    \$\begingroup\$ stackoverflow.com/questions/8391979/… \$\endgroup\$
    – Leonid
    May 16, 2012 at 2:15
  • \$\begingroup\$ More relevant - stackoverflow.com/questions/9042922/… \$\endgroup\$
    – Leonid
    May 16, 2012 at 2:24
  • \$\begingroup\$ What exactly do you consider a "valid number"? IMHO in most use cases a "valid number" doesn't necessarily mean it has to be a valid Java Integer/Long/Float/Double. \$\endgroup\$
    – RoToRa
    May 16, 2012 at 10:57
  • \$\begingroup\$ What else could it be? \$\endgroup\$
    – kentcdodds
    May 16, 2012 at 11:21
  • \$\begingroup\$ I'm guessing that @RoToRa is referring to range checking. It won't matter if the value can be legally parsed to a given type if it is outside the range of reasonable/permitted values. \$\endgroup\$ May 16, 2012 at 12:55

5 Answers 5

10
\$\begingroup\$

NumberUtils.isNumber from Apache Commons Lang does that.

\$\endgroup\$
1
  • \$\begingroup\$ See my benchmarking answer. This was definitely the fastest. Thanks for the tip! \$\endgroup\$
    – kentcdodds
    May 16, 2012 at 15:35
5
\$\begingroup\$

Well, my first instinct would be to apply a refactoring - specifically Replace Conditional With Polymorphism.

To do this, create an interface with one method (isValidNumber works). Create four implementations, one each for ints, doubles, floats and longs. Create a HashMap<Class, MyInterface>. Create one instance of each of the four classes and add them to the collection twice, once using the wrapped and unwrapped form of each type (int and Integer).

Now your main isValidNumber implementation has about two lines. You get the implementation object out of the HashMap and then call it, returning the result.

Now whether to use the parse methods from the numeric types or to use regular expressions or to code something of your own, such as a simple state machine, that's almost a matter of circumstances and personal taste. Each approach has strengths and weaknesses, though all three will work. If it's inside a tight loop, performance might even matter (though it's highly doubtful it will make that much difference).

\$\endgroup\$
1
  • \$\begingroup\$ I'm not sure how to refactor this as the article you linked to discusses. You recommend making classes with the method? \$\endgroup\$
    – kentcdodds
    May 16, 2012 at 5:41
4
\$\begingroup\$

Ok, don't hate me, but this is all the answers above compiled into one. I benchmarked all the answers with the following code:

public static void parseNumber() throws Exception {
  long numberHelperTotal = 0;
  long numberUtilsTotal = 0;
  long regExTotal = 0;
  long bruteForceTotal = 0;
  long scannerTotal = 0;
  int iterations = 5;
  for (int i = 0; i < iterations; i++) {
    long numberHelper = 0;
    long numberUtils = 0;
    long regEx = 0;
    long bruteForce = 0;
    long scanner = 0;
    for (int j = 0; j < 99999; j++) {
      long start;
      long end;
      Random rand = new Random();
      String string = ((rand.nextBoolean()) ? "" : "-") + String.valueOf(rand.nextDouble() * j);
      //NumberHelper this is the try/catch solution seen above
      start = System.nanoTime();
      NumberHelper.isValidNumber(double.class, string);
      end = System.nanoTime();
      numberHelper += end - start;

      //NumberUtils
      start = System.nanoTime();
      NumberUtils.isNumber(string);
      end = System.nanoTime();
      numberUtils += end - start;

      //RegEx
      start = System.nanoTime();
      Pattern p = Pattern.compile("^[-+]?[0-9]*\\.?[0-9]+$");
      Matcher m = p.matcher(string);
      if (m.matches()) {
        Double.parseDouble(string);
      }
      end = System.nanoTime();
      regEx += end - start;

      //Brute Force (not international support) and messy support for E and negatives
      //This is not the way to do it...
      start = System.nanoTime();
      int decimalpoints = 0;
      for (char c : string.toCharArray()) {
        if (Character.isDigit(c)) {
          continue;
        }
        if (c != '.') {
          if (c == '-' || c == 'E') {
            decimalpoints--;
          } else {
            //return false
            //because it should never return false in this test, I will throw an exception here if it does.
            throw new Exception("Brute Force returned false! It doesn't work! The character is " + c + " Here's the number: " + string);
          }
        }
        if (decimalpoints > 0) {
          //return false
          //because it should never return false in this test, I will throw an exception here if it does.
          throw new Exception("Brute Force returned false! It doesn't work! The character is " + c + " Here's the number: " + string);
        }
        decimalpoints++;
      }
      end = System.nanoTime();
      bruteForce += end - start;

      //Scanner
      start = System.nanoTime();
      Scanner scanNumber = new Scanner(string);
      if (scanNumber.hasNextDouble()) {//check if the next chars are integer
        //return true;
      } else {
        //return false;
        //because it should never return false in this test, I will throw an exception here if it does.
        throw new Exception("Scanner returned false! It doesn't work! Here's the number: " + string);
      }
      end = System.nanoTime();
      scanner += end - start;

      //Increase averages
      //For debug:
      //System.out.println("String: " + string);
      //System.out.println("NumberHelper: " + numberHelper);
      //System.out.println("NumberUtils: " + numberUtils);
      //System.out.println("RegEx: " + regEx);
      //System.out.println("Brute Force: " + bruteForce);
      //System.out.println("Scanner: " + scanner);
    }
    numberHelperTotal += numberHelper;
    numberUtilsTotal += numberUtils;
    regExTotal += regEx;
    bruteForceTotal += bruteForce;
    scannerTotal += scanner;
  }

  long numberHelperAvg = numberHelperTotal / iterations;
  long numberUtilsAvg = numberUtilsTotal / iterations;
  long regExAvg = regExTotal / iterations;
  long bruteForceAvg = bruteForceTotal / iterations;
  long scannerAvg = scannerTotal / iterations;
  System.out.println("NumberHelper: " + (numberHelperAvg / 1000000) + " milliseconds -> " + (numberHelperAvg / 1000000000) + " seconds");
  System.out.println("NumberUtils: " + (numberUtilsAvg / 1000000) + " milliseconds -> " + (numberUtilsAvg / 1000000000) + " seconds");
  System.out.println("RegEx: " + (regExAvg / 1000000) + " milliseconds -> " + (regExAvg / 1000000000) + " seconds");
  System.out.println("Brute Force: " + (bruteForceAvg / 1000000) + " milliseconds -> " + (bruteForceAvg / 1000000000) + " seconds");
  System.out.println("Scanner: " + (scannerAvg / 1000000) + " milliseconds -> " + (scannerAvg / 1000000000) + " seconds");
}

The standings in the end were:

NumberHelper: 158 milliseconds -> 0 seconds //Third place (this is the try/catch solution)
NumberUtils: 26 milliseconds -> 0 seconds //First place
RegEx: 444 milliseconds -> 0 seconds //Fourth place
Brute Force: 39 milliseconds -> 0 seconds //Second place, but a messy solution...
Scanner: 22204 milliseconds -> 22 seconds //LAST place. Slow solution
\$\endgroup\$
3
  • 1
    \$\begingroup\$ I am sorry but this is a horrible benchmark. How do you actually run it? Do you have any warm up at all? \$\endgroup\$
    – Krroae27
    May 16, 2012 at 22:07
  • \$\begingroup\$ Haha, well I guess I still have a lot to learn about benchmarking. I've never done it before. I run it as is... \$\endgroup\$
    – kentcdodds
    May 17, 2012 at 1:27
  • \$\begingroup\$ A nice little read for you. stackoverflow.com/questions/504103/… My last comment was bad, should have added in a few things sorry.. (Regex is slow and I wouldn't use it for this but not as slow as your benchmark because the pattern should be static and well... just read the other post.) \$\endgroup\$
    – Krroae27
    May 17, 2012 at 2:08
2
\$\begingroup\$

You could always just brute force it (NOTE: Not internationalized):

int decimalpoints = 0;
foreach (Char i: numString) {
    if (i.isDigit()) continue;
    if (i != '.') return false;
    if (decimalpoints > 0) return false;
    decimalpoints += 1;
}
\$\endgroup\$
3
  • 1
    \$\begingroup\$ Nope, this one won't work for all cases. For example, Double.parseDouble("10E-3") is valid and evaluates to 0.01. \$\endgroup\$
    – ZeroOne
    May 16, 2012 at 21:23
  • 1
    \$\begingroup\$ True, but it still may be acceptable to just not handle scientific notation input formats. \$\endgroup\$
    – pjz
    May 16, 2012 at 23:58
  • \$\begingroup\$ That's true too. \$\endgroup\$
    – ZeroOne
    May 17, 2012 at 9:06
1
\$\begingroup\$

Saying "is this string a valid number" is quite pointless when you don't have a spec what a valid number is. For example, a valid JSON number can have a leading minus but not a leading plus, it can't start with a zero unless the integer part is a single zero, if there's a decimal point (not comma) then there must be digits after it, and the exponent can have a plus or minus and as many leading zeroes as it wants, just to be inconsistent.

So there may be a spec, and you better test for that spec. If Double.parseDouble can't handle it then tough; check it by hand. Or you are handling user input. In that case I'd ask for common sense. 10e-3 should probably not be considered valid input, nor should 0x1234, nor should 0777 be different from 777. I'd probably ask to be nice about spaces, use localised decimal and thousand separators, handle $ or € gracefully.

About the pattern matching speed test: I think patterns need to be compiled, and the compiling takes a long time, compared to the actual matching. I would assume that to parse a number using some specific method, a pattern would be compiled once and then reused again and again, so that's how it should be measured.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.