Skip to content

Rounding

This document specifies rounding modes and rules for plain text accounting systems.

Overview

Rounding converts a number to fewer decimal places. The rounding mode determines how to handle the discarded digits.

When Rounding Occurs

Explicit Rounding

User-requested rounding for display or calculation:

round(123.456, 2) = 123.46

Implicit Rounding

System-performed rounding due to precision limits:

1/3 = 0.333... → 0.33333333... (truncated at precision limit)

Division Results

Division may produce non-terminating decimals:

100 / 3 = 33.333...

Rounding Modes

HALF_EVEN (Banker's Rounding)

Round to nearest; if equidistant, round to even. This is the recommended default.

Input Rounded (1 decimal)
1.25 1.2 (round down to even)
1.35 1.4 (round up to even)
1.45 1.4 (round down to even)
1.55 1.6 (round up to even)

Rationale: Minimizes cumulative bias over many operations.

HALF_UP

Round to nearest; if equidistant, round away from zero.

Input Rounded (1 decimal)
1.25 1.3
1.35 1.4
-1.25 -1.3
-1.35 -1.4

HALF_DOWN

Round to nearest; if equidistant, round toward zero.

Input Rounded (1 decimal)
1.25 1.2
1.35 1.3
-1.25 -1.2
-1.35 -1.3

CEILING (Round Up)

Always round toward positive infinity.

Input Rounded (1 decimal)
1.21 1.3
1.29 1.3
-1.21 -1.2
-1.29 -1.2

FLOOR (Round Down)

Always round toward negative infinity.

Input Rounded (1 decimal)
1.21 1.2
1.29 1.2
-1.21 -1.3
-1.29 -1.3

TRUNCATE (Toward Zero)

Discard digits beyond precision (round toward zero).

Input Rounded (1 decimal)
1.29 1.2
-1.29 -1.2

AWAY_FROM_ZERO

Round away from zero.

Input Rounded (1 decimal)
1.21 1.3
-1.21 -1.3

Rounding Mode Summary

Mode 0.5 rounds to -0.5 rounds to Bias
HALF_EVEN 0 (even) 0 (even) None
HALF_UP 1 -1 Slight up
HALF_DOWN 0 0 Slight down
CEILING 1 0 Up
FLOOR 0 -1 Down
TRUNCATE 0 0 Toward zero

Default Rounding Mode

Implementations SHOULD use HALF_EVEN as the default:

  • Widely used in financial systems
  • Minimizes statistical bias
  • IEEE 754 default
  • Recommended by regulatory bodies

Context-Specific Rounding

Display Rounding

For reports and output:

1234.567 → 1234.57  (display with 2 decimals)

Original precision preserved internally.

Currency Rounding

Round to currency's minor unit:

Currency Decimals Example
USD 2 1.234 → 1.23
JPY 0 1.5 → 2
KWD 3 1.2345 → 1.235

Cost Basis Rounding

For lot cost calculations:

1000 USD / 3 shares = 333.333... USD per share

; Rounded:
= 333.33 USD per share (HALF_EVEN)

; Remainder handling may be needed

Tax Calculations

Tax jurisdictions may mandate specific rounding:

; US: Generally HALF_UP for tax
; EU: Often HALF_EVEN
; Some: TRUNCATE for tax benefit calculations

Rounding Functions

Round to Decimal Places

def round_decimal(value: Decimal, places: int, mode: RoundingMode) -> Decimal:
    """Round value to specified decimal places."""
    quantize_exp = Decimal(10) ** -places
    return value.quantize(quantize_exp, rounding=mode)

# Examples:
round_decimal(1.235, 2, HALF_EVEN) = 1.24
round_decimal(1.245, 2, HALF_EVEN) = 1.24  # Even preference

Round to Significant Figures

def round_sigfigs(value: Decimal, sigfigs: int, mode: RoundingMode) -> Decimal:
    """Round to specified significant figures."""
    ...

# Examples:
round_sigfigs(1234.5, 3, HALF_EVEN) = 1230
round_sigfigs(0.001234, 3, HALF_EVEN) = 0.00123

Rounding in Calculations

Intermediate vs. Final

Recommended: Round only final results, not intermediate values.

# Good: Full precision intermediate
result = (a * b + c * d).round(2)

# Bad: Rounding intermediates accumulates error
result = a.round(2) * b.round(2) + c.round(2) * d.round(2)

Allocation with Rounding

Distributing amounts with remainder:

Total: 100.00
Split: 3 ways
Each:  33.33, 33.33, 33.34  (handles remainder)

Algorithm:

def allocate(total: Decimal, n: int) -> List[Decimal]:
    base = (total / n).quantize(Decimal('0.01'), FLOOR)
    remainder = total - base * n
    result = [base] * n
    # Distribute remainder penny by penny
    for i in range(int(remainder * 100)):
        result[i] += Decimal('0.01')
    return result

Rounding Errors

Cumulative Error

Many small roundings can accumulate:

sum(round(1/3, 2) for _ in range(3)) = 0.99 (not 1.00)

Mitigation: Keep full precision; round only for display.

Balance Discrepancies

Rounding may cause tiny imbalances:

100 USD → split 3 ways:
  33.33 USD
  33.33 USD
  33.33 USD
  ─────────
  99.99 USD (0.01 short)

Solution: Use tolerance in balance checking or explicit remainder handling.

Configuration

Option Syntax

option "rounding_mode" "HALF_EVEN"
option "display_precision" "2"

Per-Currency Precision

option "precision" "USD:2"
option "precision" "BTC:8"
option "precision" "JPY:0"

Implementation

Python Example

from decimal import Decimal, ROUND_HALF_EVEN, ROUND_HALF_UP

def round_amount(amount: Decimal, places: int = 2) -> Decimal:
    exp = Decimal(10) ** -places
    return amount.quantize(exp, rounding=ROUND_HALF_EVEN)

Rust Example

use rust_decimal::{Decimal, RoundingStrategy};

fn round_amount(amount: Decimal, places: u32) -> Decimal {
    amount.round_dp_with_strategy(places, RoundingStrategy::MidpointNearestEven)
}

Error Messages

Precision Loss Warning

WARNING: Precision loss in calculation
  --> ledger.beancount:42:15
   |
42 |   cost: 100.00 / 3
   |         ^^^^^^^^^^
   |
   = result 33.333... rounded to 33.33

Rounding Mode Not Supported

ERROR: Unknown rounding mode
  --> ledger.beancount:5:18
   |
 5 | option "rounding_mode" "UNKNOWN"
   |                        ^^^^^^^^^
   |
   = valid modes: HALF_EVEN, HALF_UP, HALF_DOWN, CEILING, FLOOR, TRUNCATE

Cross-Format Notes

Feature Beancount Ledger hledger
Default mode HALF_EVEN HALF_UP HALF_EVEN
Configurable Limited Yes Yes
Display precision Per-commodity Per-commodity Per-commodity
Allocation Manual Built-in Manual