2

I'm new to kotlin and I'm working on operators overloading for a custom class I defined. The class is called "Rational" and represents a rational number, like for example 117/1098. Class is defined as below and I have overloaded a bunch of operators, like plus, minus, times and so on. However I'm uncertain about what I have to do to overload "in" operator.

Here is my class:

data class Rational(val rational: String) {
    private val numerator: BigInteger
    private val denominator: BigInteger

    init {
        val splitted = rational.split("/")
        numerator = splitted[0].toBigInteger()
        denominator = when (splitted[1]) {
            "0" -> throw Exception("not allowed")
            else -> splitted[1].toBigInteger()
        }
    }

    operator fun plus(number: Rational): Rational {
        val gcm = denominator * number.denominator
        val numerator = (gcm / denominator) * numerator + (gcm / number.denominator) * number.numerator
        return Rational("$numerator/$gcm")
    }

    operator fun minus(number: Rational): Rational {
        val gcm = denominator * number.denominator
        val numerator = (gcm / denominator) * numerator - (gcm / number.denominator) * number.numerator
        return Rational("$numerator/$gcm")
    }

    operator fun times(number: Rational): Rational {
        val numerator = numerator * number.numerator
        val denominator = denominator * number.denominator
        return Rational("$numerator/$denominator")
    }

    operator fun div(number: Rational): Rational {
        val numerator = numerator * number.denominator
        val denominator = denominator * number.numerator
        return Rational("$numerator/$denominator")
    }

    operator fun compareTo(number: Rational): Int {
        val ratio = this.numerator.toFloat() / this.denominator.toFloat()
        val numberRatio = number.numerator.toFloat() / number.denominator.toFloat()
        if (ratio > numberRatio) {
            return 1
        } else if (ratio == numberRatio) {
            return 0
        }
        return -1
    }

    operator fun unaryMinus(): Rational {
        val inverseNumerator = -numerator
        return Rational("$inverseNumerator/$denominator")
    }

    operator fun unaryPlus(): Rational {
        return Rational("$numerator/$denominator")
    }

    operator fun rangeTo(end: Rational): Any {
        var range: MutableList<Rational> = arrayListOf()
        val startNumerator = this.numerator.toInt()
        val endNumerator = end.numerator.toInt()
        var index = 0
        if (this.denominator == end.denominator) {
            for (i in startNumerator..endNumerator) {
                range.add(index, Rational("$i/$denominator"))
            }
        }
        return range
    }

    operator fun contains(number: Rational): Boolean {
        if (this.denominator % number.denominator == 0.toBigInteger()
                && this.numerator <= number.numerator) {
            return true
        }
        return false
    }

    override fun toString(): String {
        val gcd = numerator.gcd(denominator)
        return if (gcd != null) {
            val newNumerator = numerator / gcd
            val newDenominator = denominator / gcd
            "$newNumerator/$newDenominator"
        } else {
            "$numerator/$denominator"
        }
    }
}
infix fun Int.divBy(denominator: Int): Rational {
    if (denominator == 0) {
        throw Exception("denominator 0 not allowed")
    }

    return Rational("$this/$denominator")
}

infix fun Long.divBy(denominator: Long): Rational {
    if (denominator == 0L) {
        throw Exception("denominator 0 not allowed")
    }
    return Rational("$this/$denominator")
}

infix fun BigInteger.divBy(denominator: BigInteger): Rational {
    if (denominator == 0.toBigInteger()) {
        throw Exception("denominator 0 not allowed")
    }
    return Rational("$this/$denominator")
}

fun String.toRational(): Rational {
    return Rational(this)
}

And here is my main body that obviously still doesn't compile:

fun main() {
    val half = 1 divBy 2
    val third = 1 divBy 3
    val twoThirds = 2 divBy 3

    println(half in third..twoThirds) // this line does not compile beacause in operator is not defined for the class
}

I guess I have to override "rangeTo" operator but I'm uncertain about the operator prototype. I there somebody that can please help me to get to the right track?

2
  • 2
    As a matter of design, I'd suggest adding a constructor that takes a pair of BigIntegers. You can then do the GCD calculation there, instead of in toString(). Avoiding all the conversion of BigIntegers to Stringss and back again should save memory and time, as well as simplifying the code a bit. You'll probably also want to override equals() and hashCode(), along with operators inc(), and dec(). If you override toByte() &c, you can implement Number too. And why define compareTo() without implementing Comparable? — Yes, I've implemented a Rational class too :-) Commented Feb 12, 2019 at 23:24
  • This looks exactly like the Week 4 assignment for the Coursera course "Kotlin for Java Developers". I wish I hadn't found it. Commented Oct 28, 2022 at 18:44

3 Answers 3

7

The way to make in work is for the third..twoThirds call to return something that has a contains(Rational) method, which is what the in call translates to.

One way to do this is to return a ClosedRange<Rational> here, like so:

operator fun rangeTo(end: Rational): ClosedRange<Rational> {
    return object : ClosedRange<Rational> {
        override val endInclusive: Rational = end
        override val start: Rational = this@Rational
    }
}

This puts a type constraint on Rational, as a ClosedRange needs a Comparable implementation to be able to determine whether a value belongs in it. You can do this by implementing the Comparable interface, and then adding operator to your existing compareTo operator (plus it's a good practice to rename the parameter to match the interface):

data class Rational(val rational: String) : Comparable<Rational> {

    ...

    override operator fun compareTo(other: Rational): Int {
        val ratio = this.numerator.toFloat() / this.denominator.toFloat()
        val numberRatio = other.numerator.toFloat() / other.denominator.toFloat()
        if (ratio > numberRatio) {
            return 1
        } else if (ratio == numberRatio) {
            return 0
        }
        return -1
    }

}

You could also avoid the conversion to floats entirely by using this implementation instead, as suggested in the comment below by @gidds:

override operator fun compareTo(other: Rational): Int {
    return (numerator * other.denominator - denominator * other.numerator).signum()
}

Also, your current contains implementation could probably be discarded, as you no longer need it, and it functions rather oddly.


To add something other than the direct answer here: as @Eugene Petrenko suggested in their answer, it would be practical to add a couple constructors other than the one that uses a String, for example one that takes two Ints, and one that takes two BigIntegerss.

Sign up to request clarification or add additional context in comments.

3 Comments

compareTo() doesn't need to do any division or conversions at all: you could just do e.g. (numerator * other.denominator - denominator * other.numerator).sign.
Oops, of course! I'll add this to the answer itself as well, thank you!
Thanks for all your help :) everything works fine now!
0

The in operator is declared inverse. You need an extension function on the right side that takes the left side.

https://kotlinlang.org/docs/reference/operator-overloading.html#in

You miss an infix function divBy to allow turing Int into Rational, e.g.

infix fun Int.divBy(i: Int) = Rational("$this/$i")

Not the code like val half = 1 divBy 2 will work. Theoretically, it may make sense to add a constructor for Rational from Ints to avoid parsing.

There is an incorrect return type in rangeTo method in the Rational class, it should not be Any. It should be declared as

data class RationalRange(val left: Rational, val right: Rational) {
  operator fun contains(r: Rational) = left <= r && r <= right
}

operator fun rangeTo(end: Rational): RationalRange(this, end) 

Now the example with x in a..b should work.

UPD: added the RationalRange. I missed the point, sorry. You do not need contains function implemented for the Rational class at all.

The compareTo function of Rational is unlikely to use .toFloat() instead, you may implement that directly with integer numbers

3 Comments

rangeTo should most definitely not return a Rational, it makes no sense to then check if a Rational contains another.
@Eugene, thanks a lot. Makes more sense know. I have indeed defined infix functions but I forgot to cut & paste them, going to update the question
Thanks for all your help :) everything works fine now!
0

A straightforward solution is to implement the Comparable Interface in your class.

data class Rational(val rational: String) : Comparable<Rational>

Then implement the compareTo() function, with your comparison logic.

override fun compareTo(other: Rational): Int {
    //Normalize the numerators of each rational
    val thisNumerator = this.numerator * other.denominator
    val otherNumerator = other.numerator * this.denominator
    //Then compare them
    return when{
        thisNumerator > otherNumerator -> 1
        thisNumerator < otherNumerator -> -1
        else -> 0
    }
}

This will resolve the compile error without you needing to override the rangeTo() function with custom logic.

1 Comment

Use can simplify this: override fun compareTo(other: Rational): Int { val thisNumerator = numerator * other.denominator val otherNumerator = other.numerator * denominator return thisNumerator.compareTo(otherNumerator) }

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.