From c833aab0c021fccb9a73e38d87d90e2afa68c92c Mon Sep 17 00:00:00 2001 From: Joakim Hassila Date: Fri, 27 Mar 2026 13:46:31 +0100 Subject: [PATCH 1/5] =?UTF-8?q?feat!:=20make=20NaN=20signalling=20?= =?UTF-8?q?=E2=80=94=20trap=20on=20use=20instead=20of=20silent=20propagati?= =?UTF-8?q?on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NaN remains as a sentinel value (.nan, .isNaN) but now traps immediately when used in any computation (arithmetic, rounding, negation, abs, pow). Representation paths (description, doubleValue, decimalValue, codable) still handle NaN gracefully. This catches NaN misuse at the point of error rather than letting it propagate silently through calculations. BREAKING CHANGE: Operations on NaN now trap instead of returning NaN. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../FixedPointDecimal+Arithmetic.swift | 39 +++--- .../FixedPointDecimal+Numeric.swift | 20 ++-- .../FixedPointDecimal+Overflow.swift | 20 ++-- .../FixedPointDecimal+Rounding.swift | 10 +- .../FixedPointDecimal+SwiftUI.swift | 9 +- .../FixedPointDecimal/FixedPointDecimal.swift | 32 +++-- .../ArithmeticTests.swift | 34 +++--- .../ConversionTests.swift | 6 +- .../EdgeCaseTests.swift | 113 +++++++++++------- .../OverflowTests.swift | 63 +++++----- Tests/FixedPointDecimalTests/PowTests.swift | 35 +++--- .../PropertyTests.swift | 10 +- .../RoundingTests.swift | 6 +- 13 files changed, 212 insertions(+), 185 deletions(-) diff --git a/Sources/FixedPointDecimal/FixedPointDecimal+Arithmetic.swift b/Sources/FixedPointDecimal/FixedPointDecimal+Arithmetic.swift index 71c8223..24095f9 100644 --- a/Sources/FixedPointDecimal/FixedPointDecimal+Arithmetic.swift +++ b/Sources/FixedPointDecimal/FixedPointDecimal+Arithmetic.swift @@ -28,8 +28,8 @@ extension FixedPointDecimal { extension FixedPointDecimal { /// Returns the sum of two values. /// - /// If either operand is NaN, the result is NaN. - /// Traps on overflow, matching Swift `Int` behavior. + /// Traps if either operand is NaN or if the result overflows, + /// matching Swift `Int` behavior. /// /// ```swift /// let a: FixedPointDecimal = "10.5" @@ -41,10 +41,11 @@ extension FixedPointDecimal { /// - lhs: The first addend. /// - rhs: The second addend. /// - Returns: The sum of `lhs` and `rhs`. + /// - Precondition: Neither operand may be NaN. /// - Precondition: The result must fit in `Int64` after scaling. @inlinable public static func + (lhs: Self, rhs: Self) -> Self { - if lhs.isNaN || rhs.isNaN { return .nan } + precondition(!lhs.isNaN && !rhs.isNaN, "NaN in FixedPointDecimal addition") let (result, overflow) = lhs._storage.addingReportingOverflow(rhs._storage) precondition(!overflow, "FixedPointDecimal addition overflow") precondition(result != .min, "FixedPointDecimal addition produced NaN sentinel") @@ -53,7 +54,7 @@ extension FixedPointDecimal { /// Adds the right-hand value to the left-hand value in place. /// - /// Traps on overflow. Propagates NaN. + /// Traps on overflow or NaN. /// /// ```swift /// var total: FixedPointDecimal = "100.0" @@ -71,8 +72,8 @@ extension FixedPointDecimal { /// Returns the difference of two values. /// - /// If either operand is NaN, the result is NaN. - /// Traps on overflow, matching Swift `Int` behavior. + /// Traps if either operand is NaN or if the result overflows, + /// matching Swift `Int` behavior. /// /// ```swift /// let a: FixedPointDecimal = "10.5" @@ -84,10 +85,11 @@ extension FixedPointDecimal { /// - lhs: The minuend. /// - rhs: The subtrahend. /// - Returns: The difference of `lhs` and `rhs`. + /// - Precondition: Neither operand may be NaN. /// - Precondition: The result must fit in `Int64` after scaling. @inlinable public static func - (lhs: Self, rhs: Self) -> Self { - if lhs.isNaN || rhs.isNaN { return .nan } + precondition(!lhs.isNaN && !rhs.isNaN, "NaN in FixedPointDecimal subtraction") let (result, overflow) = lhs._storage.subtractingReportingOverflow(rhs._storage) precondition(!overflow, "FixedPointDecimal subtraction overflow") precondition(result != .min, "FixedPointDecimal subtraction produced NaN sentinel") @@ -96,7 +98,7 @@ extension FixedPointDecimal { /// Subtracts the right-hand value from the left-hand value in place. /// - /// Traps on overflow. Propagates NaN. + /// Traps on overflow or NaN. /// /// ```swift /// var balance: FixedPointDecimal = "100.0" @@ -117,7 +119,7 @@ extension FixedPointDecimal { /// Uses `Int128` intermediate arithmetic to prevent precision loss during /// the multiply-then-divide-by-scale-factor operation. The result is /// rounded using banker's rounding (round half to even). If either operand - /// is NaN, the result is NaN. Traps if the final result does not fit in + /// Traps if either operand is NaN or if the final result does not fit in /// `Int64`. /// /// ```swift @@ -130,10 +132,11 @@ extension FixedPointDecimal { /// - lhs: The first factor. /// - rhs: The second factor. /// - Returns: The product of `lhs` and `rhs`. + /// - Precondition: Neither operand may be NaN. /// - Precondition: The result must fit in `Int64` after scaling. @inlinable public static func * (lhs: Self, rhs: Self) -> Self { - if lhs.isNaN || rhs.isNaN { return .nan } + precondition(!lhs.isNaN && !rhs.isNaN, "NaN in FixedPointDecimal multiplication") let wide = Int128(lhs._storage) * Int128(rhs._storage) let scaled = _bankersDiv(wide, Int128(scaleFactor)) precondition(scaled > Int128(Int64.min) && scaled <= Int128(Int64.max), @@ -143,7 +146,7 @@ extension FixedPointDecimal { /// Multiplies the left-hand value by the right-hand value in place. /// - /// Traps on overflow. Propagates NaN. + /// Traps on overflow or NaN. /// /// - Parameters: /// - lhs: The value to modify. @@ -157,7 +160,7 @@ extension FixedPointDecimal { /// /// Uses `Int128` intermediate arithmetic for precision. The result is /// rounded using banker's rounding (round half to even). If either operand - /// is NaN, the result is NaN. Traps on division by zero or if the result + /// Traps if either operand is NaN, on division by zero, or if the result /// does not fit in `Int64`. /// /// ```swift @@ -170,11 +173,12 @@ extension FixedPointDecimal { /// - lhs: The dividend. /// - rhs: The divisor. /// - Returns: The quotient of `lhs` divided by `rhs`. + /// - Precondition: Neither operand may be NaN. /// - Precondition: `rhs` must not be zero. /// - Precondition: The result must fit in `Int64` after scaling. @inlinable public static func / (lhs: Self, rhs: Self) -> Self { - if lhs.isNaN || rhs.isNaN { return .nan } + precondition(!lhs.isNaN && !rhs.isNaN, "NaN in FixedPointDecimal division") precondition(rhs._storage != 0, "Division by zero") let wide = Int128(lhs._storage) * Int128(scaleFactor) let result = _bankersDiv(wide, Int128(rhs._storage)) @@ -185,7 +189,7 @@ extension FixedPointDecimal { /// Divides the left-hand value by the right-hand value in place. /// - /// Traps on division by zero or overflow. Propagates NaN. + /// Traps on division by zero, overflow, or NaN. /// /// - Parameters: /// - lhs: The value to modify. @@ -212,7 +216,7 @@ extension FixedPointDecimal { /// Returns the remainder of dividing the first value by the second. /// /// The sign of the result matches the sign of the dividend (`lhs`). - /// If either operand is NaN, the result is NaN. + /// Traps if either operand is NaN. /// /// ```swift /// let a: FixedPointDecimal = "10.0" @@ -224,17 +228,18 @@ extension FixedPointDecimal { /// - lhs: The dividend. /// - rhs: The divisor. /// - Returns: The remainder of `lhs` divided by `rhs`. + /// - Precondition: Neither operand may be NaN. /// - Precondition: `rhs` must not be zero. @inlinable public static func % (lhs: Self, rhs: Self) -> Self { - if lhs.isNaN || rhs.isNaN { return .nan } + precondition(!lhs.isNaN && !rhs.isNaN, "NaN in FixedPointDecimal remainder") precondition(rhs._storage != 0, "Division by zero in remainder") return Self(rawValue: lhs._storage % rhs._storage) } /// Divides the left-hand value by the right-hand value and stores the remainder in place. /// - /// Propagates NaN. + /// Traps on NaN. /// /// - Parameters: /// - lhs: The value to modify. diff --git a/Sources/FixedPointDecimal/FixedPointDecimal+Numeric.swift b/Sources/FixedPointDecimal/FixedPointDecimal+Numeric.swift index 81d7217..f89d787 100644 --- a/Sources/FixedPointDecimal/FixedPointDecimal+Numeric.swift +++ b/Sources/FixedPointDecimal/FixedPointDecimal+Numeric.swift @@ -25,16 +25,16 @@ extension FixedPointDecimal { /// The absolute value of this instance. /// - /// Returns NaN for NaN, matching `Double.nan.magnitude` behavior. + /// Traps on NaN. /// /// ```swift /// let v = FixedPointDecimal("-5.0")! /// v.magnitude // 5.0 - /// FixedPointDecimal.nan.magnitude.isNaN // true /// ``` + /// - Precondition: The value must not be NaN. @inlinable public var magnitude: Magnitude { - if isNaN { return .nan } + precondition(!isNaN, "magnitude called on NaN") return FixedPointDecimal(rawValue: abs(_storage)) } @@ -63,35 +63,35 @@ extension FixedPointDecimal { /// Returns the additive inverse of this value. /// - /// Returns NaN if the operand is NaN, matching the behavior of all - /// arithmetic operators on this type. + /// Traps if the operand is NaN. /// /// ```swift /// let price = FixedPointDecimal("42.5")! /// let neg = -price // -42.5 - /// (-FixedPointDecimal.nan).isNaN // true /// ``` /// /// - Parameter operand: The value to negate. - /// - Returns: The negated value, or NaN if the operand is NaN. + /// - Returns: The negated value. + /// - Precondition: The operand must not be NaN. @inlinable public prefix static func - (operand: Self) -> Self { - if operand.isNaN { return .nan } + precondition(!operand.isNaN, "NaN in FixedPointDecimal negation") return Self(rawValue: -operand._storage) } /// Replaces this value with its additive inverse. /// - /// If the value is NaN, this is a no-op (NaN is preserved). + /// Traps if the value is NaN. /// /// ```swift /// var price = FixedPointDecimal("42.5")! /// price.negate() /// // price is now -42.5 /// ``` + /// - Precondition: The value must not be NaN. @inlinable public mutating func negate() { - if isNaN { return } + precondition(!isNaN, "NaN in FixedPointDecimal negation") _storage = -_storage } } diff --git a/Sources/FixedPointDecimal/FixedPointDecimal+Overflow.swift b/Sources/FixedPointDecimal/FixedPointDecimal+Overflow.swift index 5effbfa..d963951 100644 --- a/Sources/FixedPointDecimal/FixedPointDecimal+Overflow.swift +++ b/Sources/FixedPointDecimal/FixedPointDecimal+Overflow.swift @@ -71,7 +71,7 @@ extension FixedPointDecimal { /// Returns the sum of this value and the given value, along with a Boolean /// indicating whether overflow occurred in the operation. /// - /// If either operand is NaN, the result is `(.nan, false)`. + /// Traps if either operand is NaN. /// /// ```swift /// let a: FixedPointDecimal = "50.0" @@ -82,9 +82,10 @@ extension FixedPointDecimal { /// /// - Parameter other: The value to add. /// - Returns: A tuple containing the partial sum and a Boolean overflow flag. + /// - Precondition: Neither operand may be NaN. @inlinable public func addingReportingOverflow(_ other: Self) -> (partialValue: Self, overflow: Bool) { - if isNaN || other.isNaN { return (.nan, false) } + precondition(!isNaN && !other.isNaN, "NaN in FixedPointDecimal addition") let (result, overflow) = _storage.addingReportingOverflow(other._storage) return (Self(rawValue: result), overflow || result == .min) } @@ -92,7 +93,7 @@ extension FixedPointDecimal { /// Returns the difference of this value and the given value, along with a /// Boolean indicating whether overflow occurred in the operation. /// - /// If either operand is NaN, the result is `(.nan, false)`. + /// Traps if either operand is NaN. /// /// ```swift /// let a: FixedPointDecimal = "50.0" @@ -103,9 +104,10 @@ extension FixedPointDecimal { /// /// - Parameter other: The value to subtract. /// - Returns: A tuple containing the partial difference and a Boolean overflow flag. + /// - Precondition: Neither operand may be NaN. @inlinable public func subtractingReportingOverflow(_ other: Self) -> (partialValue: Self, overflow: Bool) { - if isNaN || other.isNaN { return (.nan, false) } + precondition(!isNaN && !other.isNaN, "NaN in FixedPointDecimal subtraction") let (result, overflow) = _storage.subtractingReportingOverflow(other._storage) return (Self(rawValue: result), overflow || result == .min) } @@ -114,7 +116,7 @@ extension FixedPointDecimal { /// indicating whether overflow occurred in the operation. /// /// Uses `Int128` intermediate arithmetic. Overflow is reported when the scaled - /// result exceeds `Int64` range. If either operand is NaN, the result is `(.nan, false)`. + /// result exceeds `Int64` range. Traps if either operand is NaN. /// /// ```swift /// let a: FixedPointDecimal = "10.0" @@ -125,9 +127,10 @@ extension FixedPointDecimal { /// /// - Parameter other: The value to multiply by. /// - Returns: A tuple containing the partial product and a Boolean overflow flag. + /// - Precondition: Neither operand may be NaN. @inlinable public func multipliedReportingOverflow(by other: Self) -> (partialValue: Self, overflow: Bool) { - if isNaN || other.isNaN { return (.nan, false) } + precondition(!isNaN && !other.isNaN, "NaN in FixedPointDecimal multiplication") let wide = Int128(_storage) * Int128(other._storage) let scaled = Self._bankersDiv(wide, Int128(Self.scaleFactor)) let fits = scaled > Int128(Int64.min) && scaled <= Int128(Int64.max) @@ -139,7 +142,7 @@ extension FixedPointDecimal { /// a Boolean indicating whether overflow occurred in the operation. /// /// Overflow is reported for division by zero or when the result exceeds - /// `Int64` range. If either operand is NaN, the result is `(.nan, false)`. + /// `Int64` range. Traps if either operand is NaN. /// /// ```swift /// let a: FixedPointDecimal = "100.0" @@ -153,9 +156,10 @@ extension FixedPointDecimal { /// /// - Parameter other: The value to divide by. /// - Returns: A tuple containing the partial quotient and a Boolean overflow flag. + /// - Precondition: Neither operand may be NaN. @inlinable public func dividedReportingOverflow(by other: Self) -> (partialValue: Self, overflow: Bool) { - if isNaN || other.isNaN { return (.nan, false) } + precondition(!isNaN && !other.isNaN, "NaN in FixedPointDecimal division") guard other._storage != 0 else { return (.zero, true) } diff --git a/Sources/FixedPointDecimal/FixedPointDecimal+Rounding.swift b/Sources/FixedPointDecimal/FixedPointDecimal+Rounding.swift index 27876fb..f1806e2 100644 --- a/Sources/FixedPointDecimal/FixedPointDecimal+Rounding.swift +++ b/Sources/FixedPointDecimal/FixedPointDecimal+Rounding.swift @@ -28,7 +28,7 @@ extension FixedPointDecimal { /// Returns this value rounded to the specified number of fractional decimal digits. /// - /// Returns NaN unchanged. + /// Traps on NaN. /// /// ```swift /// let price = FixedPointDecimal("123.456789")! @@ -42,12 +42,13 @@ extension FixedPointDecimal { /// - scale: The number of fractional digits to keep (0...8). Default is 0. /// - mode: The rounding mode. Default is `.toNearestOrEven`. /// - Returns: The rounded value. + /// - Precondition: The value must not be NaN. /// - Precondition: `scale` must be in `0...8`. @inlinable public func rounded(scale: Int = 0, _ mode: RoundingMode = .toNearestOrEven) -> Self { precondition(scale >= 0 && scale <= Self.fractionalDigitCount, "Scale must be in 0...\(Self.fractionalDigitCount)") - if isNaN { return .nan } + precondition(!isNaN, "NaN in FixedPointDecimal rounding") guard scale < Self.fractionalDigitCount else { return self } let divisor = Self._powerOf10(Self.fractionalDigitCount - scale) @@ -174,7 +175,7 @@ extension FixedPointDecimal { /// Returns the absolute value of a `FixedPointDecimal`. /// -/// Returns NaN unchanged. +/// Traps on NaN. /// /// ```swift /// let v: FixedPointDecimal = "-42.5" @@ -183,8 +184,9 @@ extension FixedPointDecimal { /// /// - Parameter value: The value whose absolute value is returned. /// - Returns: The absolute value of `value`. +/// - Precondition: The value must not be NaN. @inlinable public func abs(_ value: FixedPointDecimal) -> FixedPointDecimal { - if value.isNaN { return .nan } + precondition(!value.isNaN, "abs called on NaN") return FixedPointDecimal(rawValue: abs(value._storage)) } diff --git a/Sources/FixedPointDecimal/FixedPointDecimal+SwiftUI.swift b/Sources/FixedPointDecimal/FixedPointDecimal+SwiftUI.swift index 2249c25..8f2af89 100644 --- a/Sources/FixedPointDecimal/FixedPointDecimal+SwiftUI.swift +++ b/Sources/FixedPointDecimal/FixedPointDecimal+SwiftUI.swift @@ -18,12 +18,13 @@ extension FixedPointDecimal: VectorArithmetic { /// Scales the raw value by a `Double` factor. Used by SwiftUI's animation system. /// /// The result is clamped to the valid range (`Int64.min + 1 ... Int64.max`) - /// to avoid producing the NaN sentinel. NaN values are left unchanged. + /// to avoid producing the NaN sentinel. Traps on NaN. /// /// - Parameter rhs: The scaling factor. + /// - Precondition: The value must not be NaN. @inlinable public mutating func scale(by rhs: Double) { - if isNaN { return } + precondition(!isNaN, "NaN in FixedPointDecimal scale") let scaled = Double(rawValue) * rhs if scaled >= Double(Int64.max) { self = Self(rawValue: .max) @@ -38,10 +39,10 @@ extension FixedPointDecimal: VectorArithmetic { /// The squared magnitude of the raw storage, used by SwiftUI for /// animation interpolation. /// - /// Returns `Double.nan` for NaN values. + /// - Precondition: The value must not be NaN. @inlinable public var magnitudeSquared: Double { - if isNaN { return .nan } + precondition(!isNaN, "magnitudeSquared called on NaN") let d = Double(rawValue) return d * d } diff --git a/Sources/FixedPointDecimal/FixedPointDecimal.swift b/Sources/FixedPointDecimal/FixedPointDecimal.swift index 5f54856..283002b 100644 --- a/Sources/FixedPointDecimal/FixedPointDecimal.swift +++ b/Sources/FixedPointDecimal/FixedPointDecimal.swift @@ -279,29 +279,31 @@ public struct FixedPointDecimal: Sendable, BitwiseCopyable { /// Unlike `Double`, where ULP varies across the range, `FixedPointDecimal` /// has uniform precision — the distance between any two adjacent /// representable values is always `0.00000001`. - /// Returns NaN for NaN. + /// - Precondition: The value must not be NaN. @inlinable public var ulp: FixedPointDecimal { - if isNaN { return .nan } + precondition(!isNaN, "ulp called on NaN") return .leastNonzeroMagnitude } /// The least representable value greater than this one. /// - /// Returns NaN for NaN. Traps on `.max` (no representable value above). + /// - Precondition: The value must not be NaN. + /// - Precondition: The value must be less than `.max`. @inlinable public var nextUp: FixedPointDecimal { - if isNaN { return .nan } + precondition(!isNaN, "nextUp called on NaN") precondition(self < .max, "nextUp called on .max") return FixedPointDecimal(rawValue: _storage + 1) } /// The greatest representable value less than this one. /// - /// Returns NaN for NaN. Traps on `.min` (no representable value below). + /// - Precondition: The value must not be NaN. + /// - Precondition: The value must be greater than `.min`. @inlinable public var nextDown: FixedPointDecimal { - if isNaN { return .nan } + precondition(!isNaN, "nextDown called on NaN") precondition(self > .min, "nextDown called on .min") return FixedPointDecimal(rawValue: _storage - 1) } @@ -410,7 +412,7 @@ public struct FixedPointDecimal: Sendable, BitwiseCopyable { /// - Returns: `x` raised to the power `n`. @inlinable public static func pow(_ x: FixedPointDecimal, _ n: Int) -> FixedPointDecimal { - guard !x.isNaN else { return .nan } + precondition(!x.isNaN, "NaN in FixedPointDecimal pow") if n == 0 { return FixedPointDecimal(rawValue: scaleFactor) } // 1 if n == 1 { return x } @@ -421,24 +423,20 @@ public struct FixedPointDecimal: Sendable, BitwiseCopyable { if shift >= 0, shift < _pow10Table.count { return FixedPointDecimal(rawValue: _pow10Table[shift]) } - return shift < 0 ? .zero : .nan + precondition(shift < 0, "FixedPointDecimal pow overflow") + return .zero } if n < 0 { let positive = pow(x, -n) - if positive.isNaN || positive == .zero { return .nan } - let (result, overflow) = FixedPointDecimal(rawValue: scaleFactor) - .dividedReportingOverflow(by: positive) - return overflow ? .nan : result + precondition(positive != .zero, "Division by zero in FixedPointDecimal pow") + return FixedPointDecimal(rawValue: scaleFactor) / positive } - // Repeated multiplication with overflow detection. - // Uses multipliedReportingOverflow to return .nan instead of trapping. + // Repeated multiplication — traps on overflow via `*`. var result = x for _ in 1 ..< n { - let (product, overflow) = result.multipliedReportingOverflow(by: x) - if overflow { return .nan } - result = product + result = result * x } return result } diff --git a/Tests/FixedPointDecimalTests/ArithmeticTests.swift b/Tests/FixedPointDecimalTests/ArithmeticTests.swift index 9635913..c117517 100644 --- a/Tests/FixedPointDecimalTests/ArithmeticTests.swift +++ b/Tests/FixedPointDecimalTests/ArithmeticTests.swift @@ -303,12 +303,11 @@ struct ArithmeticTests { #expect(result == FixedPointDecimal.min) } - @Test("NaN / Int64 returns NaN (guards against Int64.min / -1 overflow)") - func nanDivByInt64NegOne() { - // NaN sentinel is Int64.min; dividing Int64.min by -1 would overflow - // but the NaN check catches it first - let result = FixedPointDecimal.nan / (-1) - #expect(result.isNaN) + @Test("NaN / Int64 traps (guards against Int64.min / -1 overflow)") + func nanDivByInt64NegOne() async { + await #expect(processExitsWith: .failure) { + _ = FixedPointDecimal.nan / (-1) + } } // MARK: - Remainder Edge Cases @@ -353,24 +352,21 @@ struct ArithmeticTests { #expect(a == 1.3 as FixedPointDecimal) } - // MARK: - NaN Propagation in Mixed-Type Arithmetic + // MARK: - NaN Trapping in Mixed-Type Arithmetic - @Test("NaN * Int64 propagates NaN") - func nanTimesInt64() { - let nan = FixedPointDecimal.nan - #expect((nan * 5).isNaN) + @Test("NaN * Int64 traps") + func nanTimesInt64() async { + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal.nan * 5 } } - @Test("Int64 * NaN propagates NaN") - func int64TimesNaN() { - let nan = FixedPointDecimal.nan - #expect((5 * nan).isNaN) + @Test("Int64 * NaN traps") + func int64TimesNaN() async { + await #expect(processExitsWith: .failure) { _ = 5 * FixedPointDecimal.nan } } - @Test("NaN / Int64 propagates NaN") - func nanDividedByInt64() { - let nan = FixedPointDecimal.nan - #expect((nan / 5).isNaN) + @Test("NaN / Int64 traps") + func nanDividedByInt64() async { + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal.nan / 5 } } // MARK: - Iterator Aggregation (Sum and Product) diff --git a/Tests/FixedPointDecimalTests/ConversionTests.swift b/Tests/FixedPointDecimalTests/ConversionTests.swift index e81672a..9948e55 100644 --- a/Tests/FixedPointDecimalTests/ConversionTests.swift +++ b/Tests/FixedPointDecimalTests/ConversionTests.swift @@ -207,9 +207,9 @@ struct ConversionTests { #expect(value.magnitude == 42.5 as FixedPointDecimal) } - @Test("Numeric magnitude for NaN is NaN") - func magnitudeNaN() { - #expect(FixedPointDecimal.nan.magnitude.isNaN) + @Test("Numeric magnitude for NaN traps") + func magnitudeNaN() async { + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal.nan.magnitude } } @Test("Numeric magnitude for zero") diff --git a/Tests/FixedPointDecimalTests/EdgeCaseTests.swift b/Tests/FixedPointDecimalTests/EdgeCaseTests.swift index 5b2db4b..ffbcc26 100644 --- a/Tests/FixedPointDecimalTests/EdgeCaseTests.swift +++ b/Tests/FixedPointDecimalTests/EdgeCaseTests.swift @@ -25,18 +25,44 @@ struct EdgeCaseTests { #expect(!(nan < nan)) // nan == nan, so not less than } - @Test("NaN propagates through arithmetic") - func nanPropagation() { - let nan = FixedPointDecimal.nan - let value: FixedPointDecimal = 42 - #expect((nan + value).isNaN) - #expect((value + nan).isNaN) - #expect((nan - value).isNaN) - #expect((nan * value).isNaN) - #expect((nan / value).isNaN) - #expect((value / nan).isNaN) - #expect((nan % value).isNaN) - #expect((value % nan).isNaN) + @Test("NaN + value traps") + func nanAddTraps() async { + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal.nan + 42 } + } + + @Test("value + NaN traps") + func valueAddNanTraps() async { + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal(42) + .nan } + } + + @Test("NaN - value traps") + func nanSubTraps() async { + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal.nan - 42 } + } + + @Test("NaN * value traps") + func nanMulTraps() async { + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal.nan * 42 } + } + + @Test("NaN / value traps") + func nanDivTraps() async { + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal.nan / 42 } + } + + @Test("value / NaN traps") + func valueDivNanTraps() async { + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal(42) / .nan } + } + + @Test("NaN % value traps") + func nanModTraps() async { + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal.nan % 42 } + } + + @Test("value % NaN traps") + func valueModNanTraps() async { + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal(42) % .nan } } @Test("NaN double value") @@ -190,43 +216,43 @@ struct EdgeCaseTests { // MARK: - NaN with Remainder - @Test("NaN % value = NaN") - func nanRemainder() { - let value: FixedPointDecimal = 3 - #expect((FixedPointDecimal.nan % value).isNaN) - #expect((value % FixedPointDecimal.nan).isNaN) + @Test("NaN % value traps (remainder)") + func nanRemainder() async { + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal.nan % 3 } + } + + @Test("value % NaN traps (remainder)") + func valueRemainderNan() async { + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal(3) % .nan } } // MARK: - NaN Negation - @Test("Negation of NaN returns NaN") - func negateNaN() { - let result = -FixedPointDecimal.nan - #expect(result.isNaN) + @Test("Negation of NaN traps") + func negateNaN() async { + await #expect(processExitsWith: .failure) { _ = -FixedPointDecimal.nan } } - @Test("Mutating negate of NaN preserves NaN") - func mutatingNegateNaN() { - var value = FixedPointDecimal.nan - value.negate() - #expect(value.isNaN) + @Test("Mutating negate of NaN traps") + func mutatingNegateNaN() async { + await #expect(processExitsWith: .failure) { + var nan = FixedPointDecimal.nan + nan.negate() + } } // MARK: - NaN Rounding - @Test("NaN rounded returns NaN at each scale") - func nanRoundedAllScales() { - for scale in 0...8 { - #expect(FixedPointDecimal.nan.rounded(scale: scale).isNaN, - "NaN.rounded(scale: \(scale)) should be NaN") - } + @Test("NaN rounded traps") + func nanRoundedTraps() async { + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal.nan.rounded(scale: 0) } } // MARK: - NaN absoluteValue - @Test("NaN absoluteValue returns NaN") - func nanAbsoluteValue() { - #expect(abs(FixedPointDecimal.nan).isNaN) + @Test("NaN absoluteValue traps") + func nanAbsoluteValue() async { + await #expect(processExitsWith: .failure) { _ = abs(FixedPointDecimal.nan) } } // MARK: - Distance & Advance Edge Cases @@ -409,11 +435,12 @@ struct EdgeCaseTests { #expect(value == -42.5 as FixedPointDecimal) } - @Test("VectorArithmetic scale NaN is no-op") - func vectorScaleNaN() { - var value = FixedPointDecimal.nan - value.scale(by: 2.0) - #expect(value.isNaN) + @Test("VectorArithmetic scale NaN traps") + func vectorScaleNaN() async { + await #expect(processExitsWith: .failure) { + var nan = FixedPointDecimal.nan + nan.scale(by: 2.0) + } } @Test("VectorArithmetic scale clamps to max") @@ -438,9 +465,9 @@ struct EdgeCaseTests { #expect(value.magnitudeSquared == expected) } - @Test("VectorArithmetic magnitudeSquared of NaN") - func vectorMagnitudeSquaredNaN() { - #expect(FixedPointDecimal.nan.magnitudeSquared.isNaN) + @Test("VectorArithmetic magnitudeSquared of NaN traps") + func vectorMagnitudeSquaredNaN() async { + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal.nan.magnitudeSquared } } #endif diff --git a/Tests/FixedPointDecimalTests/OverflowTests.swift b/Tests/FixedPointDecimalTests/OverflowTests.swift index 4c8deb5..2976d80 100644 --- a/Tests/FixedPointDecimalTests/OverflowTests.swift +++ b/Tests/FixedPointDecimalTests/OverflowTests.swift @@ -102,44 +102,39 @@ struct OverflowTests { // MARK: - NaN with Overflow-Reporting Operators - @Test("NaN addingReportingOverflow returns NaN, no overflow") - func nanAddReporting() { - let nan = FixedPointDecimal.nan - let value: FixedPointDecimal = 42 - let (result1, overflow1) = nan.addingReportingOverflow(value) - #expect(result1.isNaN) - #expect(!overflow1) - - let (result2, overflow2) = value.addingReportingOverflow(nan) - #expect(result2.isNaN) - #expect(!overflow2) - } - - @Test("NaN subtractingReportingOverflow returns NaN, no overflow") - func nanSubReporting() { - let nan = FixedPointDecimal.nan - let value: FixedPointDecimal = 42 - let (result, overflow) = nan.subtractingReportingOverflow(value) - #expect(result.isNaN) - #expect(!overflow) + @Test("NaN addingReportingOverflow traps (lhs)") + func nanAddReportingLhs() async { + await #expect(processExitsWith: .failure) { + _ = FixedPointDecimal.nan.addingReportingOverflow(FixedPointDecimal(42)) + } } - @Test("NaN multipliedReportingOverflow returns NaN, no overflow") - func nanMulReporting() { - let nan = FixedPointDecimal.nan - let value: FixedPointDecimal = 42 - let (result, overflow) = nan.multipliedReportingOverflow(by: value) - #expect(result.isNaN) - #expect(!overflow) + @Test("NaN addingReportingOverflow traps (rhs)") + func nanAddReportingRhs() async { + await #expect(processExitsWith: .failure) { + _ = FixedPointDecimal(42).addingReportingOverflow(.nan) + } } - @Test("NaN dividedReportingOverflow returns NaN, no overflow") - func nanDivReporting() { - let nan = FixedPointDecimal.nan - let value: FixedPointDecimal = 42 - let (result, overflow) = nan.dividedReportingOverflow(by: value) - #expect(result.isNaN) - #expect(!overflow) + @Test("NaN subtractingReportingOverflow traps") + func nanSubReporting() async { + await #expect(processExitsWith: .failure) { + _ = FixedPointDecimal.nan.subtractingReportingOverflow(FixedPointDecimal(42)) + } + } + + @Test("NaN multipliedReportingOverflow traps") + func nanMulReporting() async { + await #expect(processExitsWith: .failure) { + _ = FixedPointDecimal.nan.multipliedReportingOverflow(by: FixedPointDecimal(42)) + } + } + + @Test("NaN dividedReportingOverflow traps") + func nanDivReporting() async { + await #expect(processExitsWith: .failure) { + _ = FixedPointDecimal.nan.dividedReportingOverflow(by: FixedPointDecimal(42)) + } } // MARK: - Overflow near boundary diff --git a/Tests/FixedPointDecimalTests/PowTests.swift b/Tests/FixedPointDecimalTests/PowTests.swift index 3dfc9d3..6f0dc26 100644 --- a/Tests/FixedPointDecimalTests/PowTests.swift +++ b/Tests/FixedPointDecimalTests/PowTests.swift @@ -35,10 +35,10 @@ struct PowTests { #expect(FixedPointDecimal.pow(1, -100) == 1) } - @Test("pow NaN propagates") - func powNaN() { - #expect(FixedPointDecimal.pow(.nan, 2).isNaN) - #expect(FixedPointDecimal.pow(.nan, 0).isNaN) + @Test("pow NaN traps") + func powNaN() async { + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal.pow(.nan, 2) } + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal.pow(.nan, 0) } } @Test("pow zero base") @@ -46,7 +46,11 @@ struct PowTests { #expect(FixedPointDecimal.pow(0, 1) == 0) #expect(FixedPointDecimal.pow(0, 5) == 0) #expect(FixedPointDecimal.pow(0, 0) == 1) // 0^0 = 1 by convention - #expect(FixedPointDecimal.pow(0, -1).isNaN) // 1/0 = NaN + } + + @Test("pow zero base negative exponent traps") + func powZeroBaseNegativeExponent() async { + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal.pow(0, -1) } } @Test("pow type inference in context") @@ -75,25 +79,23 @@ struct PowTests { #expect(FixedPointDecimal.pow(FixedPointDecimal(-2.0), 3) == -8.0) } - @Test("pow overflow returns NaN") - func powOverflow() { + @Test("pow overflow traps") + func powOverflow() async { // Int64 max raw value is ~9.2×10^18, so 10^11 × 10^8 (scale) overflows - #expect(FixedPointDecimal.pow(10, 11).isNaN) + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal.pow(10, 11) } // Large base to large power - #expect(FixedPointDecimal.pow(FixedPointDecimal(1000000), 4).isNaN) + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal.pow(FixedPointDecimal(1000000), 4) } // Negative exponent where positive overflows - #expect(FixedPointDecimal.pow(FixedPointDecimal(1000000), -4).isNaN) + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal.pow(FixedPointDecimal(1000000), -4) } } @Test("pow overflow boundary — largest non-overflowing power of 10") func powOverflowBoundary() { // 10^10 = 10_000_000_000 — rawValue = 10^18, fits in Int64 let p10 = FixedPointDecimal.pow(10, 10) - #expect(!p10.isNaN) #expect(p10 == 10000000000 as FixedPointDecimal) - // 10^11 would be rawValue = 10^19, overflows Int64 - #expect(FixedPointDecimal.pow(10, 11).isNaN) + // 10^11 would be rawValue = 10^19, overflows Int64 — tested in powOverflow } // MARK: - numberOfFractionalDigits @@ -222,10 +224,9 @@ struct PowTests { #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, 11) overflows traps") + func pow10Overflow() async { + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal.pow(10, 11) } } @Test("pow(10, n) fast path matches general path for all valid exponents") diff --git a/Tests/FixedPointDecimalTests/PropertyTests.swift b/Tests/FixedPointDecimalTests/PropertyTests.swift index 079e8a6..9b79fd7 100644 --- a/Tests/FixedPointDecimalTests/PropertyTests.swift +++ b/Tests/FixedPointDecimalTests/PropertyTests.swift @@ -330,13 +330,11 @@ struct PropertyTests { // Test addition let (addResult, addOvf) = a.addingReportingOverflow(b) if !addOvf { - #expect(!addResult.isNaN || a.isNaN || b.isNaN, + #expect(!addResult.isNaN, "Non-overflow add produced NaN for \(a)+\(b)") - if !a.isNaN && !b.isNaN { - let decSum = Self.toDecimal(a) + Self.toDecimal(b) - #expect(Decimal(addResult) == decSum, - "Add overflow detection: \(a)+\(b)") - } + let decSum = Self.toDecimal(a) + Self.toDecimal(b) + #expect(Decimal(addResult) == decSum, + "Add overflow detection: \(a)+\(b)") } valid += 1 diff --git a/Tests/FixedPointDecimalTests/RoundingTests.swift b/Tests/FixedPointDecimalTests/RoundingTests.swift index 7269a18..5f1ec2d 100644 --- a/Tests/FixedPointDecimalTests/RoundingTests.swift +++ b/Tests/FixedPointDecimalTests/RoundingTests.swift @@ -114,9 +114,9 @@ struct RoundingTests { #expect(abs(FixedPointDecimal.zero) == .zero) } - @Test("Absolute value of NaN") - func absNaN() { - #expect(abs(FixedPointDecimal.nan).isNaN) + @Test("Absolute value of NaN traps") + func absNaN() async { + await #expect(processExitsWith: .failure) { _ = abs(FixedPointDecimal.nan) } } // MARK: - Rounding at Every Scale From 09ce175dc9050b92dfd883b9dd3c10f50580419c Mon Sep 17 00:00:00 2001 From: Joakim Hassila Date: Fri, 27 Mar 2026 13:49:37 +0100 Subject: [PATCH 2/5] test: add trap tests for ulp, nextUp, nextDown on NaN Co-Authored-By: Claude Opus 4.6 (1M context) --- .../FixedPointDecimalTests/EdgeCaseTests.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Tests/FixedPointDecimalTests/EdgeCaseTests.swift b/Tests/FixedPointDecimalTests/EdgeCaseTests.swift index ffbcc26..83c13c5 100644 --- a/Tests/FixedPointDecimalTests/EdgeCaseTests.swift +++ b/Tests/FixedPointDecimalTests/EdgeCaseTests.swift @@ -241,6 +241,23 @@ struct EdgeCaseTests { } } + // MARK: - NaN Properties + + @Test("ulp on NaN traps") + func ulpNaN() async { + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal.nan.ulp } + } + + @Test("nextUp on NaN traps") + func nextUpNaN() async { + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal.nan.nextUp } + } + + @Test("nextDown on NaN traps") + func nextDownNaN() async { + await #expect(processExitsWith: .failure) { _ = FixedPointDecimal.nan.nextDown } + } + // MARK: - NaN Rounding @Test("NaN rounded traps") From a4bd413fde98509cf172ef8611a5223f87010af0 Mon Sep 17 00:00:00 2001 From: Joakim Hassila Date: Fri, 27 Mar 2026 13:50:35 +0100 Subject: [PATCH 3/5] test: add FixedWidthInteger init?(exactly: FixedPointDecimal) tests Cover signed and unsigned integer exact conversion including success, fractional rejection, NaN rejection, and out-of-range rejection. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ConversionTests.swift | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/Tests/FixedPointDecimalTests/ConversionTests.swift b/Tests/FixedPointDecimalTests/ConversionTests.swift index 9948e55..5dbcc7d 100644 --- a/Tests/FixedPointDecimalTests/ConversionTests.swift +++ b/Tests/FixedPointDecimalTests/ConversionTests.swift @@ -537,6 +537,69 @@ struct ConversionTests { #expect(result == nil) } + // MARK: - FixedWidthInteger init?(exactly: FixedPointDecimal) + + @Test("Int64(exactly:) succeeds for exact integers") + func int64ExactlySucceeds() { + #expect(Int64(exactly: FixedPointDecimal(42)) == 42) + #expect(Int64(exactly: FixedPointDecimal(0)) == 0) + #expect(Int64(exactly: FixedPointDecimal(-99)) == -99) + } + + @Test("Int64(exactly:) returns nil for fractional values") + func int64ExactlyFractional() { + #expect(Int64(exactly: FixedPointDecimal(42.5)) == nil) + #expect(Int64(exactly: FixedPointDecimal(0.00000001)) == nil) + } + + @Test("Int64(exactly:) returns nil for NaN") + func int64ExactlyNaN() { + #expect(Int64(exactly: FixedPointDecimal.nan) == nil) + } + + @Test("Int32(exactly:) succeeds for in-range integers") + func int32ExactlySucceeds() { + #expect(Int32(exactly: FixedPointDecimal(1000)) == 1000) + #expect(Int32(exactly: FixedPointDecimal(-1000)) == -1000) + } + + @Test("Int32(exactly:) returns nil for out-of-range integers") + func int32ExactlyOutOfRange() { + #expect(Int32(exactly: FixedPointDecimal(Int64(Int32.max) + 1)) == nil) + } + + @Test("Int16(exactly:) returns nil for NaN") + func int16ExactlyNaN() { + #expect(Int16(exactly: FixedPointDecimal.nan) == nil) + } + + @Test("UInt64(exactly:) succeeds for non-negative integers") + func uint64ExactlySucceeds() { + #expect(UInt64(exactly: FixedPointDecimal(42)) == 42) + #expect(UInt64(exactly: FixedPointDecimal(0)) == 0) + } + + @Test("UInt64(exactly:) returns nil for negative values") + func uint64ExactlyNegative() { + #expect(UInt64(exactly: FixedPointDecimal(-1)) == nil) + } + + @Test("UInt64(exactly:) returns nil for fractional values") + func uint64ExactlyFractional() { + #expect(UInt64(exactly: FixedPointDecimal(1.5)) == nil) + } + + @Test("UInt64(exactly:) returns nil for NaN") + func uint64ExactlyNaN() { + #expect(UInt64(exactly: FixedPointDecimal.nan) == nil) + } + + @Test("UInt16(exactly:) returns nil for out-of-range values") + func uint16ExactlyOutOfRange() { + #expect(UInt16(exactly: FixedPointDecimal(Int64(UInt16.max) + 1)) == nil) + #expect(UInt16(exactly: FixedPointDecimal(-1)) == nil) + } + @Test("FPD negation matches Decimal negation") func negationParityWithDecimal() { let values = ["0", "1", "-1", "123.45", "-99.99", "0.00000001"] From 6f2830a989f8f09886c8793ecec1fb1f4c04cf7e Mon Sep 17 00:00:00 2001 From: Joakim Hassila Date: Fri, 27 Mar 2026 13:51:26 +0100 Subject: [PATCH 4/5] test: add Int/Int64/Int32 init?(exactly:) tests Cover concrete overloads for Int, Int64, and Int32 exact conversion from FixedPointDecimal: success, fractional rejection, NaN rejection, and out-of-range rejection. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ConversionTests.swift | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/Tests/FixedPointDecimalTests/ConversionTests.swift b/Tests/FixedPointDecimalTests/ConversionTests.swift index 5dbcc7d..88dd572 100644 --- a/Tests/FixedPointDecimalTests/ConversionTests.swift +++ b/Tests/FixedPointDecimalTests/ConversionTests.swift @@ -537,7 +537,25 @@ struct ConversionTests { #expect(result == nil) } - // MARK: - FixedWidthInteger init?(exactly: FixedPointDecimal) + // MARK: - Concrete Int/Int64/Int32 init?(exactly: FixedPointDecimal) + + @Test("Int(exactly:) succeeds for exact integers") + func intExactlySucceeds() { + #expect(Int(exactly: FixedPointDecimal(42)) == 42) + #expect(Int(exactly: FixedPointDecimal(0)) == 0) + #expect(Int(exactly: FixedPointDecimal(-99)) == -99) + } + + @Test("Int(exactly:) returns nil for fractional values") + func intExactlyFractional() { + #expect(Int(exactly: FixedPointDecimal(42.5)) == nil) + #expect(Int(exactly: FixedPointDecimal(0.00000001)) == nil) + } + + @Test("Int(exactly:) returns nil for NaN") + func intExactlyNaN() { + #expect(Int(exactly: FixedPointDecimal.nan) == nil) + } @Test("Int64(exactly:) succeeds for exact integers") func int64ExactlySucceeds() { @@ -563,11 +581,23 @@ struct ConversionTests { #expect(Int32(exactly: FixedPointDecimal(-1000)) == -1000) } + @Test("Int32(exactly:) returns nil for fractional values") + func int32ExactlyFractional() { + #expect(Int32(exactly: FixedPointDecimal(1.5)) == nil) + } + @Test("Int32(exactly:) returns nil for out-of-range integers") func int32ExactlyOutOfRange() { #expect(Int32(exactly: FixedPointDecimal(Int64(Int32.max) + 1)) == nil) } + @Test("Int32(exactly:) returns nil for NaN") + func int32ExactlyNaN() { + #expect(Int32(exactly: FixedPointDecimal.nan) == nil) + } + + // MARK: - FixedWidthInteger generic init?(exactly: FixedPointDecimal) + @Test("Int16(exactly:) returns nil for NaN") func int16ExactlyNaN() { #expect(Int16(exactly: FixedPointDecimal.nan) == nil) From 06630d20ddd03ad64645f4fabc675ad9cccae017 Mon Sep 17 00:00:00 2001 From: Joakim Hassila Date: Fri, 27 Mar 2026 13:52:18 +0100 Subject: [PATCH 5/5] test: add tests for isFinite, sign, leastFiniteMagnitude, and significand:exponent: traps Also fix stale NaN-propagation example in .nan doc comment. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../FixedPointDecimal/FixedPointDecimal.swift | 1 - Tests/FixedPointDecimalTests/BasicTests.swift | 20 +++++++++++++++++++ .../PreconditionTests.swift | 14 +++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/Sources/FixedPointDecimal/FixedPointDecimal.swift b/Sources/FixedPointDecimal/FixedPointDecimal.swift index 283002b..21db149 100644 --- a/Sources/FixedPointDecimal/FixedPointDecimal.swift +++ b/Sources/FixedPointDecimal/FixedPointDecimal.swift @@ -200,7 +200,6 @@ public struct FixedPointDecimal: Sendable, BitwiseCopyable { /// ```swift /// let missing: FixedPointDecimal = .nan /// missing.isNaN // true - /// (missing + FixedPointDecimal(1)).isNaN // true (NaN propagates) /// ``` @inlinable public static var nan: FixedPointDecimal { diff --git a/Tests/FixedPointDecimalTests/BasicTests.swift b/Tests/FixedPointDecimalTests/BasicTests.swift index 720b9be..e76ca91 100644 --- a/Tests/FixedPointDecimalTests/BasicTests.swift +++ b/Tests/FixedPointDecimalTests/BasicTests.swift @@ -89,6 +89,26 @@ struct BasicTests { #expect(FixedPointDecimal.min.rawValue == Int64.min + 1) #expect(FixedPointDecimal.leastNonzeroMagnitude.rawValue == 1) #expect(FixedPointDecimal.greatestFiniteMagnitude.rawValue == Int64.max) + #expect(FixedPointDecimal.leastFiniteMagnitude == .min) + } + + @Test("isFinite returns true for non-NaN, false for NaN") + func isFinite() { + #expect(FixedPointDecimal.zero.isFinite) + #expect(FixedPointDecimal(42).isFinite) + #expect(FixedPointDecimal(-1).isFinite) + #expect(FixedPointDecimal.max.isFinite) + #expect(FixedPointDecimal.min.isFinite) + #expect(!FixedPointDecimal.nan.isFinite) + } + + @Test("sign returns .plus for positive/zero/NaN, .minus for negative") + func sign() { + #expect(FixedPointDecimal(42).sign == .plus) + #expect(FixedPointDecimal.zero.sign == .plus) + #expect(FixedPointDecimal(-42).sign == .minus) + #expect(FixedPointDecimal.min.sign == .minus) + #expect(FixedPointDecimal.nan.sign == .plus) } @Test("Integer part and fractional part") diff --git a/Tests/FixedPointDecimalTests/PreconditionTests.swift b/Tests/FixedPointDecimalTests/PreconditionTests.swift index 3370ecf..7dbcddc 100644 --- a/Tests/FixedPointDecimalTests/PreconditionTests.swift +++ b/Tests/FixedPointDecimalTests/PreconditionTests.swift @@ -35,6 +35,20 @@ struct PreconditionTests { } } + @Test("init(significand:exponent:) traps on out-of-range shift") + func initSignificandExponentOutOfRange() async { + await #expect(processExitsWith: .failure) { + _ = FixedPointDecimal(significand: 1, exponent: 100) + } + } + + @Test("init(significand:exponent:) traps on overflow") + func initSignificandExponentOverflow() async { + await #expect(processExitsWith: .failure) { + _ = FixedPointDecimal(significand: Int(Int64.max), exponent: 1) + } + } + @Test("init(_ Double) traps on Double.nan") func initDoubleNaN() async { await #expect(processExitsWith: .failure) {