Skip to content

Commit 17f5a59

Browse files
committed
Support sorting FrameColumn and column of Lists by their size in notebooks
1 parent 5d6b7af commit 17f5a59

File tree

2 files changed

+111
-1
lines changed

2 files changed

+111
-1
lines changed

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import org.jetbrains.kotlinx.dataframe.api.dataFrameOf
3030
import org.jetbrains.kotlinx.dataframe.api.frames
3131
import org.jetbrains.kotlinx.dataframe.api.getColumn
3232
import org.jetbrains.kotlinx.dataframe.api.into
33+
import org.jetbrains.kotlinx.dataframe.api.isFrameColumn
34+
import org.jetbrains.kotlinx.dataframe.api.isList
3335
import org.jetbrains.kotlinx.dataframe.api.sortWith
3436
import org.jetbrains.kotlinx.dataframe.api.toDataFrame
3537
import org.jetbrains.kotlinx.dataframe.api.values
@@ -111,8 +113,15 @@ public object KotlinNotebookPluginUtils {
111113
private fun createComparator(sortKeys: List<ColumnPath>, isDesc: List<Boolean>): Comparator<DataRow<*>> {
112114
return Comparator { row1, row2 ->
113115
for ((key, desc) in sortKeys.zip(isDesc)) {
114-
val comparisonResult = if (row1.df().getColumn(key).valuesAreComparable()) {
116+
val column = row1.df().getColumn(key)
117+
val comparisonResult = if (column.valuesAreComparable()) {
115118
compareComparableValues(row1, row2, key, desc)
119+
} else if (column.isFrameColumn()) {
120+
val firstValue = column[row1].rowsCount()
121+
val secondValue = column[row2].rowsCount()
122+
firstValue.compare(secondValue, desc)
123+
} else if (column.isList()) {
124+
compareListSizes(row1, row2, key, desc)
116125
} else {
117126
compareStringValues(row1, row2, key, desc)
118127
}
@@ -124,6 +133,21 @@ public object KotlinNotebookPluginUtils {
124133
}
125134
}
126135

136+
private fun compareListSizes(
137+
row1: DataRow<*>,
138+
row2: DataRow<*>,
139+
key: ColumnPath,
140+
desc: Boolean,
141+
): Int {
142+
val firstValue = (row1.getValueOrNull(key) as? List<*>)?.size ?: 0
143+
val secondValue = (row2.getValueOrNull(key) as? List<*>)?.size ?: 0
144+
return if (desc) {
145+
secondValue.compareTo(firstValue)
146+
} else {
147+
firstValue.compareTo(secondValue)
148+
}
149+
}
150+
127151
@Suppress("UNCHECKED_CAST")
128152
private fun compareComparableValues(
129153
row1: DataRow<*>,
@@ -159,6 +183,8 @@ public object KotlinNotebookPluginUtils {
159183
}
160184
}
161185

186+
private fun <T : Comparable<T>> T.compare(other: T, desc: Boolean) = if (desc) other.compareTo(this) else this.compareTo(other)
187+
162188
internal fun isDataframeConvertable(dataframeLike: Any?): Boolean =
163189
when (dataframeLike) {
164190
is Pivot<*>,
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package org.jetbrains.kotlinx.dataframe
2+
3+
import io.kotest.matchers.shouldBe
4+
import org.jetbrains.kotlinx.dataframe.api.dataFrameOf
5+
import org.jetbrains.kotlinx.dataframe.api.toColumn
6+
import org.jetbrains.kotlinx.dataframe.jupyter.KotlinNotebookPluginUtils
7+
import org.junit.Test
8+
import kotlin.random.Random
9+
10+
/**
11+
* Other tests are located in Jupyter module:
12+
* org.jetbrains.kotlinx.dataframe.jupyter.RenderingTests
13+
*/
14+
class KotlinNotebookPluginUtilsTests {
15+
@Test
16+
fun `sort lists by size descending`() {
17+
val random = Random(123)
18+
val lists = List(20) { List(random.nextInt(1, 100)) { it } } + null
19+
val df = dataFrameOf("listColumn" to lists)
20+
21+
val res = KotlinNotebookPluginUtils.sortByColumns(df, listOf(listOf("listColumn")), desc = listOf(true))
22+
23+
res["listColumn"].values() shouldBe lists.sortedByDescending { it?.size ?: 0 }
24+
}
25+
26+
@Test
27+
fun `sort lists by size ascending`() {
28+
val lists = listOf(listOf(1, 2, 3), listOf(1), listOf(1, 2), null)
29+
val df = dataFrameOf("listColumn" to lists)
30+
31+
val res = KotlinNotebookPluginUtils.sortByColumns(df, listOf(listOf("listColumn")), desc = listOf(false))
32+
33+
res["listColumn"].values() shouldBe listOf(null, listOf(1), listOf(1, 2), listOf(1, 2, 3))
34+
}
35+
36+
@Test
37+
fun `sort empty lists`() {
38+
val lists = listOf(listOf(1, 2), emptyList(), listOf(1), emptyList())
39+
val df = dataFrameOf("listColumn" to lists)
40+
41+
val res = KotlinNotebookPluginUtils.sortByColumns(df, listOf(listOf("listColumn")), desc = listOf(true))
42+
43+
res["listColumn"].values() shouldBe listOf(listOf(1, 2), listOf(1), emptyList(), emptyList())
44+
}
45+
46+
@Test
47+
fun `sort lists with equal sizes preserves stability`() {
48+
val lists = listOf(listOf("a"), listOf("b"), listOf("c"))
49+
val df = dataFrameOf("listColumn" to lists)
50+
51+
val res = KotlinNotebookPluginUtils.sortByColumns(df, listOf(listOf("listColumn")), desc = listOf(true))
52+
53+
res["listColumn"].values() shouldBe lists
54+
}
55+
56+
@Test
57+
fun `sort frame column by row count descending`() {
58+
val frames = listOf(
59+
dataFrameOf("x" to listOf(1)),
60+
dataFrameOf("x" to listOf(1, 2, 3)),
61+
dataFrameOf("x" to listOf(1, 2)),
62+
DataFrame.empty(),
63+
)
64+
val df = dataFrameOf("nested" to frames.toColumn())
65+
66+
val res = KotlinNotebookPluginUtils.sortByColumns(df, listOf(listOf("nested")), desc = listOf(true))
67+
68+
res["nested"].values().map { (it as DataFrame<*>).rowsCount() } shouldBe listOf(3, 2, 1, 0)
69+
}
70+
71+
@Test
72+
fun `sort frame column by row count ascending`() {
73+
val frames = listOf(
74+
dataFrameOf("x" to listOf(1, 2, 3)),
75+
dataFrameOf("x" to listOf(1)),
76+
DataFrame.empty(),
77+
)
78+
val df = dataFrameOf("nested" to frames.toColumn())
79+
80+
val res = KotlinNotebookPluginUtils.sortByColumns(df, listOf(listOf("nested")), desc = listOf(false))
81+
82+
res["nested"].values().map { (it as DataFrame<*>).rowsCount() } shouldBe listOf(0, 1, 3)
83+
}
84+
}

0 commit comments

Comments
 (0)