BigDecimal – tính toán với độ chính xác cao

BigDecimal đại diện cho một số thập phân có độ chính xác cao. Một BigDecimal object là immutable và được chia làm 2 phần:

  • Precision- Biểu diễn tất các ký số có trong BigDecimal dưới dạng số nguyên không phân biệt phần thập phân.
  • Scale (32 bit) – Số nguyên biểu diễn số chữ số thập phân.

Ví dụ BigDecimal 3.14 có phần Precision là 314 và scale là 2.

Chúng ta sử dụng BigDecimal cho các phép tính số học đòi hỏi độ chính xác cao như các bài toán liên qua đến tiền tệ etc. 

Khởi tạo BigDecimal

Chúng ta có thể khởi tạo BigDecimal object từ String, mảng character int, long, double, BigInteger. 

BigDecimal bdFromString = new BigDecimal("0.1");

BigDecimal bdFromCharArray = new BigDecimal(new char[ {'3','.','1','6','1','5'});

BigDecimal bdlFromInt = new BigDecimal(42);

BigDecimal bdFromLong = new BigDecimal(123412345678901L);

BigInteger bigInteger = BigInteger.probablePrime(100, new Random());

BigDecimal bdFromBigInteger = new BigDecimal(bigInteger);
 

Chúng ta cũng có thể tạo BigDecimal với double, nhưng hãy cẩn trọng, xem ví dụ sau:

BigDecimal bdFromDouble = new BigDecimal(0.1d);

System.out.println(bdFromDouble.toString()); // 0.1000000000000000055511151231257827021181583404541015625

Trên mình đã khởi tạo BigDecimal với giá trị là 0.1d, mong muốn BigDecimal của mình mang giá trị 0.1 thế nhưng kết quả lại khác hoàn toàn. Đó là vì 0.1 không có giá trị chính xác trong double, BigDecimal chỉ đơn giản là lấy giá trị sai của 0.1 trong double sang và tất nhiên là giá trị cũng sẽ bị sai.

Vì vậy chúng ta nên sử dụng String để khởi tạo một giá trị double hoặc sử dụng BigDecimal.valueOf() để có giá trị chính xác như mong muốn.

BigDecimal bdFromLong1 = BigDecimal.valueOf(123412345678901L); // 123412345678901
        
BigDecimal bdFromLong2 = BigDecimal.valueOf(123412345678901L, 2); // 1234123456789.01
        
BigDecimal bdFromDouble = BigDecimal.valueOf(0.1d); // 0.1

Phép tính trong BigDecimal

BigDecimal cũng giống như các kiểu dữ liệu Number khác(Integer, Long, Double, etc) nó cung cấp đầy đủ các phép tính cộng, trừ, nhân, chia, so sánh, etc.

Kiểm tra các thành phần trong BigDecimal như unscaled, scale, sign.

int precision = bd.precision(); // 9

int scale = bd.scale(); // 4

int signum = bd.signum(); // -1

So sánh BigDecimal

compareTo()

Sử dụng compareTo() method để so sánh giữa BigDecimal với một BigDecimal khác. Method compareTo() trả về -1 nếu nhỏ hơn BigDecimal được so sánh, 0 nếu bằng và 1 nếu lớn hơn.

BigDecimal bd1 = new BigDecimal("1.0");
BigDecimal bd2 = new BigDecimal("1.00");
BigDecimal bd3 = new BigDecimal("2.0");

System.out.println(bd1.compareTo(bd2)); // 0

System.out.println(bd1.compareTo(bd3)); // -1

System.out.println(bd3.compareTo(bd1)); 1

Note: Chúng ta nhận thấy rằng compareTo() bỏ qua phần scale khi so sánh. 1.0 = 1.00

equals()

Trong khi compareTo() bỏ qua phần scale khi so sánh thì equals() sẽ chỉ trả về true khi 2 BigDecimal object bằng nhau cả về precision scale

BigDecimal bd1 = new BigDecimal("1.0");
BigDecimal bd2 = new BigDecimal("1.00");
BigDecimal bd3 = new BigDecimal("1.0");

System.out.println(bd1.equals(bd2)); // false

System.out.println(bd1.equals(bd3)); // true

Phép tính cơ bản trong BigDecimal

Cộng BigDecimal – add()

BigDecimal bd1 = new BigDecimal("1.0");
BigDecimal bd2 = new BigDecimal("2.0");

BigDecimal r = bd1.add(bd2);

Trừ BigDecimal – subtract()

BigDecimal bd1 = new BigDecimal("1.0");
BigDecimal bd2 = new BigDecimal("2.0");

BigDecimal r = bd1.subtract(bd2);

Nhân BigDecimal – multiply()

BigDecimal bd1 = new BigDecimal("1.0");
BigDecimal bd2 = new BigDecimal("2.0");

BigDecimal r = bd1.multiply(bd2);

Chia BigDecimal – divide()

BigDecimal bd1 = new BigDecimal("1.0");
BigDecimal bd2 = new BigDecimal("2.0");

BigDecimal r = bd1.divide(bd2);

Làm tròn trong BigDecimal

Làm tròn một số, đơn giản là chúng ta rút gọn nó thành một số ngắn hơn, đơn giản hơn và có ý nghĩa hơn. Ví dụ như bạn có một muốn lưu một khoảng tiền thay vì lưu $24.784917 quá dài dòng thì chúng ta có thể làm tròn nó thành $24.78 ngắn gọn, đơn giản dễ dàng xử lý hơn không =).

Để làm tròn BigDecimal chúng ta có 2 class để control các hành vi bên trong:

  • Enum RoundingMode cung cấp cho chúng ta 8 mode sau:
    • CEILING -Làm tròn lên hướng về dương vô cực.
    • FLOOR – Làm tròn xuống hướng về âm vô cực.
    • UP – Làm tròn lên hướng ra xa số không.
    • DOWN – Làm tròn xuống vòng về số không.
    • HALF_UP – Làm tròn về giá trị gần nhất. Nếu giá trị ở 2 đầu giống nhau thì tương tự UP_MODE.
    • HALF_DOWN – Làm tròn về giá trị gần nhất. Nếu giá trị ở 2 đầu giống nhau thì tương tự DOWN_MODE.
    • HAFL_EVEN –  Làm tròn về giá trị gần nhất. Nếu giá trị ở 2 đầu giống nhau thì chọn đầu giá trị chẵn.
    • UNNECESSARY – Khẳng định giá trị được yêu cầu làm tròn không cần làm tròn. Nếu giá trị được yêu cầu cần làm tròn ArithmeticException sẽ được ném ra.
  • MathContext đóng gói precisionRoundingMode

Note:

  • HAFL_EVEN là mode giảm thiểu sai lệch khi làm tròn nhiều nhất nhưng nó cũng là mode được sử dụng nhiều nhất.
  • MathContent cũng định nghĩa sẵn cho chúng ta một số MathContent object thường dùng
    • DECIMAL32 – 7 số precision và  rounding mode là HALF_EVEN
    • DECIMAL64 – 16 số precision and a rounding mode là HALF_EVEN
    • DECIMAL128 – 34 số precision và rounding mode là HALF_EVEN
    • UNLIMITED – không giới hạn số precision.

Dưới đây là bảng kết quả làm tròn với các mode cụ thể với giá trị của  precision là 1.

  Kết quả: BigDecimal r = new BigDecimal(“Input_Number”).round(new MathContext(1, Your_Rounding_Up)
Input Number UP DOWN CEILING FLOOR HALF_UP HALF_DOWN HALF_EVEN UNNECESSARY
5.5 6 5 6 5 6 5 6 ArithmeticException
2.5 3 2 3 2 3 2 2 ArithmeticException
1.6 2 1 2 1 2 2 2 ArithmeticException
1.1 2 1 1 ArithmeticException
1.0
-1.0 -1.0  -1.0  -1.0  -1.0  -1.0  -1.0  -1.0  -1.0 
-1.1 -2 -1 -1 -2 -1 -1 -1 ArithmeticException
-1.6 -2 -1 -1 -2 -2 -2 -2 ArithmeticException
-2.5 -3 -2 -2 -3 -3 -2 -2 ArithmeticException
-5.5 -6 -5 -5 -6 -6 -5 -6 ArithmeticException

Ví dụ

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;

public class Main {
    public static void main(String[] args) {
        BigDecimal bd = new BigDecimal("2.5");
        // Round to 1 digit using HALF_EVEN
        BigDecimal rounded = bd
                .round(new MathContext(1, RoundingMode.HALF_EVEN));

        System.out.println(rounded.toString());
    }
}

Output: 2

Ngoài ta chúng ta có thể sử dụng setScale() method để làm tròn. Cho một ví dụ tính tiền giá của một sản phẩm như sau

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;

public class Main {

    public static BigDecimal calculateTotalAmount(BigDecimal quantity,
                                                  BigDecimal unitPrice, BigDecimal discountRate, BigDecimal taxRate) {
        BigDecimal amount = quantity.multiply(unitPrice, MathContext.DECIMAL32);
        BigDecimal discount = amount.multiply(discountRate);
        BigDecimal discountedAmount = amount.subtract(discount);
        BigDecimal tax = discountedAmount.multiply(taxRate);
        BigDecimal total = discountedAmount.add(tax);

        // round to 2 decimal places using HALF_EVEN
        BigDecimal roundedTotal = total.setScale(2, RoundingMode.HALF_EVEN);

        return roundedTotal;

    }

    public static void main(String[] args) {
        BigDecimal quantity = new BigDecimal("4.5");
        BigDecimal unitPrice = new BigDecimal("2.69");
        BigDecimal discountRate = new BigDecimal("0.10");
        BigDecimal taxRate = new BigDecimal("0.0725");

        BigDecimal amountToBePaid = calculateTotalAmount(quantity, unitPrice, discountRate, taxRate);
        System.out.println(amountToBePaid);
    }
}

Output: 11.68

Chúng ta có thể làm tròn tại thời thực các phép tính cộng trừ, nhân chia etc.

public static void main(String[] args) {
        BigDecimal b1 = new BigDecimal("4.513");
        BigDecimal b2 = new BigDecimal("2.69313");

        BigDecimal resAdd = b1.add(b2, MathContext.DECIMAL32);
        BigDecimal resSub = b1.subtract(b2, MathContext.DECIMAL32);
        BigDecimal resMul = b1.multiply(b2, MathContext.DECIMAL32);
        BigDecimal resDiv = b1.divide(b2, MathContext.DECIMAL32);
}

Tóm lược

BigDecimal được sử dụng rất nhiều trong các ứng dụng yêu cầu độ chính xác cao như ngân hàng, bảo hiểm etc. BigDecimal gồm 2 thành phần chính là percision tổng số chữ số trong BigDecimal và scale là số chữ số phía sau dấu thập phân. 

Làm tròn BigDecimal là một vấn đề khó nên các bạn cần phải thực hành nhiều để thấy sự khác biệt khi thay đổi các tham số trong các hàm làm tròn.

Nguồn tham khảo

https://docs.oracle.com/javase/7/docs/api/java/math/RoundingMode.html

https://www.baeldung.com/java-bigdecimal-biginteger

 

3 1 vote
Article Rating
Subscribe
Notify of
guest
3 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments
3
0
Would love your thoughts, please comment.x
()
x