Skip to content

Commit 35176c9

Browse files
authored
Merge pull request #650 from Kotlin/pojo-toDataFrame
POJO toDataFrame support (and array improvements)
2 parents 7b6669f + fca4b8f commit 35176c9

File tree

24 files changed

+1268
-198
lines changed

24 files changed

+1268
-198
lines changed

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

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import org.jetbrains.kotlinx.dataframe.impl.asList
1313
import org.jetbrains.kotlinx.dataframe.impl.columnName
1414
import org.jetbrains.kotlinx.dataframe.impl.columns.guessColumnType
1515
import org.jetbrains.kotlinx.dataframe.index
16+
import kotlin.reflect.KCallable
1617
import kotlin.reflect.KClass
1718
import kotlin.reflect.KProperty
1819

@@ -115,24 +116,26 @@ public fun Iterable<Pair<String, Iterable<Any?>>>.toDataFrameFromPairs(): AnyFra
115116
public interface TraversePropertiesDsl {
116117

117118
/**
118-
* Skip given [classes] during recursive (dfs) traversal
119+
* Skip given [classes] during recursive (dfs) traversal.
119120
*/
120121
public fun exclude(vararg classes: KClass<*>)
121122

122123
/**
123-
* Skip given [properties] during recursive (dfs) traversal
124+
* Skip given [properties] during recursive (dfs) traversal.
125+
* These can also be getter-like functions (like `getX()` or `isX()`).
124126
*/
125-
public fun exclude(vararg properties: KProperty<*>)
127+
public fun exclude(vararg properties: KCallable<*>)
126128

127129
/**
128-
* Store given [classes] in ValueColumns without transformation into ColumnGroups or FrameColumns
130+
* Store given [classes] in ValueColumns without transformation into ColumnGroups or FrameColumns.
129131
*/
130132
public fun preserve(vararg classes: KClass<*>)
131133

132134
/**
133-
* Store given [properties] in ValueColumns without transformation into ColumnGroups or FrameColumns
135+
* Store given [properties] in ValueColumns without transformation into ColumnGroups or FrameColumns.
136+
* These can also be getter-like functions (like `getX()` or `isX()`).
134137
*/
135-
public fun preserve(vararg properties: KProperty<*>)
138+
public fun preserve(vararg properties: KCallable<*>)
136139
}
137140

138141
public inline fun <reified T> TraversePropertiesDsl.preserve(): Unit = preserve(T::class)
@@ -148,7 +151,7 @@ public abstract class CreateDataFrameDsl<T> : TraversePropertiesDsl {
148151
public infix fun AnyBaseCol.into(path: ColumnPath): Unit = add(this, path)
149152

150153
public abstract fun properties(
151-
vararg roots: KProperty<*>,
154+
vararg roots: KCallable<*>,
152155
maxDepth: Int = 0,
153156
body: (TraversePropertiesDsl.() -> Unit)? = null,
154157
)

core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/codeGen/MarkersExtractor.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import org.jetbrains.kotlinx.dataframe.DataFrame
44
import org.jetbrains.kotlinx.dataframe.DataRow
55
import org.jetbrains.kotlinx.dataframe.annotations.ColumnName
66
import org.jetbrains.kotlinx.dataframe.annotations.DataSchema
7-
import org.jetbrains.kotlinx.dataframe.impl.schema.getPropertiesOrder
7+
import org.jetbrains.kotlinx.dataframe.impl.schema.getPropertyOrderFromPrimaryConstructor
88
import org.jetbrains.kotlinx.dataframe.schema.ColumnSchema
99
import kotlin.reflect.KClass
1010
import kotlin.reflect.KType
@@ -53,7 +53,7 @@ internal object MarkersExtractor {
5353
}
5454

5555
private fun getFields(markerClass: KClass<*>, nullableProperties: Boolean): List<GeneratedField> {
56-
val order = getPropertiesOrder(markerClass)
56+
val order = getPropertyOrderFromPrimaryConstructor(markerClass) ?: emptyMap()
5757
return markerClass.memberProperties.sortedBy { order[it.name] ?: Int.MAX_VALUE }.mapIndexed { _, it ->
5858
val fieldName = ValidFieldName.of(it.name)
5959
val columnName = it.findAnnotation<ColumnName>()?.name ?: fieldName.unquoted

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import java.net.URL
1616
import java.time.LocalDateTime
1717
import java.time.LocalTime
1818
import kotlin.reflect.KType
19+
import kotlin.reflect.full.isSubtypeOf
1920
import kotlin.reflect.jvm.jvmErasure
21+
import kotlin.reflect.typeOf
2022

2123
internal fun String.truncate(limit: Int): RenderedContent = if (limit in 1 until length) {
2224
if (limit < 4) RenderedContent.truncatedText("...", this)
@@ -57,6 +59,11 @@ internal fun renderType(type: KType?): String {
5759
else -> {
5860
val fullName = type.jvmErasure.qualifiedName ?: return type.toString()
5961
val name = when {
62+
// catching cases like `typeOf<Array<Int>>().jvmErasure.qualifiedName == "IntArray"`
63+
// https://github.com/Kotlin/dataframe/issues/678
64+
type.isSubtypeOf(typeOf<Array<*>>()) ->
65+
"Array"
66+
6067
type.classifier == URL::class ->
6168
fullName.removePrefix("java.net.")
6269

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

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@file:OptIn(ExperimentalUnsignedTypes::class)
2+
13
package org.jetbrains.kotlinx.dataframe.impl
24

35
import org.jetbrains.kotlinx.dataframe.AnyFrame
@@ -467,3 +469,100 @@ internal fun nothingType(nullable: Boolean): KType =
467469
} else {
468470
typeOf<List<Nothing>>()
469471
}.arguments.first().type!!
472+
473+
@OptIn(ExperimentalUnsignedTypes::class)
474+
private val primitiveArrayClasses = setOf(
475+
BooleanArray::class,
476+
ByteArray::class,
477+
ShortArray::class,
478+
IntArray::class,
479+
LongArray::class,
480+
FloatArray::class,
481+
DoubleArray::class,
482+
CharArray::class,
483+
484+
UByteArray::class,
485+
UShortArray::class,
486+
UIntArray::class,
487+
ULongArray::class,
488+
)
489+
490+
/**
491+
* Returns `true` if this class is a primitive array class like `XArray`.
492+
*
493+
* Use [KClass.isArray] to also check for `Array<>`.
494+
*/
495+
internal val KClass<*>.isPrimitiveArray: Boolean
496+
get() = this in primitiveArrayClasses
497+
498+
/**
499+
* Returns `true` if this class is an array, either a primitive array like `XArray` or `Array<>`.
500+
*
501+
* Use [KClass.isPrimitiveArray] to only check for primitive arrays.
502+
*/
503+
internal val KClass<*>.isArray: Boolean
504+
get() = this in primitiveArrayClasses ||
505+
this.qualifiedName == Array::class.qualifiedName // instance check fails
506+
507+
/**
508+
* Returns `true` if this type is of a primitive array like `XArray`.
509+
*
510+
* Use [KType.isArray] to also check for `Array<>`.
511+
*/
512+
internal val KType.isPrimitiveArray: Boolean
513+
get() =
514+
if (arguments.isNotEmpty()) {
515+
// Catching https://github.com/Kotlin/dataframe/issues/678
516+
// as typeOf<Array<Int>>().classifier == IntArray::class
517+
false
518+
} else {
519+
(classifier as? KClass<*>)?.isPrimitiveArray == true
520+
}
521+
522+
/**
523+
* Returns `true` if this type is of an array, either a primitive array like `XArray` or `Array<>`.
524+
*
525+
* Use [KType.isPrimitiveArray] to only check for primitive arrays.
526+
*/
527+
internal val KType.isArray: Boolean
528+
get() = (classifier as? KClass<*>)?.isArray == true
529+
530+
/**
531+
* Returns `true` if this object is a primitive array like `XArray`.
532+
*
533+
* Use [Any.isArray] to also check for the `Array<>` object.
534+
*/
535+
internal val Any.isPrimitiveArray: Boolean
536+
get() = this::class.isPrimitiveArray
537+
538+
/**
539+
* Returns `true` if this object is an array, either a primitive array like `XArray` or `Array<>`.
540+
*
541+
* Use [Any.isPrimitiveArray] to only check for primitive arrays.
542+
*/
543+
internal val Any.isArray: Boolean
544+
get() = this::class.isArray
545+
546+
/**
547+
* If [this] is an array of any kind, the function returns it as a list of values,
548+
* else it returns `null`.
549+
*/
550+
internal fun Any.asArrayAsListOrNull(): List<*>? =
551+
when (this) {
552+
is BooleanArray -> asList()
553+
is ByteArray -> asList()
554+
is ShortArray -> asList()
555+
is IntArray -> asList()
556+
is LongArray -> asList()
557+
is FloatArray -> asList()
558+
is DoubleArray -> asList()
559+
is CharArray -> asList()
560+
561+
is UByteArray -> asList()
562+
is UShortArray -> asList()
563+
is UIntArray -> asList()
564+
is ULongArray -> asList()
565+
566+
is Array<*> -> asList()
567+
else -> null
568+
}

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

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import org.jetbrains.kotlinx.dataframe.impl.columns.toColumnSet
1414
import org.jetbrains.kotlinx.dataframe.nrow
1515
import java.math.BigDecimal
1616
import java.math.BigInteger
17+
import kotlin.reflect.KCallable
1718
import kotlin.reflect.KClass
19+
import kotlin.reflect.KFunction
1820
import kotlin.reflect.KProperty
1921
import kotlin.reflect.KType
2022
import kotlin.reflect.KTypeProjection
@@ -23,6 +25,7 @@ import kotlin.reflect.full.createType
2325
import kotlin.reflect.full.findAnnotation
2426
import kotlin.reflect.full.isSubtypeOf
2527
import kotlin.reflect.full.starProjectedType
28+
import kotlin.reflect.full.valueParameters
2629
import kotlin.reflect.full.withNullability
2730
import kotlin.reflect.jvm.jvmErasure
2831
import kotlin.reflect.typeOf
@@ -337,5 +340,61 @@ internal fun List<String>.joinToCamelCaseString(): String {
337340
.replaceFirstChar { it.lowercaseChar() }
338341
}
339342

343+
/** Returns `true` if this callable is a getter-like function.
344+
*
345+
* A callable is considered getter-like if it is either a property getter,
346+
* or it's a function with no (type) parameters that starts with "get"/"is". */
347+
internal fun KFunction<*>.isGetterLike(): Boolean =
348+
(name.startsWith("get") || name.startsWith("is")) &&
349+
valueParameters.isEmpty() &&
350+
typeParameters.isEmpty()
351+
352+
/** Returns `true` if this callable is a getter-like function.
353+
*
354+
* A callable is considered getter-like if it is either a property getter,
355+
* or it's a function with no (type) parameters that starts with "get"/"is". */
356+
internal fun KProperty<*>.isGetterLike(): Boolean = true
357+
358+
/**
359+
* Returns `true` if this callable is a getter-like function.
360+
*
361+
* A callable is considered getter-like if it is either a property getter,
362+
* or it's a function with no (type) parameters that starts with "get"/"is".
363+
*/
364+
internal fun KCallable<*>.isGetterLike(): Boolean =
365+
when (this) {
366+
is KProperty<*> -> isGetterLike()
367+
is KFunction<*> -> isGetterLike()
368+
else -> false
369+
}
370+
371+
/** Returns the column name for this callable.
372+
* If the callable contains the [ColumnName][org.jetbrains.kotlinx.dataframe.annotations.ColumnName] annotation, its [ColumnName.name][org.jetbrains.kotlinx.dataframe.annotations.ColumnName.name] is returned.
373+
* Otherwise, the name of the callable is returned with proper getter-trimming if it's a [KFunction]. */
374+
@PublishedApi
375+
internal val KFunction<*>.columnName: String
376+
get() = findAnnotation<ColumnName>()?.name
377+
?: name
378+
.removePrefix("get")
379+
.removePrefix("is")
380+
.replaceFirstChar { it.lowercase() }
381+
382+
/** Returns the column name for this callable.
383+
* If the callable contains the [ColumnName][org.jetbrains.kotlinx.dataframe.annotations.ColumnName] annotation, its [ColumnName.name][org.jetbrains.kotlinx.dataframe.annotations.ColumnName.name] is returned.
384+
* Otherwise, the name of the callable is returned with proper getter-trimming if it's a [KFunction]. */
340385
@PublishedApi
341-
internal val <T> KProperty<T>.columnName: String get() = findAnnotation<ColumnName>()?.name ?: name
386+
internal val KProperty<*>.columnName: String
387+
get() = findAnnotation<ColumnName>()?.name ?: name
388+
389+
/**
390+
* Returns the column name for this callable.
391+
* If the callable contains the [ColumnName] annotation, its [ColumnName.name] is returned.
392+
* Otherwise, the name of the callable is returned with proper getter-trimming if it's a [KFunction].
393+
*/
394+
@PublishedApi
395+
internal val KCallable<*>.columnName: String
396+
get() = when (this) {
397+
is KFunction<*> -> columnName
398+
is KProperty<*> -> columnName
399+
else -> findAnnotation<ColumnName>()?.name ?: name
400+
}

0 commit comments

Comments
 (0)