11package com.github.encryptsl.lite.eco.common.extensions
22import java.math.BigDecimal
3+ import java.math.BigInteger
34import java.math.RoundingMode
45import java.text.DecimalFormat
56import java.text.DecimalFormatSymbols
@@ -9,53 +10,42 @@ private val units = arrayOf(
910 " " , " K" , " M" , " B" , " T" , " Q" , " Qn" , " S" , " Se" , " O" , " N" , " D"
1011)
1112
12- /* *
13- * Converts a string to BigDecimal, including truncated units (e.g., "1.5M" → 1500000).
14- */
1513fun String.toValidDecimal (): BigDecimal ? {
16- if (this .isBlank() || this .contains(" " )) return null
17- return decompressNumber(this )
14+ val s = this .trim()
15+ if (s.isBlank() || s.contains(" " )) return null
16+ return decompressNumber(s)
1817}
1918
20- /* *
21- * Returns a number in abbreviated format (e.g., 1500 → "1.5K").
22- */
2319fun BigDecimal.compactFormat (pattern : String , compactPattern : String , locale : String ): String {
24- val (value, unit) = compactNumber(this ) ? : (null to null )
25- return value?.let {
26- formatNumber(it, compactPattern, locale, compacted = true ) + unit
27- } ? : formatNumber(this , pattern, locale)
20+ val pair = compactNumber(this )
21+ return if (pair != null ) {
22+ val (value, unit) = pair
23+ formatNumber(value, compactPattern, locale, compacted = true ) + unit
24+ } else {
25+ formatNumber(this , pattern, locale)
26+ }
2827}
2928
30- /* *
31- * Returns a number as currency according to the specified pattern and locale.
32- */
3329fun BigDecimal.moneyFormat (pattern : String , locale : String ): String {
3430 return formatNumber(this , pattern, locale)
3531}
3632
37- /* *
38- * Formats a number according to a pattern and locale.
39- */
4033private fun formatNumber (number : BigDecimal , pattern : String , locale : String , compacted : Boolean = false): String {
4134 val formatter = DecimalFormat ().apply {
4235 decimalFormatSymbols = DecimalFormatSymbols .getInstance(getLocale(locale))
43- roundingMode = if (compacted) RoundingMode .UNNECESSARY else RoundingMode .HALF_UP
36+ // always safe rounding; UNNECESSARY caused exceptions
37+ roundingMode = if (compacted) RoundingMode .HALF_UP else RoundingMode .HALF_UP
4438 applyPattern(pattern)
4539 }
4640 return formatter.format(number)
4741}
4842
49- /* *
50- * Decompress string → BigDecimal (e.g., "2.5M" → 2500000).
51- */
5243private fun decompressNumber (str : String ): BigDecimal ? {
53- val upper = str.uppercase(Locale .getDefault())
44+ val upper = str.trim(). uppercase(Locale .getDefault())
5445
55- // find suffix
5646 val unit = units
5747 .filter { it.isNotEmpty() }
58- .sortedByDescending { it.length } // so that "Qn" takes precedence over "Q"
48+ .sortedByDescending { it.length }
5949 .firstOrNull { upper.endsWith(it) }
6050
6151 return if (unit != null ) {
@@ -67,36 +57,28 @@ private fun decompressNumber(str: String): BigDecimal? {
6757 }
6858}
6959
70- /* *
71- * Compact BigDecimal → Pair(value, unit).
72- */
7360private fun compactNumber (number : BigDecimal ): Pair <BigDecimal , String >? {
74- if (number == BigDecimal . ZERO ) return null
61+ if (number.compareTo( BigDecimal . ZERO ) == 0 ) return null
7562
76- val absNum = number.abs()
77- val exp = absNum.toBigInteger ().toString().length - 1 // log10 over string length
78- val unitIndex = exp / 3
63+ // use integer part to determine number of digits -> stable decision unit
64+ val absIntPart : BigInteger = number.abs ().toBigInteger()
65+ if (absIntPart == BigInteger . ZERO ) return null
7966
67+ val digits = absIntPart.toString().length
68+ val unitIndex = (digits - 1 ) / 3
8069 if (unitIndex == 0 ) return null
8170
82- if (unitIndex >= units.size) {
83- // fallback: use the last known unit
84- val divisor = BigDecimal .TEN .pow((units.size - 1 ) * 3 )
85- return number.divide(divisor) to units.last()
86- }
71+ val index = if (unitIndex >= units.size) units.size - 1 else unitIndex
72+ val divisor = BigDecimal .TEN .pow(index * 3 )
8773
88- val divisor = BigDecimal .TEN .pow(unitIndex * 3 )
89- return number.divide(divisor) to units[unitIndex]
74+ // scale = 1 -> display up to 1 decimal place (for 44_888 -> 44.9K)
75+ // If you always want no decimal places (44K), use scale = 0 and RoundingMode.DOWN
76+ val scaled = number.divide(divisor, 1 , RoundingMode .HALF_UP )
77+
78+ return scaled to units[index]
9079}
9180
92- /* *
93- * Returns the locale based on a string (e.g., "en", "en-US", "zh_CN").
94- */
9581private fun getLocale (localeStr : String ): Locale {
96- val parts = localeStr.split(" -" , " _" )
97- return when (parts.size) {
98- 1 -> Locale .of(parts[0 ])
99- 2 -> Locale .of(parts[0 ], parts[1 ])
100- else -> Locale .of(parts[0 ], parts[1 ], parts[2 ])
101- }
82+ // compatible and safe: "en", "en-US", "zh_CN" -> "en", "en-US", "zh-CN"
83+ return Locale .forLanguageTag(localeStr.replace(' _' , ' -' ))
10284}
0 commit comments