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
175 changes: 175 additions & 0 deletions Sources/D/Decimal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,181 @@ extension Decimal: Comparable {
}
}
}
extension Decimal {
/// Creates a `Decimal` from a fraction, rounding to the nearest value
/// with **halves rounding away from zero**.
///
/// - `1.235` (s=3) -> `1.24`
/// - `-1.235` (s=3) -> `-1.24`
@inlinable public static func roundedToNearest(
n: Int64,
d: Int64,
digits: Int
) -> Self? {
.init(numerator: n, denominator: d, digits: digits, rounding: .nearest)
}
/// Creates a `Decimal` from a fractional representation, rounded to a
/// specified number of significant digits.
///
/// - Parameters:
/// - numerator: The numerator of the fraction.
/// - denominator: The denominator of the fraction.
/// - significantDigits: The number of significant digits to preserve. Must be
/// greater than 0 and less than 19.
/// - rounding: The rounding rule to apply.
/// - Returns: A `Decimal` instance, or `nil` if the denominator is zero or
/// `significantDigits` is not in the valid range.
@inlinable public init?(
numerator: Int64,
denominator: Int64,
digits: Int,
rounding: DecimalRoundingMode
) {
// 1. Validate inputs
guard denominator != 0, (1...18).contains(digits) else {
return nil
}

if numerator == 0 {
self = .zero
return
}

// 2. Handle signs and use UInt64 for math to safely handle Int64.min
let isNegative: Bool = (numerator < 0) != (denominator < 0)

let n: UInt64 = numerator == .min ?
UInt64(bitPattern: Int64.max) + 1 : UInt64(abs(numerator))
let d: UInt64 = denominator == .min ?
UInt64(bitPattern: Int64.max) + 1 : UInt64(abs(denominator))

// 3-6. Perform core calculation (non-inlined)
guard let (unroundedUnits, remainder, powerOfFirstDigit) =
Self.__calculateUnrounded(n: n, d: d, digits: digits)
else {
// Result was non-zero but too small to represent
self = .zero
return
}

// 7. Perform rounding (inlined)
let roundingDigit: UInt64 = unroundedUnits % 10
var truncatedUnits: UInt64 = unroundedUnits / 10 // This has `s` digits

let needsIncrement: Bool
switch rounding {
case .toZero:
needsIncrement = false
case .awayFromZero:
// Increment if any fractional part exists at all
needsIncrement = (roundingDigit > 0 || remainder > 0)
case .nearest:
// Increment if at or above midpoint (5 or greater)
needsIncrement = roundingDigit >= 5
}

if needsIncrement {
truncatedUnits &+= 1
}

// 8. Finalize `units` and `power` (inlined)
var finalUnits: Int64
var finalPower: Int = powerOfFirstDigit - (digits - 1)

// Check for rounding overflow (e.g., 9.99 (s=2) rounds to 10.0)
let powerOfTen: Int64 = Self.power(Int64(digits))

if truncatedUnits == powerOfTen {
finalUnits = Int64(truncatedUnits / 10)
finalPower += 1
} else {
finalUnits = Int64(truncatedUnits)
}

self.init(
units: isNegative ? -finalUnits : finalUnits,
power: Int64(finalPower)
)
}
/// Performs the rounding-independent, non-inlinable core logic for
/// fractional initialization.
///
/// This function performs the long-division to gather `digits + 1`
/// significant digits.
///
/// - Returns: A tuple containing the raw components for rounding, or `nil`
/// if the result is too small to represent.
@usableFromInline static func __calculateUnrounded(
n: UInt64,
d: UInt64,
digits: Int
) -> (units: UInt64, remainder: UInt64, powerOfFirstDigit: Int)? {
// 3. Find power of first significant digit
var q: UInt64 = n / d
var r: UInt64 = n % d

let powerOfFirstDigit: Int
var units: UInt64 // We will build `s + 1` digits here
var digitsGathered: Int

if q > 0 {
// Case A: Result is >= 1
var power: Int = 0
var tempQ: UInt64 = q
while tempQ >= 10 {
tempQ /= 10
power += 1
}
powerOfFirstDigit = power
units = q
digitsGathered = power + 1
} else {
// Case B: Result is < 1. Find first non-zero digit.
var power: Int = -1
let overflowGuard: UInt64 = UInt64.max / 10

while r <= overflowGuard {
r &*= 10
q = r / d
if q > 0 {
// Found the first digit
break
}
// Digit was 0, so decrement power and continue
power &-= 1
}
r = r % d

guard q > 0 else {
// Result is too small (e.g., 1 / 10^50), effectively zero
return nil
}

powerOfFirstDigit = power
units = q
digitsGathered = 1
}

// 4. Gather remaining digits (we need `digits + 1` total)
let targetDigits: Int = digits + 1
let overflowGuard: UInt64 = UInt64.max / 10

while digitsGathered < targetDigits && units <= overflowGuard {
r &*= 10
units = (units &* 10) &+ (r / d)
r = r % d
digitsGathered &+= 1
}

// 5. Pad with zeros if exact value has fewer digits (e.g., 1/4, s=3 -> "250")
while digitsGathered < targetDigits && units <= overflowGuard {
units &*= 10
digitsGathered &+= 1
}

return (units, r, powerOfFirstDigit)
}
}

extension Decimal: ExpressibleByIntegerLiteral {
@inlinable public init(integerLiteral: Int64) {
Expand Down
22 changes: 22 additions & 0 deletions Sources/D/DecimalRoundingMode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/// Defines rounding behavior when creating a `Decimal` from a fraction.
@frozen public enum DecimalRoundingMode {
/// Rounds towards zero (truncation).
///
/// - `1.239` (s=3) -> `1.23`
/// - `-1.239` (s=3) -> `-1.23`
case toZero

/// Rounds away from zero if any fractional part exists.
///
/// - `1.231` (s=3) -> `1.24`
/// - `-1.231` (s=3) -> `-1.24`
/// - `1.230` (s=3) -> `1.23`
case awayFromZero

/// Rounds to the nearest value; halves round away from zero.
///
/// - `1.235` (s=3) -> `1.24`
/// - `1.234` (s=3) -> `1.23`
/// - `-1.235` (s=3) -> `-1.24`
case nearest
}
Loading