New SI formatting or something

This commit is contained in:
DBotThePony 2022-07-03 21:53:36 +07:00
parent 5833448584
commit 73e0c9a57e
Signed by: DBot
GPG Key ID: DCC23B5715498507
4 changed files with 306 additions and 18 deletions

View File

@ -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<SiPrefix> = ImmutableList.builder<SiPrefix>()
.add(KILO)
.add(MEGA)
.add(GIGA)
.add(TERA)
.add(PETA)
.add(EXA)
.add(ZETTA)
.add(YOTTA)
.build()
val DECIMALS: List<SiPrefix> = ImmutableList.builder<SiPrefix>()
.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<SiPrefix> = ImmutableList.builder<SiPrefix>()
.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
}

View File

@ -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")
}
}

View File

@ -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) {

View File

@ -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))
}
}