Skip to content
Draft
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
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ allprojects {
// enables support for kotlin.time.Instant as kotlinx.datetime.Instant was deprecated; Issue #1350
// Can be removed once kotlin.time.Instant is marked "stable".
optIn.add("kotlin.time.ExperimentalTime")
freeCompilerArgs.addAll("-Xallow-contracts-on-more-functions")
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
@file:OptIn(ExperimentalContracts::class)
@file:OptIn(ExperimentalContracts::class, ExperimentalExtendedContracts::class)

package org.jetbrains.kotlinx.dataframe.api

import org.jetbrains.kotlinx.dataframe.AnyCol
import org.jetbrains.kotlinx.dataframe.DataColumn
import org.jetbrains.kotlinx.dataframe.columns.ColumnGroup
import org.jetbrains.kotlinx.dataframe.columns.ColumnKind
import org.jetbrains.kotlinx.dataframe.columns.FrameColumn
Expand All @@ -17,6 +18,7 @@ import org.jetbrains.kotlinx.dataframe.util.IS_COMPARABLE
import org.jetbrains.kotlinx.dataframe.util.IS_COMPARABLE_REPLACE
import org.jetbrains.kotlinx.dataframe.util.IS_INTER_COMPARABLE_IMPORT
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.ExperimentalExtendedContracts
import kotlin.contracts.contract
import kotlin.reflect.KType
import kotlin.reflect.full.isSubtypeOf
Expand All @@ -40,21 +42,36 @@ public fun AnyCol.isValueColumn(): Boolean {
public fun AnyCol.isSubtypeOf(type: KType): Boolean =
this.type.isSubtypeOf(type) && (!this.type.isMarkedNullable || type.isMarkedNullable)

public inline fun <reified T> AnyCol.isSubtypeOf(): Boolean = isSubtypeOf(typeOf<T>())
public inline fun <reified T> AnyCol.isSubtypeOf(): Boolean {
contract { returns(true) implies (this@isSubtypeOf is DataColumn<T>) }
return isSubtypeOf(typeOf<T>())
}

public inline fun <reified T> AnyCol.isType(): Boolean = type() == typeOf<T>()
public inline fun <reified T> AnyCol.isType(): Boolean {
contract { returns(true) implies (this@isType is DataColumn<T>) }
return type() == typeOf<T>()
}

/** Returns `true` when this column's type is a subtype of `Number?` */
public fun AnyCol.isNumber(): Boolean = isSubtypeOf<Number?>()
public fun AnyCol.isNumber(): Boolean {
contract { returns(true) implies (this@isNumber is ValueColumn<Number?>) }
return isSubtypeOf<Number?>()
}

/** Returns `true` only when this column's type is exactly `Number` or `Number?`. */
public fun AnyCol.isMixedNumber(): Boolean = type().isMixedNumber()
public fun AnyCol.isMixedNumber(): Boolean {
contract { returns(true) implies (this@isMixedNumber is ValueColumn<Number?>) }
return type().isMixedNumber()
}

/**
* Returns `true` when this column has the (nullable) type of either:
* [Byte], [Short], [Int], [Long], [Float], or [Double].
*/
public fun AnyCol.isPrimitiveNumber(): Boolean = type().isPrimitiveNumber()
public fun AnyCol.isPrimitiveNumber(): Boolean {
contract { returns(true) implies (this@isPrimitiveNumber is ValueColumn<Number?>) }
return type().isPrimitiveNumber()
}

/**
* Returns `true` when this column has the (nullable) type of either:
Expand All @@ -63,9 +80,15 @@ public fun AnyCol.isPrimitiveNumber(): Boolean = type().isPrimitiveNumber()
* Careful: Will return `true` if the column contains multiple number types that
* might NOT be primitive.
*/
public fun AnyCol.isPrimitiveOrMixedNumber(): Boolean = type().isPrimitiveOrMixedNumber()
public fun AnyCol.isPrimitiveOrMixedNumber(): Boolean {
contract { returns(true) implies (this@isPrimitiveOrMixedNumber is ValueColumn<Number?>) }
return type().isPrimitiveOrMixedNumber()
}

public fun AnyCol.isList(): Boolean = typeClass == List::class
public fun AnyCol.isList(): Boolean {
contract { returns(true) implies (this@isList is ValueColumn<List<*>>) }
Copy link
Collaborator

@koperagen koperagen Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to consider nullability: List<*>?
is it safe?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh you're right, should indeed be List<*>?

return typeClass == List::class
}

/** @include [valuesAreComparable] */
@Deprecated(
Expand All @@ -84,4 +107,7 @@ public fun AnyCol.isComparable(): Boolean = valuesAreComparable()
*
* Technically, this means the values' common type `T(?)` is a subtype of [Comparable]`<in T>(?)`
*/
public fun AnyCol.valuesAreComparable(): Boolean = isValueColumn() && type().isIntraComparable()
public fun <T> DataColumn<T>.valuesAreComparable(): Boolean {
contract { returns(true) implies (this@valuesAreComparable is ValueColumn<Comparable<T>>) }
return isValueColumn() && type().isIntraComparable()
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,13 @@ public fun DataColumn<Any>.asNumbers(): ValueColumn<Number> {

public fun <T : Any> DataColumn<T>.asComparable(): DataColumn<Comparable<T>> {
require(valuesAreComparable())
return this as DataColumn<Comparable<T>>
return this
}

@JvmName("asComparableNullable")
public fun <T : Any?> DataColumn<T?>.asComparable(): DataColumn<Comparable<T>?> {
require(valuesAreComparable())
return this as DataColumn<Comparable<T>?>
return this
}

public fun <T> ColumnReference<T?>.castToNotNullable(): ColumnReference<T> = cast()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ internal fun describeImpl(cols: List<AnyCol>): DataFrame<ColumnDescription> {
?.key
}
if (hasNumericCols) {
ColumnDescription::mean from { if (it.isNumber()) it.asNumbers().mean() else null }
ColumnDescription::std from { if (it.isNumber()) it.asNumbers().std() else null }
ColumnDescription::mean from { if (it.isNumber()) it.mean() else null }
ColumnDescription::std from { if (it.isNumber()) it.std() else null }
}
if (hasComparableCols || hasNumericCols) {
ColumnDescription::min from inferType {
Expand Down Expand Up @@ -113,10 +113,10 @@ private fun List<AnyCol>.collectAll(atAnyDepth: Boolean): List<AnyCol> =
@Suppress("UNCHECKED_CAST")
private fun DataColumn<Any?>.convertToComparableOrNull(): DataColumn<Comparable<Any>?>? {
return when {
valuesAreComparable() -> asComparable()
valuesAreComparable() -> this

// Found incomparable number types, convert all to Double first
isNumber() -> cast<Number?>().map {
isNumber() -> map {
if (it?.isPrimitiveNumber() == false) {
// Cannot calculate statistics of a non-primitive number type
return@convertToComparableOrNull null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -736,7 +736,7 @@ internal fun <T> DataFrame<T>.parseImpl(options: ParserOptions?, columns: Column

// Base case, parse the column if it's a `String?` column
col.isSubtypeOf<String?>() ->
col.cast<String?>().tryParseImpl(options)
col.tryParseImpl(options)

else -> col
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ internal fun AnyFrame.toHtmlData(
configuration: DisplayConfiguration,
): ColumnDataForJs {
val values = if (rowsLimit != null) rows().take(rowsLimit) else rows()
val scale = if (col.isNumber()) col.asNumbers().scale() else 1
val scale = if (col.isNumber()) col.scale() else 1
val format = if (scale > 0) {
RendererDecimalFormat.fromPrecision(scale)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public fun AnyFrame.renderToString(
}
val values = cols.map {
val top = it.take(rowsLimit)
val precision = if (top.isNumber()) top.asNumbers().scale() else 0
val precision = if (top.isNumber()) top.scale() else 0
val decimalFormat =
if (precision >= 0) RendererDecimalFormat.fromPrecision(precision) else RendererDecimalFormat.of("%e")
top.values().map {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ internal fun encodeValue(col: AnyCol, index: Int, customEncoders: List<CustomEnc
matchingEncoder != null -> matchingEncoder.encode(col[index])

col.isList() -> col[index]?.let { list ->
val values = (list as List<*>).map { convert(it) }
val values = list.map { convert(it) }
JsonArray(values)
} ?: JsonArray(emptyList())

Expand Down