Custom formatter.

NumberFormatter from java is bad.

Updated tests, LGTM.
This commit is contained in:
Sad Ellie 2022-11-20 11:03:52 +04:00
parent b49b706d3f
commit cc1c94e86e
2 changed files with 83 additions and 109 deletions

View File

@ -21,98 +21,108 @@ package com.sadellie.unitto.screens
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import com.sadellie.unitto.FirebaseHelper
import com.sadellie.unitto.data.*
import com.sadellie.unitto.data.preferences.OutputFormat
import com.sadellie.unitto.data.preferences.Separator
import com.sadellie.unitto.data.units.AbstractUnit
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.NumberFormat
import java.util.*
import kotlin.math.floor
import kotlin.math.log10
import kotlin.math.max
object Formatter {
private var nf: NumberFormat = NumberFormat.getInstance(Locale.GERMANY)
private const val SPACE = " "
private const val PERIOD = "."
private const val COMMA = ","
/**
* Currently used symbol to separate fractional part
* Grouping separator.
*/
private var grouping: String = SPACE
/**
* Fractional part separator.
*/
var fractional = KEY_COMMA
/**
* Change current separator
* Change current separator to another [separator].
*
* @param separator [Separator] to change to
* @see [Separator]
*/
fun setSeparator(separator: Int) {
nf = when (separator) {
Separator.PERIOD -> NumberFormat.getInstance(Locale.GERMANY) // .
Separator.COMMA -> NumberFormat.getInstance(Locale.US) // ,
else -> NumberFormat.getInstance(Locale.FRANCE) //  
grouping = when (separator) {
Separator.PERIOD -> PERIOD
Separator.COMMA -> COMMA
else -> SPACE
}
fractional = if (separator == Separator.PERIOD) KEY_COMMA else KEY_DOT
}
/**
* Custom formatter function which work with big decimals and with strings ending with a dot.
* Also doesn't lose any precision
* @param[input] The string we want to format. Will be split with dot symbol
* Format [input].
*
* This will replace operators to their more appealing variants: divide, multiply and minus.
* Plus operator remains unchanged.
*
* Numbers will also be formatted.
*
* @see [formatNumber]
*/
fun format(input: String): String {
// NOTE: We receive input like 1234 or 1234. or 1234.5
// NOTICE DOTS, not COMMAS
// Don't do anything to engineering string.
if (input.contains(KEY_E)) return formatNumber(input)
var formattedInput = input
var output = input
val allNumbers = input.split(
// We may receive expressions
// Find all numbers in that expression
val allNumbers: List<String> = input.split(
KEY_MINUS,
KEY_DIVIDE,
KEY_PLUS,
KEY_MINUS_DISPLAY,
KEY_MULTIPLY_DISPLAY,
KEY_DIVIDE_DISPLAY,
KEY_MULTIPLY
)
fun innerFormat(str: String): String {
// For engineering string we only replace decimal separator
if (str.contains(KEY_E)) return str.replace(KEY_DOT, fractional)
allNumbers.forEach {
output = output.replace(it, formatNumber(it))
}
return try {
var result = String()
// Formatting everything before fractional part
result += nf.format(str.substringBefore(KEY_DOT).toBigInteger())
// Now we add the part after dot
if (str.contains(KEY_DOT)) {
result += fractional + str.substringAfter(KEY_DOT)
return output.replace(KEY_MINUS, KEY_MINUS_DISPLAY)
.replace(KEY_DIVIDE, KEY_DIVIDE_DISPLAY)
.replace(KEY_MULTIPLY, KEY_MULTIPLY_DISPLAY)
}
/**
* When user input is like "+123" (with plus) it gets lost after formatting, but returns
* on unformattable input, i.e. "+123-23"
* Format given [input].
*
* Input must be a number. Will replace grouping separators and fractional part separators.
*
* @see grouping
* @see fractional
*/
if (str.startsWith(KEY_PLUS)) {
result = KEY_PLUS + result
}
result
} catch (e: NumberFormatException) {
str
} catch (e: Exception) {
// Bad practise, but still
Log.e("FormatterError", e.toString())
FirebaseHelper().recordException(e)
str
}
private fun formatNumber(input: String): String {
val splitInput = input.split(".")
var firstPart = splitInput[0]
// Number of empty symbols (spaces) we need to add to correctly split into chunks.
val offset = 3 - firstPart.length.mod(3)
var output = if (offset != 3) {
// We add some spaces at the begging so that last chunk has 3 symbols
firstPart = " ".repeat(offset) + firstPart
firstPart.chunked(3).joinToString(this.grouping).drop(offset)
} else {
firstPart.chunked(3).joinToString(this.grouping)
}
allNumbers.forEach {
formattedInput = formattedInput.replace(it, innerFormat(it))
// Handling fractional part
if (input.contains(".")) {
output = output + fractional + splitInput.getOrElse(1) { "" }
}
return formattedInput.replace(KEY_MINUS, KEY_MINUS_DISPLAY)
.replace(KEY_DIVIDE, KEY_DIVIDE_DISPLAY)
.replace(KEY_MULTIPLY, KEY_MULTIPLY_DISPLAY)
return output
}
}

View File

@ -27,6 +27,9 @@ private val formatter = Formatter
private const val ENG_VALUE = "123.3E+21"
private const val COMPLETE_VALUE = "123456.789"
private const val INCOMPLETE_VALUE = "123456."
private const val NO_FRACTIONAL_VALUE = "123456"
private const val INCOMPLETE_EXPR = "50+123456/8*0.8-12+"
private const val COMPLETE_EXPR = "50+123456/8*0.8-12+0"
class FormatterTest {
@ -34,75 +37,36 @@ class FormatterTest {
fun setSeparatorSpaces() {
formatter.setSeparator(Separator.SPACES)
assertEquals(".", formatter.fractional)
assertEquals("123.3E+21", formatter.format(ENG_VALUE))
assertEquals("123 456.789", formatter.format(COMPLETE_VALUE))
assertEquals("123 456.", formatter.format(INCOMPLETE_VALUE))
assertEquals("123 456", formatter.format(NO_FRACTIONAL_VALUE))
assertEquals("50+123 456÷8×0.812+", formatter.format(INCOMPLETE_EXPR))
assertEquals("50+123 456÷8×0.812+0", formatter.format(COMPLETE_EXPR))
}
@Test
fun setSeparatorComma() {
formatter.setSeparator(Separator.COMMA)
assertEquals(".", formatter.fractional)
assertEquals("123.3E+21", formatter.format(ENG_VALUE))
assertEquals("123,456.789", formatter.format(COMPLETE_VALUE))
assertEquals("123,456.", formatter.format(INCOMPLETE_VALUE))
assertEquals("123,456", formatter.format(NO_FRACTIONAL_VALUE))
assertEquals("50+123,456÷8×0.812+", formatter.format(INCOMPLETE_EXPR))
assertEquals("50+123,456÷8×0.812+0", formatter.format(COMPLETE_EXPR))
}
@Test
fun setSeparatorPeriod() {
formatter.setSeparator(Separator.PERIOD)
assertEquals(",", formatter.fractional)
}
// ENGINEERING
@Test
fun formatEngineeringWithSpaces() {
formatter.setSeparator(Separator.SPACES)
assertEquals("123.3E+21", formatter.format(ENG_VALUE))
}
@Test
fun formatEngineeringWithComma() {
formatter.setSeparator(Separator.COMMA)
assertEquals("123.3E+21", formatter.format(ENG_VALUE))
}
@Test
fun formatEngineeringWithPeriod() {
formatter.setSeparator(Separator.PERIOD)
assertEquals("123,3E+21", formatter.format(ENG_VALUE))
}
// COMPLETE
@Test
fun formatCompleteWithSpaces() {
formatter.setSeparator(Separator.SPACES)
assertEquals("123 456.789", formatter.format(COMPLETE_VALUE))
}
@Test
fun formatCompleteWithComma() {
formatter.setSeparator(Separator.COMMA)
assertEquals("123,456.789", formatter.format(COMPLETE_VALUE))
}
@Test
fun formatCompleteWithPeriod() {
formatter.setSeparator(Separator.PERIOD)
assertEquals("123.456,789", formatter.format(COMPLETE_VALUE))
}
// INCOMPLETE
@Test
fun formatIncompleteWithSpaces() {
formatter.setSeparator(Separator.SPACES)
assertEquals("123 456.", formatter.format(INCOMPLETE_VALUE))
}
@Test
fun formatIncompleteWithComma() {
formatter.setSeparator(Separator.COMMA)
assertEquals("123,456.", formatter.format(INCOMPLETE_VALUE))
}
@Test
fun formatIncompleteWithPeriod() {
formatter.setSeparator(Separator.PERIOD)
assertEquals("123.456,", formatter.format(INCOMPLETE_VALUE))
assertEquals("123.456", formatter.format(NO_FRACTIONAL_VALUE))
assertEquals("50+123.456÷8×0,812+", formatter.format(INCOMPLETE_EXPR))
assertEquals("50+123.456÷8×0,812+0", formatter.format(COMPLETE_EXPR))
}
}