ordo-one/fixedpoint
Financial systems need exact decimal arithmetic -- binary floating-point (`Double`) cannot represent
Features
- 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
Performance
| 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.
Range and Precision
| 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).
Installation
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"),
]
)Protocol Conformances
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
Usage
Arithmetic
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").
Conversions
// 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 partString Parsing
let price: FixedPointDecimal = 99.95 // literal syntax
let parsed = FixedPointDecimal("99.95")! // failable init (runtime)
String(price) // "99.95"Rounding
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)SwiftUI Integration
// 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
)
}Codable
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)Atomic Operations
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)NaN
let nan = FixedPointDecimal.nan
nan.isNaN // true
nan == nan // true (sentinel semantics)
nan + someValue // traps (NaN is signalling)
nan.description // "nan"Overflow Handling
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 &+ bBuilding and Testing
swift build
swift test # 541 tests across 14 suitesBenchmarks
Benchmarks 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
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.
Acknowledgments
- Benchmark infrastructure powered by package-benchmark
- Entirely built with Claude Code with careful guidance and coaching
External Test Suite Validation
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.
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.
License
Apache License 2.0. See LICENSE for details.
Package Metadata
Repository: ordo-one/fixedpoint
Default branch: main
README: README.md