Skip to content

Commit e602a9b

Browse files
committed
created new overloads for Map<*, *>.toDataRow() expanded with maxDepth and convertKeysToString to allow recursive map -> datarow conversion with key->string handling
1 parent f01a160 commit e602a9b

4 files changed

Lines changed: 156 additions & 3 deletions

File tree

core/api/core.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4312,6 +4312,9 @@ public final class org/jetbrains/kotlinx/dataframe/api/TypeConversionsKt {
43124312
public static final fun toDataFrame (Lorg/jetbrains/kotlinx/dataframe/DataRow;)Lorg/jetbrains/kotlinx/dataframe/DataFrame;
43134313
public static final fun toDataFrame (Lorg/jetbrains/kotlinx/dataframe/columns/BaseColumn;)Lorg/jetbrains/kotlinx/dataframe/DataFrame;
43144314
public static final fun toDataRow (Ljava/util/Map;)Lorg/jetbrains/kotlinx/dataframe/DataRow;
4315+
public static final fun toDataRow (Ljava/util/Map;I)Lorg/jetbrains/kotlinx/dataframe/DataRow;
4316+
public static final fun toDataRow (Ljava/util/Map;IZ)Lorg/jetbrains/kotlinx/dataframe/DataRow;
4317+
public static synthetic fun toDataRow$default (Ljava/util/Map;IZILjava/lang/Object;)Lorg/jetbrains/kotlinx/dataframe/DataRow;
43154318
public static final fun toDoubleArray (Lorg/jetbrains/kotlinx/dataframe/DataColumn;)[D
43164319
public static final fun toFloatArray (Lorg/jetbrains/kotlinx/dataframe/DataColumn;)[F
43174320
public static final fun toFrameColumn (Ljava/lang/Iterable;Ljava/lang/String;)Lorg/jetbrains/kotlinx/dataframe/columns/FrameColumn;
@@ -5300,6 +5303,7 @@ public final class org/jetbrains/kotlinx/dataframe/impl/UtilsKt {
53005303
public static final fun headPlusArray (J[J)[J
53015304
public static final fun headPlusArray (S[S)[S
53025305
public static final fun headPlusArray (Z[Z)[Z
5306+
public static final fun letIf (Ljava/lang/Object;ZLkotlin/jvm/functions/Function1;)Ljava/lang/Object;
53035307
public static final fun toCamelCaseByDelimiters (Ljava/lang/String;Lkotlin/text/Regex;Ljava/lang/String;)Ljava/lang/String;
53045308
public static synthetic fun toCamelCaseByDelimiters$default (Ljava/lang/String;Lkotlin/text/Regex;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/String;
53055309
}

core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/typeConversions.kt

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import org.jetbrains.kotlinx.dataframe.impl.columns.ColumnAccessorImpl
2929
import org.jetbrains.kotlinx.dataframe.impl.columns.asAnyFrameColumn
3030
import org.jetbrains.kotlinx.dataframe.impl.columns.asValues
3131
import org.jetbrains.kotlinx.dataframe.impl.columns.forceResolve
32+
import org.jetbrains.kotlinx.dataframe.impl.letIf
3233
import org.jetbrains.kotlinx.dataframe.impl.owner
3334
import org.jetbrains.kotlinx.dataframe.index
3435
import org.jetbrains.kotlinx.dataframe.util.DEPRECATED_ACCESS_API
@@ -404,9 +405,59 @@ public fun <T> DataRow<T>.toDataFrame(): DataFrame<T> = owner[index..index]
404405

405406
public fun AnyRow.toMap(): Map<String, Any?> = df().columns().associate { it.name() to it[index] }
406407

407-
public fun Map<String, Any?>.toDataRow(): DataRow<*> {
408-
val df = mapValues { listOf(it.value) }.toDataFrame()
409-
return DataRowImpl(0, df)
408+
/**
409+
* Converts [this] key-value [Map] to a [DataRow], representing a single row of a [DataFrame].
410+
*
411+
* By default, nested maps are ignored, but you can increase [maxDepth] to include them.
412+
* If their keys are not [String] and [convertKeysToString] is true, they are converted to strings and also converted,
413+
* else, they remain [Maps][Map].
414+
*
415+
* ### For Example
416+
*
417+
* ```kotlin
418+
* val map = mapOf("name" to "Alice", "age" to 30, "address" to mapOf("city" to "New York", "zip" to "10001"))
419+
* val dataRow = map.toDataRow(maxDepth = 1)
420+
* dataRow["name"] == "Alice"
421+
* dataRow.get { "address"["city"] } == "New York"
422+
* ```
423+
*
424+
* @param maxDepth How deep the recursion should go, converting [maps][Map] to [data rows][DataRow]. The default is 0; only top-level.
425+
* @param convertKeysToString If true, non-string keys are converted to [strings][String]. Default is `true`.
426+
* If false, nested [maps][Map] with non-string keys are ignored.
427+
* @see [Iterable.toDataFrame]
428+
*/
429+
@JvmOverloads
430+
public fun Map<*, *>.toDataRow(maxDepth: Int = 0, convertKeysToString: Boolean = true): DataRow<*> {
431+
fun Map<*, *>.recurse(currentDepth: Int): DataRow<*> {
432+
val mapped = this
433+
.letIf(convertKeysToString) { map -> map.mapKeys { it.key.toString() } }
434+
.mapValues { (_, value) ->
435+
when (value) {
436+
is Map<*, *> if currentDepth < maxDepth -> {
437+
@Suppress("UNCHECKED_CAST")
438+
try {
439+
(value as Map<String, Any?>).recurse(currentDepth + 1)
440+
} catch (_: ClassCastException) {
441+
value
442+
}
443+
}
444+
445+
else -> value
446+
}.let(::listOf)
447+
}
448+
449+
@Suppress("UNCHECKED_CAST")
450+
val df = (mapped as Map<String, List<Any?>>).toDataFrame()
451+
return DataRowImpl(0, df)
452+
}
453+
return try {
454+
this.recurse(0)
455+
} catch (e: ClassCastException) {
456+
throw IllegalArgumentException(
457+
"Toplevel map keys must be strings for conversion to DataRow. Set `convertKeysToString = true` to convert them automatically.",
458+
e,
459+
)
460+
}
410461
}
411462

412463
// endregion

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import org.jetbrains.kotlinx.dataframe.impl.columns.toColumnSet
1414
import org.jetbrains.kotlinx.dataframe.nrow
1515
import java.lang.reflect.Method
1616
import java.math.BigDecimal
17+
import kotlin.contracts.ExperimentalContracts
18+
import kotlin.contracts.InvocationKind
19+
import kotlin.contracts.contract
1720
import kotlin.reflect.KCallable
1821
import kotlin.reflect.KClass
1922
import kotlin.reflect.KFunction
@@ -511,3 +514,19 @@ internal val KCallable<*>.columnName: String
511514
is KProperty<*> -> columnName
512515
else -> findAnnotation<ColumnName>()?.name ?: getterName
513516
}
517+
518+
/**
519+
* Shortcut for
520+
* ```kt
521+
* .let { if (predicate) block(it) else it }
522+
* ```
523+
* @see let
524+
*/
525+
@OptIn(ExperimentalContracts::class)
526+
@PublishedApi
527+
internal inline fun <T> T.letIf(predicate: Boolean, block: (T) -> T): T {
528+
contract {
529+
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
530+
}
531+
return if (predicate) block(this) else this
532+
}

core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/testSets/person/DataRowTests.kt

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.jetbrains.kotlinx.dataframe.testSets.person
22

3+
import io.kotest.assertions.throwables.shouldThrow
34
import io.kotest.matchers.shouldBe
45
import org.jetbrains.kotlinx.dataframe.api.by
56
import org.jetbrains.kotlinx.dataframe.api.columnNames
@@ -127,4 +128,82 @@ class DataRowTests : BaseTest() {
127128
row["a"] shouldBe 1
128129
row["b"] shouldBe true
129130
}
131+
132+
@Test
133+
fun `toDataRow nested`() {
134+
val map = mapOf(
135+
"name" to "a",
136+
"metadata" to
137+
mapOf(
138+
"country" to "Philippines",
139+
"region" to mapOf("name" to "Caraga", "code" to "XIII"),
140+
"population" to mapOf("value" to "12345", "year" to 2020),
141+
"wrongMap" to mapOf(1 to 2, 4 to 4),
142+
),
143+
)
144+
map.toDataRow() shouldBe map.toDataRow(maxDepth = 0, convertKeysToString = true)
145+
map.toDataRow(maxDepth = 0).let { row ->
146+
row["name"] shouldBe "a"
147+
row["metadata"] shouldBe mapOf(
148+
"country" to "Philippines",
149+
"region" to mapOf("name" to "Caraga", "code" to "XIII"),
150+
"population" to mapOf("value" to "12345", "year" to 2020),
151+
"wrongMap" to mapOf(1 to 2, 4 to 4),
152+
)
153+
}
154+
155+
map.toDataRow(maxDepth = 1).let { row ->
156+
row["name"] shouldBe "a"
157+
row.getColumnGroup("metadata").let { row ->
158+
row["country"] shouldBe "Philippines"
159+
row["region"] shouldBe mapOf("name" to "Caraga", "code" to "XIII")
160+
row["population"] shouldBe mapOf("value" to "12345", "year" to 2020)
161+
row["wrongMap"] shouldBe mapOf(1 to 2, 4 to 4)
162+
}
163+
}
164+
165+
map.toDataRow(maxDepth = 2).let { row ->
166+
row["name"] shouldBe "a"
167+
row.getColumnGroup("metadata").let { row ->
168+
row["country"] shouldBe "Philippines"
169+
row.getColumnGroup("region").let { row ->
170+
row["name"] shouldBe "Caraga"
171+
row["code"] shouldBe "XIII"
172+
}
173+
row.getColumnGroup("population").let { row ->
174+
row["value"] shouldBe "12345"
175+
row["year"] shouldBe 2020
176+
}
177+
row.getColumnGroup("wrongMap").let { row ->
178+
row["1"] shouldBe 2
179+
row["4"] shouldBe 4
180+
}
181+
}
182+
}
183+
184+
map.toDataRow(maxDepth = 2, convertKeysToString = false).let { row ->
185+
row["name"] shouldBe "a"
186+
row.getColumnGroup("metadata").let { row ->
187+
row["country"] shouldBe "Philippines"
188+
row.getColumnGroup("region").let { row ->
189+
row["name"] shouldBe "Caraga"
190+
row["code"] shouldBe "XIII"
191+
}
192+
row.getColumnGroup("population").let { row ->
193+
row["value"] shouldBe "12345"
194+
row["year"] shouldBe 2020
195+
}
196+
row["wrongMap"] shouldBe mapOf(1 to 2, 4 to 4)
197+
}
198+
}
199+
200+
val otherMap = mapOf(1 to 1, "2" to 2)
201+
otherMap.toDataRow().let { row ->
202+
row["1"] shouldBe 1
203+
row["2"] shouldBe 2
204+
}
205+
shouldThrow<IllegalArgumentException> {
206+
otherMap.toDataRow(convertKeysToString = false)
207+
}
208+
}
130209
}

0 commit comments

Comments
 (0)