BigDecimal转换为SQL NUMBER:检查值是否大于精度

10
在我的应用程序中,我使用BigDecimal处理数字并将它们存储为NUMBER(15,5)。现在我需要在Java中适当地检查BigDecimal值是否适合该列,以便我可以生成适当的错误消息,而无需执行SQL,捕获异常并验证供应商错误代码。我的数据库是Oracle 10.3,这些错误会导致错误1438
在一些搜索后,我没有找到这样的代码,因此我想出了自己的代码。但我对这个代码真的不满意...简单,但同时也足够简单,以至于怀疑它的正确性。我使用许多值进行了测试,包括随机数和边界值,并且似乎可以工作。但由于我对数字很差,我需要更加健壮和经过充分测试的代码。
//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;
}

编辑:最近的测试没有出现数字超出范围的异常,只是被默默地舍入了,我不确定这和我第一次测试时有什么不同。这样的舍入是不可接受的,因为应用程序是财务相关的,任何舍入/截断都必须是显式的(通过BigDecimal方法)。除去异常,这个测试方法必须确保数字不会因为非重要数字而过大而失去精度。对于晚期的澄清感到抱歉。
感谢您的时间。
我仍然对这个问题很好奇。我的代码仍在运行,我还没有得到正确性或失败情况的“证明”,或者这种测试的标准代码。
因此,我正在为此提供悬赏,希望能得到其中任何一种。

我能想到的唯一不同的方法是使用toString,然后拆分BD并比较字符串的长度,但我认为你的方法比这更好。 - InsertNickHere
是的,我这样做是为了用随机值测试这个函数成千上万次......只要在这个字符串操作版本中去掉负号信号,它们总是同意响应。尽管如此,在我看来这仍然是有偏见的测试。 - mdrg
4个回答

5
以下正则表达式也可以实现相同的效果:
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());
    }
}

这可能是测试您的代码的另一种方式,也可以是代码本身。该正则表达式要求0到10个数字,可选地后跟一个小数点和0到5个数字。我不确定是否需要符号,当我考虑它时。在前面添加类似[+-]{0,1}的东西即可。

这里有一个更好的类,也许还有一个带有部分测试的测试类。

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"));
    }
}

我不想使用正则表达式,因为BigDecimal也接受指数表示法(123.45e-1),处理起来看起来很混乱...这正是我遇到最奇怪的情况。但还是谢谢你的建议。 - mdrg
指数错误已经被你成功捕获。我将BigDecimal().toString更改为.toPlainString(),现在指数问题得到了解决。我已经添加了代码更改和新的测试用例;-) 正则表达式有一种简单而残酷的诚实。然而,这可能不是它的正确用途。我相信《代码大全》这本书建议在测试时使用两种不同的技术来编写复杂任务的代码——如果它们的答案不同,那么你就有一个bug。而且你关心你的代码是否正确。 - Tony Ennis
2
哎呀,我因为一行牢不可破的解决方案和测试用例而被踩了。这群人真难搞! - Tony Ennis

3
您的函数存在一个问题,即在某些情况下可能过于限制。请考虑以下示例:
BigDecimal a = new BigDecimal("0.000005"); /* scale 6 */
a = a.multiply(new BigDecimal("2")); /* 0.000010 */
return testBigDecimal(a); /* returns false */

如您所见,比例尺并未调整。我现在无法测试高精度(1e11/2)是否会发生类似情况。

我建议采用更直接的方法:

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;
}

更新

你在评论中问到,如果我们尝试插入0.000010,数据库会不会抛出错误。实际上,如果您尝试插入太多精度的值,数据库永远不会抛出错误,它将默默地舍入插入的值。

因此,第一次检查不需要避免Oracle错误,我假设您执行此测试是为了确保要插入的值等于实际插入的值。由于0.000010和0.00001相等(使用BigDecimal.compareTo),它们不应该返回相同的结果吗?


这很有趣,谢谢。但是重点在于如何防止数据库错误... 如果DBMS在这种情况下(0.000010)引发异常,那么即使移除非显著数字以确保精度正确,我也必须予以拒绝。我可能会很快测试它,并尽快发布结果。 - mdrg
@mdrg:如果您尝试插入具有过多精度的数字,Oracle不会抛出错误 :) 它会默默地将其四舍五入以适应列(.000011 将被四舍五入为 .00001)。请参阅我的更新答案。 - Vincent Malgrat
这真是太糟糕了,在我看来。无论如何,现在我不确定我是如何遇到这个ora-01438错误的,因为我可以存储和复制舍入行为,但这仍然对我们的金融应用程序是不可接受的。我将编辑问题并包含此信息。 - mdrg
1
@mdrg:当列无法容纳插入的值(例如您的情况下为1e11)时,会引发ORA-01438错误。沉默的四舍五入行为可能会令人惊讶,但实际上这是许多语言的常见行为:在Java中,int i = 7/2;不会引发错误,而是会自动四舍五入为3。 - Vincent Malgrat
整数除法严格来说并不是四舍五入,但我知道你的意思。我原以为无论精度过大还是比例过大都会出现错误,感谢澄清。在我的应用程序中,最好的解决方案是两者都拒绝。 - mdrg

1

不要循环数千个随机数,而是编写测试用例来测试“边缘”情况——最大值+.00001、最大值、最大值-.00001、0、null、最小值-.00001、最小值、最小值+.00001以及小数点右侧有4、5和6个值的数。可能还有更多。

如果在junit中拥有这些测试用例,那就很好了。


我已经测试过它们,感觉还可以。但是,我不敢将这个函数标记为“正确”;并不是因为我喜欢把事情搞得复杂化,而是感觉没有正确地覆盖所有的输入。 - mdrg

1

好吧,既然没有人提出其他解决方案,我就把代码保留原样。

我无法使这个精度/比例尺测试失败,它总是与正则表达式解决方案匹配,所以也许两者都是正确的(我测试了边界和超过5M个随机生成的值)。我将使用精度/比例尺解决方案,因为它快了85%以上,如果它失败了,我会进行替换。

感谢你的回复Tony。


我的之前的“答案”,仅供历史记录,但我正在寻找一个真正的答案=)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接