Contents

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
  • @frozen for cross-module inlining and optimal ContiguousArray layout
  • Pure Swift core -- no Foundation dependency except for Decimal conversions
  • 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, Double conversion, Decimal conversion, arithmetic) use banker's rounding (round half to even). Explicit rounded(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 &- b

Integer and float literals work directly via ExpressibleByIntegerLiteral and ExpressibleByFloatLiteral:

a * 100        // integer literal
a * 0.5        // float literal

Float 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 part

String 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 &+ b

Building and Testing

swift build
swift test        # 541 tests across 14 suites

Benchmarks

Benchmarks use package-benchmark and compare every operation against Foundation.Decimal:

cd Benchmarks
swift package benchmark

Metrics 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 run

The 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

External Test Suite Validation

Arithmetic correctness is validated against three established decimal test suites with zero failures across all compatible test vectors:

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.

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