跳到正文
DEV Community· TokenPeak·· 3 小時前AI 評分50

Decimal 算術不能替你決定捨入時點

Decimal Arithmetic Cannot Choose Your Rounding Boundary

AI 導讀

這個 Python 示例指出,改用 Decimal 可精確保留十進制輸入,但不能決定金額應逐筆計算還是彙總後捨入。兩筆各 0.004 虛構 credits 的費用,逐筆捨入至小數點後兩位為 0.00,先加總再按 ROUND_HALF_UP 捨入則為 0.01。示例只驗證本地計算;實際結算須依據服務提供者的捨入單位、彙總邊界及結算規則核實。

正文

AI and affiliation disclosure: An AI agent prepared this technical exercise for TokenPeak. The rates are fictional credits, not product prices. The self-contained Python example and additional local checks were executed offline. No network request, invoice or production billing implementation was tested.

Replacing floating-point arithmetic with Decimal can preserve a decimal input precisely. It does not answer a different question: at what stage should an amount be rounded?

Consider two events whose unrounded charges are each 0.004 fictional credits. Rounding each event to two decimal places gives zero plus zero. Adding the unrounded events first gives 0.008, which rounds to 0.01 under the policy used below. Both computations use decimal arithmetic. They disagree because they implement different contracts.

Make the policy visible in the code

This complete example uses small, trusted fixture values. It is an executable demonstration of rounding placement, not a general parser for external rate tables or a billing service. The fictional unit has two display decimal places; that is an exercise choice, not a statement about any currency or provider.

from decimal import Decimal, ROUND_HALF_UP, localcontext

MILLION = Decimal("1000000")
STEP = Decimal("0.01")

def charge(tokens, rate):
    # Trusted fixture: nonnegative integer tokens and Decimal rate.
    return Decimal(tokens) * rate / MILLION

def rounded(amount):
    return amount.quantize(STEP, rounding=ROUND_HALF_UP)

with localcontext() as ctx:
    ctx.prec = 40
    events = [charge(1000, Decimal("4")) for _ in range(2)]
    per_event = sum((rounded(x) for x in events), Decimal("0"))
    after_sum = rounded(sum(events, Decimal("0")))

    assert events == [Decimal("0.004"), Decimal("0.004")]
    assert per_event == Decimal("0.00")
    assert after_sum == Decimal("0.01")
    assert rounded(Decimal("0.005")) == Decimal("0.01")
    print(per_event, after_sum)

The program prints 0.00 0.01. String construction preserves the intended decimal literals. The local precision is sufficient for these bounded fixtures, and the explicit rounding argument prevents the example from silently relying on a surrounding context's default rounding mode.

That precision setting is not proof that arbitrary inputs will be exact. Larger operands, more significant digits or different operations need a deliberate precision and error-handling policy. A production reader also needs to reject invalid types and non-finite or unsupported values before doing arithmetic. Those concerns are outside this short exercise.

Distinguish display from settlement

Suppose a dashboard displays each event as 0.00 while a separate statement aggregates unrounded amounts. Adding the display strings cannot reproduce that statement. The lost fractions are no longer present in the displayed values.

Keeping an unrounded amount and a formatted amount as distinct fields makes the difference inspectable. It still does not tell you which amount an external provider actually debits. You need that provider's rounding unit, aggregation boundary and settlement rules, followed by authorized evidence that the rules explain a real sample.

The boundary may be per event, per line item or per statement. A conversion to another unit introduces another possible rounding step. Changing the rounding mode also changes tie cases: the explicit half-up choice here is not a universal rule for money. Do not choose whichever policy makes a historical total match without documenting why that policy applies.

Test a disagreement, not just a happy total

The useful regression case is the pair of small events because the two policies produce different answers. A single whole-credit event would let both implementations pass and would not protect the intended boundary.

Keep zero events, an exact tie and a value already on the chosen quantum as additional cases. For a real application, add examples derived from its actual written contract. Fixture tests establish what the local implementation computes; they do not establish provider behavior, tax treatment or realized savings.

The design decision belongs beside the calculation: preserve precision through the agreed aggregation stage, then apply the specified rounding rule. Decimal is a tool for expressing that decision, not a substitute for making it.

來源:DEV Community · dev.to