From 85c690d3512d898bee915be3d1b89bbc78fd468e 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 (cherry picked from commit 5a592bca984ed186db520ae80049de80c5ae9c3e) --- .../builtin/BuiltinListFunctionsTest.scala | 247 +++++++++--------- 1 file changed, 124 insertions(+), 123 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 aca5a1254..0f1dd4161 100644 --- a/src/test/scala/org/camunda/feel/impl/builtin/BuiltinListFunctionsTest.scala +++ b/src/test/scala/org/camunda/feel/impl/builtin/BuiltinListFunctionsTest.scala @@ -19,388 +19,389 @@ 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() } } From 7887940df91e2cdf227df620d0464922c7de6c4c 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. (cherry picked from commit 9c3ed24e955ea13baec89b0a2448962326bce0cd) --- .../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 0f1dd4161..c4191207c 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 455e7361127812a2d201a8d1358f6b1f90c0ba89 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. (cherry picked from commit f5c5017cd86a29332beae6648bb193c260b09ada) --- .../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 35f3fa4ec20fc889931dda60d126b8944ba76b57 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 (cherry picked from commit 88cf0248d6791c5b131aca1e003bddc6827ea07c) --- 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 9f6f753bcaf06fca69141ba96b197b1adfa16b0b 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 (cherry picked from commit 9797b74f10a1a644b7d83dc0bdd19ff225c95dc1) --- .../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 4862d8c0a908c65eeda900a0c2307e6984578e6c 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. (cherry picked from commit 1e30164e289b47ac35b9ee0910c73f0a8b6c39c0) --- .../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 1289bf916..f9174172d 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), @@ -391,7 +395,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 cb96b37c072e237c4ff6b669412f298a7ef7fc44 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. (cherry picked from commit 1ed648d171252ae6fd80a6cd49bf72b7b1c36144) --- .../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 c4191207c..507eb4e8d 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 d0e7ba095cc5b2f23754ed2020d0ef2db7d3ed43 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. (cherry picked from commit fa45a3d250fb9f2b22a5a952e6eb329af860bdd6) --- .../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 f9174172d..bce857000 100644 --- a/src/main/scala/org/camunda/feel/impl/builtin/ListBuiltinFunctions.scala +++ b/src/main/scala/org/camunda/feel/impl/builtin/ListBuiltinFunctions.scala @@ -378,15 +378,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 ) @@ -394,18 +390,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 e43f03f94f7c8a7ddb3ef262a8b6e58a2b8a5060 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. (cherry picked from commit 0a623d5f3a443b21da47f30b0c7088a3d90baa7f) --- .../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 507eb4e8d..4f96e7964 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 a56afb50ce885d0808ff194c002b2ea3a0e88589 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. (cherry picked from commit a47d8352949477473616e80067ec8b3181af1445) --- .../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 bce857000..e4285bf8c 100644 --- a/src/main/scala/org/camunda/feel/impl/builtin/ListBuiltinFunctions.scala +++ b/src/main/scala/org/camunda/feel/impl/builtin/ListBuiltinFunctions.scala @@ -408,7 +408,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 c633560dbd8b9703cd5eb8308def4b45efda0b7b 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 (cherry picked from commit 4f78808156a506b7bb63a7339298f480f6a0f4f5) --- .../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) + }) } }