Skip to content
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 @@ -4,6 +4,7 @@ import com.inductiveautomation.ignition.common.Dataset
import com.inductiveautomation.ignition.common.TypeUtilities
import com.inductiveautomation.ignition.common.script.PyArgParser
import com.inductiveautomation.ignition.common.script.builtin.KeywordArgs
import com.inductiveautomation.ignition.common.script.hints.ScriptArg
import com.inductiveautomation.ignition.common.script.hints.ScriptFunction
import com.inductiveautomation.ignition.common.util.DatasetBuilder
import org.apache.poi.ss.usermodel.CellType.BOOLEAN
Expand Down Expand Up @@ -342,4 +343,66 @@ object DatasetExtensions {
return dataset.build()
}
}

@Suppress("unused")
@ScriptFunction(docBundlePrefix = "DatasetExtensions")
fun equals(
@ScriptArg("dataset1") ds1: Dataset,
@ScriptArg("dataset2") ds2: Dataset,
): Boolean {
return ds1 === ds2 || (columnsEqual(ds1, ds2) && valuesEqual(ds1, ds2))
}

@Suppress("unused")
@ScriptFunction(docBundlePrefix = "DatasetExtensions")
fun valuesEqual(
@ScriptArg("dataset1") ds1: Dataset,
@ScriptArg("dataset2") ds2: Dataset,
): Boolean {
if (ds1 === ds2) {
return true
}
if (ds1.rowCount != ds2.rowCount || ds1.columnCount != ds2.columnCount) {
return false
}
return ds1.rowIndices.all { row ->
ds1.columnIndices.all { col ->
ds1[row, col] == ds2[row, col]
}
}
}

@Suppress("unused")
@ScriptFunction(docBundlePrefix = "DatasetExtensions")
@JvmOverloads
fun columnsEqual(
@ScriptArg("dataset1") ds1: Dataset,
@ScriptArg("dataset2") ds2: Dataset,
@ScriptArg("ignoreCase") ignoreCase: Boolean = false,
@ScriptArg("includeTypes") includeTypes: Boolean = true,
): Boolean {
if (ds1 === ds2) {
return true
}
if (ds1.columnCount != ds2.columnCount) {
return false
}

val columnNames = ds1.columnNames zip ds2.columnNames
val columnNamesMatch = columnNames.all { (left, right) ->
left.equals(right, ignoreCase)
}
if (!columnNamesMatch) {
return false
}

if (!includeTypes) {
return true
}

val columnTypes = ds1.columnTypes.asSequence() zip ds2.columnTypes.asSequence()
return columnTypes.all { (left, right) ->
left == right
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,20 @@ fromExcel.param.lastRow=The last row (zero-indexed) in the Excel document to ret
fromExcel.param.firstColumn=The first column (zero-indexed) in the Excel document to retrieve data from. If not supplied, the first non-empty column will be used.
fromExcel.param.lastColumn=The last column (zero-indexed) in the Excel document to retrieve data from. If not supplied, the last non-empty column will be used.
fromExcel.returns=A Dataset created from the Excel document. Types are assumed based on the first row of input data.

equals.desc=Compares two datasets for structural equality.
equals.param.dataset1=The first dataset. Must not be null.
equals.param.dataset2=The second dataset. Must not be null.
equals.returns=True if the two datasets have the same number of columns, with the same types, in the same order, with the same data in each row.

valuesEqual.desc=Compares two datasets' content.
valuesEqual.param.dataset1=The first dataset. Must not be null.
valuesEqual.param.dataset2=The second dataset. Must not be null.
valuesEqual.returns=True if the two datasets have the same values.

columnsEqual.desc=Compares two datasets' column definitions.
columnsEqual.param.dataset1=The first dataset. Must not be null.
columnsEqual.param.dataset2=The second dataset. Must not be null.
columnsEqual.param.ignoreCase=Pass True if the column names should be compared case-insensitive. Defaults to False.
columnsEqual.param.includeTypes=Pass True if the column types must match as well. Defaults to True.
columnsEqual.returns=True if the two datasets have the same columns, per additional parameters.
38 changes: 38 additions & 0 deletions common/src/test/kotlin/org/imdc/extensions/common/DSBuilder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.imdc.extensions.common

import com.inductiveautomation.ignition.common.BasicDataset
import com.inductiveautomation.ignition.common.Dataset

class DSBuilder {
data class Column(val name: String, val rows: List<*>, val type: Class<*>)

val columns = mutableListOf<Column>()

inline fun <reified T> column(name: String, data: List<T>) {
columns.add(Column(name, data, T::class.java))
}

inline fun <reified T> column(name: String, builder: MutableList<T>.() -> Unit) {
column(name, buildList(builder))
}

fun build(): Dataset {
val colCount = columns.size
val rowCount = columns.maxOf { it.rows.size }
val data = Array(colCount) { arrayOfNulls<Any>(rowCount) }

for (c in 0 until colCount) {
for (r in 0 until rowCount) {
data[c][r] = columns.getOrNull(c)?.rows?.getOrNull(r)
}
}

return BasicDataset(columns.map { it.name }, columns.map { it.type }, data)
}

companion object {
fun dataset(block: DSBuilder.() -> Unit): Dataset {
return DSBuilder().apply(block).build()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package org.imdc.extensions.common

import com.inductiveautomation.ignition.common.BasicDataset
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import org.imdc.extensions.common.DSBuilder.Companion.dataset
import java.util.Date

class DatasetEqualityTests : FunSpec(
{
val dataset1 = dataset {
column("a", listOf(1, 2, 3))
column("b", listOf(3.14, 2.18, 4.96))
column("c", listOf("string", "strung", "strang"))
}
val copyOfDs1 = BasicDataset(dataset1)
val ds1WithCaps = BasicDataset(listOf("A", "B", "C"), dataset1.columnTypes, dataset1)
val ds1WithOtherNames = BasicDataset(listOf("ant", "bat", "cat"), dataset1.columnTypes, dataset1)
val dataset2 = dataset {
column("j", listOf(3, 2, 1))
column("k", listOf(1.0, 2.0, 3.0))
column("l", listOf("chess", "chass", "chuss"))
}
val copyOfDs2 = BasicDataset(dataset2)
val ds2WithCaps = BasicDataset(listOf("J", "K", "L"), dataset2.columnTypes, dataset2)
val ds2WithOtherNames = BasicDataset(listOf("joe", "kim", "lee"), dataset2.columnTypes, dataset2)

val dataset3 = dataset {
column("t_stamp", listOf(1667605391000, 1667605392000, 1667605393000, 1667605394000).map(::Date))
column("tag_1", listOf(1.0, 2.0, 3.0, 4.0))
column("tag_2", listOf(true, false, true, false))
}
val copyOfDs3 = BasicDataset(dataset3)
val ds3WithAliases = BasicDataset(listOf("timestamp", "doubleTag", "boolTag"), dataset3.columnTypes, dataset3)

context("General equality") {
test("Same dataset") {
DatasetExtensions.equals(dataset1, dataset1) shouldBe true
DatasetExtensions.equals(dataset2, dataset2) shouldBe true
DatasetExtensions.equals(dataset3, dataset3) shouldBe true
}

test("Copied dataset") {
DatasetExtensions.equals(dataset1, copyOfDs1) shouldBe true
DatasetExtensions.equals(dataset2, copyOfDs2) shouldBe true
DatasetExtensions.equals(dataset3, copyOfDs3) shouldBe true
}

test("Different datasets") {
DatasetExtensions.equals(dataset1, dataset2) shouldBe false
DatasetExtensions.equals(dataset1, dataset3) shouldBe false
DatasetExtensions.equals(dataset2, dataset3) shouldBe false
}

test("Datasets with same structure, but different columns") {
DatasetExtensions.equals(dataset1, ds1WithCaps) shouldBe false
DatasetExtensions.equals(dataset1, ds1WithOtherNames) shouldBe false
DatasetExtensions.equals(dataset2, ds2WithCaps) shouldBe false
DatasetExtensions.equals(dataset2, ds2WithOtherNames) shouldBe false
DatasetExtensions.equals(dataset3, ds3WithAliases) shouldBe false
}
}

context("Value equality") {
test("Same dataset") {
DatasetExtensions.valuesEqual(dataset1, dataset1) shouldBe true
DatasetExtensions.valuesEqual(dataset2, dataset2) shouldBe true
DatasetExtensions.valuesEqual(dataset3, dataset3) shouldBe true
}

test("Copied dataset") {
DatasetExtensions.valuesEqual(dataset1, copyOfDs1) shouldBe true
DatasetExtensions.valuesEqual(dataset2, copyOfDs2) shouldBe true
DatasetExtensions.valuesEqual(dataset3, copyOfDs3) shouldBe true
}

test("Different datasets") {
DatasetExtensions.valuesEqual(dataset1, dataset2) shouldBe false
DatasetExtensions.valuesEqual(dataset1, dataset3) shouldBe false
DatasetExtensions.valuesEqual(dataset2, dataset3) shouldBe false
}

test("Datasets with same structure, but different columns") {
DatasetExtensions.valuesEqual(dataset1, ds1WithCaps) shouldBe true
DatasetExtensions.valuesEqual(dataset1, ds1WithOtherNames) shouldBe true
DatasetExtensions.valuesEqual(dataset2, ds2WithCaps) shouldBe true
DatasetExtensions.valuesEqual(dataset2, ds2WithOtherNames) shouldBe true
DatasetExtensions.valuesEqual(dataset3, ds3WithAliases) shouldBe true
}
}

context("Column equality") {
test("Same dataset") {
DatasetExtensions.columnsEqual(dataset1, dataset1) shouldBe true
DatasetExtensions.columnsEqual(dataset2, dataset2) shouldBe true
DatasetExtensions.columnsEqual(dataset3, dataset3) shouldBe true
}

test("Copied dataset") {
DatasetExtensions.columnsEqual(dataset1, copyOfDs1) shouldBe true
DatasetExtensions.columnsEqual(dataset2, copyOfDs2) shouldBe true
DatasetExtensions.columnsEqual(dataset3, copyOfDs3) shouldBe true
}

test("Different datasets") {
DatasetExtensions.columnsEqual(dataset1, dataset2) shouldBe false
DatasetExtensions.columnsEqual(dataset1, dataset3) shouldBe false
DatasetExtensions.columnsEqual(dataset2, dataset3) shouldBe false
}

test("Datasets with same structure, but different columns") {
DatasetExtensions.columnsEqual(dataset1, ds1WithCaps) shouldBe false
DatasetExtensions.columnsEqual(dataset1, ds1WithCaps, ignoreCase = true) shouldBe true
DatasetExtensions.columnsEqual(dataset1, ds1WithOtherNames) shouldBe false
DatasetExtensions.columnsEqual(dataset2, ds2WithCaps) shouldBe false
DatasetExtensions.columnsEqual(dataset2, ds2WithCaps, ignoreCase = true) shouldBe true
DatasetExtensions.columnsEqual(dataset2, ds2WithOtherNames) shouldBe false
DatasetExtensions.columnsEqual(dataset3, ds3WithAliases) shouldBe false

val ds1WithDifferentTypes = BasicDataset(
dataset1.columnNames,
listOf(String::class.java, Int::class.java, Boolean::class.java),
dataset1,
)
DatasetExtensions.columnsEqual(dataset1, ds1WithDifferentTypes) shouldBe false
DatasetExtensions.columnsEqual(dataset1, ds1WithDifferentTypes, ignoreCase = true) shouldBe false
DatasetExtensions.columnsEqual(dataset1, ds1WithDifferentTypes, includeTypes = false) shouldBe true
DatasetExtensions.columnsEqual(
dataset1,
ds1WithDifferentTypes,
includeTypes = false,
ignoreCase = true,
) shouldBe true

val ds1WithDifferentTypesAndCase = BasicDataset(
listOf("A", "B", "C"),
dataset1.columnTypes,
dataset1,
)
DatasetExtensions.columnsEqual(dataset1, ds1WithDifferentTypesAndCase) shouldBe false
DatasetExtensions.columnsEqual(dataset1, ds1WithDifferentTypesAndCase, ignoreCase = true) shouldBe true
DatasetExtensions.columnsEqual(
dataset1,
ds1WithDifferentTypesAndCase,
includeTypes = false,
) shouldBe false
DatasetExtensions.columnsEqual(
dataset1,
ds1WithDifferentTypesAndCase,
ignoreCase = true,
includeTypes = false,
) shouldBe true
}
}
},
)