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