From 29c613aa0bd0aa7177028f8eee285716fa594b52 Mon Sep 17 00:00:00 2001 From: Efe Ejemudaro Date: Mon, 5 Jul 2021 17:40:50 +0100 Subject: [PATCH] Configurable Maximum Decimal Digits Number (#27) --- README.md | 21 ++- .../currencyedittext/CurrencyEditText.kt | 16 ++- .../currencyedittext/CurrencyInputWatcher.kt | 13 +- library/src/main/res-public/values/attrs.xml | 1 + .../CurrencyInputWatcherTest.kt | 126 +++++++++++++++++- .../android/currencyedittext/Exts.kt | 2 +- sample/src/main/res/layout/activity_main.xml | 1 + 7 files changed, 168 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index de9d30f..715694d 100755 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ A library to dynamically format your `EditTexts` to take currency inputs. -[![Build Status](https://travis-ci.com/CottaCush/CurrencyEditText.svg?branch=master)](https://travis-ci.com/CottaCush/CurrencyEditText) -[ ![Download](https://api.bintray.com/packages/cottacush/maven/CurrencyEditText/images/download.svg) ](https://bintray.com/cottacush/maven/CurrencyEditText/_latestVersion) +[![ci](https://github.com/CottaCush/CurrencyEditText/actions/workflows/ci.yml/badge.svg)](https://github.com/CottaCush/CurrencyEditText/actions/workflows/ci.yml) +[![Maven Central](https://img.shields.io/maven-central/v/com.cottacush/CurrencyEditText.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22com.cottacush%22%20AND%20a:%22CurrencyEditText%22) @@ -13,8 +13,9 @@ A library to dynamically format your `EditTexts` to take currency inputs. Add the dependency to your app's `build.gradle`: ```groovy -implementation 'com.cottacush:CurrencyEditText:0.0.7' +implementation 'com.cottacush:CurrencyEditText:' ``` +For versions, kindly head over to the [releases page](https://github.com/CottaCush/CurrencyEditText/releases) ## Usage @@ -66,6 +67,20 @@ The `CurrencyEditText` uses the default `Locale` if no locale is specified. `Loc ```kotlin currencyEditText.setLocale("en-NG") //Requires API level 21 and above. ``` + +### Decimal Places +The maximum number of decimal digits can be specified using the `maxNumberOfDecimalDigits` attributes in the xml, requiring +a minimum value of 1. It has a default value of 2. + +```xml + +``` +or programmatically: +```kotlin + currencyEditText.setMaxNumberOfDecimalDigits(3) +``` ## Getting the input value diff --git a/library/src/main/java/com/cottacush/android/currencyedittext/CurrencyEditText.kt b/library/src/main/java/com/cottacush/android/currencyedittext/CurrencyEditText.kt index 0697cef..d48ca18 100644 --- a/library/src/main/java/com/cottacush/android/currencyedittext/CurrencyEditText.kt +++ b/library/src/main/java/com/cottacush/android/currencyedittext/CurrencyEditText.kt @@ -25,10 +25,14 @@ import com.google.android.material.textfield.TextInputEditText import java.math.BigDecimal import java.util.* -class CurrencyEditText(context: Context, attrs: AttributeSet?) : TextInputEditText(context, attrs) { +class CurrencyEditText( + context: Context, + attrs: AttributeSet? +) : TextInputEditText(context, attrs) { private lateinit var currencySymbolPrefix: String private var textWatcher: CurrencyInputWatcher private var locale: Locale = Locale.getDefault() + private var maxDP: Int init { var useCurrencySymbolAsHint = false @@ -44,6 +48,7 @@ class CurrencyEditText(context: Context, attrs: AttributeSet?) : TextInputEditTe prefix = getString(R.styleable.CurrencyEditText_currencySymbol).orEmpty() localeTag = getString(R.styleable.CurrencyEditText_localeTag) useCurrencySymbolAsHint = getBoolean(R.styleable.CurrencyEditText_useCurrencySymbolAsHint, false) + maxDP = getInt(R.styleable.CurrencyEditText_maxNumberOfDecimalDigits, 2) } finally { recycle() } @@ -51,7 +56,7 @@ class CurrencyEditText(context: Context, attrs: AttributeSet?) : TextInputEditTe currencySymbolPrefix = if (prefix.isBlank()) "" else "$prefix " if (useCurrencySymbolAsHint) hint = currencySymbolPrefix if (isLollipopAndAbove() && !localeTag.isNullOrBlank()) locale = getLocaleFromTag(localeTag!!) - textWatcher = CurrencyInputWatcher(this, currencySymbolPrefix, locale) + textWatcher = CurrencyInputWatcher(this, currencySymbolPrefix, locale, maxDP) addTextChangedListener(textWatcher) } @@ -72,9 +77,14 @@ class CurrencyEditText(context: Context, attrs: AttributeSet?) : TextInputEditTe invalidateTextWatcher() } + fun setMaxNumberOfDecimalDigits(maxDP: Int) { + this.maxDP = maxDP + invalidateTextWatcher() + } + private fun invalidateTextWatcher() { removeTextChangedListener(textWatcher) - textWatcher = CurrencyInputWatcher(this, currencySymbolPrefix, locale) + textWatcher = CurrencyInputWatcher(this, currencySymbolPrefix, locale, maxDP) addTextChangedListener(textWatcher) } diff --git a/library/src/main/java/com/cottacush/android/currencyedittext/CurrencyInputWatcher.kt b/library/src/main/java/com/cottacush/android/currencyedittext/CurrencyInputWatcher.kt index 07aa015..2e8f156 100644 --- a/library/src/main/java/com/cottacush/android/currencyedittext/CurrencyInputWatcher.kt +++ b/library/src/main/java/com/cottacush/android/currencyedittext/CurrencyInputWatcher.kt @@ -19,6 +19,7 @@ import android.annotation.SuppressLint import android.text.Editable import android.text.TextWatcher import android.widget.EditText +import java.lang.IllegalArgumentException import java.math.RoundingMode import java.text.DecimalFormat import java.text.DecimalFormatSymbols @@ -30,11 +31,17 @@ import kotlin.math.min class CurrencyInputWatcher( private val editText: EditText, private val currencySymbol: String, - locale: Locale + locale: Locale, + private val maxNumberOfDecimalPlaces: Int = 2 ) : TextWatcher { + init { + if (maxNumberOfDecimalPlaces < 1) { + throw IllegalArgumentException("Maximum number of Decimal Digits must be a positive integer") + } + } + companion object { - const val MAX_NO_OF_DECIMAL_PLACES = 2 const val FRACTION_FORMAT_PATTERN_PREFIX = "#,##0." } @@ -124,6 +131,6 @@ class CurrencyInputWatcher( */ private fun getFormatSequenceAfterDecimalSeparator(number: String): String { val noOfCharactersAfterDecimalPoint = number.length - number.indexOf(decimalFormatSymbols.decimalSeparator) - 1 - return "0".repeat(min(noOfCharactersAfterDecimalPoint, MAX_NO_OF_DECIMAL_PLACES)) + return "0".repeat(min(noOfCharactersAfterDecimalPoint, maxNumberOfDecimalPlaces)) } } diff --git a/library/src/main/res-public/values/attrs.xml b/library/src/main/res-public/values/attrs.xml index 36f781c..9ee26ef 100644 --- a/library/src/main/res-public/values/attrs.xml +++ b/library/src/main/res-public/values/attrs.xml @@ -4,5 +4,6 @@ + \ No newline at end of file diff --git a/library/src/test/java/com/cottacush/android/currencyedittext/CurrencyInputWatcherTest.kt b/library/src/test/java/com/cottacush/android/currencyedittext/CurrencyInputWatcherTest.kt index deaeee0..b6523d3 100644 --- a/library/src/test/java/com/cottacush/android/currencyedittext/CurrencyInputWatcherTest.kt +++ b/library/src/test/java/com/cottacush/android/currencyedittext/CurrencyInputWatcherTest.kt @@ -17,8 +17,10 @@ package com.cottacush.android.currencyedittext import android.text.Editable import com.cottacush.android.currencyedittext.model.LocaleVars +import org.junit.Assert import org.junit.Test import org.mockito.Mockito.* +import java.lang.IllegalArgumentException class CurrencyInputWatcherTest { @@ -277,12 +279,132 @@ class CurrencyInputWatcherTest { } } - private fun setupTestVariables(locale: LocaleVars): TestVars { + @Test + fun `Should keep a default of 2 decimal places when the max dp value isn't specified`() { + for (locale in locales) { + val currentEditTextContent = "${locale.currencySymbol}1${locale.groupingSeparator}320${locale.decimalSeparator}50992" + val expectedText = "${locale.currencySymbol}1${locale.groupingSeparator}320${locale.decimalSeparator}50" + + val (editText, editable, watcher) = setupTestVariables(locale) + val watcherWithDefaultDP = CurrencyInputWatcher(editText, locale.currencySymbol, locale.tag.toLocale()) + `when`(editable.toString()).thenReturn(currentEditTextContent) + + watcherWithDefaultDP.runAllWatcherMethods(editable) + + verify(editText, times(1)).setText(expectedText) + } + } + + @Test + fun `Should throw an Exception when maximum dp is set to zero`() { + for (locale in locales) { + try { + val (editText, editable, watcher) = setupTestVariables(locale, decimalPlaces = 0) + Assert.fail("Should have caught an illegalArgumentException at this point") + } catch (e: IllegalArgumentException) { } + } + } + + @Test + fun `Should keep only one decimal place when maximum dp is set to 1`() { + for (locale in locales) { + val currentEditTextContent = "${locale.currencySymbol}1${locale.groupingSeparator}320${locale.decimalSeparator}50992" + val expectedText = "${locale.currencySymbol}1${locale.groupingSeparator}320${locale.decimalSeparator}5" + + val (editText, editable, watcher) = setupTestVariables(locale, decimalPlaces = 1) + `when`(editable.toString()).thenReturn(currentEditTextContent) + + watcher.runAllWatcherMethods(editable) + + verify(editText, times(1)).setText(expectedText) + } + } + + @Test + fun `Should keep only two decimal places when maximum dp is set to 2`() { + for (locale in locales) { + val currentEditTextContent = "${locale.currencySymbol}1${locale.groupingSeparator}320${locale.decimalSeparator}51992" + val expectedText = "${locale.currencySymbol}1${locale.groupingSeparator}320${locale.decimalSeparator}51" + + val (editText, editable, watcher) = setupTestVariables(locale, decimalPlaces = 2) + `when`(editable.toString()).thenReturn(currentEditTextContent) + + watcher.runAllWatcherMethods(editable) + + verify(editText, times(1)).setText(expectedText) + } + } + + @Test + fun `Should keep only three decimal places when maximum dp is set to 3`() { + for (locale in locales) { + val currentEditTextContent = "${locale.currencySymbol}1${locale.groupingSeparator}320${locale.decimalSeparator}51992" + val expectedText = "${locale.currencySymbol}1${locale.groupingSeparator}320${locale.decimalSeparator}519" + + val (editText, editable, watcher) = setupTestVariables(locale, decimalPlaces = 3) + `when`(editable.toString()).thenReturn(currentEditTextContent) + + watcher.runAllWatcherMethods(editable) + + verify(editText, times(1)).setText(expectedText) + } + } + + @Test + fun `Should keep only seven decimal digits when maximum dp is set to 7`() { + for (locale in locales) { + val currentEditTextContent = "${locale.currencySymbol}1${locale.groupingSeparator}320${locale.decimalSeparator}519923345634" + val expectedText = "${locale.currencySymbol}1${locale.groupingSeparator}320${locale.decimalSeparator}5199233" + + val (editText, editable, watcher) = setupTestVariables(locale, decimalPlaces = 7) + `when`(editable.toString()).thenReturn(currentEditTextContent) + + watcher.runAllWatcherMethods(editable) + + verify(editText, times(1)).setText(expectedText) + } + } + + @Test + fun `Should keep up to ten decimal places when maximum dp is set to 10`() { + for (locale in locales) { + val currentEditTextContent = "${locale.currencySymbol}1${locale.groupingSeparator}320${locale.decimalSeparator}519923345634" + val expectedText = "${locale.currencySymbol}1${locale.groupingSeparator}320${locale.decimalSeparator}5199233456" + + val (editText, editable, watcher) = setupTestVariables(locale, decimalPlaces = 10) + `when`(editable.toString()).thenReturn(currentEditTextContent) + + watcher.runAllWatcherMethods(editable) + + verify(editText, times(1)).setText(expectedText) + } + } + + @Test + fun `Should change maximum decimal digits to 3 if setMaxNumberOfDecimalDigits(3) is called after being init with decimal digits 2`() { + for (locale in locales) { + val currentEditTextContent = "${locale.currencySymbol}1${locale.groupingSeparator}320${locale.decimalSeparator}519923345634" + val firstExpectedText = "${locale.currencySymbol}1${locale.groupingSeparator}320${locale.decimalSeparator}51" + val secondExpectedText = "${locale.currencySymbol}1${locale.groupingSeparator}320${locale.decimalSeparator}519" + + val (editText, editable, watcher) = setupTestVariables(locale, decimalPlaces = 2) + val secondWatcher = locale.toWatcher(editText, 3) + `when`(editable.toString()).thenReturn(currentEditTextContent) + + watcher.runAllWatcherMethods(editable) + secondWatcher.runAllWatcherMethods(editable) + + verify(editText, times(1)).setText(firstExpectedText) + verify(editText, times(1)).setText(secondExpectedText) + } + } + + private fun setupTestVariables(locale: LocaleVars, decimalPlaces: Int = 2): TestVars { val editText = mock(CurrencyEditText::class.java) val editable = mock(Editable::class.java) `when`(editText.text).thenReturn(editable) `when`(editable.append(isA(String::class.java))).thenReturn(editable) - val watcher = locale.toWatcher(editText) + val watcher = locale.toWatcher(editText, decimalPlaces) return TestVars(editText, editable, watcher) } } diff --git a/library/src/test/java/com/cottacush/android/currencyedittext/Exts.kt b/library/src/test/java/com/cottacush/android/currencyedittext/Exts.kt index a6d58de..7a35ea6 100644 --- a/library/src/test/java/com/cottacush/android/currencyedittext/Exts.kt +++ b/library/src/test/java/com/cottacush/android/currencyedittext/Exts.kt @@ -41,4 +41,4 @@ fun String.removeFirstChar() = removeCharAt(0) fun String.toLocale(): Locale = Locale.Builder().setLanguageTag(this).build() -fun LocaleVars.toWatcher(editText: EditText) = CurrencyInputWatcher(editText, currencySymbol, tag.toLocale()) +fun LocaleVars.toWatcher(editText: EditText, decimalPlaces: Int = 2) = CurrencyInputWatcher(editText, currencySymbol, tag.toLocale(), decimalPlaces) diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml index 44c3b61..66659b1 100644 --- a/sample/src/main/res/layout/activity_main.xml +++ b/sample/src/main/res/layout/activity_main.xml @@ -11,6 +11,7 @@ app:currencySymbol="$" app:useCurrencySymbolAsHint="true" app:localeTag="en-NG" + app:maxNumberOfDecimalDigits="2" android:layout_width="wrap_content" android:layout_height="60dp" android:ems="10"