Financial systems need exact decimal arithmetic -- binary floating-point (Double) cannot represent
0.1 exactly, causing accumulation errors. Foundation.Decimal solves
exactness but carries inherent overhead: it is a 20-byte variable-precision type whose arithmetic
must operate on a multi-word mantissa, handle variable exponents, and manage intermediate results
that may exceed the mantissa width.
FixedPointDecimal takes a different approach: fix the precision at compile time (8 fractional
digits) and use a plain Int64 as the backing store.
let price: FixedPointDecimal = 123.45
let quantity: FixedPointDecimal = 1000
let notional = price * quantity // 123450Full API documentation is available on the Swift Package Index.
- Zero heap allocations for all arithmetic, comparison, conversion, and rounding operations
@frozenfor cross-module inlining and optimalContiguousArraylayout- Pure Swift core -- no Foundation dependency except for
Decimalconversions - Cross-platform -- Linux + all Apple platforms, x86 + ARM, Swift 6.2
- Safe by default -- trapping arithmetic (matching Swift
Int), with wrapping and overflow-reporting variants - NaN support -- sentinel-based NaN that traps all operations
- Banker's rounding everywhere -- all entry points (string parsing,
Doubleconversion,Decimalconversion, arithmetic) use banker's rounding (round half to even). Explicitrounded(scale:_:)supports six modes
| Operation | FixedPointDecimal | Foundation.Decimal | Speedup |
|---|---|---|---|
| Addition | 0.67 ns | 240 ns | 359x |
| Subtraction | 0.67 ns | 283 ns | 424x |
| Multiplication | 8 ns | 607 ns | 79x |
| Division | 8 ns | 1,285 ns | 168x |
Comparison (<) |
0.33 ns | 300 ns | 901x |
Equality (==) |
0.34 ns | 317 ns | 943x |
| Hash | 5 ns | 261 ns | 48x |
| To Double | 0.49 ns | 271 ns | 551x |
| String description | 44 ns | 1,045 ns | 24x |
| JSON encode | 320 ns | 1,215 ns | 3.8x |
| JSON decode | 457 ns | 831 ns | 1.8x |
init(significand:exponent:) |
0.41 ns | — | — |
init(Double) |
1.4 ns | 2,319 ns | 1,622x |
rounded(scale:) |
2 ns | 705 ns | 349x |
Zero heap allocations across all operations. 8 bytes in-memory and on the wire (vs 20 for Decimal).
Measured on Apple M4 Max, Swift 6.2, p50 wall clock, using package-benchmark. See the full performance analysis for instruction counts, allocation breakdowns, and memory layout details.
| Property | Value |
|---|---|
| Fractional digits | 8 (fixed) |
| Minimum value | -92,233,720,368.54775807 |
| Maximum value | 92,233,720,368.54775807 |
| Smallest positive | 0.00000001 |
| Storage | @frozen struct, Int64 (8 bytes) |
Eight fractional digits cover all practical financial instruments: cents (2), mils (3), basis
points (4), FX pips (5), and cryptocurrency satoshis (8). The range (~92 billion) is sufficient
for individual prices and quantities but may require Int128 backing for aggregated notional
values (see 06-future-128bit.md).
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/ordo-one/FixedPoint.git", from: "1.0.0"),
]Then add the dependency to your target:
.target(
name: "MyTarget",
dependencies: [
.product(name: "FixedPointDecimal", package: "FixedPoint"),
]
)Core (all platforms):
Sendable, BitwiseCopyable, AtomicRepresentable, Equatable, Hashable, Comparable,
AdditiveArithmetic, Numeric, SignedNumeric, Strideable,
ExpressibleByIntegerLiteral, ExpressibleByFloatLiteral, Codable,
CustomStringConvertible, CustomDebugStringConvertible, LosslessStringConvertible,
CustomReflectable
SwiftUI (macOS/iOS):
VectorArithmetic, Plottable
Foundation (all platforms):
Decimal.FormatStyle forwarding (.number, .percent, .currency(code:) and all modifiers),
plus a dedicated FixedPointDecimalFormatStyle conforming to FormatStyle and ParseableFormatStyle
let a: FixedPointDecimal = 10.25
let b: FixedPointDecimal = 3
a + b // 13.25
a - b // 7.25
a * b // 30.75
a / b // 3.41666667 (banker's rounding)
a % b // 1.25
// Wrapping (when you need non-trapping overflow — not faster than checked operators)
a &+ b
a &- bInteger and float literals work directly via ExpressibleByIntegerLiteral and ExpressibleByFloatLiteral:
a * 100 // integer literal
a * 0.5 // float literalFloat literals go through Double, but this is safe for all values within FixedPointDecimal's
8-digit fractional range — values like 0.1, 0.2, 0.3 all convert correctly because
round(value * 10^8) produces the exact integer. For values with more than 15 total significant
digits, use the string initializer: FixedPointDecimal("12345678901.12345678").
// From/to Double (pure Swift, no Foundation)
let fromDouble: FixedPointDecimal = 123.45
let toDouble = Double(fromDouble) // 123.45
// From/to Foundation.Decimal (for UI presentation)
let fromDecimal = FixedPointDecimal(someDecimal)
let toDecimal = Decimal(fromDecimal)
// Failable exact conversions (nil if not exactly representable)
let exact = Double(exactly: someFixedPoint) // Optional(123.45)
let intVal = Int(exactly: someFixedPoint) // nil if has fractional part
// Truncating integer conversions (matching Int(someDouble) semantics)
let truncated = Int(someFixedPoint) // truncates fractional partlet price: FixedPointDecimal = 99.95 // literal syntax
let parsed = FixedPointDecimal("99.95")! // failable init (runtime)
String(price) // "99.95"let value: FixedPointDecimal = 123.456789
value.rounded() // 123 (integer rounding, banker's)
value.rounded(scale: 2) // 123.46 (banker's rounding)
value.rounded(scale: 2, .towardZero) // 123.45
value.rounded(scale: 0, .up) // 124
value.rounded(scale: 0, .toNearestOrAwayFromZero) // 123 (schoolbook rounding)// FormatStyle for TextField binding
TextField("Price", value: $price, format: .fixedPointDecimal)
price.formatted(.fixedPointDecimal.precision(2)) // "123.46"
// Decimal.FormatStyle forwarding — full locale-aware formatting
price.formatted(.number) // "123.45" (default locale)
price.formatted(.number.locale(Locale(identifier: "de_DE"))) // "123,45"
price.formatted(.currency(code: "USD")) // "$123.45"
price.formatted(.currency(code: "SEK")) // "123,45 kr"
price.formatted(.percent) // "12,345%"
// VectorArithmetic — animated transitions work automatically
struct PriceView: View {
var price: FixedPointDecimal
var body: some View {
Text(price, format: .fixedPointDecimal.precision(2))
}
}
// Plottable — direct use in Swift Charts
Chart(data) { item in
LineMark(
x: .value("Time", item.timestamp),
y: .value("Price", item.price) // FixedPointDecimal
)
}Encodes as a human-readable JSON string. Decodes flexibly from String, integer, or floating-point JSON values:
// Encoding: always a string for precision safety
let data = try JSONEncoder().encode(price) // "123.45"
// Decoding: accepts multiple formats for interoperability
// "123.45" -- string (canonical)
// 123 -- integer (face value, not raw storage)
// 123.45 -- floating-point (from external APIs)
let decoded = try JSONDecoder().decode(FixedPointDecimal.self, from: data)AtomicRepresentable enables lock-free atomic operations via the Synchronization module:
import Synchronization
let bestBid = Atomic<FixedPointDecimal>(FixedPointDecimal(100.50))
bestBid.store(FixedPointDecimal(100.55), ordering: .releasing)
let current = bestBid.load(ordering: .acquiring)let nan = FixedPointDecimal.nan
nan.isNaN // true
nan == nan // true (sentinel semantics)
nan + someValue // traps (NaN is signalling)
nan.description // "nan"Default operators trap on overflow, matching Swift Int:
// Trapping (default -- catches bugs in development)
let result = a + b // traps if overflow
// Overflow-reporting (for defensive checks)
let (value, overflow) = a.addingReportingOverflow(b)
// Wrapping (non-trapping overflow — not faster than checked operators)
let wrapped = a &+ bswift build
swift test # 541 tests across 14 suitesBenchmarks use package-benchmark and compare
every operation against Foundation.Decimal:
cd Benchmarks
swift package benchmarkMetrics collected: wall clock time, CPU instructions, heap allocations (malloc count).
Fuzz testing uses libFuzzer via Swift's -sanitize=fuzzer
flag. This requires the open-source Swift toolchain on Linux (not available in Xcode on macOS).
# Build only
bash Fuzz/run.sh
# Build and run (Ctrl-C to stop)
bash Fuzz/run.sh run
# Run for 60 seconds
bash Fuzz/run.sh run -max_total_time=60
# Debug build (for lldb)
bash Fuzz/run.sh debug runThe fuzzer validates invariants across all operations:
- Arithmetic: commutativity, NaN trapping, no silent NaN sentinel creation
- Comparisons: strict total order (exactly one of
<,==,>) - Conversions: String, Double, Decimal, Codable round-trips
- Rounding: scale-8 identity, no overflow
- Hashing: equal values produce equal hashes
Crash artifacts are saved as Fuzz/crash-* files for reproduction.
- Benchmark infrastructure powered by package-benchmark
- Entirely built with Claude Code with careful guidance and coaching
Arithmetic correctness is validated against three established decimal test suites with zero failures across all compatible test vectors:
- General Decimal Arithmetic (Mike Cowlishaw / IBM) -- 2,492 vectors passed across 12 operations (add, subtract, multiply, divide, remainder, compare, abs, negate, min, max, plus) from 81,300+ total vectors. ICU License.
- Fahmy Arithmetic Test Vectors (Cairo University) -- 157 vectors passed for add, multiply, divide. These directed vectors found bugs in IBM decNumber and Intel's decimal library. Permissive with attribution.
- Intel Decimal Floating-Point Math Library -- 37 vectors passed for add, subtract, abs, negate with BID64 hex decoding. BSD 3-Clause License.
Vectors are skipped (never failed) when they fall outside our type's domain: values exceeding our range (~92 billion), operands needing more than 8 fractional digits, infinity/NaN arithmetic (we trap), overflow/underflow conditions, or non-half_even rounding on multiply/divide (our arithmetic uses banker's rounding). Rounding-independent operations (add, subtract, compare, abs, negate, min, max) run with all rounding modes since the result is exact. See UPDATING.md for how to refresh the test suites.
Apache License 2.0. See LICENSE for details.