Skip to content

Commit fd7f5de

Browse files
committed
fix: handle limit decimal places
1 parent 6b0900f commit fd7f5de

File tree

1 file changed

+100
-22
lines changed

1 file changed

+100
-22
lines changed

app/src/main/java/to/bitkit/ui/utils/visualTransformation/MonetaryVisualTransformation.kt

Lines changed: 100 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import androidx.compose.ui.text.input.VisualTransformation
77
import java.text.DecimalFormat
88
import java.text.DecimalFormatSymbols
99
import java.util.Locale
10+
import kotlin.text.iterator
1011

1112
class MonetaryVisualTransformation(
1213
private val decimalPlaces: Int = 2
@@ -19,66 +20,143 @@ class MonetaryVisualTransformation(
1920
return TransformedText(text, OffsetMapping.Identity)
2021
}
2122

22-
val formattedText = formatMonetaryValue(originalText)
23-
val offsetMapping = createOffsetMapping(originalText, formattedText)
23+
// Limit decimal places before formatting
24+
val limitedText = limitDecimalPlaces(originalText)
25+
val formattedText = formatMonetaryValue(limitedText)
26+
val offsetMapping = createOffsetMapping(limitedText, formattedText)
2427

2528
return TransformedText(
2629
AnnotatedString(formattedText),
2730
offsetMapping
2831
)
2932
}
3033

31-
private fun formatMonetaryValue(text: String): String {
34+
private fun limitDecimalPlaces(text: String): String {
3235
val cleanText = text.replace(",", "").replace(" ", "")
33-
val doubleValue = cleanText.toDoubleOrNull() ?: return text
36+
37+
val decimalIndex = cleanText.indexOf('.')
38+
if (decimalIndex == -1) {
39+
return cleanText
40+
}
41+
42+
val integerPart = cleanText.substring(0, decimalIndex)
43+
val decimalPart = cleanText.substring(decimalIndex + 1)
44+
45+
// Limit decimal part to specified places
46+
val limitedDecimalPart = decimalPart.take(decimalPlaces)
47+
48+
return if (limitedDecimalPart.isEmpty() && cleanText.endsWith(".")) {
49+
"$integerPart."
50+
} else if (limitedDecimalPart.isEmpty()) {
51+
integerPart
52+
} else {
53+
"$integerPart.$limitedDecimalPart"
54+
}
55+
}
56+
57+
private fun formatMonetaryValue(text: String): String {
58+
// Handle cases where user is typing a decimal point
59+
if (text.isEmpty() || text == ".") {
60+
return text
61+
}
62+
63+
// If text ends with a decimal point, preserve it
64+
val endsWithDecimal = text.endsWith(".")
65+
val textToFormat = if (endsWithDecimal) text.dropLast(1) else text
66+
67+
// If the text to format is empty after removing the decimal, return original
68+
if (textToFormat.isEmpty()) {
69+
return text
70+
}
71+
72+
val doubleValue = textToFormat.toDoubleOrNull() ?: return text
3473

3574
val formatSymbols = DecimalFormatSymbols(Locale.getDefault()).apply {
3675
groupingSeparator = ','
3776
decimalSeparator = '.'
3877
}
3978

40-
val decimalPlacesPattern = "#".repeat(decimalPlaces)
41-
val formatter = DecimalFormat("#,##0.$decimalPlacesPattern", formatSymbols).apply {
42-
minimumFractionDigits = 0
43-
maximumFractionDigits = decimalPlaces
79+
// Only format the integer part if user is typing a decimal
80+
val formatter = if (endsWithDecimal) {
81+
DecimalFormat("#,##0", formatSymbols)
82+
} else {
83+
val decimalPlacesPattern = "#".repeat(decimalPlaces)
84+
DecimalFormat("#,##0.$decimalPlacesPattern", formatSymbols).apply {
85+
minimumFractionDigits = 0
86+
maximumFractionDigits = decimalPlaces
87+
}
4488
}
4589

46-
return formatter.format(doubleValue)
90+
val formatted = formatter.format(doubleValue)
91+
return if (endsWithDecimal) "$formatted." else formatted
4792
}
4893

4994
private fun createOffsetMapping(original: String, transformed: String): OffsetMapping {
5095
return object : OffsetMapping {
5196
override fun originalToTransformed(offset: Int): Int {
52-
val cleanOriginal = original.take(offset).replace(",", "").replace(" ", "")
97+
if (offset <= 0) return 0
98+
if (offset >= original.length) return transformed.length
99+
100+
val originalSubstring = original.take(offset)
53101
var transformedOffset = 0
54-
var cleanOffset = 0
102+
var originalIndex = 0
55103

56104
for (char in transformed) {
57-
if (char == ',' || char == ' ') {
105+
if (originalIndex >= originalSubstring.length) break
106+
107+
if (char == ',') {
108+
// Skip comma in transformed, don't advance original
58109
transformedOffset++
59-
} else {
60-
if (cleanOffset >= cleanOriginal.length) break
61-
cleanOffset++
110+
} else if (originalIndex < originalSubstring.length &&
111+
originalSubstring[originalIndex] == char) {
112+
// Characters match, advance both
113+
originalIndex++
62114
transformedOffset++
115+
} else {
116+
// Look for next matching character in original
117+
var found = false
118+
for (i in originalIndex until originalSubstring.length) {
119+
if (originalSubstring[i] == char) {
120+
originalIndex = i + 1
121+
transformedOffset++
122+
found = true
123+
break
124+
}
125+
}
126+
if (!found) break
63127
}
64128
}
65129

66130
return transformedOffset.coerceAtMost(transformed.length)
67131
}
68132

69133
override fun transformedToOriginal(offset: Int): Int {
70-
val transformedSubstring = transformed.take(offset)
71-
val cleanCount = transformedSubstring.count { it != ',' && it != ' ' }
134+
if (offset <= 0) return 0
135+
if (offset >= transformed.length) return original.length
72136

137+
val transformedSubstring = transformed.take(offset)
73138
var originalOffset = 0
74-
var cleanOffset = 0
139+
var transformedIndex = 0
75140

76141
for (char in original) {
77-
if (char != ',' && char != ' ') {
78-
if (cleanOffset >= cleanCount) break
79-
cleanOffset++
142+
if (transformedIndex >= transformedSubstring.length) break
143+
144+
if (char == transformedSubstring[transformedIndex]) {
145+
// Characters match, advance both
146+
transformedIndex++
147+
originalOffset++
148+
} else if (transformedIndex < transformedSubstring.length - 1 &&
149+
transformedSubstring[transformedIndex] == ',') {
150+
// Skip comma in transformed
151+
transformedIndex++
152+
if (transformedIndex < transformedSubstring.length &&
153+
char == transformedSubstring[transformedIndex]) {
154+
transformedIndex++
155+
originalOffset++
156+
}
157+
} else {
158+
originalOffset++
80159
}
81-
originalOffset++
82160
}
83161

84162
return originalOffset.coerceAtMost(original.length)

0 commit comments

Comments
 (0)