From 76510c87cf20e919bbf6b66ae3cb6d7c5aa63c98 Mon Sep 17 00:00:00 2001 From: Romain Guy Date: Tue, 7 Jun 2022 17:30:34 -0700 Subject: [PATCH] Add Rational numbers support --- README.md | 20 + build.gradle.kts | 2 +- gradle.properties | 2 +- .../dev/romainguy/kotlin/math/Matrix.kt | 4 +- .../dev/romainguy/kotlin/math/Quaternion.kt | 8 +- .../dev/romainguy/kotlin/math/Rational.kt | 251 ++++++++++++ .../dev/romainguy/kotlin/math/Scalar.kt | 2 +- .../dev/romainguy/kotlin/math/RationalTest.kt | 357 ++++++++++++++++++ 8 files changed, 639 insertions(+), 7 deletions(-) create mode 100644 src/commonMain/kotlin/dev/romainguy/kotlin/math/Rational.kt create mode 100644 src/commonTest/kotlin/dev/romainguy/kotlin/math/RationalTest.kt diff --git a/README.md b/README.md index 8b64cf0..af78f82 100644 --- a/README.md +++ b/README.md @@ -214,3 +214,23 @@ rotationMatrix = rotation(quaternion = Quaternion(y = 1.0f, w = 1.0f)) // rotati The file `Scalar.kt` contains various helper methods to use common math operations with floats. It is intended to be used in combination with Kotlin 1.2's new float math methods. + +## Rational numbers + +This library provides simple support for rational numbers to avoid numerical imprecisions. The +current implementation is limited to 32 bits of storage for the numerator and the denominator. +The current implementation is also not written for speed. + +``` +val a = Rational(2, 5) // Representats 2/5 +val b = Rational(127) // Integers can be represented exactly +val c = Rational(0.25f) // Floats and doubles will be converted to a rational representation + +// The following operators are supported: +println(+a) +println(-a) +println(a + b) +println(a - b) +println(c * d) +println(c / d) +``` diff --git a/build.gradle.kts b/build.gradle.kts index 525892a..949f51b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.konan.target.HostManager import java.net.URL plugins { - kotlin("multiplatform") version "1.6.0" + kotlin("multiplatform") version "1.6.21" id("io.github.gradle-nexus.publish-plugin") version "1.1.0" id("org.jetbrains.dokka") version "1.6.0" id("maven-publish") diff --git a/gradle.properties b/gradle.properties index 033e10b..76757da 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=dev.romainguy -VERSION_NAME=1.3.0 +VERSION_NAME=1.4.0 POM_DESCRIPTION=Graphics oriented math library for Kotlin diff --git a/src/commonMain/kotlin/dev/romainguy/kotlin/math/Matrix.kt b/src/commonMain/kotlin/dev/romainguy/kotlin/math/Matrix.kt index d0c3d3b..4a0646b 100644 --- a/src/commonMain/kotlin/dev/romainguy/kotlin/math/Matrix.kt +++ b/src/commonMain/kotlin/dev/romainguy/kotlin/math/Matrix.kt @@ -682,8 +682,8 @@ fun eulerAngles(m: Mat4, order: RotationsOrder = RotationsOrder.ZYX): Float3 { RotationsOrder.ZXY -> { this[order.pitch] = asin(clamp(m.y.z, -1.0f, 1.0f)) if (abs(m.y.z) < 0.9999999f) { - this[order.roll] = atan2(-m.x.z, m.z.z); - this[order.yaw] = atan2(-m.y.x, m.y.y); + this[order.roll] = atan2(-m.x.z, m.z.z) + this[order.yaw] = atan2(-m.y.x, m.y.y) } else { this[order.roll] = 0.0f this[order.yaw] = atan2(m.x.y, m.x.x) diff --git a/src/commonMain/kotlin/dev/romainguy/kotlin/math/Quaternion.kt b/src/commonMain/kotlin/dev/romainguy/kotlin/math/Quaternion.kt index 472dc41..71ded13 100644 --- a/src/commonMain/kotlin/dev/romainguy/kotlin/math/Quaternion.kt +++ b/src/commonMain/kotlin/dev/romainguy/kotlin/math/Quaternion.kt @@ -19,7 +19,6 @@ package dev.romainguy.kotlin.math import kotlin.math.* - enum class QuaternionComponent { X, Y, Z, W } @@ -81,7 +80,12 @@ data class Quaternion( * Default is [RotationsOrder.ZYX] which means that the object will first be rotated around its Z * axis, then its Y axis and finally its X axis. */ - fun fromEuler(yaw: Float = 0.0f, pitch: Float = 0.0f, roll: Float = 0.0f, order: RotationsOrder = RotationsOrder.ZYX): Quaternion { + fun fromEuler( + yaw: Float = 0.0f, + pitch: Float = 0.0f, + roll: Float = 0.0f, + order: RotationsOrder = RotationsOrder.ZYX + ): Quaternion { val c1 = cos(yaw * 0.5f) val s1 = sin(yaw * 0.5f) val c2 = cos(pitch * 0.5f) diff --git a/src/commonMain/kotlin/dev/romainguy/kotlin/math/Rational.kt b/src/commonMain/kotlin/dev/romainguy/kotlin/math/Rational.kt new file mode 100644 index 0000000..54917e6 --- /dev/null +++ b/src/commonMain/kotlin/dev/romainguy/kotlin/math/Rational.kt @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2017 Romain Guy + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("NOTHING_TO_INLINE") + +package dev.romainguy.kotlin.math + +import kotlin.jvm.JvmInline +import kotlin.math.abs +import kotlin.math.sign + +fun Rational(value: Double) = pack(value) + +fun Rational(value: Float) = pack(value.toDouble()) + +fun Rational(value: Int) = Rational(pack(value, 1)) + +fun Rational(numerator: Int, denominator: Int) = Rational(pack(numerator, denominator)) + +@JvmInline +value class Rational(private val r: Long) : Comparable { + companion object { + val NaN = Rational(0, 0) + val POSITIVE_INFINITY = Rational(1, 0) + val NEGATIVE_INFINITY = Rational(-1, 0) + val ZERO = Rational(0, 1) + } + + val sign: Int + get() = if ((r ushr 32).toInt() < 0) -1 else 1 + val numerator: Int + get() = (r ushr 32).toInt() + val denominator: Int + get() = (r and 0xFFFFFFFFL).toInt() + + fun component1() = numerator + + fun component2() = denominator + + fun isNaN() = r == 0L + + fun isFinite() = denominator != 0 + + fun isInfinite() = numerator != 0 && denominator == 0 + + fun isZero() = numerator == 0 && denominator != 0 + + fun toDouble() = numerator.toDouble() / denominator.toDouble() + + fun toFloat() = numerator.toFloat() / denominator.toFloat() + + fun toInt() = when { + isNaN() -> 0 + isInfinite() -> if (sign > 0) Int.MAX_VALUE else Int.MIN_VALUE + else -> numerator / denominator + } + + fun toLong() = when { + isNaN() -> 0L + isInfinite() -> if (sign > 0) Long.MAX_VALUE else Long.MIN_VALUE + else -> numerator.toLong() / denominator.toLong() + } + + operator fun unaryMinus(): Rational { + return Rational(-numerator, denominator) + } + + operator fun unaryPlus() = Rational(r) + + operator fun plus(other: Rational): Rational { + if (r == 0L || other.r == 0L) return NaN + + var n = numerator.toLong() + var d = denominator.toLong() + + // Infinite + if (n != 0L && d == 0L) return this + + val on = other.numerator.toLong() + val od = other.denominator.toLong() + + // Infinite + if (on != 0L && od == 0L) return this + + val a = n * od + val b = d * on + + n = a + b + d *= od + + val gcd = gcd(n, d) + n /= gcd + d /= gcd + + return Rational((n shl 32) or d) + } + + operator fun minus(other: Rational) = this + (-other) + + operator fun times(other: Rational): Rational { + if (r == 0L || other.r == 0L) return NaN + + var n = numerator.toLong() + var d = denominator.toLong() + + // Infinite + if (n != 0L && d == 0L) return this + + val on = other.numerator.toLong() + val od = other.denominator.toLong() + + // Infinite + if (on != 0L && od == 0L) return this + + n *= on + d *= od + + val gcd = gcd(n, d) + n /= gcd + d /= gcd + + return Rational((n shl 32) or d) + } + + operator fun div(other: Rational): Rational { + if (r == 0L || other.r == 0L) return NaN + + var n = numerator.toLong() + var d = denominator.toLong() + + // Infinite + if (n != 0L && d == 0L) return this + + // Swap for division + val on = other.denominator.toLong() + val od = other.numerator.toLong() + + // Division by infinity + if (on == 0L && od != 0L) return ZERO + + n *= on + d *= od + + val gcd = gcd(n, d) + n /= gcd + d /= gcd + + return Rational((n shl 32) or d) + } + + override fun compareTo(other: Rational): Int { + if (r == other.r) return 0 + return when { + isNaN() -> 1 + other.isNaN() -> -1 + isInfinite() && other.isInfinite() -> if (sign > other.sign) 1 else -1 + else -> { + val a = numerator.toLong() * other.denominator.toLong() + val b = other.numerator.toLong() * denominator.toLong() + return if (a > b) 1 else -1 + } + } + } + + override fun toString() = when { + isNaN() -> "NaN" + isInfinite() -> "${if (sign > 0) "+" else "-"}Infinity" + isZero() -> "${if (sign < 0) "-" else ""}0" + else -> if (denominator == 1) "$numerator" else "$numerator/$denominator" + } +} + +// Note: we should use a binary implementation of GCD instead of relying on modulo +private tailrec fun gcd(a: Int, b: Int): Int = if (b == 0) abs(a) else gcd(b, a % b) +private tailrec fun gcd(a: Long, b: Long): Long = if (b == 0L) abs(a) else gcd(b, a % b) + +private fun pack(value: Double) = when { + value.isNaN() -> Rational.NaN + value.isInfinite() -> if (value > 0.0) Rational.POSITIVE_INFINITY else Rational.NEGATIVE_INFINITY + value == 0.0 -> if (sign(value) > 0.0) Rational.ZERO else -Rational.ZERO + else -> { + val bits = value.toRawBits() + val sign = bits ushr 63 + val exponent = ((bits ushr 52) xor (sign shl 11)) - 1023L + val fraction = bits shl 12 + + var n = 1L + var d = 1L + + var i = 63 + while (i >= 12) { + n = n * 2L + ((fraction ushr i) and 1L) + d *= 2L + i-- + } + + if (exponent > 0) { + n *= 1L shl exponent.toInt() + } else { + d *= 1L shl -exponent.toInt() + } + + if (sign == 1L) n *= -1 + + // Simplify before clamping to Int + val gcd = gcd(n, d) + n /= gcd + d /= gcd + + // TODO: We should skip this and finish the packing ourselves to avoid another GCD + Rational(n.toInt(), d.toInt()) + } +} + +private fun pack(numerator: Int, denominator: Int): Long { + var n = numerator + var d = denominator + + // Normalize sign + if (d < 0) { + n = -n + d = -d + } + + if (d == 0) { + if (n > 0) n = 1 // +Inf + else if (n < 0) n = -1 // -Inf + else n = 0 // NaN + } else if (n == 0) { + d = 1 // 0 + } else { + val gcd = gcd(n, d) // Common case + n /= gcd + d /= gcd + } + + return (n.toLong() shl 32) or d.toLong() +} diff --git a/src/commonMain/kotlin/dev/romainguy/kotlin/math/Scalar.kt b/src/commonMain/kotlin/dev/romainguy/kotlin/math/Scalar.kt index f18e0b2..dd1d79d 100644 --- a/src/commonMain/kotlin/dev/romainguy/kotlin/math/Scalar.kt +++ b/src/commonMain/kotlin/dev/romainguy/kotlin/math/Scalar.kt @@ -20,7 +20,7 @@ package dev.romainguy.kotlin.math import kotlin.math.pow -const val PI = 3.1415926536f +const val PI = 3.1415927f const val HALF_PI = PI * 0.5f const val TWO_PI = PI * 2.0f const val FOUR_PI = PI * 4.0f diff --git a/src/commonTest/kotlin/dev/romainguy/kotlin/math/RationalTest.kt b/src/commonTest/kotlin/dev/romainguy/kotlin/math/RationalTest.kt new file mode 100644 index 0000000..d5466ce --- /dev/null +++ b/src/commonTest/kotlin/dev/romainguy/kotlin/math/RationalTest.kt @@ -0,0 +1,357 @@ +/* + * Copyright (C) 2017 Romain Guy + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.romainguy.kotlin.math + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class RationalTest { + @Test + fun fromInt() { + assertEquals(0, Rational(0).numerator) + assertEquals(1, Rational(0).denominator) + + assertEquals(2, Rational(2).numerator) + assertEquals(1, Rational(2).denominator) + + assertEquals(-2, Rational(-2).numerator) + assertEquals(1, Rational(-2).denominator) + + assertEquals(Int.MAX_VALUE, Rational(Int.MAX_VALUE).numerator) + assertEquals(1, Rational(Int.MAX_VALUE).denominator) + + assertEquals(Int.MIN_VALUE, Rational(Int.MIN_VALUE).numerator) + assertEquals(1, Rational(Int.MIN_VALUE).denominator) + } + + @Test + fun fromFloat() { + assertEquals(0, Rational(0.0f).numerator) + assertEquals(1, Rational(0.0f).denominator) + + assertEquals(0, Rational(-0.0f).numerator) + assertEquals(1, Rational(-0.0f).denominator) + + assertEquals(2, Rational(2.0f).numerator) + assertEquals(1, Rational(2.0f).denominator) + + assertEquals(-2, Rational(-2.0f).numerator) + assertEquals(1, Rational(-2.0f).denominator) + + assertEquals(1, Rational(0.25f).numerator) + assertEquals(4, Rational(0.25f).denominator) + + assertEquals(14913081, Rational(3.0f / 27.0f).numerator) + assertEquals(134217728, Rational(3.0f / 27.0f).denominator) + + assertEquals(Rational.NaN, Rational(Float.NaN)) + assertEquals(Rational.POSITIVE_INFINITY, Rational(Float.POSITIVE_INFINITY)) + assertEquals(Rational.NEGATIVE_INFINITY, Rational(Float.NEGATIVE_INFINITY)) + } + + @Test + fun fromDouble() { + assertEquals(0, Rational(0.0).numerator) + assertEquals(1, Rational(0.0).denominator) + + assertEquals(0, Rational(-0.0).numerator) + assertEquals(1, Rational(-0.0).denominator) + + assertEquals(2, Rational(2.0).numerator) + assertEquals(1, Rational(2.0).denominator) + + assertEquals(-2, Rational(-2.0).numerator) + assertEquals(1, Rational(-2.0).denominator) + + assertEquals(1, Rational(0.25).numerator) + assertEquals(4, Rational(0.25).denominator) + + assertEquals(-1, Rational(3.0 / 27.0).numerator) + assertEquals(0, Rational(3.0 / 27.0).denominator) + + assertEquals(Rational.NaN, Rational(Double.NaN)) + assertEquals(Rational.POSITIVE_INFINITY, Rational(Double.POSITIVE_INFINITY)) + assertEquals(Rational.NEGATIVE_INFINITY, Rational(Double.NEGATIVE_INFINITY)) + } + + @Test + fun isNaN() { + assertTrue(Rational.NaN.isNaN()) + assertFalse(Rational.POSITIVE_INFINITY.isNaN()) + assertFalse(Rational.NEGATIVE_INFINITY.isNaN()) + assertFalse(Rational.ZERO.isNaN()) + assertFalse(Rational(2, 5).isNaN()) + } + + @Test + fun isFinite() { + assertFalse(Rational.NaN.isFinite()) + assertFalse(Rational.POSITIVE_INFINITY.isFinite()) + assertFalse(Rational.NEGATIVE_INFINITY.isFinite()) + assertTrue(Rational.ZERO.isFinite()) + assertTrue(Rational(2, 5).isFinite()) + } + + @Test + fun isInfinite() { + assertFalse(Rational.NaN.isInfinite()) + assertTrue(Rational.POSITIVE_INFINITY.isInfinite()) + assertTrue(Rational.NEGATIVE_INFINITY.isInfinite()) + assertFalse(Rational.ZERO.isInfinite()) + assertFalse(Rational(2, 5).isInfinite()) + } + + @Test + fun isZero() { + assertFalse(Rational.NaN.isZero()) + assertFalse(Rational.POSITIVE_INFINITY.isZero()) + assertFalse(Rational.NEGATIVE_INFINITY.isZero()) + assertTrue(Rational.ZERO.isZero()) + assertFalse(Rational(2, 5).isZero()) + } + + @Test + fun sign() { + assertEquals(1, Rational(2, 5).sign) + assertEquals(-1, Rational(-2, 5).sign) + assertEquals(-1, Rational(2, -5).sign) + assertEquals(1, Rational(-2, -5).sign) + assertEquals(1, Rational.POSITIVE_INFINITY.sign) + assertEquals(-1, Rational.NEGATIVE_INFINITY.sign) + assertEquals(1, Rational.ZERO.sign) + assertEquals(1, Rational.NaN.sign) + } + + @Test + fun numerator() { + assertEquals(2, Rational(2, 5).numerator) + assertEquals(-2, Rational(2, -5).numerator) + assertEquals(-279, Rational(-279, 51233).numerator) + assertEquals(279, Rational(-279, -51233).numerator) + assertEquals(2, Rational(4, 30).numerator) + + assertEquals(1, Rational.POSITIVE_INFINITY.numerator) + assertEquals(-1, Rational.NEGATIVE_INFINITY.numerator) + assertEquals(0, Rational.ZERO.numerator) + assertEquals(0, Rational.NaN.numerator) + } + + @Test + fun denominator() { + assertEquals(5, Rational(2, 5).denominator) + assertEquals(51233, Rational(-279, 51233).denominator) + assertEquals(5, Rational(2, -5).denominator) + assertEquals(5, Rational(-2, -5).denominator) + assertEquals(15, Rational(4, 30).denominator) + + assertEquals(0, Rational.POSITIVE_INFINITY.denominator) + assertEquals(0, Rational.NEGATIVE_INFINITY.denominator) + assertEquals(1, Rational.ZERO.denominator) + assertEquals(0, Rational.NaN.denominator) + } + + @Test + fun string() { + assertEquals("NaN", Rational.NaN.toString()) + assertEquals("+Infinity", Rational.POSITIVE_INFINITY.toString()) + assertEquals("-Infinity", Rational.NEGATIVE_INFINITY.toString()) + assertEquals("0", Rational.ZERO.toString()) + assertEquals("2/5", Rational(2, 5).toString()) + assertEquals("2", Rational(2).toString()) + } + + @Test + fun toDouble() { + assertEquals(Double.NaN, Rational.NaN.toDouble()) + assertEquals(Double.POSITIVE_INFINITY, Rational.POSITIVE_INFINITY.toDouble()) + assertEquals(Double.NEGATIVE_INFINITY, Rational.NEGATIVE_INFINITY.toDouble()) + assertEquals(0.0, Rational.ZERO.toDouble()) + assertEquals(2.0 / 5.0, Rational(2, 5).toDouble()) + } + + @Test + fun toFloat() { + assertEquals(Float.NaN, Rational.NaN.toFloat()) + assertEquals(Float.POSITIVE_INFINITY, Rational.POSITIVE_INFINITY.toFloat()) + assertEquals(Float.NEGATIVE_INFINITY, Rational.NEGATIVE_INFINITY.toFloat()) + assertEquals(0.0f, Rational.ZERO.toFloat()) + assertEquals(2.0f / 5.0f, Rational(2, 5).toFloat()) + } + + @Test + fun toInt() { + assertEquals(0, Rational.NaN.toInt()) + assertEquals(Int.MAX_VALUE, Rational.POSITIVE_INFINITY.toInt()) + assertEquals(Int.MIN_VALUE, Rational.NEGATIVE_INFINITY.toInt()) + assertEquals(0, Rational.ZERO.toInt()) + assertEquals(2 / 5, Rational(2, 5).toInt()) + } + + @Test + fun toLong() { + assertEquals(0L, Rational.NaN.toLong()) + assertEquals(Long.MAX_VALUE, Rational.POSITIVE_INFINITY.toLong()) + assertEquals(Long.MIN_VALUE, Rational.NEGATIVE_INFINITY.toLong()) + assertEquals(0L, Rational.ZERO.toLong()) + assertEquals(2L / 5L, Rational(2, 5).toLong()) + } + + @Test + fun unaryPlus() { + assertEquals(Rational.NaN, +Rational.NaN) + assertEquals(Rational.POSITIVE_INFINITY, +Rational.POSITIVE_INFINITY) + assertEquals(Rational.NEGATIVE_INFINITY, +Rational.NEGATIVE_INFINITY) + assertEquals(Rational.ZERO, +Rational.ZERO) + assertEquals(Rational(2, 5), +Rational(2, 5)) + assertEquals(Rational(-2, 5), +Rational(-2, 5)) + assertEquals(Rational(-2, 5), +Rational(2, -5)) + assertEquals(Rational(2, 5), +Rational(-2, -5)) + } + + @Test + fun unaryMinus() { + assertEquals(Rational.NaN, -Rational.NaN) + assertEquals(Rational.NEGATIVE_INFINITY, -Rational.POSITIVE_INFINITY) + assertEquals(Rational.POSITIVE_INFINITY, -Rational.NEGATIVE_INFINITY) + assertTrue((-Rational.ZERO).isZero()) + assertEquals(Rational(-2, 5), -Rational(2, 5)) + assertEquals(Rational(2, 5), -Rational(-2, 5)) + assertEquals(Rational(2, 5), -Rational(2, -5)) + assertEquals(Rational(-2, 5), -Rational(-2, -5)) + } + + @Test + fun addition() { + assertEquals(Rational.NaN, Rational.NaN + Rational.NaN) + assertEquals(Rational.NaN, Rational.NaN + Rational.POSITIVE_INFINITY) + assertEquals(Rational.NaN, Rational.NaN + Rational.NEGATIVE_INFINITY) + assertEquals(Rational.NaN, Rational.NaN + Rational.ZERO) + assertEquals(Rational.NaN, Rational.NaN + Rational(2, 5)) + + assertEquals(Rational.POSITIVE_INFINITY, Rational.POSITIVE_INFINITY + Rational.POSITIVE_INFINITY) + assertEquals(Rational.POSITIVE_INFINITY, Rational.POSITIVE_INFINITY + Rational.NEGATIVE_INFINITY) + assertEquals(Rational.POSITIVE_INFINITY, Rational.POSITIVE_INFINITY + Rational.ZERO) + assertEquals(Rational.POSITIVE_INFINITY, Rational.POSITIVE_INFINITY + Rational(2, 5)) + + assertEquals(Rational.NEGATIVE_INFINITY, Rational.NEGATIVE_INFINITY + Rational.POSITIVE_INFINITY) + assertEquals(Rational.NEGATIVE_INFINITY, Rational.NEGATIVE_INFINITY + Rational.NEGATIVE_INFINITY) + assertEquals(Rational.NEGATIVE_INFINITY, Rational.NEGATIVE_INFINITY + Rational.ZERO) + assertEquals(Rational.NEGATIVE_INFINITY, Rational.NEGATIVE_INFINITY + Rational(2, 5)) + + assertEquals(Rational(2, 5), Rational(2, 5) + Rational.ZERO) + assertEquals(Rational(3, 5), Rational(2, 5) + Rational(1, 5)) + assertEquals(Rational(1, 5), Rational(-2, 5) + Rational(3, 5)) + assertEquals(Rational(-4, 5), Rational(-2, 5) + Rational(-2, 5)) + } + + @Test + fun subtraction() { + assertEquals(Rational.NaN, Rational.NaN - Rational.NaN) + assertEquals(Rational.NaN, Rational.NaN - Rational.POSITIVE_INFINITY) + assertEquals(Rational.NaN, Rational.NaN - Rational.NEGATIVE_INFINITY) + assertEquals(Rational.NaN, Rational.NaN - Rational.ZERO) + assertEquals(Rational.NaN, Rational.NaN - Rational(2, 5)) + + assertEquals(Rational.POSITIVE_INFINITY, Rational.POSITIVE_INFINITY - Rational.POSITIVE_INFINITY) + assertEquals(Rational.POSITIVE_INFINITY, Rational.POSITIVE_INFINITY - Rational.NEGATIVE_INFINITY) + assertEquals(Rational.POSITIVE_INFINITY, Rational.POSITIVE_INFINITY - Rational.ZERO) + assertEquals(Rational.POSITIVE_INFINITY, Rational.POSITIVE_INFINITY - Rational(2, 5)) + + assertEquals(Rational.NEGATIVE_INFINITY, Rational.NEGATIVE_INFINITY - Rational.POSITIVE_INFINITY) + assertEquals(Rational.NEGATIVE_INFINITY, Rational.NEGATIVE_INFINITY - Rational.NEGATIVE_INFINITY) + assertEquals(Rational.NEGATIVE_INFINITY, Rational.NEGATIVE_INFINITY - Rational.ZERO) + assertEquals(Rational.NEGATIVE_INFINITY, Rational.NEGATIVE_INFINITY - Rational(2, 5)) + + assertEquals(Rational(2, 5), Rational(2, 5) - Rational.ZERO) + assertEquals(Rational(1, 5), Rational(2, 5) - Rational(1, 5)) + assertEquals(Rational(-1, 5), Rational(2, 5) - Rational(3, 5)) + assertEquals(Rational(-4, 5), Rational(-2, 5) - Rational(2, 5)) + assertEquals(Rational(1, 5), Rational(-2, 5) - Rational(-3, 5)) + } + + @Test + fun multiplication() { + assertEquals(Rational.NaN, Rational.NaN * Rational.NaN) + assertEquals(Rational.NaN, Rational.NaN * Rational.POSITIVE_INFINITY) + assertEquals(Rational.NaN, Rational.NaN * Rational.NEGATIVE_INFINITY) + assertEquals(Rational.NaN, Rational.NaN * Rational.ZERO) + assertEquals(Rational.NaN, Rational.NaN * Rational(2, 5)) + + assertEquals(Rational.POSITIVE_INFINITY, Rational.POSITIVE_INFINITY * Rational.POSITIVE_INFINITY) + assertEquals(Rational.POSITIVE_INFINITY, Rational.POSITIVE_INFINITY * Rational.NEGATIVE_INFINITY) + assertEquals(Rational.POSITIVE_INFINITY, Rational.POSITIVE_INFINITY * Rational.ZERO) + assertEquals(Rational.POSITIVE_INFINITY, Rational.POSITIVE_INFINITY * Rational(2, 5)) + + assertEquals(Rational.NEGATIVE_INFINITY, Rational.NEGATIVE_INFINITY * Rational.POSITIVE_INFINITY) + assertEquals(Rational.NEGATIVE_INFINITY, Rational.NEGATIVE_INFINITY * Rational.NEGATIVE_INFINITY) + assertEquals(Rational.NEGATIVE_INFINITY, Rational.NEGATIVE_INFINITY * Rational.ZERO) + assertEquals(Rational.NEGATIVE_INFINITY, Rational.NEGATIVE_INFINITY * Rational(2, 5)) + + assertEquals(Rational.ZERO, Rational(2, 5) * Rational.ZERO) + assertEquals(Rational(4, 25), Rational(2, 5) * Rational(2, 5)) + assertEquals(Rational(-6, 25), Rational(2, 5) * Rational(-3, 5)) + assertEquals(Rational(6, 25), Rational(-2, 5) * Rational(-3, 5)) + } + + @Test + fun division() { + assertEquals(Rational.NaN, Rational.NaN / Rational.NaN) + assertEquals(Rational.NaN, Rational.NaN / Rational.POSITIVE_INFINITY) + assertEquals(Rational.NaN, Rational.NaN / Rational.NEGATIVE_INFINITY) + assertEquals(Rational.NaN, Rational.NaN / Rational.ZERO) + assertEquals(Rational.NaN, Rational.NaN / Rational(2, 5)) + + assertEquals(Rational.POSITIVE_INFINITY, Rational.POSITIVE_INFINITY / Rational.POSITIVE_INFINITY) + assertEquals(Rational.POSITIVE_INFINITY, Rational.POSITIVE_INFINITY / Rational.NEGATIVE_INFINITY) + assertEquals(Rational.POSITIVE_INFINITY, Rational.POSITIVE_INFINITY / Rational.ZERO) + assertEquals(Rational.POSITIVE_INFINITY, Rational.POSITIVE_INFINITY / Rational(2, 5)) + + assertEquals(Rational.NEGATIVE_INFINITY, Rational.NEGATIVE_INFINITY / Rational.POSITIVE_INFINITY) + assertEquals(Rational.NEGATIVE_INFINITY, Rational.NEGATIVE_INFINITY / Rational.NEGATIVE_INFINITY) + assertEquals(Rational.NEGATIVE_INFINITY, Rational.NEGATIVE_INFINITY / Rational.ZERO) + assertEquals(Rational.NEGATIVE_INFINITY, Rational.NEGATIVE_INFINITY / Rational(2, 5)) + + assertEquals(Rational.POSITIVE_INFINITY, Rational(2, 5) / Rational.ZERO) + assertEquals(Rational.ZERO, Rational(2, 5) / Rational.POSITIVE_INFINITY) + assertEquals(Rational.ZERO, Rational(2, 5) / Rational.NEGATIVE_INFINITY) + assertEquals(Rational.ZERO, Rational.ZERO / Rational(2, 5)) + assertEquals(Rational(-6, 25), Rational(2, 5) * Rational(-3, 5)) + } + + @Test + fun compareTo() { + assertEquals(0, Rational.NaN.compareTo(Rational.NaN)) + assertEquals(0, Rational.POSITIVE_INFINITY.compareTo(Rational.POSITIVE_INFINITY)) + assertEquals(0, Rational.NEGATIVE_INFINITY.compareTo(Rational.NEGATIVE_INFINITY)) + assertEquals(0, Rational.ZERO.compareTo(Rational.ZERO)) + assertEquals(0, Rational(2, 5).compareTo(Rational(2, 5))) + assertEquals(0, Rational(-2, 5).compareTo(Rational(-2, 5))) + + assertEquals(1, Rational(2, 5).compareTo(Rational(-2, 5))) + assertEquals(-1, Rational(-2, 5).compareTo(Rational(2, 5))) + + assertEquals(1, Rational(2, 5).compareTo(Rational.NEGATIVE_INFINITY)) + assertEquals(1, Rational(-2, 5).compareTo(Rational.NEGATIVE_INFINITY)) + assertEquals(-1, Rational(2, 5).compareTo(Rational.POSITIVE_INFINITY)) + assertEquals(-1, Rational(-2, 5).compareTo(Rational.POSITIVE_INFINITY)) + + assertEquals(-1, Rational(1, 4).compareTo(Rational(3, 5))) + assertEquals(1, Rational(3, 4).compareTo(Rational(3, 5))) + } +} \ No newline at end of file