diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/convert.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/convert.kt index 3aa6c55cd..3d65fc355 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/convert.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/convert.kt @@ -110,6 +110,10 @@ public fun AnyCol.convertTo(newType: KType): AnyCol { public fun DataColumn.convertToLocalDateTime(): DataColumn = convertTo() public fun DataColumn.convertToLocalDateTime(): DataColumn = convertTo() +@JvmName("convertToLocalDateFromT") +public fun DataColumn.convertToLocalDate(): DataColumn = convertTo() +public fun DataColumn.convertToLocalDate(): DataColumn = convertTo() + @JvmName("convertToLocalTimeFromT") public fun DataColumn.convertToLocalTime(): DataColumn = convertTo() public fun DataColumn.convertToLocalTime(): DataColumn = convertTo() diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/exceptions/CellConversionException.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/exceptions/CellConversionException.kt new file mode 100644 index 000000000..3955b294c --- /dev/null +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/exceptions/CellConversionException.kt @@ -0,0 +1,15 @@ +package org.jetbrains.kotlinx.dataframe.exceptions + +import kotlin.reflect.KType + +public class CellConversionException( + value: Any?, + from: KType, + to: KType, + public val column: String, + public val row: Int?, + override val cause: Throwable? +) : TypeConversionException(value, from, to) { + override val message: String + get() = "${super.message} in column $column, row $row" +} diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/exceptions/TypeConversionException.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/exceptions/TypeConversionException.kt index 0995a7cb4..8b3bd54fa 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/exceptions/TypeConversionException.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/exceptions/TypeConversionException.kt @@ -1,8 +1,8 @@ package org.jetbrains.kotlinx.dataframe.exceptions -import kotlin.reflect.* +import kotlin.reflect.KType -public class TypeConversionException(public val value: Any?, public val from: KType, public val to: KType) : RuntimeException() { +public open class TypeConversionException(public val value: Any?, public val from: KType, public val to: KType) : RuntimeException() { override val message: String get() = "Failed to convert '$value' from $from to $to" diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/convert.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/convert.kt index 4d7e91917..8272feae7 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/convert.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/convert.kt @@ -27,6 +27,7 @@ import org.jetbrains.kotlinx.dataframe.api.to import org.jetbrains.kotlinx.dataframe.columns.values import org.jetbrains.kotlinx.dataframe.dataTypes.IFRAME import org.jetbrains.kotlinx.dataframe.dataTypes.IMG +import org.jetbrains.kotlinx.dataframe.exceptions.CellConversionException import org.jetbrains.kotlinx.dataframe.exceptions.TypeConversionException import org.jetbrains.kotlinx.dataframe.exceptions.TypeConverterNotFoundException import org.jetbrains.kotlinx.dataframe.impl.columns.DataColumnInternal @@ -79,18 +80,32 @@ internal fun AnyCol.convertToTypeImpl(to: KType): AnyCol { else -> throw TypeConversionException(null, from, to) } - return when { - from == to -> this - from.isSubtypeOf(to) -> (this as DataColumnInternal<*>).changeType(to.withNullability(hasNulls())) - else -> when (val converter = getConverter(from, to, ParserOptions(locale = Locale.getDefault()))) { - null -> when (from.classifier) { - Any::class, Number::class, java.io.Serializable::class -> { + fun applyConverter(converter: TypeConverter): AnyCol { + var currentRow = 0 + try { + val values = values.mapIndexed { row, value -> + currentRow = row + value?.let { converter(value) }.checkNulls() + } + return DataColumn.createValueColumn(name, values, to.withNullability(nullsFound)) + } catch (e: TypeConversionException) { + throw CellConversionException(e.value, e.from, e.to, this.name(), currentRow, e) + } + } + + fun convertPerCell(): AnyCol { + var currentRow = 0 + try { + return when (from.classifier) { + Any::class, Comparable::class, Number::class, java.io.Serializable::class -> { // find converter for every value - val values = values.map { - it?.let { + val values = values.mapIndexed { row, value -> + currentRow = row + value?.let { val clazz = it.javaClass.kotlin val type = clazz.createStarProjectedType(false) - val converter = getConverter(type, to, ParserOptions(locale = Locale.getDefault())) ?: throw TypeConverterNotFoundException(from, to) + val converter = getConverter(type, to, ParserOptions(locale = Locale.getDefault())) + ?: throw TypeConverterNotFoundException(from, to) converter(it) }.checkNulls() } @@ -98,12 +113,17 @@ internal fun AnyCol.convertToTypeImpl(to: KType): AnyCol { } else -> throw TypeConverterNotFoundException(from, to) } - else -> { - val values = values.map { - it?.let { converter(it) }.checkNulls() - } - DataColumn.createValueColumn(name, values, to.withNullability(nullsFound)) - } + } catch (e: TypeConversionException) { + throw CellConversionException(e.value, e.from, e.to, this.name(), currentRow, e) + } + } + + return when { + from == to -> this + from.isSubtypeOf(to) -> (this as DataColumnInternal<*>).changeType(to.withNullability(hasNulls())) + else -> when (val converter = getConverter(from, to, ParserOptions(locale = Locale.getDefault()))) { + null -> convertPerCell() + else -> applyConverter(converter) } } } diff --git a/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/convert.kt b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/convert.kt index 527f34a47..e670d749e 100644 --- a/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/convert.kt +++ b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/convert.kt @@ -8,6 +8,7 @@ import kotlinx.datetime.Instant import org.jetbrains.kotlinx.dataframe.DataColumn import org.jetbrains.kotlinx.dataframe.DataFrame import org.jetbrains.kotlinx.dataframe.annotations.DataSchema +import org.jetbrains.kotlinx.dataframe.exceptions.CellConversionException import org.jetbrains.kotlinx.dataframe.exceptions.TypeConversionException import org.jetbrains.kotlinx.dataframe.exceptions.TypeConverterNotFoundException import org.jetbrains.kotlinx.dataframe.hasNulls @@ -80,6 +81,10 @@ class ConvertTests { columnOf("a").convertTo() } + shouldThrow { + columnOf("1", "10", "a").convertTo() + }.row shouldBe 2 + shouldThrow { columnOf(EnumClass.A).convertTo() } diff --git a/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/ParserTests.kt b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/ParserTests.kt index be7fef78c..f4a8ab160 100644 --- a/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/ParserTests.kt +++ b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/ParserTests.kt @@ -3,18 +3,27 @@ package org.jetbrains.kotlinx.dataframe.io import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toKotlinLocalDate +import kotlinx.datetime.toKotlinLocalDateTime import org.jetbrains.kotlinx.dataframe.DataColumn import org.jetbrains.kotlinx.dataframe.DataFrame import org.jetbrains.kotlinx.dataframe.api.cast import org.jetbrains.kotlinx.dataframe.api.columnOf import org.jetbrains.kotlinx.dataframe.api.convertTo import org.jetbrains.kotlinx.dataframe.api.convertToDouble +import org.jetbrains.kotlinx.dataframe.api.convertToLocalDate +import org.jetbrains.kotlinx.dataframe.api.convertToLocalDateTime +import org.jetbrains.kotlinx.dataframe.api.convertToLocalTime import org.jetbrains.kotlinx.dataframe.api.parse import org.jetbrains.kotlinx.dataframe.api.parser +import org.jetbrains.kotlinx.dataframe.api.plus +import org.jetbrains.kotlinx.dataframe.api.times import org.jetbrains.kotlinx.dataframe.api.tryParse import org.jetbrains.kotlinx.dataframe.exceptions.TypeConversionException import org.junit.Test import java.math.BigDecimal +import java.time.LocalTime import java.util.Locale import kotlin.reflect.typeOf @@ -75,6 +84,50 @@ class ParserTests { ) } + @Test + fun `convert to date and time`() { + val daysToStandardMillis = 24 * 60 * 60 * 1000L + val longCol = columnOf(1L, 60L, 3600L).times(1000L).plus(daysToStandardMillis * 366) + val datetimeCol = longCol.convertToLocalDateTime(TimeZone.UTC) + + datetimeCol.shouldBe( + columnOf( + java.time.LocalDateTime.of(1971, 1, 2, 0, 0, 1).toKotlinLocalDateTime(), + java.time.LocalDateTime.of(1971, 1, 2, 0, 1, 0).toKotlinLocalDateTime(), + java.time.LocalDateTime.of(1971, 1, 2, 1, 0, 0).toKotlinLocalDateTime() + ) + ) + longCol.convertToLocalDate(TimeZone.UTC).shouldBe( + columnOf( + java.time.LocalDate.of(1971, 1, 2).toKotlinLocalDate(), + java.time.LocalDate.of(1971, 1, 2).toKotlinLocalDate(), + java.time.LocalDate.of(1971, 1, 2).toKotlinLocalDate() + ) + ) + longCol.convertToLocalTime(TimeZone.UTC).shouldBe( + columnOf( + LocalTime.of(0, 0, 1), + LocalTime.of(0, 1, 0), + LocalTime.of(1, 0, 0) + ) + ) + + datetimeCol.convertToLocalDate().shouldBe( + columnOf( + java.time.LocalDate.of(1971, 1, 2).toKotlinLocalDate(), + java.time.LocalDate.of(1971, 1, 2).toKotlinLocalDate(), + java.time.LocalDate.of(1971, 1, 2).toKotlinLocalDate() + ) + ) + datetimeCol.convertToLocalTime().shouldBe( + columnOf( + LocalTime.of(0, 0, 1), + LocalTime.of(0, 1, 0), + LocalTime.of(1, 0, 0) + ) + ) + } + @Test fun `converting String to Double in different locales`() { val currentLocale = Locale.getDefault()