Skip to content

Commit

Permalink
Configurable Maximum Decimal Digits Number (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
efguydan authored Jul 5, 2021
1 parent 9f2e296 commit 29c613a
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 12 deletions.
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

<img src="https://raw.githubusercontent.com/cottacush/currencyEditText/master/sample.gif" width="280" />

Expand All @@ -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:<insert-latest-version-here>'
```
For versions, kindly head over to the [releases page](https://github.com/CottaCush/CurrencyEditText/releases)

## Usage

Expand Down Expand Up @@ -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
<com.cottacush.android.currencyedittext.CurrencyEditText
...
app:maxNumberOfDecimalDigits="3" />
```
or programmatically:
```kotlin
currencyEditText.setMaxNumberOfDecimalDigits(3)
```
## Getting the input value

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -44,14 +48,15 @@ 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()
}
}
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)
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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."
}

Expand Down Expand Up @@ -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))
}
}
1 change: 1 addition & 0 deletions library/src/main/res-public/values/attrs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
<attr name="currencySymbol" format="string"/>
<attr name="localeTag" format="string"/>
<attr name="useCurrencySymbolAsHint" format="boolean"/>
<attr name="maxNumberOfDecimalDigits" format="integer"/>
</declare-styleable>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
1 change: 1 addition & 0 deletions sample/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 29c613a

Please sign in to comment.