Chuyển đến nội dung
MINH VO Ghi chép từ công việc
của một kỹ sư tại Việt Nam
Cơ bản11 phút đọc

Dùng số thập phân chính xác khi nhập dữ liệu tài chính

Chọn giai đoạn làm tròn, giữ chuỗi tiền nguồn và phân bổ số cent theo phần dư mà vẫn bảo toàn tổng hóa đơn đã thanh toán.

Các biểu đồ giấy gồm biểu đồ phân tán, phân phối và bảng dữ liệu màu ô liu trên bàn màu be

Phân tích số tiền thập phân trực tiếp từ chuỗi nguồn và chọn rõ giai đoạn làm tròn. Kiểu Decimal giữ chính xác những giá trị như 0.1, nhưng không tự quyết định hóa đơn làm tròn từng dòng hay chỉ làm tròn tổng cuối. Hai chính sách ấy có thể tạo ra những số tiền hợp lệ khác nhau.

Với lập trình viên nhập hóa đơn vào bảng phân tích, hợp đồng cần quy định cả cách biểu diễn số lẫn quy tắc làm tròn nghiệp vụ. Một giá trị được biểu diễn chính xác vẫn có thể bị cộng ở sai giai đoạn. Đối soát đòi hỏi tái hiện chính sách của hệ thống nguồn, kể cả cách xử lý giá trị nằm đúng giữa hai mức làm tròn.

Ba khoản phí nhỏ làm lộ khác biệt chính sách

Giả sử một hóa đơn giả lập chứa ba khoản phí, mỗi khoản 0.005 đơn vị tiền tệ. Đồng tiền trong ví dụ hiển thị hai chữ số thập phân; chính sách đã chọn làm tròn trường hợp nằm giữa theo hướng ra xa số không.

from decimal import Decimal, ROUND_HALF_UP

fees = [Decimal("0.005"), Decimal("0.005"), Decimal("0.005")]
cent = Decimal("0.01")
rounded_lines = [fee.quantize(cent, rounding=ROUND_HALF_UP) for fee in fees]
line_total = sum(rounded_lines, Decimal("0.00"))
invoice_total = sum(fees, Decimal("0.00")).quantize(
    cent, rounding=ROUND_HALF_UP
)
print("round each line:", line_total)
print("round final total:", invoice_total)

Làm tròn mỗi khoản phí cho 0.01 trên mỗi dòng, tổng cộng 0.03. Cộng các khoản chính xác trước được 0.015, rồi làm tròn thành 0.02. Chênh lệch một cent đến từ chính sách, không phải lỗi biểu diễn dấu phẩy động.

Tài liệu decimal của Python mô tả cách khởi tạo, ngữ cảnh tính toán, lượng tử hóa và các chế độ làm tròn. Mã ghi rõ chế độ vì dựa vào giá trị mặc định sẽ để một phần quan trọng của hợp đồng hóa đơn ở dạng ngầm định.

Ba khoản phí chính xác 0.005 cho tổng 0.03 khi làm tròn từng dòng, nhưng cho tổng 0.02 khi chỉ làm tròn tổng một lầnMở ảnh kích thước đầy đủ ↗

Hai nhánh trong hình áp dụng giai đoạn làm tròn khác nhau lên cùng dữ liệu nguồn. Khi đối soát, cần xác định bên phát hành dùng nhánh nào trước khi kết luận một kết quả là sai.

Khởi tạo từ chuỗi trước khi chuyển sang số nhị phân

Chuyển một số dấu phẩy động nhị phân sang Decimal giữ lại giá trị xấp xỉ của số nhị phân đó. Phép chuyển không khôi phục chuỗi thập phân ban đầu. Nếu hợp đồng nguồn là số thập phân, hãy phân tích trực tiếp chuỗi nguồn.

from_text = Decimal("0.1")
from_float = Decimal(0.1)
print(from_text)
print(from_float)
print(from_text == from_float)

Giá trị thứ nhất chính xác là 0.1. Giá trị thứ hai có phần thập phân dài hơn, tương ứng với số dấu phẩy động nhị phân; phép so sánh bằng trả về false. Đây là hành vi đúng vì hàm khởi tạo nhận hai đầu vào khác nhau.

Định dạng một số float đã phân tích thành chuỗi ngắn có thể che khuất lịch sử chuyển đổi. Chuỗi ấy hữu ích để hiển thị, nhưng không chứng minh đầu vào ban đầu có độ chính xác đó. Giữ chuỗi thập phân gốc và thang đo đã khai báo khi nhập nếu việc kiểm toán hoặc tái dựng cần các thuộc tính này.

Độ chính xác cũng là giới hạn tính toán

Decimal còn biểu diễn được những giá trị đặc biệt như NaN và vô cực. Hàm khởi tạo nhận một chuỗi không có nghĩa kết quả là số tiền hóa đơn hữu hạn. Kiểm tra tính hữu hạn và số chữ số thập phân được phép trước khi tính, đồng thời giữ đường từ chối rõ ràng cho giá trị đặc biệt. Nếu chỉ kiểm tra khởi tạo thành công, một giá trị không hữu hạn có thể lọt vào và làm hỏng tổng.

Các phép tính Decimal vẫn dùng ngữ cảnh có độ chính xác hữu hạn. Khởi tạo chính xác một chuỗi không đồng nghĩa mọi phép chia tiếp theo đều có biểu diễn thập phân hữu hạn và chính xác. Chia một cho ba cần làm tròn trong mọi biểu diễn thập phân hữu hạn.

Chọn độ chính xác làm việc đủ lớn cho bước trung gian, rồi lượng tử hóa cuối cùng tại ranh giới đã quy định. Kiểm thử số tiền lớn và đầu vào có phần lẻ dài với ngữ cảnh đó. Cách làm đúng cho một hóa đơn ngắn vẫn có thể thất bại khi lô lớn vượt quá ngân sách chữ số đã giả định.

Thư viện cung cấp tín hiệu cho các trạng thái như kết quả không chính xác hoàn toàn và phép tính không hợp lệ. Quyết định trạng thái nào phải làm lần nhập thất bại, trạng thái nào là hệ quả dự kiến của bước làm tròn được ghi rõ. Theo dõi tín hiệu mà không biết giai đoạn phát sinh dễ tạo cảnh báo nhiễu; bỏ qua tất cả lại có thể giấu mất mát độ chính xác.

Đồng tiền và đơn vị thuộc về giá trị

Không thể an toàn cộng một số tiền thiếu mã tiền tệ với số tiền khác. Lưu rõ đồng tiền hoặc đơn vị tài chính và nhóm tổng theo đơn vị đó, trừ khi đã áp dụng phép quy đổi có tài liệu.

Các đơn vị khác nhau không nhất thiết cùng dùng quy tắc hai chữ số thập phân của ví dụ. Quy đổi tỷ giá có thể cần độ chính xác trung gian cao hơn khoản thanh toán cuối. Giữ riêng số tiền nguồn, tỷ giá, ngày tỷ giá và số tiền đích đã làm tròn nếu báo cáo cần giải thích phép quy đổi.

Lưu bằng số nguyên theo đơn vị tiền nhỏ nhất là lựa chọn khác khi thang đo cố định và đã biết. Cách này đơn giản hóa phép cộng chính xác ở thang đo ấy, nhưng phí có phần nhỏ hơn hoặc phép tính tỷ giá trung gian có thể cần thang đo mịn hơn. Lựa chọn phải phù hợp với các phép tính mà bộ dữ liệu cần bảo toàn.

Đối soát thành phần trước khi so tổng

Khi hóa đơn nhập lệch một cent, hãy so cách làm tròn dòng, thuế, chiết khấu và lượng tử hóa cuối trước khi đổi bộ phân tích. Ví dụ cho thấy bộ phân tích chính xác vẫn có thể cho tổng khác bên phát hành nếu chính sách tổng hợp khác nhau.

Giá trị âm cần bộ dữ liệu kiểm thử riêng. Khoản hoàn tiền nằm đúng giữa hai mức kiểm tra quy tắc có xử lý dấu như hợp đồng yêu cầu hay không. Số không, phí rất nhỏ và nhiều khoản lẻ lặp lại còn làm lộ những giả định mà hóa đơn toàn số nguyên thường không chạm tới.

Lưu chính sách tính toán với phiên bản phép biến đổi và giữ chuỗi thập phân nguồn theo quy tắc lưu giữ thích hợp. Báo cáo tài chính có thể tái lập khi người khác xác định được đầu vào chính xác, đơn vị, độ chính xác làm việc và những điểm đã làm tròn. Decimal cung cấp cách biểu diễn; hợp đồng hóa đơn xác định ý nghĩa của số cent nhận được.

Phân bổ tổng đã làm tròn mà không tạo thêm một cent

Sau khi chọn làm tròn ở cấp hóa đơn, bảng hạ nguồn vẫn có thể cần số tiền thanh toán trên từng dòng. Làm tròn riêng từng khoản phân bổ có thể tái tạo chênh lệch. Trong ví dụ ba khoản phí, tổng hóa đơn đã làm tròn là hai cent, nhưng mỗi dòng bằng nhau nếu làm tròn riêng đều thành một cent. Bên tiêu thụ không thể vừa giữ các dòng làm tròn độc lập đó vừa giữ tổng hai cent mà không có điều chỉnh rõ ràng.

Một hợp đồng có thể phân bổ tổng thanh toán bằng số nguyên theo đơn vị tiền nhỏ nhất. Gán cho mỗi dòng trọng số nguyên không âm biểu thị tỷ phần, lấy phần nguyên dưới của từng tỷ phần, rồi phân phối số đơn vị còn lại theo phần dư lớn nhất. Dùng định danh dòng ổn định để phá hòa khi phần dư bằng nhau. Đây là chính sách phân bổ được chọn cho ví dụ, không phải tuyên bố rằng mọi bên phát hành phải dùng quy tắc tiền tệ ấy.

def allocate_minor_units(total_units, weighted_lines):
    if type(total_units) is not int or total_units < 0:
        raise ValueError("nonnegative integer total required")
    ids = [line_id for line_id, weight in weighted_lines]
    if len(set(ids)) != len(ids):
        raise ValueError("line identities must be unique")
    if any(type(w) is not int or w < 0 for _, w in weighted_lines):
        raise ValueError("nonnegative integer weights required")
    weight_total = sum(w for _, w in weighted_lines)
    if weight_total == 0:
        raise ValueError("positive total weight required")
    amounts, remainders = {}, []
    for line_id, weight in weighted_lines:
        whole, remainder = divmod(total_units * weight, weight_total)
        amounts[line_id] = whole
        remainders.append((remainder, line_id))
    remaining = total_units - sum(amounts.values())
    for _, line_id in sorted(remainders, key=lambda x: (-x[0], x[1]))[:remaining]:
        amounts[line_id] += 1
    return amounts

allocated = allocate_minor_units(2, [("A", 5), ("B", 5), ("C", 5)])
print(allocated)
print("allocated units:", sum(allocated.values()))

Trọng số năm đơn vị tương ứng với các khoản phí bằng nhau được biểu diễn theo phần nghìn; thang đo chung triệt tiêu trong tỷ lệ. Mỗi tỷ phần chính xác là hai phần ba cent. Phần nguyên dưới của mỗi dòng bằng không, còn hai cent cần phân bổ. Mọi phần dư bằng nhau nên A và B nhận một cent, C nhận không. Các khoản cộng lại bằng hai, đúng tổng hóa đơn đã chọn sau làm tròn.

Hóa đơn đã làm tròn hai cent được phân bổ một cent cho A, một cent cho B và không cho C bằng trọng số bằng nhau cùng quy tắc phá hòa theo định danh; làm tròn riêng từng dòng sẽ cho ba centMở ảnh kích thước đầy đủ ↗

Hàm dùng phép chia nguyên và phần dư, không xấp xỉ số thập phân tuần hoàn hai phần ba. Hàm giả định định danh dòng ổn định và so sánh được, đồng tiền chung đã xác thực, tổng đã được lượng tử hóa theo chính sách hóa đơn. Hàm không nhận tổng âm hoặc trọng số trộn khoản ghi nợ với khoản ghi có. Những trường hợp đó cần quy tắc phân bổ riêng, không phải phép lấy trị tuyệt đối âm thầm đổi ý nghĩa kế toán.

Phân bổ bằng không không có nghĩa phí gốc của dòng C không tồn tại hoặc được miễn. Giữ phí nguồn 0.005 bên cạnh khoản thanh toán đã phân bổ. Nếu câu hỏi phân tích liên quan phí phát sinh chưa làm tròn, dùng số tiền nguồn. Nếu câu hỏi liên quan cách chia khoản thanh toán hóa đơn, dùng số phân bổ. Đặt cùng một tên chung chung cho hai cột sẽ che mất khác biệt chính sách đối với bên dùng dữ liệu tiếp theo.

Kiểm tra bảo toàn tổng và phá hòa riêng biệt

Với mỗi lần gọi mới, phân bổ một cent cho ba trọng số bằng nhau. Chỉ A nhận một cent. Đảo thứ tự danh sách đầu vào rồi xác nhận A vẫn thắng khi hòa vì định danh quyết định, không phải thứ tự đến. Phân bổ 100 cent với trọng số A bằng ba, B bằng hai và C bằng một: kết quả lần lượt là 50, 33 và 17 cent. Cent cuối thuộc về C vì phần dư của C lớn hơn B.

Kiểm tra thêm tổng bằng không với trọng số dương, định danh dòng trùng, bộ trọng số toàn không và tổng kiểu Boolean. Trường hợp đầu cho không trên mọi dòng; ba đầu vào còn lại bị từ chối. Phép cộng số nguyên chính xác giúp kiểm tra bảo toàn đơn giản, nhưng không chứng minh trọng số hoặc tổng hóa đơn lấy từ đúng phiên bản nguồn. Giữ bằng chứng ấy cùng phiên bản phân bổ.

Với quy tắc làm tròn thập phân ban đầu, thử phí hoàn tiền Decimal("-0.005"). Chế độ half-up cho âm 0.01, phù hợp với làm tròn trường hợp nằm giữa theo hướng ra xa số không. Tách thử nghiệm này khỏi hàm phân bổ không âm: một lớp hỗ trợ số tiền thập phân âm không có nghĩa mọi thuật toán hạ nguồn đều hỗ trợ.

Bản ghi đối soát lúc này giải thích được ba đại lượng: tổng chính xác chưa làm tròn, số tiền hóa đơn đã làm tròn và tổng đơn vị tiền nhỏ nhất đã phân bổ. Hai đại lượng sau phải bằng nhau theo hợp đồng phân bổ này; chênh lệch của chúng với tổng chưa làm tròn là điều chỉnh làm tròn có tài liệu. Kiểm tra từng ranh giới giúp chẩn đoán rõ hơn việc chấp nhận chênh một cent như nhiễu không thể tránh.

Nguồn tham khảo & đọc thêm

  1. Python decimal arithmetic
← Về trang bài viết

Hình minh họa

100%