diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/core/Formatting.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/core/Formatting.kt index 8c1ca5d4d..d1a1e0651 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/core/Formatting.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/core/Formatting.kt @@ -1,5 +1,9 @@ package ru.dbotthepony.mc.otm.core +import com.google.common.collect.ImmutableList +import net.minecraft.network.chat.Component +import net.minecraft.network.chat.TextComponent +import java.math.BigDecimal import java.math.BigInteger fun BigInteger.formatReadableNumber(): String { @@ -40,3 +44,239 @@ fun BigInteger.formatReadableNumber(): String { return String(buffer) } + +enum class SiPrefix( + val power: Int, + fractional: Boolean, + val symbol: Char, +) { + // multiplies + KILO (3, false, 'k'), + MEGA (6, false, 'M'), + GIGA (9, false, 'G'), + TERA (12, false, 'T'), + PETA (15, false, 'P'), + EXA (18, false, 'E'), + ZETTA(21, false, 'Z'), + YOTTA(24, false, 'Y'), + + // decimals + DECI (1, true, 'd'), + CENTI(2, true, 'c'), + MILLI(3, true, 'm'), + MICRO(6, true, 'μ'), + NANO (9, true, 'n'), + PICO (12, true, 'p'), + FEMTO(15, true, 'f'), + ATTO (18, true, 'a'), + ZEPTO(21, true, 'z'), + YOCTO(24, true, 'y'); + + val formatLocaleKey = "otm.suffix.${name.lowercase()}".intern() + val rawLocaleKey = "otm.suffix_raw.${name.lowercase()}".intern() + + val string = if (fractional) "0." + "0".repeat(power) else "1" + "0".repeat(power) + + fun paddedIndex(input: String, index: Int): Char { + val finalIndex = input.length - power + index + + if (finalIndex >= 0) { + return input[finalIndex] + } + + return '0' + } + + val decimal = BigDecimal(string) + val fraction = Fraction(decimal) + val impreciseFraction = ImpreciseFraction(string) + val integer = if (!fractional) BigInteger(string) else null + + companion object { + val MULTIPLIES: List = ImmutableList.builder() + .add(KILO) + .add(MEGA) + .add(GIGA) + .add(TERA) + .add(PETA) + .add(EXA) + .add(ZETTA) + .add(YOTTA) + .build() + + val DECIMALS: List = ImmutableList.builder() + .add(DECI) + .add(CENTI) + .add(MILLI) + .add(MICRO) + .add(NANO) + .add(PICO) + .add(FEMTO) + .add(ATTO) + .add(ZEPTO) + .add(YOCTO) + .build() + + val DECIMALS_IMPRECISE: List = ImmutableList.builder() + .add(DECI) + .add(CENTI) + .add(MILLI) + .add(MICRO) + .build() + } +} + +private fun formatSi(si: SiPrefix, divided: String, remainder: String, decimalPlaces: Int, isNegative: Boolean): String { + @Suppress("NAME_SHADOWING") + val decimalPlaces = decimalPlaces.coerceAtMost(si.power) + + val add = (if (isNegative) 1 else 0) + + val buffer = CharArray(divided.length + 2 + decimalPlaces + add) + buffer[buffer.size - 1] = si.symbol + + if (isNegative) { + buffer[0] = '-' + } + + for (i in divided.indices) { + buffer[add + i] = divided[i] + } + + buffer[add + divided.length] = '.' + + for (i in 0 until decimalPlaces) { + buffer[add + i + divided.length + 1] = si.paddedIndex(remainder, i) + } + + return String(buffer) +} + +fun BigDecimal.determineSiPrefix(): SiPrefix? { + if (isZero) { + return null + } + + var num = this + + if (isNegative) { + num = -this + } + + var prev: SiPrefix? = null + + if (num >= BigDecimal.ONE) { + for (value in SiPrefix.MULTIPLIES) { + if (value.decimal <= num) { + prev = value + } else { + break + } + } + } else { + for (value in SiPrefix.DECIMALS) { + if (value.decimal >= num) { + prev = value + } else { + break + } + } + } + + return prev +} + +fun BigDecimal.formatSiTranslatable(): Component { + if (isZero) { + return TextComponent("0.00") + } else if (this == BigDecimal.ONE) { + return TextComponent("1.00") + } + + return TextComponent("1.00") +} + +fun BigInteger.determineSiPrefix(): SiPrefix? { + if (isZero) { + return null + } + + var num = this + + if (isNegative) { + num = -this + } + + var prev: SiPrefix? = null + + if (num >= BigInteger.ONE) { + for (value in SiPrefix.MULTIPLIES) { + if (value.integer!! <= num) { + prev = value + } else { + break + } + } + } + + return prev +} + +fun BigInteger.formatSi(decimalPlaces: Int = 2): String { + require(decimalPlaces >= 0) { "Invalid amount of decimal places required: $decimalPlaces" } + val prefix = determineSiPrefix() ?: return toString() // + "." + "0".repeat(decimalPlaces) + val isNegative = isNegative + val arr = (if (isNegative) -this else this).divideAndRemainder(prefix.integer) + val divided = arr[0] + val remainder = arr[1] + + if (decimalPlaces == 0) { + if (isNegative) { + return "-" + divided.toString() + prefix.symbol + } else { + return divided.toString() + prefix.symbol + } + } + + return formatSi(prefix, divided.toString(), remainder.toString(), decimalPlaces, isNegative) +} + +fun ImpreciseFraction.determineSiPrefix(): SiPrefix? { + if (isZero) { + return null + } + + var num = this + + if (isNegative) { + num = -this + } + + var prev: SiPrefix? = null + + if (num >= ImpreciseFraction.ONE) { + for (value in SiPrefix.MULTIPLIES) { + if (value.impreciseFraction <= num) { + prev = value + } else { + break + } + } + } else { + for (value in SiPrefix.DECIMALS_IMPRECISE) { + if (value.impreciseFraction >= num) { + prev = value + } else { + break + } + } + } + + return prev +} + +fun ImpreciseFraction.formatSi(decimalPlaces: Int = 2): String { + require(decimalPlaces >= 0) { "Invalid amount of decimal places required: $decimalPlaces" } + val prefix = determineSiPrefix() ?: return toString(decimalPlaces) + return (this / prefix.impreciseFraction).toString(decimalPlaces) + prefix.symbol +} diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/core/ImpreciseFraction.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/core/ImpreciseFraction.kt index a82c81cf7..e40069dfa 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/core/ImpreciseFraction.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/core/ImpreciseFraction.kt @@ -75,6 +75,27 @@ private fun bytesToLongBE( */ 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 @@ -370,27 +391,12 @@ class ImpreciseFraction @JvmOverloads constructor(whole: BigInteger, decimal: Do if (decimals == 0) { return whole.toString() } else if (decimals > 0) { - val strDecimal = if (decimal == 0.0 || decimal == -0.0 || decimal > 0.0 && decimal < 1E-11 || decimal < 0.0 && decimal > -1E-11) { - "0" - } else if (decimal > 0.0) { - decimal.toString().substring(2) - } else { - decimal.toString().substring(3) - } - - if (strDecimal.length < decimals) { - return "$whole.${strDecimal + "0".repeat(decimals - strDecimal.length)}" - } else if (strDecimal.length > decimals) { - return "$whole.${strDecimal.substring(0, decimals)}" - } else { - return "$whole.$strDecimal" - } + return "$whole.${decimals(decimal, decimals)}" } return when (signum()) { - 1 -> "$whole.${decimal.toString().substring(2)}" + 1, -1 -> "$whole.${decimals(decimal, 11)}" 0 -> "0.0" - -1 -> "$whole.${decimal.toString().substring(3)}" else -> throw IllegalArgumentException("invalid signum") } } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/core/Math.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/core/Math.kt index a529d8367..0d1a04b9a 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/core/Math.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/core/Math.kt @@ -1,11 +1,16 @@ package ru.dbotthepony.mc.otm.core +import java.math.BigDecimal import java.math.BigInteger 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 BigDecimal.isZero get() = this == BigDecimal.ZERO +inline val BigDecimal.isPositive get() = this > BigDecimal.ZERO +inline val BigDecimal.isNegative get() = this < BigDecimal.ZERO + @Suppress("SameParameterValue") fun equalDownDivision(a: Int, b: Int): Int { if (a % b == 0) { diff --git a/src/test/kotlin/ru/dbotthepony/mc/otm/tests/FormattingTests.kt b/src/test/kotlin/ru/dbotthepony/mc/otm/tests/FormattingTests.kt index 6402b6ead..3096a73eb 100644 --- a/src/test/kotlin/ru/dbotthepony/mc/otm/tests/FormattingTests.kt +++ b/src/test/kotlin/ru/dbotthepony/mc/otm/tests/FormattingTests.kt @@ -3,12 +3,14 @@ package ru.dbotthepony.mc.otm.tests import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import ru.dbotthepony.mc.otm.core.ImpreciseFraction import ru.dbotthepony.mc.otm.core.formatReadableNumber +import ru.dbotthepony.mc.otm.core.formatSi import java.math.BigInteger object FormattingTests { @Test - @DisplayName("BigInteger formatting") + @DisplayName("BigInteger formatting as readable number") fun biginteger() { assertEquals("0", BigInteger("0").formatReadableNumber()) assertEquals("45", BigInteger("45").formatReadableNumber()) @@ -29,4 +31,39 @@ object FormattingTests { assertEquals("2 730 250 200", BigInteger("2730250200").formatReadableNumber()) assertEquals("1 222 730 250 200", BigInteger("1222730250200").formatReadableNumber()) } + + @Test + @DisplayName("BigInteger formatting as si number") + fun bigintegerSi() { + assertEquals("0", BigInteger("0").formatSi()) + assertEquals("420", BigInteger("420").formatSi()) + assertEquals("-420", BigInteger("-420").formatSi()) + assertEquals("555", BigInteger("555").formatSi()) + assertEquals("55", BigInteger("55").formatSi()) + assertEquals("1.20k", BigInteger("1205").formatSi()) + assertEquals("-1.20k", BigInteger("-1205").formatSi()) + assertEquals("1.21k", BigInteger("1215").formatSi()) + assertEquals("4.50M", BigInteger("4501204").formatSi()) + assertEquals("4.00M", BigInteger("4000111").formatSi()) + assertEquals("4.0011M", BigInteger("4001111").formatSi(4)) + assertEquals("-4.0011M", BigInteger("-4001111").formatSi(4)) + } + + @Test + @DisplayName("ImpreciseFraction formatting as si number") + fun impreciseFractionSi() { + assertEquals("0.00", ImpreciseFraction("0").formatSi(2)) + assertEquals("14.62", ImpreciseFraction("14.62").formatSi(2)) + assertEquals("1.00k", ImpreciseFraction("1000").formatSi(2)) + assertEquals("1.00k", ImpreciseFraction("1000.1").formatSi(2)) + assertEquals("1.00k", ImpreciseFraction("1004.2").formatSi(2)) + assertEquals("1.01k", ImpreciseFraction("1014.5").formatSi(2)) + assertEquals("1.014k", ImpreciseFraction("1014.5").formatSi(3)) + assertEquals("1.01k", ImpreciseFraction("1014.256").formatSi(2)) + assertEquals("12.73k", ImpreciseFraction("12734.256").formatSi(2)) + assertEquals("127.34k", ImpreciseFraction("127342.256").formatSi(2)) + assertEquals("1.27M", ImpreciseFraction("1273421.256").formatSi(2)) + assertEquals("1.273M", ImpreciseFraction("1273421.256").formatSi(3)) + assertEquals("1.2734M", ImpreciseFraction("1273421.256").formatSi(4)) + } }