From 48ee669458076392e5bfc990856020aa7b05b9c1 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Sun, 18 Jun 2023 17:45:48 +0700 Subject: [PATCH] New Decimal class, with fixed point and compile-time precision --- .../mc/otm/compat/mekanism/Conversions.kt | 10 +- .../dbotthepony/mc/otm/core/math/Decimal.kt | 840 ++++++++---------- .../ru/dbotthepony/mc/otm/core/math/Math.kt | 42 +- .../dbotthepony/mc/otm/tests/DecimalTests.kt | 131 ++- 4 files changed, 521 insertions(+), 502 deletions(-) diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/compat/mekanism/Conversions.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/compat/mekanism/Conversions.kt index fbf931c09..ac8219067 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/compat/mekanism/Conversions.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/compat/mekanism/Conversions.kt @@ -4,7 +4,7 @@ import mekanism.api.math.FloatingLong import ru.dbotthepony.mc.otm.core.math.Decimal import java.math.BigInteger -private val LONG_OVERFLOW = BigInteger.valueOf(Long.MAX_VALUE) + BigInteger.valueOf(Long.MAX_VALUE) + BigInteger.TWO +private val LONG_OVERFLOW = (BigInteger.valueOf(Long.MAX_VALUE) + BigInteger.valueOf(Long.MAX_VALUE) + BigInteger.TWO) * Decimal.PRECISION_POW_BI private val LONG_OVERFLOW1 = BigInteger.valueOf(Long.MAX_VALUE) + BigInteger.valueOf(Long.MAX_VALUE) + BigInteger.ONE fun Decimal.toFloatingLong(): FloatingLong { @@ -17,14 +17,16 @@ fun Decimal.toFloatingLong(): FloatingLong { return FloatingLong.MAX_VALUE } - return FloatingLong.create(whole.toLong(), (decimal * 10_000.0).toInt().toShort()) + return FloatingLong.create(whole.toLong(), (fractionalFloat * 10_000.0).toInt().toShort()) } fun FloatingLong.toDecimal(): Decimal { + var conv = BigInteger.valueOf(value) * Decimal.PRECISION_POW_BI + BigInteger.valueOf((decimal.toDouble() / 10_000.0 * Decimal.PRECISION_DOUBLE).toLong()) + // Overflow if (value < 0L) { - return Decimal(LONG_OVERFLOW + BigInteger.valueOf(value), decimal / 10_000.0) + conv += LONG_OVERFLOW } - return Decimal(value, decimal / 10_000.0) + return Decimal.raw(conv) } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/core/math/Decimal.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/core/math/Decimal.kt index a200f1ff9..835c637c0 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/core/math/Decimal.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/core/math/Decimal.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.mc.otm.core.math +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import net.minecraft.nbt.ByteArrayTag import net.minecraft.nbt.CompoundTag import net.minecraft.nbt.NbtAccounter @@ -9,133 +10,17 @@ import net.minecraft.network.FriendlyByteBuf import net.minecraft.util.RandomSource import net.minecraftforge.common.ForgeConfigSpec import ru.dbotthepony.mc.otm.config.ObservedConfigValue -import ru.dbotthepony.mc.otm.core.util.readDouble import ru.dbotthepony.mc.otm.core.util.readVarIntLE -import ru.dbotthepony.mc.otm.core.util.writeDouble import ru.dbotthepony.mc.otm.core.util.writeVarIntLE import java.io.InputStream import java.io.OutputStream import java.math.BigDecimal import java.math.BigInteger import java.math.MathContext -import kotlin.math.absoluteValue - -//private fun isZero(value: BigInteger) = value == BigInteger.ZERO -private fun isZero(value: BigInteger) = value.signum() == 0 - -private fun unsignedInt(value: Byte): Int { - return value.toInt() and 0xFF -} - -private fun usignedLong(value: Byte): Long { - return value.toLong() and 0xFFL -} - -private fun longToBytesBE(value: Long): ByteArray { - return byteArrayOf( - (value ushr 56).toByte(), - (value ushr 48).toByte(), - (value ushr 40).toByte(), - (value ushr 32).toByte(), - (value ushr 24).toByte(), - (value ushr 16).toByte(), - (value ushr 8).toByte(), - (value).toByte(), - ) -} - -private fun bytesToLongBE(value: ByteArray, from: Int = 0): Long { - return ( - (usignedLong(value[from + 7])) or - (usignedLong(value[from + 6]) shl 8) or - (usignedLong(value[from + 5]) shl 16) or - (usignedLong(value[from + 4]) shl 24) or - (usignedLong(value[from + 3]) shl 32) or - (usignedLong(value[from + 2]) shl 40) or - (usignedLong(value[from + 1]) shl 48) or - (usignedLong(value[from]) shl 56) - ) -} - -private fun bytesToLongBE( - value7: Byte, - value6: Byte, - value5: Byte, - value4: Byte, - value3: Byte, - value2: Byte, - value1: Byte, - value0: Byte, -): Long { - return ( - (usignedLong(value0)) or - (usignedLong(value1) shl 8) or - (usignedLong(value2) shl 16) or - (usignedLong(value3) shl 24) or - (usignedLong(value4) shl 32) or - (usignedLong(value5) shl 40) or - (usignedLong(value6) shl 48) or - (usignedLong(value7) shl 56) - ) -} - -/** - * Constant value which represent edge of meaningful bits. - * - * Equals to 0.000000000001 - */ -const val EPSILON = 0.000000000001 - -private val zeroes = Array(16) { "0".repeat(it).intern() } -private val nums = Array(10) { it.toString()[0] } - -private fun decimals(input: Double, places: Int): String { - if (weakEqualDoubles(input, 0.0)) { - return zeroes.elementAtOrElse(places) { "0".repeat(places) } - } - - val buffer = CharArray(places) - - @Suppress("NAME_SHADOWING") - var input = input.absoluteValue - - for (i in 0 until places) { - input *= 10.0 - buffer[i] = nums[(input % 10.0).toInt()] - } - - return String(buffer) -} - -fun weakEqualDoubles(a: Double, b: Double): Boolean { - if (a == b) - return true - - return (a - b).absoluteValue <= EPSILON -} - -fun weakCompareDoubles(a: Double, b: Double): Int { - if (weakEqualDoubles(a, b)) - return 0 - - if (a > b) - return 1 - - return -1 -} - -fun weakLessThan(a: Double, b: Double) = weakCompareDoubles(a, b) < 0 -fun weakGreaterThan(a: Double, b: Double) = weakCompareDoubles(a, b) > 0 -fun weakLessOrEqual(a: Double, b: Double) = weakCompareDoubles(a, b) <= 0 -fun weakGreaterOrEqual(a: Double, b: Double) = weakCompareDoubles(a, b) >= 0 - -private val PERCENTAGE_CONTEXT = MathContext(6) - -private const val MEANINGFUL_BITS_LONG = 1000000000000000000L -private const val MEANINGFUL_BITS_DOUBLE = MEANINGFUL_BITS_LONG.toDouble() -private val MEANINGFUL_BITS_BI = BigInteger.valueOf(MEANINGFUL_BITS_LONG) +import java.math.RoundingMode private val BI_MINUS_ONE = -BigInteger.ONE +private val PERCENTAGE_CONTEXT = MathContext(6) // 6 ибо это число знаков для вычисления процента fun Decimal(value: Byte) = Decimal.valueOf(value) fun Decimal(value: Short) = Decimal.valueOf(value) @@ -143,198 +28,160 @@ fun Decimal(value: Int) = Decimal.valueOf(value) fun Decimal(value: Long) = Decimal.valueOf(value) /** - * Decimal class for having infinitely precise [whole] part, while having fixed precision [decimal] part. - * - * In essence, this class is pretty much like [BigDecimal], except decimal part of this number is - * not guaranteed to be precise (stored as [Double]). The reason behind creation of this class, however, - * is to allow infinite precision of [whole] part, while leaving [decimal] part to be rounded in inexact operations. - * - * This class is value based, hence it is immutable, and can not be utilized as synchronization lock, and should not be compared - * using identity (`===`) comparison. - * - * **In general, this class is lenient about precision**, to avoid gameplay-wise player frustration, rounding is placed in multiple parts of this class, - * including its constructor (e.g. it is possible to get `Decimal.ZERO == Decimal("0.00000001")`). [EPSILON] is the reference point of precision, - * any value lesser than [EPSILON] is considered zero (including difference between *the value* and (-)1.0). + * Fixed point arbitrary precision Decimal value. Values of this class embed [BigInteger] unscaled value, scale + * is defined at compile time by [PRECISION]. */ -@Suppress("unused") -class Decimal @JvmOverloads constructor(whole: BigInteger, decimal: Double = 0.0) : Number(), Comparable, java.io.Serializable { - constructor(whole: Byte, decimal: Double) : this(BigInteger.valueOf(whole.toLong()), decimal) - constructor(whole: Short, decimal: Double) : this(BigInteger.valueOf(whole.toLong()), decimal) - constructor(whole: Int, decimal: Double) : this(BigInteger.valueOf(whole.toLong()), decimal) - constructor(whole: Long, decimal: Double) : this(BigInteger.valueOf(whole), decimal) - constructor(value: BigDecimal) : this(value.toBigInteger(), value.remainder(BigDecimal.ONE).toDouble()) // TODO - constructor(value: Float) : this(BigInteger.valueOf((value - value % 1.0).toLong()), value % 1.0) - constructor(value: Double) : this(BigInteger.valueOf((value - value % 1.0).toLong()), value % 1.0) +class Decimal private constructor(val mag: BigInteger, marker: Nothing?) : Number(), Comparable { + constructor(value: BigInteger) : this(value * PRECISION_POW_BI, null) + constructor(value: BigDecimal) : this(value.setScale(PRECISION, RoundingMode.HALF_UP).unscaledValue(), null) + constructor(value: Float) : this(BigDecimal.valueOf(value.toDouble())) + constructor(value: Double) : this(BigDecimal(value)) constructor(value: String) : this(BigDecimal(value)) - /** - * Inexact fractional part of this fraction - */ - val decimal: Double + private var _whole: BigInteger? = null + private var _fractional: BigInteger? = null + + private fun computeWholeFractional(): BigInteger { + val (a, b) = mag.divideAndRemainder(PRECISION_POW_BI) + _whole = a + _fractional = b + return a + } /** - * Exact whole part of this fraction + * Whole part of this Decimal */ - val whole: BigInteger + val whole: BigInteger get() { + return _whole ?: computeWholeFractional() + } - init { - @Suppress("name_shadowing") - var decimal = decimal + /** + * Arbitrary fractional part of this Decimal, as [BigInteger] + * + * Makes sense only when utilized along [PRECISION], [PRECISION_POW], [PRECISION_POW_BI] + */ + val fractional: BigInteger get() { + if (_fractional == null) computeWholeFractional() + return _fractional!! + } - if (decimal < 1.0 && decimal + EPSILON >= 1.0) { - // разница между дробью и 1 крайне мала - decimal = 1.0 - } else if (decimal > -1.0 && decimal - EPSILON <= -1.0) { - // разница между дробью и -1 крайне мала - decimal = -1.0 - } + /** + * *Signed* normalized (-1,1) fractional part of this Decimal, as [Float] + */ + inline val fractionalFloat: Float get() { + return fractional.toFloat() / PRECISION_FLOAT + } - @Suppress("name_shadowing") - var whole = whole + /** + * *Signed* normalized (-1,1) fractional part of this Decimal, as [Double] + */ + inline val fractionalDouble: Double get() { + return fractional.toDouble() / PRECISION_DOUBLE + } - // нормализуем - if (decimal <= -1f || decimal >= 1f) { - val frac = decimal % 1f - whole += BigInteger.valueOf((decimal - frac).toLong()) - decimal = frac - } + override fun compareTo(other: Decimal): Int { + return mag.compareTo(other.mag) + } - // дробная часть равна или очень близка к нулю - if (weakEqualDoubles(decimal, 0.0)) { - this.decimal = 0.0 - this.whole = whole - // целая часть и дробная часть имеют одинаковые знаки - } else if (decimal > 0.0 && whole.signum() >= 0 || decimal < 0.0 && whole.signum() <= 0) { - this.decimal = decimal - this.whole = whole + override fun toByte(): Byte { + return whole.toByte() + } + + override fun toChar(): Char { + return whole.toChar() + } + + override fun toDouble(): Double { + return mag.toDouble() / PRECISION_DOUBLE + } + + override fun toFloat(): Float { + return mag.toFloat() / PRECISION_FLOAT + } + + override fun toInt(): Int { + if (whole > BI_INT_MAX) { + return Int.MAX_VALUE + } else if (whole < BI_INT_MIN) { + return Int.MIN_VALUE } else { - // целая часть и дробная часть имеют разные знаки - - if (decimal > 0.0) { - this.decimal = decimal - 1f - this.whole = whole + BigInteger.ONE - //} else if (double < 0.0 && whole.signum() > 0) { - } else { - this.decimal = decimal + 1f - this.whole = whole - BigInteger.ONE - } - - //throw InternalError("This should never happen, but it happened. Input was: $whole $double") + return whole.toInt() } } - val isNaN get() = decimal.isNaN() + override fun toLong(): Long { + if (whole > BI_LONG_MAX) { + return Long.MAX_VALUE + } else if (whole < BI_LONG_MIN) { + return Long.MIN_VALUE + } else { + return whole.toLong() + } + } - operator fun plus(other: Decimal) = Decimal(whole + other.whole, decimal + other.decimal) - operator fun minus(other: Decimal) = Decimal(whole - other.whole, decimal - other.decimal) + override fun toShort(): Short { + return whole.toShort() + } - operator fun times(other: Decimal): Decimal { - if (other == ONE) + operator fun plus(other: Decimal): Decimal { + if (other.mag.signum() == 0) { return this - else if (other == MINUS_ONE) - return -this - - val a = whole - val c = other.whole - - val bZero = weakEqualDoubles(decimal, 0.0) - val dZero = weakEqualDoubles(other.decimal, 0.0) - - if (bZero && dZero) { - if (isZero(a) || isZero(c)) - return ZERO - - return Decimal(a * c) - } else if (isZero(a) && isZero(c)) { - if (bZero || dZero) - return ZERO - - return Decimal(0, decimal * other.decimal) - } else if (bZero) { - val d = other.decimal * MEANINGFUL_BITS_DOUBLE - val dL = d.toLong() - val adInflated = a * BigInteger.valueOf(dL) - val ad = adInflated.divideAndRemainder(MEANINGFUL_BITS_BI) - - if (isZero(c)) { - return Decimal( - ad[0], - ad[1].toDouble() / MEANINGFUL_BITS_DOUBLE - ) - } - - return Decimal( - a * c + ad[0], - ad[1].toDouble() / MEANINGFUL_BITS_DOUBLE - ) - } else if (dZero) { - val b = decimal * MEANINGFUL_BITS_DOUBLE - val bL = b.toLong() - val bcInflated = c * BigInteger.valueOf(bL) - val bc = bcInflated.divideAndRemainder(MEANINGFUL_BITS_BI) - - if (isZero(a)) { - return Decimal( - bc[0], - bc[1].toDouble() / MEANINGFUL_BITS_DOUBLE - ) - } - - return Decimal( - a * c + bc[0], - bc[1].toDouble() / MEANINGFUL_BITS_DOUBLE - ) } - val b = decimal * MEANINGFUL_BITS_DOUBLE - val d = other.decimal * MEANINGFUL_BITS_DOUBLE + return Decimal(mag + other.mag, null).interned() + } - val bL = b.toLong() - val dL = d.toLong() + operator fun minus(other: Decimal): Decimal { + if (other.mag.signum() == 0) { + return this + } - val bcInflated = c * BigInteger.valueOf(bL) - val adInflated = a * BigInteger.valueOf(dL) + return Decimal(mag - other.mag, null).interned() + } - val bc = bcInflated.divideAndRemainder(MEANINGFUL_BITS_BI) - val ad = adInflated.divideAndRemainder(MEANINGFUL_BITS_BI) + operator fun times(other: Decimal): Decimal { + if (other.mag.signum() == 0) { + return ZERO + } else if (other.mag == BigInteger.ONE) { + return this + } else if (other.mag == BI_MINUS_ONE) { + return Decimal(-mag, null).interned() + } - return Decimal( - a * c + bc[0] + ad[0], - decimal * other.decimal + bc[1].toDouble() / MEANINGFUL_BITS_DOUBLE + ad[1].toDouble() / MEANINGFUL_BITS_DOUBLE - ) + val result = mag * other.mag + val (a, b) = result.divideAndRemainder(PRECISION_POW_BI) + + if (b >= PRECISION_POW_BI_HIGH) { + return Decimal(a + BigInteger.ONE, null).interned() + } else { + return Decimal(a, null).interned() + } } operator fun div(other: Decimal): Decimal { - if (other == ONE) + if (other.mag.signum() == 0) { + throw ArithmeticException("Attempt to divide $this by zero") + } else if (other.mag == BigInteger.ONE) { return this - else if (other == MINUS_ONE) - return -this - - if (isZero && other.isZero) - return NaN - - if (!isZero && other.isZero) - throw ArithmeticException("Divide by zero") - - //if (isZero(whole) && isZero(other.whole)) - // return ImpreciseFraction(BigInteger.ZERO, decimal / other.decimal) - - val a = toBigDecmial() - val b = other.toBigDecmial() - val div = a.divideAndRemainder(b, MathContext(a.precision() + b.precision())) - - val bD = b.toDouble() - - if (bD.isInfinite() || bD == 0.0) { - return Decimal(div[0].toBigInteger()) + } else if (other.mag == BI_MINUS_ONE) { + return Decimal(-mag, null).interned() } - return Decimal(div[0].toBigInteger(), div[1].toDouble() / bD) + return Decimal((mag * PRECISION_POW_BI) / other.mag, null).interned() } + operator fun rem(other: Decimal): Decimal { + return Decimal(mag % other.mag, null) + } + + // Primitive operators operator fun plus(other: Float): Decimal { if (other == 0f) { return this + } else if (other.isNaN()) { + throw ArithmeticException("Attempt to add NaN to $this") + } else if (other.isInfinite()) { + throw ArithmeticException("Attempt to add infinity to $this") } return plus(Decimal(other)) @@ -343,6 +190,10 @@ class Decimal @JvmOverloads constructor(whole: BigInteger, decimal: Double = 0.0 operator fun minus(other: Float): Decimal { if (other == 0f) { return this + } else if (other.isNaN()) { + throw ArithmeticException("Attempt to subtract NaN from $this") + } else if (other.isInfinite()) { + throw ArithmeticException("Attempt to subtract infinity from $this") } return minus(Decimal(other)) @@ -355,6 +206,10 @@ class Decimal @JvmOverloads constructor(whole: BigInteger, decimal: Double = 0.0 return ZERO } else if (other == -1f) { return -this + } else if (other.isNaN()) { + throw ArithmeticException("Attempt to multiply $this by NaN") + } else if (other.isInfinite()) { + throw ArithmeticException("Attempt to multiply $this by infinity") } return times(Decimal(other)) @@ -362,14 +217,15 @@ class Decimal @JvmOverloads constructor(whole: BigInteger, decimal: Double = 0.0 operator fun div(other: Float): Decimal { if (other == 0f) { - if (isZero) - return NaN - else - throw ArithmeticException("Divide by zero") + throw ArithmeticException("Attempt to divide $this by zero") } else if (other == 1f) { return this } else if (other == -1f) { return -this + } else if (other.isNaN()) { + throw ArithmeticException("Attempt to divide $this by NaN") + } else if (other.isInfinite()) { + throw ArithmeticException("Attempt to divide $this by infinity") } return div(Decimal(other)) @@ -378,6 +234,10 @@ class Decimal @JvmOverloads constructor(whole: BigInteger, decimal: Double = 0.0 operator fun plus(other: Double): Decimal { if (other == 0.0) { return this + } else if (other.isNaN()) { + throw ArithmeticException("Attempt to add NaN to $this") + } else if (other.isInfinite()) { + throw ArithmeticException("Attempt to add infinity to $this") } return plus(Decimal(other)) @@ -386,6 +246,10 @@ class Decimal @JvmOverloads constructor(whole: BigInteger, decimal: Double = 0.0 operator fun minus(other: Double): Decimal { if (other == 0.0) { return this + } else if (other.isNaN()) { + throw ArithmeticException("Attempt to subtract NaN from $this") + } else if (other.isInfinite()) { + throw ArithmeticException("Attempt to subtract infinity from $this") } return minus(Decimal(other)) @@ -398,6 +262,10 @@ class Decimal @JvmOverloads constructor(whole: BigInteger, decimal: Double = 0.0 return ZERO } else if (other == -1.0) { return -this + } else if (other.isNaN()) { + throw ArithmeticException("Attempt to multiply $this by NaN") + } else if (other.isInfinite()) { + throw ArithmeticException("Attempt to multiply $this by infinity") } return times(Decimal(other)) @@ -405,14 +273,15 @@ class Decimal @JvmOverloads constructor(whole: BigInteger, decimal: Double = 0.0 operator fun div(other: Double): Decimal { if (other == 0.0) { - if (isZero) - return NaN - else - throw ArithmeticException("Divide by zero") + throw ArithmeticException("Attempt to divide $this by zero") } else if (other == 1.0) { return this } else if (other == -1.0) { return -this + } else if (other.isNaN()) { + throw ArithmeticException("Attempt to divide $this by NaN") + } else if (other.isInfinite()) { + throw ArithmeticException("Attempt to divide $this by infinity") } return div(Decimal(other)) @@ -448,10 +317,7 @@ class Decimal @JvmOverloads constructor(whole: BigInteger, decimal: Double = 0.0 operator fun div(other: Int): Decimal { if (other == 0) { - if (isZero) - return NaN - else - throw ArithmeticException("Divide by zero") + throw ArithmeticException("Attempt to divide $this by zero") } else if (other == 1) { return this } else if (other == -1) { @@ -491,10 +357,7 @@ class Decimal @JvmOverloads constructor(whole: BigInteger, decimal: Double = 0.0 operator fun div(other: Long): Decimal { if (other == 0L) { - if (isZero) - return NaN - else - throw ArithmeticException("Divide by zero") + throw ArithmeticException("Attempt to divide $this by zero") } else if (other == 1L) { return this } else if (other == -1L) { @@ -523,7 +386,7 @@ class Decimal @JvmOverloads constructor(whole: BigInteger, decimal: Double = 0.0 operator fun times(other: BigInteger): Decimal { if (other == BigInteger.ONE) { return this - } else if (other == BigInteger.ZERO) { + } else if (other.signum() == 0) { return ZERO } else if (other == BI_MINUS_ONE) { return -this @@ -534,7 +397,7 @@ class Decimal @JvmOverloads constructor(whole: BigInteger, decimal: Double = 0.0 operator fun div(other: BigInteger): Decimal { if (other == BigInteger.ZERO) { - return NaN + throw ArithmeticException("Attempt to divide $this by zero") } else if (other == BigInteger.ONE) { return this } else if (other == BI_MINUS_ONE) { @@ -543,9 +406,70 @@ class Decimal @JvmOverloads constructor(whole: BigInteger, decimal: Double = 0.0 return div(Decimal(other)) } + // /Primitive operators operator fun unaryMinus(): Decimal { - return Decimal(-whole, -decimal) + if (mag == BigInteger.ZERO) { + return this + } + + return Decimal(-mag, null).interned() + } + + operator fun unaryPlus() = this + + /** + * Sign number of this Decimal, as defined by [BigInteger.signum] + */ + fun signum() = mag.signum() + + /** + * Whenever this Decimal is negative (less than zero) + */ + inline val isNegative get() = signum() < 0 + + /** + * Whenever this Decimal is positive (bigger than zero) + */ + inline val isPositive get() = signum() > 0 + + /** + * Whenever this Decimal is zero + */ + inline val isZero get() = signum() == 0 + + /** + * Alias for [coerceAtLeast] with [ZERO] constant, except this method might + * perform faster. + */ + fun moreThanZero(): Decimal { + if (signum() >= 0) + return this + + return ZERO + } + + /** + * Alias for [coerceAtMost] with [ZERO] constant, except this method might + * perform faster. + */ + fun lessOrZero(): Decimal { + if (signum() <= 0) + return this + + return ZERO + } + + fun toByteArray(): ByteArray { + return mag.toByteArray() + } + + fun serializeNBT(): Tag { + return ByteArrayTag(toByteArray()) + } + + fun write(buff: FriendlyByteBuf) { + buff.writeByteArray(toByteArray()) } val absoluteValue: Decimal @@ -557,156 +481,93 @@ class Decimal @JvmOverloads constructor(whole: BigInteger, decimal: Double = 0.0 } } - override fun equals(other: Any?): Boolean { - if (isNaN) return false - return other === this || other is Decimal && other.whole == whole && weakEqualDoubles(decimal, other.decimal) - } - - override fun hashCode(): Int { - if (isNaN) - return Double.NaN.hashCode() - - return 31 * decimal.hashCode() + whole.hashCode() - } - + /** + * Truncates fractional part of this Decimal + */ fun floor(): Decimal { - if (decimal == 0.0) - return this - return Decimal(whole) } - fun toString(decimals: Int): String { - if (isNaN) - return "NaN" + override fun equals(other: Any?): Boolean { + return this === other || other is Decimal && mag == other.mag + } + override fun hashCode(): Int { + return mag.hashCode() + } + + fun toString(decimals: Int): String { if (decimals == 0) { return whole.toString() - } else if (decimals > 0) { - return "$whole.${decimals(decimal, decimals)}" } - return when (signum()) { - 1, -1 -> "$whole.${decimals(decimal, 11)}" - 0 -> "0.0" - else -> throw IllegalArgumentException("invalid signum") + if (mag.signum() == 0) { + if (decimals < 0) { + return "0.0" + } else { + return "0." + "0".repeat(decimals) + } + } + + var original = mag.toString() + + if (mag.signum() < 0) { + // если у нас отрицательное число - убираем знак минуса + original = original.substring(1) + } + + if (original.length <= PRECISION) { + // если у нас чисто дробное число - дописываем нули в начало + original = "0".repeat(PRECISION - original.length + 1) + original + } + + // теперь у нас беззнаковая строка с нужной длиной + + if (decimals < 0) { + // нам неважно количество знаков после запятой + var result = original.substring(0, original.length - PRECISION) + "." + original.substring(original.length - PRECISION) + + if (mag.signum() < 0) { + result = "-$result" + } + + var pos = result.length - 1 + + while (result[pos] == '0') { + if (result[pos - 1] == '0' || result[pos - 1] != '.') + pos-- + else + break + } + + return result.substring(0, pos + 1) + } else { + // нужно некоторое количество знаков после запятой + val result = original.substring(0, original.length - PRECISION) + "." + var decimalPart = original.substring(original.length - PRECISION) + + if (decimalPart.length < decimals) { + decimalPart += "0".repeat(decimals - decimalPart.length) + } else if (decimalPart.length > decimals) { + decimalPart = decimalPart.substring(0, decimals) + } + + if (mag.signum() < 0) { + return "-$result$decimalPart" + } else { + return "$result$decimalPart" + } } } - override fun toString() = toString(-1) + override fun toString(): String = toString(-1) fun toBigDecmial(): BigDecimal { - return BigDecimal(whole) + BigDecimal(decimal) - } - - private var _signum = Int.MIN_VALUE - - fun signum(): Int { - if (_signum == Int.MIN_VALUE) { - var cmp = whole.signum() - if (cmp == 0) cmp = if (decimal == 0.0) 0 else if (1.0 / decimal > 0.0) 1 else -1 - _signum = cmp - } - - return _signum - } - - override fun compareTo(other: Decimal): Int { - if (isNaN) - return 1 - else if (other.isNaN) - return -1 - - val a = signum() - val b = other.signum() - - if (a < b) - return -1 - else if (a > b) - return 1 - - var cmp = whole.compareTo(other.whole) - if (cmp == 0) cmp = weakCompareDoubles(decimal, other.decimal) - - return cmp - } - - fun toByteArray(): ByteArray { - val whole = whole.toByteArray() - return byteArrayOf(whole.size.toByte(), *whole, *longToBytesBE(decimal.toBits())) - } - - fun serializeNBT(): Tag { - return ByteArrayTag(toByteArray()) - } - - val isZero get() = weakEqualDoubles(decimal, 0.0) && isZero(whole) - val isPositive get() = this > ZERO - val isNegative get() = this < ZERO - - fun moreThanZero(): Decimal { - if (signum() >= 0) - return this - - return ZERO - } - - fun lessOrZero(): Decimal { - if (signum() <= 0) - return this - - return ZERO - } - - override fun toInt(): Int { - if (whole > BI_INT_MAX) { - return Int.MAX_VALUE - } else if (whole < BI_INT_MIN) { - return Int.MIN_VALUE - } - - return whole.toInt() - } - - fun write(buff: FriendlyByteBuf) { - val whole = whole.toByteArray() - buff.writeByteArray(whole) - buff.writeDouble(decimal) - } - - override fun toFloat(): Float { - return whole.toFloat() + decimal.toFloat() - } - - override fun toDouble(): Double { - return whole.toDouble() + decimal - } - - override fun toByte(): Byte { - return toInt().toByte() - } - - override fun toChar(): Char { - return toInt().toChar() - } - - override fun toLong(): Long { - if (whole > BI_LONG_MAX) { - return Long.MAX_VALUE - } else if (whole < BI_LONG_MIN) { - return Long.MIN_VALUE - } - - return whole.toLong() - } - - override fun toShort(): Short { - return toInt().toShort() + return BigDecimal(mag, PRECISION) } fun percentage(divisor: Decimal, zeroing: Boolean = true): Float { if ((isZero || divisor.isZero) && zeroing) return 0f - if (isNaN || divisor.isNaN || divisor.isZero) return Float.NaN if (this >= divisor) return 1f @@ -717,88 +578,100 @@ class Decimal @JvmOverloads constructor(whole: BigInteger, decimal: Double = 0.0 return toBigDecmial().divide(divisor.toBigDecmial(), PERCENTAGE_CONTEXT).toFloat() } - operator fun rem(divisor: Decimal): Decimal { - TODO("Not yet implemented") + private fun interned(): Decimal { + return pool[this] ?: this } + @Suppress("unused") companion object { - @JvmStatic - val serialVersionUID: Long = 287354739494574838L + /** + * Amount of digits after fixed point, in 10 radix + */ + const val PRECISION = 11 - @JvmField val MINUS_ZERO = Decimal(0, -0.0) - @JvmField val ZERO = Decimal(BigInteger.ZERO) - @JvmField val ONE = Decimal(BigInteger.ONE) - @JvmField val MINUS_ONE = Decimal(-BigInteger.ONE) - @JvmField val TWO = Decimal(BigInteger.TWO) - @JvmField val TEN = Decimal(BigInteger.TEN) - - @JvmField val INT_MAX_VALUE = Decimal(BI_INT_MAX) - @JvmField val INT_MIN_VALUE = Decimal(BI_INT_MIN) - @JvmField val LONG_MAX_VALUE = Decimal(BI_LONG_MAX) - @JvmField val LONG_MIN_VALUE = Decimal(BI_LONG_MIN) - - @JvmField val FLOAT_MAX_VALUE = Decimal(BI_FLOAT_MAX) - @JvmField val FLOAT_MIN_VALUE = Decimal(BI_FLOAT_MIN) - @JvmField val DOUBLE_MAX_VALUE = Decimal(BI_DOUBLE_MAX) - @JvmField val DOUBLE_MIN_VALUE = Decimal(BI_DOUBLE_MIN) - - @JvmField val ONE_TENTH = Decimal("0.1") - - private val cache = Array(2048) { Decimal(it - 1024, 0.0) } + /** + * 10 in power of [PRECISION] (10^[PRECISION]), as [Long] + */ + @JvmField + val PRECISION_POW: Long init { - cache[1024] = ZERO - cache[1025] = ONE - cache[1023] = MINUS_ONE - cache[1026] = TWO - cache[1034] = TEN + var i = 10L + for (u in 1 until PRECISION) i *= 10L + PRECISION_POW = i + } + + @JvmField + val PRECISION_DOUBLE = PRECISION_POW.toDouble() + @JvmField + val PRECISION_FLOAT = PRECISION_POW.toFloat() + @JvmField + val PRECISION_INT = PRECISION_POW.toInt() + + @JvmField + val PRECISION_POW_BI: BigInteger = BigInteger.valueOf(PRECISION_POW) + @JvmField + val PRECISION_POW_BI_NEGATIVE: BigInteger = -PRECISION_POW_BI + @JvmField + val PRECISION_POW_BI_HIGH: BigInteger = BigInteger.valueOf(PRECISION_POW / 2) + + private val cache = Array(2048) { Decimal(BigInteger.valueOf(it.toLong() - 1024)) } + private val pool = Object2ObjectOpenHashMap() + + init { + for (value in cache) { + pool[value] = value + } } /** * Returns pooled value if present, otherwise constructs new object */ + @JvmStatic fun valueOf(value: Int): Decimal { if (value in -1024 .. 1023) { return cache[value + 1024] } - return Decimal(value, 0.0) + return Decimal(BigInteger.valueOf(value.toLong())) } /** * Returns pooled value if present, otherwise constructs new object */ + @JvmStatic fun valueOf(value: Long): Decimal { if (value in -1024L .. 1023L) { return cache[value.toInt() + 1024] } - return Decimal(value, 0.0) + return Decimal(BigInteger.valueOf(value)) } /** * Returns pooled value if present, otherwise constructs new object */ + @JvmStatic fun valueOf(value: Short) = valueOf(value.toInt()) /** * Returns pooled value if present, otherwise constructs new object */ + @JvmStatic fun valueOf(value: Byte) = valueOf(value.toInt()) - @JvmField - val NaN = Decimal(0, Double.NaN) + @JvmStatic fun valueOf(value: BigInteger) = Decimal(value).interned() + @JvmStatic fun valueOf(value: BigDecimal) = Decimal(value).interned() + @JvmStatic fun valueOf(value: String) = Decimal(value).interned() + @JvmStatic fun valueOf(value: Float) = Decimal(value).interned() + @JvmStatic fun valueOf(value: Double) = Decimal(value).interned() @JvmStatic fun fromByteArray(input: ByteArray): Decimal { - val size = unsignedInt(input[0]) - - if (size > 0) { - val slice = input.copyOfRange(1, 1 + size) - val bits = bytesToLongBE(input, 1 + size) - return Decimal(BigInteger(slice), Double.fromBits(bits)) + if (input.isEmpty()) { + return ZERO } else { - return Decimal(BigInteger.ZERO, Double.fromBits(bytesToLongBE(input, 1))) + return Decimal(BigInteger(input), null) } } @@ -815,8 +688,34 @@ class Decimal @JvmOverloads constructor(whole: BigInteger, decimal: Double = 0.0 @JvmStatic fun read(buff: FriendlyByteBuf): Decimal { - return Decimal(BigInteger(buff.readByteArray()), buff.readDouble()) + return Decimal(BigInteger(buff.readByteArray())) } + + /** + * Takes in [value] as-is and constructs [Decimal] out of it ([value] is treated as already scaled by [PRECISION]) + */ + @JvmStatic + fun raw(value: BigInteger): Decimal = Decimal(value, null) + + @JvmField val ZERO = valueOf(0) + @JvmField val ONE = valueOf(1) + @JvmField val TWO = valueOf(2) + @JvmField val TEN = valueOf(10) + @JvmField val MINUS_ONE = valueOf(-1) + @JvmField val MINUS_TWO = valueOf(-2) + @JvmField val MINUS_TEN = valueOf(-10) + @JvmField val ONE_TENTH = valueOf("0.1") + @JvmField val MINUS_ONE_TENTH = valueOf("-0.1") + + @JvmField val INT_MAX_VALUE = Decimal(BI_INT_MAX) + @JvmField val INT_MIN_VALUE = Decimal(BI_INT_MIN) + @JvmField val LONG_MAX_VALUE = Decimal(BI_LONG_MAX) + @JvmField val LONG_MIN_VALUE = Decimal(BI_LONG_MIN) + + @JvmField val FLOAT_MAX_VALUE = Decimal(BI_FLOAT_MAX) + @JvmField val FLOAT_MIN_VALUE = Decimal(BI_FLOAT_MIN) + @JvmField val DOUBLE_MAX_VALUE = Decimal(BI_DOUBLE_MAX) + @JvmField val DOUBLE_MIN_VALUE = Decimal(BI_DOUBLE_MIN) } } @@ -826,17 +725,16 @@ fun FriendlyByteBuf.writeDecimal(value: Decimal) = value.write(this) fun InputStream.readDecimal(sizeLimit: NbtAccounter = NbtAccounter(512L)): Decimal { val size = readVarIntLE(sizeLimit) require(size >= 0) { "Negative payload size: $size" } - sizeLimit.accountBytes(size.toLong() + 8L) + sizeLimit.accountBytes(size.toLong()) val bytes = ByteArray(size) read(bytes) - return Decimal(BigInteger(bytes), readDouble()) + return Decimal.fromByteArray(bytes) } fun OutputStream.writeDecimal(value: Decimal) { - val bytes = value.whole.toByteArray() + val bytes = value.toByteArray() writeVarIntLE(bytes.size) write(bytes) - writeDouble(value.decimal) } fun CompoundTag.getDecimal(key: String) = Decimal.deserializeNBT(this[key]) diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/core/math/Math.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/core/math/Math.kt index 248380d83..21ddabcfc 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/core/math/Math.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/core/math/Math.kt @@ -2,14 +2,15 @@ package ru.dbotthepony.mc.otm.core.math import java.math.BigDecimal import java.math.BigInteger +import kotlin.math.absoluteValue -inline val BigInteger.isZero get() = this == BigInteger.ZERO -inline val BigInteger.isPositive get() = this > BigInteger.ZERO -inline val BigInteger.isNegative get() = this < BigInteger.ZERO +inline val BigInteger.isZero get() = this.signum() == 0 +inline val BigInteger.isPositive get() = this.signum() > 0 +inline val BigInteger.isNegative get() = this.signum() < 0 -inline val BigDecimal.isZero get() = this == BigDecimal.ZERO -inline val BigDecimal.isPositive get() = this > BigDecimal.ZERO -inline val BigDecimal.isNegative get() = this < BigDecimal.ZERO +inline val BigDecimal.isZero get() = this.signum() == 0 +inline val BigDecimal.isPositive get() = this.signum() > 0 +inline val BigDecimal.isNegative get() = this.signum() < 0 val BI_INT_MAX: BigInteger = BigInteger.valueOf(Int.MAX_VALUE.toLong()) val BI_INT_MIN: BigInteger = BigInteger.valueOf(Int.MIN_VALUE.toLong()) @@ -21,6 +22,35 @@ val BI_FLOAT_MIN: BigInteger = BigDecimal(Float.MIN_VALUE.toString()).toBigInteg val BI_DOUBLE_MAX: BigInteger = BigDecimal(Double.MAX_VALUE.toString()).toBigInteger() val BI_DOUBLE_MIN: BigInteger = BigDecimal(Double.MIN_VALUE.toString()).toBigInteger() +/** + * Constant value which represent edge of meaningful bits. + * + * Equals to 0.000000000001 + */ +const val EPSILON = 0.000000000001 + +fun weakEqualDoubles(a: Double, b: Double): Boolean { + if (a == b) + return true + + return (a - b).absoluteValue <= EPSILON +} + +fun weakCompareDoubles(a: Double, b: Double): Int { + if (weakEqualDoubles(a, b)) + return 0 + + if (a > b) + return 1 + + return -1 +} + +fun weakLessThan(a: Double, b: Double) = weakCompareDoubles(a, b) < 0 +fun weakGreaterThan(a: Double, b: Double) = weakCompareDoubles(a, b) > 0 +fun weakLessOrEqual(a: Double, b: Double) = weakCompareDoubles(a, b) <= 0 +fun weakGreaterOrEqual(a: Double, b: Double) = weakCompareDoubles(a, b) >= 0 + fun BigInteger.toIntSafe(): Int { if (this > BI_INT_MAX) { return Int.MAX_VALUE diff --git a/src/test/kotlin/ru/dbotthepony/mc/otm/tests/DecimalTests.kt b/src/test/kotlin/ru/dbotthepony/mc/otm/tests/DecimalTests.kt index 9f27dde91..366fd3dc3 100644 --- a/src/test/kotlin/ru/dbotthepony/mc/otm/tests/DecimalTests.kt +++ b/src/test/kotlin/ru/dbotthepony/mc/otm/tests/DecimalTests.kt @@ -11,46 +11,135 @@ import java.io.ObjectOutputStream object DecimalTests { @Test - @DisplayName("ImpreciseFraction comparison") + @DisplayName("Decimal comparison") fun comparison() { - check(Decimal(642, 0.43774) > Decimal(641, 0.43774)) { "must be bigger" } - check(Decimal(642, 0.43774) > Decimal(-641, 0.43774)) { "must be bigger" } + check(Decimal(642.43774) > Decimal(641.43774)) { "must be bigger" } + check(Decimal(642.43774) > Decimal(-641.43774)) { "must be bigger" } check(Decimal(0) == Decimal(0)) { "integer notation" } check(Decimal(0.1) > Decimal(0)) { "must be bigger" } check(Decimal(-0.1) < Decimal(0)) { "must be lesser" } - check(Decimal(-1, -0.1) < Decimal(-1)) { "must be lesser" } + check(Decimal(-1.1) < Decimal(-1)) { "must be lesser" } check(Decimal(0.1) == Decimal(0.1)) { "double notation" } check(Decimal("0.1") == Decimal("0.1")) { "string notation" } } @Test - @DisplayName("ImpreciseFraction mathematical operations") + @DisplayName("Decimal mathematical operations") fun math() { - check((Decimal(1) + Decimal(2)) == Decimal(3)) { "1 + 2 != 3" } - check((Decimal(0, 0.5) + Decimal(0, 0.5)) == Decimal(1)) { "0.5 + 0.5 != 1" } - check((Decimal(0, 0.5) + Decimal(0, -0.5)) == Decimal(0)) { "0.5 + -0.5 != 1" } - check((Decimal(0, 0.5) - Decimal(0, -0.5)) == Decimal(1)) { "0.5 - -0.5 != 1" } - check((Decimal(0, 0.3) - Decimal(1, 0.2)) == Decimal(0, -0.9)) { "0.3 - 1.2 != -0.9 ${Decimal(0, -0.9)} ${Decimal(0, 0.3) - Decimal(1, 0.2)}" } - check((Decimal(0, 0.3) * Decimal(0, 0.3)) == Decimal(0, 0.09)) { "0.3 * 0.3 != 0.9 ${Decimal(0, 0.3) * Decimal(0, 0.3)}" } - check((Decimal(2, 0.3) * Decimal(2, 0.3)) == Decimal(5, 0.29)) { "2.3 * 2.3 != 5.29 ${Decimal(2, 0.3) * Decimal(2, 0.3)}" } - check((Decimal(4) / Decimal(2)) == Decimal(2)) { "4 / 2 != 2" } - check((Decimal(6) / Decimal(2)) == Decimal(3)) { "6 / 2 != 2" } - check((Decimal(1) / Decimal(0, 0.25)) == Decimal(4)) { "1 / 0.25 != 4" } + assertEquals(Decimal(3), Decimal(1) + Decimal(2)) + assertEquals(Decimal(5), Decimal(2) + Decimal(3)) + assertEquals(Decimal(-1), Decimal(6) + Decimal(-7)) + assertEquals(Decimal(-1), Decimal(6) - Decimal(7)) + assertEquals(Decimal(2), Decimal(1) * Decimal(2)) + assertEquals(Decimal(4), Decimal(2) * Decimal(2)) + assertEquals(Decimal(16), Decimal(4) * Decimal(4)) + assertEquals(Decimal(100_000), Decimal(100) * Decimal(1_000)) + assertEquals(Decimal(4), Decimal(16) / Decimal(4)) + assertEquals(Decimal(3), Decimal(12) / Decimal(4)) + assertEquals(Decimal(1), Decimal(15) / Decimal(15)) + + assertEquals(Decimal("0.1"), Decimal(1) / Decimal(10)) + assertEquals(Decimal("-0.1"), Decimal(1) / Decimal(-10)) + assertEquals(Decimal("-0.1"), Decimal(-1) / Decimal(10)) + assertEquals(Decimal("0.1"), Decimal(-1) / Decimal(-10)) + assertEquals(Decimal("0.5"), Decimal("0.25") + Decimal("0.25")) + assertEquals(Decimal("0.5"), Decimal("0.25") * Decimal(2)) + assertEquals(Decimal("-0.5"), Decimal("0.25") * Decimal(-2)) + assertEquals(Decimal("-0.5"), Decimal("-0.25") * Decimal(2)) + assertEquals(Decimal("0.3"), Decimal(0.2) + Decimal(0.1)) + assertEquals(Decimal("0.3"), Decimal("0.2") + Decimal("0.1")) } @Test - @DisplayName("ImpreciseFraction store/load") + @DisplayName("Decimal precision test") + fun precisionTest() { + assertEquals(0.0, Decimal(4).fractionalDouble) + assertEquals(0.2, Decimal(4.2).fractionalDouble) + assertEquals(0.2, Decimal("4.2").fractionalDouble) + assertEquals(0.25, Decimal("4.25").fractionalDouble) + assertEquals(-0.25, Decimal("-1.25").fractionalDouble) + assertEquals(-0.2, Decimal("-1.2").fractionalDouble) + assertEquals(0.0, Decimal("-1").fractionalDouble) + + assertEquals(0.0f, Decimal(4).fractionalFloat) + assertEquals(0.2f, Decimal(4.2).fractionalFloat) + assertEquals(0.2f, Decimal("4.2").fractionalFloat) + assertEquals(0.25f, Decimal("4.25").fractionalFloat) + assertEquals(-0.25f, Decimal("-1.25").fractionalFloat) + assertEquals(-0.2f, Decimal("-1.2").fractionalFloat) + assertEquals(0.0f, Decimal("-1").fractionalFloat) + } + + @Test + @DisplayName("Decimal toString() tests") + fun toStringTest() { + assertEquals("0.0", Decimal(0).toString()) + assertEquals("0.0", Decimal("0.000").toString()) + + assertEquals("0.001", Decimal("0.001").toString()) + assertEquals("0.1", Decimal("0.1").toString()) + assertEquals("1.0", Decimal("1.0").toString()) + assertEquals("4.0", Decimal("4.0").toString()) + assertEquals("4.0", Decimal("4.000").toString()) + assertEquals("4.0001", Decimal("4.0001").toString()) + + assertEquals("-0.001", Decimal("-0.001").toString()) + assertEquals("-0.1", Decimal("-0.1").toString()) + assertEquals("-1.0", Decimal("-1.0").toString()) + assertEquals("-4.0", Decimal("-4.0").toString()) + assertEquals("-4.0", Decimal("-4.000").toString()) + assertEquals("-4.0001", Decimal("-4.0001").toString()) + + assertEquals("0.0", Decimal("0.000").toString(1)) + assertEquals("0.00", Decimal("0.000").toString(2)) + assertEquals("0.000", Decimal("0.000").toString(3)) + assertEquals("0.001", Decimal("0.001").toString(3)) + assertEquals("0.00", Decimal("0.001").toString(2)) + assertEquals("0.0", Decimal("0.001").toString(1)) + assertEquals("1.0", Decimal("1.001").toString(1)) + assertEquals("1.1", Decimal("1.101").toString(1)) + assertEquals("1.10", Decimal("1.101").toString(2)) + assertEquals("1.101", Decimal("1.101").toString(3)) + assertEquals("1.1010", Decimal("1.101").toString(4)) + } + + @Test + @DisplayName("Decimal store/load") fun storeLoad() { run { - val f = Decimal(4, 0.28) + val f = Decimal(4.28) val loaded = Decimal.fromByteArray(f.toByteArray()) - check(f == loaded) { "$f != $loaded" } + assertEquals(f, loaded) } run { - val f = Decimal(32748293658335L, 0.3472302174) + val f = Decimal(1) val loaded = Decimal.fromByteArray(f.toByteArray()) - check(f == loaded) { "$f != $loaded" } + assertEquals(f, loaded) + } + + run { + val f = Decimal(0) + val loaded = Decimal.fromByteArray(f.toByteArray()) + assertEquals(f, loaded) + } + + run { + val f = Decimal("23784399123654793927324950293475093257324097021387409873240957021934750934089734095273098475") + val loaded = Decimal.fromByteArray(f.toByteArray()) + assertEquals(f, loaded) + } + + run { + val f = Decimal("23784399123654793927324950293475093257324097021387409873240957021934750934089734095273098475.25") + val loaded = Decimal.fromByteArray(f.toByteArray()) + assertEquals(f, loaded) + } + + run { + val f = Decimal(32748293658335.3472302174) + val loaded = Decimal.fromByteArray(f.toByteArray()) + assertEquals(f, loaded) } } @@ -67,7 +156,7 @@ object DecimalTests { } @Test - @DisplayName("ImpreciseFraction serialization") + @DisplayName("Decimal serialization") fun serialization() { val output = FastByteArrayOutputStream() val outputStream = ObjectOutputStream(output)