Skip to content

Commit

Permalink
Merge pull request #650 from Kotlin/pojo-toDataFrame
Browse files Browse the repository at this point in the history
POJO toDataFrame support (and array improvements)
  • Loading branch information
Jolanrensen authored May 1, 2024
2 parents 7b6669f + fca4b8f commit 35176c9
Show file tree
Hide file tree
Showing 24 changed files with 1,268 additions and 198 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import org.jetbrains.kotlinx.dataframe.impl.asList
import org.jetbrains.kotlinx.dataframe.impl.columnName
import org.jetbrains.kotlinx.dataframe.impl.columns.guessColumnType
import org.jetbrains.kotlinx.dataframe.index
import kotlin.reflect.KCallable
import kotlin.reflect.KClass
import kotlin.reflect.KProperty

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

/**
* Skip given [classes] during recursive (dfs) traversal
* Skip given [classes] during recursive (dfs) traversal.
*/
public fun exclude(vararg classes: KClass<*>)

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

/**
* Store given [classes] in ValueColumns without transformation into ColumnGroups or FrameColumns
* Store given [classes] in ValueColumns without transformation into ColumnGroups or FrameColumns.
*/
public fun preserve(vararg classes: KClass<*>)

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

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

public abstract fun properties(
vararg roots: KProperty<*>,
vararg roots: KCallable<*>,
maxDepth: Int = 0,
body: (TraversePropertiesDsl.() -> Unit)? = null,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import org.jetbrains.kotlinx.dataframe.DataFrame
import org.jetbrains.kotlinx.dataframe.DataRow
import org.jetbrains.kotlinx.dataframe.annotations.ColumnName
import org.jetbrains.kotlinx.dataframe.annotations.DataSchema
import org.jetbrains.kotlinx.dataframe.impl.schema.getPropertiesOrder
import org.jetbrains.kotlinx.dataframe.impl.schema.getPropertyOrderFromPrimaryConstructor
import org.jetbrains.kotlinx.dataframe.schema.ColumnSchema
import kotlin.reflect.KClass
import kotlin.reflect.KType
Expand Down Expand Up @@ -53,7 +53,7 @@ internal object MarkersExtractor {
}

private fun getFields(markerClass: KClass<*>, nullableProperties: Boolean): List<GeneratedField> {
val order = getPropertiesOrder(markerClass)
val order = getPropertyOrderFromPrimaryConstructor(markerClass) ?: emptyMap()
return markerClass.memberProperties.sortedBy { order[it.name] ?: Int.MAX_VALUE }.mapIndexed { _, it ->
val fieldName = ValidFieldName.of(it.name)
val columnName = it.findAnnotation<ColumnName>()?.name ?: fieldName.unquoted
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import java.net.URL
import java.time.LocalDateTime
import java.time.LocalTime
import kotlin.reflect.KType
import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.jvm.jvmErasure
import kotlin.reflect.typeOf

internal fun String.truncate(limit: Int): RenderedContent = if (limit in 1 until length) {
if (limit < 4) RenderedContent.truncatedText("...", this)
Expand Down Expand Up @@ -57,6 +59,11 @@ internal fun renderType(type: KType?): String {
else -> {
val fullName = type.jvmErasure.qualifiedName ?: return type.toString()
val name = when {
// catching cases like `typeOf<Array<Int>>().jvmErasure.qualifiedName == "IntArray"`
// https://github.com/Kotlin/dataframe/issues/678
type.isSubtypeOf(typeOf<Array<*>>()) ->
"Array"

type.classifier == URL::class ->
fullName.removePrefix("java.net.")

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:OptIn(ExperimentalUnsignedTypes::class)

package org.jetbrains.kotlinx.dataframe.impl

import org.jetbrains.kotlinx.dataframe.AnyFrame
Expand Down Expand Up @@ -467,3 +469,100 @@ internal fun nothingType(nullable: Boolean): KType =
} else {
typeOf<List<Nothing>>()
}.arguments.first().type!!

@OptIn(ExperimentalUnsignedTypes::class)
private val primitiveArrayClasses = setOf(
BooleanArray::class,
ByteArray::class,
ShortArray::class,
IntArray::class,
LongArray::class,
FloatArray::class,
DoubleArray::class,
CharArray::class,

UByteArray::class,
UShortArray::class,
UIntArray::class,
ULongArray::class,
)

/**
* Returns `true` if this class is a primitive array class like `XArray`.
*
* Use [KClass.isArray] to also check for `Array<>`.
*/
internal val KClass<*>.isPrimitiveArray: Boolean
get() = this in primitiveArrayClasses

/**
* Returns `true` if this class is an array, either a primitive array like `XArray` or `Array<>`.
*
* Use [KClass.isPrimitiveArray] to only check for primitive arrays.
*/
internal val KClass<*>.isArray: Boolean
get() = this in primitiveArrayClasses ||
this.qualifiedName == Array::class.qualifiedName // instance check fails

/**
* Returns `true` if this type is of a primitive array like `XArray`.
*
* Use [KType.isArray] to also check for `Array<>`.
*/
internal val KType.isPrimitiveArray: Boolean
get() =
if (arguments.isNotEmpty()) {
// Catching https://github.com/Kotlin/dataframe/issues/678
// as typeOf<Array<Int>>().classifier == IntArray::class
false
} else {
(classifier as? KClass<*>)?.isPrimitiveArray == true
}

/**
* Returns `true` if this type is of an array, either a primitive array like `XArray` or `Array<>`.
*
* Use [KType.isPrimitiveArray] to only check for primitive arrays.
*/
internal val KType.isArray: Boolean
get() = (classifier as? KClass<*>)?.isArray == true

/**
* Returns `true` if this object is a primitive array like `XArray`.
*
* Use [Any.isArray] to also check for the `Array<>` object.
*/
internal val Any.isPrimitiveArray: Boolean
get() = this::class.isPrimitiveArray

/**
* Returns `true` if this object is an array, either a primitive array like `XArray` or `Array<>`.
*
* Use [Any.isPrimitiveArray] to only check for primitive arrays.
*/
internal val Any.isArray: Boolean
get() = this::class.isArray

/**
* If [this] is an array of any kind, the function returns it as a list of values,
* else it returns `null`.
*/
internal fun Any.asArrayAsListOrNull(): List<*>? =
when (this) {
is BooleanArray -> asList()
is ByteArray -> asList()
is ShortArray -> asList()
is IntArray -> asList()
is LongArray -> asList()
is FloatArray -> asList()
is DoubleArray -> asList()
is CharArray -> asList()

is UByteArray -> asList()
is UShortArray -> asList()
is UIntArray -> asList()
is ULongArray -> asList()

is Array<*> -> asList()
else -> null
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import org.jetbrains.kotlinx.dataframe.impl.columns.toColumnSet
import org.jetbrains.kotlinx.dataframe.nrow
import java.math.BigDecimal
import java.math.BigInteger
import kotlin.reflect.KCallable
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KProperty
import kotlin.reflect.KType
import kotlin.reflect.KTypeProjection
Expand All @@ -23,6 +25,7 @@ import kotlin.reflect.full.createType
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.full.starProjectedType
import kotlin.reflect.full.valueParameters
import kotlin.reflect.full.withNullability
import kotlin.reflect.jvm.jvmErasure
import kotlin.reflect.typeOf
Expand Down Expand Up @@ -337,5 +340,61 @@ internal fun List<String>.joinToCamelCaseString(): String {
.replaceFirstChar { it.lowercaseChar() }
}

/** Returns `true` if this callable is a getter-like function.
*
* A callable is considered getter-like if it is either a property getter,
* or it's a function with no (type) parameters that starts with "get"/"is". */
internal fun KFunction<*>.isGetterLike(): Boolean =
(name.startsWith("get") || name.startsWith("is")) &&
valueParameters.isEmpty() &&
typeParameters.isEmpty()

/** Returns `true` if this callable is a getter-like function.
*
* A callable is considered getter-like if it is either a property getter,
* or it's a function with no (type) parameters that starts with "get"/"is". */
internal fun KProperty<*>.isGetterLike(): Boolean = true

/**
* Returns `true` if this callable is a getter-like function.
*
* A callable is considered getter-like if it is either a property getter,
* or it's a function with no (type) parameters that starts with "get"/"is".
*/
internal fun KCallable<*>.isGetterLike(): Boolean =
when (this) {
is KProperty<*> -> isGetterLike()
is KFunction<*> -> isGetterLike()
else -> false
}

/** Returns the column name for this callable.
* 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.
* Otherwise, the name of the callable is returned with proper getter-trimming if it's a [KFunction]. */
@PublishedApi
internal val KFunction<*>.columnName: String
get() = findAnnotation<ColumnName>()?.name
?: name
.removePrefix("get")
.removePrefix("is")
.replaceFirstChar { it.lowercase() }

/** Returns the column name for this callable.
* 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.
* Otherwise, the name of the callable is returned with proper getter-trimming if it's a [KFunction]. */
@PublishedApi
internal val <T> KProperty<T>.columnName: String get() = findAnnotation<ColumnName>()?.name ?: name
internal val KProperty<*>.columnName: String
get() = findAnnotation<ColumnName>()?.name ?: name

/**
* Returns the column name for this callable.
* If the callable contains the [ColumnName] annotation, its [ColumnName.name] is returned.
* Otherwise, the name of the callable is returned with proper getter-trimming if it's a [KFunction].
*/
@PublishedApi
internal val KCallable<*>.columnName: String
get() = when (this) {
is KFunction<*> -> columnName
is KProperty<*> -> columnName
else -> findAnnotation<ColumnName>()?.name ?: name
}
Loading

0 comments on commit 35176c9

Please sign in to comment.