mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-19 00:35:26 +02:00
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:
parent
193170c734
commit
aff3cd4ca3
@ -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")
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
@ -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 }
|
||||
|
@ -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) }
|
||||
|
@ -85,7 +85,7 @@ fun TopScreenPart(
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
MyTextField(
|
||||
Modifier.fillMaxWidth(),
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user