From 3b994761173497dddf161a579757f00e036192f7 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Sat, 17 Oct 2026 11:01:03 -0400 Subject: [PATCH] fix(core): round Decimal.doubleValue correctly `doubleValue` divided the mantissa by ten once per decimal digit, so 0.007 became 0.006999999999999999 and 0.000001 became 1.0000000000000002e-06. Android's `BigDecimal.toDouble()` rounds correctly, so the two apps could send different `native_amount` doubles in intent `client_exchange_data` for the same amount. Parse `description` with `Double(String)` instead. `NSDecimalNumber`'s `doubleValue` is no fix: over 200k sampled decimals it missed on exactly the same values as the loop, while the string parse missed none. --- .../Extensions/Decimal+Double.swift | 57 +++---------------- .../DecimalDoubleTests.swift | 52 +++++++++++++++++ 2 files changed, 60 insertions(+), 49 deletions(-) create mode 100644 FlipcashCore/Tests/FlipcashCoreTests/DecimalDoubleTests.swift diff --git a/FlipcashCore/Sources/FlipcashCore/Extensions/Decimal+Double.swift b/FlipcashCore/Sources/FlipcashCore/Extensions/Decimal+Double.swift index 2275ddd7d..36eeb6715 100644 --- a/FlipcashCore/Sources/FlipcashCore/Extensions/Decimal+Double.swift +++ b/FlipcashCore/Sources/FlipcashCore/Extensions/Decimal+Double.swift @@ -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..