More robust "=" logic

This commit is contained in:
Sad Ellie 2023-11-08 23:32:59 +03:00
parent 1537df88eb
commit 689c812c11
6 changed files with 104 additions and 69 deletions

View File

@ -20,6 +20,7 @@ package com.sadellie.unitto.core.ui.common.textfield
import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.SavedStateHandle
import com.sadellie.unitto.core.base.Token import com.sadellie.unitto.core.base.Token
fun TextFieldValue.addTokens(tokens: String): TextFieldValue { fun TextFieldValue.addTokens(tokens: String): TextFieldValue {
@ -131,6 +132,18 @@ fun TextFieldValue.deleteTokens(): TextFieldValue {
) )
} }
/**
* Tries to get a [TextFieldValue]. Places cursor at the end.
*
* @receiver [SavedStateHandle] Where to look for.
* @param key Key to find
* @return [TextFieldValue] with cursor at the end.
*/
fun SavedStateHandle.getTextField(key: String): TextFieldValue =
with(get(key) ?: "") {
TextFieldValue(this, TextRange(this.length))
}
/** /**
* <b>!!! Recursive !!!</b> (one wrong step and you are dead 💀) * <b>!!! Recursive !!!</b> (one wrong step and you are dead 💀)
*/ */

View File

@ -63,7 +63,7 @@ class CalculatorScreenTest {
CalculatorScreen( CalculatorScreen(
uiState = CalculatorUIState.Ready( uiState = CalculatorUIState.Ready(
input = TextFieldValue(), input = TextFieldValue(),
output = CalculationResult.Default(), output = CalculationResult.Empty,
radianMode = false, radianMode = false,
precision = 3, precision = 3,
outputFormat = OutputFormat.PLAIN, outputFormat = OutputFormat.PLAIN,
@ -97,7 +97,7 @@ class CalculatorScreenTest {
CalculatorScreen( CalculatorScreen(
uiState = CalculatorUIState.Ready( uiState = CalculatorUIState.Ready(
input = TextFieldValue(), input = TextFieldValue(),
output = CalculationResult.Default(), output = CalculationResult.Empty,
radianMode = false, radianMode = false,
precision = 3, precision = 3,
outputFormat = OutputFormat.PLAIN, outputFormat = OutputFormat.PLAIN,

View File

@ -45,6 +45,8 @@ internal sealed class CalculatorUIState {
sealed class CalculationResult { sealed class CalculationResult {
data class Default(val text: String = "") : CalculationResult() data class Default(val text: String = "") : CalculationResult()
data object Empty: CalculationResult()
data object DivideByZeroError : CalculationResult() { data object DivideByZeroError : CalculationResult() {
@StringRes @StringRes
val label: Int = R.string.calculator_divide_by_zero_error val label: Int = R.string.calculator_divide_by_zero_error

View File

@ -28,6 +28,7 @@ import com.sadellie.unitto.core.ui.common.textfield.AllFormatterSymbols
import com.sadellie.unitto.core.ui.common.textfield.addBracket import com.sadellie.unitto.core.ui.common.textfield.addBracket
import com.sadellie.unitto.core.ui.common.textfield.addTokens import com.sadellie.unitto.core.ui.common.textfield.addTokens
import com.sadellie.unitto.core.ui.common.textfield.deleteTokens import com.sadellie.unitto.core.ui.common.textfield.deleteTokens
import com.sadellie.unitto.core.ui.common.textfield.getTextField
import com.sadellie.unitto.data.common.isExpression import com.sadellie.unitto.data.common.isExpression
import com.sadellie.unitto.data.common.setMinimumRequiredScale import com.sadellie.unitto.data.common.setMinimumRequiredScale
import com.sadellie.unitto.data.common.stateIn import com.sadellie.unitto.data.common.stateIn
@ -45,6 +46,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal import java.math.BigDecimal
import javax.inject.Inject import javax.inject.Inject
@ -55,24 +57,23 @@ internal class CalculatorViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle, private val savedStateHandle: SavedStateHandle,
) : ViewModel() { ) : ViewModel() {
private val _inputKey = "CALCULATOR_INPUT" private val _inputKey = "CALCULATOR_INPUT"
private val _input = MutableStateFlow( private val _input = MutableStateFlow(savedStateHandle.getTextField(_inputKey))
with(savedStateHandle[_inputKey] ?: "") { private val _result = MutableStateFlow<CalculationResult>(CalculationResult.Empty)
TextFieldValue(this, TextRange(this.length))
}
)
private val _result = MutableStateFlow<CalculationResult>(CalculationResult.Default())
private val _equalClicked = MutableStateFlow(false) private val _equalClicked = MutableStateFlow(false)
private val _prefs = userPrefsRepository.calculatorPrefs
.stateIn(viewModelScope, null)
val uiState: StateFlow<CalculatorUIState> = combine( val uiState: StateFlow<CalculatorUIState> = combine(
_input, _input,
_result, _result,
userPrefsRepository.calculatorPrefs, _prefs,
calculatorHistoryRepository.historyFlow, calculatorHistoryRepository.historyFlow,
_equalClicked, ) { input, result, prefs, history ->
) { input, result, prefs, history, showError -> prefs ?: return@combine CalculatorUIState.Loading
return@combine CalculatorUIState.Ready( return@combine CalculatorUIState.Ready(
input = input, input = input,
output = if (!showError and (result !is CalculationResult.Default)) CalculationResult.Default() else result, output = result,
radianMode = prefs.radianMode, radianMode = prefs.radianMode,
precision = prefs.precision, precision = prefs.precision,
outputFormat = prefs.outputFormat, outputFormat = prefs.outputFormat,
@ -85,12 +86,31 @@ internal class CalculatorViewModel @Inject constructor(
) )
} }
.mapLatest { ui -> .mapLatest { ui ->
if (ui !is CalculatorUIState.Ready) return@mapLatest ui
if (_equalClicked.value) return@mapLatest ui
if (!ui.input.text.isExpression()) {
_result.update { CalculationResult.Empty }
return@mapLatest ui
}
_result.update {
try {
CalculationResult.Default(
calculate( calculate(
input = ui.input.text, input = ui.input.text,
radianMode = ui.radianMode, radianMode = ui.radianMode,
outputFormat = ui.outputFormat,
precision = ui.precision
) )
.setMinimumRequiredScale(ui.precision)
.trimZeros()
.toStringWith(ui.outputFormat)
)
} catch (e: ExpressionException.DivideByZero) {
CalculationResult.Empty
} catch (e: Exception) {
CalculationResult.Empty
}
}
ui ui
} }
@ -130,9 +150,11 @@ internal class CalculatorViewModel @Inject constructor(
} }
fun clearInput() = _input.update { fun clearInput() = _input.update {
_equalClicked.update { false }
savedStateHandle[_inputKey] = "" savedStateHandle[_inputKey] = ""
TextFieldValue() TextFieldValue()
} }
fun onCursorChange(selection: TextRange) = _input.update { it.copy(selection = selection) } fun onCursorChange(selection: TextRange) = _input.update { it.copy(selection = selection) }
fun updateRadianMode(newValue: Boolean) = viewModelScope.launch { fun updateRadianMode(newValue: Boolean) = viewModelScope.launch {
@ -143,56 +165,47 @@ internal class CalculatorViewModel @Inject constructor(
calculatorHistoryRepository.clear() calculatorHistoryRepository.clear()
} }
fun evaluate() = viewModelScope.launch(Dispatchers.IO) { fun evaluate() = viewModelScope.launch {
when (val result = _result.value) { val prefs = _prefs.value ?: return@launch
is CalculationResult.Default -> { if (_equalClicked.value) return@launch
calculatorHistoryRepository.add( if (!_input.value.text.isExpression()) return@launch
expression = _input.value.text.replace("-", Token.Operator.minus),
result = result.text val result = try {
) calculate(_input.value.text, prefs.radianMode)
_input.update { TextFieldValue(result.text, TextRange(result.text.length)) } } catch (e: ExpressionException.DivideByZero) {
_result.update { CalculationResult.Default() }
_equalClicked.update { true } _equalClicked.update { true }
} _result.update { CalculationResult.DivideByZeroError }
return@launch
is CalculationResult.DivideByZeroError -> { } catch (e: ExpressionException.FactorialCalculation) {
_equalClicked.update { true } _equalClicked.update { true }
} _result.update { CalculationResult.Error }
return@launch
is CalculationResult.Error -> { } catch (e: Exception) {
// skip for generic error (bad expression and stuff _equalClicked.update { true }
} _result.update { CalculationResult.Error }
}
}
private fun calculate(
input: String,
radianMode: Boolean,
outputFormat: Int,
precision: Int,
) = viewModelScope.launch(Dispatchers.Default) {
if (!input.isExpression()) {
_result.update { CalculationResult.Default() }
return@launch return@launch
} }
.setMinimumRequiredScale(prefs.precision)
.trimZeros()
.toStringWith(prefs.outputFormat)
_result.update { calculatorHistoryRepository.add(
try { expression = _input.value.text.replace("-", Token.Operator.minus),
CalculationResult.Default( result = result
Expression(input, radianMode = radianMode) )
_input.update { TextFieldValue(result, TextRange(result.length)) }
_result.update { CalculationResult.Empty }
}
private suspend fun calculate(
input: String,
radianMode: Boolean,
): BigDecimal = withContext(Dispatchers.Default) {
Expression(input, radianMode)
.calculate() .calculate()
.also { .also {
if (it > BigDecimal.valueOf(Double.MAX_VALUE)) throw ExpressionException.TooBig() if (it > BigDecimal.valueOf(Double.MAX_VALUE)) throw ExpressionException.TooBig()
} }
.setMinimumRequiredScale(precision)
.trimZeros()
.toStringWith(outputFormat)
)
} catch (e: ExpressionException.DivideByZero) {
CalculationResult.DivideByZeroError
} catch (e: Exception) {
CalculationResult.Error
}
}
} }
} }

View File

@ -22,6 +22,7 @@ import android.content.res.Configuration
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.sizeIn
@ -83,6 +84,15 @@ fun TextBox(
) )
if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) { if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) {
when (output) { when (output) {
is CalculationResult.Empty -> {
Spacer(
modifier = Modifier
.weight(2f)
.fillMaxWidth()
.padding(horizontal = 8.dp)
)
}
is CalculationResult.Default -> { is CalculationResult.Default -> {
var outputTF by remember(output) { var outputTF by remember(output) {
mutableStateOf(TextFieldValue(output.text)) mutableStateOf(TextFieldValue(output.text))

View File

@ -28,6 +28,7 @@ import com.sadellie.unitto.core.ui.common.textfield.AllFormatterSymbols
import com.sadellie.unitto.core.ui.common.textfield.addBracket import com.sadellie.unitto.core.ui.common.textfield.addBracket
import com.sadellie.unitto.core.ui.common.textfield.addTokens import com.sadellie.unitto.core.ui.common.textfield.addTokens
import com.sadellie.unitto.core.ui.common.textfield.deleteTokens import com.sadellie.unitto.core.ui.common.textfield.deleteTokens
import com.sadellie.unitto.core.ui.common.textfield.getTextField
import com.sadellie.unitto.data.common.combine import com.sadellie.unitto.data.common.combine
import com.sadellie.unitto.data.common.isExpression import com.sadellie.unitto.data.common.isExpression
import com.sadellie.unitto.data.common.stateIn import com.sadellie.unitto.data.common.stateIn
@ -64,11 +65,7 @@ internal class ConverterViewModel @Inject constructor(
) : ViewModel() { ) : ViewModel() {
private val converterInputKey = "CONVERTER_INPUT" private val converterInputKey = "CONVERTER_INPUT"
private val _input = MutableStateFlow( private val _input = MutableStateFlow(savedStateHandle.getTextField(converterInputKey))
with(savedStateHandle[converterInputKey] ?: "") {
TextFieldValue(this, TextRange(this.length))
}
)
private val _calculation = MutableStateFlow<BigDecimal?>(null) private val _calculation = MutableStateFlow<BigDecimal?>(null)
private val _result = MutableStateFlow<ConverterResult>(ConverterResult.Loading) private val _result = MutableStateFlow<ConverterResult>(ConverterResult.Loading)
private val _unitFrom = MutableStateFlow<AbstractUnit?>(null) private val _unitFrom = MutableStateFlow<AbstractUnit?>(null)