001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 *     http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014package org.gbif.utils.number;
015
016import java.math.BigDecimal;
017import java.text.DecimalFormat;
018import java.text.NumberFormat;
019
020import org.junit.jupiter.api.Test;
021
022import static org.junit.jupiter.api.Assertions.assertEquals;
023import static org.junit.jupiter.api.Assertions.assertFalse;
024import static org.junit.jupiter.api.Assertions.assertNotEquals;
025import static org.junit.jupiter.api.Assertions.assertTrue;
026
027/**
028 * Set of tests to illustrate the issue with double and how some problems can be solved with {@link BigDecimal}.
029 * {@link BigDecimal} is specially useful to work with base 10 type of data (money, metric measures).
030 *
031 * Be aware that {@link BigDecimal} requires more memory to work with (it's an Object) and it's also slower on
032 * computations (it's an immutable Object).
033 */
034// @Disabled("Demonstration of Java API behavior")
035public class DoubleVsBigDecimal {
036
037  /**
038   * Demonstrates that the sum of 1/10 (10 times) is not exactly equals to 1d using double.
039   */
040  @Test
041  public void testOneTenthAdditions() {
042
043    double oneInDouble = 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1;
044    assertFalse(oneInDouble == 1d); // oneInDouble value: 0.9999999999999999
045
046    // Try the same with BigDecimal
047    BigDecimal zeroPointOne = new BigDecimal("0.1");
048    BigDecimal currentValue = zeroPointOne;
049    for (int i = 0; i < 9; i++) {
050      currentValue = currentValue.add(zeroPointOne);
051    }
052    assertEquals(currentValue, new BigDecimal("1.0"));
053    assertEquals(1d, currentValue.doubleValue());
054  }
055
056  /**
057   * Demonstrates the effect of Double approximations on equality.
058   */
059  @Test
060  public void testDoubleApproximation() {
061    assertEquals(999199.1231231236, 999199.1231231235);
062  }
063
064  /**
065   * Demonstrates the possible issue with the default rounding mode of NumberFormat (RoundingMode.HALF_EVEN).
066   * This is a modified version from the blog post:
067   * https://blogs.oracle.com/CoreJavaTechTips/entry/the_need_for_bigdecimal
068   *
069   * No idea why the blog says "why does 90.045 round down to 90.04 and not up to 90.05" because it is not the case.
070   */
071  @Test
072  public void testRoundingUsingFormat() {
073
074    // Simple default DecimalFormat to keep only 2 decimals
075    // this simply demonstrates the default rounding behavior (RoundingMode.HALF_EVEN)
076    NumberFormat myFormatter = new DecimalFormat("##.##");
077    assertEquals("90.05", myFormatter.format(90.045)); // 90.045
078    assertEquals("90.14", myFormatter.format(90.145));
079    assertEquals("90.25", myFormatter.format(90.245));
080    assertEquals("90.34", myFormatter.format(90.345));
081
082    // same thing with NumberFormat for Currency
083    NumberFormat moneyFormatter = NumberFormat.getCurrencyInstance();
084    assertTrue(moneyFormatter.format(90.045).contains("90.05"));
085    assertTrue(moneyFormatter.format(90.145).contains("90.14"));
086  }
087
088  /**
089   * Example from https://en.wikipedia.org/wiki/Floating_point#Accuracy_problems
090   * Simply demonstrates that (a + b) + c is not necessarily equal to a + (b + c) using double.
091   */
092  @Test
093  public void testAdditionAssociativity() {
094    double a = 1234.567, b = 45.67834, c = 0.0004;
095
096    double t1 = (a + b) + c; // 1280.2457399999998
097    double t2 = a + (b + c); // 1280.24574
098    assertNotEquals(t2, t1, 0.0);
099
100    // Try the same with BigDecimal
101    BigDecimal a2 = new BigDecimal("1234.567"),
102        b2 = new BigDecimal("45.67834"),
103        c2 = new BigDecimal("0.0004");
104
105    // BigDecimla is immutable
106    BigDecimal t3 = a2.add(b2);
107    t3 = t3.add(c2);
108
109    BigDecimal t4 = b2.add(c2);
110    t4 = a2.add(t4);
111
112    assertEquals(t3, t4);
113    assertEquals(new BigDecimal("1280.24574"), t3);
114  }
115}