Fractional output

This commit is contained in:
Sad Ellie 2023-11-13 22:08:06 +03:00
parent 689c812c11
commit e06bb76b02
8 changed files with 272 additions and 13 deletions

View File

@ -24,6 +24,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.Typography
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
@ -87,7 +88,7 @@ private val FontFamily.Companion.lato: FontFamily
@Preview(widthDp = 480)
@Composable
private fun PreviewTypography() {
private fun PreviewSystemTypography() {
MaterialTheme(
typography = TypographySystem
) {
@ -123,3 +124,30 @@ private fun PreviewTypography() {
}
}
}
@Preview(widthDp = 480)
@Composable
private fun PreviewNumberTypography() {
CompositionLocalProvider(
LocalNumberTypography provides NumberTypographyUnitto
) {
val textStyles = mapOf(
"displayLarge" to LocalNumberTypography.current.displayLarge,
"displayMedium" to LocalNumberTypography.current.displayMedium,
"displaySmall" to LocalNumberTypography.current.displaySmall,
)
LazyColumn(Modifier.background(MaterialTheme.colorScheme.background)) {
textStyles.forEach { (label, style) ->
item {
Text(
text = "$label 123 Error 7 12",
style = style,
color = MaterialTheme.colorScheme.onBackground
)
}
}
}
}
}

View File

@ -43,7 +43,11 @@ sealed class ExpressionException(override val message: String): Exception(messag
class TooBig : ExpressionException("Value is too big")
}
class Expression(input: String, private val radianMode: Boolean = true) {
class Expression(
input: String,
private val radianMode: Boolean = true,
private val roundingMode: RoundingMode = RoundingMode.HALF_EVEN
) {
private val tokens = Tokenizer(input).tokenize()
private var cursorPosition = 0
@ -104,7 +108,7 @@ class Expression(input: String, private val radianMode: Boolean = true) {
val divisor = parseFactor()
if (divisor.compareTo(BigDecimal.ZERO) == 0) throw ExpressionException.DivideByZero()
expression = expression.divide(divisor, RoundingMode.HALF_EVEN)
expression = expression.divide(divisor, roundingMode)
}
}
}

View File

@ -43,9 +43,11 @@ internal sealed class CalculatorUIState {
}
sealed class CalculationResult {
data class Default(val text: String = "") : CalculationResult()
data class Default(val text: String) : CalculationResult()
data object Empty: CalculationResult()
data class Fraction(val text: String) : CalculationResult()
data object Empty : CalculationResult()
data object DivideByZeroError : CalculationResult() {
@StringRes

View File

@ -40,6 +40,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import io.github.sadellie.evaluatto.Expression
import io.github.sadellie.evaluatto.ExpressionException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
@ -48,6 +49,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
import java.math.RoundingMode
import javax.inject.Inject
@HiltViewModel
@ -62,6 +64,7 @@ internal class CalculatorViewModel @Inject constructor(
private val _equalClicked = MutableStateFlow(false)
private val _prefs = userPrefsRepository.calculatorPrefs
.stateIn(viewModelScope, null)
private var _fractionJob: Job? = null
val uiState: StateFlow<CalculatorUIState> = combine(
_input,
@ -123,6 +126,7 @@ internal class CalculatorViewModel @Inject constructor(
} else {
it.addTokens(tokens)
}
_fractionJob?.cancel()
savedStateHandle[_inputKey] = newValue.text
newValue
}
@ -134,6 +138,7 @@ internal class CalculatorViewModel @Inject constructor(
} else {
it.addBracket()
}
_fractionJob?.cancel()
savedStateHandle[_inputKey] = newValue.text
newValue
}
@ -145,12 +150,14 @@ internal class CalculatorViewModel @Inject constructor(
} else {
it.deleteTokens()
}
_fractionJob?.cancel()
savedStateHandle[_inputKey] = newValue.text
newValue
}
fun clearInput() = _input.update {
_equalClicked.update { false }
_fractionJob?.cancel()
savedStateHandle[_inputKey] = ""
TextFieldValue()
}
@ -171,7 +178,7 @@ internal class CalculatorViewModel @Inject constructor(
if (!_input.value.text.isExpression()) return@launch
val result = try {
calculate(_input.value.text, prefs.radianMode)
calculate(_input.value.text, prefs.radianMode, RoundingMode.DOWN)
} catch (e: ExpressionException.DivideByZero) {
_equalClicked.update { true }
_result.update { CalculationResult.DivideByZeroError }
@ -185,24 +192,36 @@ internal class CalculatorViewModel @Inject constructor(
_result.update { CalculationResult.Error }
return@launch
}
_equalClicked.update { true }
val resultFormatted = result
.setMinimumRequiredScale(prefs.precision)
.trimZeros()
.toStringWith(prefs.outputFormat)
calculatorHistoryRepository.add(
expression = _input.value.text.replace("-", Token.Operator.minus),
result = result
)
withContext(Dispatchers.IO) {
calculatorHistoryRepository.add(
expression = _input.value.text.replace("-", Token.Operator.minus),
result = resultFormatted
)
}
_input.update { TextFieldValue(result, TextRange(result.length)) }
_result.update { CalculationResult.Empty }
_fractionJob?.cancel()
_fractionJob = launch(Dispatchers.Default) {
val fraction = result.toFractionalString()
_input.update { TextFieldValue(resultFormatted, TextRange(resultFormatted.length)) }
_result.update { CalculationResult.Fraction(fraction) }
}
}
private suspend fun calculate(
input: String,
radianMode: Boolean,
roundingMode: RoundingMode = RoundingMode.HALF_EVEN,
): BigDecimal = withContext(Dispatchers.Default) {
Expression(input, radianMode)
Expression(input, radianMode, roundingMode)
.calculate()
.also {
if (it > BigDecimal.valueOf(Double.MAX_VALUE)) throw ExpressionException.TooBig()

View File

@ -0,0 +1,105 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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.feature.calculator
import com.sadellie.unitto.core.base.Token
import java.math.BigDecimal
import java.math.BigInteger
import java.math.RoundingMode
fun BigDecimal.toFractionalString(): String {
// 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 integralBI = integral.toBigInteger()
if (fractional.isEqualTo(BigDecimal.ZERO)) return integralBI.toString()
val res: String = if (integral.isEqualTo(BigDecimal.ZERO)) "" else "$integralBI "
val repeatingDecimals = fractional.repeatingDecimals()
val (finalNumerator, finalDenominator) = if (repeatingDecimals == null) {
fractional.notRepeatingFractional()
} else {
fractional.repeatingFractional(repeatingDecimals.length)
}
if (finalDenominator > maxDenominator) return ""
return "$res$finalNumerator$finalDenominator"
}
private fun BigDecimal.notRepeatingFractional(): Pair<BigInteger, BigInteger> {
val fractionalPrecision = BigInteger.TEN.pow(scale())
// 0.000123456 -> 123456
val fractionalBI: BigInteger = (this * fractionalPrecision.toBigDecimal()).toBigInteger()
val gcdVal = fractionalBI.gcd(fractionalPrecision)
val numerator = fractionalBI / gcdVal
val denominator = fractionalPrecision / gcdVal
return numerator to denominator
}
private fun BigDecimal.repeatingFractional(
repeatingLength: Int
): Pair<BigInteger, BigInteger> {
val multiplier = BigInteger.TEN.pow(repeatingLength)
val multiplied = (this * multiplier.toBigDecimal()).stripTrailingZeros()
val numerator = (multiplied - this.setScale(multiplied.scale(), RoundingMode.DOWN)).stripTrailingZeros()
val denominator = multiplier - BigInteger.ONE
// get rid of decimal in numerator
val bigIntegerMultiplies = BigDecimal.TEN.pow(scale())
var finalNumerator = numerator.multiply(bigIntegerMultiplies).toBigInteger()
var finalDenominator = denominator.multiply(bigIntegerMultiplies.toBigInteger())
val gcd = finalNumerator.gcd(finalDenominator)
finalNumerator /= gcd
finalDenominator /= gcd
return finalNumerator to finalDenominator
}
private fun BigDecimal.repeatingDecimals(): String? {
val inputString = scaleByPowerOfTen(scale()).toBigInteger().toString()
repeat(inputString.length) { index ->
val stringInFront = inputString.substring(index)
(1..stringInFront.length/2).forEach checkLoop@{ loop ->
val pattern = stringInFront.take(loop)
val checkRange = stringInFront.substring(0, stringInFront.length - stringInFront.length % pattern.length)
val checkChunks = checkRange.chunked(pattern.length)
val matched = checkChunks.all { it == pattern }
if (matched) {
return if (pattern == Token.Digit._0) null else pattern
}
}
}
return null
}
private fun BigDecimal.isEqualTo(bd: BigDecimal): Boolean = compareTo(bd) == 0
private val maxDenominator by lazy { BigInteger("1000000000") }

View File

@ -112,6 +112,23 @@ fun TextBox(
)
}
is CalculationResult.Fraction -> {
var outputTF by remember(output) {
mutableStateOf(TextFieldValue(output.text))
}
UnformattedTextField(
modifier = Modifier
.weight(2f)
.fillMaxWidth()
.padding(horizontal = 8.dp),
value = outputTF,
minRatio = 1f,
onCursorChange = { outputTF = outputTF.copy(selection = it) },
textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.6f),
readOnly = true,
)
}
is CalculationResult.DivideByZeroError -> {
UnformattedTextField(
modifier = Modifier

View File

@ -0,0 +1,84 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 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.feature.calculator
import org.junit.Assert.assertEquals
import org.junit.Test
import java.math.BigDecimal
class DecimalToFractionTest {
@Test
fun testNoDecimal1() {
val bd = BigDecimal("100")
assertFractional("100", bd.toFractionalString())
}
@Test
fun testNoDecimal2() {
val bd = BigDecimal("100.000000000")
assertFractional("100", bd.toFractionalString())
}
@Test
fun testSimpleDecimal1() {
val bd = BigDecimal("0.25")
assertFractional("1/4", bd.toFractionalString())
}
@Test
fun testSimpleDecimal2() {
val bd = BigDecimal("100.25")
assertFractional("100 1/4", bd.toFractionalString())
}
@Test
fun testRepeating1() {
val bd = BigDecimal("0.666666666")
assertFractional("2/3", bd.toFractionalString())
}
@Test
fun testRepeating2() {
val bd = BigDecimal("4.666666666")
assertFractional("4 2/3", bd.toFractionalString())
}
@Test
fun testRepeating3() {
val bd = BigDecimal("0.78571428571428571428")
assertFractional("11/14", bd.toFractionalString())
}
@Test
fun testRepeating4() {
val bd = BigDecimal("66.78571428571428571428")
assertFractional("66 11/14", bd.toFractionalString())
}
@Test
fun testRepeating5() {
val bd = BigDecimal("0.666000")
assertFractional("333/500", bd.toFractionalString())
}
private fun assertFractional(expected: String, actual: String) = assertEquals(
expected,
actual.replace("", "/")
)
}