Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion .swiftlint_refinement.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
# https://realm.github.io/SwiftLint/rule-directory.html
disabled_rules:
# - some_rule_to_disable
- trailing_comma
- file_length
- type_body_length
- function_body_length
- cyclomatic_complexity
- for_where
- shorthand_operator
- redundant_discardable_let
- sorted_imports
- extension_access_modifier
- vertical_whitespace_opening_braces
- attributes
- file_name
- legacy_multiple
- identical_operands
- array_init
- non_optional_string_data_conversion
- prefer_self_in_static_references
- number_separator

identifier_name:
min_length: 1
allowed_symbols: ['_']
excluded:
- id
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,30 @@ let benchmarks: @Sendable () -> Void = {
blackHole(accumulated)
}

// MARK: - pow(10, n) vs pow(x, n)

Benchmark("FixedPointDecimal pow(10, n)", configuration: defaultConfiguration) { benchmark in
let ten: FixedPointDecimal = 10
let exponents = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
var i = 0

for _ in benchmark.scaledIterations {
blackHole(FixedPointDecimal.pow(ten, exponents[i % exponents.count]))
i &+= 1
}
}

Benchmark("FixedPointDecimal pow(2, n)", configuration: defaultConfiguration) { benchmark in
let two: FixedPointDecimal = 2
let exponents = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
var i = 0

for _ in benchmark.scaledIterations {
blackHole(FixedPointDecimal.pow(two, exponents[i % exponents.count]))
i &+= 1
}
}

// MARK: - Construction: from Double

Benchmark("FixedPointDecimal init(Double)", configuration: defaultConfiguration) { benchmark in
Expand Down
9 changes: 9 additions & 0 deletions Sources/FixedPointDecimal/FixedPointDecimal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,15 @@ public struct FixedPointDecimal: Sendable, BitwiseCopyable {
if n == 0 { return FixedPointDecimal(rawValue: scaleFactor) } // 1
if n == 1 { return x }

// Fast path: pow(10, n) via lookup table — O(1) instead of n-1 multiplications.
if x._storage == 10 &* scaleFactor {
let shift = fractionalDigitCount + n
if shift >= 0, shift < _pow10Table.count {
return FixedPointDecimal(rawValue: _pow10Table[shift])
}
return shift < 0 ? .zero : .nan
}

if n < 0 {
let positive = pow(x, -n)
if positive.isNaN || positive == .zero { return .nan }
Expand Down
68 changes: 66 additions & 2 deletions Tests/FixedPointDecimalTests/PowTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,71 @@ struct PowTests {
func significandExponentPrecisionLoss() {
// exponent -10 means shift = 8 + (-10) = -2, so divide by 100
// 12345 / 100 = 123 (truncated) → 0.00000123
let v = FixedPointDecimal(significand: 12345, exponent: -10)
#expect(v == FixedPointDecimal(rawValue: 123))
let value = FixedPointDecimal(significand: 12345, exponent: -10)
#expect(value == FixedPointDecimal(rawValue: 123))
}

// MARK: - pow(10, n) fast path

@Test("pow(10, n) positive exponents via lookup table")
func pow10Positive() {
let ten: FixedPointDecimal = 10
#expect(FixedPointDecimal.pow(ten, 0) == 1)
#expect(FixedPointDecimal.pow(ten, 1) == 10)
#expect(FixedPointDecimal.pow(ten, 2) == 100)
#expect(FixedPointDecimal.pow(ten, 3) == 1000)
#expect(FixedPointDecimal.pow(ten, 4) == 10000)
#expect(FixedPointDecimal.pow(ten, 5) == 100000)
#expect(FixedPointDecimal.pow(ten, 6) == 1000000)
#expect(FixedPointDecimal.pow(ten, 7) == 10000000)
#expect(FixedPointDecimal.pow(ten, 8) == 100000000)
#expect(FixedPointDecimal.pow(ten, 9) == 1000000000)
#expect(FixedPointDecimal.pow(ten, 10) == 10000000000)
}

@Test("pow(10, n) negative exponents via lookup table")
func pow10Negative() {
let ten: FixedPointDecimal = 10
#expect(FixedPointDecimal.pow(ten, -1) == 0.1)
#expect(FixedPointDecimal.pow(ten, -2) == 0.01)
#expect(FixedPointDecimal.pow(ten, -3) == 0.001)
#expect(FixedPointDecimal.pow(ten, -4) == 0.0001)
#expect(FixedPointDecimal.pow(ten, -5) == 0.00001)
#expect(FixedPointDecimal.pow(ten, -6) == 0.000001)
#expect(FixedPointDecimal.pow(ten, -7) == 0.0000001)
#expect(FixedPointDecimal.pow(ten, -8) == 0.00000001)
}

@Test("pow(10, -9) below representable precision returns zero")
func pow10BelowPrecision() {
let ten: FixedPointDecimal = 10
#expect(FixedPointDecimal.pow(ten, -9) == .zero)
}

@Test("pow(10, 11) overflows returns NaN")
func pow10Overflow() {
let ten: FixedPointDecimal = 10
#expect(FixedPointDecimal.pow(ten, 11).isNaN)
}

@Test("pow(10, n) fast path matches general path for all valid exponents")
func pow10MatchesGeneral() {
let ten: FixedPointDecimal = 10
for exponent in -8...10 {
let fast = FixedPointDecimal.pow(ten, exponent)
if exponent >= 0 {
var manual: FixedPointDecimal = 1
for _ in 0..<exponent {
let (product, _) = manual.multipliedReportingOverflow(by: ten)
manual = product
}
#expect(fast == manual, "pow(10, \(exponent)): fast=\(fast) manual=\(manual)")
} else {
let positive = FixedPointDecimal.pow(ten, -exponent)
let one: FixedPointDecimal = 1
let (manual, _) = one.dividedReportingOverflow(by: positive)
#expect(fast == manual, "pow(10, \(exponent)): fast=\(fast) manual=\(manual)")
}
}
}
}
Loading