From 5a592bca984ed186db520ae80049de80c5ae9c3e Mon Sep 17 00:00:00 2001 From: Philipp Ossler Date: Fri, 23 Aug 2024 13:43:54 +0200 Subject: [PATCH 01/11] refactor: Migrate to new test style --- .../builtin/BuiltinListFunctionsTest.scala | 255 +++++++++--------- 1 file changed, 128 insertions(+), 127 deletions(-) diff --git a/src/test/scala/org/camunda/feel/impl/builtin/BuiltinListFunctionsTest.scala b/src/test/scala/org/camunda/feel/impl/builtin/BuiltinListFunctionsTest.scala index a75c1d345..f8bce31b3 100644 --- a/src/test/scala/org/camunda/feel/impl/builtin/BuiltinListFunctionsTest.scala +++ b/src/test/scala/org/camunda/feel/impl/builtin/BuiltinListFunctionsTest.scala @@ -19,395 +19,396 @@ package org.camunda.feel.impl.builtin import org.scalatest.matchers.should.Matchers import org.scalatest.flatspec.AnyFlatSpec import org.camunda.feel._ -import org.camunda.feel.impl.FeelIntegrationTest +import org.camunda.feel.impl.{EvaluationResultMatchers, FeelEngineTest, FeelIntegrationTest} import org.camunda.feel.syntaxtree._ +import java.time.LocalDate import scala.math.BigDecimal.int2bigDecimal /** @author * Philipp */ -class BuiltinListFunctionsTest extends AnyFlatSpec with Matchers with FeelIntegrationTest { +class BuiltinListFunctionsTest + extends AnyFlatSpec + with Matchers + with FeelEngineTest + with EvaluationResultMatchers { "A list contains() function" should "return if the list contains Number" in { - eval(" list contains([1,2,3], 2) ") should be(ValBoolean(true)) + evaluateExpression(" list contains([1,2,3], 2) ") should returnResult(true) - eval(" list contains([1,2,3], 4) ") should be(ValBoolean(false)) + evaluateExpression(" list contains([1,2,3], 4) ") should returnResult(false) } it should "return if the list contains String" in { - eval(""" list contains(["a","b"], "a") """) should be(ValBoolean(true)) + evaluateExpression(""" list contains(["a","b"], "a") """) should returnResult(true) - eval(""" list contains(["a","b"], "c") """) should be(ValBoolean(false)) + evaluateExpression(""" list contains(["a","b"], "c") """) should returnResult(false) } "A count() function" should "return the size of a list" in { - eval(" count([1,2,3]) ") should be(ValNumber(3)) + evaluateExpression(" count([1,2,3]) ") should returnResult(3) } "A min() function" should "return the null if empty list" in { - eval(" min([]) ") should be(ValNull) + evaluateExpression(" min([]) ") should returnNull() } it should "return the minimum item of numbers" in { - eval(" min([1,2,3]) ") should be(ValNumber(1)) - eval(" min(1,2,3) ") should be(ValNumber(1)) + evaluateExpression(" min([1,2,3]) ") should returnResult(1) + evaluateExpression(" min(1,2,3) ") should returnResult(1) } it should "return the minimum item of date" in { - eval(""" min([date("2017-01-01"), date("2018-01-01"), date("2019-01-01")]) """) should be( - ValDate("2017-01-01") - ) + evaluateExpression( + """ min([date("2017-01-01"), date("2018-01-01"), date("2019-01-01")]) """ + ) should returnResult(LocalDate.parse("2017-01-01")) } it should "return null if value is not comparable" in { - eval(""" min([true, false]) """) should be(ValNull) + evaluateExpression(""" min([true, false]) """) should returnNull() } "A max() function" should "return the null if empty list" in { - eval(" max([]) ") should be(ValNull) + evaluateExpression(" max([]) ") should returnNull() } it should "return the maximum item of numbers" in { - eval(" max([1,2,3]) ") should be(ValNumber(3)) - eval(" max(1,2,3) ") should be(ValNumber(3)) + evaluateExpression(" max([1,2,3]) ") should returnResult(3) + evaluateExpression(" max(1,2,3) ") should returnResult(3) } it should "return the maximum item of date" in { - eval(""" max([date("2017-01-01"), date("2018-01-01"), date("2019-01-01")]) """) should be( - ValDate("2019-01-01") + evaluateExpression( + """ max([date("2017-01-01"), date("2018-01-01"), date("2019-01-01")]) """ + ) should returnResult( + LocalDate.parse("2019-01-01") ) } it should "return null if value is not comparable" in { - eval(""" max([true, false]) """) should be(ValNull) + evaluateExpression(""" max([true, false]) """) should returnNull() } "A sum() function" should "return null if empty list" in { - eval(" sum([]) ") should be(ValNull) + evaluateExpression(" sum([]) ") should returnNull() } it should "return sum of numbers" in { - eval(" sum([1,2,3]) ") should be(ValNumber(6)) - eval(" sum(1,2,3) ") should be(ValNumber(6)) + evaluateExpression(" sum([1,2,3]) ") should returnResult(6) + evaluateExpression(" sum(1,2,3) ") should returnResult(6) } "A mean() function" should "return null if empty list" in { - eval(" mean([]) ") should be(ValNull) + evaluateExpression(" mean([]) ") should returnNull() } it should "return mean of numbers" in { - eval(" mean([1,2,3]) ") should be(ValNumber(2)) - eval(" mean(1,2,3) ") should be(ValNumber(2)) + evaluateExpression(" mean([1,2,3]) ") should returnResult(2) + evaluateExpression(" mean(1,2,3) ") should returnResult(2) } "A median() function" should "return null if empty list" in { - eval(" median([]) ") should be(ValNull) + evaluateExpression(" median([]) ") should returnNull() } it should "return the median of numbers" in { - eval(" median(8, 2, 5, 3, 4) ") should be(ValNumber(4)) - eval(" median([6, 1, 2, 3]) ") should be(ValNumber(2.5)) + evaluateExpression(" median(8, 2, 5, 3, 4) ") should returnResult(4) + evaluateExpression(" median([6, 1, 2, 3]) ") should returnResult(2.5) } "A stddev() function" should "return null if empty list" in { - eval(" stddev([]) ") should be(ValNull) + evaluateExpression(" stddev([]) ") should returnNull() } it should "return the standard deviation" in { - eval(" stddev(2, 4, 7, 5) ") should be(ValNumber(2.0816659994661326)) - eval(" stddev([2, 4, 7, 5]) ") should be(ValNumber(2.0816659994661326)) + evaluateExpression(" stddev(2, 4, 7, 5) ") should returnResult(2.0816659994661326) + evaluateExpression(" stddev([2, 4, 7, 5]) ") should returnResult(2.0816659994661326) } "A mode() function" should "return empty list if empty list" in { - eval(" mode([]) ") should be(ValList(List.empty)) + evaluateExpression(" mode([]) ") should returnResult(List.empty) } it should "return the mode of the list" in { - eval(" mode(6, 3, 9, 6, 6) ") should be(ValList(List(ValNumber(6)))) - eval(" mode([6, 1, 9, 6, 1]) ") should be(ValList(List(ValNumber(1), ValNumber(6)))) + evaluateExpression(" mode(6, 3, 9, 6, 6) ") should returnResult(List(6)) + evaluateExpression(" mode([6, 1, 9, 6, 1]) ") should returnResult(List(1, 6)) } "A and() / all() function" should "return true if empty list" in { - eval(" and([]) ") should be(ValBoolean(true)) - eval(" all([]) ") should be(ValBoolean(true)) + evaluateExpression(" and([]) ") should returnResult(true) + evaluateExpression(" all([]) ") should returnResult(true) } it should "return true if all items are true" in { - eval(" and([false,null,true]) ") should be(ValBoolean(false)) - eval(" all([false,null,true]) ") should be(ValBoolean(false)) + evaluateExpression(" and([false,null,true]) ") should returnResult(false) + evaluateExpression(" all([false,null,true]) ") should returnResult(false) - eval(" and(false,null,true) ") should be(ValBoolean(false)) - eval(" all(false,null,true) ") should be(ValBoolean(false)) + evaluateExpression(" and(false,null,true) ") should returnResult(false) + evaluateExpression(" all(false,null,true) ") should returnResult(false) - eval(" and([true,true]) ") should be(ValBoolean(true)) - eval(" all([true,true]) ") should be(ValBoolean(true)) + evaluateExpression(" and([true,true]) ") should returnResult(true) + evaluateExpression(" all([true,true]) ") should returnResult(true) - eval(" and(true,true) ") should be(ValBoolean(true)) - eval(" all(true,true) ") should be(ValBoolean(true)) + evaluateExpression(" and(true,true) ") should returnResult(true) + evaluateExpression(" all(true,true) ") should returnResult(true) } it should "return null if argument is invalid" in { - eval("and(0)") should be(ValNull) - eval("all(0)") should be(ValNull) + evaluateExpression("and(0)") should returnNull() + evaluateExpression("all(0)") should returnNull() } it should "return null if one item is not a boolean value" in { - eval("and(true, null, true)") should be(ValNull) - eval("all(true, null, true)") should be(ValNull) + evaluateExpression("and(true, null, true)") should returnNull() + evaluateExpression("all(true, null, true)") should returnNull() } it should "return true if all items are true (huge list)" in { val hugeList = (1 to 10_000).map(_ => true).toList - eval("all(xs)", Map("xs" -> hugeList)) should be(ValBoolean(true)) + evaluateExpression("all(xs)", Map("xs" -> hugeList)) should returnResult(true) } it should "return null if items are not boolean values (huge list)" in { val hugeList = (1 to 10_000).toList - eval("all(xs)", Map("xs" -> hugeList)) should be(ValNull) + evaluateExpression("all(xs)", Map("xs" -> hugeList)) should returnNull() } "A or() / any() function" should "return false if empty list" in { - eval(" or([]) ") should be(ValBoolean(false)) - eval(" any([]) ") should be(ValBoolean(false)) + evaluateExpression(" or([]) ") should returnResult(false) + evaluateExpression(" any([]) ") should returnResult(false) } it should "return false if all items are false" in { - eval(" or([false,null,true]) ") should be(ValBoolean(true)) - eval(" any([false,null,true]) ") should be(ValBoolean(true)) + evaluateExpression(" or([false,null,true]) ") should returnResult(true) + evaluateExpression(" any([false,null,true]) ") should returnResult(true) - eval(" or(false,null,true) ") should be(ValBoolean(true)) - eval(" any(false,null,true) ") should be(ValBoolean(true)) + evaluateExpression(" or(false,null,true) ") should returnResult(true) + evaluateExpression(" any(false,null,true) ") should returnResult(true) - eval(" or([false,false]) ") should be(ValBoolean(false)) - eval(" any([false,false]) ") should be(ValBoolean(false)) + evaluateExpression(" or([false,false]) ") should returnResult(false) + evaluateExpression(" any([false,false]) ") should returnResult(false) - eval(" or(false,false) ") should be(ValBoolean(false)) - eval(" any(false,false) ") should be(ValBoolean(false)) + evaluateExpression(" or(false,false) ") should returnResult(false) + evaluateExpression(" any(false,false) ") should returnResult(false) } it should "return null if argument is invalid" in { - eval("or(0)") should be(ValNull) - eval("any(0)") should be(ValNull) + evaluateExpression("or(0)") should returnNull() + evaluateExpression("any(0)") should returnNull() } it should "return null if one item is not a boolean value" in { - eval("or(false, null, false)") should be(ValNull) - eval("any(false, null, false)") should be(ValNull) + evaluateExpression("or(false, null, false)") should returnNull() + evaluateExpression("any(false, null, false)") should returnNull() } it should "return false if all items are false (huge list)" in { val hugeList = (1 to 10_000).map(_ => false).toList - eval("any(xs)", Map("xs" -> hugeList)) should be(ValBoolean(false)) + evaluateExpression("any(xs)", Map("xs" -> hugeList)) should returnResult(false) } it should "return null if items are not boolean values (huge list)" in { val hugeList = (1 to 10_000).toList - eval("any(xs)", Map("xs" -> hugeList)) should be(ValNull) + evaluateExpression("any(xs)", Map("xs" -> hugeList)) should returnNull() } "A sublist() function" should "return list starting with _" in { - eval(" sublist([1,2,3], 2) ") should be(ValList(List(ValNumber(2), ValNumber(3)))) + evaluateExpression(" sublist([1,2,3], 2) ") should returnResult(List(2, 3)) } it should "return list starting with _ and length _" in { - eval(" sublist([1,2,3], 1, 2) ") should be(ValList(List(ValNumber(1), ValNumber(2)))) + evaluateExpression(" sublist([1,2,3], 1, 2) ") should returnResult(List(1, 2)) } "A append() function" should "return list with item appended" in { - eval(" append([1,2], 3) ") should be(ValList(List(ValNumber(1), ValNumber(2), ValNumber(3)))) - eval(" append([1], 2, 3) ") should be(ValList(List(ValNumber(1), ValNumber(2), ValNumber(3)))) + evaluateExpression(" append([1,2], 3) ") should returnResult(List(1, 2, 3)) + evaluateExpression(" append([1], 2, 3) ") should returnResult(List(1, 2, 3)) } "A concatenate() function" should "return list with item appended" in { - eval(" concatenate([1,2],[3]) ") should be( - ValList(List(ValNumber(1), ValNumber(2), ValNumber(3))) - ) - eval(" concatenate([1],[2],[3]) ") should be( - ValList(List(ValNumber(1), ValNumber(2), ValNumber(3))) - ) + evaluateExpression(" concatenate([1,2],[3]) ") should returnResult(List(1, 2, 3)) + evaluateExpression(" concatenate([1],[2],[3]) ") should returnResult(List(1, 2, 3)) } "A insert before() function" should "return list with new item at _" in { - eval(" insert before([1,3],2,2) ") should be( - ValList(List(ValNumber(1), ValNumber(2), ValNumber(3))) - ) + evaluateExpression(" insert before([1,3],2,2) ") should returnResult(List(1, 2, 3)) } "A remove() function" should "return list with item at _ removed" in { - eval(" remove([1,1,3],2) ") should be(ValList(List(ValNumber(1), ValNumber(3)))) + evaluateExpression(" remove([1,1,3],2) ") should returnResult(List(1, 3)) } "A reverse() function" should "reverse the list" in { - eval(" reverse([1,2,3]) ") should be(ValList(List(ValNumber(3), ValNumber(2), ValNumber(1)))) + evaluateExpression(" reverse([1,2,3]) ") should returnResult(List(3, 2, 1)) } "A index of() function" should "return empty list if no match" in { - eval(" index of([1,2,3,2], 4) ") should be(ValList(List())) + evaluateExpression(" index of([1,2,3,2], 4) ") should returnResult(List()) } it should "return list of positions containing the match" in { - eval(" index of([1,2,3,2], 1) ") should be(ValList(List(ValNumber(1)))) - eval(" index of([1,2,3,2], 2) ") should be(ValList(List(ValNumber(2), ValNumber(4)))) - eval(" index of([1,2,3,2], 3) ") should be(ValList(List(ValNumber(3)))) + evaluateExpression(" index of([1,2,3,2], 1) ") should returnResult(List(1)) + evaluateExpression(" index of([1,2,3,2], 2) ") should returnResult(List(2, 4)) + evaluateExpression(" index of([1,2,3,2], 3) ") should returnResult(List(3)) } "A union() function" should "concatenate with duplicate removal" in { - eval(" union([1,2],[2,3]) ") should be(ValList(List(ValNumber(1), ValNumber(2), ValNumber(3)))) - eval(" union([1,2],[2,3], [4]) ") should be( - ValList(List(ValNumber(1), ValNumber(2), ValNumber(3), ValNumber(4))) - ) + evaluateExpression(" union([1,2],[2,3]) ") should returnResult(List(1, 2, 3)) + evaluateExpression(" union([1,2],[2,3], [4]) ") should returnResult(List(1, 2, 3, 4)) } "A distinct values() function" should "remove duplicates" in { - eval(" distinct values([1,2,3,2,1]) ") should be( - ValList(List(ValNumber(1), ValNumber(2), ValNumber(3))) - ) + evaluateExpression(" distinct values([1,2,3,2,1]) ") should returnResult(List(1, 2, 3)) } "A duplicate values() function" should "return duplicate values" in { - eval(" duplicate values([1,2,3,2,1]) ") should be(ValList(List(ValNumber(1), ValNumber(2)))) + evaluateExpression(" duplicate values([1,2,3,2,1]) ") should returnResult(List(1, 2)) } it should "return null duplicate values" in { - eval(" duplicate values([1,2,1,null,null]) ") should be(ValList(List(ValNumber(1), ValNull))) + evaluateExpression(" duplicate values([1,2,1,null,null]) ") should returnResult( + List(1, null) + ) } it should "return duplicate values for named parameters" in { - eval(" duplicate values(list: [1,2,3,2,1]) ") should be( - ValList(List(ValNumber(1), ValNumber(2))) - ) + evaluateExpression(" duplicate values(list: [1,2,3,2,1]) ") should returnResult(List(1, 2)) } "A flatten() function" should "flatten nested lists" in { - eval(" flatten([[1,2],[[3]], 4]) ") should be( - ValList(List(ValNumber(1), ValNumber(2), ValNumber(3), ValNumber(4))) - ) + evaluateExpression(" flatten([[1,2],[[3]], 4]) ") should returnResult(List(1, 2, 3, 4)) } it should "flatten a huge list of lists" in { val hugeList = (1 to 10_000).map(List(_)).toList - eval("flatten(xs)", Map("xs" -> hugeList)) should be( - ValList( - hugeList.flatten.map(ValNumber(_)) - ) - ) + evaluateExpression("flatten(xs)", Map("xs" -> hugeList)) should returnResult(hugeList.flatten) } "A sort() function" should "sort list of numbers" in { - eval(" sort(list: [3,1,4,5,2], precedes: function(x,y) x < y) ") should be( - ValList(List(ValNumber(1), ValNumber(2), ValNumber(3), ValNumber(4), ValNumber(5))) - ) + evaluateExpression( + " sort(list: [3,1,4,5,2], precedes: function(x,y) x < y) " + ) should returnResult(List(1, 2, 3, 4, 5)) } "A product() function" should "return null if empty list" in { - eval(" product([]) ") should be(ValNull) + evaluateExpression(" product([]) ") should returnNull() } it should "return product of numbers" in { - eval(" product([2,3,4]) ") should be(ValNumber(24)) - eval(" product(2,3,4) ") should be(ValNumber(24)) + evaluateExpression(" product([2,3,4]) ") should returnResult(24) + evaluateExpression(" product(2,3,4) ") should returnResult(24) } "A join function" should "return an empty string if the input list is empty" in { - eval(" string join([]) ") should be(ValString("")) + evaluateExpression(" string join([]) ") should returnResult("") } it should "return an empty string if the input list is empty and a delimiter is defined" in { - eval(""" string join([], "X") """) should be(ValString("")) + evaluateExpression(""" string join([], "X") """) should returnResult("") } it should "return joined strings" in { - eval(""" string join(["foo","bar","baz"]) """) should be(ValString("foobarbaz")) + evaluateExpression(""" string join(["foo","bar","baz"]) """) should returnResult("foobarbaz") } it should "return joined strings when delimiter is null" in { - eval(""" string join(["foo","bar","baz"], null) """) should be(ValString("foobarbaz")) + evaluateExpression(""" string join(["foo","bar","baz"], null) """) should returnResult( + "foobarbaz" + ) } it should "return original string when list contains a single entry" in { - eval(""" string join(["a"], "X") """) should be(ValString("a")) + evaluateExpression(""" string join(["a"], "X") """) should returnResult("a") } it should "ignore null strings" in { - eval(""" string join(["foo", null, "baz"], null) """) should be(ValString("foobaz")) + evaluateExpression(""" string join(["foo", null, "baz"], null) """) should returnResult( + "foobaz" + ) } it should "ignore null strings with delimiter" in { - eval(""" string join(["foo", null, "baz"], "X") """) should be(ValString("fooXbaz")) + evaluateExpression(""" string join(["foo", null, "baz"], "X") """) should returnResult( + "fooXbaz" + ) } it should "return joined strings with custom separator" in { - eval(""" string join(["foo","bar","baz"], "::") """) should be(ValString("foo::bar::baz")) + evaluateExpression(""" string join(["foo","bar","baz"], "::") """) should returnResult( + "foo::bar::baz" + ) } it should "return joined strings with custom separator, a prefix and a suffix" in { - eval(""" string join(["foo","bar","baz"], "::", "hello-", "-goodbye") """) should be( - ValString("hello-foo::bar::baz-goodbye") + evaluateExpression( + """ string join(["foo","bar","baz"], "::", "hello-", "-goodbye") """ + ) should returnResult( + "hello-foo::bar::baz-goodbye" ) } it should "return null if the list contains other values than strings" in { - eval(""" string join(["foo", 123, "bar"]) """) should be(ValNull) + evaluateExpression(""" string join(["foo", 123, "bar"]) """) should returnNull() } "A list is empty() function" should "return if the list is empty" in { - eval(" is empty([]) ") should be(ValBoolean(true)) - eval(" is empty([1]) ") should be(ValBoolean(false)) - eval(" is empty([1,2,3]) ") should be(ValBoolean(false)) - eval(" is empty(list: [1]) ") should be(ValBoolean(false)) + evaluateExpression(" is empty([]) ") should returnResult(true) + evaluateExpression(" is empty([1]) ") should returnResult(false) + evaluateExpression(" is empty([1,2,3]) ") should returnResult(false) + evaluateExpression(" is empty(list: [1]) ") should returnResult(false) } } From 9c3ed24e955ea13baec89b0a2448962326bce0cd Mon Sep 17 00:00:00 2001 From: Philipp Ossler Date: Fri, 23 Aug 2024 13:46:33 +0200 Subject: [PATCH 02/11] test: Verify distinct values() removes duplicates Add a test to verify that the function removes duplicated context values from a list. --- .../builtin/BuiltinListFunctionsTest.scala | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/test/scala/org/camunda/feel/impl/builtin/BuiltinListFunctionsTest.scala b/src/test/scala/org/camunda/feel/impl/builtin/BuiltinListFunctionsTest.scala index f8bce31b3..1c90be221 100644 --- a/src/test/scala/org/camunda/feel/impl/builtin/BuiltinListFunctionsTest.scala +++ b/src/test/scala/org/camunda/feel/impl/builtin/BuiltinListFunctionsTest.scala @@ -306,6 +306,62 @@ class BuiltinListFunctionsTest evaluateExpression(" distinct values([1,2,3,2,1]) ") should returnResult(List(1, 2, 3)) } + it should "invoked with named parameter" in { + + evaluateExpression(" distinct values(list: [1,2,3,2,1]) ") should returnResult(List(1, 2, 3)) + } + + it should "remove duplicated context values" in { + + evaluateExpression("distinct values([{a:1},{a:2},{a:1},{b:3}])") should returnResult( + List(Map("a" -> 1), Map("a" -> 2), Map("b" -> 3)) + ) + + evaluateExpression("distinct values([{a:1},{a:null},{a:null}])") should returnResult( + List(Map("a" -> 1), Map("a" -> null)) + ) + + evaluateExpression("distinct values([{a:1},{},{}])") should returnResult( + List(Map("a" -> 1), Map()) + ) + + evaluateExpression( + "distinct values([{a:1,b:{c:2}}, {a:1,b:{c:3}}, {a:1,b:{c:2}}, {a:1,b:{c:3},d:4}])" + ) should returnResult( + List( + Map("a" -> 1, "b" -> Map("c" -> 2)), + Map("a" -> 1, "b" -> Map("c" -> 3)), + Map("a" -> 1, "b" -> Map("c" -> 3), "d" -> 4) + ) + ) + } + + it should "remove duplicated list values" in { + evaluateExpression(" distinct values([[1],[2],[3],[2]]) ") should returnResult( + List(List(1), List(2), List(3)) + ) + + evaluateExpression(" distinct values([[1],[null],[1],[null]]) ") should returnResult( + List(List(1), List(null)) + ) + + evaluateExpression(" distinct values([[1],[],[]]) ") should returnResult( + List(List(1), List.empty) + ) + + evaluateExpression(" distinct values([[1,2],[4,5],[1,2],[4]]) ") should returnResult( + List(List(1, 2), List(4, 5), List(4)) + ) + } + + it should "remove duplicated null values" in { + evaluateExpression(" distinct values([1,null,2,null]) ") should returnResult(List(1, null, 2)) + } + + it should "preserve the order" in { + evaluateExpression(" distinct values([1,2,3,4,2,3,1]) ") should returnResult(List(1, 2, 3, 4)) + } + "A duplicate values() function" should "return duplicate values" in { evaluateExpression(" duplicate values([1,2,3,2,1]) ") should returnResult(List(1, 2)) From f5c5017cd86a29332beae6648bb193c260b09ada Mon Sep 17 00:00:00 2001 From: Philipp Ossler Date: Fri, 23 Aug 2024 14:48:44 +0200 Subject: [PATCH 03/11] refactor: Extract equals comparison Move the equals comparison in a separate class to be reusable. --- .../impl/interpreter/FeelInterpreter.scala | 53 +----------- .../feel/impl/interpreter/ValComparator.scala | 86 +++++++++++++++++++ 2 files changed, 89 insertions(+), 50 deletions(-) create mode 100644 src/main/scala/org/camunda/feel/impl/interpreter/ValComparator.scala diff --git a/src/main/scala/org/camunda/feel/impl/interpreter/FeelInterpreter.scala b/src/main/scala/org/camunda/feel/impl/interpreter/FeelInterpreter.scala index b1fa21078..4fb06e5ae 100644 --- a/src/main/scala/org/camunda/feel/impl/interpreter/FeelInterpreter.scala +++ b/src/main/scala/org/camunda/feel/impl/interpreter/FeelInterpreter.scala @@ -519,58 +519,11 @@ class FeelInterpreter { withValues( x, y, - { - case (ValNull, _) => f(c(ValNull, y.toOption.getOrElse(ValNull))) - case (_, ValNull) => f(c(x.toOption.getOrElse(ValNull), ValNull)) - case (ValNumber(x), ValNumber(y)) => f(c(x, y)) - case (ValBoolean(x), ValBoolean(y)) => f(c(x, y)) - case (ValString(x), ValString(y)) => f(c(x, y)) - case (ValDate(x), ValDate(y)) => f(c(x, y)) - case (ValLocalTime(x), ValLocalTime(y)) => f(c(x, y)) - case (ValTime(x), ValTime(y)) => f(c(x, y)) - case (ValLocalDateTime(x), ValLocalDateTime(y)) => f(c(x, y)) - case (ValDateTime(x), ValDateTime(y)) => f(c(x, y)) - case (ValYearMonthDuration(x), ValYearMonthDuration(y)) => f(c(x, y)) - case (ValDayTimeDuration(x), ValDayTimeDuration(y)) => f(c(x, y)) - case (ValList(x), ValList(y)) => - if (x.size != y.size) { - f(false) - - } else { - val isEqual = x.zip(y).foldRight(true) { case ((x, y), listIsEqual) => - listIsEqual && { - checkEquality(x, y, c, f) match { - case ValBoolean(itemIsEqual) => itemIsEqual - case _ => false - } - } - } - f(isEqual) - } - case (ValContext(x), ValContext(y)) => - val xVars = x.variableProvider.getVariables - val yVars = y.variableProvider.getVariables - - if (xVars.keys != yVars.keys) { - f(false) - - } else { - val isEqual = xVars.keys.foldRight(true) { case (key, contextIsEqual) => - contextIsEqual && { - val xVal = context.valueMapper.toVal(xVars(key)) - val yVal = context.valueMapper.toVal(yVars(key)) - - checkEquality(xVal, yVal, c, f) match { - case ValBoolean(entryIsEqual) => entryIsEqual - case _ => false - } - } - } - f(isEqual) - } - case _ => + { (x, y) => + new ValComparator(context.valueMapper).compare(x, y).toOption.getOrElse { error(EvaluationFailureType.NOT_COMPARABLE, s"Can't compare '$x' with '$y'") ValNull + } } ) diff --git a/src/main/scala/org/camunda/feel/impl/interpreter/ValComparator.scala b/src/main/scala/org/camunda/feel/impl/interpreter/ValComparator.scala new file mode 100644 index 000000000..b190ba6e7 --- /dev/null +++ b/src/main/scala/org/camunda/feel/impl/interpreter/ValComparator.scala @@ -0,0 +1,86 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. Camunda licenses this file to you under the Apache License, + * Version 2.0; you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.camunda.feel.impl.interpreter + +import org.camunda.feel.context.Context +import org.camunda.feel.syntaxtree._ +import org.camunda.feel.valuemapper.ValueMapper + +class ValComparator(private val valueMapper: ValueMapper) { + + def compare(x: Val, y: Val): Val = (x, y) match { + // both values are null + case (ValNull, _) => ValBoolean(ValNull == y.toOption.getOrElse(ValNull)) + case (_, ValNull) => ValBoolean(x.toOption.getOrElse(ValNull) == ValNull) + // compare values of the same type + case (ValNumber(x), ValNumber(y)) => ValBoolean(x == y) + case (ValBoolean(x), ValBoolean(y)) => ValBoolean(x == y) + case (ValString(x), ValString(y)) => ValBoolean(x == y) + case (ValDate(x), ValDate(y)) => ValBoolean(x == y) + case (ValLocalTime(x), ValLocalTime(y)) => ValBoolean(x == y) + case (ValTime(x), ValTime(y)) => ValBoolean(x == y) + case (ValLocalDateTime(x), ValLocalDateTime(y)) => ValBoolean(x == y) + case (ValDateTime(x), ValDateTime(y)) => ValBoolean(x == y) + case (ValYearMonthDuration(x), ValYearMonthDuration(y)) => ValBoolean(x == y) + case (ValDayTimeDuration(x), ValDayTimeDuration(y)) => ValBoolean(x == y) + case (ValList(x), ValList(y)) => compare(x, y) + case (ValContext(x), ValContext(y)) => compare(x, y) + // values have a different type + case _ => ValError(s"Can't compare '$x' with '$y'") + } + + private def compare(x: List[Val], y: List[Val]): ValBoolean = { + if (x.size != y.size) { + ValBoolean(false) + + } else { + val itemsAreEqual = x.zip(y).foldRight(true) { case ((x, y), listIsEqual) => + listIsEqual && { + compare(x, y) match { + case ValBoolean(itemIsEqual) => itemIsEqual + case _ => false + } + } + } + ValBoolean(itemsAreEqual) + } + } + + private def compare(x: Context, y: Context): ValBoolean = { + val xVars = x.variableProvider.getVariables + val yVars = y.variableProvider.getVariables + + if (xVars.keys != yVars.keys) { + ValBoolean(false) + + } else { + val itemsAreEqual = xVars.keys.foldRight(true) { case (key, contextIsEqual) => + contextIsEqual && { + val xVal = valueMapper.toVal(xVars(key)) + val yVal = valueMapper.toVal(yVars(key)) + + compare(xVal, yVal) match { + case ValBoolean(entryIsEqual) => entryIsEqual + case _ => false + } + } + } + ValBoolean(itemsAreEqual) + } + } + +} From 88cf0248d6791c5b131aca1e003bddc6827ea07c Mon Sep 17 00:00:00 2001 From: Philipp Ossler Date: Tue, 27 Aug 2024 09:31:48 +0200 Subject: [PATCH 04/11] refactor: Instantiate comparator once --- src/main/scala/org/camunda/feel/FeelEngine.scala | 2 +- .../org/camunda/feel/impl/interpreter/FeelInterpreter.scala | 6 ++++-- .../scala/org/camunda/feel/impl/FeelIntegrationTest.scala | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/scala/org/camunda/feel/FeelEngine.scala b/src/main/scala/org/camunda/feel/FeelEngine.scala index 46efab5e1..e1b38e7ba 100644 --- a/src/main/scala/org/camunda/feel/FeelEngine.scala +++ b/src/main/scala/org/camunda/feel/FeelEngine.scala @@ -119,7 +119,7 @@ class FeelEngine( val clock: FeelEngineClock = FeelEngine.defaultClock ) { - private val interpreter = new FeelInterpreter() + private val interpreter = new FeelInterpreter(valueMapper) private val validator = new ExpressionValidator( externalFunctionsEnabled = configuration.externalFunctionsEnabled diff --git a/src/main/scala/org/camunda/feel/impl/interpreter/FeelInterpreter.scala b/src/main/scala/org/camunda/feel/impl/interpreter/FeelInterpreter.scala index 4fb06e5ae..1e8f7ba70 100644 --- a/src/main/scala/org/camunda/feel/impl/interpreter/FeelInterpreter.scala +++ b/src/main/scala/org/camunda/feel/impl/interpreter/FeelInterpreter.scala @@ -39,7 +39,9 @@ import scala.reflect.ClassTag /** @author * Philipp Ossler */ -class FeelInterpreter { +class FeelInterpreter(private val valueMapper: ValueMapper) { + + private val valueComparator = new ValComparator(valueMapper) def eval(expression: Exp)(implicit context: EvalContext): Val = { // Check if the current thread was interrupted, otherwise long-running evaluations can not be interrupted and fully block the thread @@ -520,7 +522,7 @@ class FeelInterpreter { x, y, { (x, y) => - new ValComparator(context.valueMapper).compare(x, y).toOption.getOrElse { + valueComparator.compare(x, y).toOption.getOrElse { error(EvaluationFailureType.NOT_COMPARABLE, s"Can't compare '$x' with '$y'") ValNull } diff --git a/src/test/scala/org/camunda/feel/impl/FeelIntegrationTest.scala b/src/test/scala/org/camunda/feel/impl/FeelIntegrationTest.scala index 709116f5b..a59394d2b 100644 --- a/src/test/scala/org/camunda/feel/impl/FeelIntegrationTest.scala +++ b/src/test/scala/org/camunda/feel/impl/FeelIntegrationTest.scala @@ -37,7 +37,7 @@ import org.camunda.feel.{ trait FeelIntegrationTest { val interpreter: FeelInterpreter = - new FeelInterpreter + new FeelInterpreter(ValueMapper.defaultValueMapper) private val clock: TimeTravelClock = new TimeTravelClock From 9797b74f10a1a644b7d83dc0bdd19ff225c95dc1 Mon Sep 17 00:00:00 2001 From: Philipp Ossler Date: Tue, 27 Aug 2024 09:34:30 +0200 Subject: [PATCH 05/11] refactor: Remove unused parameter --- .../feel/impl/interpreter/FeelInterpreter.scala | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/main/scala/org/camunda/feel/impl/interpreter/FeelInterpreter.scala b/src/main/scala/org/camunda/feel/impl/interpreter/FeelInterpreter.scala index 1e8f7ba70..60072b841 100644 --- a/src/main/scala/org/camunda/feel/impl/interpreter/FeelInterpreter.scala +++ b/src/main/scala/org/camunda/feel/impl/interpreter/FeelInterpreter.scala @@ -82,7 +82,7 @@ class FeelInterpreter(private val valueMapper: ValueMapper) { // simple unary tests case InputEqualTo(x) => - withVal(getImplicitInputValue, i => checkEquality(i, eval(x), _ == _, ValBoolean)) + withVal(getImplicitInputValue, i => checkEquality(i, eval(x))) case InputLessThan(x) => withVal(getImplicitInputValue, i => dualOp(i, eval(x), _ < _, ValBoolean)) case InputLessOrEqual(x) => @@ -120,7 +120,7 @@ class FeelInterpreter(private val valueMapper: ValueMapper) { withValOrNull(withNumber(eval(x), x => ValNumber(-x))) // dual comparators - case Equal(x, y) => checkEquality(eval(x), eval(y), _ == _, ValBoolean) + case Equal(x, y) => checkEquality(eval(x), eval(y)) case LessThan(x, y) => dualOp(eval(x), eval(y), _ < _, ValBoolean) case LessOrEqual(x, y) => dualOp(eval(x), eval(y), _ <= _, ValBoolean) case GreaterThan(x, y) => dualOp(eval(x), eval(y), _ > _, ValBoolean) @@ -515,18 +515,15 @@ class FeelInterpreter(private val valueMapper: ValueMapper) { } } - private def checkEquality(x: Val, y: Val, c: (Any, Any) => Boolean, f: Boolean => Val)(implicit - context: EvalContext - ): Val = + private def checkEquality(x: Val, y: Val)(implicit context: EvalContext): Val = withValues( x, y, - { (x, y) => + (x, y) => valueComparator.compare(x, y).toOption.getOrElse { error(EvaluationFailureType.NOT_COMPARABLE, s"Can't compare '$x' with '$y'") ValNull } - } ) private def dualOp(x: Val, y: Val, c: (Val, Val) => Boolean, f: Boolean => Val)(implicit @@ -670,7 +667,7 @@ class FeelInterpreter(private val valueMapper: ValueMapper) { // the expression contains the input value ValBoolean(true) case x => - checkEquality(inputValue, x, _ == _, ValBoolean) match { + checkEquality(inputValue, x) match { case ValBoolean(true) => // the expression is the input value ValBoolean(true) From 1e30164e289b47ac35b9ee0910c73f0a8b6c39c0 Mon Sep 17 00:00:00 2001 From: Philipp Ossler Date: Tue, 27 Aug 2024 06:54:36 +0200 Subject: [PATCH 06/11] fix: distinct values() for list Filter duplicate list entries by using the value comparator. The equals() for list and context values doesn't work properly because of the nested structure. --- .../feel/impl/builtin/ListBuiltinFunctions.scala | 16 ++++++++++++++-- .../feel/impl/interpreter/BuiltinFunctions.scala | 2 +- .../feel/impl/interpreter/ValComparator.scala | 5 +++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/main/scala/org/camunda/feel/impl/builtin/ListBuiltinFunctions.scala b/src/main/scala/org/camunda/feel/impl/builtin/ListBuiltinFunctions.scala index 44fc60984..d209e4c84 100644 --- a/src/main/scala/org/camunda/feel/impl/builtin/ListBuiltinFunctions.scala +++ b/src/main/scala/org/camunda/feel/impl/builtin/ListBuiltinFunctions.scala @@ -18,6 +18,7 @@ package org.camunda.feel.impl.builtin import org.camunda.feel.impl.builtin.BuiltinFunction.builtinFunction import org.camunda.feel.Number +import org.camunda.feel.impl.interpreter.ValComparator import org.camunda.feel.syntaxtree.{ Val, ValBoolean, @@ -28,10 +29,13 @@ import org.camunda.feel.syntaxtree.{ ValNumber, ValString } +import org.camunda.feel.valuemapper.ValueMapper import scala.annotation.tailrec -object ListBuiltinFunctions { +class ListBuiltinFunctions(private val valueMapper: ValueMapper) { + + private val valueComparator = new ValComparator(valueMapper) def functions = Map( "list contains" -> List(listContainsFunction), @@ -392,7 +396,15 @@ object ListBuiltinFunctions { builtinFunction( params = List("list"), invoke = { case List(ValList(list)) => - ValList(list.distinct) + val distinctList = list.foldLeft(List[Val]())((result, item) => + if (result.exists(y => valueComparator.equals(item, y))) { + // duplicate value + result + } else { + result :+ item + } + ) + ValList(distinctList) } ) diff --git a/src/main/scala/org/camunda/feel/impl/interpreter/BuiltinFunctions.scala b/src/main/scala/org/camunda/feel/impl/interpreter/BuiltinFunctions.scala index 16f6f5195..2a9b5125c 100644 --- a/src/main/scala/org/camunda/feel/impl/interpreter/BuiltinFunctions.scala +++ b/src/main/scala/org/camunda/feel/impl/interpreter/BuiltinFunctions.scala @@ -42,7 +42,7 @@ class BuiltinFunctions(clock: FeelEngineClock, valueMapper: ValueMapper) extends new ConversionBuiltinFunctions(valueMapper).functions ++ BooleanBuiltinFunctions.functions ++ StringBuiltinFunctions.functions ++ - ListBuiltinFunctions.functions ++ + new ListBuiltinFunctions(valueMapper).functions ++ NumericBuiltinFunctions.functions ++ new ContextBuiltinFunctions(valueMapper).functions ++ RangeBuiltinFunction.functions ++ diff --git a/src/main/scala/org/camunda/feel/impl/interpreter/ValComparator.scala b/src/main/scala/org/camunda/feel/impl/interpreter/ValComparator.scala index b190ba6e7..50900f53e 100644 --- a/src/main/scala/org/camunda/feel/impl/interpreter/ValComparator.scala +++ b/src/main/scala/org/camunda/feel/impl/interpreter/ValComparator.scala @@ -22,6 +22,11 @@ import org.camunda.feel.valuemapper.ValueMapper class ValComparator(private val valueMapper: ValueMapper) { + def equals(x: Val, y: Val): Boolean = compare(x, y) match { + case ValBoolean(isEqual) => isEqual + case _ => false + } + def compare(x: Val, y: Val): Val = (x, y) match { // both values are null case (ValNull, _) => ValBoolean(ValNull == y.toOption.getOrElse(ValNull)) From 1ed648d171252ae6fd80a6cd49bf72b7b1c36144 Mon Sep 17 00:00:00 2001 From: Philipp Ossler Date: Tue, 27 Aug 2024 10:23:23 +0200 Subject: [PATCH 07/11] test: Verify union() removes duplicates Add a test to verify that the function removes duplicated context values. --- .../builtin/BuiltinListFunctionsTest.scala | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/test/scala/org/camunda/feel/impl/builtin/BuiltinListFunctionsTest.scala b/src/test/scala/org/camunda/feel/impl/builtin/BuiltinListFunctionsTest.scala index 1c90be221..581fd7a5a 100644 --- a/src/test/scala/org/camunda/feel/impl/builtin/BuiltinListFunctionsTest.scala +++ b/src/test/scala/org/camunda/feel/impl/builtin/BuiltinListFunctionsTest.scala @@ -301,6 +301,58 @@ class BuiltinListFunctionsTest evaluateExpression(" union([1,2],[2,3], [4]) ") should returnResult(List(1, 2, 3, 4)) } + it should "invoked with named parameter" in { + + evaluateExpression(" union(lists: [[1,2],[2,3]]) ") should returnResult(List(1, 2, 3)) + } + + it should "remove duplicated context values" in { + + evaluateExpression("union([{a:1},{a:2}],[{a:1},{b:3}])") should returnResult( + List(Map("a" -> 1), Map("a" -> 2), Map("b" -> 3)) + ) + + evaluateExpression("union([{a:1},{a:null}],[{a:null},{b:2}])") should returnResult( + List(Map("a" -> 1), Map("a" -> null), Map("b" -> 2)) + ) + + evaluateExpression("union([{a:1},{}],[{},{b:2}])") should returnResult( + List(Map("a" -> 1), Map(), Map("b" -> 2)) + ) + + evaluateExpression( + "union([{a:1,b:{c:2}}, {a:1,b:{c:3}}], [{a:1,b:{c:2}}, {a:1,b:{c:3},d:4}])" + ) should returnResult( + List( + Map("a" -> 1, "b" -> Map("c" -> 2)), + Map("a" -> 1, "b" -> Map("c" -> 3)), + Map("a" -> 1, "b" -> Map("c" -> 3), "d" -> 4) + ) + ) + } + + it should "remove duplicated list values" in { + evaluateExpression(" union([[1],[2]],[[3],[2]]) ") should returnResult( + List(List(1), List(2), List(3)) + ) + + evaluateExpression(" union([[1],[null]],[[1],[null]]) ") should returnResult( + List(List(1), List(null)) + ) + + evaluateExpression(" union([[1],[]],[[],[2]]) ") should returnResult( + List(List(1), List.empty, List(2)) + ) + + evaluateExpression(" union([[1,2],[4,5]],[[1,2],[4]]) ") should returnResult( + List(List(1, 2), List(4, 5), List(4)) + ) + } + + it should "remove duplicated null values" in { + evaluateExpression(" union([1,null],[2,null]) ") should returnResult(List(1, null, 2)) + } + "A distinct values() function" should "remove duplicates" in { evaluateExpression(" distinct values([1,2,3,2,1]) ") should returnResult(List(1, 2, 3)) From fa45a3d250fb9f2b22a5a952e6eb329af860bdd6 Mon Sep 17 00:00:00 2001 From: Philipp Ossler Date: Tue, 27 Aug 2024 10:30:21 +0200 Subject: [PATCH 08/11] fix: union() with context entries Remove duplicated context entries from the concatenated list. --- .../impl/builtin/ListBuiltinFunctions.scala | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/src/main/scala/org/camunda/feel/impl/builtin/ListBuiltinFunctions.scala b/src/main/scala/org/camunda/feel/impl/builtin/ListBuiltinFunctions.scala index d209e4c84..285420af7 100644 --- a/src/main/scala/org/camunda/feel/impl/builtin/ListBuiltinFunctions.scala +++ b/src/main/scala/org/camunda/feel/impl/builtin/ListBuiltinFunctions.scala @@ -379,15 +379,11 @@ class ListBuiltinFunctions(private val valueMapper: ValueMapper) { private def unionFunction = builtinFunction( params = List("lists"), invoke = { case List(ValList(lists)) => - ValList( - lists - .flatMap(_ match { - case ValList(list) => list - case v => List(v) - }) - .toList - .distinct - ) + val listOfLists = lists.flatMap { + case ValList(list) => list + case v => List(v) + } + ValList(distinct(listOfLists)) }, hasVarArgs = true ) @@ -395,18 +391,19 @@ class ListBuiltinFunctions(private val valueMapper: ValueMapper) { private def distinctValuesFunction = builtinFunction( params = List("list"), - invoke = { case List(ValList(list)) => - val distinctList = list.foldLeft(List[Val]())((result, item) => - if (result.exists(y => valueComparator.equals(item, y))) { - // duplicate value - result - } else { - result :+ item - } - ) - ValList(distinctList) + invoke = { case List(ValList(list)) => ValList(distinct(list)) } + ) + + private def distinct(list: List[Val]): List[Val] = { + list.foldLeft(List[Val]())((result, item) => + if (result.exists(y => valueComparator.equals(item, y))) { + // duplicate value + result + } else { + result :+ item } ) + } private def duplicateValuesFunction = builtinFunction( From 0a623d5f3a443b21da47f30b0c7088a3d90baa7f Mon Sep 17 00:00:00 2001 From: Philipp Ossler Date: Tue, 27 Aug 2024 10:45:58 +0200 Subject: [PATCH 09/11] test: Verify duplicate values() return duplicates Add a test to verify that the function returns duplicated context values. --- .../builtin/BuiltinListFunctionsTest.scala | 61 +++++++++++++++++-- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/src/test/scala/org/camunda/feel/impl/builtin/BuiltinListFunctionsTest.scala b/src/test/scala/org/camunda/feel/impl/builtin/BuiltinListFunctionsTest.scala index 581fd7a5a..a2a80c0b7 100644 --- a/src/test/scala/org/camunda/feel/impl/builtin/BuiltinListFunctionsTest.scala +++ b/src/test/scala/org/camunda/feel/impl/builtin/BuiltinListFunctionsTest.scala @@ -419,16 +419,67 @@ class BuiltinListFunctionsTest evaluateExpression(" duplicate values([1,2,3,2,1]) ") should returnResult(List(1, 2)) } - it should "return null duplicate values" in { + it should "invoked with named parameter" in { + + evaluateExpression(" duplicate values(list: [1,2,3,2,1]) ") should returnResult(List(1, 2)) + } + + it should "return an empty list if there are no duplicates" in { + + evaluateExpression(" duplicate values(list: [1,2,3,4,5]) ") should returnResult(List.empty) + } + + it should "return duplicated context values" in { + + evaluateExpression("duplicate values([{a:1},{a:2},{a:1},{a:2},{b:3}])") should returnResult( + List(Map("a" -> 1), Map("a" -> 2)) + ) + + evaluateExpression( + "duplicate values([{a:1},{a:null},{a:null},{b:2},{a:1}])" + ) should returnResult( + List(Map("a" -> 1), Map("a" -> null)) + ) + + evaluateExpression("duplicate values([{a:1},{},{},{b:2},{a:1}])") should returnResult( + List(Map("a" -> 1), Map.empty) + ) + + evaluateExpression( + "duplicate values([{a:1,b:{c:2}}, {a:1,b:{c:3}}, {a:1,b:{c:2}}, {a:1,b:{c:3},d:4}, {a:1}])" + ) should returnResult( + List( + Map("a" -> 1, "b" -> Map("c" -> 2)) + ) + ) + } + + it should "return duplicated list values" in { + evaluateExpression(" duplicate values([[1],[2],[3],[2],[3]]) ") should returnResult( + List(List(2), List(3)) + ) + + evaluateExpression(" duplicate values([[1],[null],[1],[null],[2]]) ") should returnResult( + List(List(1), List(null)) + ) + + evaluateExpression(" duplicate values([[1],[],[],[2],[1]]) ") should returnResult( + List(List(1), List.empty) + ) - evaluateExpression(" duplicate values([1,2,1,null,null]) ") should returnResult( - List(1, null) + evaluateExpression(" duplicate values([[1,2],[4,5],[1,2],[4]]) ") should returnResult( + List(List(1, 2)) ) } - it should "return duplicate values for named parameters" in { + it should "return duplicated null values" in { + evaluateExpression(" duplicate values([1,null,2,null,2]) ") should returnResult(List(null, 2)) + } - evaluateExpression(" duplicate values(list: [1,2,3,2,1]) ") should returnResult(List(1, 2)) + it should "preserve the order" in { + evaluateExpression(" duplicate values([1,2,3,4,2,3,1]) ") should returnResult(List(1, 2, 3)) + + evaluateExpression(" duplicate values([1,2,3,4,3,1,2]) ") should returnResult(List(1, 2, 3)) } "A flatten() function" should "flatten nested lists" in { From a47d8352949477473616e80067ec8b3181af1445 Mon Sep 17 00:00:00 2001 From: Philipp Ossler Date: Tue, 27 Aug 2024 10:57:28 +0200 Subject: [PATCH 10/11] fix: duplicate values() for context entries Return duplicated context entries from list. --- .../org/camunda/feel/impl/builtin/ListBuiltinFunctions.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/scala/org/camunda/feel/impl/builtin/ListBuiltinFunctions.scala b/src/main/scala/org/camunda/feel/impl/builtin/ListBuiltinFunctions.scala index 285420af7..dea911e08 100644 --- a/src/main/scala/org/camunda/feel/impl/builtin/ListBuiltinFunctions.scala +++ b/src/main/scala/org/camunda/feel/impl/builtin/ListBuiltinFunctions.scala @@ -409,7 +409,10 @@ class ListBuiltinFunctions(private val valueMapper: ValueMapper) { builtinFunction( params = List("list"), invoke = { case List(ValList(list)) => - ValList(list.distinct.filter(x => list.count(_ == x) > 1)) + val duplicatedValues = + distinct(list).filter(x => list.count(valueComparator.equals(_, x)) > 1) + + ValList(duplicatedValues) } ) From 4f78808156a506b7bb63a7339298f480f6a0f4f5 Mon Sep 17 00:00:00 2001 From: Philipp Ossler Date: Tue, 3 Sep 2024 13:12:37 +0200 Subject: [PATCH 11/11] refactor: Simplify value comparator --- .../feel/impl/interpreter/ValComparator.scala | 38 ++++--------------- 1 file changed, 8 insertions(+), 30 deletions(-) diff --git a/src/main/scala/org/camunda/feel/impl/interpreter/ValComparator.scala b/src/main/scala/org/camunda/feel/impl/interpreter/ValComparator.scala index 50900f53e..6d9e3b75e 100644 --- a/src/main/scala/org/camunda/feel/impl/interpreter/ValComparator.scala +++ b/src/main/scala/org/camunda/feel/impl/interpreter/ValComparator.scala @@ -49,43 +49,21 @@ class ValComparator(private val valueMapper: ValueMapper) { } private def compare(x: List[Val], y: List[Val]): ValBoolean = { - if (x.size != y.size) { - ValBoolean(false) - - } else { - val itemsAreEqual = x.zip(y).foldRight(true) { case ((x, y), listIsEqual) => - listIsEqual && { - compare(x, y) match { - case ValBoolean(itemIsEqual) => itemIsEqual - case _ => false - } - } - } - ValBoolean(itemsAreEqual) - } + ValBoolean( + x.size == y.size && x.zip(y).forall { case (itemX, itemY) => equals(itemX, itemY) } + ) } private def compare(x: Context, y: Context): ValBoolean = { val xVars = x.variableProvider.getVariables val yVars = y.variableProvider.getVariables - if (xVars.keys != yVars.keys) { - ValBoolean(false) - - } else { - val itemsAreEqual = xVars.keys.foldRight(true) { case (key, contextIsEqual) => - contextIsEqual && { - val xVal = valueMapper.toVal(xVars(key)) - val yVal = valueMapper.toVal(yVars(key)) + ValBoolean(xVars.keys == yVars.keys && xVars.keys.forall { key => + val xVal = valueMapper.toVal(xVars(key)) + val yVal = valueMapper.toVal(yVars(key)) - compare(xVal, yVal) match { - case ValBoolean(entryIsEqual) => entryIsEqual - case _ => false - } - } - } - ValBoolean(itemsAreEqual) - } + equals(xVal, yVal) + }) } }