Compare commits

...

13 Commits

Author SHA1 Message Date
9b328c93c7 Updated the uppercase and lowercase 2022-12-16 20:49:57 +07:00
6deb73600d Updated 2022-12-07 08:10:25 +07:00
chhoeung-mengsreang
aaf3c6aea8 Task: add computeFromBaseRate function in money-modules 2021-11-30 17:01:43 +07:00
c4e7f514cb Removed gradle wrapper from money module 2021-07-29 09:02:18 +07:00
f6914f5282 Updated the money value tests 2021-04-19 08:40:02 +07:00
88abd05184 Updated and changes for money-module 2021-03-25 16:55:43 +07:00
Sambo Chea
78578cf1cc Update README.md 2021-03-18 20:34:50 +07:00
Sambo Chea
c5395b89ef Create README.md 2021-03-07 13:00:03 +07:00
0a330a0bc1 Updated and changes for money-module 2021-02-18 08:59:04 +07:00
f89b0cf01d Fixed auto formatter and auto currency with default from system and custom formatter override and check and extract function 2021-02-09 15:36:10 +07:00
8509c578ca Add money config for locale and currency from system and set default and configuratoion auto with apply default locale and currency
But for auto translate from currency to view not have yet
We need to create a new file or use exist model to generate the locale format with custom currency from system or use defined
2021-02-09 13:54:59 +07:00
1a447f1954 Add config and formatter data set to concurrent map 2021-02-09 12:30:24 +07:00
0676972a83 Add find currency from java built-in and fixed for money view 2021-02-09 12:27:31 +07:00
13 changed files with 398 additions and 48 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

139
README.md Normal file
View File

@@ -0,0 +1,139 @@
# Money Object Module
- Allow to do better with money module
- Exchangeable for different currencies with custom override
- Add money config able to custom by set of config
- So on.
### Example (Tests)
```kotlin
import com.cubetiqs.money.*
import org.junit.Assert
import org.junit.Test
import java.util.*
class MoneyTests {
private fun initMoneyConfig() {
applyMoneyConfig {
setProperties(buildMoneyConfigProperties {
setDeliEqual(':')
setDeliSplit(',')
})
fromJsonRates(MyBatchRates.getJsonRates())
}
}
object MyBatchRates {
fun getJsonRates(): String {
return """
{"USD": 1.0,"KHR": 4000.0, "eur": 0.5}
""".trimIndent()
}
}
@Test
fun exchange_2usd_to_khr_test() {
initMoneyConfig()
// Is valid for money config?
Assert.assertTrue(MoneyConfig.isConfigRatesValid())
// arithmetic operators calculation
val moneyUsd =
(2 withCurrency "usd") divideWith (2 withCurrency "usd") plusOf 1 minusOf 1 plusOf 1 multiplyOf 2 divideOf 2 divideWith (8000 withCurrency "khr") plusOf 1
val moneyKhr = moneyUsd exchangeTo "khr"
// Is correct exchange?
Assert.assertEquals(8000.0, moneyKhr.getValue(), 0.0)
// complex operators and exchanging
val sum =
((moneyUsd + moneyKhr) * Money.TEN) exchangeTo MoneyCurrency.KHR minusWith (Money.ONE exchangeTo MoneyCurrency.KHR)
Assert.assertEquals(156000.0, sum.getValue(), 0.0)
}
@Test
fun moneyGenerator() {
initMoneyConfig()
val moneyGen = MoneyObject(
value = 1.0,
currency = "usd",
operator = MoneyObject.MoneyOperator.PLUS,
with = MoneyObject(
value = 8000.0,
currency = "khr",
operator = MoneyObject.MoneyOperator.MINUS,
with = MoneyObject(
value = 1000.0,
currency = "khr",
)
)
)
val result = moneyGen.compute()
Assert.assertEquals(2.75, result.getValue(), 0.0)
}
@Test
fun moneyGeneratorBuilder() {
initMoneyConfig()
val expected = 72000.0
val builder = MoneyObject.builder()
.with(10.0, "usd", '+')
.with(1.5, "eur", '+')
.with(8000.0, "khr")
.with(10000.0, "khr")
.with(2000.0, "khr")
.with(.5, "eur", '-')
.with(1.0, "usd")
.withCurrency("khr")
.build()
val result = builder.compute()
Assert.assertEquals(expected, result.getValue(), 0.0)
}
@Test
fun moneyGeneratorBuilderWithStringValues() {
initMoneyConfig()
val values = "usd:1:+,khr:4000:-,usd:1:+,eur:1:+" // result = 3
val expected1 = 3.0
val expected2 = 3.5
val builder = MoneyObject.builder()
.withCurrency("usd")
.with(1.0, "usd", '-')
.with(4000.0, "khr")
.with(1.0, "usd")
.with(1.0, "usd")
.with(1.0, "usd")
.with(1.0, "usd")
.with(1.0, "eur", '-') // 2 usd
.with(1.0, "usd")
.with(2.0, "usd", '/')
.with(2.0, "usd")
.build() // 3.5
val result1 = MoneyObject.builder()
.parseFromString(values)
.withCurrency("usd")
.build()
.compute()
val result2 = builder.compute()
Assert.assertEquals(expected1, result1.getValue(), 0.0)
Assert.assertEquals(expected2, result2.getValue(), 0.0)
}
@Test
fun moneyFormatterTest() {
val systemCurrency = Currency.getInstance("USD").symbol
println(systemCurrency)
}
}
```

Binary file not shown.

View File

@@ -1,5 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -1,5 +1,10 @@
package com.cubetiqs.money
import java.lang.Exception
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
/**
* Default money config in static object.
* Sample parse format: USD=1,KHR=4000,EUR=0.99
@@ -8,27 +13,74 @@ package com.cubetiqs.money
* @since 1.0
*/
object MoneyConfig {
private const val CONFIG_RATE_INITIAL_SIZE = 10
private const val CONFIG_FORMATTER_INITIAL_SIZE = 10
private const val LOCALES_INITIAL_SIZE = 5
private const val CURRENCIES_INITIAL_SIZE = 5
private const val CONFIG_INITIAL_SIZE = 50
/**
* All money currencies and its rate are stored in memory state for exchange or compute the value.
*
* Key is the currency
* Value is the rate
*/
private val config: MutableMap<String, Double> = mutableMapOf()
private val configRates: ConcurrentMap<String, Double> = ConcurrentHashMap(CONFIG_RATE_INITIAL_SIZE)
// use to format the money for each value, if have
private val configFormatter: MutableMap<String, MoneyFormatProvider> = mutableMapOf()
private val configFormatter: ConcurrentMap<String, MoneyFormatProvider> =
ConcurrentHashMap(CONFIG_FORMATTER_INITIAL_SIZE)
// use to custom locales and allow to detect auto formatter, if enable auto locale format
private val configLocales: ConcurrentMap<String, Locale> = ConcurrentHashMap(LOCALES_INITIAL_SIZE)
// use to custom currencies and allow to detect auto, when use auto-detect for currencies translate
private val configCurrencies: ConcurrentMap<String, Currency> = ConcurrentHashMap(CURRENCIES_INITIAL_SIZE)
// allow to use config of any states
private val configs: ConcurrentMap<String, Any?> = ConcurrentHashMap(CONFIG_INITIAL_SIZE)
// allow to set the value into custom config
fun setConfig(key: String, value: Any?) = apply {
if (configs.containsKey(key)) {
configs.replace(key, value)
} else {
this.configs[key] = value
}
}
// allow to get the value from custom config
fun <T> getConfigOrDefault(key: String, defaultValue: T? = null): T? {
return (try {
@Suppress("UNCHECKED_CAST")
configs[key] as? T
} catch (ex: Exception) {
null
}) ?: defaultValue
}
// use to identified for config dataset with prefix mode
private var configPrefix: String = ""
private fun isConfigPrefixValid() = configPrefix.isNotEmpty() && configPrefix.isNotBlank()
// 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()
fun getConfigs(): Map<String, Any?> {
return mapOf(
"configRates" to configRates,
"configFormatter" to configFormatter,
"configLocales" to configLocales,
"configCurrencies" to configCurrencies,
"configs" to configs,
)
}
// validate the config rates, if have it's valid
fun isConfigRatesValid(): Boolean {
return configRates.isNotEmpty()
}
/**
@@ -56,6 +108,26 @@ object MoneyConfig {
this.fallbackRate = fallbackRate
}
fun setAutoLocaleFormatter(enabled: Boolean) = apply {
setConfig(getConfigKey("autoLocaleFormatter"), enabled)
}
fun setAutoCurrencyFormatter(enabled: Boolean) = apply {
setConfig(getConfigKey("autoCurrencyFormatter"), enabled)
}
// use to auto format, when this true
// and formatter for money value, with current locale or custom set locale
fun isAutoLocaleFormatterEnabled(): Boolean {
return getConfigOrDefault(getConfigKey("autoLocaleFormatter"), false) ?: false
}
// use to auto format, when this true
// and formatter for money value, with current currency or custom set currency
fun isAutoCurrencyFormatterEnabled(): Boolean {
return getConfigOrDefault(getConfigKey("autoCurrencyFormatter"), false) ?: false
}
// get custom config key within currency generally
// example: myOwned_usd
private fun getConfigKey(key: String): String {
@@ -75,12 +147,12 @@ object MoneyConfig {
fun parse(config: String, clearAllStates: Boolean = true) {
// remove all states, if needed
if (clearAllStates) {
if (configPrefix.isEmpty() || config.isBlank()) {
MoneyConfig.config.clear()
if (!isConfigPrefixValid()) {
configRates.clear()
} else {
val keys = MoneyConfig.config.filter { it.key.startsWith(prefix = configPrefix) }.keys
val keys = configRates.filter { it.key.startsWith(prefix = configPrefix) }.keys
keys.forEach { key ->
MoneyConfig.config.remove(key)
configRates.remove(key)
}
}
}
@@ -93,16 +165,16 @@ object MoneyConfig {
.split(getProperties().deliEqual)
if (temp.size == 2) {
val currency = temp[0]
.toUpperCase()
.uppercase()
.trim()
val key = getConfigKey(currency)
val value = temp[1].toDouble()
// set the value into dataset
if (MoneyConfig.config.containsKey(key)) {
MoneyConfig.config.replace(key, value)
if (configRates.containsKey(key)) {
configRates.replace(key, value)
} else {
MoneyConfig.config.put(key, value)
configRates.put(key, value)
}
} else {
throw MoneyCurrencyStateException("money config format $temp is not valid!")
@@ -113,12 +185,12 @@ 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()
val currencyKey = currency.uppercase().trim()
val key = getConfigKey(currencyKey)
if (config.containsKey(key)) {
config.replace(key, rate)
if (configRates.containsKey(key)) {
configRates.replace(key, rate)
} else {
config[key] = rate
configRates[key] = rate
}
}
@@ -148,14 +220,81 @@ object MoneyConfig {
}
// all currencies with its rate
fun getConfig() = config
fun getConfigRates() = configRates
@Throws(MoneyCurrencyStateException::class)
fun getRate(currency: StdMoney.Currency): Double {
return getConfig()[getConfigKey(currency.getCurrency().toUpperCase().trim())]
return getConfigRates()[getConfigKey(currency.getCurrency().uppercase().trim())]
?: if (fallbackRate > 0) fallbackRate else throw MoneyCurrencyStateException("money currency ${currency.getCurrency()} is not valid!")
}
// apply custom locale below
fun applyDefaultLocale(locale: Locale = Locale.getDefault()) = apply {
configLocales[MoneyFormatter.DEFAULT_LOCALE] = locale
}
// get default locale from system or user defined
private fun getDefaultLocale(): Locale {
return configLocales[MoneyFormatter.DEFAULT_LOCALE] ?: Locale.getDefault()
}
// add custom locale
fun applyLocale(locale: Locale) = apply {
if (isConfigPrefixValid()) {
if (configLocales.containsKey(configPrefix)) {
configLocales.replace(configPrefix, locale)
} else {
configLocales[configPrefix] = locale
}
}
}
// get locale by config prefix or default if not found
fun getLocale(): Locale {
if (isConfigPrefixValid() && configLocales.containsKey(configPrefix)) {
return configLocales[configPrefix] ?: getDefaultLocale()
}
return getDefaultLocale()
}
// get default currency from system or user defined
private fun getDefaultCurrency(): Currency {
return configCurrencies[MoneyFormatter.DEFAULT_CURRENCY] ?: Currency.getInstance(getDefaultLocale())
}
// apply the default currency for system at runtime
fun applyDefaultCurrency(currency: StdMoney.Currency) = apply {
configCurrencies[MoneyFormatter.DEFAULT_CURRENCY] = currency.findCurrency()
}
// apply the currency into the config for system currency at runtime
fun applyCurrency(currency: StdMoney.Currency) = apply {
if (isConfigPrefixValid()) {
val systemCurrency = currency.findCurrency()
if (systemCurrency != null) {
if (configCurrencies.containsKey(configPrefix)) {
configCurrencies.replace(configPrefix, systemCurrency)
} else {
configCurrencies[configPrefix] = systemCurrency
}
}
}
}
// get currency by config prefix or default if not found
fun getCurrency(): Currency {
if (isConfigPrefixValid() && configCurrencies.containsKey(configPrefix)) {
return configCurrencies[configPrefix] ?: getDefaultCurrency()
}
return getDefaultCurrency()
}
fun getCurrencySymbol(): String {
return getCurrency().getSymbol(getDefaultLocale())
}
// apply default formatter for all not exists
fun applyDefaultFormatter(
provider: MoneyFormatProvider? = null
@@ -167,7 +306,7 @@ object MoneyConfig {
// add money formatter by currency of each money value
fun addFormatter(currency: String, formatter: MoneyFormatProvider) = apply {
val key = getConfigKey(currency.toUpperCase().trim())
val key = getConfigKey(currency.uppercase().trim())
if (configFormatter.containsKey(key)) {
configFormatter.replace(key, formatter)
} else {
@@ -179,7 +318,7 @@ object MoneyConfig {
fun getFormatter(currency: String? = null): MoneyFormatter {
// apply default formatter
val formatter = (if (!currency.isNullOrEmpty()) {
val key = getConfigKey(currency.toUpperCase().trim())
val key = getConfigKey(currency.uppercase().trim())
configFormatter[key]
} else {
null

View File

@@ -14,7 +14,7 @@ open class MoneyCurrency(
private var configs: Map<String, Any?>? = null,
) : Serializable, StdMoney.Currency {
override fun getCurrency(): String {
return name.toUpperCase().trim()
return name.uppercase().trim()
}
fun getSymbol(): String? {

View File

@@ -14,4 +14,23 @@ object MoneyExchangeUtils {
fun getBaseCurrency(): StdMoney.Currency {
return StdMoney.USD
}
/**
* Compute the Base Exchange Price / Converter to any Rates.
* Multiple differentiate exchange relation computation.
* Example: A -> B -> C meant You can with all variables, but must find the based MEANT of its.
*
* Sample Explanation: Matrix Multiply
* 1 USD -> 0.90 EUR
* 1 USD -> 4000 KHR
* If I want to exchange from EUR to KHR, I need.
* 1 EUR -> 1 * (1 / 0.90) / (1 / 4000)
*
* @author sombochea
* @since 1.0
*/
fun computeFromBaseRate(amountFrom: Double = 1.0, baseRate: Double = 1.0, rateFrom: Double, rateTo: Double): Double {
// amount * ((baseRate / rateFrom) / (baseRate / rateTo))
return amountFrom.times((baseRate.div(rateFrom)).div(baseRate.div(rateTo)))
}
}

View File

@@ -1,12 +1,14 @@
package com.cubetiqs.money
import java.util.*
infix fun StdMoney.exchangeTo(currency: StdMoney.Currency): StdMoney {
return MoneyExchangeUtils.exchange(this, currency)
}
infix fun StdMoney.exchangeTo(currency: String): StdMoney = this exchangeTo object : StdMoney.Currency {
override fun getCurrency(): String {
return currency.toUpperCase().trim()
return currency.uppercase().trim()
}
}
@@ -67,7 +69,7 @@ infix fun Number.withCurrency(currency: StdMoney.Currency): StdMoney = object :
infix fun Number.withCurrency(currency: String): StdMoney = this withCurrency object : StdMoney.Currency {
override fun getCurrency(): String {
return currency.toUpperCase().trim()
return currency.uppercase().trim()
}
}
@@ -127,3 +129,8 @@ fun MoneyView.asStdMoney(): StdMoney {
}
}
}
// detect currency symbol, if needed
fun StdMoney.Currency.findCurrency(): Currency? {
return Currency.getInstance(this.getCurrency())
}

View File

@@ -2,6 +2,7 @@ package com.cubetiqs.money
import java.io.Serializable
import java.math.RoundingMode
import java.text.NumberFormat
/**
* Money Formatter (Final class)
@@ -25,6 +26,12 @@ class MoneyFormatter(
}
}
// allow to force the auto format from money config
private var disableAutoFormat: Boolean = true
fun setDisableAutoFormat(disabled: Boolean) = apply {
this.disableAutoFormat = disabled
}
// 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 }
@@ -40,8 +47,24 @@ class MoneyFormatter(
override fun format(): String {
value?.getValue() ?: return ""
var autoFormat = if (disableAutoFormat) {
null
} else {
autoFormatValueFromConfig(force = false)
}
if (!autoFormat.isNullOrEmpty()) {
return autoFormat
}
// if don't have the pattern, precision and rounding, will able to use system auto format enabled
if (getPattern() == null && getPrecision() < 0 && getRoundingMode() == null) {
return value?.getValue().toString()
autoFormat = autoFormatValueFromConfig(force = true)
return if (autoFormat.isNullOrEmpty()) {
value?.getValue().toString()
} else {
autoFormat
}
}
if (getPrecision() > -1) {
@@ -55,7 +78,30 @@ class MoneyFormatter(
return value?.asMoneyString(overrideSymbol) ?: ""
}
private fun autoFormatValueFromConfig(force: Boolean): String? {
if (force || MoneyConfig.isAutoLocaleFormatterEnabled()) {
val systemCurrency = if (MoneyConfig.isAutoCurrencyFormatterEnabled()) {
MoneyConfig.getCurrency()
} else {
value?.getCurrency()?.findCurrency()
}
if (systemCurrency != null) {
val numberFormatter = NumberFormat.getNumberInstance(MoneyConfig.getLocale())
if (getRoundingMode() != null) {
numberFormatter.roundingMode = getRoundingMode()
}
numberFormatter.currency = systemCurrency
return numberFormatter.format(value?.getValue())
}
}
return null
}
companion object {
const val DEFAULT_FORMATTER = "defaultFormatter"
const val DEFAULT_LOCALE = "defaultLocale"
const val DEFAULT_CURRENCY = "defaultCurrency"
}
}

View File

@@ -105,7 +105,7 @@ open class MoneyObject(
if (temp.length == 1) {
operator(temp[0])
} else {
valueOf(temp.toUpperCase().trim())
valueOf(temp.uppercase().trim())
}
}
is Char -> {

View File

@@ -4,23 +4,37 @@ open class MoneyView(
private var value: Number? = null,
private var currency: String? = null,
) : MoneyMixin {
constructor(money: StdMoney) : this() {
private var _currency: StdMoney.Currency? = null
constructor(value: StdMoney) : this() {
val money = if (MoneyConfig.isAutoCurrencyFormatterEnabled()) {
value exchangeTo MoneyConfig.getCurrency().currencyCode
} else {
value
}
this._currency = money.getCurrency()
this.value = money.getValue()
this.currency = money.getCurrency().getCurrency()
this.currency = this._currency?.getCurrency()
}
fun getValue(): Double {
return value?.toDouble() ?: 0.0
return (value?.toDouble() ?: 0.0)
}
fun getCurrency(): String? {
return currency
}
fun getSymbol(): String? {
return this._currency?.findCurrency()?.symbol
}
fun getFormat(): String {
return MoneyConfig
.getFormatter(getCurrency())
.setValue(this.asStdMoney())
.setDisableAutoFormat(true)
.format()
}
}

View File

@@ -60,7 +60,7 @@ interface StdMoney : MoneyMixin {
fun initCurrency(currency: String?): Currency {
return object : Currency {
override fun getCurrency(): String {
return currency?.toUpperCase()?.trim() ?: "USD"
return currency?.uppercase()?.trim() ?: "USD"
}
}
}

View File

@@ -1,6 +1,7 @@
import com.cubetiqs.money.*
import org.junit.Assert
import org.junit.Test
import java.util.*
class MoneyTests {
private fun initMoneyConfig() {
@@ -26,19 +27,8 @@ class MoneyTests {
fun exchange_2usd_to_khr_test() {
initMoneyConfig()
// applyMoneyConfig {
// setProperties(buildMoneyConfigProperties {
// setDeliEqual(':')
// setDeliSplit(',')
// })
// // parse("USD:1,KHR:4000")
// // appendRate("usd", 1.0)
// // appendRate("khr", 4000.0)
// fromJson(MyBatchRates.getJsonRates())
// }
// Is valid for money config?
Assert.assertTrue(MoneyConfig.isValid())
Assert.assertTrue(MoneyConfig.isConfigRatesValid())
// arithmetic operators calculation
val moneyUsd =
@@ -138,6 +128,7 @@ class MoneyTests {
@Test
fun moneyFormatterTest() {
val systemCurrency = Currency.getInstance("USD").symbol
println(systemCurrency)
}
}