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
This commit is contained in:
Sambo Chea 2021-02-09 12:05:02 +07:00
parent 7497e11ee3
commit 003e1a59db
8 changed files with 293 additions and 20 deletions

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -16,6 +16,16 @@ object MoneyConfig {
*/ */
private val config: MutableMap<String, Double> = mutableMapOf() private val config: MutableMap<String, Double> = mutableMapOf()
// use to format the money for each value, if have
private val configFormatter: MutableMap<String, MoneyFormatProvider> = 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 // validate the config, if have it's valid
fun isValid(): Boolean { fun isValid(): Boolean {
return config.isNotEmpty() return config.isNotEmpty()
@ -37,6 +47,24 @@ object MoneyConfig {
return 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 * Parse the config string to currency's map within rates
* Key is money's currency (String) * Key is money's currency (String)
@ -47,7 +75,14 @@ object MoneyConfig {
fun parse(config: String, clearAllStates: Boolean = true) { fun parse(config: String, clearAllStates: Boolean = true) {
// remove all states, if needed // remove all states, if needed
if (clearAllStates) { if (clearAllStates) {
if (configPrefix.isEmpty() || config.isBlank()) {
MoneyConfig.config.clear() 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) val rates = config.split(getProperties().deliSplit)
@ -60,11 +95,14 @@ object MoneyConfig {
val currency = temp[0] val currency = temp[0]
.toUpperCase() .toUpperCase()
.trim() .trim()
val key = getConfigKey(currency)
val value = temp[1].toDouble() 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 { } else {
MoneyConfig.config.put(currency, value) MoneyConfig.config.put(key, value)
} }
} else { } else {
throw MoneyCurrencyStateException("money config format $temp is not valid!") 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 { fun appendRate(currency: String, rate: Double) = apply {
val currencyKey = currency.toUpperCase().trim() val currencyKey = currency.toUpperCase().trim()
if (config.containsKey(currencyKey)) { val key = getConfigKey(currencyKey)
config.replace(currencyKey, rate) if (config.containsKey(key)) {
config.replace(key, rate)
} else { } else {
config[currencyKey] = rate config[key] = rate
} }
} }
// append the rate via provider
// no need to change currency prefix
fun appendRate(provider: MoneyExchangeProvider) = apply { fun appendRate(provider: MoneyExchangeProvider) = apply {
val currency = provider.getCurrency() val currency = provider.getCurrency()
val rate = provider.getRate() val rate = provider.getRate()
@ -109,8 +152,43 @@ object MoneyConfig {
@Throws(MoneyCurrencyStateException::class) @Throws(MoneyCurrencyStateException::class)
fun getRate(currency: StdMoney.Currency): Double { fun getRate(currency: StdMoney.Currency): Double {
return getConfig()[currency.getCurrency().toUpperCase().trim()] return getConfig()[getConfigKey(currency.getCurrency().toUpperCase().trim())]
?: throw MoneyCurrencyStateException("money currency ${currency.getCurrency()} is not valid!") ?: 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( class MoneyConfigProperties(

View File

@ -73,7 +73,7 @@ infix fun Number.withCurrency(currency: String): StdMoney = this withCurrency ob
// toString function for StdMoney interface // toString function for StdMoney interface
fun StdMoney.asString(): String = "StdMoney(value=${getValue()}, currency=${getCurrency().getCurrency()})" 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 { fun String?.fromStringToMoney(): StdMoney {
val values = this?.split(":") val values = this?.split(":")
if (values.isNullOrEmpty()) { if (values.isNullOrEmpty()) {
@ -110,16 +110,20 @@ fun StdMoney.tryToCastToMixin(): MoneyMixin {
} }
} }
inline fun buildMoneyConfigProperties( // transfer std money to money view
builderAction: MoneyConfig.MoneyConfigProperties.MoneyConfigPropertiesBuilder.() -> Unit fun StdMoney.asMoneyView(): MoneyView {
): MoneyConfig.MoneyConfigProperties { return MoneyView(this)
return MoneyConfig
.builder().apply(builderAction)
.build()
} }
inline fun applyMoneyConfig( // transfer money view to std money
builderAction: MoneyConfig.() -> Unit, fun MoneyView.asStdMoney(): StdMoney {
) { return object : StdMoney {
MoneyConfig.apply(builderAction) override fun getCurrency(): StdMoney.Currency {
return StdMoney.initCurrency(this@asStdMoney.getCurrency())
}
override fun getValue(): Double {
return this@asStdMoney.getValue()
}
}
} }

View File

@ -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
}
}

View File

@ -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"
}
}

View File

@ -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()
}
}

View File

@ -135,4 +135,9 @@ class MoneyTests {
Assert.assertEquals(expected1, result1.getValue(), 0.0) Assert.assertEquals(expected1, result1.getValue(), 0.0)
Assert.assertEquals(expected2, result2.getValue(), 0.0) Assert.assertEquals(expected2, result2.getValue(), 0.0)
} }
@Test
fun moneyFormatterTest() {
}
} }