Skip to content

Commit

Permalink
Use custom logic for Rounding Down Operation
Browse files Browse the repository at this point in the history
* add custom logic for truncating extra decimal digits

* improve CurrencyRounderTest with Parameterized Tests

* fix spotless errors

* clean up code
  • Loading branch information
efguydan authored Dec 27, 2021
1 parent 927a3f4 commit 9f57f60
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,11 @@ 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
import java.text.NumberFormat
import java.text.ParseException
import java.util.Locale
import java.util.*
import kotlin.math.min

class CurrencyInputWatcher(
Expand All @@ -49,12 +47,9 @@ class CurrencyInputWatcher(
private val wholeNumberDecimalFormat =
(NumberFormat.getNumberInstance(locale) as DecimalFormat).apply {
applyPattern("#,##0")
roundingMode = RoundingMode.DOWN
}

private val fractionDecimalFormat = (NumberFormat.getNumberInstance(locale) as DecimalFormat).apply {
roundingMode = RoundingMode.DOWN
}
private val fractionDecimalFormat = (NumberFormat.getNumberInstance(locale) as DecimalFormat)

val decimalFormatSymbols: DecimalFormatSymbols
get() = wholeNumberDecimalFormat.decimalFormatSymbols
Expand Down Expand Up @@ -100,6 +95,13 @@ class CurrencyInputWatcher(
if (numberWithoutGroupingSeparator == decimalFormatSymbols.decimalSeparator.toString()) {
numberWithoutGroupingSeparator = "0$numberWithoutGroupingSeparator"
}

numberWithoutGroupingSeparator = truncateNumberToMaxDecimalDigits(
numberWithoutGroupingSeparator,
maxNumberOfDecimalPlaces,
decimalFormatSymbols.decimalSeparator
)

val parsedNumber = fractionDecimalFormat.parse(numberWithoutGroupingSeparator)!!
val selectionStartIndex = editText.selectionStart
if (hasDecimalPoint) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Copyright (c) 2019 Cotta & Cush Limited.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.cottacush.android.currencyedittext

/**
* Helper method to truncate extra decimal digits from numbers.
* Was created because the previously used approach, [java.math.RoundingMode.DOWN] approach
* didn't work correctly for some devices.
*
* @param number the original number to format
* @param maxDecimalDigits the maximum number of decimal digits permitted
* @param decimalSeparator the decimal separator of the currently selected locale
* @return a version of number that has a maximum of [maxDecimalDigits] decimal digits.
* e.g.
* - 14.333 with 2 max decimal digits return 14.33
* - 19.2 with 2 max decimal digits return 19.2
*/
fun truncateNumberToMaxDecimalDigits(
number: String,
maxDecimalDigits: Int,
decimalSeparator: Char
): String {
// Split number into whole and decimal part
val arr = number
.split(decimalSeparator)
.toMutableList()

// We should have exactly 2 elements in our string;
// the whole part and the decimal part
if (arr.size != 2) {
return number
}

// Take the first n (or shorter) from the decimal digits.
arr[1] = arr[1].take(maxDecimalDigits)

return arr.joinToString(separator = decimalSeparator.toString())
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class CurrencyInputWatcherTest {
// TODO Add more locale tests by their, tags, decimal separator and theur grouping separator
private val locales = listOf(
LocaleVars("en-NG", '.', ',', "$ "),
LocaleVars("en-US", '.', ',', "$ "),
LocaleVars("da-DK", ',', '.', "$ "),
LocaleVars("fr-CA", ',', ' ', "$ ")
)
Expand Down Expand Up @@ -399,6 +400,23 @@ class CurrencyInputWatcherTest {
}
}

@Test
fun `Should retain valid number if imputed`() {
// This test tries to replicate issue #29 which fails on some devices and passes for some.
// It however passes on my local. but might be helpful to have the test in here.
for (locale in locales) {
val currentEditTextContent = "515${locale.decimalSeparator}809"
val expectedText = "${locale.currencySymbol}515${locale.decimalSeparator}809"

val (editText, editable, watcher) = setupTestVariables(locale, decimalPlaces = 3)
`when`(editable.toString()).thenReturn(currentEditTextContent)

watcher.runAllWatcherMethods(editable)

verify(editText, times(1)).setText(expectedText)
}
}

private fun setupTestVariables(locale: LocaleVars, decimalPlaces: Int = 2): TestVars {
val editText = mock(CurrencyEditText::class.java)
val editable = mock(Editable::class.java)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Copyright (c) 2019 Cotta & Cush Limited.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.cottacush.android.currencyedittext

import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized

@RunWith(Parameterized::class)
class CurrencyRounderTest(
private val current: String,
private val expected: String,
private val decimalDigits: Int,
private val decimalSeparator: Char
) {

companion object {
private const val POINT_DECIMAL_SEPARATOR = '.'
private const val COMMA_DECIMAL_SEPARATOR = ','
private const val TWO = 2
private const val THREE = 3

private val emptyStringTestCase = arrayOf("", "", TWO, POINT_DECIMAL_SEPARATOR)
private val extraDecimalTestCase1 = arrayOf("223.55644234234", "223.55", TWO, POINT_DECIMAL_SEPARATOR)
private val extraDecimalTestCaseThreeDpVersion = arrayOf("223.55644234234", "223.556", THREE, POINT_DECIMAL_SEPARATOR)
private val extraDecimalTestCase2 = arrayOf("334,242,203.4234234", "334,242,203.423", THREE, POINT_DECIMAL_SEPARATOR)
private val extraDecimalTestCaseWhiteSpaceGrouping = arrayOf("334 242 203.4234234", "334 242 203.423", THREE, POINT_DECIMAL_SEPARATOR)
private val extraDecimalTestCaseCommaDecimal = arrayOf("334.242.203,4234234", "334.242.203,42", TWO, COMMA_DECIMAL_SEPARATOR)
private val emptyWholeTestCase = arrayOf(".4234234", ".42", TWO, POINT_DECIMAL_SEPARATOR)
private val emptyDecimalTestCase = arrayOf("4,234,234.", "4,234,234.", TWO, POINT_DECIMAL_SEPARATOR)
private val shorterDecimalTestCase = arrayOf("23.45", "23.45", THREE, POINT_DECIMAL_SEPARATOR)
private val shorterDecimalTestCase2 = arrayOf("23.4", "23.4", THREE, POINT_DECIMAL_SEPARATOR)
private val exactDecimalTestCase = arrayOf("515.809", "515.809", THREE, POINT_DECIMAL_SEPARATOR)
private val noDecimalTestCase = arrayOf("5 809", "5 809", THREE, POINT_DECIMAL_SEPARATOR)
private val noDecimalTestCase2 = arrayOf("5", "5", THREE, POINT_DECIMAL_SEPARATOR)
private val noDecimalTestCase3 = arrayOf("5,452,635,242,242,423,434,333", "5,452,635,242,242,423,434,333", THREE, POINT_DECIMAL_SEPARATOR)
private val multipleDecimalTestCase = arrayOf("343,432,242,342", "343,432,242,342", TWO, COMMA_DECIMAL_SEPARATOR)

@JvmStatic
@Parameterized.Parameters
fun data(): Iterable<Array<Any>> = listOf(
emptyStringTestCase,
extraDecimalTestCase1,
extraDecimalTestCaseThreeDpVersion,
extraDecimalTestCase2,
extraDecimalTestCaseWhiteSpaceGrouping,
extraDecimalTestCaseCommaDecimal,
emptyWholeTestCase,
emptyDecimalTestCase,
shorterDecimalTestCase,
shorterDecimalTestCase2,
exactDecimalTestCase,
noDecimalTestCase,
noDecimalTestCase2,
noDecimalTestCase3,
multipleDecimalTestCase
)
}

@Test
fun `should return expected value for set of valid inputs`() {
Assert.assertEquals(
expected,
truncateNumberToMaxDecimalDigits(current, decimalDigits, decimalSeparator)
)
}
}
2 changes: 1 addition & 1 deletion sample/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
app:currencySymbol="$"
app:useCurrencySymbolAsHint="true"
app:localeTag="en-NG"
app:maxNumberOfDecimalDigits="2"
app:maxNumberOfDecimalDigits="3"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:ems="10"
Expand Down

0 comments on commit 9f57f60

Please sign in to comment.