Skip to content

Commit 0318952

Browse files
committed
Align preserve/unfold logic for nested and top objects
1 parent b67c871 commit 0318952

File tree

6 files changed

+85
-40
lines changed

6 files changed

+85
-40
lines changed

core/api/core.api

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5907,8 +5907,8 @@ public final class org/jetbrains/kotlinx/dataframe/impl/api/SchemaKt {
59075907
}
59085908

59095909
public final class org/jetbrains/kotlinx/dataframe/impl/api/ToDataFrameKt {
5910-
public static final fun convertToDataFrame (Ljava/lang/Iterable;Lkotlin/reflect/KClass;Ljava/util/List;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;I)Lorg/jetbrains/kotlinx/dataframe/DataFrame;
5911-
public static final fun createDataFrameImpl (Ljava/lang/Iterable;Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/kotlinx/dataframe/DataFrame;
5910+
public static final fun convertToDataFrame (Ljava/lang/Iterable;Lkotlin/reflect/KType;Ljava/util/List;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;I)Lorg/jetbrains/kotlinx/dataframe/DataFrame;
5911+
public static final fun createDataFrameImpl (Ljava/lang/Iterable;Lkotlin/reflect/KType;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/kotlinx/dataframe/DataFrame;
59125912
public static final fun getCanBeUnfolded (Lkotlin/reflect/KClass;)Z
59135913
public static final fun getHasProperties (Lkotlin/reflect/KClass;)Z
59145914
public static final fun isValueType (Lkotlin/reflect/KClass;)Z
@@ -5919,7 +5919,7 @@ public final class org/jetbrains/kotlinx/dataframe/impl/api/ToSequenceKt {
59195919
}
59205920

59215921
public final class org/jetbrains/kotlinx/dataframe/impl/api/UnfoldKt {
5922-
public static final fun unfoldImpl (Lorg/jetbrains/kotlinx/dataframe/DataColumn;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/kotlinx/dataframe/DataColumn;
5922+
public static final fun unfoldImpl (Lorg/jetbrains/kotlinx/dataframe/DataColumn;Lkotlin/reflect/KType;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/kotlinx/dataframe/DataColumn;
59235923
}
59245924

59255925
public final class org/jetbrains/kotlinx/dataframe/impl/api/UpdateKt {

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

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import org.jetbrains.kotlinx.dataframe.annotations.Interpretable
1111
import org.jetbrains.kotlinx.dataframe.annotations.Refine
1212
import org.jetbrains.kotlinx.dataframe.columns.ColumnPath
1313
import org.jetbrains.kotlinx.dataframe.impl.ColumnNameGenerator
14-
import org.jetbrains.kotlinx.dataframe.impl.api.canBeUnfolded
1514
import org.jetbrains.kotlinx.dataframe.impl.api.createDataFrameImpl
1615
import org.jetbrains.kotlinx.dataframe.impl.asList
1716
import org.jetbrains.kotlinx.dataframe.impl.columnName
@@ -21,28 +20,21 @@ import org.jetbrains.kotlinx.dataframe.util.DEPRECATED_ACCESS_API
2120
import kotlin.reflect.KCallable
2221
import kotlin.reflect.KClass
2322
import kotlin.reflect.KProperty
23+
import kotlin.reflect.typeOf
2424

2525
// region read DataFrame from objects
2626

2727
@Refine
2828
@Interpretable("toDataFrameDefault")
2929
public inline fun <reified T> Iterable<T>.toDataFrame(): DataFrame<T> =
3030
toDataFrame {
31-
// check if type is value: primitives, primitive arrays, datetime types etc.,
32-
// or has no properties
33-
if (!T::class.canBeUnfolded) {
34-
// create a single `value` column
35-
ValueProperty<T>::value.name from { it }
36-
} else {
37-
// otherwise creates columns based on properties
38-
properties()
39-
}
31+
properties()
4032
}
4133

4234
@Refine
4335
@Interpretable("toDataFrameDsl")
4436
public inline fun <reified T> Iterable<T>.toDataFrame(noinline body: CreateDataFrameDsl<T>.() -> Unit): DataFrame<T> =
45-
createDataFrameImpl(T::class, body)
37+
createDataFrameImpl(typeOf<T>(), body)
4638

4739
@Refine
4840
@Interpretable("toDataFrame")

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,18 @@ import org.jetbrains.kotlinx.dataframe.impl.api.unfoldImpl
1313
import org.jetbrains.kotlinx.dataframe.util.DEPRECATED_ACCESS_API
1414
import kotlin.reflect.KCallable
1515
import kotlin.reflect.KProperty
16+
import kotlin.reflect.typeOf
1617

1718
public inline fun <reified T> DataColumn<T>.unfold(vararg roots: KCallable<*>, maxDepth: Int = 0): AnyCol =
18-
unfoldImpl { properties(roots = roots, maxDepth) }
19+
unfoldImpl(typeOf<T>()) { properties(roots = roots, maxDepth) }
1920

2021
@Refine
2122
@Interpretable("DataFrameUnfold")
2223
public fun <T> DataFrame<T>.unfold(
2324
vararg roots: KCallable<*>,
2425
maxDepth: Int = 0,
2526
columns: ColumnsSelector<T, *>,
26-
): DataFrame<T> = replace(columns).with { it.unfoldImpl { properties(roots = roots, maxDepth) } }
27+
): DataFrame<T> = replace(columns).with { it.unfoldImpl(it.type()) { properties(roots = roots, maxDepth) } }
2728

2829
public fun <T> DataFrame<T>.unfold(vararg columns: String): DataFrame<T> = unfold { columns.toColumnSet() }
2930

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

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import java.time.temporal.TemporalAmount
2828
import kotlin.reflect.KCallable
2929
import kotlin.reflect.KClass
3030
import kotlin.reflect.KProperty
31+
import kotlin.reflect.KType
3132
import kotlin.reflect.KVisibility
3233
import kotlin.reflect.full.isSubclassOf
3334
import kotlin.reflect.full.memberFunctions
@@ -98,13 +99,11 @@ internal val KClass<*>.isValueType: Boolean
9899
*/
99100
@PublishedApi
100101
internal val KClass<*>.hasProperties: Boolean
101-
get() = this.memberProperties.any { it.visibility == KVisibility.PUBLIC } ||
102-
// check pojo-like classes
103-
this.memberFunctions.any { it.visibility == KVisibility.PUBLIC && it.isGetterLike() }
102+
get() = properties().isNotEmpty()
104103

105104
internal class CreateDataFrameDslImpl<T>(
106105
override val source: Iterable<T>,
107-
private val clazz: KClass<*>,
106+
private val type: KType,
108107
private val prefix: ColumnPath = emptyPath(),
109108
private val configuration: TraverseConfiguration = TraverseConfiguration(),
110109
) : CreateDataFrameDsl<T>(),
@@ -119,7 +118,7 @@ internal class CreateDataFrameDslImpl<T>(
119118
}
120119

121120
override operator fun String.invoke(builder: CreateDataFrameDsl<T>.() -> Unit) {
122-
val child = CreateDataFrameDslImpl(source, clazz, prefix + this)
121+
val child = CreateDataFrameDslImpl(source, type, prefix + this)
123122
builder(child)
124123
columns.addAll(child.columns)
125124
}
@@ -182,7 +181,7 @@ internal class CreateDataFrameDslImpl<T>(
182181
}
183182
val df = convertToDataFrame(
184183
data = source,
185-
clazz = clazz,
184+
type = type,
186185
roots = roots.toList(),
187186
excludes = dsl.excludeProperties,
188187
preserveClasses = dsl.preserveClasses,
@@ -197,33 +196,34 @@ internal class CreateDataFrameDslImpl<T>(
197196

198197
@PublishedApi
199198
internal fun <T> Iterable<T>.createDataFrameImpl(
200-
clazz: KClass<*>,
199+
type: KType,
201200
body: CreateDataFrameDslImpl<T>.() -> Unit,
202201
): DataFrame<T> {
203-
val builder = CreateDataFrameDslImpl(this, clazz)
202+
val builder = CreateDataFrameDslImpl(this, type)
204203
builder.body()
205204
return builder.columns.toDataFrameFromPairs()
206205
}
207206

208207
@PublishedApi
209208
internal fun convertToDataFrame(
210209
data: Iterable<*>,
211-
clazz: KClass<*>,
210+
type: KType,
212211
roots: List<KCallable<*>>,
213212
excludes: Set<KCallable<*>>,
214213
preserveClasses: Set<KClass<*>>,
215214
preserveProperties: Set<KCallable<*>>,
216215
maxDepth: Int,
217216
): AnyFrame {
217+
val clazz = type.classifierOrAny()
218+
// this check relies on later recursive calls having roots = emptyList()
219+
if (roots.isEmpty() && !clazz.canBeUnfolded) {
220+
val column = DataColumn.createByType("value", data.toList(), type)
221+
return dataFrameOf(column)
222+
}
223+
218224
val properties: List<KCallable<*>> = roots
219225
.ifEmpty {
220-
clazz.memberProperties
221-
.filter { it.visibility == KVisibility.PUBLIC }
222-
}
223-
// fall back to getter functions for pojo-like classes if no member properties were found
224-
.ifEmpty {
225-
clazz.memberFunctions
226-
.filter { it.visibility == KVisibility.PUBLIC && it.isGetterLike() }
226+
clazz.properties()
227227
}
228228
// sort properties by order in constructor
229229
.sortWithConstructor(clazz)
@@ -302,10 +302,11 @@ internal fun convertToDataFrame(
302302
val keepSubtree =
303303
maxDepth <= 0 && !fieldKind.shouldBeConvertedToFrameColumn && !fieldKind.shouldBeConvertedToColumnGroup
304304
val shouldCreateValueCol = keepSubtree ||
305-
kClass == Any::class ||
306305
kClass in preserveClasses ||
307306
property in preserveProperties ||
308-
kClass.isValueType
307+
!kClass.canBeUnfolded &&
308+
!fieldKind.shouldBeConvertedToFrameColumn &&
309+
!fieldKind.shouldBeConvertedToColumnGroup
309310

310311
val shouldCreateFrameCol = kClass == DataFrame::class && !nullable
311312
val shouldCreateColumnGroup = kClass == DataRow::class
@@ -368,7 +369,7 @@ internal fun convertToDataFrame(
368369
require(it is Iterable<*>)
369370
convertToDataFrame(
370371
data = it,
371-
clazz = elementClass,
372+
type = elementType,
372373
roots = emptyList(),
373374
excludes = excludes,
374375
preserveClasses = preserveClasses,
@@ -386,7 +387,7 @@ internal fun convertToDataFrame(
386387
else -> {
387388
val df = convertToDataFrame(
388389
data = values,
389-
clazz = kClass,
390+
type = returnType,
390391
roots = emptyList(),
391392
excludes = excludes,
392393
preserveClasses = preserveClasses,
@@ -403,3 +404,15 @@ internal fun convertToDataFrame(
403404
dataFrameOf(columns)
404405
}
405406
}
407+
408+
private fun KType.classifierOrAny(): KClass<*> = classifier as? KClass<*> ?: Any::class
409+
410+
private fun KClass<*>.properties(): List<KCallable<*>> {
411+
// fall back to getter functions for pojo-like classes if no member properties were found
412+
return memberProperties
413+
.filter { it.visibility == KVisibility.PUBLIC }
414+
.ifEmpty {
415+
memberFunctions
416+
.filter { it.visibility == KVisibility.PUBLIC && it.isGetterLike() }
417+
}
418+
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,18 @@ import org.jetbrains.kotlinx.dataframe.api.asColumnGroup
77
import org.jetbrains.kotlinx.dataframe.api.asDataColumn
88
import org.jetbrains.kotlinx.dataframe.columns.ColumnKind
99
import org.jetbrains.kotlinx.dataframe.typeClass
10+
import kotlin.reflect.KType
1011

1112
@PublishedApi
12-
internal fun <T> DataColumn<T>.unfoldImpl(body: CreateDataFrameDsl<T>.() -> Unit): AnyCol =
13+
internal fun <T> DataColumn<T>.unfoldImpl(type: KType, body: CreateDataFrameDsl<T>.() -> Unit): AnyCol =
1314
when (kind()) {
1415
ColumnKind.Group, ColumnKind.Frame -> this
1516

1617
else -> when {
1718
!typeClass.canBeUnfolded -> this
1819

1920
else -> values()
20-
.createDataFrameImpl(typeClass) { (this as CreateDataFrameDsl<T>).body() }
21+
.createDataFrameImpl(type) { (this as CreateDataFrameDsl<T>).body() }
2122
.asColumnGroup(name())
2223
.asDataColumn()
2324
}

core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,7 @@ class CreateDataFrameTests {
213213
df.rowsCount() shouldBe 1
214214

215215
val childCol = df[Entry::child]
216-
childCol.kind() shouldBe ColumnKind.Group
217-
childCol.asColumnGroup().columnsCount() shouldBe 0
216+
childCol.kind() shouldBe ColumnKind.Value
218217
}
219218

220219
@Test
@@ -642,4 +641,43 @@ class CreateDataFrameTests {
642641
val df = files.toDataFrame(columnName = "files")
643642
df["files"][0] shouldBe File("data.csv")
644643
}
644+
645+
class MyEmptyDeclaration
646+
647+
class TestItem(val name: String, val containingDeclaration: MyEmptyDeclaration, val test: Int)
648+
649+
@Test
650+
fun `preserve empty interface consistency`() {
651+
val df = listOf(MyEmptyDeclaration(), MyEmptyDeclaration()).toDataFrame()
652+
df["value"].type() shouldBe typeOf<MyEmptyDeclaration>()
653+
}
654+
655+
@Test
656+
fun `preserve nested empty interface consistency`() {
657+
val df = List(10) {
658+
TestItem(
659+
"Test1",
660+
MyEmptyDeclaration(),
661+
123,
662+
)
663+
}.toDataFrame(maxDepth = 2)
664+
665+
df["containingDeclaration"].type() shouldBe typeOf<MyEmptyDeclaration>()
666+
}
667+
668+
@Test
669+
fun `preserve value type consistency`() {
670+
val list = listOf(mapOf("a" to 1))
671+
val df = list.toDataFrame(maxDepth = 1)
672+
df["value"].type() shouldBe typeOf<Map<String, Int>>()
673+
}
674+
675+
class MapContainer(val map: Map<String, Int>)
676+
677+
@Test
678+
fun `preserve nested value type consistency`() {
679+
val list = listOf(MapContainer(mapOf("a" to 1)))
680+
val df = list.toDataFrame(maxDepth = 2)
681+
df["map"].type() shouldBe typeOf<Map<String, Int>>()
682+
}
645683
}

0 commit comments

Comments
 (0)