diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/core/Fraction.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/core/Fraction.kt index 7cfda1e2a..7ebdca0a7 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/core/Fraction.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/core/Fraction.kt @@ -43,7 +43,10 @@ inline fun invertCompare(int: Int): Int { return if (int < 0) 1 else -1 } +private val BI_MINUS_ZERO = BigInteger("-0") + @JvmRecord +@Suppress("unused") data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @JvmField val divisor: BigInteger = BigInteger.ONE, @JvmField val compact: Boolean = true) { @JvmOverloads constructor(value: Long, compact: Boolean = true) : this(BigInteger.valueOf(value), compact = compact) @JvmOverloads constructor(value: Int, compact: Boolean = true) : this(BigInteger.valueOf(value.toLong()), compact = compact) @@ -69,6 +72,9 @@ data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @ val c = divisor.signum() if (a != b && a != c) { + if (divisor == BigInteger.ZERO || divisor == BI_MINUS_ZERO) + return Fraction(-value, -divisor) + val mod = value % divisor if (mod == BigInteger.ZERO) @@ -77,6 +83,9 @@ data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @ return Fraction(-value, -divisor) } + if (divisor == BigInteger.ZERO || divisor == BI_MINUS_ZERO) + return this + val mod = value % divisor if (mod == BigInteger.ZERO) @@ -86,13 +95,19 @@ data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @ } fun isZero(): Boolean { + if (isNaN()) return false return value == BigInteger.ZERO } fun compact(): Fraction { + if (isNaN()) return this + if (value == BigInteger.ZERO || value == BigInteger.ONE || divisor == BigInteger.ONE) return this + if (divisor == BigInteger.ZERO || divisor == BI_MINUS_ZERO) + return this + val mod = value % divisor if (mod == BigInteger.ZERO) @@ -102,6 +117,8 @@ data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @ } fun canonize(): Fraction { + if (isNaN()) return this + if (value == BigInteger.ZERO || value == BigInteger.ONE || divisor == BigInteger.ONE || divisor == BigInteger.ZERO) return this @@ -120,6 +137,8 @@ data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @ if (other == null) return false + if (isNaN() || other.isNaN()) return false + val a = compact() val b = other.compact() @@ -127,6 +146,8 @@ data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @ } operator fun compareTo(other: Fraction): Int { + if (isNaN() || other.isNaN()) return 0 + if (divisor == other.divisor) return value.compareTo(other.value) @@ -156,7 +177,9 @@ data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @ return cmp } - fun plusCompact(other: Fraction): Fraction { + private fun plusCompact(other: Fraction): Fraction { + if (isNaN()) return this + if (divisor == other.divisor) { if (divisor == BigInteger.ONE) return Fraction(value + other.value) @@ -166,6 +189,9 @@ data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @ if (new.compareTo(BigInteger.ZERO) == 0) return ZERO + if (divisor == BigInteger.ZERO || divisor == BI_MINUS_ZERO) + return Fraction(new, divisor) + val mod = new % divisor if (mod == BigInteger.ZERO) @@ -176,6 +202,10 @@ data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @ val new = value * other.divisor + other.value * divisor val div = divisor * other.divisor + + if (div == BigInteger.ZERO || div == BI_MINUS_ZERO) + return Fraction(new, div) + val mod = new % div if (mod == BigInteger.ZERO) @@ -185,6 +215,9 @@ data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @ } operator fun plus(other: Fraction): Fraction { + if (isNaN()) return this + if (other.isNaN()) return other + if (compact) return plusCompact(other) @@ -195,7 +228,7 @@ data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @ return Fraction(value * other.divisor + other.value * divisor, divisor * other.divisor, compact = false) } - fun minusCompact(other: Fraction): Fraction { + private fun minusCompact(other: Fraction): Fraction { if (divisor == other.divisor) { if (divisor == BigInteger.ONE) return Fraction(value - other.value) @@ -205,6 +238,9 @@ data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @ if (new.compareTo(BigInteger.ZERO) == 0) return ZERO + if (divisor == BigInteger.ZERO || divisor == BI_MINUS_ZERO) + return Fraction(new, divisor) + val mod = new % divisor if (mod == BigInteger.ZERO) @@ -215,6 +251,10 @@ data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @ val new = value * other.divisor - other.value * divisor val div = divisor * other.divisor + + if (div == BigInteger.ZERO || div == BI_MINUS_ZERO) + return Fraction(new, div) + val mod = new % div if (mod == BigInteger.ZERO) @@ -224,6 +264,9 @@ data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @ } operator fun minus(other: Fraction): Fraction { + if (isNaN()) return this + if (other.isNaN()) return other + if (compact) return minusCompact(other) @@ -234,13 +277,17 @@ data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @ return Fraction(value * other.divisor - other.value * divisor, divisor * other.divisor, compact = false) } - fun timesCompact(other: Fraction): Fraction { + private fun timesCompact(other: Fraction): Fraction { val new = value * other.value if (new.compareTo(BigInteger.ZERO) == 0) return ZERO val div = divisor * other.divisor + + if (div == BigInteger.ZERO || div == BI_MINUS_ZERO) + return Fraction(new, div) + val mod = new % div if (mod == BigInteger.ZERO) @@ -250,19 +297,26 @@ data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @ } operator fun times(other: Fraction): Fraction { + if (isNaN()) return this + if (other.isNaN()) return other + if (compact) return timesCompact(other) return Fraction(value * other.value, divisor * other.divisor, compact = false) } - fun divCompact(other: Fraction): Fraction { + private fun divCompact(other: Fraction): Fraction { val new = value * other.divisor if (new.compareTo(BigInteger.ZERO) == 0) return ZERO val div = divisor * other.value + + if (div == BigInteger.ZERO || div == BI_MINUS_ZERO) + return Fraction(new, div) + val mod = new % div if (mod == BigInteger.ZERO) @@ -272,6 +326,9 @@ data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @ } operator fun div(other: Fraction): Fraction { + if (isNaN()) return this + if (other.isNaN()) return other + if (compact) return divCompact(other) @@ -279,6 +336,7 @@ data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @ } operator fun unaryMinus(): Fraction { + if (isNaN()) return this return Fraction(-value, divisor) } @@ -347,19 +405,27 @@ data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @ */ operator fun rem(other: Fraction): Fraction { + if (isNaN()) return this + if (other.isNaN()) return other return Fraction((this / other).wholePart()) } // Преобразования fun toFloat(): Float { + if (isNaN()) return Float.NaN return (value / divisor).toFloat() + ((value % divisor).toFloat() / divisor.toFloat()) } fun toDouble(): Double { + if (isNaN()) return Double.NaN return (value / divisor).toDouble() + ((value % divisor).toDouble() / divisor.toDouble()) } fun toByteArray(): ByteArray { + if (isNaN()) { + return byteArrayOf(1, 0, 1, 0) + } + val bytesA = value.toByteArray() val bytesB = divisor.toByteArray() @@ -367,41 +433,52 @@ data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @ } fun toInt(): Int { + if (isNaN()) throw ArithmeticException("Fraction is not a number") return (value / divisor).toInt() } fun toLong(): Long { + if (isNaN()) throw ArithmeticException("Fraction is not a number") return (value / divisor).toLong() } @JvmOverloads fun toBigDecimal(context: MathContext = DEFAULT_MATH_CONTEXT): BigDecimal { + if (isNaN()) throw ArithmeticException("Fraction is not a number") return BigDecimal(value).divide(BigDecimal(divisor), context) } // Утилиты fun wholePart(): BigInteger { + if (isNaN()) throw ArithmeticException("Fraction is not a number") return value / divisor } fun fractionPart(): Fraction { + if (isNaN()) return this return Fraction(value % divisor, divisor) } fun modPart(): BigInteger { + if (isNaN()) throw ArithmeticException("Fraction is not a number") return value % divisor } override fun toString(): String { + if (isNaN()) return "NaN" + return "$value/$divisor" } fun formattedString(): String { + if (isNaN()) return "NaN" return "${wholePart()} ${modPart()}/$divisor" } @JvmOverloads fun decimalString(nums: Int = 2, strict: Boolean = false): String { + if (isNaN()) return "NaN" + val whole = wholePart() val fraction = modPart() @@ -438,6 +515,8 @@ data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @ } fun signnum(): Int { + if (isNaN()) return 0 + val a = value.signum() val b = divisor.signum() @@ -503,7 +582,10 @@ data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @ // Позволяет получить процент от деления данного на divisor с точностью до 5 цифр fun percentage(divisor: Fraction): Float { + if (isNaN() || divisor.isNaN()) return Float.NaN + val mul = (this * TEN_THOUSAND) / divisor + if (mul.isNaN()) return Float.NaN return mul.wholePart().toFloat() / 10_000f } @@ -512,6 +594,9 @@ data class Fraction @JvmOverloads constructor(@JvmField val value: BigInteger, @ @JvmField val ZERO = Fraction(BigInteger.ZERO) + @JvmField + val NaN = Fraction(BigInteger.ZERO, BigInteger.ZERO) + @JvmField val ONE_HUNDRED = Fraction(BigInteger.valueOf(100)) diff --git a/src/test/kotlin/ru/dbotthepony/mc/otm/tests/FractionTests.kt b/src/test/kotlin/ru/dbotthepony/mc/otm/tests/FractionTests.kt index 0377bc603..f494d7aff 100644 --- a/src/test/kotlin/ru/dbotthepony/mc/otm/tests/FractionTests.kt +++ b/src/test/kotlin/ru/dbotthepony/mc/otm/tests/FractionTests.kt @@ -2,6 +2,7 @@ package ru.dbotthepony.mc.otm.tests import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import ru.dbotthepony.mc.otm.core.Fraction import java.math.BigDecimal import java.math.BigInteger @@ -36,6 +37,34 @@ object FractionTests { println("canonical -1/-2 == ${Fraction(-1, -2).canonize()}") } + @Test + @DisplayName("Fraction NaN behavior") + fun nan() { + assert((Fraction.NaN + Fraction.ONE).isNaN()) + assert((Fraction.NaN - Fraction.ONE).isNaN()) + assert((Fraction.NaN / Fraction.ONE).isNaN()) + assert((Fraction.NaN * Fraction.ONE).isNaN()) + + assert((Fraction.ONE + Fraction.NaN).isNaN()) + assert((Fraction.ONE - Fraction.NaN).isNaN()) + assert((Fraction.ONE / Fraction.NaN).isNaN()) + assert((Fraction.ONE * Fraction.NaN).isNaN()) + + assert((Fraction.ONE / Fraction.ZERO).isNaN()) + assert((Fraction.ONE * Fraction(0, 0)).isNaN()) + assert((Fraction.ONE * Fraction(1, 0)).isNaN()) + + assert(Fraction.NaN.toFloat().isNaN()) + assert(Fraction.NaN.toDouble().isNaN()) + + assertThrows {Fraction.NaN.toInt()} + assertThrows {Fraction.NaN.toLong()} + assertThrows {Fraction.NaN.wholePart()} + + assert(Fraction.fromByteArray(Fraction.NaN.toByteArray()).isNaN()) + assert(Fraction.NaN.percentage(Fraction.ONE).isNaN()) + } + @Test @DisplayName("Fraction comparison") fun equality() {