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).isNaN // true (propagates)
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 propagation, 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
Apache License 2.0. See LICENSE for details.