[272] Decimals and Zatoshi type (#330)

[272] Decimals and Zatoshi type (330)
- Zatoshi type implemented
- conversions implemented
- 'from' convert methods refactored to better readable syntax
- fixed issue with rounding Decimal (in Zatoshi -> Decimal conversion)
- rounded -> roundedZec
- comments resolved
- zcashNumberFormatter by default
This commit is contained in:
Lukas Korba 2022-05-27 17:19:21 +02:00 committed by GitHub
parent 9bed9d5927
commit bab1dc6f82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 262 additions and 6 deletions

View File

@ -101,6 +101,9 @@
9E2F1C8F280EDE09004E65FE /* Drawer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2F1C8E280EDE09004E65FE /* Drawer.swift */; };
9E37A2B827C8F59F00AE57B3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 9E37A2B727C8F59F00AE57B3 /* Localizable.strings */; };
9E391124283E4CAC0073DD9A /* ImportWalletTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E391123283E4CAC0073DD9A /* ImportWalletTests.swift */; };
9E391129283F74590073DD9A /* Zatoshi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E391128283F74590073DD9A /* Zatoshi.swift */; };
9E39112A283F90F10073DD9A /* Double+Zcash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EDDEA8B28250F9C00B4100C /* Double+Zcash.swift */; };
9E39112E283F91600073DD9A /* ZatoshiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E39112D283F91600073DD9A /* ZatoshiTests.swift */; };
9E4DC6E027C409A100E657F4 /* NeumorphicDesignModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4DC6DF27C409A100E657F4 /* NeumorphicDesignModifier.swift */; };
9E4DC6E227C4C6B700E657F4 /* SecantButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4DC6E127C4C6B700E657F4 /* SecantButtonStyles.swift */; };
9E5BF63F2819542C00BA3F17 /* TransactionHistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF63E2819542C00BA3F17 /* TransactionHistoryTests.swift */; };
@ -138,7 +141,6 @@
9EAFEB9228081E9400199FC9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F93874EF273C4DE200F0E875 /* HomeView.swift */; };
9EBEF87A27CE369800B4F343 /* RecoveryPhraseValidationFlowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBEF87927CE369800B4F343 /* RecoveryPhraseValidationFlowView.swift */; };
9ECAE56827FC713C0089A0EF /* DatabaseFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ECAE56727FC713C0089A0EF /* DatabaseFiles.swift */; };
9EDDEA8C28250F9C00B4100C /* Double+Zcash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EDDEA8B28250F9C00B4100C /* Double+Zcash.swift */; };
9EDDEAA22829610D00B4100C /* CurrencySelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EDDEA9F2829610D00B4100C /* CurrencySelectionTests.swift */; };
9EDDEAA32829610D00B4100C /* TransactionAmountInputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EDDEAA02829610D00B4100C /* TransactionAmountInputTests.swift */; };
9EDDEAA42829610D00B4100C /* TransactionAddressInputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EDDEAA12829610D00B4100C /* TransactionAddressInputTests.swift */; };
@ -291,6 +293,8 @@
9E2F1C8E280EDE09004E65FE /* Drawer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Drawer.swift; sourceTree = "<group>"; };
9E37A2B727C8F59F00AE57B3 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; };
9E391123283E4CAC0073DD9A /* ImportWalletTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportWalletTests.swift; sourceTree = "<group>"; };
9E391128283F74590073DD9A /* Zatoshi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Zatoshi.swift; sourceTree = "<group>"; };
9E39112D283F91600073DD9A /* ZatoshiTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZatoshiTests.swift; sourceTree = "<group>"; };
9E4DC6DF27C409A100E657F4 /* NeumorphicDesignModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeumorphicDesignModifier.swift; sourceTree = "<group>"; };
9E4DC6E127C4C6B700E657F4 /* SecantButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecantButtonStyles.swift; sourceTree = "<group>"; };
9E5BF63B2818305D00BA3F17 /* TransactionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionState.swift; sourceTree = "<group>"; };
@ -834,6 +838,7 @@
0D35CC45277A36E00074316A /* ScrollableWhenScaled.swift */,
2EDA07A327EDE2A900D6F09B /* DebugFrame.swift */,
9E2F1C832809B606004E65FE /* DebugMenu.swift */,
9E391128283F74590073DD9A /* Zatoshi.swift */,
);
path = Utils;
sourceTree = "<group>";
@ -956,6 +961,7 @@
9EAFEB852805A23100199FC9 /* WrappedSecItemTests.swift */,
9EF8135B27ECC25E0075AF48 /* UserPreferencesStorageTests.swift */,
9E02B56B27FED475005B809B /* DatabaseFilesTests.swift */,
9E39112D283F91600073DD9A /* ZatoshiTests.swift */,
);
path = UtilTests;
sourceTree = "<group>";
@ -1302,7 +1308,9 @@
9EBEF87A27CE369800B4F343 /* RecoveryPhraseValidationFlowView.swift in Sources */,
9E4DC6E227C4C6B700E657F4 /* SecantButtonStyles.swift in Sources */,
0DDB6A5127737D4A0012A410 /* RecoveryPhraseBackupFailedView.swift in Sources */,
9E391129283F74590073DD9A /* Zatoshi.swift in Sources */,
0D6D628B276A528E002FB4CC /* DropDelegate.swift in Sources */,
9E39112A283F90F10073DD9A /* Double+Zcash.swift in Sources */,
9E2DF99D27CF704D00649636 /* ImportSeedEditor.swift in Sources */,
F9971A5327680DD000A2DB75 /* ProfileStore.swift in Sources */,
669FDAEB272C23C2007B9422 /* CircularFrameBadge.swift in Sources */,
@ -1354,7 +1362,6 @@
0DF2DC5427235E3E00FA31E2 /* View+InnerShadow.swift in Sources */,
9EAFEB84280597B700199FC9 /* WrappedSecItem.swift in Sources */,
9E2AC10327DA28200042AA47 /* WalletStorage.swift in Sources */,
9EDDEA8C28250F9C00B4100C /* Double+Zcash.swift in Sources */,
9ECAE56827FC713C0089A0EF /* DatabaseFiles.swift in Sources */,
9E5BF6462821028C00BA3F17 /* WrappedUserDefaults.swift in Sources */,
F9971A6B27680E1000A2DB75 /* WalletInfoStore.swift in Sources */,
@ -1403,6 +1410,7 @@
9E01F8282833CDA0000EFC57 /* ScanTests.swift in Sources */,
9EDDEAA42829610D00B4100C /* TransactionAddressInputTests.swift in Sources */,
6654C7442715A4AC00901167 /* OnboardingStoreTests.swift in Sources */,
9E39112E283F91600073DD9A /* ZatoshiTests.swift in Sources */,
9EDDEAA32829610D00B4100C /* TransactionAmountInputTests.swift in Sources */,
9EAFEB862805A23100199FC9 /* WrappedSecItemTests.swift in Sources */,
9E5BF644281FEC9900BA3F17 /* SendTests.swift in Sources */,

View File

@ -19,6 +19,8 @@ final class AppDelegate: NSObject, UIApplicationDelegate {
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
// set the default behavior for the NSDecimalNumber
NSDecimalNumber.defaultBehavior = Zatoshi.decimalHandler
appViewStore.send(.appDelegate(.didFinishLaunching))
return true
}

View File

@ -14,6 +14,6 @@ extension Double {
}
func asZecString() -> String {
NumberFormatter.zcashFormatter.string(from: NSNumber(value: self)) ?? ""
NumberFormatter.zcashNumberFormatter.string(from: NSNumber(value: self)) ?? ""
}
}

View File

@ -14,6 +14,6 @@ extension Int64 {
}
func asZecString() -> String {
NumberFormatter.zcashFormatter.string(from: NSNumber(value: self.asHumanReadableZecBalance())) ?? ""
NumberFormatter.zcashNumberFormatter.string(from: NSNumber(value: self.asHumanReadableZecBalance())) ?? ""
}
}

View File

@ -10,7 +10,7 @@ extension String {
}
extension NumberFormatter {
static let zcashFormatter: NumberFormatter = {
static let zcashNumberFormatter: NumberFormatter = {
var formatter = NumberFormatter()
formatter.maximumFractionDigits = 8
formatter.maximumIntegerDigits = 8
@ -22,7 +22,7 @@ extension NumberFormatter {
extension String {
var doubleValue: Double? {
return NumberFormatter.zcashFormatter.number(from: self)?.doubleValue
return NumberFormatter.zcashNumberFormatter.number(from: self)?.doubleValue
}
}

View File

@ -0,0 +1,74 @@
//
// Zatoshi.swift
// secant-testnet
//
// Created by Lukáš Korba on 26.05.2022.
//
import Foundation
struct Zatoshi {
enum Constants {
static let oneZecInZatoshi: Int64 = 100_000_000
static let maxZecSupply: Int64 = 21_000_000
static let maxZatoshi: Int64 = Constants.oneZecInZatoshi * Constants.maxZecSupply
}
static var decimalHandler = NSDecimalNumberHandler(
roundingMode: NSDecimalNumber.RoundingMode.bankers,
scale: 8,
raiseOnExactness: true,
raiseOnOverflow: true,
raiseOnUnderflow: true,
raiseOnDivideByZero: true
)
@Clamped(-Constants.maxZatoshi...Constants.maxZatoshi)
var amount: Int64 = 0
/// Converts `Zatoshi` to `NSDecimalNumber`
var decimalValue: NSDecimalNumber {
NSDecimalNumber(decimal: Decimal(amount) / Decimal(Constants.oneZecInZatoshi))
}
/// Converts `Zatoshi` to human readable format, up to 8 fraction digits
var decimalString: String {
decimalValue.roundedZec.stringValue
}
/// Converts `Decimal` to `Zatoshi`
static func from(decimal: Decimal) -> Zatoshi {
let roundedZec = NSDecimalNumber(decimal: decimal).roundedZec
let zec2zatoshi = Decimal(Constants.oneZecInZatoshi) * roundedZec.decimalValue
return Zatoshi(amount: NSDecimalNumber(decimal: zec2zatoshi).int64Value)
}
/// Converts `String` to `Zatoshi`
static func from(decimalString: String, formatter: NumberFormatter = NumberFormatter.zcashNumberFormatter) -> Zatoshi? {
if let number = formatter.number(from: decimalString) {
return Zatoshi.from(decimal: number.decimalValue)
}
return nil
}
static func + (left: Zatoshi, right: Zatoshi) -> Zatoshi {
Zatoshi(amount: left.amount + right.amount)
}
static func - (left: Zatoshi, right: Zatoshi) -> Zatoshi {
Zatoshi(amount: left.amount - right.amount)
}
}
extension NSDecimalNumber {
/// Converts `NSDecimalNumber` to human readable format, up to 8 fraction digits
var decimalString: String {
self.rounding(accordingToBehavior: Zatoshi.decimalHandler).stringValue
}
/// Round the decimal to 8 fraction digits
var roundedZec: NSDecimalNumber {
self.rounding(accordingToBehavior: Zatoshi.decimalHandler)
}
}

View File

@ -0,0 +1,172 @@
//
// ZatoshiTests.swift
// secantTests
//
// Created by Lukáš Korba on 26.05.2022.
//
import XCTest
@testable import secant_testnet
class ZatoshiTests: XCTestCase {
func testLowerBound() throws {
let number = Zatoshi(amount: -Zatoshi.Constants.maxZatoshi - 1)
XCTAssertEqual(
-Zatoshi.Constants.maxZatoshi,
number.amount,
"Zatoshi tests: `testLowerBound` the value is expected to be clamped to lower bound but it's \(number.amount)"
)
}
func testUpperBound() throws {
let number = Zatoshi(amount: Zatoshi.Constants.maxZatoshi + 1)
XCTAssertEqual(
Zatoshi.Constants.maxZatoshi,
number.amount,
"Zatoshi tests: `testUpperBound` the value is expected to be clamped to upper bound but it's \(number.amount)"
)
}
func testAddingZatoshi() throws {
let numberA1 = Zatoshi(amount: 100_000)
let numberB1 = Zatoshi(amount: 200_000)
let result1 = numberA1 + numberB1
XCTAssertEqual(
result1.amount,
Zatoshi(amount: 300_000).amount,
"Zatoshi tests: `testAddingZatoshi` the value is expected to be 300_000 but it's \(result1.amount)"
)
let numberA2 = Zatoshi(amount: -100_000)
let numberB2 = Zatoshi(amount: 200_000)
let result2 = numberA2 + numberB2
XCTAssertEqual(
result2.amount,
Zatoshi(amount: 100_000).amount,
"Zatoshi tests: `testAddingZatoshi` the value is expected to be 100_000 but it's \(result2.amount)"
)
let numberA3 = Zatoshi(amount: 100_000)
let numberB3 = Zatoshi(amount: -200_000)
let result3 = numberA3 + numberB3
XCTAssertEqual(
result3.amount,
Zatoshi(amount: -100_000).amount,
"Zatoshi tests: `testAddingZatoshi` the value is expected to be -100_000 but it's \(result3.amount)"
)
let number = Zatoshi(amount: Zatoshi.Constants.maxZatoshi)
let result4 = number + number
XCTAssertEqual(
result4.amount,
Zatoshi.Constants.maxZatoshi,
"Zatoshi tests: `testAddingZatoshi` the value is expected to be clapmed to upper bound but it's \(result4.amount)"
)
}
func testSubtractingZatoshi() throws {
let numberA1 = Zatoshi(amount: 100_000)
let numberB1 = Zatoshi(amount: 200_000)
let result1 = numberA1 - numberB1
XCTAssertEqual(
result1.amount,
Zatoshi(amount: -100_000).amount,
"Zatoshi tests: `testSubtractingZatoshi` the value is expected to be -100_000 but it's \(result1.amount)"
)
let numberA2 = Zatoshi(amount: -100_000)
let numberB2 = Zatoshi(amount: 200_000)
let result2 = numberA2 - numberB2
XCTAssertEqual(
result2.amount,
Zatoshi(amount: -300_000).amount,
"Zatoshi tests: `testSubtractingZatoshi` the value is expected to be -300_000 but it's \(result2.amount)"
)
let numberA3 = Zatoshi(amount: 100_000)
let numberB3 = Zatoshi(amount: -200_000)
let result3 = numberA3 - numberB3
XCTAssertEqual(
result3.amount,
Zatoshi(amount: 300_000).amount,
"Zatoshi tests: `testSubtractingZatoshi` the value is expected to be 300_000 but it's \(result3.amount)"
)
let number = Zatoshi(amount: -Zatoshi.Constants.maxZatoshi)
let result4 = number + number
XCTAssertEqual(
result4.amount,
-Zatoshi.Constants.maxZatoshi,
"Zatoshi tests: `testSubtractingZatoshi` the value is expected to be clapmed to lower bound but it's \(result4.amount)"
)
}
func testHumanReadable() throws {
// result of this division is 1.4285714285714285714285714285714285714
let number = Zatoshi.from(decimal: Decimal(200.0 / 140.0))
// IMPORTANT: the INTERNAL value of number is still 1.4285714285714285714285714285714285714!!!
// but decimalString is rounding it to maximumFractionDigits set to be 8
// We can't compare it to double value 1.42857143 (or even Decimal(1.42857143))
// so we convert it to string, in that case we are prooving it to be rendered
// to the user exactly the way we want
XCTAssertEqual(
number.decimalString,
"1.42857143",
"Zatoshi tests: the value is expected to be 1.42857143 but it's \(number.decimalString)"
)
}
func testUSDtoZecToUSD() throws {
// The price of zec is $140, we want to send $200
let usd2zec = NSDecimalNumber(decimal: 200.0 / 140.0)
XCTAssertEqual(
usd2zec.decimalString,
"1.42857143",
"Zatoshi tests: `testUSDtoZatoshiToUSD` the value is expected to be 1.42857143 but it's \(usd2zec.decimalString)"
)
// convert it back
let zec2usd = NSDecimalNumber(decimal: usd2zec.decimalValue * 140.0)
XCTAssertEqual(
zec2usd.decimalString,
"200",
"Zatoshi tests: `testUSDtoZatoshiToUSD` the value is expected to be 200 but it's \(zec2usd.decimalString)"
)
}
func testStringToZatoshi() throws {
if let number = Zatoshi.from(decimalString: "200.0") {
XCTAssertEqual(
number.decimalString,
"200",
"Zatoshi tests: `testStringToZec` the value is expected to be 200 but it's \(number.decimalString)"
)
} else {
XCTFail("Zatoshi tests: `testStringToZatoshi` failed to convert number.")
}
if let number = Zatoshi.from(decimalString: "0.02836478949923") {
XCTAssertEqual(
number.amount,
2_836_479,
"Zatoshi tests: the value is expected to be 2_836_478 but it's \(number.amount)"
)
} else {
XCTFail("Zatoshi tests: `testStringToZatoshi` failed to convert number.")
}
}
}