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/builtin/ListBuiltinFunctions.scala b/src/main/scala/org/camunda/feel/impl/builtin/ListBuiltinFunctions.scala index 44fc60984..dea911e08 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), @@ -375,15 +379,11 @@ object ListBuiltinFunctions { 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 ) @@ -391,16 +391,28 @@ object ListBuiltinFunctions { private def distinctValuesFunction = builtinFunction( params = List("list"), - invoke = { case List(ValList(list)) => - ValList(list.distinct) + 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( 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) } ) 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/FeelInterpreter.scala b/src/main/scala/org/camunda/feel/impl/interpreter/FeelInterpreter.scala index b1fa21078..60072b841 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 @@ -80,7 +82,7 @@ class FeelInterpreter { // 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) => @@ -118,7 +120,7 @@ class FeelInterpreter { 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) @@ -513,65 +515,15 @@ class FeelInterpreter { } } - 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, - { - 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) => + 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 @@ -715,7 +667,7 @@ class FeelInterpreter { // 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) 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..6d9e3b75e --- /dev/null +++ b/src/main/scala/org/camunda/feel/impl/interpreter/ValComparator.scala @@ -0,0 +1,69 @@ +/* + * 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 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)) + 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 = { + 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 + + ValBoolean(xVars.keys == yVars.keys && xVars.keys.forall { key => + val xVal = valueMapper.toVal(xVars(key)) + val yVal = valueMapper.toVal(yVars(key)) + + equals(xVal, yVal) + }) + } + +} 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 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..a2a80c0b7 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,555 @@ 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)) + } + + 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 { - 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)) + } + + 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 { - 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 { + it should "invoked with named parameter" in { - eval(" duplicate values([1,2,1,null,null]) ") should be(ValList(List(ValNumber(1), ValNull))) + evaluateExpression(" duplicate values(list: [1,2,3,2,1]) ") should returnResult(List(1, 2)) } - it should "return duplicate values for named parameters" in { + it should "return an empty list if there are no duplicates" 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,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)) + ) ) } - "A flatten() function" should "flatten nested lists" in { + 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)) + ) - eval(" flatten([[1,2],[[3]], 4]) ") should be( - ValList(List(ValNumber(1), ValNumber(2), ValNumber(3), ValNumber(4))) + evaluateExpression(" duplicate values([[1],[],[],[2],[1]]) ") should returnResult( + List(List(1), List.empty) + ) + + evaluateExpression(" duplicate values([[1,2],[4,5],[1,2],[4]]) ") should returnResult( + List(List(1, 2)) ) } + it should "return duplicated null values" in { + evaluateExpression(" duplicate values([1,null,2,null,2]) ") should returnResult(List(null, 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 { + + 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) } }