从 BigDecimal 中删除小数结尾的零

Posted

技术标签:

【中文标题】从 BigDecimal 中删除小数结尾的零【英文标题】:Remove decimal ending zeros from 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 种方法——几乎没有矫枉过正。另外,你并不真的需要这些方法——就像我说的那样,你不应该担心内部的有效数字,直到你需要以某种方式输出一个值,然后你应该舍入/截断它。例如。如果您进行除法,然后将结果乘以一个非常大的数字,则数字会根据被乘数的大小变得重要,过早截断数字可能会改变结果。 【参考方案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()

以上是关于从 BigDecimal 中删除小数结尾的零的主要内容,如果未能解决你的问题,请参考以下文章

在小数点后删除十进制数中的零

将 BigDecimal 格式化为最多 2 个十进制数字的字符串,在小数部分删除 0

2018-07-13关于BigDecimal.ROUND_DOWN丢失精度的坑

BigDecimal.setScale 处理java小数点

java中"大数除法"结果如何保留5位小数

java.math.BigDecimal 不能转换为 java.lang.String?