Skip to content

Commit

Permalink
Add Rational numbers support
Browse files Browse the repository at this point in the history
  • Loading branch information
romainguy committed Jun 8, 2022
1 parent d95234e commit 76510c8
Show file tree
Hide file tree
Showing 8 changed files with 639 additions and 7 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -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

Expand Down
4 changes: 2 additions & 2 deletions src/commonMain/kotlin/dev/romainguy/kotlin/math/Matrix.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 6 additions & 2 deletions src/commonMain/kotlin/dev/romainguy/kotlin/math/Quaternion.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
package dev.romainguy.kotlin.math

import kotlin.math.*

enum class QuaternionComponent {
X, Y, Z, W
}
Expand Down Expand Up @@ -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)
Expand Down
251 changes: 251 additions & 0 deletions src/commonMain/kotlin/dev/romainguy/kotlin/math/Rational.kt
Original file line number Diff line number Diff line change
@@ -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<Rational> {
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()
}
2 changes: 1 addition & 1 deletion src/commonMain/kotlin/dev/romainguy/kotlin/math/Scalar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 76510c8

Please sign in to comment.