# 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
- Separate formatting from arithmetic: Round numbers mathematically, then format results for display using
NumberFormatter. - Validation strategy: Compare formatted strings (not
Decimalvalues) when trailing zeros are required. - Domain-driven types: Create wrapper types (e.g.,
FormattedDecimal) to enforce formatting rules at compile time. - Precision annotations: Track decimal place requirements via custom types or metadata in persistence layers.
- 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
Decimalfor 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).