Search code examples
androidkotlindecimalreverse-engineeringfractions

Decimal to vulgar fraction, Kotlin Android


I have been trying to convert my Swift method to Kotlin and haven't gotten any help here: Decimal to vulgar fraction conversion method- need help converting from Swift to Kotlin

I figured I would change my question a little and see if I can get some help. I want help in reverse engineering/explanation of the code so I can make the conversion myself. I would learn more this way anyway.

The full code/story is linked above if interested. I'm almost at the point of wanting to pay a freelancer to help with this. Please help :) Additionally, you may need to know there is an array with super and subscript outputs for fraction inches that this method rounds to. Need explanation of the following lines:

val whole = number.toInt() // Of course i know what this does :)
val sign = if (whole < 0) -1 else 1 // I converted this myself and believe this is correct Kotlin
val fraction = number - whole.toDouble() // And I know what this does

// need explanation from here down

for (i in 1..fractions.count()) {
    if (abs(fraction) > (fractions[i].1 + fractions[i - 1].1) / 2) {
        if ((fractions[i - 1].1) == (1.0)) run {
            return@run Pair("${whole + sign}", (whole + sign).toDouble())
        } else {
            return ("$whole" $fractions[i - 1].0, ${whole.toDouble() + (sign.toDouble() * fractions[i - 1].1))
        }
    }
}

Solution

  • In general, I tend to write such computational functions as an extension function/property in kotlin, because in this way the usability will be increased.

    NumberExt.kt

    package ***
    
    import kotlin.math.abs
    
    /**
     * @author aminography
     */
    
    val Double.vulgarFraction: Pair<String, Double>
        get() {
            val whole = toInt()
            val sign = if (whole < 0) -1 else 1
            val fraction = this - whole
    
            for (i in 1 until fractions.size) {
                if (abs(fraction) > (fractionValues[i] + fractionValues[i - 1]) / 2) {
                    return if (fractionValues[i - 1] == 1.0) {
                        "${whole + sign}" to (whole + sign).toDouble()
                    } else {
                        "$whole ${fractions[i - 1]}" to whole + sign * fractionValues[i - 1]
                    }
                }
            }
            return "$whole" to whole.toDouble()
        }
    
    val Float.vulgarFraction: Pair<String, Double>
        get() = toDouble().vulgarFraction
    
    private val fractions = arrayOf(
        "",                           // 16/16
        "\u00B9\u2075/\u2081\u2086",  // 15/16
        "\u215E",                     // 7/8
        "\u00B9\u00B3/\u2081\u2086",  // 13/16
        "\u00BE",                     // 3/4
        "\u00B9\u00B9/\u2081\u2086",  // 11/16
        "\u215D",                     // 5/8
        "\u2079/\u2081\u2086",        // 9/16
        "\u00BD",                     // 1/2
        "\u2077/\u2081\u2086",        // 7/16
        "\u215C",                     // 3/8
        "\u2075/\u2081\u2086",        // 5/16
        "\u00BC",                     // 1/4
        "\u00B3/\u2081\u2086",        // 3/16
        "\u215B",                     // 1/8
        "\u00B9/\u2081\u2086",        // 1/16
        ""                            // 0/16
    )
    
    private val fractionValues = arrayOf(
        1.0, 
        15.0 / 16, 7.0 / 8, 13.0 / 16, 3.0 / 4, 11.0 / 16,
        5.0 / 8, 9.0 / 16, 1.0 / 2, 7.0 / 16, 3.0 / 8,
        5.0 / 16, 1.0 / 4, 3.0 / 16, 1.0 / 8, 1.0 / 16,
        0.0
    )
    

    Test

    val rand = java.util.Random()
    repeat(10) {
        val sign = if (rand.nextBoolean()) 1 else -1
        val number = rand.nextDouble() * rand.nextInt(100) * sign
        val vulgar = number.vulgarFraction
        println("Number: $number , Vulgar: ${vulgar.first} , Rounded: ${vulgar.second}")
    }
    

    Output:

    Number: 17.88674468660217 , Vulgar: 17 ⅞ , Rounded: 17.875
    Number: -56.98489542592821 , Vulgar: -57 , Rounded: -57.0
    Number: 39.275953137210614 , Vulgar: 39 ¼ , Rounded: 39.25
    Number: 13.422939071442359 , Vulgar: 13 ⁷/₁₆ , Rounded: 13.4375
    Number: -56.70735924226373 , Vulgar: -56 ¹¹/₁₆ , Rounded: -56.6875
    Number: 22.657555818202972 , Vulgar: 22 ¹¹/₁₆ , Rounded: 22.6875
    Number: 2.951680466645306 , Vulgar: 2 ¹⁵/₁₆ , Rounded: 2.9375
    Number: -8.8311628631306 , Vulgar: -8 ¹³/₁₆ , Rounded: -8.8125
    Number: 28.639946409572655 , Vulgar: 28 ⅝ , Rounded: 28.625
    Number: -28.439447873884085 , Vulgar: -28 ⁷/₁₆ , Rounded: -28.4375



    Explanation

    The explanation of the overall logic is a bit hard and I'll try to make it a little clearer. Note that in the below snippet, I've replaced the fractionValues[i - 1] with fractionValue for simplification.

    // First look at the 'fractions' array. It starts from 16/16=1 down to 0/16=0.
    // So it covers all the possible 16 cases for dividing a number by 16. 
    // Note that 16/16=1 and 0/16=0 are the same in terms of division residual.
    
    for (i in 1 until fractions.size) {
        // Here, we are searching for the proper fraction that is the nearest one to the
        // actual division residual. 
        // So, '|fraction| > (fractionValues[i] + fractionValues[i - 1]) / 2' means
        // that the fraction is closer to the greater one in the 'fractionValues' array 
        // (i.e. 'fractionValues[i - 1]').
        // Consider that we always want to find the proper 'fractionValues[i - 1]' and not 
        // 'fractionValues[i]' (According to the index of the 'for' loop which starts 
        // from 1, and not 0).
    
        if (abs(fraction) > (fractionValues[i] + fractionValues[i - 1]) / 2) {
    
            val fractionValue = fractionValues[i - 1]
            // Here we've found the proper fraction value (i.e. 'fractionValue').
    
            return if (fractionValue == 1.0) {
                // 'fractionValue == 1.0' means that the actual division residual was greater 
                // than 15/16 but closer to 16/16=1. So the final value should be rounded to
                // the nearest integer. Consider that in this case, the nearest integer for a
                // positive number is one more and for a negative number, one less. Finally, 
                // the summation with 'sign' does it for us :)
    
                "${whole + sign}" to (whole + sign).toDouble()
            } else {
                // Here we have 'fractionValue < 1.0'. The only thing is to calculate the 
                // rounded value which is the sum of 'whole' and the discovered 'fractionValue'.
                // As the value could be negative, by multiplying the 'sign' to the 
                // 'fractionValue', we will be sure that the summation is always correct.
    
                "$whole $fractionValue" to whole + sign * fractionValue
            }
        }
    }
    
    // Finally, if we are not able to find a proper 'fractionValue' for the input number, 
    // it means the number had an integer value.
    return "$whole" to whole.toDouble()