【问题标题】:Remove decimal ending zeros from BigDecimal从 BigDecimal 中删除小数结尾的零
【发布时间】:2017-03-16 09:26:15
【问题描述】:

我编写测试并希望将 BigDecimal 结果与预期值进行比较。 我想使用 Assert.assertEquals(BigDecimal, BigDecimal) 方法,因为如果删除它会显示精确的比较值,并且在 Eclipse 中我可以显示比较窗口。

所以在代码中我有返回 BigDecimal 的方法,四舍五入到小数点后 2 位。 在测试用例中,我现在返回没有非零十进制数字的数字。所以我想创建比例为 0 的 BigDecimal 并将返回的 BigDecimal 修剪为相同的比例。

为了更复杂,我现在有方法 getDecimal(Object [,Optionaly int scale]) 从任何具有正确 toString() 值且默认比例为 99 的对象创建 BigDecimal。我在主要的“重”代码中使用它,所以这个方法必须非常快(不需要创建另一个对象,不要使用正则表达式等)。

这么简单的问题是:如何修改 BigDecimal 实例以通过最小负载修剪结束的十进制零。

想要这样的srip

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。然后您将大大简化您的代码,并减少可能出现的错误。
  • BigDecimals 最好在未缩放时使用,除非转换为不同的类型。您不应该将代码限制为任意小数位数。你也应该使用compareTo() 而不是equals()
  • 对,更好。但不确定 PostgeSql 中的 Numeric 可以有多大,并且我们在 java 中的大多数数字都限制为精度 (17,2) 和 (15,2)。几个(?,4),所以由于乘法/除法运算,我需要一些小数保留,但不一定要有所有数字(不确定粗麻布如何处理序列化->传输多少数据)
  • 创建更多带有必要参数的方法会起作用,可能没有错误更好,但是对于每个原始类型 + 所有其他数字类型 + 所有 CharSequence 类型都有方法看起来并不好
  • 您只需要 long、double 和 String -- 其他所有内容都可以由 lib 的用户转换或由 jvm 自动转换。那是 3 种方法——几乎没有矫枉过正。另外,你并不真的需要这些方法——就像我说的那样,你不应该担心内部的有效数字,直到你需要以某种方式输出一个值,然后你应该舍入/截断它。例如。如果您进行除法,然后将结果乘以一个非常大的数字,则数字会根据被乘数的大小变得重要,过早截断数字可能会改变结果。

标签: java junit


【解决方案1】:

这个要求的official junit solution是使用hamcrest。

有了java-hamcrest 2.0.0.0,我们可以使用这个语法:

    BigDecimal a = new BigDecimal("100")
    BigDecimal b = new BigDecimal("100.00")
    assertThat(a,  Matchers.comparesEqualTo(b));

Hamcrest 1.3 Quick Reference

【讨论】:

  • 这是一个很好的测试方法。比 assertTrue(B1.compareTo(B2)) 更好,但我很伤心,如果我测试返回数字为 99 的方法,而不是异常可读性不好。
  • 写: Assert.assertThat( new BigDecimal("100.1").setScale( 99 ), Matchers.comparesEqualTo( new BigDecimal( 100 ) ) ); --- 获取:java.lang.AssertionError:预期:值等于但:
  • @Perlos 您可以轻松编写自定义 Hamcrest Matcher 并使用自定义不匹配错误消息:planetgeek.ch/2012/03/07/create-your-own-matcher(覆盖 describeMismatch 方法)
  • 是的,最好的测试是复制类 OrderingComparison,并更改方法 describeMismatchSafely。但是许可呢?
  • BSD License github.com/hamcrest/JavaHamcrest/blob/master/LICENSE.txt 您只需添加一条通知:“以二进制形式重新分发必须在提供的文档和/或其他材料中复制上述版权声明、此条件列表和以下免责声明与分布。”
【解决方案2】:

我可能找到了解决方案。创建另一个 BigDecimal 实例 2 是不好的,但不知道另一种侵入性较小的方法。如果没有必要,我会做一些优化以不创建实例。

/** 
 * 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 的测试。简单的等于

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

【讨论】:

    【解决方案3】:

    可能的解决方案:

    实现一个 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 创建包装器很奇怪,是的,如果覆盖等于方法来比较方法并覆盖 toString 方法,它会起作用。第二个给我奇怪的断言异常(带 E+n 的数字)
    • 为什么你不喜欢包装?看看更新的代码。你可以使用 assertEquals(bd1.toDouble(), bd2.toDouble()) 但它更重
    • 比较双打我不会因为浮点/分数它可能与我预期的不同。你写的正是我所期望的。但一定要记住这门课,并与团队分享以了解它。所以记住两件事。除此之外,如果我想要记录人类可读的值怎么办?我可能通过调用 stripTrailingZeros + toPlainString 的静态 util 方法来解决这个问题
    • @Perlos 好的,请看看我的新答案。
    • 由于包装器覆盖equals(),它也应该覆盖hashCode()
    猜你喜欢
    相关资源
    最近更新 更多
    热门标签