diff --git a/data/calculator/src/main/java/com/sadellie/unitto/data/calculator/CalculatorHistoryRepositoryImpl.kt b/data/calculator/src/main/java/com/sadellie/unitto/data/calculator/CalculatorHistoryRepositoryImpl.kt index ec24df7d..e5da0066 100644 --- a/data/calculator/src/main/java/com/sadellie/unitto/data/calculator/CalculatorHistoryRepositoryImpl.kt +++ b/data/calculator/src/main/java/com/sadellie/unitto/data/calculator/CalculatorHistoryRepositoryImpl.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext import java.util.Date import javax.inject.Inject @@ -43,7 +44,7 @@ class CalculatorHistoryRepositoryImpl @Inject constructor( override suspend fun add( expression: String, result: String, - ) { + ) = withContext(Dispatchers.IO) { calculatorHistoryDao.insert( CalculatorHistoryEntity( timestamp = System.currentTimeMillis(), @@ -53,16 +54,16 @@ class CalculatorHistoryRepositoryImpl @Inject constructor( ) } - override suspend fun delete(item: HistoryItem) { + override suspend fun delete(item: HistoryItem) = withContext(Dispatchers.IO) { calculatorHistoryDao.delete(item.id) } - override suspend fun clear() { + override suspend fun clear() = withContext(Dispatchers.IO) { calculatorHistoryDao.clear() } - private fun List.toHistoryItemList(): List { - return this.map { + private suspend fun List.toHistoryItemList(): List = withContext(Dispatchers.Default) { + this@toHistoryItemList.map { HistoryItem( id = it.entityId, date = Date(it.timestamp), diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt index 6872c12e..a3204831 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt @@ -332,7 +332,7 @@ private fun PreviewCalculatorScreen() { Ready( uiState = CalculatorUIState.Ready( input = TextFieldValue("1.2345"), - output = CalculationResult.Default("1234"), + output = CalculationResult.Success("1234"), radianMode = false, precision = 3, outputFormat = OutputFormat.PLAIN, diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorUIState.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorUIState.kt index 80455628..25f650e9 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorUIState.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorUIState.kt @@ -42,9 +42,7 @@ internal sealed class CalculatorUIState { } sealed class CalculationResult { - data class Default(val text: String) : CalculationResult() - - data class Fraction(val text: String) : CalculationResult() + data class Success(val text: String) : CalculationResult() data object Empty : CalculationResult() diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt index fe69c1b2..a5a4c445 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt @@ -18,7 +18,6 @@ package com.sadellie.unitto.feature.calculator -import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel @@ -31,6 +30,7 @@ import com.sadellie.unitto.core.ui.common.textfield.getTextField import com.sadellie.unitto.core.ui.common.textfield.placeCursorAtTheEnd import com.sadellie.unitto.data.common.format import com.sadellie.unitto.data.common.isExpression +import com.sadellie.unitto.data.common.isGreaterThan import com.sadellie.unitto.data.common.stateIn import com.sadellie.unitto.data.model.HistoryItem import com.sadellie.unitto.data.model.repository.CalculatorHistoryRepository @@ -40,10 +40,10 @@ import io.github.sadellie.evaluatto.Expression import io.github.sadellie.evaluatto.ExpressionException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -57,13 +57,13 @@ internal class CalculatorViewModel @Inject constructor( private val calculatorHistoryRepository: CalculatorHistoryRepository, private val savedStateHandle: SavedStateHandle, ) : ViewModel() { + private var calculationJob: Job? = null + private val inputKey = "CALCULATOR_INPUT" private val input = MutableStateFlow(savedStateHandle.getTextField(inputKey)) private val result = MutableStateFlow(CalculationResult.Empty) private val equalClicked = MutableStateFlow(false) - private val prefs = userPrefsRepository.calculatorPrefs - .stateIn(viewModelScope, null) - private var fractionJob: Job? = null + private val prefs = userPrefsRepository.calculatorPrefs.stateIn(viewModelScope, null) val uiState: StateFlow = combine( input, @@ -88,33 +88,6 @@ internal class CalculatorViewModel @Inject constructor( partialHistoryView = prefs.partialHistoryView, ) } - .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( - input = ui.input.text, - radianMode = ui.radianMode, - ) - .format(ui.precision, ui.outputFormat), - ) - } catch (e: ExpressionException.DivideByZero) { - CalculationResult.Empty - } catch (e: Exception) { - CalculationResult.Empty - } - } - - ui - } .stateIn(viewModelScope, CalculatorUIState.Loading) fun addTokens(tokens: String) { @@ -150,14 +123,15 @@ internal class CalculatorViewModel @Inject constructor( fun clearInput() = updateInput(TextFieldValue()) fun updateInput(value: TextFieldValue) { - fractionJob?.cancel() equalClicked.update { false } input.update { value } savedStateHandle[inputKey] = value.text + calculateInput() } fun updateRadianMode(newValue: Boolean) = viewModelScope.launch { userPrefsRepository.updateRadianMode(newValue) + calculateInput() } fun updateAdditionalButtons(newValue: Boolean) = viewModelScope.launch { @@ -168,54 +142,71 @@ internal class CalculatorViewModel @Inject constructor( userPrefsRepository.updateInverseMode(newValue) } - fun clearHistory() = viewModelScope.launch(Dispatchers.IO) { + fun clearHistory() = viewModelScope.launch { calculatorHistoryRepository.clear() } - fun deleteHistoryItem(item: HistoryItem) = viewModelScope.launch(Dispatchers.IO) { + fun deleteHistoryItem(item: HistoryItem) = viewModelScope.launch { calculatorHistoryRepository.delete(item) } fun equal() = viewModelScope.launch { val prefs = prefs.value ?: return@launch + val inputValue = input.value.text if (equalClicked.value) return@launch - if (!input.value.text.isExpression()) return@launch + if (!inputValue.isExpression()) return@launch - val result = try { - calculate(input.value.text, prefs.radianMode, RoundingMode.DOWN) + val calculated = try { + calculate(inputValue, prefs.radianMode, RoundingMode.HALF_EVEN) + .format(prefs.precision, prefs.outputFormat) } catch (e: ExpressionException.DivideByZero) { - equalClicked.update { true } result.update { CalculationResult.DivideByZeroError } return@launch - } catch (e: ExpressionException.FactorialCalculation) { - equalClicked.update { true } - result.update { CalculationResult.Error } - return@launch } catch (e: Exception) { - equalClicked.update { true } result.update { CalculationResult.Error } return@launch } + val fractional = try { + calculate(inputValue, prefs.radianMode, RoundingMode.DOWN) + .toFractionalString() + } catch (e: Exception) { + result.update { CalculationResult.Error } + return@launch + } + + calculatorHistoryRepository.add( + expression = inputValue, + result = calculated, + ) + equalClicked.update { true } + input.update { TextFieldValue(calculated.replace("-", Token.Operator.minus)) } + result.update { CalculationResult.Success(fractional) } + } - val resultFormatted = result - .format(prefs.precision, prefs.outputFormat) - .replace("-", Token.Operator.minus) + private fun calculateInput() { + calculationJob?.cancel() + calculationJob = viewModelScope.launch { + if (!input.value.text.isExpression()) { + result.update { CalculationResult.Empty } + return@launch + } - withContext(Dispatchers.IO) { - calculatorHistoryRepository.add( - expression = input.value.text.replace("-", Token.Operator.minus), - result = resultFormatted, - ) - } - - fractionJob?.cancel() - fractionJob = launch(Dispatchers.Default) { - val fraction = result.toFractionalString() - - input.update { TextFieldValue(resultFormatted, TextRange.Zero) } - this@CalculatorViewModel.result.update { CalculationResult.Fraction(fraction) } + val prefs = prefs.value ?: return@launch + val newResult = try { + val calculated = calculate( + input = input.value.text, + radianMode = prefs.radianMode, + roundingMode = RoundingMode.HALF_EVEN, + ) + CalculationResult.Success( + calculated.format(prefs.precision, prefs.outputFormat), + ) + } catch (e: Exception) { + CalculationResult.Empty + } + result.update { newResult } } } @@ -227,7 +218,14 @@ internal class CalculatorViewModel @Inject constructor( Expression(input, radianMode, roundingMode) .calculate() .also { - if (it > BigDecimal.valueOf(Double.MAX_VALUE)) throw ExpressionException.TooBig() + if (it.isGreaterThan(maxCalculationResult)) throw ExpressionException.TooBig() } } + + private val maxCalculationResult = BigDecimal.valueOf(Double.MAX_VALUE) + + override fun onCleared() { + viewModelScope.cancel() + super.onCleared() + } } diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/DecimalToFraction.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/DecimalToFraction.kt index fd65ccf3..03b30c19 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/DecimalToFraction.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/DecimalToFraction.kt @@ -20,6 +20,8 @@ package com.sadellie.unitto.feature.calculator import com.sadellie.unitto.core.base.Token import com.sadellie.unitto.data.common.isEqualTo +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.math.BigDecimal import java.math.BigInteger import java.math.RoundingMode @@ -36,13 +38,13 @@ import java.math.RoundingMode * @receiver [BigDecimal]. Scale doesn't matter, but should be `MAX_PRECISION` * @return */ -fun BigDecimal.toFractionalString(): String { +suspend fun BigDecimal.toFractionalString(): String = withContext(Dispatchers.Default) { // https://www.khanacademy.org/math/cc-eighth-grade-math/cc-8th-numbers-operations/cc-8th-repeating-decimals/v/coverting-repeating-decimals-to-fractions-1 // https://www.khanacademy.org/math/cc-eighth-grade-math/cc-8th-numbers-operations/cc-8th-repeating-decimals/v/coverting-repeating-decimals-to-fractions-2 - val (integral, fractional) = this.divideAndRemainder(BigDecimal.ONE) + val (integral, fractional) = this@toFractionalString.divideAndRemainder(BigDecimal.ONE) val integralBI = integral.toBigInteger() - if (fractional.isEqualTo(BigDecimal.ZERO)) return "" + if (fractional.isEqualTo(BigDecimal.ZERO)) return@withContext "" val res: String = if (integral.isEqualTo(BigDecimal.ZERO)) "" else "$integralBI " @@ -54,9 +56,9 @@ fun BigDecimal.toFractionalString(): String { fractional.repeatingFractional(repeatingDecimals.length) } - if (finalDenominator > maxDenominator) return "" + if (finalDenominator > maxDenominator) return@withContext "" - return "$res$finalNumerator⁄$finalDenominator" + return@withContext "$res$finalNumerator⁄$finalDenominator" } private fun BigDecimal.notRepeatingFractional(): Pair { diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/TextBox.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/TextBox.kt index 54420798..7a147c64 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/TextBox.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/TextBox.kt @@ -93,21 +93,7 @@ fun TextBox( ) } - is CalculationResult.Default -> { - var outputTF by remember(output) { mutableStateOf(TextFieldValue(output.text)) } - - ExpressionTextField( - modifier = calculationResultModifier, - value = outputTF, - minRatio = 0.8f, - onValueChange = { outputTF = it }, - textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.6f), - formatterSymbols = formatterSymbols, - readOnly = true, - ) - } - - is CalculationResult.Fraction -> { + is CalculationResult.Success -> { var outputTF by remember(output) { mutableStateOf(TextFieldValue(output.text)) } ExpressionTextField( diff --git a/feature/calculator/src/test/java/com/sadellie/unitto/feature/calculator/DecimalToFractionTest.kt b/feature/calculator/src/test/java/com/sadellie/unitto/feature/calculator/DecimalToFractionTest.kt index 0e6b2639..d7691f48 100644 --- a/feature/calculator/src/test/java/com/sadellie/unitto/feature/calculator/DecimalToFractionTest.kt +++ b/feature/calculator/src/test/java/com/sadellie/unitto/feature/calculator/DecimalToFractionTest.kt @@ -18,6 +18,7 @@ package com.sadellie.unitto.feature.calculator +import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Test import java.math.BigDecimal @@ -25,60 +26,52 @@ import java.math.BigDecimal class DecimalToFractionTest { @Test fun testNoDecimal1() { - val bd = BigDecimal("100") - assertFractional("", bd.toFractionalString()) + assertFractional("", "100") } @Test fun testNoDecimal2() { - val bd = BigDecimal("100.000000000") - assertFractional("", bd.toFractionalString()) + assertFractional("", "100.000000000") } @Test fun testSimpleDecimal1() { - val bd = BigDecimal("0.25") - assertFractional("1/4", bd.toFractionalString()) + assertFractional("1/4", "0.25") } @Test fun testSimpleDecimal2() { - val bd = BigDecimal("100.25") - assertFractional("100 1/4", bd.toFractionalString()) + assertFractional("100 1/4", "100.25") } @Test fun testRepeating1() { - val bd = BigDecimal("0.666666666") - assertFractional("2/3", bd.toFractionalString()) + assertFractional("2/3", "0.666666666") } @Test fun testRepeating2() { - val bd = BigDecimal("4.666666666") - assertFractional("4 2/3", bd.toFractionalString()) + assertFractional("4 2/3", "4.666666666") } @Test fun testRepeating3() { - val bd = BigDecimal("0.78571428571428571428") - assertFractional("11/14", bd.toFractionalString()) + assertFractional("11/14", "0.78571428571428571428") } @Test fun testRepeating4() { - val bd = BigDecimal("66.78571428571428571428") - assertFractional("66 11/14", bd.toFractionalString()) + assertFractional("66 11/14", "66.78571428571428571428") } @Test fun testRepeating5() { - val bd = BigDecimal("0.666000") - assertFractional("333/500", bd.toFractionalString()) + assertFractional("333/500", "0.666000") } private fun assertFractional(expected: String, actual: String) = assertEquals( expected, - actual.replace("⁄", "/"), + runBlocking { BigDecimal(actual).toFractionalString() } + .replace("⁄", "/"), ) }