Skip to content

Commit 89ddd20

Browse files
committed
added getterLike checks for KCallable in toDataFrame DSL. reworked property order sorting with multiple constructors.
1 parent 1dd6151 commit 89ddd20

File tree

10 files changed

+168
-66
lines changed

10 files changed

+168
-66
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ public interface TraversePropertiesDsl {
122122

123123
/**
124124
* Skip given [properties] during recursive (dfs) traversal.
125-
* These can also be getter-like functions.
125+
* These can also be getter-like functions (like `getX()` or `isX()`).
126126
*/
127127
public fun exclude(vararg properties: KCallable<*>)
128128

@@ -133,7 +133,7 @@ public interface TraversePropertiesDsl {
133133

134134
/**
135135
* Store given [properties] in ValueColumns without transformation into ColumnGroups or FrameColumns.
136-
* These can also be getter-like functions.
136+
* These can also be getter-like functions (like `getX()` or `isX()`).
137137
*/
138138
public fun preserve(vararg properties: KCallable<*>)
139139
}

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ import java.math.BigInteger
1717
import kotlin.reflect.KCallable
1818
import kotlin.reflect.KClass
1919
import kotlin.reflect.KFunction
20+
import kotlin.reflect.KProperty
2021
import kotlin.reflect.KType
2122
import kotlin.reflect.KTypeProjection
2223
import kotlin.reflect.KVariance
2324
import kotlin.reflect.full.createType
2425
import kotlin.reflect.full.findAnnotation
2526
import kotlin.reflect.full.isSubtypeOf
2627
import kotlin.reflect.full.starProjectedType
28+
import kotlin.reflect.full.valueParameters
2729
import kotlin.reflect.full.withNullability
2830
import kotlin.reflect.jvm.jvmErasure
2931
import kotlin.reflect.typeOf
@@ -338,8 +340,22 @@ internal fun List<String>.joinToCamelCaseString(): String {
338340
.replaceFirstChar { it.lowercaseChar() }
339341
}
340342

343+
internal fun KFunction<*>.isGetterLike(): Boolean =
344+
(name.startsWith("get") || name.startsWith("is")) && valueParameters.isEmpty()
345+
346+
internal fun KCallable<*>.isGetterLike(): Boolean =
347+
when (this) {
348+
is KProperty<*> -> true
349+
is KFunction<*> -> isGetterLike()
350+
else -> false
351+
}
352+
341353
@PublishedApi
342-
internal val <T> KCallable<T>.columnName: String
354+
internal val KProperty<*>.columnName: String
355+
get() = findAnnotation<ColumnName>()?.name ?: name
356+
357+
@PublishedApi
358+
internal val KCallable<*>.columnName: String
343359
get() = findAnnotation<ColumnName>()?.name
344360
?: when (this) {
345361
// for defining the column names based on a getter-function, we use the function name minus the get/is prefix
@@ -349,5 +365,7 @@ internal val <T> KCallable<T>.columnName: String
349365
.removePrefix("is")
350366
.replaceFirstChar { it.lowercase() }
351367

368+
is KProperty<*> -> this.columnName
369+
352370
else -> name
353371
}

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

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ import org.jetbrains.kotlinx.dataframe.impl.asList
1818
import org.jetbrains.kotlinx.dataframe.impl.columnName
1919
import org.jetbrains.kotlinx.dataframe.impl.emptyPath
2020
import org.jetbrains.kotlinx.dataframe.impl.getListType
21+
import org.jetbrains.kotlinx.dataframe.impl.isGetterLike
2122
import org.jetbrains.kotlinx.dataframe.impl.projectUpTo
22-
import org.jetbrains.kotlinx.dataframe.impl.schema.getPropertyOrderFromAnyConstructor
23-
import org.jetbrains.kotlinx.dataframe.impl.schema.getPropertyOrderFromPrimaryConstructor
23+
import org.jetbrains.kotlinx.dataframe.impl.schema.sortWithConstructors
2424
import java.lang.reflect.InvocationTargetException
2525
import java.lang.reflect.Method
2626
import java.time.temporal.Temporal
@@ -32,7 +32,6 @@ import kotlin.reflect.full.isSubclassOf
3232
import kotlin.reflect.full.memberFunctions
3333
import kotlin.reflect.full.memberProperties
3434
import kotlin.reflect.full.primaryConstructor
35-
import kotlin.reflect.full.valueParameters
3635
import kotlin.reflect.full.withNullability
3736
import kotlin.reflect.jvm.isAccessible
3837
import kotlin.reflect.jvm.javaField
@@ -93,6 +92,11 @@ internal class CreateDataFrameDslImpl<T>(
9392
}
9493

9594
override fun exclude(vararg properties: KCallable<*>) {
95+
for (prop in properties) {
96+
require(prop.isGetterLike()) {
97+
"${prop.name} is not a property or getter-like function. Only those are traversed and can be excluded."
98+
}
99+
}
96100
excludeProperties.addAll(properties)
97101
}
98102

@@ -105,11 +109,22 @@ internal class CreateDataFrameDslImpl<T>(
105109
}
106110

107111
override fun preserve(vararg properties: KCallable<*>) {
112+
for (prop in properties) {
113+
require(prop.isGetterLike()) {
114+
"${prop.name} is not a property or getter-like function. Only those are traversed and can be preserved."
115+
}
116+
}
108117
preserveProperties.addAll(properties)
109118
}
110119
}
111120

112121
override fun properties(vararg roots: KCallable<*>, maxDepth: Int, body: (TraversePropertiesDsl.() -> Unit)?) {
122+
for (prop in roots) {
123+
require(prop.isGetterLike()) {
124+
"${prop.name} is not a property or getter-like function. Only those are traversed and can be added as roots."
125+
}
126+
}
127+
113128
val dsl = configuration.clone()
114129
if (body != null) {
115130
body(dsl)
@@ -149,41 +164,20 @@ internal fun convertToDataFrame(
149164
preserveProperties: Set<KCallable<*>>,
150165
maxDepth: Int,
151166
): AnyFrame {
152-
val primaryConstructorOrder = getPropertyOrderFromPrimaryConstructor(clazz)
153-
val anyConstructorOrder = getPropertyOrderFromAnyConstructor(clazz)
154-
155167
val properties: List<KCallable<*>> = roots
156168
.ifEmpty {
157169
clazz.memberProperties
158-
.filter { it.visibility == KVisibility.PUBLIC && it.valueParameters.isEmpty() }
170+
.filter { it.visibility == KVisibility.PUBLIC }
159171
}
160172

161-
// fall back to getter functions for pojo-like classes
173+
// fall back to getter functions for pojo-like classes if no member properties were found
162174
.ifEmpty {
163175
clazz.memberFunctions
164-
.filter {
165-
it.visibility == KVisibility.PUBLIC &&
166-
it.valueParameters.isEmpty() &&
167-
(it.name.startsWith("get") || it.name.startsWith("is"))
168-
}
176+
.filter { it.visibility == KVisibility.PUBLIC && it.isGetterLike() }
169177
}
170178

171-
// sort properties by order of primary-ish constructor
172-
.let {
173-
val names = it.map { it.columnName }.toSet()
174-
175-
// use the primary constructor order if it's available,
176-
// else try to find a constructor that matches all properties
177-
val order = primaryConstructorOrder
178-
?: anyConstructorOrder.firstOrNull { map ->
179-
names.all { it in map.keys }
180-
}
181-
if (order != null) {
182-
it.sortedBy { order[it.columnName] ?: Int.MAX_VALUE }
183-
} else {
184-
it
185-
}
186-
}
179+
// sort properties by order in constructors
180+
.sortWithConstructors(clazz)
187181

188182
val columns = properties.mapNotNull {
189183
val property = it

core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/schema/Utils.kt

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ import org.jetbrains.kotlinx.dataframe.columns.ColumnKind
1313
import org.jetbrains.kotlinx.dataframe.columns.FrameColumn
1414
import org.jetbrains.kotlinx.dataframe.columns.ValueColumn
1515
import org.jetbrains.kotlinx.dataframe.hasNulls
16+
import org.jetbrains.kotlinx.dataframe.impl.columnName
1617
import org.jetbrains.kotlinx.dataframe.impl.commonType
18+
import org.jetbrains.kotlinx.dataframe.impl.isGetterLike
1719
import org.jetbrains.kotlinx.dataframe.schema.ColumnSchema
1820
import org.jetbrains.kotlinx.dataframe.schema.DataFrameSchema
1921
import org.jetbrains.kotlinx.dataframe.type
22+
import kotlin.reflect.KCallable
2023
import kotlin.reflect.KClass
2124
import kotlin.reflect.full.primaryConstructor
2225
import kotlin.reflect.full.withNullability
@@ -148,11 +151,46 @@ internal fun getPropertyOrderFromPrimaryConstructor(clazz: KClass<*>): Map<Strin
148151
?.mapIndexed { i, v -> v to i }
149152
?.toMap()
150153

151-
internal fun getPropertyOrderFromAnyConstructor(clazz: KClass<*>): List<Map<String, Int>> =
154+
internal fun getPropertyOrderFromAllConstructors(clazz: KClass<*>): List<Map<String, Int>> =
152155
clazz.constructors
153156
.map { constructor ->
154157
constructor.parameters
155158
.mapNotNull { it.name }
156159
.mapIndexed { i, v -> v to i }
157160
.toMap()
158161
}
162+
163+
/**
164+
* Sorts [this] according to the order of them in the constructors of [klass].
165+
* It prefers the primary constructor if it exists, else it falls back to the other constructors to do the sorting.
166+
* Finally, it falls back to lexicographical sorting if a property does not exist in any constructor.
167+
*/
168+
internal fun <T> Iterable<KCallable<T>>.sortWithConstructors(klass: KClass<*>): List<KCallable<T>> {
169+
require(all { it.isGetterLike() })
170+
val primaryConstructorOrder = getPropertyOrderFromPrimaryConstructor(klass)
171+
val allConstructorsOrders = getPropertyOrderFromAllConstructors(klass)
172+
173+
// starting off lexicographically, sort properties according to the order of all constructors
174+
val allConstructorsSortedProperties = allConstructorsOrders
175+
.fold(this.sortedBy { it.columnName }) { props, constructorOrder ->
176+
props
177+
.withIndex()
178+
.sortedBy { (i, it) -> constructorOrder[it.columnName] ?: i }
179+
.map { it.value }
180+
}.toList()
181+
182+
if (primaryConstructorOrder == null) {
183+
return allConstructorsSortedProperties
184+
}
185+
186+
// prefer to sort properties according to the order in the primary constructor if it exists.
187+
// if a property does not exist in the primary constructor, fall back to the other order
188+
189+
val (propsInConstructor, propsNotInConstructor) =
190+
this.partition { it.columnName in primaryConstructorOrder.keys }
191+
192+
val allConstructorsSortedPropertyNames = allConstructorsSortedProperties.map { it.columnName }
193+
194+
return propsInConstructor.sortedBy { primaryConstructorOrder[it.columnName] } +
195+
propsNotInConstructor.sortedBy { allConstructorsSortedPropertyNames.indexOf(it.columnName) }
196+
}

core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/Utils.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ fun <T : DataFrame<*>> T.alsoDebug(println: String? = null, rowsLimit: Int = 20)
2424
print(borders = true, title = true, columnTypes = true, valueLimit = -1, rowsLimit = rowsLimit)
2525
schema().print()
2626
}
27+

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ public interface TraversePropertiesDsl {
122122

123123
/**
124124
* Skip given [properties] during recursive (dfs) traversal.
125-
* These can also be getter-like functions.
125+
* These can also be getter-like functions (like `getX()` or `isX()`).
126126
*/
127127
public fun exclude(vararg properties: KCallable<*>)
128128

@@ -133,7 +133,7 @@ public interface TraversePropertiesDsl {
133133

134134
/**
135135
* Store given [properties] in ValueColumns without transformation into ColumnGroups or FrameColumns.
136-
* These can also be getter-like functions.
136+
* These can also be getter-like functions (like `getX()` or `isX()`).
137137
*/
138138
public fun preserve(vararg properties: KCallable<*>)
139139
}

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ import java.math.BigInteger
1717
import kotlin.reflect.KCallable
1818
import kotlin.reflect.KClass
1919
import kotlin.reflect.KFunction
20+
import kotlin.reflect.KProperty
2021
import kotlin.reflect.KType
2122
import kotlin.reflect.KTypeProjection
2223
import kotlin.reflect.KVariance
2324
import kotlin.reflect.full.createType
2425
import kotlin.reflect.full.findAnnotation
2526
import kotlin.reflect.full.isSubtypeOf
2627
import kotlin.reflect.full.starProjectedType
28+
import kotlin.reflect.full.valueParameters
2729
import kotlin.reflect.full.withNullability
2830
import kotlin.reflect.jvm.jvmErasure
2931
import kotlin.reflect.typeOf
@@ -338,8 +340,22 @@ internal fun List<String>.joinToCamelCaseString(): String {
338340
.replaceFirstChar { it.lowercaseChar() }
339341
}
340342

343+
internal fun KFunction<*>.isGetterLike(): Boolean =
344+
(name.startsWith("get") || name.startsWith("is")) && valueParameters.isEmpty()
345+
346+
internal fun KCallable<*>.isGetterLike(): Boolean =
347+
when (this) {
348+
is KProperty<*> -> true
349+
is KFunction<*> -> isGetterLike()
350+
else -> false
351+
}
352+
341353
@PublishedApi
342-
internal val <T> KCallable<T>.columnName: String
354+
internal val KProperty<*>.columnName: String
355+
get() = findAnnotation<ColumnName>()?.name ?: name
356+
357+
@PublishedApi
358+
internal val KCallable<*>.columnName: String
343359
get() = findAnnotation<ColumnName>()?.name
344360
?: when (this) {
345361
// for defining the column names based on a getter-function, we use the function name minus the get/is prefix
@@ -349,5 +365,7 @@ internal val <T> KCallable<T>.columnName: String
349365
.removePrefix("is")
350366
.replaceFirstChar { it.lowercase() }
351367

368+
is KProperty<*> -> this.columnName
369+
352370
else -> name
353371
}

0 commit comments

Comments
 (0)