From 003e1a59dbb8a032b1d40157690a91fd0b5bf887 Mon Sep 17 00:00:00 2001 From: Sambo Chea Date: Tue, 9 Feb 2021 12:05:02 +0700 Subject: [PATCH] Add adavanced money formatter and decimal functions for money format and add function builder and money extension updated and add money view for money module Add money formatter provider for general provider formatter --- .../kotlin/com/cubetiqs/money/DecimalUtils.kt | 61 ++++++++++++ .../cubetiqs/money/FunctionBuilderInline.kt | 21 ++++ .../kotlin/com/cubetiqs/money/MoneyConfig.kt | 96 +++++++++++++++++-- .../com/cubetiqs/money/MoneyExtension.kt | 26 ++--- .../com/cubetiqs/money/MoneyFormatProvider.kt | 17 ++++ .../com/cubetiqs/money/MoneyFormatter.kt | 61 ++++++++++++ .../kotlin/com/cubetiqs/money/MoneyView.kt | 26 +++++ src/test/kotlin/MoneyTests.kt | 5 + 8 files changed, 293 insertions(+), 20 deletions(-) create mode 100644 src/main/kotlin/com/cubetiqs/money/DecimalUtils.kt create mode 100644 src/main/kotlin/com/cubetiqs/money/FunctionBuilderInline.kt create mode 100644 src/main/kotlin/com/cubetiqs/money/MoneyFormatProvider.kt create mode 100644 src/main/kotlin/com/cubetiqs/money/MoneyFormatter.kt create mode 100644 src/main/kotlin/com/cubetiqs/money/MoneyView.kt diff --git a/src/main/kotlin/com/cubetiqs/money/DecimalUtils.kt b/src/main/kotlin/com/cubetiqs/money/DecimalUtils.kt new file mode 100644 index 0000000..513281c --- /dev/null +++ b/src/main/kotlin/com/cubetiqs/money/DecimalUtils.kt @@ -0,0 +1,61 @@ +package com.cubetiqs.money + +import java.text.DecimalFormat +import java.lang.StringBuilder +import java.math.RoundingMode + +/** + * Java Decimal utils + * + * @author sombochea + * @since 1.0 + */ +object DecimalUtils { + private const val DECIMAL_PATTERN = "#.##" + private const val ROUNDING_DECIMAL_PATTERN = "##0" + fun toStringDecimal(value: Number?, pattern: String?): String { + var _pattern = pattern + if (_pattern == null || _pattern.isEmpty()) { + _pattern = DECIMAL_PATTERN + } + return DecimalFormat(_pattern).format(value) + } + + fun toStringDecimal(value: Number?, pattern: String?, roundingMode: RoundingMode?): String { + var _pattern = pattern + if (_pattern == null || _pattern.isEmpty()) { + _pattern = DECIMAL_PATTERN + } + if (roundingMode == null) { + return toStringDecimal(value, _pattern) + } + + val formatter = DecimalFormat(_pattern) + formatter.roundingMode = roundingMode + return formatter.format(value) + } + + fun toDecimalPrecision(value: Number?, precision: Int? = null, roundingMode: RoundingMode? = null): String? { + var _precision = precision ?: -1 + if (value == null) { + return null + } + + val pattern = StringBuilder(ROUNDING_DECIMAL_PATTERN) + if (_precision > 0) { + pattern.append(".") + } + + while (_precision > 0) { + pattern.append("0") + _precision-- + } + + val decimalFormat = DecimalFormat(pattern.toString()) + if (roundingMode != null) { + decimalFormat.roundingMode = roundingMode + } + + return decimalFormat.format(value) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/cubetiqs/money/FunctionBuilderInline.kt b/src/main/kotlin/com/cubetiqs/money/FunctionBuilderInline.kt new file mode 100644 index 0000000..e2d074c --- /dev/null +++ b/src/main/kotlin/com/cubetiqs/money/FunctionBuilderInline.kt @@ -0,0 +1,21 @@ +package com.cubetiqs.money + +inline fun buildMoneyConfigProperties( + builderAction: MoneyConfig.MoneyConfigProperties.MoneyConfigPropertiesBuilder.() -> Unit +): MoneyConfig.MoneyConfigProperties { + return MoneyConfig + .builder().apply(builderAction) + .build() +} + +inline fun applyMoneyConfig( + builderAction: MoneyConfig.() -> Unit, +) { + MoneyConfig.apply(builderAction) +} + +inline fun buildMoneyFormatter( + builderAction: MoneyFormatter.() -> Unit +): MoneyFormatter { + return MoneyFormatter().apply(builderAction) +} \ No newline at end of file diff --git a/src/main/kotlin/com/cubetiqs/money/MoneyConfig.kt b/src/main/kotlin/com/cubetiqs/money/MoneyConfig.kt index 8c03a07..3605666 100644 --- a/src/main/kotlin/com/cubetiqs/money/MoneyConfig.kt +++ b/src/main/kotlin/com/cubetiqs/money/MoneyConfig.kt @@ -16,6 +16,16 @@ object MoneyConfig { */ private val config: MutableMap = mutableMapOf() + // use to format the money for each value, if have + private val configFormatter: MutableMap = mutableMapOf() + + // use to identified for config dataset with prefix mode + private var configPrefix: String = "" + // use to fallback, if the currency not found + // if the fallback greater than ZERO, then called it + // else throws + private var fallbackRate: Double = 0.0 + // validate the config, if have it's valid fun isValid(): Boolean { return config.isNotEmpty() @@ -37,6 +47,24 @@ object MoneyConfig { return MoneyConfig } + fun setConfigPrefix(prefix: String): MoneyConfig { + configPrefix = prefix + return MoneyConfig + } + + fun setFallbackRate(fallbackRate: Double) = apply { + this.fallbackRate = fallbackRate + } + + // get custom config key within currency generally + // example: myOwned_usd + private fun getConfigKey(key: String): String { + if (configPrefix.isEmpty() || configPrefix.isBlank()) { + return key + } + return "${configPrefix}_$key" + } + /** * Parse the config string to currency's map within rates * Key is money's currency (String) @@ -47,7 +75,14 @@ object MoneyConfig { fun parse(config: String, clearAllStates: Boolean = true) { // remove all states, if needed if (clearAllStates) { - MoneyConfig.config.clear() + if (configPrefix.isEmpty() || config.isBlank()) { + MoneyConfig.config.clear() + } else { + val keys = MoneyConfig.config.filter { it.key.startsWith(prefix = configPrefix) }.keys + keys.forEach { key -> + MoneyConfig.config.remove(key) + } + } } val rates = config.split(getProperties().deliSplit) @@ -60,11 +95,14 @@ object MoneyConfig { val currency = temp[0] .toUpperCase() .trim() + val key = getConfigKey(currency) val value = temp[1].toDouble() - if (MoneyConfig.config.containsKey(currency)) { - MoneyConfig.config.replace(currency, value) + + // set the value into dataset + if (MoneyConfig.config.containsKey(key)) { + MoneyConfig.config.replace(key, value) } else { - MoneyConfig.config.put(currency, value) + MoneyConfig.config.put(key, value) } } else { throw MoneyCurrencyStateException("money config format $temp is not valid!") @@ -72,15 +110,20 @@ object MoneyConfig { } } + // append the rate into dataset + // for config key are completed change inside fun appendRate(currency: String, rate: Double) = apply { val currencyKey = currency.toUpperCase().trim() - if (config.containsKey(currencyKey)) { - config.replace(currencyKey, rate) + val key = getConfigKey(currencyKey) + if (config.containsKey(key)) { + config.replace(key, rate) } else { - config[currencyKey] = rate + config[key] = rate } } + // append the rate via provider + // no need to change currency prefix fun appendRate(provider: MoneyExchangeProvider) = apply { val currency = provider.getCurrency() val rate = provider.getRate() @@ -109,8 +152,43 @@ object MoneyConfig { @Throws(MoneyCurrencyStateException::class) fun getRate(currency: StdMoney.Currency): Double { - return getConfig()[currency.getCurrency().toUpperCase().trim()] - ?: throw MoneyCurrencyStateException("money currency ${currency.getCurrency()} is not valid!") + return getConfig()[getConfigKey(currency.getCurrency().toUpperCase().trim())] + ?: if (fallbackRate > 0) fallbackRate else throw MoneyCurrencyStateException("money currency ${currency.getCurrency()} is not valid!") + } + + // apply default formatter for all not exists + fun applyDefaultFormatter( + provider: MoneyFormatProvider? = null + ) = apply { + configFormatter[MoneyFormatter.DEFAULT_FORMATTER] = buildMoneyFormatter { + setProvider(provider) + } + } + + // add money formatter by currency of each money value + fun addFormatter(currency: String, formatter: MoneyFormatProvider) = apply { + val key = getConfigKey(currency.toUpperCase().trim()) + if (configFormatter.containsKey(key)) { + configFormatter.replace(key, formatter) + } else { + configFormatter[key] = formatter + } + } + + // get formatter by currency or default + fun getFormatter(currency: String? = null): MoneyFormatter { + // apply default formatter + val formatter = (if (!currency.isNullOrEmpty()) { + val key = getConfigKey(currency.toUpperCase().trim()) + configFormatter[key] + } else { + null + }) ?: configFormatter[MoneyFormatter.DEFAULT_FORMATTER] + + return when (formatter) { + is MoneyFormatter -> formatter + else -> buildMoneyFormatter { setProvider(provider = formatter) } + } } class MoneyConfigProperties( diff --git a/src/main/kotlin/com/cubetiqs/money/MoneyExtension.kt b/src/main/kotlin/com/cubetiqs/money/MoneyExtension.kt index a3a0cde..67e9e14 100644 --- a/src/main/kotlin/com/cubetiqs/money/MoneyExtension.kt +++ b/src/main/kotlin/com/cubetiqs/money/MoneyExtension.kt @@ -73,7 +73,7 @@ infix fun Number.withCurrency(currency: String): StdMoney = this withCurrency ob // toString function for StdMoney interface fun StdMoney.asString(): String = "StdMoney(value=${getValue()}, currency=${getCurrency().getCurrency()})" -fun StdMoney.asMoneyString(): String = "${getValue()}:${getCurrency().getCurrency()}" +fun StdMoney.asMoneyString(deli: Char? = ':'): String = "${getValue()}${deli ?: ':'}${getCurrency().getCurrency()}" fun String?.fromStringToMoney(): StdMoney { val values = this?.split(":") if (values.isNullOrEmpty()) { @@ -110,16 +110,20 @@ fun StdMoney.tryToCastToMixin(): MoneyMixin { } } -inline fun buildMoneyConfigProperties( - builderAction: MoneyConfig.MoneyConfigProperties.MoneyConfigPropertiesBuilder.() -> Unit -): MoneyConfig.MoneyConfigProperties { - return MoneyConfig - .builder().apply(builderAction) - .build() +// transfer std money to money view +fun StdMoney.asMoneyView(): MoneyView { + return MoneyView(this) } -inline fun applyMoneyConfig( - builderAction: MoneyConfig.() -> Unit, -) { - MoneyConfig.apply(builderAction) +// transfer money view to std money +fun MoneyView.asStdMoney(): StdMoney { + return object : StdMoney { + override fun getCurrency(): StdMoney.Currency { + return StdMoney.initCurrency(this@asStdMoney.getCurrency()) + } + + override fun getValue(): Double { + return this@asStdMoney.getValue() + } + } } \ No newline at end of file diff --git a/src/main/kotlin/com/cubetiqs/money/MoneyFormatProvider.kt b/src/main/kotlin/com/cubetiqs/money/MoneyFormatProvider.kt new file mode 100644 index 0000000..69b965c --- /dev/null +++ b/src/main/kotlin/com/cubetiqs/money/MoneyFormatProvider.kt @@ -0,0 +1,17 @@ +package com.cubetiqs.money + +import java.math.RoundingMode + +interface MoneyFormatProvider { + fun getPattern(): String? { + return null + } + + fun getPrecision(): Int? { + return null + } + + fun getRoundingMode(): RoundingMode? { + return null + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/cubetiqs/money/MoneyFormatter.kt b/src/main/kotlin/com/cubetiqs/money/MoneyFormatter.kt new file mode 100644 index 0000000..e5354c0 --- /dev/null +++ b/src/main/kotlin/com/cubetiqs/money/MoneyFormatter.kt @@ -0,0 +1,61 @@ +package com.cubetiqs.money + +import java.io.Serializable +import java.math.RoundingMode + +/** + * Money Formatter (Final class) + * + * @see MoneyConfig for format properties for each of value within currency + * @see DecimalUtils for Utils formatter with number + */ +class MoneyFormatter( + private var pattern: String? = null, + private var precision: Int? = null, + private var roundingMode: RoundingMode? = null, +) : Serializable, StdMoneyFormation, MoneyFormatProvider { + fun setPattern(pattern: String?) = apply { this.pattern = pattern } + fun setPrecision(precision: Int?) = apply { this.precision = precision } + fun setRoundingMode(roundingMode: RoundingMode?) = apply { this.roundingMode = roundingMode } + fun setProvider(provider: MoneyFormatProvider?) = apply { + if (provider != null) { + this.pattern = provider.getPattern() + this.precision = provider.getPrecision() + this.roundingMode = provider.getRoundingMode() + } + } + + // when want to format the value for each of them, need to parse the money value here + private var value: StdMoney? = null + fun setValue(value: StdMoney?) = apply { this.value = value } + + constructor(value: StdMoney?) : this() { + this.value = value + } + + override fun getPattern() = pattern?.trim() + override fun getPrecision() = precision ?: -1 + override fun getRoundingMode() = roundingMode + + override fun format(): String { + value?.getValue() ?: return "" + + if (getPattern() == null && getPrecision() < 0 && getRoundingMode() == null) { + return value?.getValue().toString() + } + + if (getPrecision() > -1) { + return DecimalUtils.toDecimalPrecision(value?.getValue() ?: 0, getPrecision(), getRoundingMode()) ?: "" + } + + return DecimalUtils.toStringDecimal(value?.getValue() ?: 0, getPattern(), getRoundingMode()) + } + + override fun toMoneyString(overrideSymbol: Char?): String { + return value?.asMoneyString(overrideSymbol) ?: "" + } + + companion object { + const val DEFAULT_FORMATTER = "defaultFormatter" + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/cubetiqs/money/MoneyView.kt b/src/main/kotlin/com/cubetiqs/money/MoneyView.kt new file mode 100644 index 0000000..3762a79 --- /dev/null +++ b/src/main/kotlin/com/cubetiqs/money/MoneyView.kt @@ -0,0 +1,26 @@ +package com.cubetiqs.money + +open class MoneyView( + private var value: Number? = null, + private var currency: String? = null, +) : MoneyMixin { + constructor(money: StdMoney) : this() { + this.value = money.getValue() + this.currency = money.getCurrency().getCurrency() + } + + fun getValue(): Double { + return value?.toDouble() ?: 0.0 + } + + fun getCurrency(): String? { + return currency + } + + fun getFormat(): String { + return MoneyConfig + .getFormatter(getCurrency()) + .setValue(this.asStdMoney()) + .format() + } +} \ No newline at end of file diff --git a/src/test/kotlin/MoneyTests.kt b/src/test/kotlin/MoneyTests.kt index 73c40e5..d882efe 100644 --- a/src/test/kotlin/MoneyTests.kt +++ b/src/test/kotlin/MoneyTests.kt @@ -135,4 +135,9 @@ class MoneyTests { Assert.assertEquals(expected1, result1.getValue(), 0.0) Assert.assertEquals(expected2, result2.getValue(), 0.0) } + + @Test + fun moneyFormatterTest() { + + } } \ No newline at end of file