I'm working on a Kotlin project that needs to work with 'rational' numbers. To do that I have a class that stores a rational number as two integers (numerator and denominator) and performs some arithmetic using those values. The problem I am running into is that one of the requirements is that the values be normalized. An incoming value of 2/4 should be stored as 1/2, 6/8 should be 1/4, and so on. I also want to make this a 'data class' so I can take advantage of the built-in equals and hashcode functions.
My class looks like this:
data class Rational(val numerator: BigInteger, val denominator: BigInteger): Comparable<Rational>
{
init {
if(denominator.equals(0))
{
throw IllegalArgumentException("0 is not a valid denominator")
}
//normalize the value by reducing to the least common denominator
val gcd = numerator.gcd(denominator)
this.numerator = numerator.div(gcd)
this.denominator = denominator.div(gcd)
}
}
infix fun BigInteger.divBy(d: BigInteger): Rational
{
return Rational(this, d)
}
fun main() {
val half = 2 divBy 4
println(half) // expected: Rational(numerator=1, denominator=2)
}
which doesn't compile since the parameters are 'val'. I don't want to make the properties mutable, but I'm not sure how else to do the calculation before setting the value. I can't remove the modifiers because they are required for a data class.
What is the procedure for initializing properties that need to be processed before the values are set? The only answer I have been able to find so far is this: How to store temporary variable while initializing a Kotlin object? which appears to be for an old (pre 1.0) version of kotlin.
I believe this is not directly possible. By design, a data class is a simple data holder. If we pass a value to it, we expect it to hold exactly this value. Transforming the value on-the-fly may be considered an unexpected behavior. Maybe it would be better to provide a normalized()
member function or a factory function?
Having said that, we can cheat it a little by hiding the primary constructor and providing invoke()
operator for the companion object:
fun main() {
println(Rational(4.toBigInteger(), 8.toBigInteger())) // Rational(numerator=1, denominator=2)
}
data class Rational private constructor(val numerator: BigInteger, val denominator: BigInteger) {
companion object {
operator fun invoke(numerator: BigInteger, denominator: BigInteger): Rational {
if(denominator.equals(0))
{
throw IllegalArgumentException("0 is not a valid denominator")
}
//normalize the value by reducing to the least common denominator
val gcd = numerator.gcd(denominator)
return Rational(numerator.div(gcd), denominator.div(gcd))
}
}
}
Of course, this is not entirely what you asked for. We don't use the constructor here, but a function which looks like a constructor. Also, we need to remember about the copy()
function which doesn't do normalization and I believe we can't overwrite it.