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()
// 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
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(

View File

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

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(expected2, result2.getValue(), 0.0)
}
@Test
fun moneyFormatterTest() {
}
}