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
57 changes: 8 additions & 49 deletions FlipcashCore/Sources/FlipcashCore/Extensions/Decimal+Double.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,59 +8,18 @@

import Foundation

/// A copy from Swift Foundation
/// Reference:
/// https://github.com/apple/swift-corelibs-foundation/blob/main/Sources/Foundation/Decimal.swift
extension Decimal {

fileprivate subscript(index: UInt32) -> UInt16 {
get {
switch index {
case 0: return _mantissa.0
case 1: return _mantissa.1
case 2: return _mantissa.2
case 3: return _mantissa.3
case 4: return _mantissa.4
case 5: return _mantissa.5
case 6: return _mantissa.6
case 7: return _mantissa.7
default: fatalError("Invalid index \(index) for _mantissa")
}
}
set {
switch index {
case 0: _mantissa.0 = newValue
case 1: _mantissa.1 = newValue
case 2: _mantissa.2 = newValue
case 3: _mantissa.3 = newValue
case 4: _mantissa.4 = newValue
case 5: _mantissa.5 = newValue
case 6: _mantissa.6 = newValue
case 7: _mantissa.7 = newValue
default: fatalError("Invalid index \(index) for _mantissa")
}
}
}


/// The nearest `Double` to this value, matching Android's `BigDecimal.toDouble()`.
///
/// `native_amount` in intent exchange data is a double, so both platforms must land on the
/// same one. `NSDecimalNumber.doubleValue` scales the mantissa by ten once per decimal digit
/// and can miss by an ulp or more (0.007 becomes 0.006999999999999999). `description` is
/// locale-independent and `Double(String)` rounds correctly.
public var doubleValue: Double {
if _length == 0 {
return _isNegative == 1 ? Double.nan : 0
}

var d = 0.0
for idx in (0..<min(_length, 8)).reversed() {
d = d * 65536 + Double(self[idx])
}

if _exponent < 0 {
for _ in _exponent..<0 {
d /= 10.0
}
} else {
for _ in 0..<_exponent {
d *= 10.0
}
}
return _isNegative != 0 ? -d : d
return Double(description) ?? NSDecimalNumber(decimal: self).doubleValue
}
}
52 changes: 52 additions & 0 deletions FlipcashCore/Tests/FlipcashCoreTests/DecimalDoubleTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// DecimalDoubleTests.swift
// FlipcashCoreTests
//

import Foundation
import Testing
@testable import FlipcashCore

@Suite("Decimal.doubleValue")
struct DecimalDoubleTests {

// `native_amount` goes on the wire as a double, and Android's
// `BigDecimal.toDouble()` is correctly rounded. Each value must land on the
// same double `Double(String)` parses. 0.007, 0.000001 and the Int64-max
// amount were each off by an ulp or more when the conversion divided by ten
// once per decimal digit.
@Test("Converts to the correctly rounded double", arguments: [
"0.001",
"0.007",
"0.000001",
"0.8",
"0.2",
"4.95",
"123456789.123456789",
"9223372036854.775807",
"-0.007",
"100000000000000000000",
])
func correctlyRounded(_ string: String) throws {
let decimal = try #require(Decimal(string: string))
let expected = try #require(Double(string))
#expect(decimal.doubleValue == expected)
}

@Test("Native amounts in the multi-payment intent vectors convert exactly", arguments: [
"0.800000", "0.200000", "9223372036854.775807", "0.000001",
"0.001000", "0.002000", "0.003000", "0.004000", "0.005000",
"0.006000", "0.007000", "0.008000", "0.009000", "0.010000",
])
func multiPaymentVectorAmounts(_ string: String) throws {
let decimal = try #require(Decimal(string: string))
let expected = try #require(Double(string))
#expect(decimal.doubleValue == expected)
}

@Test("Zero and NaN")
func zeroAndNaN() {
#expect(Decimal.zero.doubleValue == 0)
#expect(Decimal.nan.doubleValue.isNaN)
}
}
Loading