mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-19 08:45:27 +02:00
More robust "=" logic
This commit is contained in:
parent
1537df88eb
commit
689c812c11
@ -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 💀)
|
||||||
*/
|
*/
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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 ->
|
||||||
calculate(
|
if (ui !is CalculatorUIState.Ready) return@mapLatest ui
|
||||||
input = ui.input.text,
|
if (_equalClicked.value) return@mapLatest ui
|
||||||
radianMode = ui.radianMode,
|
|
||||||
outputFormat = ui.outputFormat,
|
if (!ui.input.text.isExpression()) {
|
||||||
precision = ui.precision
|
_result.update { CalculationResult.Empty }
|
||||||
)
|
return@mapLatest ui
|
||||||
|
}
|
||||||
|
|
||||||
|
_result.update {
|
||||||
|
try {
|
||||||
|
CalculationResult.Default(
|
||||||
|
calculate(
|
||||||
|
input = ui.input.text,
|
||||||
|
radianMode = ui.radianMode,
|
||||||
|
)
|
||||||
|
.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
|
|
||||||
)
|
|
||||||
_input.update { TextFieldValue(result.text, TextRange(result.text.length)) }
|
|
||||||
_result.update { CalculationResult.Default() }
|
|
||||||
_equalClicked.update { true }
|
|
||||||
}
|
|
||||||
|
|
||||||
is CalculationResult.DivideByZeroError -> {
|
val result = try {
|
||||||
_equalClicked.update { true }
|
calculate(_input.value.text, prefs.radianMode)
|
||||||
}
|
} catch (e: ExpressionException.DivideByZero) {
|
||||||
|
_equalClicked.update { true }
|
||||||
is CalculationResult.Error -> {
|
_result.update { CalculationResult.DivideByZeroError }
|
||||||
// skip for generic error (bad expression and stuff
|
return@launch
|
||||||
}
|
} catch (e: ExpressionException.FactorialCalculation) {
|
||||||
}
|
_equalClicked.update { true }
|
||||||
}
|
_result.update { CalculationResult.Error }
|
||||||
|
return@launch
|
||||||
private fun calculate(
|
} catch (e: Exception) {
|
||||||
input: String,
|
_equalClicked.update { true }
|
||||||
radianMode: Boolean,
|
_result.update { CalculationResult.Error }
|
||||||
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)
|
)
|
||||||
.calculate()
|
|
||||||
.also {
|
_input.update { TextFieldValue(result, TextRange(result.length)) }
|
||||||
if (it > BigDecimal.valueOf(Double.MAX_VALUE)) throw ExpressionException.TooBig()
|
_result.update { CalculationResult.Empty }
|
||||||
}
|
}
|
||||||
.setMinimumRequiredScale(precision)
|
|
||||||
.trimZeros()
|
private suspend fun calculate(
|
||||||
.toStringWith(outputFormat)
|
input: String,
|
||||||
)
|
radianMode: Boolean,
|
||||||
} catch (e: ExpressionException.DivideByZero) {
|
): BigDecimal = withContext(Dispatchers.Default) {
|
||||||
CalculationResult.DivideByZeroError
|
Expression(input, radianMode)
|
||||||
} catch (e: Exception) {
|
.calculate()
|
||||||
CalculationResult.Error
|
.also {
|
||||||
|
if (it > BigDecimal.valueOf(Double.MAX_VALUE)) throw ExpressionException.TooBig()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user