Back to ExprK.

Added more operators for calculator. Formatting and other stuff are broken.

P.S. Using my own fork of ExprK. Added square root operator to it.
This commit is contained in:
Sad Ellie 2022-11-21 17:53:43 +04:00
parent 193170c734
commit aff3cd4ca3
6 changed files with 577 additions and 83 deletions

View File

@ -12,6 +12,8 @@ plugins {
// Firebase Crashlytics
id("com.google.firebase.crashlytics")
// id("io.freefair.lombok") version "6.6"
}
val composeVersion = "1.4.0-alpha02"
@ -151,6 +153,8 @@ dependencies {
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test:runner:1.5.1")
androidTestImplementation("androidx.test:rules:1.5.0")
testImplementation("org.robolectric:robolectric:4.9")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
// Compose and navigation
implementation("androidx.compose.ui:ui:$composeVersion")
@ -200,6 +204,6 @@ dependencies {
// ComposeReorderable
implementation("org.burnoutcrew.composereorderable:reorderable:0.9.6")
// EvalEx
implementation("com.ezylang:EvalEx:3.0.1")
// ExprK
implementation("com.github.sadellie:ExprK:e55cba8f41")
}

View File

@ -44,12 +44,26 @@ const val KEY_DIVIDE_DISPLAY = "÷"
const val KEY_MULTIPLY = "*"
const val KEY_MULTIPLY_DISPLAY = "×"
const val KEY_LEFT_BRACKET = "("
const val KEY_RIGHT_BRACKET = ")"
const val KEY_EXPONENT = "^"
const val KEY_EXPONENT_DISPLAY = "^"
const val KEY_SQRT = ""
val OPERATORS = listOf(
KEY_PLUS,
KEY_MINUS,
KEY_MINUS_DISPLAY,
KEY_MULTIPLY,
KEY_MULTIPLY_DISPLAY,
KEY_DIVIDE,
KEY_DIVIDE_DISPLAY,
KEY_SQRT,
KEY_EXPONENT,
)
val INTERNAL_DISPLAY: Map<String, String> = hashMapOf(
KEY_MINUS to KEY_MINUS_DISPLAY,
KEY_MULTIPLY to KEY_MULTIPLY_DISPLAY,
KEY_DIVIDE to KEY_DIVIDE_DISPLAY,
KEY_EXPONENT to KEY_EXPONENT_DISPLAY,
)

View File

@ -24,9 +24,10 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.ezylang.evalex.BaseException
import com.ezylang.evalex.Expression
import com.github.keelar.exprk.ExpressionException
import com.github.keelar.exprk.Expressions
import com.sadellie.unitto.FirebaseHelper
import com.sadellie.unitto.data.INTERNAL_DISPLAY
import com.sadellie.unitto.data.KEY_0
import com.sadellie.unitto.data.KEY_1
import com.sadellie.unitto.data.KEY_2
@ -39,9 +40,12 @@ import com.sadellie.unitto.data.KEY_8
import com.sadellie.unitto.data.KEY_9
import com.sadellie.unitto.data.KEY_DIVIDE
import com.sadellie.unitto.data.KEY_DOT
import com.sadellie.unitto.data.KEY_LEFT_BRACKET
import com.sadellie.unitto.data.KEY_MINUS
import com.sadellie.unitto.data.KEY_MULTIPLY
import com.sadellie.unitto.data.KEY_PLUS
import com.sadellie.unitto.data.KEY_RIGHT_BRACKET
import com.sadellie.unitto.data.KEY_SQRT
import com.sadellie.unitto.data.OPERATORS
import com.sadellie.unitto.data.preferences.UserPreferences
import com.sadellie.unitto.data.preferences.UserPreferencesRepository
@ -70,11 +74,13 @@ import javax.inject.Inject
class MainViewModel @Inject constructor(
private val userPrefsRepository: UserPreferencesRepository,
private val basedUnitRepository: MyBasedUnitsRepository,
private val application: Application,
private val mContext: Application,
private val allUnitsRepository: AllUnitsRepository
) : ViewModel() {
private val _inputValue: MutableStateFlow<String> = MutableStateFlow(KEY_0)
val inputValue: MutableStateFlow<String> = MutableStateFlow(KEY_0)
private val latestInputStack: MutableList<String> = mutableListOf(KEY_0)
private val _inputDisplayValue: MutableStateFlow<String> = MutableStateFlow(KEY_0)
private val _deleteButtonEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false)
private val _negateButtonEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false)
private val _isLoadingDatabase: MutableStateFlow<Boolean> = MutableStateFlow(true)
@ -88,15 +94,13 @@ class MainViewModel @Inject constructor(
)
val mainFlow = combine(
_inputValue, _isLoadingDatabase, _isLoadingNetwork, _showError, _userPrefs
) { inputValue, isLoadingDatabase, isLoadingNetwork, showError, _ ->
_inputDisplayValue, _isLoadingDatabase, _isLoadingNetwork, _showError, _userPrefs
) { inputDisplayValue, isLoadingDatabase, isLoadingNetwork, showError, _ ->
return@combine MainScreenUIState(
inputValue = inputValue,
inputValue = inputDisplayValue,
resultValue = convertValue(),
deleteButtonEnabled = _deleteButtonEnabled.value,
dotButtonEnabled = !_inputValue.value.takeLastWhile {
it.toString() !in OPERATORS.minus(KEY_DOT)
}.contains(KEY_DOT),
deleteButtonEnabled = inputValue.value != KEY_0,
dotButtonEnabled = canEnterDot(),
negateButtonEnabled = _negateButtonEnabled.value,
isLoadingDatabase = isLoadingDatabase,
isLoadingNetwork = isLoadingNetwork,
@ -122,31 +126,37 @@ class MainViewModel @Inject constructor(
*/
private fun convertValue(): String {
val cleanInput = _inputValue.value.dropLastWhile { !it.isDigit() }
var cleanInput = inputValue.value.dropLastWhile { !it.isDigit() }
// AUTOCLOSE ALL BRACKETS
val leftBrackets = inputValue.value.count { it.toString() == KEY_LEFT_BRACKET }
val rightBrackets = inputValue.value.count { it.toString() == KEY_RIGHT_BRACKET }
val neededBrackets = leftBrackets - rightBrackets
if (neededBrackets > 0) {
cleanInput += KEY_RIGHT_BRACKET.repeat(neededBrackets)
}
// Kotlin doesn't have a multi catch
val calculatedInput = try {
val evaluated = Expression(cleanInput)
val evaluated = Expressions()
// Optimal precision, not too low, not too high. Balanced for performance and UX.
.evaluate()
.numberValue
.eval(cleanInput)
.setScale(_userPrefs.value.digitsPrecision, RoundingMode.HALF_EVEN)
.stripTrailingZeros()
if (evaluated.compareTo(BigDecimal.ZERO) == 0) BigDecimal.ZERO else evaluated
} catch (e: Exception) {
// Kotlin doesn't have a multi catch
when (e) {
is BaseException, is ArrayIndexOutOfBoundsException, is NumberFormatException, is ArithmeticException -> return mainFlow.value.resultValue
is ExpressionException, is ArrayIndexOutOfBoundsException, is IndexOutOfBoundsException, is NumberFormatException, is ArithmeticException -> return mainFlow.value.resultValue
else -> throw e
}
}
// Ugly way of determining when to hide calculated value
_calculatedValue.value = if (calculatedInput.toPlainString() != cleanInput) {
// Expression is not same as the result, show result
_calculatedValue.value = if (!latestInputStack.none { it in OPERATORS }) {
calculatedInput.toStringWith(_userPrefs.value.outputFormat)
} else {
// Expression is same as the result, don't show result, the input text is enough
null
}
@ -182,7 +192,7 @@ class MainViewModel @Inject constructor(
_negateButtonEnabled.update { clickedUnit.group.canNegate }
// Now we change to positive if the group we switched to supports negate
if (!clickedUnit.group.canNegate) {
_inputValue.update { _inputValue.value.removePrefix(KEY_MINUS) }
inputValue.update { inputValue.value.removePrefix(KEY_MINUS) }
}
// Now setting up right unit (pair for the left one)
@ -293,90 +303,146 @@ class MainViewModel @Inject constructor(
* @param[symbolToAdd] Digit/Symbol we want to add, can be any digit 0..9 or a dot symbol
*/
fun processInput(symbolToAdd: String) {
val lastSymbol = _inputValue.value.last().toString()
val lastSecondSymbol = _inputValue.value.takeLast(2).dropLast(1)
val lastTwoSymbols = latestInputStack.takeLast(2)
val lastSymbol: String = lastTwoSymbols.getOrNull(1) ?: lastTwoSymbols[0]
val lastSecondSymbol: String? = lastTwoSymbols.getOrNull(0)
_deleteButtonEnabled.update { true }
when (symbolToAdd) {
KEY_0 -> {
// Don't add zero if the input is already a zero
if (_inputValue.value == KEY_0) return
// Don't add zero if there is a zero and operator in front
if ((lastSecondSymbol in OPERATORS) and (lastSymbol == KEY_0)) return
_inputValue.update { _inputValue.value + symbolToAdd }
}
KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9 -> {
// Replace single zero (default input) if it's here
if (_inputValue.value == KEY_0) {
_inputValue.update { symbolToAdd }
} else {
_inputValue.update { _inputValue.value + symbolToAdd }
}
}
KEY_DOT -> {
_inputValue.update { _inputValue.value + symbolToAdd }
}
KEY_MINUS -> {
when {
// Replace single zero with minus (to support negative numbers)
(_inputValue.value == KEY_0) -> {
if (symbolToAdd == KEY_MINUS) _inputValue.update { symbolToAdd }
}
// Don't allow multiple minuses near each other
(lastSymbol == KEY_MINUS) -> {}
// Don't allow plus and minus be near each other
(lastSymbol == KEY_PLUS) -> {
_inputValue.update { _inputValue.value.dropLast(1) + symbolToAdd }
}
else -> {
_inputValue.update { _inputValue.value + symbolToAdd }
}
}
}
KEY_PLUS, KEY_DIVIDE, KEY_MULTIPLY -> {
when {
// Don't need expressions that start with zero
(_inputValue.value == KEY_0) or (_inputValue.value == KEY_MINUS) -> {}
(inputValue.value == KEY_0) or (inputValue.value == KEY_MINUS) -> {}
/**
* For situations like "50+-", when user clicks "/" we delete "-" so it becomes
* "50+". We don't add "/' here. User will click "/" second time and the input
* will be "50/".
*/
(lastSecondSymbol in OPERATORS) and (lastSymbol == KEY_MINUS) -> {
(lastSecondSymbol in OPERATORS) and (lastSymbol == KEY_MINUS)-> {
deleteDigit()
}
// Don't allow multiple operators near each other
(lastSymbol in OPERATORS) -> {
_inputValue.update { _inputValue.value.dropLast(1) + symbolToAdd }
deleteDigit()
setInputSymbols(symbolToAdd)
}
else -> {
_inputValue.update { _inputValue.value + symbolToAdd }
setInputSymbols(symbolToAdd)
}
}
}
KEY_0 -> {
// Don't add zero if the input is already a zero
if (inputValue.value == KEY_0) return
// Prevents things like "-00" and "4+000"
if ((lastSecondSymbol in OPERATORS) and (lastSymbol == KEY_0)) return
setInputSymbols(symbolToAdd)
}
KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9 -> {
// Replace single zero (default input) if it's here
if (inputValue.value == KEY_0) {
setInputSymbols(symbolToAdd, false)
} else {
setInputSymbols(symbolToAdd)
}
}
KEY_MINUS -> {
when {
// Replace single zero with minus (to support negative numbers)
(inputValue.value == KEY_0) -> {
setInputSymbols(symbolToAdd, false)
}
// Don't allow multiple minuses near each other
(lastSymbol.compareTo(KEY_MINUS) == 0) -> {}
// Don't allow plus and minus be near each other
(lastSymbol == KEY_PLUS) -> {
deleteDigit()
setInputSymbols(symbolToAdd)
}
else -> {
setInputSymbols(symbolToAdd)
}
}
}
KEY_DOT -> {
setInputSymbols(symbolToAdd)
}
KEY_LEFT_BRACKET, KEY_RIGHT_BRACKET -> {
when {
// Replace single zero with minus (to support negative numbers)
(inputValue.value == KEY_0) -> {
setInputSymbols(symbolToAdd, false)
}
else -> {
setInputSymbols(symbolToAdd)
}
}
}
KEY_SQRT -> {
when {
// Replace single zero with minus (to support negative numbers)
(inputValue.value == KEY_0) -> {
setInputSymbols(symbolToAdd, false)
}
else -> {
setInputSymbols(symbolToAdd)
}
}
}
else -> {
when {
// Replace single zero with minus (to support negative numbers)
(inputValue.value == KEY_0) -> {
setInputSymbols(symbolToAdd, false)
}
else -> {
setInputSymbols(symbolToAdd)
}
}
}
}
}
/**
* Deletes last symbol from input and handles buttons state (enabled/disabled)
*/
fun deleteDigit() {
// Deleting last symbol
var inputToSet = _inputValue.value.dropLast(1)
val lastSymbol = latestInputStack.removeLast()
/*
Now we check what we have left
We deleted last symbol and we got Empty string, just minus symbol, or zero
Do not allow deleting anything beyond this (disable button)
Set input to default (zero)
Skipping this block means that we are left we acceptable value, i.e. 123.03
*/
if (inputToSet in listOf(String(), KEY_MINUS, KEY_0)) {
_deleteButtonEnabled.update { false }
inputToSet = KEY_0
// We will need to delete last symbol from both values
val displayRepresentation: String = INTERNAL_DISPLAY[lastSymbol] ?: lastSymbol
// If this value are same, it means that after deleting there will be no symbols left, set to default
if (lastSymbol == inputValue.value) {
setInputSymbols(KEY_0, false)
} else {
inputValue.update { it.removeSuffix(lastSymbol) }
_inputDisplayValue.update { it.removeSuffix(displayRepresentation) }
}
}
_inputValue.update { inputToSet }
/**
* Adds given [symbol] to [inputValue] and [_inputDisplayValue] and updates [latestInputStack].
*
* By default add symbol, but if [add] is False, will replace current input (when replacing
* default [KEY_0] input).
*/
private fun setInputSymbols(symbol: String, add: Boolean = true) {
val displaySymbol: String = INTERNAL_DISPLAY[symbol] ?: symbol
when {
add -> {
inputValue.update { it + symbol }
_inputDisplayValue.update { it + displaySymbol }
latestInputStack.add(symbol)
}
else -> {
inputValue.update { symbol }
_inputDisplayValue.update { displaySymbol }
latestInputStack.add(symbol)
}
}
}
/**
@ -384,7 +450,7 @@ class MainViewModel @Inject constructor(
*/
fun clearInput() {
_deleteButtonEnabled.update { false }
_inputValue.update { KEY_0 }
setInputSymbols(KEY_0, false)
}
/**
@ -392,6 +458,13 @@ class MainViewModel @Inject constructor(
*/
fun inputValue() = (mainFlow.value.calculatedValue ?: mainFlow.value.inputValue).toBigDecimal()
/**
* Returns True if can be placed.
*/
private fun canEnterDot(): Boolean = !inputValue.value.takeLastWhile {
it.toString() !in OPERATORS.minus(KEY_DOT)
}.contains(KEY_DOT)
/**
* Saves latest pair of units into datastore
*/
@ -418,7 +491,7 @@ class MainViewModel @Inject constructor(
// Now we load units data from database
val allBasedUnits = basedUnitRepository.getAll()
allUnitsRepository.loadFromDatabase(application, allBasedUnits)
allUnitsRepository.loadFromDatabase(mContext, allBasedUnits)
// User is free to convert values and units on units screen can be sorted properly
_negateButtonEnabled.update { unitFrom.group.canNegate }

View File

@ -39,11 +39,16 @@ import com.sadellie.unitto.data.KEY_CLEAR
import com.sadellie.unitto.data.KEY_DIVIDE
import com.sadellie.unitto.data.KEY_DIVIDE_DISPLAY
import com.sadellie.unitto.data.KEY_DOT
import com.sadellie.unitto.data.KEY_EXPONENT
import com.sadellie.unitto.data.KEY_EXPONENT_DISPLAY
import com.sadellie.unitto.data.KEY_LEFT_BRACKET
import com.sadellie.unitto.data.KEY_MINUS
import com.sadellie.unitto.data.KEY_MINUS_DISPLAY
import com.sadellie.unitto.data.KEY_MULTIPLY
import com.sadellie.unitto.data.KEY_MULTIPLY_DISPLAY
import com.sadellie.unitto.data.KEY_PLUS
import com.sadellie.unitto.data.KEY_RIGHT_BRACKET
import com.sadellie.unitto.data.KEY_SQRT
import com.sadellie.unitto.screens.Formatter
/**
@ -76,24 +81,28 @@ fun Keyboard(
// Column modifier
val cModifier = Modifier.weight(1f)
Column(cModifier) {
KeyboardButton(bModifier, KEY_LEFT_BRACKET, isPrimary = false, onClick = addDigit)
KeyboardButton(bModifier, KEY_7, onClick = addDigit)
KeyboardButton(bModifier, KEY_4, onClick = addDigit)
KeyboardButton(bModifier, KEY_1, onClick = addDigit)
KeyboardButton(bModifier, KEY_0, onClick = addDigit)
}
Column(cModifier) {
KeyboardButton(bModifier, KEY_RIGHT_BRACKET, isPrimary = false, onClick = addDigit)
KeyboardButton(bModifier, KEY_8, onClick = addDigit)
KeyboardButton(bModifier, KEY_5, onClick = addDigit)
KeyboardButton(bModifier, KEY_2, onClick = addDigit)
KeyboardButton(bModifier, Formatter.fractional, enabled = dotButtonEnabled) { addDigit(KEY_DOT) }
}
Column(cModifier) {
KeyboardButton(bModifier, KEY_EXPONENT_DISPLAY, isPrimary = false, onClick = { addDigit(KEY_EXPONENT) })
KeyboardButton(bModifier, KEY_9, onClick = addDigit)
KeyboardButton(bModifier, KEY_6, onClick = addDigit)
KeyboardButton(bModifier, KEY_3, onClick = addDigit)
KeyboardButton(bModifier, KEY_CLEAR, enabled = deleteButtonEnabled, onLongClick = clearInput) { deleteDigit() }
}
Column(cModifier) {
KeyboardButton(bModifier, KEY_SQRT, isPrimary = false, onClick = { addDigit(KEY_SQRT) })
KeyboardButton(bModifier, KEY_DIVIDE_DISPLAY, isPrimary = false) { addDigit(KEY_DIVIDE) }
KeyboardButton(bModifier, KEY_MULTIPLY_DISPLAY, isPrimary = false) { addDigit(KEY_MULTIPLY) }
KeyboardButton(bModifier, KEY_MINUS_DISPLAY, isPrimary = false) { addDigit(KEY_MINUS) }

View File

@ -85,7 +85,7 @@ fun TopScreenPart(
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(24.dp)
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
MyTextField(
Modifier.fillMaxWidth(),

View File

@ -0,0 +1,394 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2022-2022 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.screens
import android.os.StrictMode
import androidx.room.Room
import com.sadellie.unitto.data.KEY_0
import com.sadellie.unitto.data.KEY_1
import com.sadellie.unitto.data.KEY_2
import com.sadellie.unitto.data.KEY_3
import com.sadellie.unitto.data.KEY_4
import com.sadellie.unitto.data.KEY_5
import com.sadellie.unitto.data.KEY_6
import com.sadellie.unitto.data.KEY_7
import com.sadellie.unitto.data.KEY_8
import com.sadellie.unitto.data.KEY_9
import com.sadellie.unitto.data.KEY_COMMA
import com.sadellie.unitto.data.KEY_DIVIDE
import com.sadellie.unitto.data.KEY_DOT
import com.sadellie.unitto.data.KEY_EXPONENT
import com.sadellie.unitto.data.KEY_LEFT_BRACKET
import com.sadellie.unitto.data.KEY_MINUS
import com.sadellie.unitto.data.KEY_MULTIPLY
import com.sadellie.unitto.data.KEY_PLUS
import com.sadellie.unitto.data.KEY_RIGHT_BRACKET
import com.sadellie.unitto.data.KEY_SQRT
import com.sadellie.unitto.data.preferences.DataStoreModule
import com.sadellie.unitto.data.preferences.UserPreferencesRepository
import com.sadellie.unitto.data.units.AllUnitsRepository
import com.sadellie.unitto.data.units.database.MyBasedUnitDatabase
import com.sadellie.unitto.data.units.database.MyBasedUnitsRepository
import com.sadellie.unitto.screens.main.MainViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
@ExperimentalCoroutinesApi
class CoroutineTestRule(private val dispatcher: TestDispatcher = UnconfinedTestDispatcher()) :
TestWatcher() {
override fun starting(description: Description) {
super.starting(description)
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description) {
super.finished(description)
Dispatchers.resetMain()
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Config(manifest = Config.NONE)
@RunWith(RobolectricTestRunner::class)
class MainViewModelTest {
@ExperimentalCoroutinesApi
@get:Rule
val coroutineTestRule = CoroutineTestRule()
private lateinit var viewModel: MainViewModel
private val allUnitsRepository = AllUnitsRepository()
@Before
fun setUp() {
viewModel = MainViewModel(
UserPreferencesRepository(
DataStoreModule().provideUserPreferencesDataStore(
RuntimeEnvironment.getApplication()
)
), MyBasedUnitsRepository(
Room.inMemoryDatabaseBuilder(
RuntimeEnvironment.getApplication(), MyBasedUnitDatabase::class.java
).build().myBasedUnitDao()
), RuntimeEnvironment.getApplication(), allUnitsRepository
)
}
@Test
fun processInputTest() = runTest {
// https://github.com/robolectric/robolectric/issues/6377#issuecomment-843779001
StrictMode.enableDefaults()
val testDispatcher = UnconfinedTestDispatcher(testScheduler)
Dispatchers.setMain(testDispatcher)
viewModel.mainFlow.launchIn(CoroutineScope(testDispatcher))
/**
* For simplicity comments will have structure:
* currentInput | userInput | processed internal input | processed display output
*/
`test 0`()
`test digits from 1 to 9`()
`test plus, divide and multiply operators`()
`test dot`()
`test minus`()
`test brackets`()
Dispatchers.resetMain()
}
private fun `test 0`() {
// 0 | 000 | 0 | 0
viewModel.processInput(KEY_0)
viewModel.processInput(KEY_0)
viewModel.processInput(KEY_0)
assertEquals("0", viewModel.inputValue.value)
assertEquals("0", viewModel.mainFlow.value.inputValue)
viewModel.clearInput()
// 123 | 000 | 123000
viewModel.processInput(KEY_1)
viewModel.processInput(KEY_2)
viewModel.processInput(KEY_3)
viewModel.processInput(KEY_0)
viewModel.processInput(KEY_0)
viewModel.processInput(KEY_0)
assertEquals("123000", viewModel.inputValue.value)
assertEquals("123000", viewModel.mainFlow.value.inputValue)
viewModel.clearInput()
// 123. | 000 | 123.000
viewModel.processInput(KEY_1)
viewModel.processInput(KEY_2)
viewModel.processInput(KEY_3)
viewModel.processInput(KEY_DOT)
viewModel.processInput(KEY_0)
viewModel.processInput(KEY_0)
viewModel.processInput(KEY_0)
assertEquals("123.000", viewModel.inputValue.value)
assertEquals("123.000", viewModel.mainFlow.value.inputValue)
viewModel.clearInput()
// - | 000 | -0
viewModel.processInput(KEY_MINUS)
viewModel.processInput(KEY_0)
viewModel.processInput(KEY_0)
viewModel.processInput(KEY_0)
assertEquals("-0", viewModel.inputValue.value)
assertEquals("0", viewModel.mainFlow.value.inputValue)
viewModel.clearInput()
// 12+ | 000 | 12+0
viewModel.processInput(KEY_1)
viewModel.processInput(KEY_2)
viewModel.processInput(KEY_PLUS)
viewModel.processInput(KEY_0)
viewModel.processInput(KEY_0)
viewModel.processInput(KEY_0)
assertEquals("12+0", viewModel.inputValue.value)
assertEquals("12+0", viewModel.mainFlow.value.inputValue)
viewModel.clearInput()
// √0 | 000 | √0
viewModel.processInput(KEY_SQRT)
viewModel.processInput(KEY_0)
viewModel.processInput(KEY_0)
viewModel.processInput(KEY_0)
assertEquals("√0", viewModel.inputValue.value)
assertEquals("√0", viewModel.mainFlow.value.inputValue)
viewModel.clearInput()
}
private fun `test digits from 1 to 9`() {
// 0 | 123456789 | 123456789
viewModel.processInput(KEY_1)
viewModel.processInput(KEY_2)
viewModel.processInput(KEY_3)
viewModel.processInput(KEY_4)
viewModel.processInput(KEY_5)
viewModel.processInput(KEY_6)
viewModel.processInput(KEY_7)
viewModel.processInput(KEY_8)
viewModel.processInput(KEY_9)
assertEquals("123456789", viewModel.inputValue.value)
assertEquals("123456789", viewModel.mainFlow.value.inputValue)
viewModel.clearInput()
}
private fun `test plus, divide and multiply operators`() {
// 0 | +++ | 0
viewModel.processInput(KEY_PLUS)
assertEquals("0", viewModel.inputValue.value)
assertEquals("0", viewModel.mainFlow.value.inputValue)
viewModel.clearInput()
// 123 | +++ | 123+
viewModel.processInput(KEY_1)
viewModel.processInput(KEY_2)
viewModel.processInput(KEY_3)
viewModel.processInput(KEY_PLUS)
viewModel.processInput(KEY_PLUS)
viewModel.processInput(KEY_PLUS)
assertEquals("123+", viewModel.inputValue.value)
assertEquals("123+", viewModel.mainFlow.value.inputValue)
viewModel.clearInput()
// 1- | *** | 1*
viewModel.processInput(KEY_1)
viewModel.processInput(KEY_MINUS)
viewModel.processInput(KEY_MULTIPLY)
viewModel.processInput(KEY_MULTIPLY)
viewModel.processInput(KEY_MULTIPLY)
assertEquals("1*", viewModel.inputValue.value)
assertEquals("1×", viewModel.mainFlow.value.inputValue)
viewModel.clearInput()
// 1/- | +++ | 1+
viewModel.processInput(KEY_1)
viewModel.processInput(KEY_DIVIDE)
viewModel.processInput(KEY_MINUS)
viewModel.processInput(KEY_PLUS)
viewModel.processInput(KEY_PLUS)
viewModel.processInput(KEY_PLUS)
assertEquals("1+", viewModel.inputValue.value)
assertEquals("1+", viewModel.mainFlow.value.inputValue)
viewModel.clearInput()
}
private fun `test dot`() {
// 0 | . | 0.
viewModel.processInput(KEY_DOT)
assertEquals("0.", viewModel.inputValue.value)
assertEquals("0.", viewModel.mainFlow.value.inputValue)
viewModel.clearInput()
// 1 | . | 1.
viewModel.processInput(KEY_1)
viewModel.processInput(KEY_DOT)
assertEquals("1.", viewModel.inputValue.value)
assertEquals("1.", viewModel.mainFlow.value.inputValue)
viewModel.clearInput()
// 1+ | . | 1+.
viewModel.processInput(KEY_1)
viewModel.processInput(KEY_PLUS)
viewModel.processInput(KEY_DOT)
assertEquals("1+.", viewModel.inputValue.value)
assertEquals("1+.", viewModel.mainFlow.value.inputValue)
viewModel.clearInput()
// √ | . | √.
viewModel.processInput(KEY_SQRT)
viewModel.processInput(KEY_DOT)
assertEquals("√.", viewModel.inputValue.value)
assertEquals("√.", viewModel.mainFlow.value.inputValue)
viewModel.clearInput()
}
private fun `test minus`() {
// 0 | --- | -
viewModel.processInput(KEY_MINUS)
viewModel.processInput(KEY_MINUS)
viewModel.processInput(KEY_MINUS)
assertEquals("-", viewModel.inputValue.value)
assertEquals("", viewModel.mainFlow.value.inputValue)
viewModel.clearInput()
// 12 | --- | 12-
viewModel.processInput(KEY_1)
viewModel.processInput(KEY_2)
viewModel.processInput(KEY_MINUS)
viewModel.processInput(KEY_MINUS)
viewModel.processInput(KEY_MINUS)
assertEquals("12-", viewModel.inputValue.value)
assertEquals("12", viewModel.mainFlow.value.inputValue)
viewModel.clearInput()
// 12+ | --- | 12-
viewModel.processInput(KEY_1)
viewModel.processInput(KEY_2)
viewModel.processInput(KEY_PLUS)
viewModel.processInput(KEY_MINUS)
viewModel.processInput(KEY_MINUS)
viewModel.processInput(KEY_MINUS)
assertEquals("12-", viewModel.inputValue.value)
assertEquals("12", viewModel.mainFlow.value.inputValue)
viewModel.clearInput()
// 12/ | --- | 12/-
viewModel.processInput(KEY_1)
viewModel.processInput(KEY_2)
viewModel.processInput(KEY_DIVIDE)
viewModel.processInput(KEY_MINUS)
viewModel.processInput(KEY_MINUS)
viewModel.processInput(KEY_MINUS)
assertEquals("12/-", viewModel.inputValue.value)
assertEquals("12÷", viewModel.mainFlow.value.inputValue)
viewModel.clearInput()
// √ | --- | √-
viewModel.processInput(KEY_SQRT)
viewModel.processInput(KEY_MINUS)
viewModel.processInput(KEY_MINUS)
viewModel.processInput(KEY_MINUS)
assertEquals("√-", viewModel.inputValue.value)
assertEquals("√–", viewModel.mainFlow.value.inputValue)
viewModel.clearInput()
}
private fun `test brackets`() {
// 0 | ( | (
viewModel.processInput(KEY_LEFT_BRACKET)
assertEquals("(", viewModel.inputValue.value)
assertEquals("(", viewModel.mainFlow.value.inputValue)
viewModel.clearInput()
// 0 | ((( | (((
viewModel.processInput(KEY_LEFT_BRACKET)
viewModel.processInput(KEY_LEFT_BRACKET)
viewModel.processInput(KEY_LEFT_BRACKET)
assertEquals("(((", viewModel.inputValue.value)
assertEquals("(((", viewModel.mainFlow.value.inputValue)
viewModel.clearInput()
// √ | (10+2) | √(10+2)
viewModel.processInput(KEY_SQRT)
viewModel.processInput(KEY_LEFT_BRACKET)
viewModel.processInput(KEY_1)
viewModel.processInput(KEY_0)
viewModel.processInput(KEY_PLUS)
viewModel.processInput(KEY_2)
viewModel.processInput(KEY_RIGHT_BRACKET)
assertEquals("√(10+2)", viewModel.inputValue.value)
assertEquals("√(10+2)", viewModel.mainFlow.value.inputValue)
}
@Test
fun deleteSymbolTest() = runTest {
// https://github.com/robolectric/robolectric/issues/6377#issuecomment-843779001
StrictMode.enableDefaults()
val testDispatcher = UnconfinedTestDispatcher(testScheduler)
Dispatchers.setMain(testDispatcher)
viewModel.mainFlow.launchIn(CoroutineScope(testDispatcher))
listOf(
KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_0,
KEY_DOT, KEY_COMMA, KEY_LEFT_BRACKET, KEY_RIGHT_BRACKET,
KEY_PLUS, KEY_MINUS, KEY_DIVIDE, KEY_MULTIPLY, KEY_EXPONENT, KEY_SQRT,
).forEach {
// We enter one symbol and delete it, should be default as a result
viewModel.processInput(it)
viewModel.deleteDigit()
assertEquals("0", viewModel.mainFlow.value.inputValue)
assertEquals("0", viewModel.inputValue.value)
}
viewModel.clearInput()
// Now we check that we can delete multiple values
viewModel.processInput(KEY_3)
viewModel.processInput(KEY_SQRT)
viewModel.processInput(KEY_9)
viewModel.deleteDigit()
assertEquals("3√", viewModel.inputValue.value)
assertEquals("3√", viewModel.mainFlow.value.inputValue)
Dispatchers.resetMain()
}
}