diff --git a/app/src/main/java/com/darkempire78/opencalculator/DecimalToFraction.kt b/app/src/main/java/com/darkempire78/opencalculator/DecimalToFraction.kt new file mode 100644 index 00000000..30d6e7ab --- /dev/null +++ b/app/src/main/java/com/darkempire78/opencalculator/DecimalToFraction.kt @@ -0,0 +1,104 @@ +package com.darkempire78.opencalculator + +import java.math.BigDecimal +import java.math.BigInteger +import java.math.MathContext +import java.math.RoundingMode + +class DecimalToFraction { + // Recursive function to return GCD of a and b + private fun gcd(_a: BigInteger, _b: BigInteger): BigInteger { + val a = _a.abs() + val b = _b.abs() + if (a == BigInteger.ZERO) return b + else if (b == BigInteger.ZERO) return a + return if (a < b) gcd(a, b % a) + else gcd(b, a % b) + } + + fun approximateRational(value: BigDecimal, tolerance: BigDecimal = BigDecimal("1E-10")): Pair { + var r0 = value + var r1 = BigDecimal.ONE + var a0 = r0.toBigInteger() + var a1 = BigInteger.ONE + var b0 = BigInteger.ONE + var b1 = BigInteger.ZERO + + var r = r0 - a0.toBigDecimal() + var n = BigInteger.ONE + + while (r.abs() > tolerance) { + r = BigDecimal.ONE.divide(r, MathContext.DECIMAL128) + val ai = r.toBigInteger() + + val newA = ai.multiply(a0).add(a1) + val newB = ai.multiply(b0).add(b1) + + a1 = a0 + b1 = b0 + a0 = newA + b0 = newB + + r = r.subtract(ai.toBigDecimal()) + + n++ + } + + return Pair(a0, b0) + } + + fun needsApproximation(value: BigDecimal, significantFigures : Int): Boolean { + val roundedValue = value.round(MathContext(significantFigures, RoundingMode.HALF_UP)) + val difference = value.subtract(roundedValue).abs() + val tolerance = BigDecimal("1E-${significantFigures+1}") + + return difference > tolerance + } + + fun getFraction(number: BigDecimal): String { + var intVal = number.toBigInteger() // Fetch integral value of the decimal + val fVal = number - BigDecimal(intVal) // Fetch fractional part of the decimal + + if (fVal.compareTo(BigDecimal.ZERO) == 0) { + return "" + } + + // Consider precision value to convert fractional part to integral equivalent + val scale = number.scale() + val pVal = BigDecimal.TEN.pow(scale) + //val pVal = BigDecimal(10000000000000) + + // Check if the number needs approximation to be converted to a fraction + // Example: 1.33333...3 -> 4/3 + if (needsApproximation(fVal, 10)) { + val (num, den) = approximateRational(fVal) + val isNgeative = if (intVal < BigInteger.ZERO) "-" else "" + intVal = intVal.abs() + return if (intVal == BigInteger.ZERO) { + "$isNgeative$num/$den" + } else { + "$isNgeative$intVal $num/$den" + } + } + + // Calculate GCD of integral equivalent of fractional part and precision value + val gcdVal = gcd((fVal * pVal).toBigInteger(), pVal.toBigInteger()) + + // Calculate num and deno + val num = ((fVal * pVal / gcdVal.toBigDecimal()).toBigInteger()).abs() + val deno = ((pVal / gcdVal.toBigDecimal()).toBigInteger()).abs() + + val isNgeative = if (intVal < BigInteger.ZERO) "-" else "" + intVal = intVal.abs() + + return if (intVal == BigInteger.ZERO) { + //"$isNgeative $text$num/$deno" + "$isNgeative$num/$deno" + } else { + "$isNgeative$intVal $num/$deno" + } + } + + // Create a function that + +} \ No newline at end of file diff --git a/app/src/main/java/com/darkempire78/opencalculator/MainActivity.kt b/app/src/main/java/com/darkempire78/opencalculator/MainActivity.kt index ae67a56a..eb96c064 100644 --- a/app/src/main/java/com/darkempire78/opencalculator/MainActivity.kt +++ b/app/src/main/java/com/darkempire78/opencalculator/MainActivity.kt @@ -9,6 +9,7 @@ import android.content.Intent import android.os.Build import android.os.Bundle import android.text.Editable +import android.text.Html import android.text.TextWatcher import android.view.HapticFeedbackConstants import android.view.MenuItem @@ -225,31 +226,6 @@ class MainActivity : AppCompatActivity() { override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { updateResultDisplay() - /*val afterTextLength = s?.length ?: 0 - // If the afterTextLength is equals to 0 we have to clear resultDisplay - if (afterTextLength == 0) { - binding.resultDisplay.setText("") - } - - /* we check if the length of the text entered into the EditText - is greater than the length of the text before the change (beforeTextLength) - by more than 1 character. If it is, we assume that this is a paste event. */ - val clipData = clipboardManager.primaryClip - if (clipData != null && clipData.itemCount > 0) { - //val clipText = clipData.getItemAt(0).coerceToText(this@MainActivity).toString() - - if (s != null) { - //val newValue = s.subSequence(start, start + count).toString() - if ( - (afterTextLength - beforeTextLength > 1) - // Removed to avoid anoying notification (https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#PastingSystemNotifications) - //|| (afterTextLength - beforeTextLength >= 1 && clipText == newValue) // Supports 1+ new caractere if it is equals to the latest element from the clipboard - ) { - // Handle paste event here - updateResultDisplay() - } - } - }*/ } override fun afterTextChanged(s: Editable?) { @@ -558,6 +534,8 @@ class MainActivity : AppCompatActivity() { withContext(Dispatchers.Main) { if (formattedResult != calculation) { binding.resultDisplay.text = formattedResult + } else if ("/" in binding.resultDisplay.text) { + // Pass -> Avoid updating the UI if the result is the same (fraction option ONLY) } else { binding.resultDisplay.text = "" } @@ -876,7 +854,7 @@ class MainActivity : AppCompatActivity() { groupingSeparatorSymbol ) - // If result is a number and it is finite + // If there is no error -> If result is a number and it is finite if (!(division_by_0 || domain_error || syntax_error || is_infinity || require_real_number)) { // Remove zeros at the end of the results (after point) @@ -911,9 +889,6 @@ class MainActivity : AppCompatActivity() { // Hide the cursor (do not remove this, it's not a duplicate) binding.input.isCursorVisible = false - - // Clear resultDisplay - binding.resultDisplay.text = "" } if (calculation != formattedResult) { @@ -964,7 +939,28 @@ class MainActivity : AppCompatActivity() { } } isEqualLastAction = true + + // Display the result in fraction if the option is enabled + if (MyPreferences(this@MainActivity).displayResultInFraction) { + val fractionResult = DecimalToFraction().getFraction(calculationResult) + if (fractionResult != binding.resultDisplay.text) { // Avoid updating the UI if the result is the same + withContext(Dispatchers.Main) { + // https://stackoverflow.com/questions/37904739/html-fromhtml-deprecated-in-android-n/37905107#37905107 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + binding.resultDisplay.text = Html.fromHtml(fractionResult, Html.FROM_HTML_MODE_LEGACY) + } else { + @Suppress("DEPRECATION") + binding.resultDisplay.text = Html.fromHtml(fractionResult) + } + } + } + } else { + withContext(Dispatchers.Main) { + binding.resultDisplay.text = "" + } + } } else { + // Display error withContext(Dispatchers.Main) { if (syntax_error) { setErrorColor(true) diff --git a/app/src/main/java/com/darkempire78/opencalculator/MyPreferences.kt b/app/src/main/java/com/darkempire78/opencalculator/MyPreferences.kt index 76673ad1..580ca87b 100644 --- a/app/src/main/java/com/darkempire78/opencalculator/MyPreferences.kt +++ b/app/src/main/java/com/darkempire78/opencalculator/MyPreferences.kt @@ -25,6 +25,7 @@ class MyPreferences(context: Context) { private const val KEY_SPLIT_PARENTHESIS_BUTTON = "darkempire78.opencalculator.SPLIT_PARENTHESIS_BUTTON" private const val KEY_DELETE_HISTORY_ON_SWIPE = "darkempire78.opencalculator.DELETE_HISTORY_ELEMENT_ON_SWIPE" private const val KEY_AUTO_SAVE_CALCULATION_WITHOUT_EQUAL_BUTTON = "darkempire78.opencalculator.AUTO_SAVE_CALCULATION_WITHOUT_EQUAL_BUTTON" + private const val KEY_DISPLAY_RESULT_IN_FRACTION = "darkempire78.opencalculator.DISPLAY_RESULT_IN_FRACTION" } private val preferences = PreferenceManager.getDefaultSharedPreferences(context) @@ -58,9 +59,10 @@ class MyPreferences(context: Context) { set(value) = preferences.edit().putBoolean(KEY_SPLIT_PARENTHESIS_BUTTON, value).apply() var deleteHistoryOnSwipe = preferences.getBoolean(KEY_DELETE_HISTORY_ON_SWIPE, true) set(value) = preferences.edit().putBoolean(KEY_DELETE_HISTORY_ON_SWIPE, value).apply() - var autoSaveCalculationWithoutEqualButton = preferences.getBoolean(KEY_AUTO_SAVE_CALCULATION_WITHOUT_EQUAL_BUTTON, true) set(value) = preferences.edit().putBoolean(KEY_AUTO_SAVE_CALCULATION_WITHOUT_EQUAL_BUTTON, value).apply() + var displayResultInFraction = preferences.getBoolean(KEY_DISPLAY_RESULT_IN_FRACTION, true) + set(value) = preferences.edit().putBoolean(KEY_DISPLAY_RESULT_IN_FRACTION, value).apply() fun getHistory(): MutableList { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 99df88ce..c626079d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -138,4 +138,6 @@ Real number required Automatically save calculations Automatically save calculations in history without needing to click the equal button + Fraction + Display results in fractions diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index a59c5204..7bfb3f9e 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -154,6 +154,16 @@ app:title="@string/settings_category_advanced" app:color="?attr/text_color"> + +