Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POJO toDataFrame support (and array improvements) #650

Merged
merged 14 commits into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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