Arbitrary Precision Numbers
Time for a math lesson! At least a Java math lesson. Rounding and precision have uses, and BigDecimal is a common solution for cash, but beware hits to performance.
Join the DZone community and get the full member experience.
Join For FreeI am working on a system that involves money handling, written in Java. And as you can imagine, one of the challenges is to make sure that money is not appearing or disappearing because of the floating point arithmetic. I asked a few developers how to handle money and one common answer was to use BigDecimal for that. Therefore, I performed a few experiments on the side and discovered few things that you probably won't see after the first look into this class. In this article, I am going to show these discoveries together with the final solution I decided to go with.
Rounding Errors
Before going into the detailed explanation, let's create an example that shows what happens with floating point numbers. Imagine you want to split $200 between four people. First, two of them will get 1/3 and the other two will get 1/6. Then let's see what happens when you put these parts back together. The following code demonstrates the situation (using Lenovo Yoga, Win 8.1, Java 1.8).
double a = 200;
double b = a / 3d;
double c = b / 2d;
double d = b + b + c + c;
System.out.println(a); // 200.0
System.out.println(b); // 66.66666666666667
System.out.println(c); // 33.333333333333336
System.out.println(d); // 200.00000000000003
As you can see, numbers a and d should be equal, but they are not. Since d is greater than a, extra cash just appeared. Strictly speaking, this is not acceptable for any financial transaction, regardless of how small the difference is, and therefore a different approach is required. I choose to use BigDecimal, but it's not the only way. So let's start with that.
Construction and Equality
At the beginning, I constructed a few numbers that are mathematically equal. My expectation was that they would be equal even after wrapping them to the BigDecimal. And here is what happened.
BigDecimal a = new BigDecimal(0.2d);
BigDecimal b = new BigDecimal(String.valueOf(0.2d));
BigDecimal c = new BigDecimal("0.2");
BigDecimal d = new BigDecimal("0.20");
System.out.println(a); // 0.200000000000000011102230246251565404236316680908203125
System.out.println(b); // 0.2
System.out.println(c); // 0.2
System.out.println(d); // 0.20
System.out.println(a.equals(b)); // false
System.out.println(a.equals(c)); // false
System.out.println(a.equals(d)); // false
System.out.println(b.equals(c)); // true
System.out.println(b.equals(d)); // false
System.out.println(c.equals(d)); // false
System.out.println(a.compareTo(b)); // 1
System.out.println(a.compareTo(c)); // 1
System.out.println(a.compareTo(d)); // 1
System.out.println(b.compareTo(c)); // 0
System.out.println(b.compareTo(d)); // 0
System.out.println(c.compareTo(d)); // 0
As you can see, my expectation was wrong. If you read the Javadoc, then you can see the reasons behind it. Or, here is my three-point extract from said Javadoc.
The double constructor can be somewhat unpredictable. This has something to do with the binary representation of floating point numbers. String constructors are predictable. So if you want to get an exact number from double, then try taking the path of double -> String -> BigDecimal.
The equals method doesn't work the same way as natural ordering. This is because BigDecimal is composed of two components called unscalledValue and scale. The value represented by the number is (unscaledValue × 10-scale). Since both components are part of the equals, then
true
is returned only if numbers have the same scale (that means 0.2 and 0.20 are not equal). The HashCode method follows the contract as well (therefore you can expect different hash codes for 0.2 and 0.20).The CompareTo method works in the same way as natural ordering. Number a is just greater than others due to a different constructor.
As a resolution, I always use the constructor that accepts the String while I make sure that the number doesn't have trailing zeros. This gives me consistency.
Precision and MathContext
Simply put, precision is the number of digits representing the number from the left-most non-zero digit to the right-most digit, including the trailing zeros. The precision of 0 is 1. Signature doesn't affect the precision. Here are the examples.
System.out.println(new BigDecimal("0")); // 1
System.out.println(new BigDecimal("100")); // 3
System.out.println(new BigDecimal("-100")); // 3
System.out.println(new BigDecimal("0.0002")); // 1
System.out.println(new BigDecimal("-0.0002")); // 1
System.out.println(new BigDecimal("0.00020")); // 2
System.out.println(new BigDecimal("-0.00020")); // 2
You can see that precision of 0.0002 is 1 and the precision of 0.00020 is 2. Although these numbers are mathematically equal, they have different precision and therefore different scale components! This leads to the issue with the equals method.
Now let's explain MathContext. This is a pairing of precision and instructions of what to do if a number has more digits than the precision (some variation of rounding is performed). Here is an example of how MathContext can change the number.
System.out.println(new BigDecimal("110.25").toPlainString());
// 110.25
System.out.println(new BigDecimal("110.25",
new MathContext(10, RoundingMode.DOWN)).toPlainString());
// 110.25
System.out.println(new BigDecimal("110.25",
new MathContext(1, RoundingMode.DOWN)).toPlainString());
// 100
System.out.println(new BigDecimal("110.25",
new MathContext(1, RoundingMode.UP)).toPlainString());
// 200
System.out.println(new BigDecimal("999",
new MathContext(2, RoundingMode.UP)).toPlainString());
// 1000
As you can see, the first two numbers have enough precision to fully capture the number. Therefore, numbers are stored exactly as they are. The third number has only one digit of precision together with instructions to always round down. Therefore, value changed to 100 (100 = 1 × 102). The fourth number is the same, but the rounding is up, so you will get 200 (200 = 2 × 102). And the last number demonstrates what happens when rounding up causes an increase in the scale portion of the number. 1000 can be written with precision 1 as 1000 = 1 × 103. This is how precision and rounding works.
Next, let's look at operations and performance.
Operations
At first, BigDecimal supports only basic operations. You won't find operations like sqrt, sin, cos, log, and so on. There are published ways on StackOverflow of how to create them, but chances are that in many cases, it's not really worth to use these type of numbers. I would rather strongly consider a way to transform the task and use standard floating point numbers.
If operations are limited to the simplest ones, then, in theory, there are two types. The first type is the one that always produces a terminating decimal expansion (meaning there is always a finite number of digits to express the numbers). This is, for example, the case with addition or multiplication. Then the second type might produce non-terminating decimal expansions. For example, take division: 1/3 = 0.333... Now in order to handle all the operations in a unified way, BigDecimal exposes an API that accepts the optional MathContext parameter for all operations. This gives a universal interface to calculate everything in the required precision.
It might be tempting to limit operations to the ones that produce only terminating decimal expansion. Then everything can be exact, right? That's true. Let's just see how this is useful in real life.
Performance
To demonstrate the relation between performance and precision, I have run these two tests. The first one is right here.
public static void main(String[] args) {
BigDecimal n = new BigDecimal("0.2");
for (int i = 0; i < 30; ++i) {
n = n.pow(2);
System.out.println(i + " - " + n.precision());
}
System.out.println(n.compareTo(BigDecimal.ZERO));
}
This one produced a number with 323,228,497 digits and was greater than 0.The whole program took about 35 minutes.
Now the second test.
public static void main(String[] args) {
BigDecimal n = new BigDecimal("0.2");
for (int i = 0; i < 30; ++i) {
n = n.pow(2, new MathContext(10000, RoundingMode.DOWN));
System.out.println(i + " - " + n.precision());
}
System.out.println(n.compareTo(BigDecimal.ZERO));
}
This one produces a number with 10,000 digits, that is also greater than 0, and the whole program took about 1 second.
The examples above are different just in precision. And as you can see, precision can a have huge impact on performance. Exact precision is nice, but you might run into performance trouble. Therefore, make sure precision is reasonably controlled.
ANumber
Previous sections pointed out some issues with arbitrary precision numbers. Here, I will show you a solution I decided to use. I have been using this for a couple of months and it does the job. My core requirements are:
All calculations are exact
The equals method works as "naturally" expected
For division, there is an option to return a remainder as well
Easy interface for comparison methods (<, <=, ==, >, >=)
Operations like add, mul, and others accept double arguments as well
Having nice interface for easy-to-read printing
And here are the classes together with unit test.
/**
* Object for arbitrary precision numbers.
* This is used for exact, but simple money calculations.
* In this class precision has same meaning as in the standard java BigDecimal class.
* That means number of digits representing the number counted from to most left non zero one.
* Rounding mode is always DOWN for all operations.
*
* @author radek.hecl
*/
public final class ANumber implements Comparable<ANumber> {
/**
* Zero.
*/
public static ANumber ZERO = new ANumber("0");
/**
* Precise number representation.
*/
private BigDecimal number;
/**
* Prevents construction from outside.
*/
private ANumber() {
}
/**
* Creates new instance.
*
* @param number number
*/
private ANumber(BigDecimal number) {
this.number = number.stripTrailingZeros();
}
/**
* Creates new instance.
*
* @param s string representation
*/
private ANumber(String s) {
this.number = new BigDecimal(s).stripTrailingZeros();
}
/**
* Performs addition operation.
*
* @param x other number
* @return addition operation result
*/
public ANumber add(ANumber x) {
return new ANumber(number.add(x.number));
}
/**
* Performs addition operation.
*
* @param x other number
* @return addition operation result
*/
public ANumber add(double x) {
return add(new ANumber(String.valueOf(x)));
}
/**
* Performs subtraction operation.
*
* @param x other number
* @return addition subtraction result
*/
public ANumber sub(ANumber x) {
return new ANumber(number.subtract(x.number));
}
/**
* Performs subtraction operation.
*
* @param x other number
* @return addition subtraction result
*/
public ANumber sub(double x) {
return sub(new ANumber(String.valueOf(x)));
}
/**
* Performs multiplication operation.
*
* @param x other number
* @return addition multiplication result
*/
public ANumber mul(ANumber x) {
return new ANumber(number.multiply(x.number));
}
/**
* Performs multiplication operation.
*
* @param x other number
* @return addition multiplication result
*/
public ANumber mul(double x) {
return mul(new ANumber(String.valueOf(x)));
}
/**
* Performs division operation.
*
* @param x divisor number
* @param precision precision of the result (see the class level comment for details)
* @return division operation result
*/
public ANumber div(ANumber x, int precision) {
return new ANumber(number.divide(x.number, new MathContext(precision, RoundingMode.DOWN)));
}
/**
* Performs division operation.
*
* @param x divisor number
* @param precision precision of the result (see the class level comment for details)
* @return division operation result
*/
public ANumber div(double x, int precision) {
return div(new ANumber(String.valueOf(x)), precision);
}
/**
* Performs division operation and returns the result with remainder.
*
* @param x divisor number
* @param precision precision of the result (see the class level comment for details)
* @return division operation result
*/
public ANumberRemainderPair divWithRemainder(ANumber x, int precision) {
BigDecimal div = number.divide(x.number, new MathContext(precision, RoundingMode.DOWN));
BigDecimal rem = number.subtract(div.multiply(x.number));
return ANumberRemainderPair.create(new ANumber(div), new ANumber(rem));
}
/**
* Performs division operation and returns the result with remainder.
*
* @param x divisor number
* @param precision precision of the result (see the class level comment for details)
* @return division operation result
*/
public ANumberRemainderPair divWithRemainder(double x, int precision) {
return divWithRemainder(new ANumber(String.valueOf(x)), precision);
}
/**
* Negates this number.
*
* @return negative of this number
*/
public ANumber negate() {
return ANumber.create(number.negate());
}
/**
* Returns absolute value of this number.
*
* @return absolute value of this number
*/
public ANumber abs() {
return ANumber.create(number.abs());
}
/**
* Returns this number powered by n.
*
* @param n power
* @param precision precision of the result (see the class level comment for details)
* @return power operation result
*/
public ANumber pow(int n, int precision) {
return ANumber.create(number.pow(n, new MathContext(precision, RoundingMode.DOWN)));
}
/**
* Truncates all the fractional part of this number.
* This is similar to narrowing type from double to long.
*
* @return number without part right from the decimal point
*/
public ANumber truncDecimals() {
return new ANumber(new BigDecimal(number.toBigInteger()));
}
/**
* Returns whether this number is equal with the other number.
*
* @param x tested number
* @return true if this number is equal to the other number
*/
public boolean eq(ANumber x) {
return number.compareTo(x.number) == 0;
}
/**
* Returns whether this number is equal with the other number.
*
* @param x tested number
* @return true if this number is equal to the other number
*/
public boolean eq(double x) {
return eq(new ANumber(String.valueOf(x)));
}
/**
* Returns whether this number is greater than the other number.
*
* @param x tested number
* @return true if this number is greater than the other one
*/
public boolean g(ANumber x) {
return number.compareTo(x.number) == 1;
}
/**
* Returns whether this number is greater than the other number.
*
* @param x tested number
* @return true if this number is greater than the other one
*/
public boolean g(double x) {
return g(new ANumber(String.valueOf(x)));
}
/**
* Returns whether this number is greater or equal to the other number.
*
* @param x tested number
* @return true if this number is greater or equal to the other one
*/
public boolean geq(ANumber x) {
return number.compareTo(x.number) >= 0;
}
/**
* Returns whether this number is greater or equal to the other number.
*
* @param x tested number
* @return true if this number is greater or equal to the other one
*/
public boolean geq(double x) {
return geq(ANumber.create(String.valueOf(x)));
}
/**
* Returns whether this number is less than the other number.
*
* @param x tested number
* @return true if this number is less than the other one
*/
public boolean l(ANumber x) {
return number.compareTo(x.number) < 0;
}
/**
* Returns whether this number is less than the other number.
*
* @param x tested number
* @return true if this number is less than the other one
*/
public boolean l(double x) {
return l(ANumber.create(String.valueOf(x)));
}
/**
* Returns whether this number is less or equal to the other number.
*
* @param x tested number
* @return true if this number is less than the other one
*/
public boolean leq(ANumber x) {
return number.compareTo(x.number) <= 0;
}
/**
* Returns whether this number is less or equal to the other number.
*
* @param x tested number
* @return true if this number is less than the other one
*/
public boolean leq(double x) {
return leq(ANumber.create(String.valueOf(x)));
}
/**
* Converts this number to the full, containing everything that can be restored.
*
* @return number in it's string representation
*/
public String toFullString() {
return number.toPlainString();
}
/**
* Converts number to the string with fixed decimal digits.
*
* This method might truncate some numbers or add extra zeros at the end.
*
* @param numDecimals number of decimals, must be non negative
* @return number as a plain string with specified number of decimals
*/
public String toFixedDecimalString(int numDecimals) {
ValidationUtils.guardNotNegativeInt(numDecimals, "numDecimals must be >= 0");
String str = number.toPlainString();
String[] parts = str.split("\\.");
if (parts.length == 1) {
if (numDecimals == 0) {
return parts[0];
}
else {
return parts[0] + "." + StringUtils.repeat("0", numDecimals);
}
}
else if (parts.length == 2) {
if (numDecimals == 0) {
return parts[0];
}
else if (parts[1].length() > numDecimals) {
return parts[0] + "." + parts[1].substring(0, numDecimals);
}
else if (parts[1].length() < numDecimals) {
return str + StringUtils.repeat("0", numDecimals - parts[1].length());
}
else {
return str;
}
}
else {
throw new RuntimeException("unexpected number of '.': " + str);
}
}
/**
* Returns double representation of this number.
* Conversion might ended up by loosing the precision.
*
* @return double representation of this value
*/
public double doubleValue() {
return number.doubleValue();
}
@Override
public int compareTo(ANumber x) {
return number.compareTo(x.number);
}
@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}
@Override
public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj);
}
@Override
public String toString() {
return "ANumber[" + number.toString() + "]";
}
/**
* Creates new instance. This method is capable to parse any result of toFullString method.
*
* @param s source string
* @return created number
*/
public static ANumber create(String s) {
return new ANumber(s);
}
/**
* Creates new instance.
*
* @param number number
* @return created number
*/
public static ANumber create(double number) {
return new ANumber(new BigDecimal(String.valueOf(number)));
}
/**
* Creates new instance.
*
* @param number number
* @return created number
*/
public static ANumber create(BigDecimal number) {
return new ANumber(number);
}
}
/**
* Pair of the arbitrary number and remainder.
* This is the result after division operation.
*
* @author radek.hecl
*/
public final class ANumberRemainderPair {
/**
* Unit number.
*/
private ANumber number;
/**
* Remainder.
*/
private ANumber remainder;
/**
* Creates new instance.
*/
private ANumberRemainderPair() {
}
/**
* Guards this object to be consistent. Throws exception if this is not the case.
*/
private void guardInvariants() {
ValidationUtils.guardNotNull(number, "number cannot be null");
ValidationUtils.guardNotNull(remainder, "remainder cannot be null");
}
/**
* Returns number.
*
* @return number
*/
public ANumber getNumber() {
return number;
}
/**
* Returns remainder.
*
* @return remainder
*/
public ANumber getRemainder() {
return remainder;
}
@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}
@Override
public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj);
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
/**
* Creates new instance.
*
* @param number number
* @param remainder remainder
* @return number remainder pair
*/
public static ANumberRemainderPair create(ANumber number, ANumber remainder) {
ANumberRemainderPair res = new ANumberRemainderPair();
res.number = number;
res.remainder = remainder;
res.guardInvariants();
return res;
}
}
/**
* Test case which proves the arbitrary number.
*
* @author radek.hecl
*/
public class ANumberTest {
/**
* Creates new instance.
*/
public ANumberTest() {
}
/**
* Tests creation.
*/
@Test
public void testCreate() {
assertEquals(ANumber.create(1), ANumber.create("1"));
assertEquals(ANumber.create(-1), ANumber.create("-1"));
assertEquals(ANumber.create(1), ANumber.create("1.00"));
assertEquals(ANumber.create(1.5), ANumber.create("1.5"));
assertEquals(ANumber.create(4.95), ANumber.create("4.95"));
assertEquals(ANumber.create(new BigDecimal("1e-25")), ANumber.create("0.0000000000000000000000001"));
assertEquals(ANumber.create(new BigDecimal("1e-25")), ANumber.create("0.000000000000000000000000100000"));
//
assertEquals(ANumber.create(1).hashCode(), ANumber.create("1").hashCode());
assertEquals(ANumber.create(1).hashCode(), ANumber.create("1.00").hashCode());
assertEquals(ANumber.create(1.5).hashCode(), ANumber.create("1.5").hashCode());
assertEquals(ANumber.create(1000).hashCode(), ANumber.create("1000").hashCode());
assertEquals(ANumber.create(1000).hashCode(), ANumber.create("1.000E3").hashCode());
assertEquals(ANumber.create(new BigDecimal("1e-25")).hashCode(), ANumber.create("0.0000000000000000000000001").hashCode());
assertEquals(ANumber.create(new BigDecimal("1e-25")).hashCode(), ANumber.create("0.000000000000000000000000100000").hashCode());
assertEquals(ANumber.create(1000), ANumber.create("1000"));
assertEquals(ANumber.create(1000), ANumber.create("1.000E3"));
assertFalse(ANumber.create(1).hashCode() == ANumber.create("1.5").hashCode());
}
/**
* Tests addition.
*/
@Test
public void testAdd() {
assertEquals(ANumber.create(2), ANumber.create(1).add(ANumber.create("1")));
// double version
assertEquals(ANumber.create(2), ANumber.create(1).add(1));
}
/**
* Tests subtraction.
*/
@Test
public void testSub() {
assertEquals(ANumber.create(2), ANumber.create(3).sub(ANumber.create(1)));
// double version
assertEquals(ANumber.create(2), ANumber.create(3).sub(1));
}
/**
* Tests multiplication.
*/
@Test
public void testMul() {
assertEquals(ANumber.create(6), ANumber.create(3).mul(ANumber.create(2)));
// double version
assertEquals(ANumber.create(6), ANumber.create(3).mul(2));
}
/**
* Tests division.
*/
@Test
public void testDiv() {
assertEquals(ANumber.create("1.5"), ANumber.create(3).div(ANumber.create(2), 2));
assertEquals(ANumber.create("0.66"), ANumber.create(2).div(ANumber.create(3), 2));
assertEquals(ANumber.create("6.6"), ANumber.create(20).div(ANumber.create(3), 2));
assertEquals(ANumber.create("0.6666666666"), ANumber.create(2).div(ANumber.create("3"), 10));
// double version
assertEquals(ANumber.create("1.5"), ANumber.create(3).div(2, 2));
assertEquals(ANumber.create("0.66"), ANumber.create(2).div(3, 2));
assertEquals(ANumber.create("6.6"), ANumber.create(20).div(3, 2));
assertEquals(ANumber.create("0.6666666666"), ANumber.create(2).div(3, 10));
}
/**
* Tests division with remainder.
*/
@Test
public void testDivWithRemainder() {
assertEquals(ANumberRemainderPair.create(ANumber.create("1.5"), ANumber.create("0")),
ANumber.create(3).divWithRemainder(ANumber.create(2), 2));
assertEquals(ANumberRemainderPair.create(ANumber.create("0.66"), ANumber.create("0.02")),
ANumber.create(2).divWithRemainder(ANumber.create(3), 2));
assertEquals(ANumberRemainderPair.create(ANumber.create("6.6"), ANumber.create("0.2")),
ANumber.create(20).divWithRemainder(ANumber.create(3), 2));
assertEquals(ANumberRemainderPair.create(ANumber.create("0.6666666666"), ANumber.create("0.0000000002")),
ANumber.create(2).divWithRemainder(ANumber.create(3), 10));
// double version
assertEquals(ANumberRemainderPair.create(ANumber.create("1.5"), ANumber.create("0")),
ANumber.create(3).divWithRemainder(2, 2));
assertEquals(ANumberRemainderPair.create(ANumber.create("0.66"), ANumber.create("0.02")),
ANumber.create(2).divWithRemainder(3, 2));
assertEquals(ANumberRemainderPair.create(ANumber.create("6.6"), ANumber.create("0.2")),
ANumber.create(20).divWithRemainder(3, 2));
assertEquals(ANumberRemainderPair.create(ANumber.create("0.6666666666"), ANumber.create("0.0000000002")),
ANumber.create(2).divWithRemainder(3, 10));
}
/**
* Tests negation.
*/
@Test
public void testNegate() {
assertEquals(ANumber.create(0), ANumber.create(0).negate());
assertEquals(ANumber.create(-1), ANumber.create(1).negate());
assertEquals(ANumber.create(1), ANumber.create(-1).negate());
}
/**
* Tests absolute value.
*/
@Test
public void testAbs() {
assertEquals(ANumber.create(0), ANumber.create(0).abs());
assertEquals(ANumber.create(1), ANumber.create(1).abs());
assertEquals(ANumber.create(1), ANumber.create(-1).abs());
}
/**
* Tests powering value.
*/
@Test
public void testPow() {
assertEquals(ANumber.create(0), ANumber.create(0).pow(3, 2));
assertEquals(ANumber.create(1), ANumber.create(1).pow(3, 2));
assertEquals(ANumber.create(8), ANumber.create(2).pow(3, 2));
assertEquals(ANumber.create(15.62), ANumber.create(2.50).pow(3, 4));
assertEquals(ANumber.create(-15.625), ANumber.create(-2.50).pow(3, 10));
}
/**
* Tests decimal truncation.
*/
@Test
public void testTruncDecimals() {
assertEquals(ANumber.create("1"), ANumber.create("1").truncDecimals());
assertEquals(ANumber.create("1"), ANumber.create("1.5").truncDecimals());
assertEquals(ANumber.create("10"), ANumber.create("10").truncDecimals());
assertEquals(ANumber.create("10"), ANumber.create("10.5").truncDecimals());
assertEquals(ANumber.create("-1"), ANumber.create("-1").truncDecimals());
assertEquals(ANumber.create("-1"), ANumber.create("-1.5").truncDecimals());
assertEquals(ANumber.create("-10"), ANumber.create("-10").truncDecimals());
assertEquals(ANumber.create("-10"), ANumber.create("-10.5").truncDecimals());
}
/**
* Tests equal operator.
*/
@Test
public void testEq() {
assertTrue(ANumber.create(0).eq(ANumber.ZERO));
assertFalse(ANumber.create(1).eq(ANumber.ZERO));
// double version
assertTrue(ANumber.create(0).eq(0));
assertFalse(ANumber.create(1).eq(0));
}
/**
* Tests greater operator.
*/
@Test
public void testG() {
assertTrue(ANumber.create(1).g(ANumber.create(0)));
assertFalse(ANumber.create(0).g(ANumber.create(0)));
assertFalse(ANumber.create(-1).g(ANumber.create(0)));
// double version
assertTrue(ANumber.create(1).g(0));
assertFalse(ANumber.create(0).g(0));
assertFalse(ANumber.create(-1).g(0));
}
/**
* Tests greater or equal operator.
*/
@Test
public void testGeq() {
assertTrue(ANumber.create(1).geq(ANumber.create(0)));
assertTrue(ANumber.create(0).geq(ANumber.create(0)));
assertFalse(ANumber.create(-1).geq(ANumber.create(0)));
// double version
assertTrue(ANumber.create(1).geq(0));
assertTrue(ANumber.create(0).geq(0));
assertFalse(ANumber.create(-1).geq(0));
}
/**
* Tests less operator.
*/
@Test
public void testL() {
assertFalse(ANumber.create(1).l(ANumber.create(0)));
assertFalse(ANumber.create(0).l(ANumber.create(0)));
assertTrue(ANumber.create(-1).l(ANumber.create(0)));
// double version
assertFalse(ANumber.create(1).l(0));
assertFalse(ANumber.create(0).l(0));
assertTrue(ANumber.create(-1).l(0));
}
/**
* Tests less or equal operator.
*/
@Test
public void testLeq() {
assertFalse(ANumber.create(1).leq(ANumber.create(0)));
assertTrue(ANumber.create(0).leq(ANumber.create(0)));
assertTrue(ANumber.create(-1).leq(ANumber.create(0)));
// double version
assertFalse(ANumber.create(1).leq(0));
assertTrue(ANumber.create(0).leq(0));
assertTrue(ANumber.create(-1).leq(0));
}
/**
* Tests conversion to full string.
*/
@Test
public void testToFullString() {
assertEquals("1.5", ANumber.create("1.5").toFullString());
assertEquals("0.0000000000000000000000001", ANumber.create("1E-25").toFullString());
}
/**
* Tests conversion to the string with fixed decimal points.
*/
@Test
public void testToFixedDecimalString() {
assertEquals("1", ANumber.create("1").toFixedDecimalString(0));
assertEquals("1", ANumber.create("1.5").toFixedDecimalString(0));
assertEquals("1.00", ANumber.create("1").toFixedDecimalString(2));
assertEquals("1.50", ANumber.create("1.5").toFixedDecimalString(2));
assertEquals("1.50", ANumber.create("1.50").toFixedDecimalString(2));
assertEquals("1.50", ANumber.create("1.500").toFixedDecimalString(2));
assertEquals("0.0000000000000000000000001", ANumber.create("1E-25").toFixedDecimalString(25));
assertEquals("0.00", ANumber.create("1E-25").toFixedDecimalString(2));
assertEquals("-1", ANumber.create("-1").toFixedDecimalString(0));
assertEquals("-1", ANumber.create("-1.5").toFixedDecimalString(0));
assertEquals("-1.00", ANumber.create("-1").toFixedDecimalString(2));
assertEquals("-1.50", ANumber.create("-1.5").toFixedDecimalString(2));
assertEquals("-1.50", ANumber.create("-1.50").toFixedDecimalString(2));
assertEquals("-1.50", ANumber.create("-1.500").toFixedDecimalString(2));
assertEquals("-0.0000000000000000000000001", ANumber.create("-1E-25").toFixedDecimalString(25));
assertEquals("-0.00", ANumber.create("-1E-25").toFixedDecimalString(2));
}
/**
* Tests compare to method.
*/
@Test
public void testCompareTo() {
assertEquals(1, ANumber.create(1).compareTo(ANumber.create(0)));
assertEquals(0, ANumber.create(0).compareTo(ANumber.create(0)));
assertEquals(-1, ANumber.create(-1).compareTo(ANumber.create(0)));
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
}
Feel free to take the pieces you like and use them in your project.
In order to be able compile and run this code, please use this mave dependencies.
<dependency>
<groupId>com.enterprisemath</groupId>
<artifactId>em-utils</artifactId>
<version>2.4.0</version>
</dependency>
<!-- For unit tests -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
Finally, an example from the first section would turn into the following:
ANumber a = ANumber.create(200);
ANumberRemainderPair b = a.divWithRemainder(3, 5);
ANumberRemainderPair c = b.getNumber().divWithRemainder(2, 5);
ANumber d = b.getNumber().add(b.getNumber()).
add(c.getNumber()).add(c.getNumber()).
add(b.getRemainder()).add(c.getRemainder());
System.out.println(a.toFullString());
// 200
System.out.println(b.getNumber().toFullString() +
" - " + b.getRemainder().toFullString());
// 66.666 - 0.002
System.out.println(c.getNumber().toFullString() +
" - " + c.getRemainder().toFullString());
// 33.333 - 0
System.out.println(d.toFullString());
// 200
Here, with the help of remainders, I was able to get back to the original number exactly. Of course, it is up to the application to decide what to do with reminders — options are open.
Summary
The BigDecimal class is useful in narrow scope calculations, like simple money handling. If you come into the situation when you need them, then here is what to expect.
- Be careful about some constructors, hashCode, and equals methods. They might work slightly differently than you would expect.
- Only limited operations are supported by default.
- Be aware of precision and non-terminating decimal expansion.
- There is a tradeoff between precision and required computing power.
A wrapper class can help you achieve a simple interface targetted to your needs.
Opinions expressed by DZone contributors are their own.
Comments