Refactor formatter

This commit is contained in:
Sad Ellie 2023-02-28 21:37:02 +04:00
parent 8d98cbfddf
commit 7150fc9133
7 changed files with 215 additions and 159 deletions

View File

@ -115,8 +115,7 @@ object Token {
multiply, multiplyDisplay,
plus, minus, minusDisplay, divide, divideDisplay,
baseA, baseB, baseC, baseD, baseE, baseF,
_1, _2, _3, _4, _5, _6, _7, _8, _9, _0,
dot
_1, _2, _3, _4, _5, _6, _7, _8, _9, _0
).sortedByDescending { it.length }
}
}

View File

@ -18,10 +18,9 @@
package com.sadellie.unitto.core.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.sadellie.unitto.core.base.Token
import android.content.Context
import com.sadellie.unitto.core.base.Separator
import com.sadellie.unitto.core.base.Token
import java.math.BigDecimal
import java.math.RoundingMode
@ -90,8 +89,6 @@ open class UnittoFormatter {
if (input.contains(Token.E)) return input.replace(Token.dot, fractional)
var output = input
// We may receive expressions. Find all numbers in this expression
val allNumbers: List<String> = input.getOnlyNumbers()
allNumbers.forEach {
@ -116,7 +113,11 @@ open class UnittoFormatter {
// Remove grouping
// 12345,6789
// Replace fractional with "." because formatter accepts only numbers where fractional is a dot
return format(removeFormat(input))
return format(
input
.replace(grouping, "")
.replace(fractional, Token.dot)
)
}
/**
@ -129,17 +130,67 @@ open class UnittoFormatter {
Separator.COMMA -> COMMA
else -> SPACE
}
.also { if (it == grouping) return input }
val sFractional = if (separator == Separator.PERIOD) Token.comma else Token.dot
return input
.replace(sGrouping, grouping)
.replace(sGrouping, "\t")
.replace(sFractional, fractional)
.replace("\t", grouping)
}
fun removeFormat(input: String): String {
return input
.replace(grouping, "")
.replace(fractional, Token.dot)
fun toSeparator(input: String, separator: Int): String {
val output = filterUnknownSymbols(input).replace(fractional, Token.dot)
val sGrouping = when (separator) {
Separator.PERIOD -> PERIOD
Separator.COMMA -> COMMA
else -> SPACE
}
val sFractional = if (separator == Separator.PERIOD) Token.comma else Token.dot
return format(output)
.replace(grouping, "\t")
.replace(fractional, sFractional)
.replace("\t", sGrouping)
}
fun removeGrouping(input: String): String = input.replace(grouping, "")
/**
* Takes [input] and [basicUnit] of the unit to format it to be more human readable.
*
* @return String like "1d 12h 12s".
*/
fun formatTime(context: Context, input: String, basicUnit: BigDecimal?): String {
if (basicUnit == null) return Token._0
try {
// Don't need magic if the input is zero
if (BigDecimal(input).compareTo(BigDecimal.ZERO) == 0) return Token._0
} catch (e: NumberFormatException) {
// For case such as "10-" and "("
return Token._0
}
// Attoseconds don't need "magic"
if (basicUnit.compareTo(BigDecimal.ONE) == 0) return formatNumber(input)
var result = if (input.startsWith(Token.minus)) Token.minus else ""
var remainingSeconds = BigDecimal(input)
.abs()
.multiply(basicUnit)
.setScale(0, RoundingMode.HALF_EVEN)
if (remainingSeconds.compareTo(BigDecimal.ZERO) == 0) return Token._0
timeDivisions.forEach { (timeStr, divider) ->
val division = remainingSeconds.divideAndRemainder(divider)
val time = division.component1()
remainingSeconds = division.component2()
if (time.compareTo(BigDecimal.ZERO) == 1) {
result += "${formatNumber(time.toPlainString())}${context.getString(timeStr)} "
}
}
return result.trimEnd()
}
/**
@ -170,47 +221,31 @@ open class UnittoFormatter {
return (output + remainingPart.replace(".", fractional))
}
/**
* Takes [input] and [basicUnit] of the unit to format it to be more human readable.
*
* @return String like "1d 12h 12s".
*/
@Composable
fun formatTime(input: String, basicUnit: BigDecimal?): String {
if (basicUnit == null) return Token._0
try {
// Don't need magic if the input is zero
if (BigDecimal(input).compareTo(BigDecimal.ZERO) == 0) return Token._0
} catch (e: NumberFormatException) {
// For case such as "10-" and "("
return Token._0
}
// Attoseconds don't need "magic"
if (basicUnit.compareTo(BigDecimal.ONE) == 0) return formatNumber(input)
var result = if (input.startsWith(Token.minus)) Token.minus else ""
var remainingSeconds = BigDecimal(input)
.abs()
.multiply(basicUnit)
.setScale(0, RoundingMode.HALF_EVEN)
if (remainingSeconds.compareTo(BigDecimal.ZERO) == 0) return Token._0
timeDivisions.forEach { (timeStr, divider) ->
val division = remainingSeconds.divideAndRemainder(divider)
val time = division.component1()
remainingSeconds = division.component2()
if (time.compareTo(BigDecimal.ZERO) == 1) {
result += "${formatNumber(time.toPlainString())}${stringResource(timeStr)} "
}
}
return result.trimEnd()
}
/**
* @receiver Must be a string with a dot (".") used as a fractional.
*/
private fun String.getOnlyNumbers(): List<String> =
numbersRegex.findAll(this).map(MatchResult::value).toList()
fun filterUnknownSymbols(input: String): String {
var clearStr = input.replace(" ", "")
var garbage = clearStr
// String with unknown symbols
Token.knownSymbols.plus(fractional).forEach {
garbage = garbage.replace(it, " ")
}
// Remove unknown symbols from input
garbage.split(" ").forEach {
clearStr = clearStr.replace(it, "")
}
clearStr = clearStr
.replace(Token.divide, Token.divideDisplay)
.replace(Token.multiply, Token.multiplyDisplay)
.replace(Token.minus, Token.minusDisplay)
return clearStr
}
}

View File

@ -18,6 +18,7 @@
package com.sadellie.unitto.core.ui
import android.content.Context
import androidx.compose.ui.test.junit4.createComposeRule
import com.sadellie.unitto.core.base.Separator
import org.junit.Assert.assertEquals
@ -25,6 +26,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import java.math.BigDecimal
private val formatter = Formatter
@ -93,98 +95,121 @@ class FormatterTest {
@Test
fun formatTimeTest() {
formatter.setSeparator(Separator.SPACES)
composeTestRule.setContent {
var basicValue = BigDecimal.valueOf(1)
assertEquals("-28", formatter.formatTime("-28", basicValue))
assertEquals("-0.05", formatter.formatTime("-0.05", basicValue))
assertEquals("0", formatter.formatTime("0", basicValue))
assertEquals("0", formatter.formatTime("-0", basicValue))
var basicValue = BigDecimal.valueOf(1)
val mContext: Context = RuntimeEnvironment.getApplication().applicationContext
assertEquals("-28", formatter.formatTime(mContext, "-28", basicValue))
assertEquals("-0.05", formatter.formatTime(mContext, "-0.05", basicValue))
assertEquals("0", formatter.formatTime(mContext, "0", basicValue))
assertEquals("0", formatter.formatTime(mContext, "-0", basicValue))
basicValue = BigDecimal.valueOf(86_400_000_000_000_000_000_000.0)
assertEquals("-28d", formatter.formatTime("-28", basicValue))
assertEquals("-1h 12m", formatter.formatTime("-0.05", basicValue))
assertEquals("0", formatter.formatTime("0", basicValue))
assertEquals("0", formatter.formatTime("-0", basicValue))
basicValue = BigDecimal.valueOf(86_400_000_000_000_000_000_000.0)
assertEquals("-28d", formatter.formatTime(mContext, "-28", basicValue))
assertEquals("-1h 12m", formatter.formatTime(mContext, "-0.05", basicValue))
assertEquals("0", formatter.formatTime(mContext, "0", basicValue))
assertEquals("0", formatter.formatTime(mContext, "-0", basicValue))
// DAYS
basicValue = BigDecimal.valueOf(86_400_000_000_000_000_000_000.0)
assertEquals("12h", formatter.formatTime("0.5", basicValue))
assertEquals("1h 12m", formatter.formatTime("0.05", basicValue))
assertEquals("7m 12s", formatter.formatTime("0.005", basicValue))
assertEquals("28d", formatter.formatTime("28", basicValue))
assertEquals("90d", formatter.formatTime("90", basicValue))
assertEquals("90d 12h", formatter.formatTime("90.5", basicValue))
assertEquals("90d 7m 12s", formatter.formatTime("90.005", basicValue))
// DAYS
basicValue = BigDecimal.valueOf(86_400_000_000_000_000_000_000.0)
assertEquals("12h", formatter.formatTime(mContext, "0.5", basicValue))
assertEquals("1h 12m", formatter.formatTime(mContext, "0.05", basicValue))
assertEquals("7m 12s", formatter.formatTime(mContext, "0.005", basicValue))
assertEquals("28d", formatter.formatTime(mContext, "28", basicValue))
assertEquals("90d", formatter.formatTime(mContext, "90", basicValue))
assertEquals("90d 12h", formatter.formatTime(mContext, "90.5", basicValue))
assertEquals("90d 7m 12s", formatter.formatTime(mContext, "90.005", basicValue))
// HOURS
basicValue = BigDecimal.valueOf(3_600_000_000_000_000_000_000.0)
assertEquals("30m", formatter.formatTime("0.5", basicValue))
assertEquals("3m", formatter.formatTime("0.05", basicValue))
assertEquals("18s", formatter.formatTime("0.005", basicValue))
assertEquals("1d 4h", formatter.formatTime("28", basicValue))
assertEquals("3d 18h", formatter.formatTime("90", basicValue))
assertEquals("3d 18h 30m", formatter.formatTime("90.5", basicValue))
assertEquals("3d 18h 18s", formatter.formatTime("90.005", basicValue))
// HOURS
basicValue = BigDecimal.valueOf(3_600_000_000_000_000_000_000.0)
assertEquals("30m", formatter.formatTime(mContext, "0.5", basicValue))
assertEquals("3m", formatter.formatTime(mContext, "0.05", basicValue))
assertEquals("18s", formatter.formatTime(mContext, "0.005", basicValue))
assertEquals("1d 4h", formatter.formatTime(mContext, "28", basicValue))
assertEquals("3d 18h", formatter.formatTime(mContext, "90", basicValue))
assertEquals("3d 18h 30m", formatter.formatTime(mContext, "90.5", basicValue))
assertEquals("3d 18h 18s", formatter.formatTime(mContext, "90.005", basicValue))
// MINUTES
basicValue = BigDecimal.valueOf(60_000_000_000_000_000_000.0)
assertEquals("30s", formatter.formatTime("0.5", basicValue))
assertEquals("3s", formatter.formatTime("0.05", basicValue))
assertEquals("300ms", formatter.formatTime("0.005", basicValue))
assertEquals("28m", formatter.formatTime("28", basicValue))
assertEquals("1h 30m", formatter.formatTime("90", basicValue))
assertEquals("1h 30m 30s", formatter.formatTime("90.5", basicValue))
assertEquals("1h 30m 300ms", formatter.formatTime("90.005", basicValue))
// MINUTES
basicValue = BigDecimal.valueOf(60_000_000_000_000_000_000.0)
assertEquals("30s", formatter.formatTime(mContext, "0.5", basicValue))
assertEquals("3s", formatter.formatTime(mContext, "0.05", basicValue))
assertEquals("300ms", formatter.formatTime(mContext, "0.005", basicValue))
assertEquals("28m", formatter.formatTime(mContext, "28", basicValue))
assertEquals("1h 30m", formatter.formatTime(mContext, "90", basicValue))
assertEquals("1h 30m 30s", formatter.formatTime(mContext, "90.5", basicValue))
assertEquals("1h 30m 300ms", formatter.formatTime(mContext, "90.005", basicValue))
// SECONDS
basicValue = BigDecimal.valueOf(1_000_000_000_000_000_000)
assertEquals("500ms", formatter.formatTime("0.5", basicValue))
assertEquals("50ms", formatter.formatTime("0.05", basicValue))
assertEquals("5ms", formatter.formatTime("0.005", basicValue))
assertEquals("28s", formatter.formatTime("28", basicValue))
assertEquals("1m 30s", formatter.formatTime("90", basicValue))
assertEquals("1m 30s 500ms", formatter.formatTime("90.5", basicValue))
assertEquals("1m 30s 5ms", formatter.formatTime("90.005", basicValue))
// SECONDS
basicValue = BigDecimal.valueOf(1_000_000_000_000_000_000)
assertEquals("500ms", formatter.formatTime(mContext, "0.5", basicValue))
assertEquals("50ms", formatter.formatTime(mContext, "0.05", basicValue))
assertEquals("5ms", formatter.formatTime(mContext, "0.005", basicValue))
assertEquals("28s", formatter.formatTime(mContext, "28", basicValue))
assertEquals("1m 30s", formatter.formatTime(mContext, "90", basicValue))
assertEquals("1m 30s 500ms", formatter.formatTime(mContext, "90.5", basicValue))
assertEquals("1m 30s 5ms", formatter.formatTime(mContext, "90.005", basicValue))
// MILLISECONDS
basicValue = BigDecimal.valueOf(1_000_000_000_000_000)
assertEquals("500µs", formatter.formatTime("0.5", basicValue))
assertEquals("50µs", formatter.formatTime("0.05", basicValue))
assertEquals("5µs", formatter.formatTime("0.005", basicValue))
assertEquals("28ms", formatter.formatTime("28", basicValue))
assertEquals("90ms", formatter.formatTime("90", basicValue))
assertEquals("90ms 500µs", formatter.formatTime("90.5", basicValue))
assertEquals("90ms 5µs", formatter.formatTime("90.005", basicValue))
// MILLISECONDS
basicValue = BigDecimal.valueOf(1_000_000_000_000_000)
assertEquals("500µs", formatter.formatTime(mContext, "0.5", basicValue))
assertEquals("50µs", formatter.formatTime(mContext, "0.05", basicValue))
assertEquals("5µs", formatter.formatTime(mContext, "0.005", basicValue))
assertEquals("28ms", formatter.formatTime(mContext, "28", basicValue))
assertEquals("90ms", formatter.formatTime(mContext, "90", basicValue))
assertEquals("90ms 500µs", formatter.formatTime(mContext, "90.5", basicValue))
assertEquals("90ms 5µs", formatter.formatTime(mContext, "90.005", basicValue))
// MICROSECONDS
basicValue = BigDecimal.valueOf(1_000_000_000_000)
assertEquals("500ns", formatter.formatTime("0.5", basicValue))
assertEquals("50ns", formatter.formatTime("0.05", basicValue))
assertEquals("5ns", formatter.formatTime("0.005", basicValue))
assertEquals("28µs", formatter.formatTime("28", basicValue))
assertEquals("90µs", formatter.formatTime("90", basicValue))
assertEquals("90µs 500ns", formatter.formatTime("90.5", basicValue))
assertEquals("90µs 5ns", formatter.formatTime("90.005", basicValue))
// MICROSECONDS
basicValue = BigDecimal.valueOf(1_000_000_000_000)
assertEquals("500ns", formatter.formatTime(mContext, "0.5", basicValue))
assertEquals("50ns", formatter.formatTime(mContext, "0.05", basicValue))
assertEquals("5ns", formatter.formatTime(mContext, "0.005", basicValue))
assertEquals("28µs", formatter.formatTime(mContext, "28", basicValue))
assertEquals("90µs", formatter.formatTime(mContext, "90", basicValue))
assertEquals("90µs 500ns", formatter.formatTime(mContext, "90.5", basicValue))
assertEquals("90µs 5ns", formatter.formatTime(mContext, "90.005", basicValue))
// NANOSECONDS
basicValue = BigDecimal.valueOf(1_000_000_000)
assertEquals("500 000 000as", formatter.formatTime("0.5", basicValue))
assertEquals("50 000 000as", formatter.formatTime("0.05", basicValue))
assertEquals("5 000 000as", formatter.formatTime("0.005", basicValue))
assertEquals("28ns", formatter.formatTime("28", basicValue))
assertEquals("90ns", formatter.formatTime("90", basicValue))
assertEquals("90ns 500 000 000as", formatter.formatTime("90.5", basicValue))
assertEquals("90ns 5 000 000as", formatter.formatTime("90.005", basicValue))
// NANOSECONDS
basicValue = BigDecimal.valueOf(1_000_000_000)
assertEquals("500 000 000as", formatter.formatTime(mContext, "0.5", basicValue))
assertEquals("50 000 000as", formatter.formatTime(mContext, "0.05", basicValue))
assertEquals("5 000 000as", formatter.formatTime(mContext, "0.005", basicValue))
assertEquals("28ns", formatter.formatTime(mContext, "28", basicValue))
assertEquals("90ns", formatter.formatTime(mContext, "90", basicValue))
assertEquals("90ns 500 000 000as", formatter.formatTime(mContext, "90.5", basicValue))
assertEquals("90ns 5 000 000as", formatter.formatTime(mContext, "90.005", basicValue))
// ATTOSECONDS
basicValue = BigDecimal.valueOf(1)
assertEquals("0.5", formatter.formatTime("0.5", basicValue))
assertEquals("0.05", formatter.formatTime("0.05", basicValue))
assertEquals("0.005", formatter.formatTime("0.005", basicValue))
assertEquals("28", formatter.formatTime("28", basicValue))
assertEquals("90", formatter.formatTime("90", basicValue))
assertEquals("90.5", formatter.formatTime("90.5", basicValue))
assertEquals("90.005", formatter.formatTime("90.005", basicValue))
}
// ATTOSECONDS
basicValue = BigDecimal.valueOf(1)
assertEquals("0.5", formatter.formatTime(mContext, "0.5", basicValue))
assertEquals("0.05", formatter.formatTime(mContext, "0.05", basicValue))
assertEquals("0.005", formatter.formatTime(mContext, "0.005", basicValue))
assertEquals("28", formatter.formatTime(mContext, "28", basicValue))
assertEquals("90", formatter.formatTime(mContext, "90", basicValue))
assertEquals("90.5", formatter.formatTime(mContext, "90.5", basicValue))
assertEquals("90.005", formatter.formatTime(mContext, "90.005", basicValue))
}
@Test
fun fromSeparatorToSpacesTest() {
formatter.setSeparator(Separator.SPACES)
assertEquals("123 456.789", formatter.fromSeparator("123,456.789", Separator.COMMA))
assertEquals("123 456.789", formatter.fromSeparator("123 456.789", Separator.SPACES))
assertEquals("123 456.789", formatter.fromSeparator("123.456,789", Separator.PERIOD))
}
@Test
fun fromSeparatorToPeriodTest() {
formatter.setSeparator(Separator.PERIOD)
assertEquals("123.456,789", formatter.fromSeparator("123,456.789", Separator.COMMA))
assertEquals("123.456,789", formatter.fromSeparator("123 456.789", Separator.SPACES))
assertEquals("123.456,789", formatter.fromSeparator("123.456,789", Separator.PERIOD))
}
@Test
fun fromSeparatorToCommaTest() {
formatter.setSeparator(Separator.COMMA)
assertEquals("123,456.789", formatter.fromSeparator("123,456.789", Separator.COMMA))
assertEquals("123,456.789", formatter.fromSeparator("123 456.789", Separator.SPACES))
assertEquals("123,456.789", formatter.fromSeparator("123.456,789", Separator.PERIOD))
}
}

View File

@ -100,7 +100,9 @@ internal fun CalculatorRoute(
// This method is called immediately after copying formatted text, we replace it with the
// the unformatted version.
clipboardManager.setText(
AnnotatedString(Formatter.removeFormat(clipboardText.text))
AnnotatedString(
clipboardText.text.replace(Formatter.grouping, "")
)
)
}

View File

@ -128,27 +128,7 @@ class TextFieldController @Inject constructor() {
private fun String.fixFormat(): String = localFormatter.reFormat(this)
private fun String.filterUnknownSymbols(): String {
var clearStr = this.replace(" ", "")
var garbage = clearStr
// String with unknown symbols
Token.knownSymbols.forEach {
garbage = garbage.replace(it, " ")
}
// Remove unknown symbols from input
garbage.split(" ").forEach {
clearStr = clearStr.replace(it, "")
}
clearStr = clearStr
.replace(Token.divide, Token.divideDisplay)
.replace(Token.multiply, Token.multiplyDisplay)
.replace(Token.minus, Token.minusDisplay)
return clearStr
}
private fun String.filterUnknownSymbols() = localFormatter.filterUnknownSymbols(this)
inner class CursorFixer {
private val illegalTokens by lazy {

View File

@ -31,6 +31,7 @@ import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalTextInputService
import androidx.compose.ui.platform.LocalTextToolbar
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import com.sadellie.unitto.core.base.Separator
@ -46,6 +47,7 @@ internal fun InputTextField(
cutCallback: () -> Unit
) {
val clipboardManager = LocalClipboardManager.current
val formattedInput: TextFieldValue by remember(value) {
derivedStateOf {
value.copy(
@ -57,7 +59,11 @@ internal fun InputTextField(
}
fun copyToClipboard() = clipboardManager.setText(
formattedInput.annotatedString.subSequence(formattedInput.selection)
AnnotatedString(
Formatter.removeGrouping(
formattedInput.annotatedString.subSequence(formattedInput.selection).text
)
)
)
CompositionLocalProvider(
@ -65,7 +71,13 @@ internal fun InputTextField(
LocalTextToolbar provides UnittoTextToolbar(
view = LocalView.current,
copyCallback = ::copyToClipboard,
pasteCallback = { pasteCallback(clipboardManager.getText()?.text ?: "") },
pasteCallback = {
pasteCallback(
Formatter.toSeparator(
clipboardManager.getText()?.text ?: "", Separator.COMMA
)
)
},
cutCallback = { copyToClipboard(); cutCallback() }
)
) {

View File

@ -38,6 +38,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.sadellie.unitto.core.ui.Formatter
@ -88,6 +89,7 @@ internal fun TopScreenPart(
targetValue = if (swapped) 0f else 180f,
animationSpec = tween(easing = FastOutSlowInEasing)
)
val mContext = LocalContext.current
Column(
modifier = modifier,
@ -115,6 +117,7 @@ internal fun TopScreenPart(
converterMode == ConverterMode.BASE -> outputValue.uppercase()
formatTime and (unitTo?.group == UnitGroup.TIME) -> {
Formatter.formatTime(
context = mContext,
input = calculatedValue ?: inputValue,
basicUnit = unitFrom?.basicUnit
)