Skip to content

Commit

Permalink
updating isGetterLike and columnName with kdocs and to be more explic…
Browse files Browse the repository at this point in the history
…it, updating convertToDataFrame implementation to be clearer too. Testing for different types of primitives from java pojo
  • Loading branch information
Jolanrensen committed Apr 25, 2024
1 parent 30b3f77 commit f3cc35c
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 95 deletions.
49 changes: 34 additions & 15 deletions core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -340,32 +340,51 @@ internal fun List<String>.joinToCamelCaseString(): String {
.replaceFirstChar { it.lowercaseChar() }
}

/** @include [KCallable.isGetterLike] */
internal fun KFunction<*>.isGetterLike(): Boolean =
(name.startsWith("get") || name.startsWith("is")) && valueParameters.isEmpty()
(name.startsWith("get") || name.startsWith("is")) &&
valueParameters.isEmpty() &&
typeParameters.isEmpty()

/** @include [KCallable.isGetterLike] */
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<*> -> true
is KProperty<*> -> isGetterLike()
is KFunction<*> -> isGetterLike()
else -> false
}

/** @include [KCallable.columnName] */
@PublishedApi
internal val KFunction<*>.columnName: String
get() = findAnnotation<ColumnName>()?.name
?: name
.removePrefix("get")
.removePrefix("is")
.replaceFirstChar { it.lowercase() }

/** @include [KCallable.columnName] */
@PublishedApi
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 iff it's a [KFunction].
*/
@PublishedApi
internal val KCallable<*>.columnName: String
get() = findAnnotation<ColumnName>()?.name
?: when (this) {
// for defining the column names based on a getter-function, we use the function name minus the get/is prefix
is KFunction<*> ->
name
.removePrefix("get")
.removePrefix("is")
.replaceFirstChar { it.lowercase() }

is KProperty<*> -> this.columnName

else -> name
}
get() = when (this) {
is KFunction<*> -> columnName
is KProperty<*> -> columnName
else -> findAnnotation<ColumnName>()?.name ?: name
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaField
import kotlin.reflect.typeOf

internal val valueTypes = setOf(
private val valueTypes = setOf(
String::class,
Boolean::class,
kotlin.time.Duration::class,
Expand Down Expand Up @@ -186,21 +186,21 @@ internal fun convertToDataFrame(
class ValueClassConverter(val unbox: Method, val box: Method)

val valueClassConverter = (it.returnType.classifier as? KClass<*>)?.let { kClass ->
if (!kClass.isValue) null else {
val constructor = requireNotNull(kClass.primaryConstructor) {
"value class $kClass is expected to have primary constructor, but couldn't obtain it"
}
val parameter = constructor.parameters.singleOrNull()
?: error("conversion of value class $kClass with multiple parameters in constructor is not yet supported")
// there's no need to unwrap if underlying field is nullable
if (parameter.type.isMarkedNullable) return@let null
// box and unbox impl methods are part of binary API of value classes
// https://youtrack.jetbrains.com/issue/KT-50518/Boxing-Unboxing-methods-for-JvmInline-value-classes-should-be-public-accessible
val unbox = kClass.java.getMethod("unbox-impl")
val box = kClass.java.methods.single { it.name == "box-impl" }
val valueClassConverter = ValueClassConverter(unbox, box)
valueClassConverter
if (!kClass.isValue) return@let null

val constructor = requireNotNull(kClass.primaryConstructor) {
"value class $kClass is expected to have primary constructor, but couldn't obtain it"
}
val parameter = constructor.parameters.singleOrNull()
?: error("conversion of value class $kClass with multiple parameters in constructor is not yet supported")
// there's no need to unwrap if underlying field is nullable
if (parameter.type.isMarkedNullable) return@let null
// box and unbox impl methods are part of binary API of value classes
// https://youtrack.jetbrains.com/issue/KT-50518/Boxing-Unboxing-methods-for-JvmInline-value-classes-should-be-public-accessible
val unbox = kClass.java.getMethod("unbox-impl")
val box = kClass.java.methods.single { it.name == "box-impl" }
val valueClassConverter = ValueClassConverter(unbox, box)
valueClassConverter
}
(property as? KProperty<*>)?.javaField?.isAccessible = true
property.isAccessible = true
Expand Down Expand Up @@ -245,84 +245,99 @@ internal fun convertToDataFrame(
typeOf<Any>()
}
}
val kclass = (returnType.classifier as KClass<*>)
val kClass = returnType.classifier as KClass<*>

val shouldCreateValueCol = (
maxDepth <= 0 &&
!returnType.shouldBeConvertedToFrameColumn() &&
!returnType.shouldBeConvertedToColumnGroup()
) ||
kClass == Any::class ||
kClass in preserveClasses ||
property in preserveProperties ||
kClass.isValueType

val shouldCreateFrameCol = kClass == DataFrame::class && !nullable
val shouldCreateColumnGroup = kClass == DataRow::class

when {
hasExceptions -> DataColumn.createWithTypeInference(it.columnName, values, nullable)

kclass == Any::class ||
preserveClasses.contains(kclass) ||
preserveProperties.contains(property) ||
(maxDepth <= 0 && !returnType.shouldBeConvertedToFrameColumn() && !returnType.shouldBeConvertedToColumnGroup()) ||
kclass.isValueType ->
shouldCreateValueCol ->
DataColumn.createValueColumn(
name = it.columnName,
values = values,
type = returnType.withNullability(nullable),
)

kclass == DataFrame::class && !nullable ->
shouldCreateFrameCol ->
DataColumn.createFrameColumn(
name = it.columnName,
groups = values as List<AnyFrame>
)

kclass == DataRow::class ->
shouldCreateColumnGroup ->
DataColumn.createColumnGroup(
name = it.columnName,
df = (values as List<AnyRow>).concat(),
)

kclass.isSubclassOf(Iterable::class) -> {
val elementType = returnType.projectUpTo(Iterable::class).arguments.firstOrNull()?.type
if (elementType == null) {
DataColumn.createValueColumn(it.columnName, values, returnType.withNullability(nullable))
} else {
val elementClass = (elementType.classifier as? KClass<*>)

when {
elementClass == null -> {
val listValues = values.map {
(it as? Iterable<*>)?.asList()
}
kClass.isSubclassOf(Iterable::class) ->
when (val elementType = returnType.projectUpTo(Iterable::class).arguments.firstOrNull()?.type) {
null ->
DataColumn.createValueColumn(
name = it.columnName,
values = values,
type = returnType.withNullability(nullable),
)

else -> {
val elementClass = elementType.classifier as? KClass<*>
when {
elementClass == null -> {
val listValues = values.map {
(it as? Iterable<*>)?.asList()
}

DataColumn.createWithTypeInference(it.columnName, listValues)
}
DataColumn.createWithTypeInference(it.columnName, listValues)
}

elementClass.isValueType -> {
val listType = getListType(elementType).withNullability(nullable)
val listValues = values.map {
(it as? Iterable<*>)?.asList()
elementClass.isValueType -> {
val listType = getListType(elementType).withNullability(nullable)
val listValues = values.map {
(it as? Iterable<*>)?.asList()
}
DataColumn.createValueColumn(it.columnName, listValues, listType)
}
DataColumn.createValueColumn(it.columnName, listValues, listType)
}

else -> {
val frames = values.map {
if (it == null) {
DataFrame.empty()
} else {
require(it is Iterable<*>)
convertToDataFrame(
data = it,
clazz = elementClass,
roots = emptyList(),
excludes = excludes,
preserveClasses = preserveClasses,
preserveProperties = preserveProperties,
maxDepth = maxDepth - 1,
)
else -> {
val frames = values.map {
if (it == null) {
DataFrame.empty()
} else {
require(it is Iterable<*>)
convertToDataFrame(
data = it,
clazz = elementClass,
roots = emptyList(),
excludes = excludes,
preserveClasses = preserveClasses,
preserveProperties = preserveProperties,
maxDepth = maxDepth - 1,
)
}
}
DataColumn.createFrameColumn(it.columnName, frames)
}
DataColumn.createFrameColumn(it.columnName, frames)
}
}
}
}


else -> {
val df = convertToDataFrame(
data = values,
clazz = kclass,
clazz = kClass,
roots = emptyList(),
excludes = excludes,
preserveClasses = preserveClasses,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ public class JavaPojo {

private int a;
private String b;
private Integer c;
private Number d;

public JavaPojo() {}

public JavaPojo(String b, int a) {
public JavaPojo(Number d, Integer c, String b, int a) {
this.a = a;
this.b = b;
this.c = c;
this.d = d;
}

public int getA() {
Expand All @@ -30,29 +34,46 @@ public void setB(String b) {
this.b = b;
}

public Integer getC() {
return c;
}

public void setC(Integer c) {
this.c = c;
}

public Number getD() {
return d;
}

public void setD(Number d) {
this.d = d;
}

public static int getNot() {
return 1;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

JavaPojo testPojo = (JavaPojo) o;

if (a != testPojo.a) return false;
return Objects.equals(b, testPojo.b);
if (!(o instanceof JavaPojo)) return false;
JavaPojo javaPojo = (JavaPojo) o;
return a == javaPojo.a && Objects.equals(b, javaPojo.b) && Objects.equals(c, javaPojo.c) && Objects.equals(d, javaPojo.d);
}

@Override
public int hashCode() {
int result = a;
result = 31 * result + (b != null ? b.hashCode() : 0);
return result;
return Objects.hash(a, b, c, d);
}

@Override
public String toString() {
return "TestPojo{" +
return "JavaPojo{" +
"a=" + a +
", b='" + b + '\'' +
", c=" + c +
", d=" + d +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -370,17 +370,25 @@ class CreateDataFrameTests {
// even though the names b, a, follow the constructor order
listOf(KotlinPojo("bb", 1)).toDataFrame() shouldBe dataFrameOf("b", "a")("bb", 1)

// cannot read java constructor parameter names with reflection, so sort lexigographically
listOf(JavaPojo("bb", 1)).toDataFrame() shouldBe dataFrameOf("a", "b")(1, "bb")

listOf(KotlinPojo("bb", 1)).toDataFrame { properties(KotlinPojo::getA) } shouldBe dataFrameOf("a")(1)
listOf(KotlinPojo("bb", 1)).toDataFrame { properties(KotlinPojo::getB) } shouldBe dataFrameOf("b")("bb")

listOf(JavaPojo("bb", 1)).toDataFrame {
// cannot read java constructor parameter names with reflection, so sort lexicographically
listOf(JavaPojo(2.0, null, "bb", 1)).toDataFrame() shouldBe
dataFrameOf(
DataColumn.createValueColumn("a", listOf(1), typeOf<Int>()),
DataColumn.createValueColumn("b", listOf("bb"), typeOf<String>()),
DataColumn.createValueColumn("c", listOf(null), typeOf<Int?>()),
DataColumn.createValueColumn("d", listOf(2.0), typeOf<Number>()),
)

listOf(KotlinPojo("bb", 1)).toDataFrame { properties(KotlinPojo::getA) } shouldBe
dataFrameOf("a")(1)
listOf(KotlinPojo("bb", 1)).toDataFrame { properties(KotlinPojo::getB) } shouldBe
dataFrameOf("b")("bb").groupBy("").concat()

listOf(JavaPojo(2.0, 3, "bb", 1)).toDataFrame {
properties(JavaPojo::getA)
} shouldBe dataFrameOf("a")(1)

listOf(JavaPojo("bb", 1)).toDataFrame {
listOf(JavaPojo(2.0, 3, "bb", 1)).toDataFrame {
properties(JavaPojo::getB)
} shouldBe dataFrameOf("b")("bb")
}
Expand Down

0 comments on commit f3cc35c

Please sign in to comment.