From e736fb674c0e37330fce58c7e98f584c0c4ef7f4 Mon Sep 17 00:00:00 2001 From: "andrei.kislitsyn" Date: Thu, 13 Mar 2025 13:02:54 +0400 Subject: [PATCH 1/4] update unfold --- .../jetbrains/kotlinx/dataframe/api/DataColumnType.kt | 10 ---------- .../org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt | 3 ++- .../org/jetbrains/kotlinx/dataframe/api/unfold.kt | 3 ++- .../kotlinx/dataframe/impl/api/toDataFrame.kt | 8 ++++++++ 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/DataColumnType.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/DataColumnType.kt index 8547f2a060..286cb69ae8 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/DataColumnType.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/DataColumnType.kt @@ -81,13 +81,3 @@ public fun AnyCol.valuesAreComparable(): Boolean = nullable = hasNulls(), ), ) - -@PublishedApi -internal fun AnyCol.isPrimitive(): Boolean = typeClass.isPrimitive() - -internal fun KClass<*>.isPrimitive(): Boolean = - isSubclassOf(Number::class) || - this == String::class || - this == Char::class || - this == Array::class || - isSubclassOf(Collection::class) diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt index 67a459c64a..575e673341 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt @@ -11,6 +11,7 @@ import org.jetbrains.kotlinx.dataframe.annotations.Interpretable import org.jetbrains.kotlinx.dataframe.annotations.Refine import org.jetbrains.kotlinx.dataframe.columns.ColumnPath import org.jetbrains.kotlinx.dataframe.impl.ColumnNameGenerator +import org.jetbrains.kotlinx.dataframe.impl.api.canBeUnfolded import org.jetbrains.kotlinx.dataframe.impl.api.createDataFrameImpl import org.jetbrains.kotlinx.dataframe.impl.api.hasProperties import org.jetbrains.kotlinx.dataframe.impl.api.isValueType @@ -30,7 +31,7 @@ public inline fun Iterable.toDataFrame(): DataFrame = toDataFrame { // check if type is value: primitives, primitive arrays, datetime types etc., // or has no properties - if (T::class.isValueType || !T::class.hasProperties) { + if (!T::class.canBeUnfolded) { // create a single `value` column ValueProperty::value from { it } } else { diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/unfold.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/unfold.kt index 6a3772175f..c4598b2995 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/unfold.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/unfold.kt @@ -8,6 +8,7 @@ import org.jetbrains.kotlinx.dataframe.DataFrame import org.jetbrains.kotlinx.dataframe.annotations.AccessApiOverload import org.jetbrains.kotlinx.dataframe.columns.ColumnKind import org.jetbrains.kotlinx.dataframe.columns.toColumnSet +import org.jetbrains.kotlinx.dataframe.impl.api.canBeUnfolded import org.jetbrains.kotlinx.dataframe.impl.api.createDataFrameImpl import org.jetbrains.kotlinx.dataframe.typeClass import kotlin.reflect.KProperty @@ -17,7 +18,7 @@ public inline fun DataColumn.unfold(): AnyCol = ColumnKind.Group, ColumnKind.Frame -> this else -> when { - isPrimitive() -> this + !typeClass.canBeUnfolded -> this else -> values() .createDataFrameImpl(typeClass) { (this as CreateDataFrameDsl).properties() } diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt index 76aea4eb4a..7d3069b0f0 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt @@ -58,6 +58,13 @@ private val valueTypes = setOf( kotlinx.datetime.DateTimeUnit::class, ) +/** + * Check + */ +@PublishedApi +internal val KClass<*>.canBeUnfolded: Boolean + get() = (!this.isValueType) && this.hasProperties + /** * Checks if `KClass` is a value type (number, datetime, string, etc.) * Should be aligned with `ConeKotlinType.isValueType()` in @@ -69,6 +76,7 @@ internal val KClass<*>.isValueType: Boolean this in valueTypes || this.isSubclassOf(Number::class) || this.isSubclassOf(Enum::class) || + this.isSubclassOf(Map::class) || // all java datetime types this.isSubclassOf(TemporalAccessor::class) || this.isSubclassOf(TemporalAmount::class) || From 89cd09b5672d73e1da4ff59c52f44f02dbcafd19 Mon Sep 17 00:00:00 2001 From: "andrei.kislitsyn" Date: Fri, 14 Mar 2025 15:18:52 +0400 Subject: [PATCH 2/4] map behavior in plugin --- core/api/core.api | 2 +- .../org/jetbrains/kotlinx/dataframe/api/DataColumnType.kt | 2 -- .../org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt | 2 -- .../org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt | 8 ++++++++ .../kotlinx/dataframe/plugin/impl/api/toDataFrame.kt | 5 +++++ .../testData/box/toDataFrameValueTypes.kt | 6 ++++++ 6 files changed, 20 insertions(+), 5 deletions(-) diff --git a/core/api/core.api b/core/api/core.api index 2acda50a99..98a724ff2f 100644 --- a/core/api/core.api +++ b/core/api/core.api @@ -1974,7 +1974,6 @@ public final class org/jetbrains/kotlinx/dataframe/api/DataColumnTypeKt { public static final fun isFrameColumn (Lorg/jetbrains/kotlinx/dataframe/DataColumn;)Z public static final fun isList (Lorg/jetbrains/kotlinx/dataframe/DataColumn;)Z public static final fun isNumber (Lorg/jetbrains/kotlinx/dataframe/DataColumn;)Z - public static final fun isPrimitive (Lorg/jetbrains/kotlinx/dataframe/DataColumn;)Z public static final fun isSubtypeOf (Lorg/jetbrains/kotlinx/dataframe/DataColumn;Lkotlin/reflect/KType;)Z public static final fun isValueColumn (Lorg/jetbrains/kotlinx/dataframe/DataColumn;)Z public static final fun valuesAreComparable (Lorg/jetbrains/kotlinx/dataframe/DataColumn;)Z @@ -5404,6 +5403,7 @@ public final class org/jetbrains/kotlinx/dataframe/impl/api/SchemaKt { public final class org/jetbrains/kotlinx/dataframe/impl/api/ToDataFrameKt { 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; public static final fun createDataFrameImpl (Ljava/lang/Iterable;Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/kotlinx/dataframe/DataFrame; + public static final fun getCanBeUnfolded (Lkotlin/reflect/KClass;)Z public static final fun getHasProperties (Lkotlin/reflect/KClass;)Z public static final fun isValueType (Lkotlin/reflect/KClass;)Z } diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/DataColumnType.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/DataColumnType.kt index 286cb69ae8..9bd832d0cf 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/DataColumnType.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/DataColumnType.kt @@ -16,12 +16,10 @@ import java.math.BigDecimal import java.math.BigInteger import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract -import kotlin.reflect.KClass import kotlin.reflect.KType import kotlin.reflect.KTypeProjection import kotlin.reflect.KVariance import kotlin.reflect.full.createType -import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.isSubtypeOf import kotlin.reflect.full.withNullability import kotlin.reflect.typeOf diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt index 575e673341..1aab859a76 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt @@ -13,8 +13,6 @@ import org.jetbrains.kotlinx.dataframe.columns.ColumnPath import org.jetbrains.kotlinx.dataframe.impl.ColumnNameGenerator import org.jetbrains.kotlinx.dataframe.impl.api.canBeUnfolded import org.jetbrains.kotlinx.dataframe.impl.api.createDataFrameImpl -import org.jetbrains.kotlinx.dataframe.impl.api.hasProperties -import org.jetbrains.kotlinx.dataframe.impl.api.isValueType import org.jetbrains.kotlinx.dataframe.impl.asList import org.jetbrains.kotlinx.dataframe.impl.columnName import org.jetbrains.kotlinx.dataframe.impl.columns.createColumnGuessingType diff --git a/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt index 47bfb89071..79be7aeae3 100644 --- a/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt +++ b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt @@ -398,6 +398,14 @@ class CreateDataFrameTests { enums.toDataFrame() shouldBe dataFrameOf("value")(*enums.toTypedArray()) } + @Test + fun `should convert iterables of non-JSON Map to DataFrame with value column`() { + val maps: List?> = listOf(mapOf(1 to null, 2 to "val"), mapOf(3 to 1, 4 to true), null) + val df = maps.toDataFrame() + df.columnNames() shouldBe listOf("value") + df["value"].toList() shouldBe maps + } + class NoPublicPropsClass(private val a: Int, private val b: String) @Test diff --git a/plugins/kotlin-dataframe/src/org/jetbrains/kotlinx/dataframe/plugin/impl/api/toDataFrame.kt b/plugins/kotlin-dataframe/src/org/jetbrains/kotlinx/dataframe/plugin/impl/api/toDataFrame.kt index a2af7daa08..d299db62a6 100644 --- a/plugins/kotlin-dataframe/src/org/jetbrains/kotlinx/dataframe/plugin/impl/api/toDataFrame.kt +++ b/plugins/kotlin-dataframe/src/org/jetbrains/kotlinx/dataframe/plugin/impl/api/toDataFrame.kt @@ -198,10 +198,14 @@ internal fun KotlinTypeFacade.toDataFrame( traverseConfiguration: TraverseConfiguration, ): PluginDataFrameSchema { + val anyType = session.builtinTypes.nullableAnyType.type + fun ConeKotlinType.isValueType() = this.isArrayTypeOrNullableArrayType || this.classId == StandardClassIds.Unit || this.classId == StandardClassIds.Any || + this.classId == StandardClassIds.Map || + this.classId == StandardClassIds.MutableMap || this.classId == StandardClassIds.String || this.classId in StandardClassIds.primitiveTypes || this.classId in StandardClassIds.unsignedTypes || @@ -226,6 +230,7 @@ internal fun KotlinTypeFacade.toDataFrame( Names.TEMPORAL_AMOUNT_CLASS_ID.constructClassLikeType(emptyArray(), isNullable = true), session ) + fun FirNamedFunctionSymbol.isGetterLike(): Boolean { val functionName = this.name.asString() return (functionName.startsWith("get") || functionName.startsWith("is")) && diff --git a/plugins/kotlin-dataframe/testData/box/toDataFrameValueTypes.kt b/plugins/kotlin-dataframe/testData/box/toDataFrameValueTypes.kt index 26857c2db0..a164ad4965 100644 --- a/plugins/kotlin-dataframe/testData/box/toDataFrameValueTypes.kt +++ b/plugins/kotlin-dataframe/testData/box/toDataFrameValueTypes.kt @@ -197,6 +197,12 @@ fun box(): String { val enumColNullable: DataColumn = enumDfNullable.value enumColNullable.print() + // Non-JSON Map + + val mapsDfNullable = listOf(mapOf(1 to null, 2 to "val"), mapOf(3 to 1, 4 to true), null).toDataFrame() + val mapsColNullable: DataColumn?> = mapsDfNullable.value + mapsColNullable.print() + return "OK" } From 28e515d47854002db7805e0fa1016b168ddb1a89 Mon Sep 17 00:00:00 2001 From: "andrei.kislitsyn" Date: Fri, 14 Mar 2025 16:40:03 +0400 Subject: [PATCH 3/4] map value type Map class --- .../org/jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt index 7d3069b0f0..283f793408 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt @@ -56,6 +56,8 @@ private val valueTypes = setOf( kotlinx.datetime.TimeZone::class, kotlinx.datetime.DateTimePeriod::class, kotlinx.datetime.DateTimeUnit::class, + Map::class, + MutableMap::class, ) /** @@ -76,7 +78,6 @@ internal val KClass<*>.isValueType: Boolean this in valueTypes || this.isSubclassOf(Number::class) || this.isSubclassOf(Enum::class) || - this.isSubclassOf(Map::class) || // all java datetime types this.isSubclassOf(TemporalAccessor::class) || this.isSubclassOf(TemporalAmount::class) || From 901a806b3f4af43d85cfcb3e4140f9283dad1da5 Mon Sep 17 00:00:00 2001 From: "andrei.kislitsyn" Date: Mon, 17 Mar 2025 13:37:20 +0400 Subject: [PATCH 4/4] canBeUnfolded kdoc --- .../jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt index 283f793408..9d213b2c0f 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt @@ -61,7 +61,14 @@ private val valueTypes = setOf( ) /** - * Check + * Determines whether a class can be unfolded into its properties. + * + * A class is considered **unfoldable** if it has at least one public property + * or getter-like function. This excludes: + * - **Value types** such as primitives, enums, and date-time types. + * - **Classes without properties**, including empty or marker classes. + * + * @return `true` if the class has unfoldable properties, `false` otherwise. */ @PublishedApi internal val KClass<*>.canBeUnfolded: Boolean