diff --git a/core/base/src/main/java/com/sadellie/unitto/core/base/Token.kt b/core/base/src/main/java/com/sadellie/unitto/core/base/Token.kt index dd392aa4..5a9cf6c8 100644 --- a/core/base/src/main/java/com/sadellie/unitto/core/base/Token.kt +++ b/core/base/src/main/java/com/sadellie/unitto/core/base/Token.kt @@ -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 { diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FormatterExtensions.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FormatterExtensions.kt index 3a775d58..8e05d594 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FormatterExtensions.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FormatterExtensions.kt @@ -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 { +private fun String.leaveLegalTokensOnly(legalTokens: List): String { val streamOfTokens = this fun peekTokenAfter(cursor: Int): String? { diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/InputTextField.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/InputTextField.kt index acb3241c..06958920 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/InputTextField.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/InputTextField.kt @@ -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( diff --git a/core/ui/src/test/java/com/sadellie/unitto/core/ui/CleanAndFilterExpression.kt b/core/ui/src/test/java/com/sadellie/unitto/core/ui/CleanAndFilterExpression.kt new file mode 100644 index 00000000..25e3158d --- /dev/null +++ b/core/ui/src/test/java/com/sadellie/unitto/core/ui/CleanAndFilterExpression.kt @@ -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 . + */ + +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)) + } +} diff --git a/core/ui/src/test/java/com/sadellie/unitto/core/ui/ExpressionTransformerTest.kt b/core/ui/src/test/java/com/sadellie/unitto/core/ui/ExpressionTransformerTest.kt index 430a76c3..5503b8e1 100644 --- a/core/ui/src/test/java/com/sadellie/unitto/core/ui/ExpressionTransformerTest.kt +++ b/core/ui/src/test/java/com/sadellie/unitto/core/ui/ExpressionTransformerTest.kt @@ -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") } } diff --git a/core/ui/src/test/java/com/sadellie/unitto/core/ui/FormatterTest.kt b/core/ui/src/test/java/com/sadellie/unitto/core/ui/FormatterExpressionTest.kt similarity index 93% rename from core/ui/src/test/java/com/sadellie/unitto/core/ui/FormatterTest.kt rename to core/ui/src/test/java/com/sadellie/unitto/core/ui/FormatterExpressionTest.kt index c0dbe8bb..559ea8a0 100644 --- a/core/ui/src/test/java/com/sadellie/unitto/core/ui/FormatterTest.kt +++ b/core/ui/src/test/java/com/sadellie/unitto/core/ui/FormatterExpressionTest.kt @@ -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()) } } \ No newline at end of file diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/DecimalToFraction.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/DecimalToFraction.kt index 61374872..937944eb 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/DecimalToFraction.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/DecimalToFraction.kt @@ -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 " diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/TextBox.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/TextBox.kt index d04b609d..673fc4df 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/TextBox.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/TextBox.kt @@ -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, ) diff --git a/feature/calculator/src/test/java/com/sadellie/unitto/feature/calculator/DecimalToFractionTest.kt b/feature/calculator/src/test/java/com/sadellie/unitto/feature/calculator/DecimalToFractionTest.kt index 70da9372..bf43177e 100644 --- a/feature/calculator/src/test/java/com/sadellie/unitto/feature/calculator/DecimalToFractionTest.kt +++ b/feature/calculator/src/test/java/com/sadellie/unitto/feature/calculator/DecimalToFractionTest.kt @@ -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