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.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.util.Log
import com.sadellie.unitto.FirebaseHelper
import com.sadellie.unitto.data.* import com.sadellie.unitto.data.*
import com.sadellie.unitto.data.preferences.OutputFormat import com.sadellie.unitto.data.preferences.OutputFormat
import com.sadellie.unitto.data.preferences.Separator import com.sadellie.unitto.data.preferences.Separator
import com.sadellie.unitto.data.units.AbstractUnit import com.sadellie.unitto.data.units.AbstractUnit
import java.math.BigDecimal import java.math.BigDecimal
import java.math.RoundingMode import java.math.RoundingMode
import java.text.NumberFormat
import java.util.*
import kotlin.math.floor import kotlin.math.floor
import kotlin.math.log10 import kotlin.math.log10
import kotlin.math.max import kotlin.math.max
object Formatter { 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 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) { fun setSeparator(separator: Int) {
nf = when (separator) { grouping = when (separator) {
Separator.PERIOD -> NumberFormat.getInstance(Locale.GERMANY) // . Separator.PERIOD -> PERIOD
Separator.COMMA -> NumberFormat.getInstance(Locale.US) // , Separator.COMMA -> COMMA
else -> NumberFormat.getInstance(Locale.FRANCE) //   else -> SPACE
} }
fractional = if (separator == Separator.PERIOD) KEY_COMMA else KEY_DOT 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. * Format [input].
* Also doesn't lose any precision *
* @param[input] The string we want to format. Will be split with dot symbol * 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 { fun format(input: String): String {
// NOTE: We receive input like 1234 or 1234. or 1234.5 // Don't do anything to engineering string.
// NOTICE DOTS, not COMMAS 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_PLUS,
KEY_MINUS_DISPLAY, KEY_MULTIPLY
KEY_MULTIPLY_DISPLAY,
KEY_DIVIDE_DISPLAY,
) )
fun innerFormat(str: String): String { allNumbers.forEach {
// For engineering string we only replace decimal separator output = output.replace(it, formatNumber(it))
if (str.contains(KEY_E)) return str.replace(KEY_DOT, fractional) }
return try { return output.replace(KEY_MINUS, KEY_MINUS_DISPLAY)
var result = String() .replace(KEY_DIVIDE, KEY_DIVIDE_DISPLAY)
// Formatting everything before fractional part .replace(KEY_MULTIPLY, KEY_MULTIPLY_DISPLAY)
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)
} }
/** /**
* When user input is like "+123" (with plus) it gets lost after formatting, but returns * Format given [input].
* on unformattable input, i.e. "+123-23" *
* Input must be a number. Will replace grouping separators and fractional part separators.
*
* @see grouping
* @see fractional
*/ */
if (str.startsWith(KEY_PLUS)) { private fun formatNumber(input: String): String {
result = KEY_PLUS + result val splitInput = input.split(".")
} var firstPart = splitInput[0]
result
} catch (e: NumberFormatException) { // Number of empty symbols (spaces) we need to add to correctly split into chunks.
str val offset = 3 - firstPart.length.mod(3)
} catch (e: Exception) { var output = if (offset != 3) {
// Bad practise, but still // We add some spaces at the begging so that last chunk has 3 symbols
Log.e("FormatterError", e.toString()) firstPart = " ".repeat(offset) + firstPart
FirebaseHelper().recordException(e) firstPart.chunked(3).joinToString(this.grouping).drop(offset)
str } else {
} firstPart.chunked(3).joinToString(this.grouping)
} }
allNumbers.forEach { // Handling fractional part
formattedInput = formattedInput.replace(it, innerFormat(it)) if (input.contains(".")) {
output = output + fractional + splitInput.getOrElse(1) { "" }
} }
return formattedInput.replace(KEY_MINUS, KEY_MINUS_DISPLAY) return output
.replace(KEY_DIVIDE, KEY_DIVIDE_DISPLAY)
.replace(KEY_MULTIPLY, KEY_MULTIPLY_DISPLAY)
} }
} }

View File

@ -27,6 +27,9 @@ private val formatter = Formatter
private const val ENG_VALUE = "123.3E+21" private const val ENG_VALUE = "123.3E+21"
private const val COMPLETE_VALUE = "123456.789" private const val COMPLETE_VALUE = "123456.789"
private const val INCOMPLETE_VALUE = "123456." 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 { class FormatterTest {
@ -34,75 +37,36 @@ class FormatterTest {
fun setSeparatorSpaces() { fun setSeparatorSpaces() {
formatter.setSeparator(Separator.SPACES) formatter.setSeparator(Separator.SPACES)
assertEquals(".", formatter.fractional) 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 @Test
fun setSeparatorComma() { fun setSeparatorComma() {
formatter.setSeparator(Separator.COMMA) formatter.setSeparator(Separator.COMMA)
assertEquals(".", formatter.fractional) 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 @Test
fun setSeparatorPeriod() { fun setSeparatorPeriod() {
formatter.setSeparator(Separator.PERIOD) formatter.setSeparator(Separator.PERIOD)
assertEquals(",", formatter.fractional) 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)) 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)) 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(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))
} }
} }