Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NumberFormat bugfix #1242

Merged
merged 15 commits into from
Nov 12, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
/*
* Copyright (c) 2024. JetBrains s.r.o.
* Use of this source code is governed by the MIT license that can be found in the LICENSE file.
*/

package org.jetbrains.letsPlot.commons.formatting.number

import kotlin.math.absoluteValue
import kotlin.math.sign

internal class Decimal internal constructor(
wholePart: String,
decimalPart: String,
sign: String
) {
val wholePart: String // never empty. "0" for zero, never contains leading zeros
val decimalPart: String // never empty. "0" for zero, never contains trailing zeros
val sign: String // empty for positive, "-" for negative
val isNegative = sign == "-"

init {
require(wholePart.all { it.isDigit() }) {
"Invalid wholePart: $wholePart"
}

require(decimalPart.all { it.isDigit() }) {
"Invalid decimalPart: $decimalPart"
}

require(sign == "" || sign == "-") {
"Sign should be empty or '-'"
}

this.wholePart = wholePart.trimStart('0').takeIf { it.isNotEmpty() } ?: "0"
this.decimalPart = decimalPart.trimEnd('0').takeIf { it.isNotEmpty() } ?: "0"
this.sign = sign
}

val isWholePartZero = this.wholePart == "0"
val isDecimalPartZero = this.decimalPart == "0"
val isZero = isWholePartZero && isDecimalPartZero

// Returns the whole part as a Long or null if the number is larger than Long.MAX_VALUE.
val wholeValue: Long? by lazy { wholePart.toLongOrNull() }

fun toDouble(): Double {
return "$sign$wholePart.$decimalPart".toDouble()
}

val asFloating: Floating by lazy { toFloating() }

fun toFloating(): Floating {
if (wholePart == "0") {
val significandDigitPos = decimalPart.indexOfFirst { it != '0' }
if (significandDigitPos == -1) {
return Floating(0, "0", 0)
}

val i = decimalPart[significandDigitPos].digitToInt()
val e = -(significandDigitPos + 1)
val fracPart = decimalPart.drop(significandDigitPos + 1)
return Floating(i, fracPart, e)
} else {
val i = wholePart[0].digitToInt()
val e = wholePart.length - 1
val fracPart = wholePart.drop(1) + decimalPart.toInt()
return Floating(i, fracPart, e)
}
}

// Shift decimal point to the left (shift < 0) or to the right (shift > 0).
fun shiftDecimalPoint(shift: Int): Decimal {
if (shift == 0) {
return this
}

if (shift > 0) {
if (decimalPart.length <= shift) {
val zeros = "0".repeat(shift - decimalPart.length)
return Decimal(wholePart + decimalPart + zeros, "0", sign)
} else {
val newIntPart = wholePart + decimalPart.take(shift)
val newFracPart = decimalPart.drop(shift)
return Decimal(newIntPart, newFracPart, sign)
}
} else {
if (shift.absoluteValue >= wholePart.length) {
val zeros = "0".repeat(shift.absoluteValue - wholePart.length)
return Decimal("0", zeros + wholePart + decimalPart, sign)
} else {
val newIntPart = wholePart.take(wholePart.length - shift.absoluteValue)
val newFracPart = wholePart.takeLast(shift.absoluteValue) + decimalPart
return Decimal(newIntPart, newFracPart, sign)
}
}
}

fun iRound(precision: Int): Decimal {
val number = wholePart + decimalPart
val (roundedNumber, _) = iRound(number, precision)

val decimalPoint = wholePart.length + if (roundedNumber.length > number.length) 1 else 0
val roundedIntPart = roundedNumber.take(decimalPoint)
val roundedFracPart = roundedNumber.drop(decimalPoint)

return Decimal(roundedIntPart, roundedFracPart, sign)
}

fun fRound(precision: Int): Decimal {
if (decimalPart.length <= precision) {
return this
}

val (roundedFracPart, carry) = round(decimalPart, precision)
val roundedIntPart = if (carry) add(wholePart, "1").first else wholePart
return Decimal(roundedIntPart, roundedFracPart, sign)
}


override fun toString(): String = "$sign$wholePart.$decimalPart"

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false

other as Decimal

if (wholePart != other.wholePart) return false
if (decimalPart != other.decimalPart) return false
if (sign != other.sign) return false

return true
}

override fun hashCode(): Int {
var result = wholePart.hashCode()
result = 31 * result + decimalPart.hashCode()
result = 31 * result + sign.hashCode()
return result
}


companion object {
val ZERO: Decimal = Decimal("0", "0", "")

fun fromNumber(value: Number): Decimal {
val dbl = value.toDouble()

if (dbl.isNaN()) {
return Decimal("NaN", "", "")
}
if (dbl.isInfinite()) {
return Decimal("Infinity", "", if (dbl < 0) "-" else "")
}

val (intStr, fracStr, exponentString) =
"^(\\d+)\\.?(\\d+)?e?([+-]?\\d+)?\$"
.toRegex()
.find(dbl.absoluteValue.toString().lowercase())
?.destructured
?: error("Wrong number: $value")

val exp = exponentString.toIntOrNull() ?: 0
val sign = if (dbl.sign < 0) "-" else ""

val (intPart, fracPart) = when {
exp == 0 -> intStr to fracStr
exp > 0 -> {
if (exp < fracStr.length) {
val intPart = intStr + fracStr.take(exp)
val fracPart = fracStr.drop(exp)
intPart to fracPart
} else {
val intPart = intStr + fracStr + "0".repeat(exp - fracStr.length)
intPart to "0"
}
}

exp < 0 -> {
val fracPart = "0".repeat(-exp - intStr.length) + intStr + fracStr
"0" to fracPart
}

else -> error("Unexpected state. value: $exp")
}

return Decimal(intPart, fracPart, sign)
}

// Add two numbers represented as strings.
// Returns a pair of the result and a carry flag (true if the result has an additional digit).
private fun add(a: String, b: String): Pair<String, Boolean> {
val maxLength = maxOf(a.length, b.length)
val range = (0 until maxLength).reversed()

val deltaA = a.length - maxLength
val deltaB = b.length - maxLength

var carry = 0
val result = CharArray(maxLength)
for (i in range) {
val va = a.getOrNull(i + deltaA)?.digitToInt() ?: 0
val vb = b.getOrNull(i + deltaB)?.digitToInt() ?: 0
val sum = va + vb + carry
result[i] = '0' + (sum % 10)
carry = sum / 10
}

return if (carry == 0) {
result.concatToString() to false
} else {
("1" + result.concatToString()) to true
}
}

private fun carryOnRound(number: String): Boolean {
when (number.length) {
0 -> return false
1 -> return number.single() >= '5'
else -> {
if (number.first() >= '5') return true
if (number.first() == '5' && number.asSequence().drop(1).any { it > '0' }) return true
return false
}
}
}

private fun round(number: String, precision: Int): Pair<String, Boolean> {
val trailingPart = number.takeLast(number.length - precision)
val significantPart = number.take(precision)

val carry = carryOnRound(trailingPart)

val (fRoundedRestPart, carryFromFracToInt) = when {
significantPart.isEmpty() -> "" to carry // round to integer - no fractional part
else -> add(significantPart, if (carry) "1" else "0")
}

val resultFracPart = if (carryFromFracToInt) fRoundedRestPart.drop(1) else fRoundedRestPart

return resultFracPart to carryFromFracToInt
}

private fun iRound(number: String, precision: Int): Pair<String, Boolean> {
// special cases like:
// round(16.5, 0) => 20.0
// Without this line, it would be 0.0
@Suppress("NAME_SHADOWING")
val precision = if (precision == 0) 1 else precision

if (number.length <= precision) {
return number to false
}

return when (val roundingPartLength = number.length - precision) {
0 -> number to false
else -> when (number[precision] >= '5') {
true -> {
val valuePart = number.take(precision) + "0".repeat(roundingPartLength) // zeroing the rounding part
val carryValue = "1" + "0".repeat(roundingPartLength) // carry to the value part
add(valuePart, carryValue).first to true
}
false -> {
val valuePart = number.take(precision) + "0".repeat(roundingPartLength)
valuePart to false
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2024. JetBrains s.r.o.
* Use of this source code is governed by the MIT license that can be found in the LICENSE file.
*/

package org.jetbrains.letsPlot.commons.formatting.number

class Floating(i: Int, fraction: String, exp: Int) {
val i: Int // 1..9 or 0 for 0.0
val fraction: String // never empty
val exp: Int

init {
require(i in 0..9) { "i should be in 0..9" }
this.i = i
this.fraction = fraction.takeIf { it.isNotEmpty() } ?: "0"
this.exp = exp
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false

other as Floating

if (i != other.i) return false
if (fraction != other.fraction) return false
if (exp != other.exp) return false

return true
}

override fun hashCode(): Int {
var result = i
result = 31 * result + fraction.hashCode()
result = 31 * result + exp
return result
}

override fun toString(): String {
return "Floating(i=$i, fraction='$fraction', e=$exp)"
}
}
Loading