Added arithmetic operations to Amount
This commit is contained in:
parent
825dc7c8b9
commit
fd7a3bc747
|
@ -79,6 +79,8 @@ kotlin {
|
|||
|
||||
val kotlinxDateTimeVersion: String by project
|
||||
val jsJodaTimeZoneVersion: String by project
|
||||
val ionspinBigNumVersion: String by project
|
||||
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
|
@ -102,12 +104,26 @@ kotlin {
|
|||
jsMain {
|
||||
dependencies {
|
||||
api(npm("@js-joda/timezone", jsJodaTimeZoneVersion))
|
||||
|
||||
implementation(npm("big.js", "6.0.3"))
|
||||
}
|
||||
}
|
||||
jsTest { }
|
||||
|
||||
nativeMain { }
|
||||
nativeTest { }
|
||||
|
||||
linuxMain {
|
||||
dependencies {
|
||||
implementation("com.ionspin.kotlin:bignum:$ionspinBigNumVersion")
|
||||
}
|
||||
}
|
||||
|
||||
mingwMain {
|
||||
dependencies {
|
||||
implementation("com.ionspin.kotlin:bignum:$ionspinBigNumVersion")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
package net.codinux.banking.client.model
|
||||
|
||||
import platform.Foundation.NSDecimalNumber
|
||||
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||
|
||||
@NoArgConstructor
|
||||
actual class Amount actual constructor(amount: String) : NSDecimalNumber(string = amount) {
|
||||
|
||||
actual companion object {
|
||||
actual val Zero = Amount("0.00")
|
||||
}
|
||||
|
||||
|
||||
actual operator fun plus(other: Amount): Amount =
|
||||
Amount(this.decimalNumberByAdding(other).stringValue)
|
||||
|
||||
actual operator fun minus(other: Amount): Amount =
|
||||
Amount(this.decimalNumberBySubtracting(other).stringValue)
|
||||
|
||||
actual operator fun times(other: Amount): Amount =
|
||||
Amount(this.decimalNumberByMultiplyingBy(other).stringValue)
|
||||
|
||||
actual operator fun div(other: Amount): Amount =
|
||||
Amount(this.decimalNumberByDividingBy(other).stringValue)
|
||||
|
||||
|
||||
actual override fun toString(): String = this.stringValue
|
||||
|
||||
}
|
|
@ -1,21 +1,31 @@
|
|||
package net.codinux.banking.client.model
|
||||
|
||||
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||
import kotlin.jvm.JvmInline
|
||||
|
||||
@JvmInline
|
||||
|
||||
internal const val DecimalPrecision = 20 // 20 to match Big.js's behavior
|
||||
|
||||
|
||||
fun Amount.toFloat() =
|
||||
this.toString().toFloat()
|
||||
|
||||
fun Amount.toDouble() =
|
||||
this.toString().toDouble()
|
||||
|
||||
|
||||
@NoArgConstructor
|
||||
value class Amount(val amount: String = "0") {
|
||||
expect class Amount(amount: String = "0") {
|
||||
|
||||
companion object {
|
||||
val Zero = Amount("0")
|
||||
|
||||
fun fromString(amount: String): Amount = Amount(amount)
|
||||
val Zero: Amount
|
||||
}
|
||||
|
||||
|
||||
constructor(amount: Double) : this(amount.toString())
|
||||
operator fun plus(other: Amount): Amount
|
||||
operator fun minus(other: Amount): Amount
|
||||
operator fun times(other: Amount): Amount
|
||||
operator fun div(other: Amount): Amount
|
||||
|
||||
override fun toString(): String
|
||||
|
||||
override fun toString() = amount
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
package net.codinux.banking.client.model.extensions
|
||||
|
||||
import net.codinux.banking.client.model.Amount
|
||||
|
||||
// TODO: really map to BigDecimal
|
||||
fun Amount.toBigDecimal(): Double = this.amount.toDouble()
|
|
@ -0,0 +1,36 @@
|
|||
package net.codinux.banking.client.model
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class AmountTest {
|
||||
|
||||
@Test
|
||||
fun add() {
|
||||
val result = Amount("0.1") + Amount("0.2")
|
||||
|
||||
assertEquals(Amount("0.3"), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun minus() {
|
||||
val result = Amount("0.1") - Amount("0.2")
|
||||
|
||||
assertEquals(Amount("-0.1"), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multiply() {
|
||||
val result = Amount("0.1") * Amount("0.2")
|
||||
|
||||
assertEquals(Amount("0.02"), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun divide() {
|
||||
val result = Amount("1") / Amount("3")
|
||||
|
||||
assertEquals(Amount("0.33333333333333333333"), result)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package net.codinux.banking.client.model
|
||||
|
||||
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||
|
||||
@JsModule("big.js")
|
||||
@JsNonModule
|
||||
open external class Big(value: String) {
|
||||
fun plus(other: Big): Big
|
||||
fun minus(other: Big): Big
|
||||
fun times(other: Big): Big
|
||||
fun div(other: Big): Big
|
||||
override fun toString(): String
|
||||
}
|
||||
|
||||
|
||||
@NoArgConstructor
|
||||
actual class Amount actual constructor(amount: String): Big(amount) {
|
||||
|
||||
actual companion object {
|
||||
actual val Zero = Amount("0.00")
|
||||
}
|
||||
|
||||
|
||||
actual operator fun plus(other: Amount): Amount {
|
||||
return Amount(super.plus(other).toString())
|
||||
}
|
||||
|
||||
actual operator fun minus(other: Amount): Amount {
|
||||
return Amount(super.minus(other).toString())
|
||||
}
|
||||
|
||||
actual operator fun times(other: Amount): Amount {
|
||||
return Amount(super.times(other).toString())
|
||||
}
|
||||
|
||||
actual operator fun div(other: Amount): Amount {
|
||||
return Amount(super.div(other).toString())
|
||||
}
|
||||
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other is Amount && this.toString() == other.toString()
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return super.hashCode()
|
||||
}
|
||||
|
||||
actual override fun toString(): String = super.toString()
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package net.codinux.banking.client.model
|
||||
|
||||
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
@NoArgConstructor
|
||||
actual class Amount actual constructor(amount: String) : BigDecimal(amount) {
|
||||
|
||||
actual companion object {
|
||||
actual val Zero = Amount("0.00")
|
||||
}
|
||||
|
||||
|
||||
actual operator fun plus(other: Amount): Amount =
|
||||
Amount(this.add(other).toString())
|
||||
|
||||
actual operator fun minus(other: Amount): Amount =
|
||||
Amount(this.subtract(other).toString())
|
||||
|
||||
actual operator fun times(other: Amount): Amount =
|
||||
Amount(this.multiply(other).toString())
|
||||
|
||||
actual operator fun div(other: Amount): Amount =
|
||||
// without RoundingMode a java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result. will be thrown
|
||||
Amount(this.divide(other, DecimalPrecision, RoundingMode.HALF_UP).toString()) // 20 to match Big.js's behaviour
|
||||
|
||||
|
||||
/* why are these methods required when deriving from BigDecimal? */
|
||||
|
||||
override fun toByte(): Byte {
|
||||
1 + 1
|
||||
return 0 // will never be called; where is this method coming from?
|
||||
}
|
||||
|
||||
override fun toShort(): Short {
|
||||
return 0 // will never be called; where is this method coming from?
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package net.codinux.banking.client.model
|
||||
|
||||
import com.ionspin.kotlin.bignum.decimal.BigDecimal
|
||||
import com.ionspin.kotlin.bignum.decimal.DecimalMode
|
||||
import com.ionspin.kotlin.bignum.decimal.RoundingMode
|
||||
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||
|
||||
@NoArgConstructor
|
||||
actual class Amount actual constructor(amount: String) {
|
||||
|
||||
actual companion object {
|
||||
actual val Zero: Amount = Amount("0.00")
|
||||
|
||||
private val decimalMode = DecimalMode(DecimalPrecision.toLong(), RoundingMode.ROUND_HALF_CEILING)
|
||||
}
|
||||
|
||||
|
||||
internal val amount: BigDecimal = BigDecimal.parseString(amount)
|
||||
|
||||
actual operator fun plus(other: Amount): Amount {
|
||||
return Amount(amount.add(other.amount).toString())
|
||||
}
|
||||
|
||||
actual operator fun minus(other: Amount): Amount {
|
||||
return Amount(amount.subtract(other.amount).toString())
|
||||
}
|
||||
|
||||
actual operator fun times(other: Amount): Amount {
|
||||
return Amount(amount.multiply(other.amount).toString())
|
||||
}
|
||||
|
||||
actual operator fun div(other: Amount): Amount {
|
||||
return Amount(amount.divide(other.amount, decimalMode).toString())
|
||||
}
|
||||
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other is Amount && this.amount == other.amount
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return amount.hashCode()
|
||||
}
|
||||
|
||||
actual override fun toString(): String = amount.toPlainString()
|
||||
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package net.codinux.banking.client.model
|
||||
|
||||
import com.ionspin.kotlin.bignum.decimal.BigDecimal
|
||||
import com.ionspin.kotlin.bignum.decimal.DecimalMode
|
||||
import com.ionspin.kotlin.bignum.decimal.RoundingMode
|
||||
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||
|
||||
@NoArgConstructor
|
||||
actual class Amount actual constructor(amount: String) {
|
||||
|
||||
actual companion object {
|
||||
actual val Zero: Amount = Amount("0.00")
|
||||
|
||||
private val decimalMode = DecimalMode(DecimalPrecision.toLong(), RoundingMode.ROUND_HALF_CEILING)
|
||||
}
|
||||
|
||||
|
||||
internal val amount: BigDecimal = BigDecimal.parseString(amount)
|
||||
|
||||
actual operator fun plus(other: Amount): Amount {
|
||||
return Amount(amount.add(other.amount).toString())
|
||||
}
|
||||
|
||||
actual operator fun minus(other: Amount): Amount {
|
||||
return Amount(amount.subtract(other.amount).toString())
|
||||
}
|
||||
|
||||
actual operator fun times(other: Amount): Amount {
|
||||
return Amount(amount.multiply(other.amount).toString())
|
||||
}
|
||||
|
||||
actual operator fun div(other: Amount): Amount {
|
||||
return Amount(amount.divide(other.amount, decimalMode).toString())
|
||||
}
|
||||
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other is Amount && this.amount == other.amount
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return amount.hashCode()
|
||||
}
|
||||
|
||||
actual override fun toString(): String = amount.toPlainString()
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package net.codinux.banking.client.model
|
||||
|
||||
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||
|
||||
@JsModule("big.js")
|
||||
@kotlin.js.JsNonModule
|
||||
open external class Big(value: String) {
|
||||
fun plus(other: Big): Big
|
||||
fun minus(other: Big): Big
|
||||
fun times(other: Big): Big
|
||||
fun div(other: Big): Big
|
||||
override fun toString(): String
|
||||
}
|
||||
|
||||
|
||||
@NoArgConstructor
|
||||
actual class Amount actual constructor(amount: String): Big(amount) {
|
||||
|
||||
actual companion object {
|
||||
actual val Zero = Amount("0.00")
|
||||
}
|
||||
|
||||
|
||||
actual operator fun plus(other: Amount): Amount {
|
||||
return Amount(super.plus(other).toString())
|
||||
}
|
||||
|
||||
actual operator fun minus(other: Amount): Amount {
|
||||
return Amount(super.minus(other).toString())
|
||||
}
|
||||
|
||||
actual operator fun times(other: Amount): Amount {
|
||||
return Amount(super.times(other).toString())
|
||||
}
|
||||
|
||||
actual operator fun div(other: Amount): Amount {
|
||||
return Amount(super.div(other).toString())
|
||||
}
|
||||
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other is Amount && this.toString() == other.toString()
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return super.hashCode()
|
||||
}
|
||||
|
||||
actual override fun toString(): String = super.toString()
|
||||
|
||||
}
|
|
@ -10,7 +10,7 @@ plugins {
|
|||
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(8)
|
||||
jvmToolchain(11)
|
||||
|
||||
jvm {
|
||||
withJava()
|
||||
|
|
|
@ -8,7 +8,6 @@ import net.codinux.banking.client.model.*
|
|||
import net.codinux.banking.client.model.AccountTransaction
|
||||
import net.codinux.banking.client.model.Amount
|
||||
import net.codinux.banking.client.model.extensions.EuropeBerlin
|
||||
import net.codinux.banking.client.model.extensions.toBigDecimal
|
||||
import net.codinux.banking.client.model.tan.*
|
||||
import net.codinux.banking.client.model.options.GetAccountDataOptions
|
||||
import net.codinux.banking.client.model.request.GetAccountDataRequest
|
||||
|
@ -238,38 +237,41 @@ open class FinTs4kMapper {
|
|||
holding.pricingTime ?: statementDate, holding.buyingDate
|
||||
)
|
||||
|
||||
private fun getTotalBalance(holding: Holding): Amount? {
|
||||
// visible for testing
|
||||
internal fun getTotalBalance(holding: Holding): Amount? {
|
||||
return if (holding.totalBalance != null) {
|
||||
mapNullableAmount(holding.totalBalance)
|
||||
} else if (holding.quantity != null && holding.marketValue != null) {
|
||||
Amount(holding.quantity!! * mapAmount(holding.marketValue!!).toBigDecimal())
|
||||
Amount((holding.quantity!! * holding.marketValue.toString().toDouble()).toString())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTotalCostPrice(holding: Holding): Amount? {
|
||||
// visible for testing
|
||||
internal fun getTotalCostPrice(holding: Holding): Amount? {
|
||||
return if (holding.totalCostPrice != null) {
|
||||
mapNullableAmount(holding.totalCostPrice)
|
||||
} else if (holding.quantity != null && holding.averageCostPrice != null) {
|
||||
Amount(holding.quantity!! * mapAmount(holding.averageCostPrice!!).toBigDecimal())
|
||||
Amount((holding.quantity!! * holding.averageCostPrice.toString().toDouble()).toString())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculatePerformance(holding: Holding): Float? {
|
||||
// visible for testing
|
||||
internal fun calculatePerformance(holding: Holding): Float? {
|
||||
val totalBalance = getTotalBalance(holding)
|
||||
val totalCostPrice = getTotalCostPrice(holding)
|
||||
|
||||
if (totalBalance != null && totalCostPrice != null) {
|
||||
return ((totalBalance.toBigDecimal() - totalCostPrice.toBigDecimal()) / totalCostPrice.toBigDecimal() * 100).toFloat()
|
||||
return ((totalBalance - totalCostPrice) / totalCostPrice).toFloat() * 100
|
||||
}
|
||||
|
||||
val marketValue = mapNullableAmount(holding.marketValue)
|
||||
val costPrice = mapNullableAmount(holding.averageCostPrice)
|
||||
if (marketValue != null && costPrice != null) {
|
||||
return ((marketValue.toBigDecimal() - costPrice.toBigDecimal()) / costPrice.toBigDecimal() * 100).toFloat()
|
||||
return ((marketValue - costPrice) / costPrice).toFloat() * 100
|
||||
}
|
||||
|
||||
return null
|
||||
|
@ -277,11 +279,11 @@ open class FinTs4kMapper {
|
|||
|
||||
protected open fun mapNullableMoney(amount: Money?) = amount?.let { mapMoney(it) }
|
||||
|
||||
protected open fun mapMoney(amount: Money) = Amount.fromString(amount.amount.string.replace(',', '.'))
|
||||
protected open fun mapMoney(amount: Money) = Amount(amount.amount.string.replace(',', '.'))
|
||||
|
||||
protected open fun mapNullableAmount(amount: net.codinux.banking.fints.model.Amount?) = amount?.let { mapAmount(it) }
|
||||
|
||||
protected open fun mapAmount(amount: net.codinux.banking.fints.model.Amount) = Amount.fromString(amount.string.replace(',', '.'))
|
||||
protected open fun mapAmount(amount: net.codinux.banking.fints.model.Amount) = Amount(amount.string.replace(',', '.'))
|
||||
|
||||
|
||||
open fun mapTanChallenge(challenge: net.codinux.banking.fints.model.TanChallenge): TanChallenge {
|
||||
|
@ -388,7 +390,7 @@ open class FinTs4kMapper {
|
|||
mapError(response)
|
||||
}
|
||||
|
||||
open fun mapToMoney(amount: Amount, currency: String): Money = Money(amount.amount, currency)
|
||||
open fun mapToMoney(amount: Amount, currency: String): Money = Money(amount.toString(), currency)
|
||||
|
||||
|
||||
protected open fun <T> mapError(response: net.dankito.banking.client.model.response.FinTsClientResponse): Response<T> {
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
package net.codinux.banking.client.fints4k
|
||||
|
||||
import net.codinux.banking.client.model.Amount
|
||||
import kotlin.test.Test
|
||||
import net.codinux.banking.fints.transactions.swift.model.Holding
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class FinTs4kMapperTest {
|
||||
|
||||
private val underTest = FinTs4kMapper()
|
||||
|
||||
|
||||
@Test
|
||||
fun getTotalBalance_TotalBalanceIsNull_CalculateByQuantityAndMarketValue() {
|
||||
val holding = Holding("", null, null, null, 4, null, null, null, fints4kAmount("13.33"))
|
||||
|
||||
val result = underTest.getTotalBalance(holding)
|
||||
|
||||
assertEquals(Amount("53.32"), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getTotalCostPrice_TotalCostPriceIsNull_CalculateByQuantityAndAverageCostPrice() {
|
||||
val holding = Holding("", null, null, null, 47, fints4kAmount("16.828"), null)
|
||||
|
||||
val result = underTest.getTotalCostPrice(holding)
|
||||
|
||||
assertEquals(Amount("790.9159999999999"), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun calculatePerformance_ByTotalBalanceAndTotalCostPrice() {
|
||||
val holding = Holding("", null, null, null, null, null, fints4kAmount("20217.12"), null, null, null, fints4kAmount("19027.04"))
|
||||
|
||||
val result = underTest.calculatePerformance(holding)
|
||||
|
||||
assertEquals(6.2546773f, result!!, 0.000001f) // for JS the result has too many decimal places, so add a tolerance
|
||||
}
|
||||
|
||||
@Test
|
||||
fun calculatePerformance_ByMarketValueAndAverageCostPrice() {
|
||||
val holding = Holding("", null, null, null, null, fints4kAmount("16.828"), null, null, fints4kAmount("16.75"), null, null)
|
||||
|
||||
val result = underTest.calculatePerformance(holding)
|
||||
|
||||
assertEquals(-0.4635132f, result!!, 0.0000001f) // for JS the result has too many decimal places, so add a tolerance
|
||||
}
|
||||
|
||||
|
||||
private fun fints4kAmount(amount: String) =
|
||||
net.codinux.banking.fints.model.Amount(amount)
|
||||
|
||||
}
|
|
@ -6,3 +6,6 @@ kotlinVersion=2.0.10
|
|||
kotlinxDateTimeVersion=0.5.0
|
||||
jsJodaTimeZoneVersion=2.3.0
|
||||
coroutinesVersion=1.8.1
|
||||
|
||||
# 0.3.10 uses Kotlin 2.0.0
|
||||
ionspinBigNumVersion=0.3.9
|
||||
|
|
|
@ -556,6 +556,11 @@ batch@0.6.1:
|
|||
resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
|
||||
integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==
|
||||
|
||||
big.js@6.0.3:
|
||||
version "6.0.3"
|
||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-6.0.3.tgz#8b4d99ac7023668e0e465d3f78c23b8ac29ad381"
|
||||
integrity sha512-n6yn1FyVL1EW2DBAr4jlU/kObhRzmr+NNRESl65VIOT8WBJj/Kezpx2zFdhJUqYI6qrtTW7moCStYL5VxeVdPA==
|
||||
|
||||
binary-extensions@^2.0.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
|
||||
|
|
Loading…
Reference in New Issue