SfSkills Domains Docs GitHub

Home / apex / apex-decimal-arithmetic-precision

apex-decimal-arithmetic-precision

Use when Apex code performs arithmetic on currency, tax, percentage, or quantity values and the result is wrong by a cent, a penny, or a fraction — Decimal scale collapses to 0 after divide, totals don't match the sum-of-line-items, currency-field rounding differs from in-app calculation, or `divide()` throws on a non-existent rounding mode.

Open the skill folder on GitHub


Apex Decimal Arithmetic Precision

Activate when an Apex calculation produces a value that is wrong by a cent, by a fraction, or — more dangerously — looks correct in unit tests but disagrees with a hand-totalled spreadsheet by enough to fail an audit. The skill resolves the calculation by giving Decimal an explicit scale and rounding mode at every step where rounding can occur, separating the "calculate" path from the "store" path, and matching Apex's behavior to the platform's currency-field behavior.


Before Starting

Gather this context before working on anything in this domain:


Core Concepts

Apex Decimal is fixed-precision, scale-tracking

Apex's Decimal type is a fixed-point decimal — internally a Java BigDecimal. Every Decimal value carries a scale (number of digits after the decimal point). Scale is preserved through addition, subtraction, and multiplication. Division is the operation where Apex requires you to declare what scale the result should have, because most divisions don't terminate in finite digits.

A Decimal literal in Apex source picks up its scale from how you wrote it:

Once the value is in a Decimal variable, scale is "sticky" across arithmetic in predictable ways:

Division requires divide(divisor, scale, roundingMode) for non-terminating results

Decimal.divide(Decimal divisor) (single-argument) throws System.MathException: Division does not result in an exact result, set scale and rounding mode for an inexact result when the result has more digits than the result's natural scale can hold. 1.0 / 3 blows up; 1.0 / 4 == 0.25 works.

Always prefer the three-argument form for any production calculation:

Decimal result = numerator.divide(denominator, 4, System.RoundingMode.HALF_UP);

The two-argument divide(divisor, scale) exists but uses HALF_EVEN (banker's rounding) silently — fine if that's what you want, dangerous if you assumed HALF_UP.

Rounding modes that matter

ModeBehaviorUse for
HALF_UP0.5 rounds away from zeroRetail prices, most user-facing money
HALF_EVEN (banker's)0.5 rounds to even neighborTax, GAAP/IFRS reporting, statistics
HALF_DOWN0.5 rounds toward zeroRare; specific contractual rounding
UP / DOWNAlways away / toward zeroTruncation; commission caps
CEILING / FLOORAlways up / down (signed)Inventory ceiling, never-overcharge
UNNECESSARYThrows if rounding actually neededUse to prove no rounding loss occurred

The platform's currency-field UI uses HALF_EVEN for display rounding. If your Apex uses HALF_UP and writes to a Currency field, the value persists as you wrote it but a downstream formula that re-rounds via the platform may shift it back. Match the rounding mode end-to-end or accept the difference.

Scale of currency, percent, and number fields

When you write a higher-scale Decimal into a field with smaller scale, the platform truncates without an explicit rounding mode at the DML boundary. To control rounding, call setScale(fieldScale, RoundingMode.X) before assignment.


Common Patterns

Calculating a line-item total with tax

public class LineItemCalc {
    public static Decimal totalWithTax(Decimal unitPrice, Integer qty, Decimal taxRatePercent) {
        // Force Decimal arithmetic — qty alone is Integer.
        Decimal subtotal = unitPrice * qty;
        Decimal tax = (subtotal * taxRatePercent).divide(100, 6, System.RoundingMode.HALF_EVEN);
        Decimal total = (subtotal + tax).setScale(2, System.RoundingMode.HALF_EVEN);
        return total;
    }
}

Notes: intermediate tax carries scale 6 to avoid double-rounding; the outer setScale(2, HALF_EVEN) aligns to the currency-field display behavior; qty (Integer) is implicitly promoted by unitPrice * qty because unitPrice is already Decimal.

Splitting a total across allocations without rounding drift

The classic trap: total / N rounded to 2 places, then summed back, drifts by 0.01 × (rounding-direction × N). Fix by allocating cumulatively and assigning the residual to the last item:

public static List<Decimal> allocate(Decimal total, Integer parts) {
    List<Decimal> out = new List<Decimal>();
    Decimal each = total.divide(parts, 2, System.RoundingMode.HALF_DOWN);
    Decimal running = 0;
    for (Integer i = 0; i < parts - 1; i++) {
        out.add(each);
        running += each;
    }
    out.add((total - running).setScale(2, System.RoundingMode.HALF_UP));
    return out;
}

The list always sums exactly to total. Useful for revenue allocation, invoice splits, and percentage-of-total reporting.


Decision Guidance

SituationChoiceRationale
Storing into a currency fieldsetScale(2, HALF_EVEN) before DMLMatches platform display rounding
User-visible price (retail)setScale(2, HALF_UP)Matches consumer expectation
Tax calculationHALF_EVENRequired by most tax authorities; defensible in audit
Cumulative average / weighted avgCarry scale 6+ in intermediates, round at endAvoid double-rounding error compounding
1 / 3-style divisionThree-arg divide(d, scale, mode)One-arg form throws on non-terminating
Equality compareRound both sides to same scale first1.50 == 1.5 is true in Apex but Decimal.valueOf('1.50').equals(1.5) is false

  1. Identify every division in the calculation. For each, decide the result scale and rounding mode based on the destination field — write them down before writing code.
  2. Identify every Integer literal (1, 100, qty) being divided or multiplied. If the operation could produce a fraction, change the literal to a Decimal (1.0, 100.0) or wrap with Decimal.valueOf().
  3. Carry intermediates at scale ≥ 4 even when the final answer is scale 2. Round only at the boundary (UI display, DML write, contract output).
  4. Replace any one-argument divide(d) with the three-argument form. Search the codebase for \.divide\([^,]+\) to find single-arg divides.
  5. Add tests for the boundary cases: zero divisor (expect MathException), exact-terminating divide (1.00 / 4), repeating divide (1.00 / 3), half-way value with both HALF_UP and HALF_EVEN to confirm the chosen mode.
  6. If the calculation feeds a Currency field, write a final setScale(2, HALF_EVEN) and confirm the stored value matches a hand calculation.
  7. Where the calculation is reused, lift it into a LineItemCalc-style stateless utility class with a single rounding-mode parameter so the policy is one place, not seven.

Review Checklist


Salesforce-Specific Gotchas

GotchaBehavior
Currency fields in multi-currency orgsDecimal reads from a Currency field carry the record's currency, not the user's. Apex doesn't auto-convert; use UserInfo.getDefaultCurrency() and the DatedConversionRate SObject if conversion is needed.
Decimal.valueOf(Double)Passes through Java double's binary float, reintroducing 0.1 + 0.2 = 0.30000000000000004 errors. Use Decimal.valueOf(String) or a Decimal literal.
Aggregate query typesSUM(Amount__c) returns Decimal in Apex but AVG() always returns scale 6 regardless of source field. Round explicitly when consuming.
Formula-field evaluationCross-object formulas evaluate at platform-level with HALF_EVEN; if Apex pre-computed the same value with HALF_UP, expect a 1-cent shift.
== vs .equals()Apex == on Decimal is value-equal (1.50 == 1.5 is true); .equals() is scale-strict. Use == for "same number" semantics.

Output Artifacts


Package files