Rounding decimals in Swift, while maintaining a trailing zero

# Rounding decimals in Swift, while maintaining a trailing zero

**Summary**
A functional test failed when rounding a `Decimal` value to three decimal places with a trailing zero. The rounding function correctly computed `609.660`, but Swift's `Decimal` type internally discards trailing zeros in its representation. The test incorrectly expected a `Decimal` containing explicit trailing zero metadata.

**Root Cause**
- Swift’s `Decimal` type stores values as integers scaled by a power of 10 (e.g., `609.66` and `609.660` both become `60966 * 10^-2`).
- Trailing zeros carry no mathematical significance, so `Decimal(609.660)` normalizes internally to match `Decimal(609.66)`.
- The test compared `Decimal` instances numerically, ignoring formatting semantics—since `Decimal(609.66) == Decimal(609.660)`, it failed outright.

**Why This Happens in Real Systems**
- Business rules often require formatted decimals with trailing zeros (e.g., currency `$10.70` or scientific measurements).
- Numeric types universally discard trailing zeros internally because they don’t affect arithmetic.
- Tests relying on decimal formatting become fragile when validation ignores presentation rules.

**Real-World Impact**
- Broken unit tests despite correct arithmetic logic.
- Display layer inconsistencies (e.g., UIs showing `609.66` instead of `609.660`).
- Misinterpretation of precision in regulated domains like finance or data serialization.

**Example** or Code
**Original Test Failure**:
```swift
let num = Decimal(609.660345)
let rounded = num.roundedDecimal(to: 3) // → Actually 609.66 internally
#expect(rounded == Decimal(609.660)) // Fails: 609.66 ≠ 609.660 (numerically equal, but test expects explicit formatting)

Solution Using NumberFormatter:

func formatDecimal(_ value: Decimal, fractionDigits: Int) -> String {
let formatter = NumberFormatter()
formatter.minimumFractionDigits = fractionDigits
formatter.maximumFractionDigits = fractionDigits
return formatter.string(from: value as NSDecimalNumber)!
}

// Usage in test:
let rounded = num.roundedDecimal(to: 3)
let formatted = formatDecimal(rounded, fractionDigits: 3)
#expect(formatted == "609.660") // Passes

How Senior Engineers Fix It

  1. Separate formatting from arithmetic: Round numbers mathematically, then format results for display using NumberFormatter.
  2. Validation strategy: Compare formatted strings (not Decimal values) when trailing zeros are required.
  3. Domain-driven types: Create wrapper types (e.g., FormattedDecimal) to enforce formatting rules at compile time.
  4. Precision annotations: Track decimal place requirements via custom types or metadata in persistence layers.
  5. Test structure: Unit test rounding logic mathematically, and UI/formatter outputs separately with string checks.

Why Juniors Miss It

  • Misunderstanding of IEEE decimal types: Assuming internal storage aligns visually with literal initializers.
  • Over-reliance on mathematical correctness at the expense of presentation rules.
  • Using Decimal for tasks that require formatting without formatters.
  • Lack of awareness about floating-point normalization (base-10 or base-2).
  • Unclear separation between data modeling (values) and presentation (formatting).