mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-18 16:25:27 +02:00
Fractional output
This commit is contained in:
parent
689c812c11
commit
e06bb76b02
Binary file not shown.
@ -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 1⁄2",
|
||||
style = style,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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") }
|
@ -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
|
||||
|
@ -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("⁄", "/")
|
||||
)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user