Skip to content

Commit cd7728e

Browse files
committed
Fix rendering of doubles with exponent
1 parent 23ffac9 commit cd7728e

File tree

6 files changed

+74
-38
lines changed

6 files changed

+74
-38
lines changed

core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/MathUtils.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import org.jetbrains.kotlinx.dataframe.io.defaultPrecision
55
import org.jetbrains.kotlinx.dataframe.typeClass
66
import java.math.BigDecimal
77

8-
internal fun <T : Number> DataColumn<T?>.precision(): Int {
8+
internal fun <T : Number> DataColumn<T?>.scale(): Int {
99
if (size() == 0) return 0
1010
return when (typeClass) {
1111
Double::class -> values().maxOf { (it as? Double)?.scale() ?: 1 }

core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/html.kt

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import org.jetbrains.kotlinx.dataframe.api.isSubtypeOf
1313
import org.jetbrains.kotlinx.dataframe.api.rows
1414
import org.jetbrains.kotlinx.dataframe.columns.ColumnGroup
1515
import org.jetbrains.kotlinx.dataframe.impl.DataFrameSize
16-
import org.jetbrains.kotlinx.dataframe.impl.precision
1716
import org.jetbrains.kotlinx.dataframe.impl.renderType
17+
import org.jetbrains.kotlinx.dataframe.impl.scale
1818
import org.jetbrains.kotlinx.dataframe.impl.truncate
1919
import org.jetbrains.kotlinx.dataframe.jupyter.CellRenderer
2020
import org.jetbrains.kotlinx.dataframe.jupyter.RenderedContent
@@ -112,8 +112,13 @@ internal fun AnyFrame.toHtmlData(
112112

113113
fun AnyFrame.columnToJs(col: AnyCol, rowsLimit: Int): ColumnDataForJs {
114114
val values = rows().take(rowsLimit)
115-
val precision = if (col.isNumber()) col.asNumbers().precision() else 1
116-
val renderConfig = configuration.copy(precision = precision)
115+
val scale = if (col.isNumber()) col.asNumbers().scale() else 1
116+
val format = if (scale > 0) {
117+
RendererDecimalFormat.fromPrecision(scale)
118+
} else {
119+
RendererDecimalFormat.of("%e")
120+
}
121+
val renderConfig = configuration.copy(decimalFormat = format)
117122
val contents = values.map {
118123
val value = it[col]
119124
if (value is AnyFrame) {
@@ -205,7 +210,7 @@ public data class DisplayConfiguration(
205210
var rowsLimit: Int = 20,
206211
var cellContentLimit: Int = 40,
207212
var cellFormatter: RowColFormatter<*, *>? = null,
208-
var precision: Int = defaultPrecision,
213+
var decimalFormat: RendererDecimalFormat = RendererDecimalFormat.DEFAULT,
209214
var isolatedOutputs: Boolean = flagFromEnv("LETS_PLOT_HTML_ISOLATED_FRAME"),
210215
internal val localTesting: Boolean = flagFromEnv("KOTLIN_DATAFRAME_LOCAL_TESTING"),
211216
) {
@@ -214,6 +219,24 @@ public data class DisplayConfiguration(
214219
}
215220
}
216221

222+
@JvmInline
223+
public value class RendererDecimalFormat private constructor(internal val format: String) {
224+
public companion object {
225+
public fun fromPrecision(precision: Int): RendererDecimalFormat {
226+
require(precision >= 0) { "precision must be >= 0. for custom format use RendererDecimalFormat.of" }
227+
return RendererDecimalFormat("%.${precision}f")
228+
}
229+
230+
public fun of(format: String): RendererDecimalFormat {
231+
return RendererDecimalFormat(format)
232+
}
233+
234+
internal val DEFAULT: RendererDecimalFormat = fromPrecision(defaultPrecision)
235+
}
236+
}
237+
238+
internal const val defaultPrecision = 6
239+
217240
private fun flagFromEnv(envName: String): Boolean {
218241
return System.getenv(envName)?.toBooleanStrictOrNull() ?: false
219242
}
@@ -222,8 +245,8 @@ internal fun String.escapeNewLines() = replace("\n", "\\n").replace("\r", "\\r")
222245

223246
internal fun String.escapeForHtmlInJs() = replace("\"", "\\\"").escapeNewLines()
224247

225-
internal fun renderValueForHtml(value: Any?, truncate: Int, precision: Int): RenderedContent {
226-
return formatter.truncate(renderValueToString(value, precision), truncate)
248+
internal fun renderValueForHtml(value: Any?, truncate: Int, format: RendererDecimalFormat): RenderedContent {
249+
return formatter.truncate(renderValueToString(value, format), truncate)
227250
}
228251

229252
internal fun String.escapeHTML(): String {

core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/io/string.kt

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import org.jetbrains.kotlinx.dataframe.api.isNumber
88
import org.jetbrains.kotlinx.dataframe.api.take
99
import org.jetbrains.kotlinx.dataframe.api.toColumn
1010
import org.jetbrains.kotlinx.dataframe.impl.owner
11-
import org.jetbrains.kotlinx.dataframe.impl.precision
1211
import org.jetbrains.kotlinx.dataframe.impl.renderType
12+
import org.jetbrains.kotlinx.dataframe.impl.scale
1313
import org.jetbrains.kotlinx.dataframe.impl.truncate
1414
import org.jetbrains.kotlinx.dataframe.index
1515
import org.jetbrains.kotlinx.dataframe.jupyter.RenderedContent
@@ -45,9 +45,10 @@ internal fun AnyFrame.renderToString(
4545
}
4646
val values = cols.map {
4747
val top = it.take(rowsLimit)
48-
val precision = if (top.isNumber()) top.asNumbers().precision() else 0
48+
val precision = if (top.isNumber()) top.asNumbers().scale() else 0
49+
val decimalFormat = if (precision >= 0) RendererDecimalFormat.fromPrecision(precision) else RendererDecimalFormat.of("%e")
4950
top.values().map {
50-
renderValueForStdout(it, valueLimit, precision = precision).truncatedContent
51+
renderValueForStdout(it, valueLimit, decimalFormat = decimalFormat).truncatedContent
5152
}
5253
}
5354
val columnLengths = values.mapIndexed { col, vals -> (vals + header[col]).map { it.length }.maxOrNull()!! + 1 }
@@ -150,26 +151,24 @@ internal fun renderValueForRowTable(value: Any?, forHtml: Boolean): RenderedCont
150151
}
151152
is AnyRow -> RenderedContent.textWithLength("DataRow $value", "DataRow".length)
152153
is Collection<*> -> renderCollectionName(value).let { RenderedContent.textWithLength("$it $value", it.length) }
153-
else -> if (forHtml) renderValueForHtml(value, valueToStringLimitForRowAsTable, defaultPrecision)
154+
else -> if (forHtml) renderValueForHtml(value, valueToStringLimitForRowAsTable, RendererDecimalFormat.DEFAULT)
154155
else renderValueForStdout(value, valueToStringLimitForRowAsTable)
155156
}
156157

157158
internal fun renderValueForStdout(
158159
value: Any?,
159160
limit: Int = valueToStringLimitDefault,
160-
precision: Int = defaultPrecision
161+
decimalFormat: RendererDecimalFormat = RendererDecimalFormat.DEFAULT
161162
): RenderedContent =
162-
renderValueToString(value, precision).truncate(limit)
163+
renderValueToString(value, decimalFormat).truncate(limit)
163164
.let { it.copy(truncatedContent = it.truncatedContent.escapeNewLines()) }
164165

165-
internal val defaultPrecision = 6
166-
167-
internal fun renderValueToString(value: Any?, precision: Int) =
166+
internal fun renderValueToString(value: Any?, decimalFormat: RendererDecimalFormat) =
168167
when (value) {
169168
is AnyFrame -> "[${value.size}]".let { if (value.nrow == 1) it + " " + value[0].toString() else it }
170-
is Double -> value.format(precision)
171-
is Float -> value.format(precision)
172-
is BigDecimal -> value.format(precision)
169+
is Double -> value.format(decimalFormat)
170+
is Float -> value.format(decimalFormat)
171+
is BigDecimal -> value.format(decimalFormat)
173172
is List<*> -> if (value.isEmpty()) "[ ]" else value.toString()
174173
else -> value.toString()
175174
}
@@ -181,6 +180,6 @@ internal fun internallyRenderable(value: Any?): Boolean {
181180
}
182181
}
183182

184-
internal fun Double.format(precision: Int): String = "%.${precision}f".format(this)
185-
internal fun Float.format(precision: Int): String = "%.${precision}f".format(this)
186-
internal fun BigDecimal.format(precision: Int): String = "%.${precision}f".format(this)
183+
internal fun Double.format(decimalFormat: RendererDecimalFormat): String = decimalFormat.format.format(this)
184+
internal fun Float.format(decimalFormat: RendererDecimalFormat): String = decimalFormat.format.format(this)
185+
internal fun BigDecimal.format(decimalFormat: RendererDecimalFormat): String = decimalFormat.format.format(this)

core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/jupyter/CellRenderer.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,11 @@ public abstract class ChainedCellRenderer(
6161

6262
public object DefaultCellRenderer : CellRenderer {
6363
public override fun content(value: Any?, configuration: DisplayConfiguration): RenderedContent {
64-
return renderValueForHtml(value, configuration.cellContentLimit, configuration.precision)
64+
return renderValueForHtml(value, configuration.cellContentLimit, configuration.decimalFormat)
6565
}
6666

6767
public override fun tooltip(value: Any?, configuration: DisplayConfiguration): String {
68-
return renderValueForHtml(value, tooltipLimit, configuration.precision).truncatedContent
68+
return renderValueForHtml(value, tooltipLimit, configuration.decimalFormat).truncatedContent
6969
}
7070
}
7171

@@ -81,7 +81,7 @@ internal class JupyterCellRenderer(
8181
if (finalVal is MimeTypedResult && "text/html" in finalVal) return RenderedContent.media(
8282
finalVal["text/html"] ?: ""
8383
)
84-
return renderValueForHtml(finalVal, configuration.cellContentLimit, configuration.precision)
84+
return renderValueForHtml(finalVal, configuration.cellContentLimit, configuration.decimalFormat)
8585
}
8686

8787
override fun maybeTooltip(value: Any?, configuration: DisplayConfiguration): String? {

core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/rendering/PrecisionTests.kt

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ package org.jetbrains.kotlinx.dataframe.rendering
33
import io.kotest.matchers.shouldBe
44
import org.jetbrains.kotlinx.dataframe.api.columnOf
55
import org.jetbrains.kotlinx.dataframe.api.filter
6-
import org.jetbrains.kotlinx.dataframe.impl.precision
6+
import org.jetbrains.kotlinx.dataframe.impl.scale
7+
import org.jetbrains.kotlinx.dataframe.io.RendererDecimalFormat
78
import org.jetbrains.kotlinx.dataframe.io.defaultPrecision
89
import org.jetbrains.kotlinx.dataframe.io.format
910
import org.junit.Test
@@ -12,29 +13,31 @@ class PrecisionTests {
1213

1314
@Test
1415
fun precision() {
15-
columnOf(1.2, 3.2).precision() shouldBe 1
16-
columnOf(1.1232, 3.2).precision() shouldBe 4
17-
columnOf(1.1220001, 12313).precision() shouldBe defaultPrecision
18-
columnOf(1, 2).precision() shouldBe 0
19-
columnOf(1.0, 2).precision() shouldBe 1
20-
columnOf(123121.0, -1231.0).precision() shouldBe 1
21-
columnOf(123121.00001, -1231.120).precision() shouldBe 5
22-
columnOf(0.000343434343434343434343).precision() shouldBe defaultPrecision
16+
columnOf(1.2, 3.2).scale() shouldBe 1
17+
columnOf(1.1232, 3.2).scale() shouldBe 4
18+
columnOf(1.1220001, 12313).scale() shouldBe defaultPrecision
19+
columnOf(1, 2).scale() shouldBe 0
20+
columnOf(1.0, 2).scale() shouldBe 1
21+
columnOf(123121.0, -1231.0).scale() shouldBe 1
22+
columnOf(123121.00001, -1231.120).scale() shouldBe 5
23+
columnOf(0.000343434343434343434343).scale() shouldBe defaultPrecision
24+
columnOf(1E24).scale() shouldBe -23
2325
}
2426

2527
@Test
2628
fun format() {
2729
val value = 1.2341
2830
val expected = "1.23"
2931
val digits = 2
30-
value.format(digits) shouldBe expected
31-
value.toFloat().format(digits) shouldBe expected
32-
value.toBigDecimal().format(digits) shouldBe expected
32+
val formatter = RendererDecimalFormat.fromPrecision(digits)
33+
value.format(formatter) shouldBe expected
34+
value.toFloat().format(formatter) shouldBe expected
35+
value.toBigDecimal().format(formatter) shouldBe expected
3336
}
3437

3538
@Test
3639
fun emptyColPrecision() {
3740
val col by columnOf(1.0)
38-
col.filter { false }.precision() shouldBe 0
41+
col.filter { false }.scale() shouldBe 0
3942
}
4043
}

core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/rendering/RenderingTests.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,15 @@ class RenderingTests {
105105
.group { all() }.into("Campus")
106106
df.toHTML().print()
107107
}
108+
109+
@Test
110+
fun `render double with exponent`() {
111+
listOf(
112+
dataFrameOf("col")(1E27) to "1.000000e+27",
113+
dataFrameOf("col")(1.123) to "1.123",
114+
dataFrameOf("col")(1.0) to "1.0",
115+
).forEach { (df, rendered) ->
116+
df.toHTML().script shouldContain rendered
117+
}
118+
}
108119
}

0 commit comments

Comments
 (0)