10

In my app, I handle numbers as BigDecimal and store them as NUMBER(15,5). Now I'd need to properly check on Java if the BigDecimal values would fit the column, so that I can generate proper error messages without executing the SQL, catching exceptions and verifying the vendor error code. My database is Oracle 10.3, and such errors cause error 1438.

After some googling, I found no such code for that, so I came up with my own. But I'm really unsatisfied with this code... simple, but at the same time simple enough to doubt its correctness. I tested it with many values, random ones and boundaries, and it seems to work. But as I'm really bad with numbers, I'd like some more robust and well-tested code.

//no constants for easier reading
public boolean testBigDecimal(BigDecimal value) {
    if (value.scale() > 5)
        return false;
    else if (value.precision() - value.scale() > 15 - 5)
        return false;
    else
        return true;
}

Edit: Recent tests did not got an exception for numbers out of scale, just got silently rounded, and I'm not sure what is different between not and when I made these first tests. Such rounding is unacceptable because the application is financial, and any rounding/truncation must be explicit (through BigDecimal methods). Exception-is-gone aside, this test method must assure that the number is not too large for the desired precision, even if by non-significant digits. Sorry about the late clarification.

Thanks for your time.


I'm still curious about this question. My code is still running, and I haven't got some "proof" of correctness or fail situation, or some standard code for this kind of test.

So, I'm putting a bounty on it, hopefully getting any of these.

Kiquenet
  • 14,494
  • 35
  • 148
  • 243
mdrg
  • 3,242
  • 2
  • 22
  • 44
  • The only different thing that comes in mind to me would be to use the toString and then split the BD and compare the length of the string, but I think your way is better than that. – InsertNickHere Sep 22 '10 at 19:18
  • Yes, I did that to test this function tens of thousands times with random values... they always agreed on the response (as long as I strip minus signal in this string manipulation version). Still, that's biased testing IMHO. – mdrg Sep 22 '10 at 19:27

4 Answers4

5

The following regexp would do the trick too:

public class Big {
    private static final Pattern p = Pattern.compile("[0-9]{0,10}(\\.[0-9]{0,5}){0,1}");

    public static void main(String[] args) {
        BigDecimal b = new BigDecimal("123123.12321");
        Matcher m = p.matcher(b.toString());
        System.out.println(b.toString() + " is valid = " + m.matches());
    }
}

This could be another way to test your code or it could be the code. The regexp requires between 0 and 10 digits optionally followed by a decimal point and 0 to 5 more digits. I didn't know if a sign was needed or not, as I think about it. Tacking something like [+-]{0,1} to the front will do.

Here is a better class, maybe, and a test class with a partial set of tests.

public class Big {
    private static final Pattern p = Pattern.compile("[0-9]{0,10}(\\.[0-9]{0,5}){0,1}");

public static boolean isValid(String s) {
    BigDecimal b = new BigDecimal(s);
    Matcher m = p.matcher(b.toPlainString());
    return m.matches();
    }
}

package thop;

import junit.framework.TestCase;

/**
 * Created by IntelliJ IDEA.
 * User: tonyennis
 * Date: Sep 22, 2010
 * Time: 6:01:15 PM
 * To change this template use File | Settings | File Templates.
 */
public class BigTest extends TestCase {

    public void testZero1() {
        assertTrue(Big.isValid("0"));
    }

    public void testZero2() {
        assertTrue(Big.isValid("0."));
    }

    public void testZero3() {
        assertTrue(Big.isValid("0.0"));
    }

    public void testZero4() {
        assertTrue(Big.isValid(".0"));
    }

    public void testTooMuchLeftSide() {
        assertFalse(Big.isValid("12345678901.0"));
    }

    public void testMaxLeftSide() {
        assertTrue(Big.isValid("1234567890.0"));
    }

    public void testMaxLeftSide2() {
        assertTrue(Big.isValid("000001234567890.0"));
    }

    public void testTooMuchScale() {
        assertFalse(Big.isValid("0.123456"));
    }

    public void testScientificNotation1() {
        assertTrue(Big.isValid("123.45e-1"));
    }

    public void testScientificNotation2() {
        assertTrue(Big.isValid("12e4"));
    }
}
Tony Ennis
  • 12,000
  • 7
  • 52
  • 73
  • I didn't want to use regexp for that, as BigDecimal also accepts exponents (123.45e-1), and dealing with that looks messy... that's exactly with exponential notation that I get most weird cases. But thanks for your suggestions. – mdrg Sep 23 '10 at 12:20
  • Good catch on the exponent bug. I changed `BigDecimal().toString` to `.toPlainString()` and now the exponent issue is handled. I added the code change and new test cases above ;-) There is a simple brutal honesty of a regexp. This may not be the right job for it, however. I believe the "Code Complete" book recommends that while testing, to code complex tasks using two different techniques - if their answers diverge, you've got a bug. And you're concerned about the correctness of your code... – Tony Ennis Sep 23 '10 at 12:48
  • 2
    Heh, I got a downvote for a one-line rock-solid solution complete with test cases. Tough crowd! – Tony Ennis Sep 24 '10 at 13:47
3

one of the problems with your function is that in some cases it may be too restrictive, consider:

BigDecimal a = new BigDecimal("0.000005"); /* scale 6 */
a = a.multiply(new BigDecimal("2")); /* 0.000010 */
return testBigDecimal(a); /* returns false */

As you can see, the scale is not adjusted down. I can't test right now if something similar happens with high-end precision (1e11/2).

I would suggest a more direct route:

public boolean testBigDecimal(BigDecimal value) {
    BigDecimal sqlScale = new BigDecimal(100000);
    BigDecimal sqlPrecision = new BigDecimal("10000000000");
    /* check that value * 1e5 is an integer */
    if (value.multiply(sqlScale)
          .compareTo(value.multiply(sqlScale)
              .setScale(0,BigDecimal.ROUND_UP)) != 0)
        return false;
    /* check that |value| < 1e10 */
    else if (value.abs().compareTo(sqlPrecision) >= 0)
        return false;
    else
        return true;
}

Update

You've asked in a comment if the database would throw an error if we try to insert 0.000010. In fact the database will never throw an error if you try to insert a value with too much precision, it will silently round the inserted value.

The first check is therefore not needed to avoid an Oracle error, I was assuming that you were performing this test to make sure that the value you want to insert is equal to the value you actually inserted. Since 0.000010 and 0.00001 are equal (with BigDecimal.compareTo) shouldn't they both return the same result?

Community
  • 1
  • 1
Vincent Malgrat
  • 66,725
  • 9
  • 119
  • 171
  • This is interesting, thanks. But still, the point is preventing a database error... would the DBMS raise an exception on such situation (0.000010) ? If it does, then I must refuse it as well, no matter if removing non-significant digits the precision is fine. I may test it soon, and I'll post the result ASAP. – mdrg Feb 15 '11 at 19:01
  • @mdrg: Oracle will not throw an error if you try to insert a number with too much precision :) it will silently round it to fit into the column (.000011 will be rounded to .00001). See my updated answer. – Vincent Malgrat Feb 16 '11 at 08:49
  • That's awful, IMO. Anyway now I'm not sure how I got this ora-01438 error, because I could store and reproduce the rounding behavior, which is still unacceptable for me, as our application is financial. I'll edit the question body including that. – mdrg Feb 16 '11 at 16:13
  • 1
    @mdrg: the ORA-01438 is raised when the column can't hold the inserted value (1e11 in your case for example). The silent rounding behaviour might surprise you but in fact it is a common behaviour for many languages: in java `int i = 7/2;` will not raise an error and will silently round to 3. – Vincent Malgrat Feb 16 '11 at 16:32
  • Integer division is not rounding, strictly speaking, but I know your point. I thought the error would rise in both cases (precision too large or scale too large), thanks for the clarification. In my app, the best solution is refusing both. – mdrg Feb 16 '11 at 18:15
1

Instead if looping over thousands of random numbers, you could write test cases that stress the 'edges' - the maximum value +.00001, the maximum value, the maximum value - .00001, 0, null, the minimum value -.00001, the minimum value, the minimum value + .00001, and values with 4, 5, and 6 values to the right of the decimal point. There are probably many more.

If you have those in junit, you're good.

Tony Ennis
  • 12,000
  • 7
  • 52
  • 73
  • I did tested them, and it was just OK. Still, I'm afraid to label this function 'correct'; not that I like to overcomplicate things, but it feels I'm not correctily covering all the inputs. – mdrg Sep 23 '10 at 12:25
1

Well, since nobody came up with another solution, I'm leaving the code as it is.

I couldn't make this precision/scale test fail, and it always matched the regex solution, so maybe both are correct (I tested the boundaries and with over 5M randomly generated values). I'll use the precision/scale solution, as it is over 85% faster, and may it fail I replace it.

Thanks for your replies Tony.


My previous "answer", still here for history purposes, but I'm looking for a real answer =)

mdrg
  • 3,242
  • 2
  • 22
  • 44