从BigDecimal中移除小数结尾的零

5
我写了一个测试,并希望将BigDecimal结果与期望值进行比较。我想使用方法Assert.assertEquals(BigDecimal, BigDecimal),因为如果失败,它会显示精确的比较值,在eclipse中我可以显示比较窗口。
所以在代码中,我有一个方法返回保留2位小数的BigDecimal。在测试用例中,我知道它返回没有非零小数位的数字。因此,我想创建一个具有0刻度的BigDecimal,并将返回的BigDecimal修剪到相同的刻度。
更复杂的是,我现在有一个方法getDecimal(Object [,Optionaly int scale]),它可以从任何具有正确toString()值的对象创建BigDecimal,默认刻度为99。我在主要的“重”代码中使用它,因此这个方法必须非常快(不必创建其他对象,不使用正则表达式等)。
所以简单的问题是:如何通过最小负载修改BigDecimal实例以修剪末尾的小数零。
想要的效果如下:
0.010 -> 0.01
5.000 -> 5
100 -> 100   not 1E+2

回复:

someTrim(new BigDecimal("100.00")).equals(new BigDecimal(100))

做一些类似数学的东西

100.00 / (2, 99, DOWN) * 50.00 -> 2500

我的代码看起来像这样:
public static BigDecimal getDecimal( Object value ) {
    // null here probably work beter, but dont want number longer then 99 decimal digits
    return getDecimal( value, 99 );
}

public static BigDecimal getDecimal( Object value, Integer scale ) {
    if ( value == null )
        return null;

    BigDecimal result = ( value instanceof BigDecimal ) ? ( BigDecimal ) value : new BigDecimal( value.toString() );

    if ( scale == null )
        return result;

    return result.setScale( scale, DOWN );
}

// Main heavy method could call this 100 000 times per tenant (cca 1500 tenants), of course not expect all in same time, but can severals
public static myModify(E entity){
    return myModify( entity.getValue(), entity.getDivisor(), entity.getMultiplicator() );
}

public static myModify( BigDecimal value, Integer divisor, BigDecimal multiplicator){
     return value.divide( getDecimal(divisor), 99, RoundingMode.DOWN ).multiply( multiplicator );
}

@Test
public void myModifyTest(){
    // Constructor have param: value, divisor, multiplicator
    E entity = new E(new BigDecimal("100.00"), 2, new BigDecimal("50.00"));
    // Should pass
    Assert.assertEquals(getDecimal(100), entity);
    // Should drop: java.lang.AssertionError: expected:<50> but was:<100>
    Assert.assertEquals(getDecimal(50), entity);
    // Not want: java.lang.AssertionError: expected:<5E+1> but was:<1E+2>
}

也许存在另一种Junit比较方法可以消除相同的错误,但我不知道它是什么。谢谢你的帮助。保尔。

这段代码非常脆弱。你应该编写两个 getDecimal 方法,一个接受 BigDecimal,另一个接受 String。然后你将大大简化你的代码,并减少可能的错误。 - Software Engineer
1
当不需要缩放时,最好使用BigDecimals,除非要转换为其他类型。您不应该将代码限制在任意数量的小数点上。您还应该使用compareTo()而不是equals() - Software Engineer
正确是更好的选择。但我不确定PostgeSql中Numeric数据类型可以有多大,大多数数字在Java中受限于精度(17,2)和(15,2)。有几个是(?,4),所以由于乘除法运算,我需要一些十进制位的保留,但不必全部保留(不确定Hessian如何处理序列化->数据传输量有多大) - Perlos
创建更多带有必要参数的方法可能会更好地工作,而且没有错误,但是为每种原始类型+所有其他数字类型+所有CharSequence类型编写方法似乎不太好。 - Perlos
1
你只需要long、double和String——其他所有的都可以由库的用户强制转换或自动向上转换。这只有3个方法,几乎不会过度。此外,你实际上并不需要这些方法——就像我说的,在内部不应该担心有效数字,直到你需要以某种方式输出值,然后你应该四舍五入/截断它。例如,如果你做了一个除法,然后将结果乘以一个非常大的数,根据乘数的数量级,数字变得重要,过早地截断数字可能会改变结果。 - Software Engineer
3个回答

1

1
这是一个很好的相等测试方法。比assertTrue(B1.compareTo(B2))更好,但正如我所说,如果我测试返回99位小数的数字的方法,那么异常并不容易读取。 - Perlos
1
写入:Assert.assertThat( new BigDecimal("100.1").setScale( 99 ), Matchers.comparesEqualTo( new BigDecimal( 100 ) ) );获取:java.lang.AssertionError: 期望值:等于 <100> 但实际值: <100.100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000> 大于 <100> - Perlos
1
@Perlos,你可以轻松地编写自定义的Hamcrest Matcher,并自定义不匹配错误消息:http://www.planetgeek.ch/2012/03/07/create-your-own-matcher/(通过覆盖describeMismatch方法)。 - frhack
是的,最好的测试方法是复制类OrderingComparison,并更改方法describeMismatchSafely。但是许可证如何处理呢? - Perlos
BSD许可证https://github.com/hamcrest/JavaHamcrest/blob/master/LICENSE.txt您只需添加一个通知:“以二进制形式重新分发必须在文档和/或随附分发的其他材料中复制上述版权声明、此条件列表和以下免责声明。” - frhack

1
我可能找到了一个解决方案。创建另外两个BigDecimal实例是不好的,但我不知道是否有其他更少侵入性的方法。我进行了一些小优化,以避免在不必要时创建实例。
/** 
 * For text comparsion or log propouse
 * @return human readable text without decimal zeros 
 */
public static String getPlainText( BigDecimal value ) {
    if ( value == null )
        return null;

    // Strip only values with decimal digits
    BigDecimal striped = ( value.scale() > 0 ) ? value.stripTrailingZeros() : value;
    return striped.toPlainString();
}

/** 
 * For comparison by equals method like test assertEquals
 * @return new instance without decimal zeros 
 */
public static BigDecimal stripDecimalZeros( BigDecimal value ) {
    if ( value == null )
        return null;

    // Strip only values with decimal digits
    BigDecimal striped = ( value.scale() > 0 ) ? value.stripTrailingZeros() : value;
    // Unscale only values with ten exponent
    return ( striped.scale() < 0 ) ? striped.setScale( 0 ) : striped;
}

感谢@frhack。可以编写自己的Macther类来测试类似于OrderingComparsion的内容。对于equals,简单易懂。
public static class BigDecimalEqualComparator extends TypeSafeMatcher<BigDecimal> {

    private final BigDecimal expected;

    private static final String[] comparisonDescriptions = { "less than", "equal to", "greater than" };

    public BigDecimalEqualComparator( BigDecimal expected ) {
        this.expected = expected;
    }

    @Override
    public boolean matchesSafely( BigDecimal actual ) {
        return actual.compareTo( expected ) == 0;
    }

    // You must change this
    @Override
    public void describeMismatchSafely( BigDecimal actual, Description mismatchDescription ) {
        mismatchDescription.appendValue( actual.stripTrailingZeros().toPlainString() ).appendText( " was " )
            .appendText( asText( actual.compareTo( expected ) ) ).appendText( " " )
            .appendValue( expected.stripTrailingZeros().toPlainString() );
    }

    @Override
    public void describeTo( Description description ) {
        description.appendText( "a value equal to " ).appendValue( expected.stripTrailingZeros().toPlainString() );
    }

    private static String asText( int comparison ) {
        return comparisonDescriptions[signum( comparison ) + 1];
    }
}

0
可能的解决方案:
实现一个BigDecimalToCompare作为BigDecimal的包装类:
public class BigDecimalToCompare {
private final BigDecimal bigDecimal;

public BigDecimal getBigDecimal(){
    return bigDecimal;
}

BigDecimalToCompare(BigDecimal bigDecimal){
    this.bigDecimal = bigDecimal;
}

@Override
public boolean equals(Object obj){
    return bigDecimal.compareTo(((BigDecimalToCompare)obj).getBigDecimal()) ==0;
}

@Override
public String toString(){
   return bigDecimal.toString();
}

public static BigDecimalToCompare of(BigDecimal bd){
    return new BigDecimalToCompare(bd);
}

}

测试如下:

@Test
public void test(){
    BigDecimal a = new BigDecimal("100");
    BigDecimal b = new BigDecimal("100.00");
    assertEquals(BigDecimalToCompare.of(a),BigDecimalToCompare.of(b));
}

另一种可能的解决方案:

assertEquals(new BigDecimal("150").stripTrailingZeros(),
                otherBigDecimal.stripTrailingZeros());

对我来说,创建BigDecimal的包装器很奇怪,是的,如果覆盖equals方法来比较方法并覆盖toString方法,它可以工作。第二个问题是给我一个奇怪的断言异常(带有E + n的数字)。 - Perlos
为什么你不喜欢这个包装器?看一下更新后的代码。你可以使用assertEquals(bd1.toDouble(), bd2.toDouble()),但它更加繁重。 - frhack
我不会比较双精度浮点数,因为它们的小数部分可能与我的预期值不同。你写的正是我所期望的。但必须记住这个类,并与团队分享以便知晓。所以要记住两件事。此外,如果我想要记录可读性强的日志值怎么办?我可能会通过调用stripTrailingZeros + toPlainString的静态工具方法来解决这个问题。 - Perlos
@Perlos 好的,请看一下我全新的答案。 - frhack
我查看了代码,但没有发现任何区别。可能应该使用 toPlainString() 方法来获得更易读的输出。 - Perlos
1
由于包装器覆盖了 equals() 方法,因此它也应该覆盖 hashCode() 方法。 - Paul

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