Skip to content

Commit

Permalink
[#655] DMN 1.5: Add 'list replace' function (DMN15-143)
Browse files Browse the repository at this point in the history
  • Loading branch information
opatrascoiu committed Dec 10, 2024
1 parent 925403b commit 925a234
Show file tree
Hide file tree
Showing 22 changed files with 1,603 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,8 @@ public Element<Type> visit(FunctionInvocation<Type> element, DMNContext context)
Parameters<Type> parameters = element.getParameters();
if (function instanceof Name && "sort".equals(((Name<Type>) function).getName())) {
visitSortParameters(element, context, parameters);
} else if (function instanceof Name && "list replace".equals(((Name<Type>) function).getName())) {
visitListReplaceParameters(element, context, parameters);
} else {
parameters.accept(this, context);
}
Expand Down Expand Up @@ -745,6 +747,65 @@ private void visitSortParameters(FunctionInvocation<Type> element, DMNContext co
}
}

private void visitListReplaceParameters(FunctionInvocation<Type> element, DMNContext context, Parameters<Type> parameters) {
ParameterTypes<Type> signature = parameters.getSignature();
if (signature.size() == 3) {
Expression<Type> listExpression;
Expression<Type> secondParameter;
boolean hasPosition = false;
Expression<Type> newItemExpression;
if (parameters instanceof PositionalParameters) {
listExpression = ((PositionalParameters<Type>) parameters).getParameters().get(0);
secondParameter = ((PositionalParameters<Type>) parameters).getParameters().get(1);
newItemExpression = ((PositionalParameters<Type>) parameters).getParameters().get(2);
} else {
listExpression = ((NamedParameters<Type>) parameters).getParameters().get("list");
secondParameter = ((NamedParameters<Type>) parameters).getParameters().get("position");
if (secondParameter == null) {
hasPosition = true;
secondParameter = ((NamedParameters<Type>) parameters).getParameters().get("match");
}
newItemExpression = ((NamedParameters<Type>) parameters).getParameters().get("newItem");
}
listExpression.accept(this, context);
Type listType = listExpression.getType();
if (listType instanceof ListType) {
Type elementType = ((ListType) listType).getElementType();
if (secondParameter instanceof FunctionDefinition) {
List<FormalParameter<Type>> formalParameters = ((FunctionDefinition<Type>) secondParameter).getFormalParameters();
formalParameters.forEach(p -> p.setType(elementType));
} else if (secondParameter instanceof Name) {
Declaration declaration = context.lookupVariableDeclaration(((Name<Type>) secondParameter).getName());
Type type = declaration.getType();
if (type instanceof FunctionType && !(type instanceof BuiltinFunctionType)) {
List<FormalParameter<Type>> formalParameters = ((FunctionType) type).getParameters();
formalParameters.forEach(p -> p.setType(elementType));
}
}
}
secondParameter.accept(this, context);
// Check type for named parameters invocation
Type secondParameterType = secondParameter.getType();
if (hasPosition && secondParameterType == NUMBER) {
handleError(context, element, String.format("Parameter 'position' must be a number, found '%s'", secondParameterType));
}
// Check cardinality and return type of 'match' parameter
if (secondParameterType instanceof FunctionType) {
List<FormalParameter<Type>> matchSignature = ((FunctionType) secondParameterType).getParameters();
if (matchSignature.size() != 2) {
handleError(context, element, String.format("'match' parameter should have 2 parameters, found '%s'", matchSignature.size()));
}
Type returnType = ((FunctionType) secondParameterType).getReturnType();
if (returnType != BOOLEAN) {
handleError(context, element, String.format("'match' parameter should return boolean, found '%s'", returnType));
}
}
newItemExpression.accept(this, context);
} else {
handleError(context, element, String.format("Expecting 3 parameters found '%s'", signature.size()));
}
}

private void inferMissingTypesInFEELFunction(Expression<Type> function, Parameters<Type> arguments, DMNContext context) {
Type functionType = function.getType();
if (functionType instanceof FEELFunctionType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@
import com.gs.dmn.context.environment.Declaration;
import com.gs.dmn.el.analysis.semantics.type.Type;
import com.gs.dmn.feel.analysis.semantics.environment.StandardEnvironmentFactory;
import com.gs.dmn.feel.analysis.semantics.type.BuiltinFunctionType;
import com.gs.dmn.feel.analysis.semantics.type.ComparableDataType;
import com.gs.dmn.feel.analysis.semantics.type.FunctionType;
import com.gs.dmn.feel.analysis.semantics.type.ListType;
import com.gs.dmn.feel.analysis.semantics.type.*;
import com.gs.dmn.feel.analysis.syntax.ast.expression.Expression;
import com.gs.dmn.feel.analysis.syntax.ast.expression.function.*;
import com.gs.dmn.feel.analysis.syntax.ast.expression.literal.ListLiteral;
Expand Down Expand Up @@ -103,6 +100,18 @@ private static Type refineFunctionType(FunctionInvocation<Type> element, Declara
Type elementType = parameters.getParameterType(1, "element");
return StandardEnvironmentFactory.makeSignavioRemoveBuiltinFunctionType(listType, elementType);
}
} else if("list replace".equals(functionName)) {
Type listType = parameters.getParameterType(0, "list");
if (!(listType instanceof ListType)) {
// Implicit conversion
listType = new ListType(listType);
}
Expression<Type> secondParam = parameters.getParameter(1, "position");
if (secondParam != null && parameters.getParameterType(1, "position") instanceof NumberType) {
return StandardEnvironmentFactory.makeListReplacePositionBuiltinFunctionType(listType);
} else {
return StandardEnvironmentFactory.makeListReplaceMatchBuiltinFunctionType(listType);
}
} else if("reverse".equals(functionName)) {
Type listType = parameters.getParameterType(0, "list");
return StandardEnvironmentFactory.makeReverseBuiltinFunctionType(listType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import static com.gs.dmn.feel.analysis.semantics.type.DateType.DATE;
import static com.gs.dmn.feel.analysis.semantics.type.DurationType.ANY_DURATION;
import static com.gs.dmn.feel.analysis.semantics.type.DaysAndTimeDurationType.DAYS_AND_TIME_DURATION;
import static com.gs.dmn.feel.analysis.semantics.type.FunctionType.ANY_FUNCTION;
import static com.gs.dmn.feel.analysis.semantics.type.YearsAndMonthsDurationType.YEARS_AND_MONTHS_DURATION;
import static com.gs.dmn.feel.analysis.semantics.type.ListType.*;
import static com.gs.dmn.feel.analysis.semantics.type.NumberType.NUMBER;
Expand Down Expand Up @@ -113,6 +114,14 @@ public static BuiltinFunctionType makeFlattenBuiltinFunctionType(Type listType)
return new BuiltinFunctionType(listType, new FormalParameter<>("list", ANY_LIST));
}

public static BuiltinFunctionType makeListReplacePositionBuiltinFunctionType(Type listType) {
return new BuiltinFunctionType(listType, new FormalParameter<>("list", ANY_LIST), new FormalParameter<>("position", NUMBER), new FormalParameter<>("newItem", ANY, false, false));
}

public static BuiltinFunctionType makeListReplaceMatchBuiltinFunctionType(Type listType) {
return new BuiltinFunctionType(listType, new FormalParameter<>("list", ANY_LIST), new FormalParameter<>("match", ANY_FUNCTION), new FormalParameter<>("newItem", ANY, false, false));
}

public static BuiltinFunctionType makeSortBuiltinFunctionType(Type listType, Type functionType) {
return new BuiltinFunctionType(listType, new FormalParameter<>("list", listType), new FormalParameter<>("function", functionType));
}
Expand Down Expand Up @@ -286,6 +295,8 @@ private static void addListFunctions(Environment environment) {
addFunctionDeclaration(environment, "stddev", new BuiltinFunctionType(NUMBER, new FormalParameter<>("n1", NUMBER), new FormalParameter<>("ns", NUMBER, false, true)));
addFunctionDeclaration(environment, "mode", new BuiltinFunctionType(NUMBER, new FormalParameter<>("list", NUMBER_LIST)));
addFunctionDeclaration(environment, "mode", new BuiltinFunctionType(NUMBER, new FormalParameter<>("n1", NUMBER), new FormalParameter<>("ns", NUMBER, false, true)));
addFunctionDeclaration(environment, "list replace", makeListReplacePositionBuiltinFunctionType(ANY_LIST));
addFunctionDeclaration(environment, "list replace", makeListReplaceMatchBuiltinFunctionType(ANY_LIST));

addFunctionDeclaration(environment, "sort", makeSortBuiltinFunctionType(ANY_LIST, ANY));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ protected ArrayList<Pair<ParameterTypes<Type>, ParameterConversions<Type>>> calc
List<Type> newTypes = new ArrayList<>();
PositionalParameterConversions<Type> conversions = new PositionalParameterConversions<>();
boolean different = false;
boolean succsefulCandidate = true;
boolean failed = false;
for (int i = 0; i < argumentSize; i++) {
// Compute new type and conversion
ConversionKind kind = candidateConversions[conversionMap[i]];
Expand Down Expand Up @@ -169,7 +169,7 @@ protected ArrayList<Pair<ParameterTypes<Type>, ParameterConversions<Type>>> calc
// Check if new argument type matches
boolean newArgumentTypeOk = Type.conformsTo(newArgumentType, parameterType);
if (!newArgumentTypeOk) {
succsefulCandidate = false;
failed = true;
break;
} else {
newTypes.add(newArgumentType);
Expand All @@ -179,7 +179,7 @@ protected ArrayList<Pair<ParameterTypes<Type>, ParameterConversions<Type>>> calc
}

// Add new candidate
if (different && succsefulCandidate) {
if (different && !failed) {
PositionalParameterTypes<Type> newSignature = new PositionalParameterTypes<>(newTypes);
candidates.add(new Pair<>(newSignature, conversions));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,15 @@ Object evaluateFunctionInvocation(Object functionDefinition, FunctionType functi
handleError(String.format("'%s' is not supported yet", secondArg.getClass()));
return null;
}
} else if ("listReplace".equals(javaFunctionName)) {
Object secondArg = argList.get(1);
if (secondArg instanceof Function) {
Function filterFunction = (Function) secondArg;
List result = ((StandardFEELLib) lib).listReplace((List) argList.get(0), makeLambdaExpression(filterFunction), argList.get(2));
return result;
} else {
return evaluateBuiltInFunction(this.lib, javaFunctionName, argList);
}
} else {
return evaluateBuiltInFunction(this.lib, javaFunctionName, argList);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public abstract class AbstractFEELToJavaVisitor<R> extends AbstractAnalysisVisit
FEEL_2_JAVA_FUNCTION.put("index of", "indexOf");
FEEL_2_JAVA_FUNCTION.put("insert before", "insertBefore");
FEEL_2_JAVA_FUNCTION.put("list contains", "listContains");
FEEL_2_JAVA_FUNCTION.put("list replace", "listReplace");

// context functions
FEEL_2_JAVA_FUNCTION.put("get value", "getValue");
Expand Down
3 changes: 3 additions & 0 deletions dmn-core/src/main/resources/dmn/1.5/FEELLexer.g4
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,9 @@ NAME:
'time' WhiteSpace+ 'offset'
{ setText("time offset"); }
|
'list' WhiteSpace+ 'replace'
{ setText("list replace"); }
|
NameStartChar ( NamePartChar )*
|
'\'' ( ~(['] | [\u000A-\u000D]) | '\'\'')* '\''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -622,4 +622,9 @@ public void test_15_cl3_0068_feel_equality() {
doSingleModelTest("1.5", "0068-feel-equality", new Pair<>("strongTyping", "false") );
}

@Test
public void test_15_cl3_1155_list_replace_function() {
doSingleModelTest("1.5", "1155-list-replace-function");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -1238,6 +1238,17 @@ public <T> List<T> remove(List<T> list, Object position) {
}
}

@Override
public <T> List<T> listReplace(List<T> list, Object position, T newItem) {
try {
return this.listLib.listReplace(list, position, newItem);
} catch (Exception e) {
String message = String.format("listReplace(%s, %s, %s)", list, position, newItem);
logError(message, e);
return null;
}
}

@Override
public <T> List<T> reverse(List<T> list) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ public interface StandardFEELLib<NUMBER, DATE, TIME, DATE_TIME, DURATION> extend

<T> List<T> remove(List<T> list, Object position);

<T> List<T> listReplace(List<T> list, Object position, T newItem);

<T> List<T> reverse(List<T> list);

<T> List<NUMBER> indexOf(List<T> list, Object match);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/
package com.gs.dmn.feel.lib.type.list;

import com.gs.dmn.runtime.DMNRuntimeException;
import com.gs.dmn.runtime.LambdaExpression;

import java.util.ArrayList;
Expand Down Expand Up @@ -123,6 +124,48 @@ public <T> List<T> remove(List<T> list, int position) {
return result;
}

@Override
public <T> List<T> listReplace(List<T> list, Object position, T newItem) {
if (list == null || position == null) {
return null;
}

if (position instanceof Number) {
return listReplaceInt(list, (Number) position, newItem);
} else if (position instanceof LambdaExpression) {
return listReplaceMatch(list, (LambdaExpression) position, newItem);
} else {
throw new DMNRuntimeException(String.format("Illegal argument '%s'. Expected number or predicate", position.getClass().getName()));
}
}

private <T> List<T> listReplaceInt(List<T> list, Number position, T newItem) {
List result = new ArrayList<>();
result.addAll(list);

int index = position.intValue();
if (index < 0) {
index = list.size() + index;
} else {
--index;
}
result.set(index, newItem);
return result;
}

private <T> List<T> listReplaceMatch(List<T> list, LambdaExpression<Boolean> match, T newItem) {
List result = new ArrayList<>();
result.addAll(list);

for (int i=0; i<list.size(); i++) {
if (match.apply(list.get(i), newItem)) {
result.set(i, newItem);
}
}

return result;
}

@Override
public <T> List<T> reverse(List<T> list) {
List result = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public interface ListLib {

<T> List<T> remove(List<T> list, int position);

<T> List<T> listReplace(List<T> list, Object position, T newItem);

<T> List<T> reverse(List<T> list);

<T> List<T> union(List<T>... lists);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,36 @@ public void testRemove() {
assertEquals(makeNumberList("1", "3"), getLib().remove(makeNumberList(1, 2, 3), makeNumber("2")));
}

@Test
public void testListReplace() {
// List or position cannot be null
assertNull(getLib().listReplace(null, makeNumber(1), null));
assertNull(getLib().listReplace(makeNumberList(1, 2), null, null));

// Zero position gives null
assertNull(getLib().listReplace(makeNumberList("1", "2", "3"), makeNumber(0), makeNumber(4)));
// Position outside bounds gives null
assertNull(getLib().listReplace(makeNumberList("1", "2", "3"), makeNumber(4), makeNumber(4)));
// Negative position outside bounds gives null
assertNull(getLib().listReplace(makeNumberList("1", "2", "3"), makeNumber(-4), makeNumber(4)));

// NewItem can be null
assertEquals(makeNumberList("1", "2", null), getLib().listReplace(makeNumberList("1", "2", "3"), makeNumber("3"), null));
assertEquals(makeNumberList(1, 4, 3), getLib().listReplace(makeNumberList("1", "2", "3"), makeNumber(2), makeNumber(4)));
// Replace last element
assertEquals(makeNumberList(1, 2, 4), getLib().listReplace(makeNumberList("1", "2", "3"), makeNumber(-1), makeNumber(4)));

// Test with predicate
LambdaExpression<Boolean> predicate = new LambdaExpression<Boolean>() {
@Override
public Boolean apply(Object... args) {
return getLib().numericLessThan((NUMBER) args[0], (NUMBER) args[1]);
}
};
assertEquals(makeNumberList(5, 5, 7, 8), getLib().listReplace(makeNumberList(2, 4, 7, 8), predicate, makeNumber(5)));
assertEquals(makeNumberList(5, 5, 5, 5), getLib().listReplace(makeNumberList(1, 2, 3, 4), (LambdaExpression<Boolean>) args -> true, makeNumber(5)));
}

@Test
public void testReverse() {
assertEquals(Collections.emptyList(), getLib().reverse(null));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ public <T> List<T> remove(List<T> list, int position) {
throw new DMNRuntimeException("Not supported yet");
}

@Override
public <T> List<T> listReplace(List<T> list, Object position, T newItem) {
throw new DMNRuntimeException("Not supported yet");
}

@Override
public <T> List<T> reverse(List<T> list) {
throw new DMNRuntimeException("Not supported yet");
Expand Down
30 changes: 30 additions & 0 deletions dmn-tck-it/dmn-tck-it-translator/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2107,6 +2107,36 @@
</configuration>
</execution>

<execution>
<id>cl3-1155-list-replace-function-test</id>
<phase>generate-sources</phase>
<goals>
<goal>dmn-to-java</goal>
</goals>
<configuration>
<inputFileDirectory>${tck.15.diagram.folder}/cl3/1155-list-replace-function/translator/1155-list-replace-function.dmn</inputFileDirectory>
<outputFileDirectory>${generated.source.code.folder}</outputFileDirectory>
<inputParameters>
<javaRootPackage>${generated.root.package}.tck.cl3_1155_list_replace_function</javaRootPackage>
</inputParameters>
</configuration>
</execution>
<execution>
<id>test-cl3-1155-list-replace-function</id>
<phase>generate-sources</phase>
<goals>
<goal>tck-to-java</goal>
</goals>
<configuration>
<inputTestFileDirectory>${tck.15.diagram.folder}/cl3/1155-list-replace-function/translator/1155-list-replace-function-test-01.xml</inputTestFileDirectory>
<inputModelFileDirectory>${tck.15.diagram.folder}/cl3/1155-list-replace-function/translator/1155-list-replace-function.dmn</inputModelFileDirectory>
<outputFileDirectory>${generated.test.code.folder}</outputFileDirectory>
<inputParameters>
<javaRootPackage>${generated.root.package}.tck.cl3_1155_list_replace_function</javaRootPackage>
</inputParameters>
</configuration>
</execution>

<execution>
<id>cl3-9001-recursive-function</id>
<phase>generate-sources</phase>
Expand Down
Loading

0 comments on commit 925a234

Please sign in to comment.