mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-20 09:15:26 +02:00
Expression formatting and cursor fixes
P.S. This commit is a mess, lots of hardcoded stuff and weird decisions.
This commit is contained in:
parent
1a6d6fdce4
commit
81f750402a
@ -25,23 +25,28 @@ import com.sadellie.unitto.core.base.KEY_0
|
|||||||
import com.sadellie.unitto.core.base.KEY_COMMA
|
import com.sadellie.unitto.core.base.KEY_COMMA
|
||||||
import com.sadellie.unitto.core.base.KEY_DOT
|
import com.sadellie.unitto.core.base.KEY_DOT
|
||||||
import com.sadellie.unitto.core.base.KEY_E
|
import com.sadellie.unitto.core.base.KEY_E
|
||||||
import com.sadellie.unitto.core.base.KEY_LEFT_BRACKET
|
|
||||||
import com.sadellie.unitto.core.base.KEY_MINUS
|
import com.sadellie.unitto.core.base.KEY_MINUS
|
||||||
import com.sadellie.unitto.core.base.KEY_RIGHT_BRACKET
|
|
||||||
import com.sadellie.unitto.core.base.OPERATORS
|
|
||||||
import com.sadellie.unitto.core.base.Separator
|
import com.sadellie.unitto.core.base.Separator
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.math.RoundingMode
|
import java.math.RoundingMode
|
||||||
|
|
||||||
object Formatter {
|
// Legacy, LOL. Will change later
|
||||||
private const val SPACE = " "
|
object Formatter : UnittoFormatter()
|
||||||
private const val PERIOD = "."
|
|
||||||
private const val COMMA = ","
|
open class UnittoFormatter {
|
||||||
|
/**
|
||||||
|
* This regex will catch things like "123.456", "123", ".456"
|
||||||
|
*/
|
||||||
|
private val numbersRegex = Regex("[\\d.]+")
|
||||||
|
|
||||||
|
private val SPACE = " "
|
||||||
|
private val PERIOD = "."
|
||||||
|
private val COMMA = ","
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Grouping separator.
|
* Grouping separator.
|
||||||
*/
|
*/
|
||||||
private var grouping: String = SPACE
|
var grouping: String = SPACE
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fractional part separator.
|
* Fractional part separator.
|
||||||
@ -91,11 +96,8 @@ object Formatter {
|
|||||||
|
|
||||||
var output = input
|
var output = input
|
||||||
|
|
||||||
// We may receive expressions
|
// We may receive expressions. Find all numbers in this expression
|
||||||
// Find all numbers in that expression
|
val allNumbers: List<String> = input.getOnlyNumbers()
|
||||||
val allNumbers: List<String> = input.split(
|
|
||||||
*OPERATORS.toTypedArray(), KEY_LEFT_BRACKET, KEY_RIGHT_BRACKET
|
|
||||||
)
|
|
||||||
|
|
||||||
allNumbers.forEach {
|
allNumbers.forEach {
|
||||||
output = output.replace(it, formatNumber(it))
|
output = output.replace(it, formatNumber(it))
|
||||||
@ -108,34 +110,50 @@ object Formatter {
|
|||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove formatting. Reverses [format]
|
||||||
|
*/
|
||||||
|
fun reFormat(input: String): String {
|
||||||
|
// We get 123.45,6789
|
||||||
|
// We need 12.345,6789
|
||||||
|
|
||||||
|
// 123.45,6789
|
||||||
|
// Remove grouping
|
||||||
|
// 12345,6789
|
||||||
|
// Replace fractional with "." because formatter accepts only numbers where fractional is a dot
|
||||||
|
|
||||||
|
val cleanString = input
|
||||||
|
.replace(grouping, "")
|
||||||
|
.replace(fractional, KEY_DOT)
|
||||||
|
return format(cleanString)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format given [input].
|
* Format given [input].
|
||||||
*
|
*
|
||||||
* Input must be a number. Will replace grouping separators and fractional part separators.
|
* Input must be a number with dot!!!. Will replace grouping separators and fractional part (dot)
|
||||||
|
* separators.
|
||||||
*
|
*
|
||||||
* @see grouping
|
* @see grouping
|
||||||
* @see fractional
|
* @see fractional
|
||||||
*/
|
*/
|
||||||
private fun formatNumber(input: String): String {
|
private fun formatNumber(input: String): String {
|
||||||
val splitInput = input.split(".")
|
if (input.any { it.isLetter() }) return input
|
||||||
var firstPart = splitInput[0]
|
|
||||||
|
var firstPart = input.takeWhile { it != '.' }
|
||||||
|
val remainingPart = input.removePrefix(firstPart)
|
||||||
|
|
||||||
// Number of empty symbols (spaces) we need to add to correctly split into chunks.
|
// Number of empty symbols (spaces) we need to add to correctly split into chunks.
|
||||||
val offset = 3 - firstPart.length.mod(3)
|
val offset = 3 - firstPart.length.mod(3)
|
||||||
var output = if (offset != 3) {
|
val output = if (offset != 3) {
|
||||||
// We add some spaces at the begging so that last chunk has 3 symbols
|
// We add some spaces at the beginning so that last chunk has 3 symbols
|
||||||
firstPart = " ".repeat(offset) + firstPart
|
firstPart = " ".repeat(offset) + firstPart
|
||||||
firstPart.chunked(3).joinToString(grouping).drop(offset)
|
firstPart.chunked(3).joinToString(grouping).drop(offset)
|
||||||
} else {
|
} else {
|
||||||
firstPart.chunked(3).joinToString(grouping)
|
firstPart.chunked(3).joinToString(grouping)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handling fractional part
|
return (output + remainingPart.replace(".", fractional))
|
||||||
if (input.contains(".")) {
|
|
||||||
output = output + fractional + splitInput.getOrElse(1) { "" }
|
|
||||||
}
|
|
||||||
|
|
||||||
return output
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -175,4 +193,10 @@ object Formatter {
|
|||||||
}
|
}
|
||||||
return result.trimEnd()
|
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()
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,7 @@ private const val INCOMPLETE_VALUE = "123456."
|
|||||||
private const val NO_FRACTIONAL_VALUE = "123456"
|
private const val NO_FRACTIONAL_VALUE = "123456"
|
||||||
private const val INCOMPLETE_EXPR = "50+123456÷8×0.8–12+"
|
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 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 SOME_BRACKETS = "(((((((("
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
@ -55,6 +56,7 @@ class FormatterTest {
|
|||||||
assertEquals("123 456", formatter.format(NO_FRACTIONAL_VALUE))
|
assertEquals("123 456", formatter.format(NO_FRACTIONAL_VALUE))
|
||||||
assertEquals("50+123 456÷8×0.8–12+", formatter.format(INCOMPLETE_EXPR))
|
assertEquals("50+123 456÷8×0.8–12+", formatter.format(INCOMPLETE_EXPR))
|
||||||
assertEquals("50+123 456÷8×0.8–12+0–√9×4^9+2×(9+8×7)", formatter.format(COMPLETE_EXPR))
|
assertEquals("50+123 456÷8×0.8–12+0–√9×4^9+2×(9+8×7)", formatter.format(COMPLETE_EXPR))
|
||||||
|
assertEquals("50+123 456÷89 078..9×0.8–12+0–√9×4^9+2×(9+8×7)×sin(13sin123cos", formatter.format(LONG_HALF_COMPLETE_EXPR))
|
||||||
assertEquals("((((((((", formatter.format(SOME_BRACKETS))
|
assertEquals("((((((((", formatter.format(SOME_BRACKETS))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,6 +71,7 @@ class FormatterTest {
|
|||||||
assertEquals("123,456", formatter.format(NO_FRACTIONAL_VALUE))
|
assertEquals("123,456", formatter.format(NO_FRACTIONAL_VALUE))
|
||||||
assertEquals("50+123,456÷8×0.8–12+", formatter.format(INCOMPLETE_EXPR))
|
assertEquals("50+123,456÷8×0.8–12+", formatter.format(INCOMPLETE_EXPR))
|
||||||
assertEquals("50+123,456÷8×0.8–12+0–√9×4^9+2×(9+8×7)", formatter.format(COMPLETE_EXPR))
|
assertEquals("50+123,456÷8×0.8–12+0–√9×4^9+2×(9+8×7)", formatter.format(COMPLETE_EXPR))
|
||||||
|
assertEquals("50+123,456÷89,078..9×0.8–12+0–√9×4^9+2×(9+8×7)×sin(13sin123cos", formatter.format(LONG_HALF_COMPLETE_EXPR))
|
||||||
assertEquals("((((((((", formatter.format(SOME_BRACKETS))
|
assertEquals("((((((((", formatter.format(SOME_BRACKETS))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,6 +86,7 @@ class FormatterTest {
|
|||||||
assertEquals("123.456", formatter.format(NO_FRACTIONAL_VALUE))
|
assertEquals("123.456", formatter.format(NO_FRACTIONAL_VALUE))
|
||||||
assertEquals("50+123.456÷8×0,8–12+", formatter.format(INCOMPLETE_EXPR))
|
assertEquals("50+123.456÷8×0,8–12+", formatter.format(INCOMPLETE_EXPR))
|
||||||
assertEquals("50+123.456÷8×0,8–12+0–√9×4^9+2×(9+8×7)", formatter.format(COMPLETE_EXPR))
|
assertEquals("50+123.456÷8×0,8–12+0–√9×4^9+2×(9+8×7)", formatter.format(COMPLETE_EXPR))
|
||||||
|
assertEquals("50+123.456÷89.078,,9×0,8–12+0–√9×4^9+2×(9+8×7)×sin(13sin123cos", formatter.format(LONG_HALF_COMPLETE_EXPR))
|
||||||
assertEquals("((((((((", formatter.format(SOME_BRACKETS))
|
assertEquals("((((((((", formatter.format(SOME_BRACKETS))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,96 +95,96 @@ class FormatterTest {
|
|||||||
formatter.setSeparator(Separator.SPACES)
|
formatter.setSeparator(Separator.SPACES)
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
var basicValue = BigDecimal.valueOf(1)
|
var basicValue = BigDecimal.valueOf(1)
|
||||||
assertEquals("-28", Formatter.formatTime("-28", basicValue))
|
assertEquals("-28", formatter.formatTime("-28", basicValue))
|
||||||
assertEquals("-0.05", Formatter.formatTime("-0.05", basicValue))
|
assertEquals("-0.05", formatter.formatTime("-0.05", basicValue))
|
||||||
assertEquals("0", Formatter.formatTime("0", basicValue))
|
assertEquals("0", formatter.formatTime("0", 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)
|
basicValue = BigDecimal.valueOf(86_400_000_000_000_000_000_000.0)
|
||||||
assertEquals("-28d", Formatter.formatTime("-28", basicValue))
|
assertEquals("-28d", formatter.formatTime("-28", basicValue))
|
||||||
assertEquals("-1h 12m", Formatter.formatTime("-0.05", basicValue))
|
assertEquals("-1h 12m", formatter.formatTime("-0.05", basicValue))
|
||||||
assertEquals("0", Formatter.formatTime("0", basicValue))
|
assertEquals("0", formatter.formatTime("0", basicValue))
|
||||||
assertEquals("0", Formatter.formatTime("-0", basicValue))
|
assertEquals("0", formatter.formatTime("-0", basicValue))
|
||||||
|
|
||||||
// DAYS
|
// DAYS
|
||||||
basicValue = BigDecimal.valueOf(86_400_000_000_000_000_000_000.0)
|
basicValue = BigDecimal.valueOf(86_400_000_000_000_000_000_000.0)
|
||||||
assertEquals("12h", Formatter.formatTime("0.5", basicValue))
|
assertEquals("12h", formatter.formatTime("0.5", basicValue))
|
||||||
assertEquals("1h 12m", Formatter.formatTime("0.05", basicValue))
|
assertEquals("1h 12m", formatter.formatTime("0.05", basicValue))
|
||||||
assertEquals("7m 12s", Formatter.formatTime("0.005", basicValue))
|
assertEquals("7m 12s", formatter.formatTime("0.005", basicValue))
|
||||||
assertEquals("28d", Formatter.formatTime("28", basicValue))
|
assertEquals("28d", formatter.formatTime("28", basicValue))
|
||||||
assertEquals("90d", Formatter.formatTime("90", basicValue))
|
assertEquals("90d", formatter.formatTime("90", basicValue))
|
||||||
assertEquals("90d 12h", Formatter.formatTime("90.5", basicValue))
|
assertEquals("90d 12h", formatter.formatTime("90.5", basicValue))
|
||||||
assertEquals("90d 7m 12s", Formatter.formatTime("90.005", basicValue))
|
assertEquals("90d 7m 12s", formatter.formatTime("90.005", basicValue))
|
||||||
|
|
||||||
// HOURS
|
// HOURS
|
||||||
basicValue = BigDecimal.valueOf(3_600_000_000_000_000_000_000.0)
|
basicValue = BigDecimal.valueOf(3_600_000_000_000_000_000_000.0)
|
||||||
assertEquals("30m", Formatter.formatTime("0.5", basicValue))
|
assertEquals("30m", formatter.formatTime("0.5", basicValue))
|
||||||
assertEquals("3m", Formatter.formatTime("0.05", basicValue))
|
assertEquals("3m", formatter.formatTime("0.05", basicValue))
|
||||||
assertEquals("18s", Formatter.formatTime("0.005", basicValue))
|
assertEquals("18s", formatter.formatTime("0.005", basicValue))
|
||||||
assertEquals("1d 4h", Formatter.formatTime("28", basicValue))
|
assertEquals("1d 4h", formatter.formatTime("28", basicValue))
|
||||||
assertEquals("3d 18h", Formatter.formatTime("90", basicValue))
|
assertEquals("3d 18h", formatter.formatTime("90", basicValue))
|
||||||
assertEquals("3d 18h 30m", Formatter.formatTime("90.5", basicValue))
|
assertEquals("3d 18h 30m", formatter.formatTime("90.5", basicValue))
|
||||||
assertEquals("3d 18h 18s", Formatter.formatTime("90.005", basicValue))
|
assertEquals("3d 18h 18s", formatter.formatTime("90.005", basicValue))
|
||||||
|
|
||||||
// MINUTES
|
// MINUTES
|
||||||
basicValue = BigDecimal.valueOf(60_000_000_000_000_000_000.0)
|
basicValue = BigDecimal.valueOf(60_000_000_000_000_000_000.0)
|
||||||
assertEquals("30s", Formatter.formatTime("0.5", basicValue))
|
assertEquals("30s", formatter.formatTime("0.5", basicValue))
|
||||||
assertEquals("3s", Formatter.formatTime("0.05", basicValue))
|
assertEquals("3s", formatter.formatTime("0.05", basicValue))
|
||||||
assertEquals("300ms", Formatter.formatTime("0.005", basicValue))
|
assertEquals("300ms", formatter.formatTime("0.005", basicValue))
|
||||||
assertEquals("28m", Formatter.formatTime("28", basicValue))
|
assertEquals("28m", formatter.formatTime("28", basicValue))
|
||||||
assertEquals("1h 30m", Formatter.formatTime("90", basicValue))
|
assertEquals("1h 30m", formatter.formatTime("90", basicValue))
|
||||||
assertEquals("1h 30m 30s", Formatter.formatTime("90.5", basicValue))
|
assertEquals("1h 30m 30s", formatter.formatTime("90.5", basicValue))
|
||||||
assertEquals("1h 30m 300ms", Formatter.formatTime("90.005", basicValue))
|
assertEquals("1h 30m 300ms", formatter.formatTime("90.005", basicValue))
|
||||||
|
|
||||||
// SECONDS
|
// SECONDS
|
||||||
basicValue = BigDecimal.valueOf(1_000_000_000_000_000_000)
|
basicValue = BigDecimal.valueOf(1_000_000_000_000_000_000)
|
||||||
assertEquals("500ms", Formatter.formatTime("0.5", basicValue))
|
assertEquals("500ms", formatter.formatTime("0.5", basicValue))
|
||||||
assertEquals("50ms", Formatter.formatTime("0.05", basicValue))
|
assertEquals("50ms", formatter.formatTime("0.05", basicValue))
|
||||||
assertEquals("5ms", Formatter.formatTime("0.005", basicValue))
|
assertEquals("5ms", formatter.formatTime("0.005", basicValue))
|
||||||
assertEquals("28s", Formatter.formatTime("28", basicValue))
|
assertEquals("28s", formatter.formatTime("28", basicValue))
|
||||||
assertEquals("1m 30s", Formatter.formatTime("90", basicValue))
|
assertEquals("1m 30s", formatter.formatTime("90", basicValue))
|
||||||
assertEquals("1m 30s 500ms", Formatter.formatTime("90.5", basicValue))
|
assertEquals("1m 30s 500ms", formatter.formatTime("90.5", basicValue))
|
||||||
assertEquals("1m 30s 5ms", Formatter.formatTime("90.005", basicValue))
|
assertEquals("1m 30s 5ms", formatter.formatTime("90.005", basicValue))
|
||||||
|
|
||||||
// MILLISECONDS
|
// MILLISECONDS
|
||||||
basicValue = BigDecimal.valueOf(1_000_000_000_000_000)
|
basicValue = BigDecimal.valueOf(1_000_000_000_000_000)
|
||||||
assertEquals("500µs", Formatter.formatTime("0.5", basicValue))
|
assertEquals("500µs", formatter.formatTime("0.5", basicValue))
|
||||||
assertEquals("50µs", Formatter.formatTime("0.05", basicValue))
|
assertEquals("50µs", formatter.formatTime("0.05", basicValue))
|
||||||
assertEquals("5µs", Formatter.formatTime("0.005", basicValue))
|
assertEquals("5µs", formatter.formatTime("0.005", basicValue))
|
||||||
assertEquals("28ms", Formatter.formatTime("28", basicValue))
|
assertEquals("28ms", formatter.formatTime("28", basicValue))
|
||||||
assertEquals("90ms", Formatter.formatTime("90", basicValue))
|
assertEquals("90ms", formatter.formatTime("90", basicValue))
|
||||||
assertEquals("90ms 500µs", Formatter.formatTime("90.5", basicValue))
|
assertEquals("90ms 500µs", formatter.formatTime("90.5", basicValue))
|
||||||
assertEquals("90ms 5µs", Formatter.formatTime("90.005", basicValue))
|
assertEquals("90ms 5µs", formatter.formatTime("90.005", basicValue))
|
||||||
|
|
||||||
// MICROSECONDS
|
// MICROSECONDS
|
||||||
basicValue = BigDecimal.valueOf(1_000_000_000_000)
|
basicValue = BigDecimal.valueOf(1_000_000_000_000)
|
||||||
assertEquals("500ns", Formatter.formatTime("0.5", basicValue))
|
assertEquals("500ns", formatter.formatTime("0.5", basicValue))
|
||||||
assertEquals("50ns", Formatter.formatTime("0.05", basicValue))
|
assertEquals("50ns", formatter.formatTime("0.05", basicValue))
|
||||||
assertEquals("5ns", Formatter.formatTime("0.005", basicValue))
|
assertEquals("5ns", formatter.formatTime("0.005", basicValue))
|
||||||
assertEquals("28µs", Formatter.formatTime("28", basicValue))
|
assertEquals("28µs", formatter.formatTime("28", basicValue))
|
||||||
assertEquals("90µs", Formatter.formatTime("90", basicValue))
|
assertEquals("90µs", formatter.formatTime("90", basicValue))
|
||||||
assertEquals("90µs 500ns", Formatter.formatTime("90.5", basicValue))
|
assertEquals("90µs 500ns", formatter.formatTime("90.5", basicValue))
|
||||||
assertEquals("90µs 5ns", Formatter.formatTime("90.005", basicValue))
|
assertEquals("90µs 5ns", formatter.formatTime("90.005", basicValue))
|
||||||
|
|
||||||
// NANOSECONDS
|
// NANOSECONDS
|
||||||
basicValue = BigDecimal.valueOf(1_000_000_000)
|
basicValue = BigDecimal.valueOf(1_000_000_000)
|
||||||
assertEquals("500 000 000as", Formatter.formatTime("0.5", basicValue))
|
assertEquals("500 000 000as", formatter.formatTime("0.5", basicValue))
|
||||||
assertEquals("50 000 000as", Formatter.formatTime("0.05", basicValue))
|
assertEquals("50 000 000as", formatter.formatTime("0.05", basicValue))
|
||||||
assertEquals("5 000 000as", Formatter.formatTime("0.005", basicValue))
|
assertEquals("5 000 000as", formatter.formatTime("0.005", basicValue))
|
||||||
assertEquals("28ns", Formatter.formatTime("28", basicValue))
|
assertEquals("28ns", formatter.formatTime("28", basicValue))
|
||||||
assertEquals("90ns", Formatter.formatTime("90", basicValue))
|
assertEquals("90ns", formatter.formatTime("90", basicValue))
|
||||||
assertEquals("90ns 500 000 000as", Formatter.formatTime("90.5", basicValue))
|
assertEquals("90ns 500 000 000as", formatter.formatTime("90.5", basicValue))
|
||||||
assertEquals("90ns 5 000 000as", Formatter.formatTime("90.005", basicValue))
|
assertEquals("90ns 5 000 000as", formatter.formatTime("90.005", basicValue))
|
||||||
|
|
||||||
// ATTOSECONDS
|
// ATTOSECONDS
|
||||||
basicValue = BigDecimal.valueOf(1)
|
basicValue = BigDecimal.valueOf(1)
|
||||||
assertEquals("0.5", Formatter.formatTime("0.5", basicValue))
|
assertEquals("0.5", formatter.formatTime("0.5", basicValue))
|
||||||
assertEquals("0.05", Formatter.formatTime("0.05", basicValue))
|
assertEquals("0.05", formatter.formatTime("0.05", basicValue))
|
||||||
assertEquals("0.005", Formatter.formatTime("0.005", basicValue))
|
assertEquals("0.005", formatter.formatTime("0.005", basicValue))
|
||||||
assertEquals("28", Formatter.formatTime("28", basicValue))
|
assertEquals("28", formatter.formatTime("28", basicValue))
|
||||||
assertEquals("90", Formatter.formatTime("90", basicValue))
|
assertEquals("90", formatter.formatTime("90", basicValue))
|
||||||
assertEquals("90.5", Formatter.formatTime("90.5", basicValue))
|
assertEquals("90.5", formatter.formatTime("90.5", basicValue))
|
||||||
assertEquals("90.005", Formatter.formatTime("90.005", basicValue))
|
assertEquals("90.005", formatter.formatTime("90.005", basicValue))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -54,16 +54,15 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.onPlaced
|
import androidx.compose.ui.layout.onPlaced
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextRange
|
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.sadellie.unitto.core.ui.Formatter
|
||||||
import com.sadellie.unitto.core.ui.common.MenuButton
|
import com.sadellie.unitto.core.ui.common.MenuButton
|
||||||
import com.sadellie.unitto.core.ui.common.UnittoScreenWithTopBar
|
import com.sadellie.unitto.core.ui.common.UnittoScreenWithTopBar
|
||||||
import com.sadellie.unitto.core.ui.Formatter
|
|
||||||
import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayMedium
|
import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayMedium
|
||||||
import com.sadellie.unitto.data.model.HistoryItem
|
import com.sadellie.unitto.data.model.HistoryItem
|
||||||
import com.sadellie.unitto.feature.calculator.components.CalculatorKeyboard
|
import com.sadellie.unitto.feature.calculator.components.CalculatorKeyboard
|
||||||
@ -205,10 +204,7 @@ private fun CalculatorScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 8.dp),
|
.padding(horizontal = 8.dp),
|
||||||
value = TextFieldValue(
|
value = uiState.input,
|
||||||
text = uiState.input,
|
|
||||||
selection = TextRange(uiState.selection.first, uiState.selection.last)
|
|
||||||
),
|
|
||||||
onCursorChange = onCursorChange,
|
onCursorChange = onCursorChange,
|
||||||
pasteCallback = addSymbol,
|
pasteCallback = addSymbol,
|
||||||
cutCallback = deleteSymbol
|
cutCallback = deleteSymbol
|
||||||
@ -306,7 +302,7 @@ private fun PreviewCalculatorScreen() {
|
|||||||
|
|
||||||
CalculatorScreen(
|
CalculatorScreen(
|
||||||
uiState = CalculatorUIState(
|
uiState = CalculatorUIState(
|
||||||
input = "12345",
|
input = TextFieldValue("12345"),
|
||||||
output = "12345",
|
output = "12345",
|
||||||
history = historyItems
|
history = historyItems
|
||||||
),
|
),
|
||||||
|
@ -18,12 +18,12 @@
|
|||||||
|
|
||||||
package com.sadellie.unitto.feature.calculator
|
package com.sadellie.unitto.feature.calculator
|
||||||
|
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import com.sadellie.unitto.data.model.HistoryItem
|
import com.sadellie.unitto.data.model.HistoryItem
|
||||||
|
|
||||||
internal data class CalculatorUIState(
|
internal data class CalculatorUIState(
|
||||||
val input: String = "",
|
val input: TextFieldValue = TextFieldValue(),
|
||||||
val output: String = "",
|
val output: String = "",
|
||||||
val selection: IntRange = 0..0,
|
|
||||||
val angleMode: AngleMode = AngleMode.RAD,
|
val angleMode: AngleMode = AngleMode.RAD,
|
||||||
val history: List<HistoryItem> = emptyList()
|
val history: List<HistoryItem> = emptyList()
|
||||||
)
|
)
|
||||||
|
@ -45,11 +45,13 @@ import org.mariuszgromada.math.mxparser.Expression
|
|||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.mariuszgromada.math.mxparser.mXparser as MathParser
|
import org.mariuszgromada.math.mxparser.mXparser as MathParser
|
||||||
|
import org.mariuszgromada.math.mxparser.License as MathParserLicense
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
internal class CalculatorViewModel @Inject constructor(
|
internal class CalculatorViewModel @Inject constructor(
|
||||||
userPrefsRepository: UserPreferencesRepository,
|
userPrefsRepository: UserPreferencesRepository,
|
||||||
private val calculatorHistoryRepository: CalculatorHistoryRepository
|
private val calculatorHistoryRepository: CalculatorHistoryRepository,
|
||||||
|
private val textFieldController: TextFieldController
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _userPrefs: StateFlow<UserPreferences> =
|
private val _userPrefs: StateFlow<UserPreferences> =
|
||||||
userPrefsRepository.userPreferencesFlow.stateIn(
|
userPrefsRepository.userPreferencesFlow.stateIn(
|
||||||
@ -58,19 +60,16 @@ internal class CalculatorViewModel @Inject constructor(
|
|||||||
UserPreferences()
|
UserPreferences()
|
||||||
)
|
)
|
||||||
|
|
||||||
private val _input: MutableStateFlow<String> = MutableStateFlow("")
|
|
||||||
private val _output: MutableStateFlow<String> = MutableStateFlow("")
|
private val _output: MutableStateFlow<String> = MutableStateFlow("")
|
||||||
private val _selection: MutableStateFlow<IntRange> = MutableStateFlow(IntRange(0, 0))
|
|
||||||
private val _angleMode: MutableStateFlow<AngleMode> = MutableStateFlow(AngleMode.RAD)
|
private val _angleMode: MutableStateFlow<AngleMode> = MutableStateFlow(AngleMode.RAD)
|
||||||
private val _history = calculatorHistoryRepository.historyFlow
|
private val _history = calculatorHistoryRepository.historyFlow
|
||||||
|
|
||||||
val uiState = combine(
|
val uiState = combine(
|
||||||
_input, _output, _selection, _angleMode, _history
|
textFieldController.input, _output, _angleMode, _history, _userPrefs
|
||||||
) { input, output, selection, angleMode, history ->
|
) { input, output, angleMode, history, _ ->
|
||||||
return@combine CalculatorUIState(
|
return@combine CalculatorUIState(
|
||||||
input = input,
|
input = input,
|
||||||
output = output,
|
output = output,
|
||||||
selection = selection,
|
|
||||||
angleMode = angleMode,
|
angleMode = angleMode,
|
||||||
history = history
|
history = history
|
||||||
)
|
)
|
||||||
@ -78,30 +77,11 @@ internal class CalculatorViewModel @Inject constructor(
|
|||||||
viewModelScope, SharingStarted.WhileSubscribed(5000L), CalculatorUIState()
|
viewModelScope, SharingStarted.WhileSubscribed(5000L), CalculatorUIState()
|
||||||
)
|
)
|
||||||
|
|
||||||
fun addSymbol(symbol: String) {
|
fun addSymbol(symbol: String) = textFieldController.addToInput(symbol)
|
||||||
val selection = _selection.value
|
|
||||||
_input.update {
|
|
||||||
if (it.isEmpty()) symbol else it.replaceRange(selection.first, selection.last, symbol)
|
|
||||||
}
|
|
||||||
_selection.update { it.first + symbol.length..it.first + symbol.length }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteSymbol() {
|
fun deleteSymbol() = textFieldController.delete()
|
||||||
val selection = _selection.value
|
|
||||||
val newSelectionStart = when (selection.last) {
|
|
||||||
0 -> return
|
|
||||||
selection.first -> _selection.value.first - 1
|
|
||||||
else -> _selection.value.first
|
|
||||||
}
|
|
||||||
|
|
||||||
_selection.update { newSelectionStart..newSelectionStart }
|
fun clearSymbols() = textFieldController.clearInput()
|
||||||
_input.update { it.removeRange(newSelectionStart, selection.last) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearSymbols() {
|
|
||||||
_selection.update { 0..0 }
|
|
||||||
_input.update { "" }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleCalculatorMode() {
|
fun toggleCalculatorMode() {
|
||||||
_angleMode.update {
|
_angleMode.update {
|
||||||
@ -117,44 +97,37 @@ internal class CalculatorViewModel @Inject constructor(
|
|||||||
|
|
||||||
// Called when user clicks "=" on a keyboard
|
// Called when user clicks "=" on a keyboard
|
||||||
fun evaluate() {
|
fun evaluate() {
|
||||||
if (!Expression(_input.value.clean).checkSyntax()) return
|
|
||||||
|
|
||||||
// Input and output can change while saving in history. This way we cache it here (i think)
|
// Input and output can change while saving in history. This way we cache it here (i think)
|
||||||
val input = _input.value
|
val currentInput = textFieldController.input.value.text
|
||||||
val output = _output.value
|
val output = _output.value
|
||||||
|
if (!Expression(currentInput.clean).checkSyntax()) return
|
||||||
|
|
||||||
|
// Save to history
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
calculatorHistoryRepository.add(
|
calculatorHistoryRepository.add(
|
||||||
expression = input,
|
expression = textFieldController.inputTextWithoutFormatting(),
|
||||||
result = output
|
result = output
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
_input.update { _output.value }
|
|
||||||
_selection.update { _input.value.length.._input.value.length }
|
|
||||||
_output.update { "" }
|
_output.update { "" }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearHistory() {
|
fun clearHistory() = viewModelScope.launch(Dispatchers.IO) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
calculatorHistoryRepository.clear()
|
calculatorHistoryRepository.clear()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun onCursorChange(selection: IntRange) {
|
fun onCursorChange(selection: IntRange) = textFieldController.moveCursor(selection)
|
||||||
// When we paste, selection is set to the length of the pasted text (start and end)
|
|
||||||
if (selection.first > _input.value.length) return
|
|
||||||
_selection.update { selection }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun calculateInput() {
|
private fun calculateInput() {
|
||||||
|
val currentInput = textFieldController.input.value.text
|
||||||
// Input is empty, don't calculate
|
// Input is empty, don't calculate
|
||||||
if (_input.value.isEmpty()) {
|
if (currentInput.isEmpty()) {
|
||||||
_output.update { "" }
|
_output.update { "" }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val calculated = Expression(_input.value.clean).calculate()
|
val calculated = Expression(currentInput.clean).calculate()
|
||||||
|
|
||||||
// Calculation error, return NaN
|
// Calculation error, return NaN
|
||||||
if (calculated.isNaN() or calculated.isInfinite()) {
|
if (calculated.isNaN() or calculated.isInfinite()) {
|
||||||
@ -168,7 +141,7 @@ internal class CalculatorViewModel @Inject constructor(
|
|||||||
.trimZeros()
|
.trimZeros()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val inputBigDecimal = BigDecimal(_input.value)
|
val inputBigDecimal = BigDecimal(currentInput)
|
||||||
|
|
||||||
// Input and output are identical values
|
// Input and output are identical values
|
||||||
if (inputBigDecimal.compareTo(calculatedBigDecimal) == 0) {
|
if (inputBigDecimal.compareTo(calculatedBigDecimal) == 0) {
|
||||||
@ -192,8 +165,7 @@ internal class CalculatorViewModel @Inject constructor(
|
|||||||
val leftBrackets = count { it.toString() == KEY_LEFT_BRACKET }
|
val leftBrackets = count { it.toString() == KEY_LEFT_BRACKET }
|
||||||
val rightBrackets = count { it.toString() == KEY_RIGHT_BRACKET }
|
val rightBrackets = count { it.toString() == KEY_RIGHT_BRACKET }
|
||||||
val neededBrackets = leftBrackets - rightBrackets
|
val neededBrackets = leftBrackets - rightBrackets
|
||||||
return this
|
return replace(KEY_MINUS_DISPLAY, KEY_MINUS)
|
||||||
.replace(KEY_MINUS_DISPLAY, KEY_MINUS)
|
|
||||||
.plus(KEY_RIGHT_BRACKET.repeat(neededBrackets.coerceAtLeast(0)))
|
.plus(KEY_RIGHT_BRACKET.repeat(neededBrackets.coerceAtLeast(0)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,10 +175,11 @@ internal class CalculatorViewModel @Inject constructor(
|
|||||||
* to load CPU very much. We use BigDecimal to achieve same result without CPU overload.
|
* to load CPU very much. We use BigDecimal to achieve same result without CPU overload.
|
||||||
*/
|
*/
|
||||||
MathParser.setCanonicalRounding(false)
|
MathParser.setCanonicalRounding(false)
|
||||||
|
MathParserLicense.iConfirmNonCommercialUse("Sad Ellie")
|
||||||
|
|
||||||
// Observe and invoke calculation without UI lag.
|
// Observe and invoke calculation without UI lag.
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
merge(_userPrefs, _input, _angleMode).collectLatest {
|
merge(_userPrefs, textFieldController.input, _angleMode).collectLatest {
|
||||||
calculateInput()
|
calculateInput()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,190 @@
|
|||||||
|
/*
|
||||||
|
* 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.feature.calculator
|
||||||
|
|
||||||
|
import androidx.compose.ui.text.TextRange
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import com.sadellie.unitto.core.base.KEY_COS
|
||||||
|
import com.sadellie.unitto.core.base.KEY_DOT
|
||||||
|
import com.sadellie.unitto.core.base.KEY_LN
|
||||||
|
import com.sadellie.unitto.core.base.KEY_LOG
|
||||||
|
import com.sadellie.unitto.core.base.KEY_SIN
|
||||||
|
import com.sadellie.unitto.core.base.KEY_TAN
|
||||||
|
import com.sadellie.unitto.core.ui.UnittoFormatter
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
class TextFieldController @Inject constructor() {
|
||||||
|
// Internally we don't care about user preference here, because during composition this
|
||||||
|
// symbols will be replaced to those that user wanted.
|
||||||
|
// We do this because it adds unnecessary logic: it requires additional logic to observe and
|
||||||
|
// react to formatting preferences at this level.
|
||||||
|
private val localFormatter: UnittoFormatter by lazy {
|
||||||
|
UnittoFormatter().also {
|
||||||
|
it.grouping = "`"
|
||||||
|
it.fractional = "|"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var input: MutableStateFlow<TextFieldValue> = MutableStateFlow(TextFieldValue())
|
||||||
|
|
||||||
|
fun addToInput(symbols: String) {
|
||||||
|
|
||||||
|
val text = input.value.text
|
||||||
|
val selection = input.value.selection
|
||||||
|
val lastToEndDistance = text.length - selection.end
|
||||||
|
|
||||||
|
val newInput = if (text.isEmpty()) {
|
||||||
|
symbols
|
||||||
|
} else {
|
||||||
|
text.replaceRange(selection.start, selection.end, symbols)
|
||||||
|
}
|
||||||
|
|
||||||
|
val inputFormatted = newInput.fixFormat()
|
||||||
|
val newSelectionStartEnd = inputFormatted.length - lastToEndDistance
|
||||||
|
|
||||||
|
input.update {
|
||||||
|
it.copy(
|
||||||
|
text = inputFormatted,
|
||||||
|
selection = TextRange(newSelectionStartEnd, newSelectionStartEnd)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun moveCursor(newPosition: IntRange) {
|
||||||
|
val cursorFixer = CursorFixer(grouping = localFormatter.grouping)
|
||||||
|
val currentInput = input.value.text
|
||||||
|
val fixedLeftCursor = cursorFixer.fixCursorIfNeeded(currentInput, newPosition.first)
|
||||||
|
val fixedRightCursor = cursorFixer.fixCursorIfNeeded(currentInput, newPosition.last)
|
||||||
|
|
||||||
|
// Will modify
|
||||||
|
input.update {
|
||||||
|
it.copy(
|
||||||
|
selection = TextRange(fixedLeftCursor, fixedRightCursor)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun delete() {
|
||||||
|
val selection = input.value.selection
|
||||||
|
val distanceFromEnd = input.value.text.length - selection.end
|
||||||
|
|
||||||
|
val newSelectionStart = when (selection.end) {
|
||||||
|
// Don't delete if at the start of the text field
|
||||||
|
0 -> return
|
||||||
|
// We don't have anything selected (cursor in one position)
|
||||||
|
// like this 1234|56 => after deleting will be like this 123|56
|
||||||
|
// Cursor moved one symbol left
|
||||||
|
selection.start -> selection.start - 1
|
||||||
|
// We have multiple symbols selected
|
||||||
|
// like this 123[45]6 => after deleting will be like this 123|6
|
||||||
|
// Cursor will be placed where selection start was
|
||||||
|
else -> selection.start
|
||||||
|
}
|
||||||
|
|
||||||
|
input.update {
|
||||||
|
val newText = it.text
|
||||||
|
.removeRange(newSelectionStart, it.selection.end)
|
||||||
|
.fixFormat()
|
||||||
|
it.copy(
|
||||||
|
text = newText,
|
||||||
|
selection = TextRange(newText.length - distanceFromEnd, newText.length - distanceFromEnd)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearInput() = input.update { TextFieldValue() }
|
||||||
|
|
||||||
|
fun inputTextWithoutFormatting() = input.value.text
|
||||||
|
.replace(localFormatter.grouping, "")
|
||||||
|
.replace(localFormatter.fractional, KEY_DOT)
|
||||||
|
|
||||||
|
private fun String.fixFormat(): String = localFormatter.reFormat(this)
|
||||||
|
|
||||||
|
inner class CursorFixer(private val grouping: String) {
|
||||||
|
fun fixCursorIfNeeded(str: String, pos: Int): Int {
|
||||||
|
// First we check if try to place cursors at illegal position
|
||||||
|
// If yes,
|
||||||
|
// we go left until cursor is position legally. Remember the distance
|
||||||
|
val bestLeft = bestPositionLeft(str, pos)
|
||||||
|
// we go right until cursor is position legally. Remember the distance
|
||||||
|
val bestRight = bestPositionRight(str, pos)
|
||||||
|
// Now we compare left and right distance
|
||||||
|
val bestPosition = listOf(bestLeft, bestRight)
|
||||||
|
// We move to the that's smaller
|
||||||
|
.minBy { abs(it - pos) }
|
||||||
|
|
||||||
|
return bestPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bestPositionLeft(str: String, pos: Int): Int {
|
||||||
|
var cursorPosition = pos
|
||||||
|
while (placedIllegally(str, cursorPosition)) cursorPosition--
|
||||||
|
return cursorPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bestPositionRight(str: String, pos: Int): Int {
|
||||||
|
var cursorPosition = pos
|
||||||
|
while (placedIllegally(str, cursorPosition)) cursorPosition++
|
||||||
|
return cursorPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun placedIllegally(str: String, pos: Int): Boolean {
|
||||||
|
// For things like "123,|456" - this is illegal
|
||||||
|
if (pos.afterToken(str, grouping)) return true
|
||||||
|
|
||||||
|
// For things like "123,456+c|os(8)" - this is illegal
|
||||||
|
val illegalTokens = listOf(
|
||||||
|
KEY_COS, KEY_SIN, KEY_LN, KEY_LOG, KEY_TAN
|
||||||
|
)
|
||||||
|
|
||||||
|
illegalTokens.forEach {
|
||||||
|
if (pos.atToken(str, it)) return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Don't use if token is 1 symbol long, it wouldn't make sense! Use [afterToken] instead.
|
||||||
|
* @see [afterToken]
|
||||||
|
*/
|
||||||
|
private fun Int.atToken(str: String, token: String): Boolean {
|
||||||
|
val checkBound = (token.length - 1).coerceAtLeast(1)
|
||||||
|
|
||||||
|
val stringToScan = str.substring(
|
||||||
|
startIndex = (this - checkBound).coerceAtLeast(0),
|
||||||
|
endIndex = (this + checkBound).coerceAtMost(str.length)
|
||||||
|
)
|
||||||
|
|
||||||
|
return stringToScan.contains(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Int.afterToken(str: String, token: String): Boolean {
|
||||||
|
val stringToScan = str.substring(
|
||||||
|
startIndex = (this - token.length).coerceAtLeast(0),
|
||||||
|
endIndex = this
|
||||||
|
)
|
||||||
|
|
||||||
|
return stringToScan.contains(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -120,7 +120,7 @@ private fun HistoryListItem(
|
|||||||
Modifier.clickable { onTextClick(historyItem.expression) }
|
Modifier.clickable { onTextClick(historyItem.expression) }
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = historyItem.expression,
|
text = Formatter.format(historyItem.expression),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
@ -22,6 +22,9 @@ import androidx.compose.foundation.text.BasicTextField
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalTextInputService
|
import androidx.compose.ui.platform.LocalTextInputService
|
||||||
@ -29,6 +32,7 @@ import androidx.compose.ui.platform.LocalTextToolbar
|
|||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import com.sadellie.unitto.core.ui.Formatter
|
||||||
import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayLarge
|
import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayLarge
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -40,6 +44,18 @@ internal fun InputTextField(
|
|||||||
cutCallback: () -> Unit
|
cutCallback: () -> Unit
|
||||||
) {
|
) {
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
val formattedInput: TextFieldValue by remember(value) {
|
||||||
|
derivedStateOf {
|
||||||
|
value.copy(
|
||||||
|
// We replace this because internally input value is already formatted, but uses
|
||||||
|
// "|" as grouping and "-" as fractional.
|
||||||
|
value.text
|
||||||
|
.replace("`", Formatter.grouping)
|
||||||
|
.replace("|", Formatter.fractional)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalTextInputService provides null,
|
LocalTextInputService provides null,
|
||||||
LocalTextToolbar provides UnittoTextToolbar(
|
LocalTextToolbar provides UnittoTextToolbar(
|
||||||
@ -51,7 +67,7 @@ internal fun InputTextField(
|
|||||||
BasicTextField(
|
BasicTextField(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
value = value,
|
value = formattedInput,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
onCursorChange(it.selection.start..it.selection.end)
|
onCursorChange(it.selection.start..it.selection.end)
|
||||||
},
|
},
|
||||||
|
@ -0,0 +1,191 @@
|
|||||||
|
/*
|
||||||
|
* 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.feature.calculator
|
||||||
|
|
||||||
|
import com.sadellie.unitto.core.base.Separator
|
||||||
|
import com.sadellie.unitto.core.ui.Formatter
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
internal class TextFieldControllerTest {
|
||||||
|
private lateinit var textFieldController: TextFieldController
|
||||||
|
|
||||||
|
private val TextFieldController.text: String
|
||||||
|
get() = this.input.value.text
|
||||||
|
.replace("`", ",")
|
||||||
|
.replace("|", ".")
|
||||||
|
|
||||||
|
private val TextFieldController.selection: IntRange
|
||||||
|
get() = this.input.value.selection.start..this.input.value.selection.end
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
textFieldController = TextFieldController()
|
||||||
|
Formatter.setSeparator(Separator.COMMA)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `add when empty`() {
|
||||||
|
// Add one symbol
|
||||||
|
textFieldController.addToInput("1")
|
||||||
|
assertEquals("1", textFieldController.text)
|
||||||
|
assertEquals(1..1, textFieldController.selection)
|
||||||
|
textFieldController.clearInput()
|
||||||
|
|
||||||
|
// Add multiple
|
||||||
|
textFieldController.addToInput("123")
|
||||||
|
assertEquals("123", textFieldController.text)
|
||||||
|
assertEquals(3..3, textFieldController.selection)
|
||||||
|
textFieldController.clearInput()
|
||||||
|
|
||||||
|
// Add multiple
|
||||||
|
textFieldController.addToInput("1234")
|
||||||
|
assertEquals("1,234", textFieldController.text)
|
||||||
|
assertEquals(5..5, textFieldController.selection)
|
||||||
|
textFieldController.clearInput()
|
||||||
|
|
||||||
|
// Add multiple
|
||||||
|
textFieldController.addToInput("123456.789")
|
||||||
|
assertEquals("123,456.789", textFieldController.text)
|
||||||
|
assertEquals(11..11, textFieldController.selection)
|
||||||
|
textFieldController.clearInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Add when not empty one symbol at a time (check formatting)`() {
|
||||||
|
// Should be 1|
|
||||||
|
textFieldController.addToInput("1")
|
||||||
|
assertEquals("1", textFieldController.text)
|
||||||
|
assertEquals(1..1, textFieldController.selection)
|
||||||
|
|
||||||
|
// Should be 12|
|
||||||
|
textFieldController.addToInput("2")
|
||||||
|
assertEquals("12", textFieldController.text)
|
||||||
|
assertEquals(2..2, textFieldController.selection)
|
||||||
|
|
||||||
|
// Should be 123|
|
||||||
|
textFieldController.addToInput("3")
|
||||||
|
assertEquals("123", textFieldController.text)
|
||||||
|
assertEquals(3..3, textFieldController.selection)
|
||||||
|
|
||||||
|
// Should be 1,234|
|
||||||
|
textFieldController.addToInput("4")
|
||||||
|
assertEquals("1,234", textFieldController.text)
|
||||||
|
assertEquals(5..5, textFieldController.selection)
|
||||||
|
|
||||||
|
// Should be 12,345|
|
||||||
|
textFieldController.addToInput("5")
|
||||||
|
assertEquals("12,345", textFieldController.text)
|
||||||
|
assertEquals(6..6, textFieldController.selection)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Delete on empty input`() {
|
||||||
|
// Delete on empty input
|
||||||
|
textFieldController.delete()
|
||||||
|
assertEquals("", textFieldController.text)
|
||||||
|
assertEquals(0..0, textFieldController.selection)
|
||||||
|
textFieldController.clearInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Delete last remaining symbol`() {
|
||||||
|
textFieldController.addToInput("1")
|
||||||
|
textFieldController.delete()
|
||||||
|
assertEquals("", textFieldController.text)
|
||||||
|
assertEquals(0..0, textFieldController.selection)
|
||||||
|
textFieldController.clearInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Delete by one symbol (check formatting)`() {
|
||||||
|
textFieldController.addToInput("123456")
|
||||||
|
// Input is formatted into 123,456
|
||||||
|
textFieldController.delete()
|
||||||
|
assertEquals("12,345", textFieldController.text)
|
||||||
|
assertEquals(6..6, textFieldController.selection)
|
||||||
|
textFieldController.delete()
|
||||||
|
assertEquals("1,234", textFieldController.text)
|
||||||
|
assertEquals(5..5, textFieldController.selection)
|
||||||
|
textFieldController.delete()
|
||||||
|
assertEquals("123", textFieldController.text)
|
||||||
|
println("in 123: ${textFieldController.selection}")
|
||||||
|
assertEquals(3..3, textFieldController.selection)
|
||||||
|
textFieldController.clearInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Delete multiple symbols, selected before separator`() {
|
||||||
|
textFieldController.addToInput("123789456")
|
||||||
|
// Input is formatted to 123,789,456
|
||||||
|
textFieldController.moveCursor(3..7)
|
||||||
|
textFieldController.delete()
|
||||||
|
assertEquals("123,456", textFieldController.text)
|
||||||
|
assertEquals(3..3, textFieldController.selection)
|
||||||
|
textFieldController.clearInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Delete multiple symbols, selected not near separator`() {
|
||||||
|
textFieldController.addToInput("123789456")
|
||||||
|
// Input is formatted to 123,789,456
|
||||||
|
textFieldController.moveCursor(3..9)
|
||||||
|
textFieldController.delete()
|
||||||
|
assertEquals("12,356", textFieldController.text)
|
||||||
|
assertEquals(4..4, textFieldController.selection)
|
||||||
|
textFieldController.clearInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Delete multiple symbols in weird input`() {
|
||||||
|
textFieldController.addToInput("123...789456")
|
||||||
|
// Input is formatted to 123...789456
|
||||||
|
textFieldController.moveCursor(3..9)
|
||||||
|
textFieldController.delete()
|
||||||
|
assertEquals(4..4, textFieldController.selection)
|
||||||
|
assertEquals("123,456", textFieldController.text)
|
||||||
|
textFieldController.clearInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `placed cursor illegally`() {
|
||||||
|
textFieldController.addToInput("123456.789")
|
||||||
|
// Input is 123,456.789
|
||||||
|
textFieldController.moveCursor(4..4)
|
||||||
|
// Cursor should be placed like this 123|,456.789
|
||||||
|
assertEquals(3..3, textFieldController.selection)
|
||||||
|
textFieldController.clearInput()
|
||||||
|
|
||||||
|
textFieldController.addToInput("123456.789+cos(")
|
||||||
|
// Input is 123,456.789+cos(
|
||||||
|
textFieldController.moveCursor(13..13)
|
||||||
|
// Cursor should be placed like this 123,456.789+c|os(
|
||||||
|
assertEquals(12..12, textFieldController.selection)
|
||||||
|
textFieldController.clearInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `get clear input text without formatting`() {
|
||||||
|
textFieldController.addToInput("123456.789+cos(..)")
|
||||||
|
// Input is 123,456.789
|
||||||
|
|
||||||
|
assertEquals("123456.789+cos(..)", textFieldController.inputTextWithoutFormatting())
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user