Skip to content
Open
Show file tree
Hide file tree
Changes from 18 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
63 changes: 56 additions & 7 deletions core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/move.kt
Original file line number Diff line number Diff line change
Expand Up @@ -621,13 +621,62 @@ public fun <T, C> MoveClause<T, C>.after(column: KProperty<*>): DataFrame<T> = a

// endregion

/* TODO: implement 'before'
fun <T, C> MoveColsClause<T, C>.before(columnPath: ColumnPath) = before { columnPath.toColumnDef() }
fun <T, C> MoveColsClause<T, C>.before(column: Column) = before { column }
fun <T, C> MoveColsClause<T, C>.before(column: KProperty<*>) = before { column.toColumnDef() }
fun <T, C> MoveColsClause<T, C>.before(column: String) = before { column.toColumnDef() }
fun <T, C> MoveColsClause<T, C>.before(column: ColumnSelector<T, *>) = afterOrBefore(column, false)
*/
// region before

/**
* Moves columns, previously selected with [move] to the position before the
* specified [column] within the [DataFrame].
*
* Returns a new [DataFrame] with updated columns.
*
* See [Selecting Columns][SelectingColumns].
*
* For more information: {@include [DocumentationUrls.Move]}
*
* ### This Before Overload
*/
@ExcludeFromSources
internal interface MoveBefore

/**
* {@include [MoveBefore]}
* @include [SelectingColumns.Dsl]
*
* ### Examples:
* ```kotlin
* df.move { age and weight }.before { surname }
* df.move { cols(3..5) }.before { col(2) }
* ```
*
* @param [column] A [ColumnSelector] specifying the column
* before which the selected columns will be placed.
*/
@Refine
@Interpretable("MoveBefore0")
public fun <T, C> MoveClause<T, C>.before(column: ColumnSelector<T, *>): DataFrame<T> = afterOrBefore(column, false)

/**
* {@include [MoveBefore]}
* @include [SelectingColumns.ColumnNames]
*
* ### Examples:
* ```kotlin
* df.move("age", "weight").before("surname")
* ```
* @param [column] The [Column Name][String] specifying the column
* before which the selected columns will be placed.
*/
public fun <T, C> MoveClause<T, C>.before(column: String): DataFrame<T> = before { column.toColumnAccessor() }

@Deprecated(DEPRECATED_ACCESS_API)
@AccessApiOverload
public fun <T, C> MoveClause<T, C>.before(column: AnyColumnReference): DataFrame<T> = before { column }
Copy link
Collaborator

Choose a reason for hiding this comment

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

no need to add these deprecated overloads :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll remove them


@Deprecated(DEPRECATED_ACCESS_API)
@AccessApiOverload
public fun <T, C> MoveClause<T, C>.before(column: KProperty<*>): DataFrame<T> = before { column.toColumnAccessor() }

// endregion

@Deprecated(TO_LEFT, ReplaceWith(TO_LEFT_REPLACE), DeprecationLevel.ERROR)
public fun <T, C> MoveClause<T, C>.toLeft(): DataFrame<T> = to(0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,25 @@ import org.jetbrains.kotlinx.dataframe.DataColumn
import org.jetbrains.kotlinx.dataframe.DataFrame
import org.jetbrains.kotlinx.dataframe.api.ColumnsSelectionDsl
import org.jetbrains.kotlinx.dataframe.api.MoveClause
import org.jetbrains.kotlinx.dataframe.api.after
import org.jetbrains.kotlinx.dataframe.api.asColumnGroup
import org.jetbrains.kotlinx.dataframe.api.cast
import org.jetbrains.kotlinx.dataframe.api.getColumn
import org.jetbrains.kotlinx.dataframe.api.getColumnGroup
import org.jetbrains.kotlinx.dataframe.api.getColumnWithPath
import org.jetbrains.kotlinx.dataframe.api.move
import org.jetbrains.kotlinx.dataframe.api.toDataFrame
import org.jetbrains.kotlinx.dataframe.columns.ColumnPath
import org.jetbrains.kotlinx.dataframe.columns.ColumnWithPath
import org.jetbrains.kotlinx.dataframe.columns.UnresolvedColumnsPolicy
import org.jetbrains.kotlinx.dataframe.columns.toColumnSet
import org.jetbrains.kotlinx.dataframe.impl.DataFrameReceiver
import org.jetbrains.kotlinx.dataframe.impl.asList
import org.jetbrains.kotlinx.dataframe.impl.columns.toColumnWithPath
import org.jetbrains.kotlinx.dataframe.impl.columns.tree.ColumnPosition
import org.jetbrains.kotlinx.dataframe.impl.columns.tree.getOrPut
import org.jetbrains.kotlinx.dataframe.path

// TODO: support 'before' mode
internal fun <T, C> MoveClause<T, C>.afterOrBefore(column: ColumnSelector<T, *>, isAfter: Boolean): DataFrame<T> {
val removeResult = df.removeImpl(columns = columns)

Expand Down Expand Up @@ -78,7 +82,16 @@ internal fun <T, C> MoveClause<T, C>.afterOrBefore(column: ColumnSelector<T, *>,
}
ColumnToInsert(path, sourceCol.data, refNode)
}
return removeResult.df.insertImpl(toInsert)
if (isAfter) {
return removeResult.df.insertImpl(toInsert)
}

// move target after last of toInsert
Copy link
Collaborator

Choose a reason for hiding this comment

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

could you expand this doc a bit? I have a bit of a hard time understanding the logic below.

so... you insert the removed columns after the target column in the first place, and then you move the target column after the removed/inserted columns?

That's... really clever :D

But it took me a while to get it :) so please expand the doc a tiny bit :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you very much, I appreciate it. That's exactly the logic. I'll try to make the doc more undesrtandable.

val logicOfAfter = removeResult.df.insertImpl(toInsert)
val lastOfInsertedCols = toInsert.last().insertionPath
val siblingsOfTargetAndTarget = removeResult.df[parentPath].asColumnGroup().columns().map { parentPath + it.path }
val target = siblingsOfTargetAndTarget.filter { it.last() == targetPath.last() }
return logicOfAfter.move { target.toColumnSet() }.after { lastOfInsertedCols }
}

internal fun <T, C> MoveClause<T, C>.moveImpl(
Expand Down
68 changes: 68 additions & 0 deletions core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/move.kt
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,72 @@ class MoveTests {
grouped.move { "a"["b"] }.after { "a"["b"] }
}.message shouldBe "Cannot move column 'a/b' after its own child column 'a/b'"
}

@Test
fun `move before first`() {
val df = dataFrameOf("1", "2")(1, 2)
shouldNotThrowAny {
df.move("2").before("1") shouldBe dataFrameOf("2", "1")(2, 1)
}
}

@Test
fun `move before in nested structure`() {
val df = grouped.move { "a"["b"] }
.before { "a"["c"]["d"] }
df.columnNames() shouldBe listOf("q", "a", "b", "w", "e", "r")
df["a"].asColumnGroup().columnNames() shouldBe listOf("c")
df["a"]["c"].asColumnGroup().columnNames() shouldBe listOf("b", "d")
}

@Test
fun `move before multiple columns`() {
val df = grouped.move { "a"["b"] and "b"["c"] }
.before { "a"["c"]["d"] }
df.columnNames() shouldBe listOf("q", "a", "b", "w", "e", "r")
df["a"].asColumnGroup().columnNames() shouldBe listOf("c")
df["a"]["c"].asColumnGroup().columnNames() shouldBe listOf("b", "c", "d")
df["b"].asColumnGroup().columnNames() shouldBe listOf("d")
}

@Test
fun `move before with column selector`() {
val df = grouped.move { colsAtAnyDepth().filter { it.name == "r" || it.name == "w" } }
.before { "a"["c"]["d"] }
df.columnNames() shouldBe listOf("q", "a", "b", "e")
df["a"]["c"].asColumnGroup().columnNames() shouldBe listOf("w", "r", "d")
}

@Test
fun `move before between groups`() {
val df = grouped.move { "a"["b"] }.before { "b"["d"] }
df.columnNames() shouldBe listOf("q", "a", "b", "w", "e", "r")
df["a"].asColumnGroup().columnNames() shouldBe listOf("c")
df["b"].asColumnGroup().columnNames() shouldBe listOf("c", "b", "d")
}

@Test
fun `should throw when moving parent before child`() {
// Simple case: direct parent-child relationship
shouldThrow<IllegalArgumentException> {
grouped.move("a").before { "a"["b"] }
}.message shouldBe "Cannot move column 'a' before its own child column 'a/b'"

// Nested case: deeper parent-child relationship
shouldThrow<IllegalArgumentException> {
grouped.move("a").before { "a"["c"]["d"] }
}.message shouldBe "Cannot move column 'a' before its own child column 'a/c/d'"

// Group case: moving group after its nested column
shouldThrow<IllegalArgumentException> {
grouped.move { "a"["c"] }.before { "a"["c"]["d"] }
}.message shouldBe "Cannot move column 'a/c' before its own child column 'a/c/d'"
}

@Test
fun `should throw when moving column before itself`() {
shouldThrow<IllegalArgumentException> {
grouped.move { "a"["b"] }.before { "a"["b"] }
}.message shouldBe "Cannot move column 'a/b' before its own child column 'a/b'"
}
}
Loading