@@ -7,6 +7,7 @@ import androidx.compose.ui.text.input.VisualTransformation
77import java.text.DecimalFormat
88import java.text.DecimalFormatSymbols
99import java.util.Locale
10+ import kotlin.text.iterator
1011
1112class 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