From b24a6258e6a4275485176f8069daddb9d2599334 Mon Sep 17 00:00:00 2001 From: Octavian Patrascoiu Date: Tue, 29 Oct 2024 16:12:19 +0000 Subject: [PATCH] [#655] DMN 1.5: Support for negative duration function (DMN15-86) --- .../semantics/FEELSemanticVisitor.java | 4 +- .../AbstractFEELInterpreterVisitor.java | 7 +- .../synthesis/FEELToTripleNativeVisitor.java | 11 +- .../dmn/feel/AbstractFEELProcessorTest.java | 35 ++++- .../AbstractCL3DMNInterpreterTest.java | 5 + .../java/com/gs/dmn/feel/lib/BaseFEELLib.java | 11 ++ .../dmn/feel/lib/type/time/DurationType.java | 2 + .../time/pure/TemporalAmountDurationType.java | 15 ++ .../type/time/xml/DefaultDurationType.java | 16 +++ .../lib/type/time/xml/DoubleDurationType.java | 16 +++ .../dmn/feel/lib/stub/DurationTypeStub.java | 5 + .../0099-arithmetic-negation-test-01.xml | 135 ++++++++++++++++++ .../0099-arithmetic-negation.dmn | 108 ++++++++++++++ .../translator/0099-arithmetic-negation.dmn | 108 ++++++++++++++ .../0099-arithmetic-negation-test-01.xml | 135 ++++++++++++++++++ .../pure/0099-arithmetic-negation-test-01.xml | 135 ++++++++++++++++++ 16 files changed, 736 insertions(+), 12 deletions(-) create mode 100644 dmn-test-cases/standard/tck/1.5/cl3/0099-arithmetic-negation/0099-arithmetic-negation-test-01.xml create mode 100644 dmn-test-cases/standard/tck/1.5/cl3/0099-arithmetic-negation/0099-arithmetic-negation.dmn create mode 100644 dmn-test-cases/standard/tck/1.5/cl3/0099-arithmetic-negation/translator/0099-arithmetic-negation.dmn create mode 100644 dmn-test-cases/standard/tck/1.5/cl3/0099-arithmetic-negation/translator/mixed/0099-arithmetic-negation-test-01.xml create mode 100644 dmn-test-cases/standard/tck/1.5/cl3/0099-arithmetic-negation/translator/pure/0099-arithmetic-negation-test-01.xml diff --git a/dmn-core/src/main/java/com/gs/dmn/feel/analysis/semantics/FEELSemanticVisitor.java b/dmn-core/src/main/java/com/gs/dmn/feel/analysis/semantics/FEELSemanticVisitor.java index fb3c0e729..5944008a1 100644 --- a/dmn-core/src/main/java/com/gs/dmn/feel/analysis/semantics/FEELSemanticVisitor.java +++ b/dmn-core/src/main/java/com/gs/dmn/feel/analysis/semantics/FEELSemanticVisitor.java @@ -648,12 +648,12 @@ public Element visit(ArithmeticNegation element, DMNContext context) // Derive type Type type = element.getLeftOperand().getType(); - element.setType(NUMBER); - if (type != NUMBER) { + if (type != NUMBER && !(type instanceof DurationType)) { handleError(context, element, String.format("Operator '%s' cannot be applied to '%s'", element.getOperator(), type)); } // Derive type + element.setType(type); return element; } diff --git a/dmn-core/src/main/java/com/gs/dmn/feel/interpreter/AbstractFEELInterpreterVisitor.java b/dmn-core/src/main/java/com/gs/dmn/feel/interpreter/AbstractFEELInterpreterVisitor.java index b0d33ffca..7d594ad74 100644 --- a/dmn-core/src/main/java/com/gs/dmn/feel/interpreter/AbstractFEELInterpreterVisitor.java +++ b/dmn-core/src/main/java/com/gs/dmn/feel/interpreter/AbstractFEELInterpreterVisitor.java @@ -725,7 +725,12 @@ public Object visit(ArithmeticNegation element, DMNContext context) { LOGGER.debug("Visiting element '{}'", element); Object leftOperand = element.getLeftOperand().accept(this, context); - return this.lib.numericUnaryMinus((NUMBER) leftOperand); + Type type = element.getType(); + if (type == NumberType.NUMBER) { + return this.lib.numericUnaryMinus((NUMBER) leftOperand); + } else { + return this.lib.durationUnaryMinus((DURATION) leftOperand); + } } @Override diff --git a/dmn-core/src/main/java/com/gs/dmn/feel/synthesis/FEELToTripleNativeVisitor.java b/dmn-core/src/main/java/com/gs/dmn/feel/synthesis/FEELToTripleNativeVisitor.java index ef327c194..a7789fb25 100644 --- a/dmn-core/src/main/java/com/gs/dmn/feel/synthesis/FEELToTripleNativeVisitor.java +++ b/dmn-core/src/main/java/com/gs/dmn/feel/synthesis/FEELToTripleNativeVisitor.java @@ -56,6 +56,8 @@ import java.util.*; import java.util.stream.Collectors; +import static com.gs.dmn.feel.analysis.semantics.type.NumberType.NUMBER; + public class FEELToTripleNativeVisitor extends AbstractFEELToJavaVisitor { private static final int INITIAL_VALUE = -1; private int filterCount = INITIAL_VALUE; @@ -366,7 +368,7 @@ public Triple visit(FilterExpression element, DMNContext context) { // Filter if (filterType == BooleanType.BOOLEAN) { return this.triples.makeCollectionLogicFilter(source, newParameterName, filter); - } else if (filterType == NumberType.NUMBER) { + } else if (filterType == NUMBER) { // Compute element type Type elementType; if (sourceType instanceof ListType) { @@ -514,7 +516,12 @@ public Triple visit(Exponentiation element, DMNContext context) { public Triple visit(ArithmeticNegation element, DMNContext context) { Expression leftOperand = element.getLeftOperand(); Triple leftOpd = (Triple) leftOperand.accept(this, context); - return this.triples.makeBuiltinFunctionInvocation("numericUnaryMinus", leftOpd); + Type type = element.getType(); + if (type == NUMBER) { + return this.triples.makeBuiltinFunctionInvocation("numericUnaryMinus", leftOpd); + } else { + return this.triples.makeBuiltinFunctionInvocation("durationUnaryMinus", leftOpd); + } } // diff --git a/dmn-core/src/test/java/com/gs/dmn/feel/AbstractFEELProcessorTest.java b/dmn-core/src/test/java/com/gs/dmn/feel/AbstractFEELProcessorTest.java index a7a4d3b2f..2481380cc 100644 --- a/dmn-core/src/test/java/com/gs/dmn/feel/AbstractFEELProcessorTest.java +++ b/dmn-core/src/test/java/com/gs/dmn/feel/AbstractFEELProcessorTest.java @@ -2192,8 +2192,12 @@ public void testExponentiation() { @Test public void testArithmeticNegation() { String number = "1"; + String yearsAndMonthsDuration = "@\"P1Y1M\""; + String daysAndTimeDuration = "@\"P1DT1H\""; + String duration = "duration(\"P1DT1H\")"; List entries = Collections.singletonList( - new EnvironmentEntry("input", NUMBER, this.lib.number("1"))); + new EnvironmentEntry("input", NUMBER, this.lib.number("1")) + ); doExpressionTest(entries, "", String.format("- %s", number), "ArithmeticNegation(NumericLiteral(1))", @@ -2201,37 +2205,54 @@ public void testArithmeticNegation() { "numericUnaryMinus(number(\"1\"))", this.lib.numericUnaryMinus(this.lib.number("1")), this.lib.number("-1")); - doExpressionTest(entries, "", String.format("-- %s", number), "NumericLiteral(1)", "number", "number(\"1\")", this.lib.number("1"), this.lib.number("1")); - doExpressionTest(entries, "", String.format("--- %s", number), "ArithmeticNegation(NumericLiteral(1))", "number", "numericUnaryMinus(number(\"1\"))", this.lib.numericUnaryMinus(this.lib.number("1")), this.lib.number("-1")); + + doExpressionTest(entries, "", String.format("- %s", yearsAndMonthsDuration), + "ArithmeticNegation(DateTimeLiteral(duration, \"P1Y1M\"))", + "years and months duration", + "durationUnaryMinus(duration(\"P1Y1M\"))", + this.lib.durationUnaryMinus(this.lib.duration("P1Y1M")), + this.lib.duration("-P1Y1M")); + doExpressionTest(entries, "", String.format("- %s", daysAndTimeDuration), + "ArithmeticNegation(DateTimeLiteral(duration, \"P1DT1H\"))", + "days and time duration", + "durationUnaryMinus(duration(\"P1DT1H\"))", + this.lib.durationUnaryMinus(this.lib.duration("P1DT1H")), + this.lib.duration("-PT25H")); + doExpressionTest(entries, "", String.format("- %s", duration), + "ArithmeticNegation(DateTimeLiteral(duration, \"P1DT1H\"))", + "days and time duration", + "durationUnaryMinus(duration(\"P1DT1H\"))", + this.lib.durationUnaryMinus(this.lib.duration("P1DT1H")), + this.lib.duration("-PT25H")); } @Test public void testArithmeticNegationOnIncorrectOperands() { Assertions.assertThrows(SemanticError.class, () -> { - String yearsAndMonthsDuration = "duration(\"P1Y1M\")"; - String daysAndTimeDuration = "duration(\"P1DT1H\")"; + String date = "date(\"2020-01-01\")"; + String time = "time(\"21:00:00\")"; List entries = Collections.singletonList( new EnvironmentEntry("input", NUMBER, this.lib.number("1"))); - doExpressionTest(entries, "", String.format("- %s", yearsAndMonthsDuration), + doExpressionTest(entries, "", String.format("- %s", date), "ArithmeticNegation(DateTimeLiteral(duration, \"P1Y1M\"))", "years and months duration", "numericUnaryMinus(duration(\"P1Y1M\"))", null, null); - doExpressionTest(entries, "", String.format("- %s", daysAndTimeDuration), + doExpressionTest(entries, "", String.format("- %s", time), "ArithmeticNegation(DateTimeLiteral(duration, \"P1DT1H\"))", "days and time duration", "numericUnaryMinus(duration(\"P1DT1H\"))", diff --git a/dmn-core/src/test/java/com/gs/dmn/runtime/interpreter/AbstractCL3DMNInterpreterTest.java b/dmn-core/src/test/java/com/gs/dmn/runtime/interpreter/AbstractCL3DMNInterpreterTest.java index ef354502a..069ba5847 100644 --- a/dmn-core/src/test/java/com/gs/dmn/runtime/interpreter/AbstractCL3DMNInterpreterTest.java +++ b/dmn-core/src/test/java/com/gs/dmn/runtime/interpreter/AbstractCL3DMNInterpreterTest.java @@ -622,6 +622,11 @@ public void test_15_cl3_0068_feel_equality() { doSingleModelTest("1.5", "0068-feel-equality", new Pair<>("strongTyping", "false") ); } + @Test + public void test_15_cl3_0099_arithmetic_negation() { + doSingleModelTest("1.5", "0099-arithmetic-negation"); + } + @Test public void test_15_cl3_1155_list_replace_function() { doSingleModelTest("1.5", "1155-list-replace-function"); diff --git a/dmn-runtime/src/main/java/com/gs/dmn/feel/lib/BaseFEELLib.java b/dmn-runtime/src/main/java/com/gs/dmn/feel/lib/BaseFEELLib.java index d3bf9d085..07a6e8d59 100644 --- a/dmn-runtime/src/main/java/com/gs/dmn/feel/lib/BaseFEELLib.java +++ b/dmn-runtime/src/main/java/com/gs/dmn/feel/lib/BaseFEELLib.java @@ -1113,6 +1113,17 @@ public DURATION durationDivideNumber(DURATION first, NUMBER second) { } } + @Override + public DURATION durationUnaryMinus(DURATION first) { + try { + return durationType.durationUnaryMinus(first); + } catch (Exception e) { + String message = String.format("durationUnaryMinus(%s", first); + logError(message, e); + return null; + } + } + // // List operators // diff --git a/dmn-runtime/src/main/java/com/gs/dmn/feel/lib/type/time/DurationType.java b/dmn-runtime/src/main/java/com/gs/dmn/feel/lib/type/time/DurationType.java index 2eea3611a..d9aa8b594 100644 --- a/dmn-runtime/src/main/java/com/gs/dmn/feel/lib/type/time/DurationType.java +++ b/dmn-runtime/src/main/java/com/gs/dmn/feel/lib/type/time/DurationType.java @@ -46,4 +46,6 @@ default boolean isDuration(Object value) { DURATION durationMultiplyNumber(DURATION first, NUMBER second); DURATION durationDivideNumber(DURATION first, NUMBER second); + + DURATION durationUnaryMinus(DURATION first); } diff --git a/dmn-runtime/src/main/java/com/gs/dmn/feel/lib/type/time/pure/TemporalAmountDurationType.java b/dmn-runtime/src/main/java/com/gs/dmn/feel/lib/type/time/pure/TemporalAmountDurationType.java index d99a5d231..c60c5e676 100644 --- a/dmn-runtime/src/main/java/com/gs/dmn/feel/lib/type/time/pure/TemporalAmountDurationType.java +++ b/dmn-runtime/src/main/java/com/gs/dmn/feel/lib/type/time/pure/TemporalAmountDurationType.java @@ -185,6 +185,21 @@ public TemporalAmount durationDivideNumber(TemporalAmount first, Number second) } } + @Override + public TemporalAmount durationUnaryMinus(TemporalAmount first) { + if (first == null) { + return null; + } + + if (isDaysAndTimeDuration(first)) { + return ((Duration) first).negated(); + } else if (isYearsAndMonthsDuration(first)) { + return ((Period) first).negated(); + } else { + throw new DMNRuntimeException(String.format("Cannot negate '%s'", first)); + } + } + private Long value(Period duration) { return duration == null ? null : duration.toTotalMonths(); } diff --git a/dmn-runtime/src/main/java/com/gs/dmn/feel/lib/type/time/xml/DefaultDurationType.java b/dmn-runtime/src/main/java/com/gs/dmn/feel/lib/type/time/xml/DefaultDurationType.java index e02e0a359..75ad7a913 100644 --- a/dmn-runtime/src/main/java/com/gs/dmn/feel/lib/type/time/xml/DefaultDurationType.java +++ b/dmn-runtime/src/main/java/com/gs/dmn/feel/lib/type/time/xml/DefaultDurationType.java @@ -19,6 +19,7 @@ import javax.xml.datatype.Duration; import java.math.BigDecimal; import java.math.RoundingMode; +import java.time.Period; public class DefaultDurationType extends BaseDefaultDurationType implements DurationType { public DefaultDurationType() { @@ -110,6 +111,21 @@ public Duration durationDivideNumber(Duration first, BigDecimal second) { } } + @Override + public Duration durationUnaryMinus(Duration first) { + if (first == null) { + return null; + } + + if (isDaysAndTimeDuration(first)) { + return first.negate(); + } else if (isYearsAndMonthsDuration(first)) { + return first.negate(); + } else { + throw new DMNRuntimeException(String.format("Cannot negate '%s'", first)); + } + } + private BigDecimal multiplyNumbers(Long firstValue, BigDecimal second) { return BigDecimal.valueOf(firstValue).multiply(second); } diff --git a/dmn-runtime/src/main/java/com/gs/dmn/feel/lib/type/time/xml/DoubleDurationType.java b/dmn-runtime/src/main/java/com/gs/dmn/feel/lib/type/time/xml/DoubleDurationType.java index dc12826d1..396602170 100644 --- a/dmn-runtime/src/main/java/com/gs/dmn/feel/lib/type/time/xml/DoubleDurationType.java +++ b/dmn-runtime/src/main/java/com/gs/dmn/feel/lib/type/time/xml/DoubleDurationType.java @@ -17,6 +17,7 @@ import com.gs.dmn.runtime.DMNRuntimeException; import javax.xml.datatype.Duration; +import java.time.Period; public class DoubleDurationType extends BaseDefaultDurationType implements DurationType { public DoubleDurationType() { @@ -100,4 +101,19 @@ public Duration durationDivideNumber(Duration first, Double second) { throw new DMNRuntimeException(String.format("Cannot divide '%s' by '%s'", first, second)); } } + + @Override + public Duration durationUnaryMinus(Duration first) { + if (first == null) { + return null; + } + + if (isDaysAndTimeDuration(first)) { + return first.negate(); + } else if (isYearsAndMonthsDuration(first)) { + return first.negate(); + } else { + throw new DMNRuntimeException(String.format("Cannot negate '%s'", first)); + } + } } diff --git a/dmn-runtime/src/test/java/com/gs/dmn/feel/lib/stub/DurationTypeStub.java b/dmn-runtime/src/test/java/com/gs/dmn/feel/lib/stub/DurationTypeStub.java index 5f543c8a1..d3c10140c 100644 --- a/dmn-runtime/src/test/java/com/gs/dmn/feel/lib/stub/DurationTypeStub.java +++ b/dmn-runtime/src/test/java/com/gs/dmn/feel/lib/stub/DurationTypeStub.java @@ -90,4 +90,9 @@ public DURATION durationMultiplyNumber(DURATION first, NUMBER second) { public DURATION durationDivideNumber(DURATION first, NUMBER second) { throw new DMNRuntimeException("Not supported yet"); } + + @Override + public DURATION durationUnaryMinus(DURATION first) { + throw new DMNRuntimeException("Not supported yet"); + } } diff --git a/dmn-test-cases/standard/tck/1.5/cl3/0099-arithmetic-negation/0099-arithmetic-negation-test-01.xml b/dmn-test-cases/standard/tck/1.5/cl3/0099-arithmetic-negation/0099-arithmetic-negation-test-01.xml new file mode 100644 index 000000000..fa5af9301 --- /dev/null +++ b/dmn-test-cases/standard/tck/1.5/cl3/0099-arithmetic-negation/0099-arithmetic-negation-test-01.xml @@ -0,0 +1,135 @@ + + + + 0099-arithmetic-negation.dmn + + + negate number + + + -10 + + + + + + negate negative number + + + 10 + + + + + + negate days and time duration + + + -P1D + + + + + + negate a negative days and time duration + + + P1D + + + + + + negate years and months duration + + + -P1Y + + + + + + negate a negative years and months duration + + + P1Y + + + + + + negate date gives null + + + + + + + + + negate datetime gives null + + + + + + + + + negate time gives null + + + + + + + + + negate context gives null + + + + + + + + + negate string gives null + + + + + + + + + negate singleton list gives null + + + + + + + + + negate range gives null + + + + + + + + + negate expression result + + + -10 + + + + + diff --git a/dmn-test-cases/standard/tck/1.5/cl3/0099-arithmetic-negation/0099-arithmetic-negation.dmn b/dmn-test-cases/standard/tck/1.5/cl3/0099-arithmetic-negation/0099-arithmetic-negation.dmn new file mode 100644 index 000000000..a738f4ce8 --- /dev/null +++ b/dmn-test-cases/standard/tck/1.5/cl3/0099-arithmetic-negation/0099-arithmetic-negation.dmn @@ -0,0 +1,108 @@ + + + arithmetic negation + + + + + -10 + + + + + + + --10 + + + + + + + -@"P1D" + + + + + + + -@"-P1D" + + + + + + + -@"P1Y" + + + + + + + -@"-P1Y" + + + + + + + -@"2021-01-01" + + + + + + + -@"2021-01-01T10:10:10" + + + + + + + -@"10:10:10" + + + + + + + -{a: 1} + + + + + + + -"10" + + + + + + + -[10] + + + + + + + -[1..5] + + + + + + + -(function(a) a)(10) + + + + + diff --git a/dmn-test-cases/standard/tck/1.5/cl3/0099-arithmetic-negation/translator/0099-arithmetic-negation.dmn b/dmn-test-cases/standard/tck/1.5/cl3/0099-arithmetic-negation/translator/0099-arithmetic-negation.dmn new file mode 100644 index 000000000..a738f4ce8 --- /dev/null +++ b/dmn-test-cases/standard/tck/1.5/cl3/0099-arithmetic-negation/translator/0099-arithmetic-negation.dmn @@ -0,0 +1,108 @@ + + + arithmetic negation + + + + + -10 + + + + + + + --10 + + + + + + + -@"P1D" + + + + + + + -@"-P1D" + + + + + + + -@"P1Y" + + + + + + + -@"-P1Y" + + + + + + + -@"2021-01-01" + + + + + + + -@"2021-01-01T10:10:10" + + + + + + + -@"10:10:10" + + + + + + + -{a: 1} + + + + + + + -"10" + + + + + + + -[10] + + + + + + + -[1..5] + + + + + + + -(function(a) a)(10) + + + + + diff --git a/dmn-test-cases/standard/tck/1.5/cl3/0099-arithmetic-negation/translator/mixed/0099-arithmetic-negation-test-01.xml b/dmn-test-cases/standard/tck/1.5/cl3/0099-arithmetic-negation/translator/mixed/0099-arithmetic-negation-test-01.xml new file mode 100644 index 000000000..fa5af9301 --- /dev/null +++ b/dmn-test-cases/standard/tck/1.5/cl3/0099-arithmetic-negation/translator/mixed/0099-arithmetic-negation-test-01.xml @@ -0,0 +1,135 @@ + + + + 0099-arithmetic-negation.dmn + + + negate number + + + -10 + + + + + + negate negative number + + + 10 + + + + + + negate days and time duration + + + -P1D + + + + + + negate a negative days and time duration + + + P1D + + + + + + negate years and months duration + + + -P1Y + + + + + + negate a negative years and months duration + + + P1Y + + + + + + negate date gives null + + + + + + + + + negate datetime gives null + + + + + + + + + negate time gives null + + + + + + + + + negate context gives null + + + + + + + + + negate string gives null + + + + + + + + + negate singleton list gives null + + + + + + + + + negate range gives null + + + + + + + + + negate expression result + + + -10 + + + + + diff --git a/dmn-test-cases/standard/tck/1.5/cl3/0099-arithmetic-negation/translator/pure/0099-arithmetic-negation-test-01.xml b/dmn-test-cases/standard/tck/1.5/cl3/0099-arithmetic-negation/translator/pure/0099-arithmetic-negation-test-01.xml new file mode 100644 index 000000000..fa5af9301 --- /dev/null +++ b/dmn-test-cases/standard/tck/1.5/cl3/0099-arithmetic-negation/translator/pure/0099-arithmetic-negation-test-01.xml @@ -0,0 +1,135 @@ + + + + 0099-arithmetic-negation.dmn + + + negate number + + + -10 + + + + + + negate negative number + + + 10 + + + + + + negate days and time duration + + + -P1D + + + + + + negate a negative days and time duration + + + P1D + + + + + + negate years and months duration + + + -P1Y + + + + + + negate a negative years and months duration + + + P1Y + + + + + + negate date gives null + + + + + + + + + negate datetime gives null + + + + + + + + + negate time gives null + + + + + + + + + negate context gives null + + + + + + + + + negate string gives null + + + + + + + + + negate singleton list gives null + + + + + + + + + negate range gives null + + + + + + + + + negate expression result + + + -10 + + + + +