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
39 changes: 22 additions & 17 deletions Sources/FixedPointDecimal/FixedPointDecimal+Arithmetic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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")
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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")
Expand All @@ -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"
Expand All @@ -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
Expand All @@ -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),
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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))
Expand All @@ -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.
Expand All @@ -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"
Expand All @@ -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.
Expand Down
20 changes: 10 additions & 10 deletions Sources/FixedPointDecimal/FixedPointDecimal+Numeric.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}

Expand Down Expand Up @@ -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
}
}
20 changes: 12 additions & 8 deletions Sources/FixedPointDecimal/FixedPointDecimal+Overflow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -82,17 +82,18 @@ 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)
}

/// 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"
Expand All @@ -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)
}
Expand All @@ -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"
Expand All @@ -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)
Expand All @@ -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"
Expand All @@ -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)
}
Expand Down
10 changes: 6 additions & 4 deletions Sources/FixedPointDecimal/FixedPointDecimal+Rounding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")!
Expand All @@ -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)
Expand Down Expand Up @@ -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"
Expand All @@ -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))
}
9 changes: 5 additions & 4 deletions Sources/FixedPointDecimal/FixedPointDecimal+SwiftUI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
Expand Down
Loading
Loading