mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-19 16:55:26 +02:00
Update text field formatting
This commit is contained in:
parent
24f03b9d3d
commit
4305354931
@ -54,7 +54,7 @@ object Token {
|
||||
|
||||
object Operator {
|
||||
const val plus = "+"
|
||||
const val minus = "−"
|
||||
const val minus = "−" // MINUS SIGN, not a regular minus (HYPHEN-MINUS)
|
||||
const val multiply = "×"
|
||||
const val divide = "÷"
|
||||
const val leftBracket = "("
|
||||
@ -122,6 +122,7 @@ object Token {
|
||||
const val comma = ","
|
||||
const val engineeringE = "E"
|
||||
const val minus = "−"
|
||||
const val fraction = "⁄"
|
||||
}
|
||||
|
||||
val expressionTokens by lazy {
|
||||
|
@ -22,11 +22,19 @@ import com.sadellie.unitto.core.base.Token
|
||||
|
||||
private val numbersRegex by lazy { Regex("[\\d.]+") }
|
||||
|
||||
/**
|
||||
* Removes formatting from expression. Reverse of [formatExpression]. Ugly symbols (for example,
|
||||
* minus) will be replaced with a [Token.Operator.minus] from [Token.sexyToUgly].
|
||||
*
|
||||
* @param formatterSymbols [FormatterSymbols] that were used in [formatExpression].
|
||||
* @return Clean expression. 123,456.789 -> 123456.789
|
||||
*/
|
||||
fun String.clearAndFilterExpression(formatterSymbols: FormatterSymbols): String {
|
||||
var clean = this
|
||||
.replace(formatterSymbols.grouping, "")
|
||||
.replace(formatterSymbols.fractional, Token.Digit.dot)
|
||||
.replace(" ", "")
|
||||
.replace(" ", Token.Operator.plus)
|
||||
.replace(Token.DisplayOnly.fraction, Token.Operator.divide)
|
||||
|
||||
Token.sexyToUgly.forEach { (token, ugliness) ->
|
||||
ugliness.forEach {
|
||||
@ -34,22 +42,29 @@ fun String.clearAndFilterExpression(formatterSymbols: FormatterSymbols): String
|
||||
}
|
||||
}
|
||||
|
||||
return clean.cleanIt(Token.expressionTokens)
|
||||
return clean.leaveLegalTokensOnly(Token.expressionTokens)
|
||||
}
|
||||
|
||||
internal fun String.clearAndFilterNumberBase(): String {
|
||||
return uppercase().cleanIt(Token.numberBaseTokens)
|
||||
return uppercase().leaveLegalTokensOnly(Token.numberBaseTokens)
|
||||
}
|
||||
|
||||
fun String.formatExpression(
|
||||
formatterSymbols: FormatterSymbols
|
||||
): String {
|
||||
var input = this
|
||||
|
||||
// Don't do anything to engineering string.
|
||||
if (input.contains(Token.DisplayOnly.engineeringE)) {
|
||||
return input.replace(Token.Digit.dot, formatterSymbols.fractional)
|
||||
}
|
||||
|
||||
// Only format integral part
|
||||
if (input.contains(Token.DisplayOnly.fraction)) {
|
||||
val (integral, fraction) = input.split(" ")
|
||||
return "${integral.formatNumber(formatterSymbols)} $fraction"
|
||||
}
|
||||
|
||||
numbersRegex
|
||||
.findAll(input)
|
||||
.map(MatchResult::value)
|
||||
@ -89,7 +104,7 @@ private fun String.formatNumber(
|
||||
return output.plus(remainingPart.replace(".", formatterSymbols.fractional))
|
||||
}
|
||||
|
||||
private fun String.cleanIt(legalTokens: List<String>): String {
|
||||
private fun String.leaveLegalTokensOnly(legalTokens: List<String>): String {
|
||||
val streamOfTokens = this
|
||||
|
||||
fun peekTokenAfter(cursor: Int): String? {
|
||||
|
@ -76,29 +76,34 @@ fun ExpressionTextField(
|
||||
readOnly: Boolean = false,
|
||||
placeholder: String = "",
|
||||
) {
|
||||
val localView = LocalView.current
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val expressionTransformer = remember(formatterSymbols) { ExpressionTransformer(formatterSymbols) }
|
||||
|
||||
fun copyCallback() {
|
||||
clipboardManager.copyWithoutGrouping(value, formatterSymbols)
|
||||
onCursorChange(TextRange(value.selection.end))
|
||||
}
|
||||
|
||||
val textToolbar: UnittoTextToolbar = if (readOnly) {
|
||||
UnittoTextToolbar(
|
||||
view = LocalView.current,
|
||||
copyCallback = ::copyCallback,
|
||||
)
|
||||
} else {
|
||||
UnittoTextToolbar(
|
||||
view = LocalView.current,
|
||||
copyCallback = ::copyCallback,
|
||||
pasteCallback = {
|
||||
pasteCallback(clipboardManager.getText()?.text?.clearAndFilterExpression(formatterSymbols) ?: "")
|
||||
},
|
||||
cutCallback = {
|
||||
clipboardManager.copyWithoutGrouping(value, formatterSymbols)
|
||||
cutCallback()
|
||||
}
|
||||
)
|
||||
val textToolbar: UnittoTextToolbar = remember(readOnly) {
|
||||
if (readOnly) {
|
||||
UnittoTextToolbar(
|
||||
view = localView,
|
||||
copyCallback = ::copyCallback,
|
||||
)
|
||||
} else {
|
||||
UnittoTextToolbar(
|
||||
view = localView,
|
||||
copyCallback = ::copyCallback,
|
||||
pasteCallback = {
|
||||
pasteCallback(clipboardManager.getText()?.text?.clearAndFilterExpression(formatterSymbols) ?: "")
|
||||
},
|
||||
cutCallback = {
|
||||
clipboardManager.copyWithoutGrouping(value, formatterSymbols)
|
||||
cutCallback()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AutoSizableTextField(
|
||||
@ -111,7 +116,7 @@ fun ExpressionTextField(
|
||||
readOnly = readOnly,
|
||||
showToolbar = textToolbar::showMenu,
|
||||
hideToolbar = textToolbar::hide,
|
||||
visualTransformation = ExpressionTransformer(formatterSymbols),
|
||||
visualTransformation = expressionTransformer,
|
||||
placeholder = placeholder,
|
||||
textToolbar = textToolbar,
|
||||
)
|
||||
@ -129,29 +134,32 @@ fun UnformattedTextField(
|
||||
readOnly: Boolean = false,
|
||||
placeholder: String = "",
|
||||
) {
|
||||
val localView = LocalView.current
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
fun copyCallback() {
|
||||
clipboardManager.copy(value)
|
||||
onCursorChange(TextRange(value.selection.end))
|
||||
}
|
||||
|
||||
val textToolbar: UnittoTextToolbar = if (readOnly) {
|
||||
UnittoTextToolbar(
|
||||
view = LocalView.current,
|
||||
copyCallback = ::copyCallback,
|
||||
)
|
||||
} else {
|
||||
UnittoTextToolbar(
|
||||
view = LocalView.current,
|
||||
copyCallback = ::copyCallback,
|
||||
pasteCallback = {
|
||||
pasteCallback(clipboardManager.getText()?.text?.clearAndFilterNumberBase() ?: "")
|
||||
},
|
||||
cutCallback = {
|
||||
clipboardManager.copy(value)
|
||||
cutCallback()
|
||||
}
|
||||
)
|
||||
val textToolbar: UnittoTextToolbar = remember(readOnly) {
|
||||
if (readOnly) {
|
||||
UnittoTextToolbar(
|
||||
view = localView,
|
||||
copyCallback = ::copyCallback,
|
||||
)
|
||||
} else {
|
||||
UnittoTextToolbar(
|
||||
view = localView,
|
||||
copyCallback = ::copyCallback,
|
||||
pasteCallback = {
|
||||
pasteCallback(clipboardManager.getText()?.text?.clearAndFilterNumberBase() ?: "")
|
||||
},
|
||||
cutCallback = {
|
||||
clipboardManager.copy(value)
|
||||
cutCallback()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AutoSizableTextField(
|
||||
|
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Unitto is a unit converter for Android
|
||||
* Copyright (c) 2023 Elshan Agaev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.sadellie.unitto.core.ui
|
||||
|
||||
import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
|
||||
import com.sadellie.unitto.core.ui.common.textfield.clearAndFilterExpression
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class CleanAndFilterExpression {
|
||||
@Test
|
||||
fun noAdditionalSymbols() {
|
||||
assertEquals("123", "123".clearAndFilterExpression(FormatterSymbols.Comma))
|
||||
assertEquals("123.456", "123.456".clearAndFilterExpression(FormatterSymbols.Comma))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hasFormatterSymbol() {
|
||||
assertEquals("123456", "123,456".clearAndFilterExpression(FormatterSymbols.Comma))
|
||||
assertEquals("123456.789", "123,456.789".clearAndFilterExpression(FormatterSymbols.Comma))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hasWrongFormatterSymbol() {
|
||||
assertEquals("123456", "123 456".clearAndFilterExpression(FormatterSymbols.Comma))
|
||||
assertEquals("123456.789", "123 456.789".clearAndFilterExpression(FormatterSymbols.Comma))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fractionExpression() {
|
||||
assertEquals("1600+1234÷56789", "1,600 1234⁄56789".clearAndFilterExpression(FormatterSymbols.Comma))
|
||||
assertEquals("123456.789+1234÷56789", "123,456.789 1234⁄56789".clearAndFilterExpression(FormatterSymbols.Comma))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun garbage() {
|
||||
// 'e' is a known symbol
|
||||
assertEquals("eeee−123", "pee pee poo poo -123".clearAndFilterExpression(FormatterSymbols.Comma))
|
||||
assertEquals("eeee−123.456", "pee pee poo poo -123.456".clearAndFilterExpression(FormatterSymbols.Comma))
|
||||
}
|
||||
}
|
@ -27,42 +27,46 @@ class ExpressionTransformerTest {
|
||||
|
||||
private val expr = ExpressionTransformer(FormatterSymbols.Comma)
|
||||
|
||||
private fun origToTrans(orig: String, trans: String, offset: Int): Int =
|
||||
expr.ExpressionMapping(orig, trans).originalToTransformed(offset)
|
||||
// Use "|" for cursor
|
||||
private fun origToTrans(orig: String, trans: String) {
|
||||
val transformed = trans.replace("|", "")
|
||||
val original = orig.replace("|", "")
|
||||
|
||||
private fun transToOrig(trans: String, orig: String, offset: Int): Int =
|
||||
expr.ExpressionMapping(orig, trans).transformedToOriginal(offset)
|
||||
val offsetInTrans = trans.indexOf("|")
|
||||
val offsetInOrig = orig.indexOf("|")
|
||||
|
||||
val expressionMapping = expr.ExpressionMapping(original, transformed)
|
||||
|
||||
assertEquals(offsetInTrans, expressionMapping.originalToTransformed(offsetInOrig))
|
||||
}
|
||||
|
||||
private fun transToOrig(trans: String, orig: String) {
|
||||
val transformed = trans.replace("|", "")
|
||||
val original = orig.replace("|", "")
|
||||
|
||||
val offsetInTrans = trans.indexOf("|")
|
||||
val offsetInOrig = orig.indexOf("|")
|
||||
|
||||
val expressionMapping = expr.ExpressionMapping(original, transformed)
|
||||
|
||||
assertEquals(offsetInOrig, expressionMapping.transformedToOriginal(offsetInTrans))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test 1234`() {
|
||||
// at the start
|
||||
assertEquals(0, origToTrans("1,234", "1234", 0))
|
||||
assertEquals(0, transToOrig("1,234", "1234", 0))
|
||||
transToOrig("|123", "|123")
|
||||
origToTrans("12|3", "12|3")
|
||||
|
||||
// somewhere in inside, no offset needed
|
||||
assertEquals(1, origToTrans("1234", "1,234", 1))
|
||||
assertEquals(1, transToOrig("1,234", "1234", 1))
|
||||
transToOrig("|1,234", "|1234")
|
||||
origToTrans("|1234", "|1,234")
|
||||
|
||||
// somewhere in inside, offset needed
|
||||
assertEquals(1, transToOrig("1,234", "1234", 2))
|
||||
transToOrig("1,234|", "1234|")
|
||||
origToTrans("1234|", "1,234|")
|
||||
|
||||
// at the end
|
||||
assertEquals(5, origToTrans("1234", "1,234", 4))
|
||||
assertEquals(4, transToOrig("1,234", "1234", 5))
|
||||
}
|
||||
transToOrig("|cos(1)+1,234", "|cos(1)+1234")
|
||||
origToTrans("co|s(1)+1234", "|cos(1)+1,234")
|
||||
|
||||
@Test
|
||||
fun `test 123`() {
|
||||
// at the start
|
||||
assertEquals(0, origToTrans("123", "123", 0))
|
||||
assertEquals(0, transToOrig("123", "123", 0))
|
||||
|
||||
// somewhere in inside
|
||||
assertEquals(1, origToTrans("123", "123", 1))
|
||||
assertEquals(1, transToOrig("123", "123", 1))
|
||||
|
||||
// at the end
|
||||
assertEquals(3, origToTrans("123", "123", 3))
|
||||
assertEquals(3, transToOrig("123", "123", 3))
|
||||
transToOrig("cos|(1)+1,234", "cos(|1)+1234")
|
||||
origToTrans("cos|(1)+1234", "cos(|1)+1,234")
|
||||
}
|
||||
}
|
||||
|
@ -32,8 +32,9 @@ private const val INCOMPLETE_EXPR = "50+123456÷8×0.8-12+"
|
||||
private const val COMPLETE_EXPR = "50+123456÷8×0.8-12+0-√9×4^9+2×(9+8×7)"
|
||||
private const val LONG_HALF_COMPLETE_EXPR = "50+123456÷89078..9×0.8-12+0-√9×4^9+2×(9+8×7)×sin(13sin123cos"
|
||||
private const val SOME_BRACKETS = "(((((((("
|
||||
private const val FRACTION_VALUE = "1600 1234⁄56789"
|
||||
|
||||
class FormatterTest {
|
||||
class FormatterExpressionTest {
|
||||
|
||||
@Test
|
||||
fun setSeparatorSpaces() {
|
||||
@ -47,6 +48,7 @@ class FormatterTest {
|
||||
assertEquals("50+123 456÷8×0.8−12+0−√9×4^9+2×(9+8×7)", COMPLETE_EXPR.format())
|
||||
assertEquals("50+123 456÷89 078..9×0.8−12+0−√9×4^9+2×(9+8×7)×sin(13sin123cos", LONG_HALF_COMPLETE_EXPR.format())
|
||||
assertEquals("((((((((", SOME_BRACKETS.format())
|
||||
assertEquals("1 600 1234⁄56789", FRACTION_VALUE.format())
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -61,6 +63,7 @@ class FormatterTest {
|
||||
assertEquals("50+123,456÷8×0.8−12+0−√9×4^9+2×(9+8×7)", COMPLETE_EXPR.format())
|
||||
assertEquals("50+123,456÷89,078..9×0.8−12+0−√9×4^9+2×(9+8×7)×sin(13sin123cos", LONG_HALF_COMPLETE_EXPR.format())
|
||||
assertEquals("((((((((", SOME_BRACKETS.format())
|
||||
assertEquals("1,600 1234⁄56789", FRACTION_VALUE.format())
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -75,5 +78,6 @@ class FormatterTest {
|
||||
assertEquals("50+123.456÷8×0,8−12+0−√9×4^9+2×(9+8×7)", COMPLETE_EXPR.format())
|
||||
assertEquals("50+123.456÷89.078,,9×0,8−12+0−√9×4^9+2×(9+8×7)×sin(13sin123cos", LONG_HALF_COMPLETE_EXPR.format())
|
||||
assertEquals("((((((((", SOME_BRACKETS.format())
|
||||
assertEquals("1.600 1234⁄56789", FRACTION_VALUE.format())
|
||||
}
|
||||
}
|
@ -24,13 +24,25 @@ import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
import java.math.RoundingMode
|
||||
|
||||
/**
|
||||
* Tries to convert [BigDecimal] into fractional string.
|
||||
*
|
||||
* 0.5 -> `1⁄2`
|
||||
*
|
||||
* 123.5 -> `123 1⁄2`
|
||||
*
|
||||
* 123 -> `Empty string`
|
||||
*
|
||||
* @receiver [BigDecimal]. Scale doesn't matter, but should be `MAX_PRECISION`
|
||||
* @return
|
||||
*/
|
||||
fun BigDecimal.toFractionalString(): String {
|
||||
// https://www.khanacademy.org/math/cc-eighth-grade-math/cc-8th-numbers-operations/cc-8th-repeating-decimals/v/coverting-repeating-decimals-to-fractions-1
|
||||
// https://www.khanacademy.org/math/cc-eighth-grade-math/cc-8th-numbers-operations/cc-8th-repeating-decimals/v/coverting-repeating-decimals-to-fractions-2
|
||||
val (integral, fractional) = this.divideAndRemainder(BigDecimal.ONE)
|
||||
val integralBI = integral.toBigInteger()
|
||||
|
||||
if (fractional.isEqualTo(BigDecimal.ZERO)) return integralBI.toString()
|
||||
if (fractional.isEqualTo(BigDecimal.ZERO)) return ""
|
||||
|
||||
val res: String = if (integral.isEqualTo(BigDecimal.ZERO)) "" else "$integralBI "
|
||||
|
||||
|
@ -116,7 +116,7 @@ fun TextBox(
|
||||
var outputTF by remember(output) {
|
||||
mutableStateOf(TextFieldValue(output.text))
|
||||
}
|
||||
UnformattedTextField(
|
||||
ExpressionTextField(
|
||||
modifier = Modifier
|
||||
.weight(2f)
|
||||
.fillMaxWidth()
|
||||
@ -124,6 +124,7 @@ fun TextBox(
|
||||
value = outputTF,
|
||||
minRatio = 1f,
|
||||
onCursorChange = { outputTF = outputTF.copy(selection = it) },
|
||||
formatterSymbols = formatterSymbols,
|
||||
textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.6f),
|
||||
readOnly = true,
|
||||
)
|
||||
|
@ -26,13 +26,13 @@ class DecimalToFractionTest {
|
||||
@Test
|
||||
fun testNoDecimal1() {
|
||||
val bd = BigDecimal("100")
|
||||
assertFractional("100", bd.toFractionalString())
|
||||
assertFractional("", bd.toFractionalString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNoDecimal2() {
|
||||
val bd = BigDecimal("100.000000000")
|
||||
assertFractional("100", bd.toFractionalString())
|
||||
assertFractional("", bd.toFractionalString())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
Loading…
x
Reference in New Issue
Block a user