You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kie.apache.org by ya...@apache.org on 2026/04/28 08:29:30 UTC
(incubator-kie-drools) branch main updated: [incubator-kie-issues#2174] Unit test cases for Dialect Handler classes (#6632)
This is an automated email from the ASF dual-hosted git repository.
yesamer pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie-drools.git
The following commit(s) were added to refs/heads/main by this push:
new 68caddd2da [incubator-kie-issues#2174] Unit test cases for Dialect Handler classes (#6632)
68caddd2da is described below
commit 68caddd2dad3310fe8094583eb84a1ec01eb7d7f
Author: ChinchuAjith <Ch...@ibm.com>
AuthorDate: Tue Apr 28 13:59:26 2026 +0530
[incubator-kie-issues#2174] Unit test cases for Dialect Handler classes (#6632)
* Unit test cases fro Dialect Handler classes
* Added logic to handle duration/period in multiplication and division scenarios
* Added logic to handle exponentials in BFeel
* Arranging tests in the order of precedence
* Fixing review comments
* Fixing review comments
---
.../ast/dialectHandlers/BFEELDialectHandler.java | 213 +++++++++++++--
.../dialectHandlers/BFEELDialectHandlerTest.java | 294 +++++++++++++++++++++
.../dialectHandlers/DefaultDialectHandlerTest.java | 93 +++++++
.../dialectHandlers/DialectHandlerFactoryTest.java | 120 +++++++++
.../dialectHandlers/FEELDialectHandlerTest.java | 140 ++++++++++
5 files changed, 845 insertions(+), 15 deletions(-)
diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/ast/dialectHandlers/BFEELDialectHandler.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/ast/dialectHandlers/BFEELDialectHandler.java
index 626c94cff7..0ec669f737 100644
--- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/ast/dialectHandlers/BFEELDialectHandler.java
+++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/ast/dialectHandlers/BFEELDialectHandler.java
@@ -19,6 +19,7 @@
package org.kie.dmn.feel.lang.ast.dialectHandlers;
import java.math.BigDecimal;
+import java.math.MathContext;
import java.time.Duration;
import java.time.LocalDate;
import java.time.chrono.ChronoPeriod;
@@ -33,7 +34,10 @@ import org.kie.dmn.feel.lang.FEELDialect;
import org.kie.dmn.feel.lang.ast.infixexecutors.EqExecutor;
import org.kie.dmn.feel.lang.types.impl.ComparablePeriod;
+import ch.obermuhlner.math.big.BigDecimalMath;
+
import static org.kie.dmn.feel.lang.ast.infixexecutors.InfixExecutorUtils.getString;
+import static org.kie.dmn.feel.util.NumberEvalHelper.getBigDecimalOrNull;
/**
* Handler implementation of the DialectHandler interface providing BFEEL specific
@@ -51,7 +55,23 @@ public class BFEELDialectHandler extends DefaultDialectHandler implements Dialec
public Map<CheckedPredicate, BiFunction<Object, Object, Object>> getAddOperations(EvaluationContext ctx) {
Map<CheckedPredicate, BiFunction<Object, Object, Object>> map = new LinkedHashMap<>();
- // any String operand → concatenate both as strings
+ // B-FEEL Precedence Table for Addition (DMN 1.6 Section 11.9):
+ // Row 1: STRING - The non-string value is converted to a string using the string() B-FEEL function
+ // and Table 58 applies. Subtraction returns an empty string.
+ // Row 2: NUMBER - The non-number value is converted to a number using the number() B-FEEL function
+ // and Table 58 applies.
+ // Row 3: DATE AND TIME - The non-date and time value is converted to a duration using the duration()
+ // B-FEEL function and Table 58 applies.
+ // Row 4: DATE - The non-date value is converted to a duration using the duration() B-FEEL function
+ // and Table 58 applies.
+ // Row 5: TIME - The non-time value is converted to a duration using the duration() B-FEEL function
+ // and Table 58 applies.
+ // Row 6: YEARS AND MONTHS DURATION - The non-years and months duration value is converted to a
+ // duration using the duration() B-FEEL function and Table 58 applies.
+ // Row 7: DAYS AND TIME DURATION - The non-days and time duration value is converted to a duration
+ // using the duration() B-FEEL function and Table 58 applies.
+
+ // Row 1: STRING - any String operand → concatenate both as strings (subtraction returns "")
map.put(
new CheckedPredicate((left, right) -> left instanceof String || right instanceof String, false),
(left, right) -> {
@@ -60,20 +80,64 @@ public class BFEELDialectHandler extends DefaultDialectHandler implements Dialec
return leftNum + rightNum;
});
- // date + number → return the number
+ // Row 2: NUMBER - date + number → convert date to 0, then add: 0 + number = number
+ // Since 0 + number = number, we return the number operand directly
map.put(
new CheckedPredicate((left, right) -> left instanceof LocalDate && right instanceof Number, false),
(left, right) -> right);
+
+ // Row 2: NUMBER - number + date → convert date to 0, then add: number + 0 = number
+ // Since number + 0 = number, we return the number operand directly
map.put(
new CheckedPredicate((left, right) -> left instanceof Number && right instanceof LocalDate, false),
(left, right) -> left);
- // Number + null → number
+ // Row 2: NUMBER - duration + number → convert duration to seconds, then add
+ map.put(
+ new CheckedPredicate((left, right) -> left instanceof Duration && right instanceof Number, false),
+ (left, right) -> {
+ BigDecimal seconds = getBigDecimalOrNull(((Duration) left).getSeconds());
+ BigDecimal rightNum = getBigDecimalOrNull(right);
+ if (seconds == null || rightNum == null) return rightNum != null ? rightNum : BigDecimal.ZERO;
+ return seconds.add(rightNum, MathContext.DECIMAL128);
+ });
+
+ // Row 2: NUMBER - number + duration → convert duration to seconds, then add
+ map.put(
+ new CheckedPredicate((left, right) -> left instanceof Number && right instanceof Duration, false),
+ (left, right) -> {
+ BigDecimal leftNum = getBigDecimalOrNull(left);
+ BigDecimal seconds = getBigDecimalOrNull(((Duration) right).getSeconds());
+ if (leftNum == null || seconds == null) return leftNum != null ? leftNum : BigDecimal.ZERO;
+ return leftNum.add(seconds, MathContext.DECIMAL128);
+ });
+
+ // Row 2: NUMBER - period + number → convert period to months, then add
+ map.put(
+ new CheckedPredicate((left, right) -> left instanceof ChronoPeriod && right instanceof Number, false),
+ (left, right) -> {
+ BigDecimal months = getBigDecimalOrNull(ComparablePeriod.toTotalMonths((ChronoPeriod) left));
+ BigDecimal rightNum = getBigDecimalOrNull(right);
+ if (months == null || rightNum == null) return rightNum != null ? rightNum : BigDecimal.ZERO;
+ return months.add(rightNum, MathContext.DECIMAL128);
+ });
+
+ // Row 2: NUMBER - number + period → convert period to months, then add
+ map.put(
+ new CheckedPredicate((left, right) -> left instanceof Number && right instanceof ChronoPeriod, false),
+ (left, right) -> {
+ BigDecimal leftNum = getBigDecimalOrNull(left);
+ BigDecimal months = getBigDecimalOrNull(ComparablePeriod.toTotalMonths((ChronoPeriod) right));
+ if (leftNum == null || months == null) return leftNum != null ? leftNum : BigDecimal.ZERO;
+ return leftNum.add(months, MathContext.DECIMAL128);
+ });
+
+ // Number + null → number (null converts to 0 in B-FEEL numeric context)
map.put(
new CheckedPredicate((left, right) -> left instanceof Number && right == null, false),
(left, right) -> left);
- // null + Number → number
+ // null + Number → number (null converts to 0 in B-FEEL numeric context)
map.put(
new CheckedPredicate((left, right) -> left == null && right instanceof Number, false),
(left, right) -> right);
@@ -344,19 +408,69 @@ public class BFEELDialectHandler extends DefaultDialectHandler implements Dialec
/**
* Builds the BFeel specific 'Power' operations.
- *
+ * <p>
+ * B-FEEL exponentiation rules:
+ * <ul>
+ * <li>Implicit Coercion: Both operands are converted to numbers using B-FEEL number() function</li>
+ * <li>Error Handling: Returns 0 instead of null for semantic errors or out-of-range scale</li>
+ * <li>Precision: Result limited to 34 digits of precision</li>
+ * <li>Range: Exponent must be in range [-999,999,999..999,999,999]</li>
+ * <li>Precedence: -4 ** 2 is interpreted as (-4) ** 2 = 16 (handled by parser)</li>
+ * </ul>
+ *
* @param ctx : Current Evaluation context
* @return : a Map of CheckedPredicate to BiFunction representing the BFeel specific 'Power' operations
*/
@Override
public Map<CheckedPredicate, BiFunction<Object, Object, Object>> getPowOperations(EvaluationContext ctx) {
Map<CheckedPredicate, BiFunction<Object, Object, Object>> map = new LinkedHashMap<>();
- //TODO Change pow behaviour for BFeel
+
+ // B-FEEL: Implicit coercion to numbers, return 0 on error instead of null
map.put(
- new CheckedPredicate((left, right) -> left instanceof String || right instanceof String, false),
- (left, right) -> Boolean.FALSE);
+ new CheckedPredicate((left, right) -> true, false),
+ (left, right) -> {
+ // Implicit coercion: convert both operands to numbers using B-FEEL number() semantics
+ BigDecimal leftNum = getBigDecimalOrNull(left);
+ BigDecimal rightNum = getBigDecimalOrNull(right);
+
+ // B-FEEL implicit coercion: Duration → seconds
+ if (leftNum == null && left instanceof Duration) {
+ leftNum = BigDecimal.valueOf(((Duration) left).getSeconds());
+ }
+ if (rightNum == null && right instanceof Duration) {
+ rightNum = BigDecimal.valueOf(((Duration) right).getSeconds());
+ }
+
+ // B-FEEL implicit coercion: ChronoPeriod → total months
+ if (leftNum == null && left instanceof ChronoPeriod) {
+ leftNum = getBigDecimalOrNull(ComparablePeriod.toTotalMonths((ChronoPeriod) left));
+ }
+ if (rightNum == null && right instanceof ChronoPeriod) {
+ rightNum = getBigDecimalOrNull(ComparablePeriod.toTotalMonths((ChronoPeriod) right));
+ }
+
+ // B-FEEL returns 0 instead of null for invalid operations
+ if (leftNum == null || rightNum == null) {
+ return BigDecimal.ZERO;
+ }
+
+ // Check exponent range: [-999,999,999..999,999,999]
+ BigDecimal minExponent = new BigDecimal("-999999999");
+ BigDecimal maxExponent = new BigDecimal("999999999");
+ if (rightNum.compareTo(minExponent) < 0 || rightNum.compareTo(maxExponent) > 0) {
+ return BigDecimal.ZERO; // Out of range, return 0
+ }
+
+ try {
+ // Apply standard FEEL semantics with 34 digits precision (DECIMAL128)
+ BigDecimal result = BigDecimalMath.pow(leftNum, rightNum, MathContext.DECIMAL128);
+ return result;
+ } catch (ArithmeticException e) {
+ // B-FEEL returns 0 for arithmetic errors instead of null
+ return BigDecimal.ZERO;
+ }
+ });
- map.putAll(getCommonPowOperations(ctx));
return map;
}
@@ -415,13 +529,39 @@ public class BFEELDialectHandler extends DefaultDialectHandler implements Dialec
new CheckedPredicate((left, right) -> left == null && right instanceof ChronoPeriod, false),
(left, right) -> ComparablePeriod.ofMonths(0));
+ // Period × Duration (mixed types) → B-FEEL Section 11.10: YMD has higher precedence than DTD
+ // Convert Duration to seconds (as number), result is Period
+ map.put(
+ new CheckedPredicate((left, right) -> left instanceof ChronoPeriod && right instanceof Duration, false),
+ (left, right) -> {
+ BigDecimal months = getBigDecimalOrNull(ComparablePeriod.toTotalMonths((ChronoPeriod) left));
+ BigDecimal seconds = getBigDecimalOrNull(((Duration) right).getSeconds());
+ if (months == null || seconds == null)
+ return ComparablePeriod.ofMonths(0);
+ return ComparablePeriod.ofMonths(months.multiply(seconds, MathContext.DECIMAL128).intValue());
+ });
+
+ // Duration × Period (mixed types) → B-FEEL Section 11.10: YMD has higher precedence than DTD
+ // Convert Duration to seconds (as number), result is Period
+ map.put(
+ new CheckedPredicate((left, right) -> left instanceof Duration && right instanceof ChronoPeriod, false),
+ (left, right) -> {
+ BigDecimal seconds = getBigDecimalOrNull(((Duration) left).getSeconds());
+ BigDecimal months = getBigDecimalOrNull(ComparablePeriod.toTotalMonths((ChronoPeriod) right));
+ if (seconds == null || months == null)
+ return ComparablePeriod.ofMonths(0);
+ return ComparablePeriod.ofMonths(seconds.multiply(months, MathContext.DECIMAL128).intValue());
+ });
+
+ // Duration × Duration (same type) → returns zero duration (B-FEEL default for disallowed operation)
map.put(
new CheckedPredicate((left, right) -> left instanceof Duration && right instanceof Duration, false),
- (left, right) -> null);
+ (left, right) -> Duration.ZERO);
+ // Period × Period (same type) → returns zero period (B-FEEL default for disallowed operation)
map.put(
new CheckedPredicate((left, right) -> left instanceof ChronoPeriod && right instanceof ChronoPeriod, false),
- (left, right) -> null);
+ (left, right) -> ComparablePeriod.ofMonths(0));
map.putAll(getCommonMultOperations(ctx));
return map;
}
@@ -445,13 +585,25 @@ public class BFEELDialectHandler extends DefaultDialectHandler implements Dialec
map.put(new CheckedPredicate((l, r) -> l instanceof String && r instanceof String, false),
(l, r) -> BigDecimal.ZERO);
- // number ÷ duration → invalid → null
+ // number ÷ duration → B-FEEL implicit coercion: convert duration to seconds, then divide
map.put(new CheckedPredicate((l, r) -> l instanceof Number && r instanceof Duration, false),
- (l, r) -> null);
+ (l, r) -> {
+ BigDecimal leftNum = getBigDecimalOrNull(l);
+ BigDecimal rightSecs = getBigDecimalOrNull(((Duration) r).getSeconds());
+ if (leftNum == null || rightSecs == null || rightSecs.compareTo(BigDecimal.ZERO) == 0)
+ return BigDecimal.ZERO; // B-FEEL returns 0 instead of null
+ return leftNum.divide(rightSecs, MathContext.DECIMAL128);
+ });
- // number ÷ period → invalid → null
+ // number ÷ period → B-FEEL implicit coercion: convert period to total months, then divide
map.put(new CheckedPredicate((l, r) -> l instanceof Number && r instanceof ChronoPeriod, false),
- (l, r) -> null);
+ (l, r) -> {
+ BigDecimal leftNum = getBigDecimalOrNull(l);
+ BigDecimal rightMonths = getBigDecimalOrNull(ComparablePeriod.toTotalMonths((ChronoPeriod) r));
+ if (leftNum == null || rightMonths == null || rightMonths.compareTo(BigDecimal.ZERO) == 0)
+ return BigDecimal.ZERO; // B-FEEL returns 0 instead of null
+ return leftNum.divide(rightMonths, MathContext.DECIMAL128);
+ });
// duration ÷ null → Duration.ZERO
map.put(new CheckedPredicate((l, r) -> l instanceof Duration && r == null, false),
@@ -473,6 +625,37 @@ public class BFEELDialectHandler extends DefaultDialectHandler implements Dialec
map.put(new CheckedPredicate((l, r) -> l == null && r instanceof Number, false),
(l, r) -> BigDecimal.ZERO);
+ // number ÷ number (including division by zero) → B-FEEL returns BigDecimal.ZERO for division by zero
+ map.put(new CheckedPredicate((l, r) -> l instanceof Number && r instanceof Number, false),
+ (l, r) -> {
+ BigDecimal leftBD = getBigDecimalOrNull(l);
+ BigDecimal rightBD = getBigDecimalOrNull(r);
+ if (leftBD == null || rightBD == null || rightBD.compareTo(BigDecimal.ZERO) == 0) {
+ return BigDecimal.ZERO; // B-FEEL returns 0 for division by zero
+ }
+ return leftBD.divide(rightBD, MathContext.DECIMAL128);
+ });
+
+ // duration ÷ duration → number (B-FEEL: returns 0 instead of null when divisor is zero)
+ map.put(new CheckedPredicate((l, r) -> l instanceof Duration && r instanceof Duration, false),
+ (l, r) -> {
+ BigDecimal leftSecs = getBigDecimalOrNull(((Duration) l).getSeconds());
+ BigDecimal rightSecs = getBigDecimalOrNull(((Duration) r).getSeconds());
+ if (leftSecs == null || rightSecs == null || rightSecs.compareTo(BigDecimal.ZERO) == 0)
+ return BigDecimal.ZERO; // B-FEEL returns 0 instead of null
+ return leftSecs.divide(rightSecs, MathContext.DECIMAL128);
+ });
+
+ // period ÷ period → number (B-FEEL: returns 0 instead of null when divisor is zero)
+ map.put(new CheckedPredicate((l, r) -> l instanceof ChronoPeriod && r instanceof ChronoPeriod, false),
+ (l, r) -> {
+ BigDecimal leftMonths = getBigDecimalOrNull(ComparablePeriod.toTotalMonths((ChronoPeriod) l));
+ BigDecimal rightMonths = getBigDecimalOrNull(ComparablePeriod.toTotalMonths((ChronoPeriod) r));
+ if (leftMonths == null || rightMonths == null || rightMonths.compareTo(BigDecimal.ZERO) == 0)
+ return BigDecimal.ZERO; // B-FEEL returns 0 instead of null
+ return leftMonths.divide(rightMonths, MathContext.DECIMAL128);
+ });
+
map.putAll(getCommonDivisionOperations(ctx));
return map;
}
diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/lang/ast/dialectHandlers/BFEELDialectHandlerTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/lang/ast/dialectHandlers/BFEELDialectHandlerTest.java
new file mode 100644
index 0000000000..532a009471
--- /dev/null
+++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/lang/ast/dialectHandlers/BFEELDialectHandlerTest.java
@@ -0,0 +1,294 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); 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.kie.dmn.feel.lang.ast.dialectHandlers;
+
+import java.math.BigDecimal;
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.Period;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.kie.dmn.feel.lang.EvaluationContext;
+import org.kie.dmn.feel.lang.types.impl.ComparablePeriod;
+import org.kie.dmn.feel.util.EvaluationContextTestUtil;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit tests for BFEELDialectHandler - BFEEL uses type coercion and returns default values
+ */
+class BFEELDialectHandlerTest {
+
+ private BFEELDialectHandler handler;
+ private EvaluationContext ctx;
+
+ @BeforeEach
+ void setUp() {
+ handler = new BFEELDialectHandler();
+ ctx = EvaluationContextTestUtil.newEmptyEvaluationContext();
+ }
+
+ @Test
+ void testAdditionWithPrecedenceOrder() {
+ //Basic addition operation
+ assertThat(handler.executeAdd(10, 20, ctx)).isEqualTo(new BigDecimal("30"));
+ assertThat(handler.executeAdd(10, null, ctx)).isEqualTo(10);
+ // Case 1 : STRING - If either operand is a string, convert non-string to string
+ // Left: string + other
+ assertThat(handler.executeAdd("Hello", 123, ctx)).isEqualTo("Hello123");
+ assertThat(handler.executeAdd("Date:", LocalDate.of(2024, 1, 1), ctx)).isEqualTo("Date:2024-01-01");
+ assertThat(handler.executeAdd("Duration:", Duration.ofDays(5), ctx)).isEqualTo("Duration:PT120H");
+ assertThat(handler.executeAdd("Period:", Period.ofMonths(3), ctx)).isEqualTo("Period:P3M");
+
+ // Right: other + string
+ assertThat(handler.executeAdd(123, "World", ctx)).isEqualTo("123World");
+ assertThat(handler.executeAdd(LocalDate.of(2024, 1, 1), " is the date", ctx)).isEqualTo("2024-01-01 is the date");
+ assertThat(handler.executeAdd(Duration.ofDays(5), " duration", ctx)).isEqualTo("PT120H duration");
+ assertThat(handler.executeAdd(Period.ofMonths(3), " period", ctx)).isEqualTo("P3M period");
+
+ // Case 2 : NUMBER - If either operand is a number (and neither is string), convert non-number to number
+ // Per BFEEL Section 11.3: number(date) = 0, number(duration) = seconds, number(period) = months
+
+ // Left: date + number → 0 + number = number
+ assertThat(handler.executeAdd(LocalDate.of(2024, 1, 1), 5, ctx)).isEqualTo(5);
+ assertThat(handler.executeAdd(LocalDate.of(2024, 1, 1), new BigDecimal("10.5"), ctx))
+ .isEqualTo(new BigDecimal("10.5"));
+
+ // Right: number + date → number + 0 = number
+ assertThat(handler.executeAdd(5, LocalDate.of(2024, 1, 1), ctx)).isEqualTo(5);
+ assertThat(handler.executeAdd(new BigDecimal("10.5"), LocalDate.of(2024, 1, 1), ctx))
+ .isEqualTo(new BigDecimal("10.5"));
+
+ // Duration + number → seconds + number (Row 2: NUMBER precedence)
+ assertThat(handler.executeAdd(Duration.ofSeconds(10), 5, ctx)).isEqualTo(new BigDecimal("15"));
+ assertThat(handler.executeAdd(5, Duration.ofSeconds(10), ctx)).isEqualTo(new BigDecimal("15"));
+
+ // Period + number → months + number (Row 2: NUMBER precedence)
+ assertThat(handler.executeAdd(Period.ofMonths(12), 3, ctx)).isEqualTo(new BigDecimal("15"));
+ assertThat(handler.executeAdd(3, Period.ofMonths(12), ctx)).isEqualTo(new BigDecimal("15"));
+
+ // Case 3: DATE - If either operand is a date, convert non-date to duration
+ // Left: date + duration
+ assertThat(handler.executeAdd(LocalDate.of(2024, 1, 1), Duration.ofDays(5), ctx))
+ .isEqualTo(LocalDate.of(2024, 1, 6));
+ assertThat(handler.executeAdd(LocalDate.of(2024, 1, 1), Period.ofMonths(2), ctx))
+ .isEqualTo(LocalDate.of(2024, 3, 1));
+ assertThat(handler.executeAdd("Hello", null, ctx)).isEqualTo("Hello");
+ }
+
+ @Test
+ void testSubtractionWithPrecedenceOrder() {
+ // Basic subtraction operation
+ assertThat(handler.executeSub(20, 10, ctx)).isEqualTo(new BigDecimal("10"));
+
+ // Case 1: STRING - If either operand is a string, subtraction returns empty string ""
+ // Left: string - other
+ assertThat(handler.executeSub("Hello", 123, ctx)).isEqualTo("");
+ assertThat(handler.executeSub("Date", LocalDate.of(2024, 1, 1), ctx)).isEqualTo("");
+ assertThat(handler.executeSub("Duration", Duration.ofDays(5), ctx)).isEqualTo("");
+
+ // Right: other - string
+ assertThat(handler.executeSub(123, "World", ctx)).isEqualTo("");
+ assertThat(handler.executeSub(LocalDate.of(2024, 1, 1), "date", ctx)).isEqualTo("");
+ assertThat(handler.executeSub(Duration.ofDays(5), "duration", ctx)).isEqualTo("");
+
+ // Case 2: NUMBER - If either operand is a number (and neither is string), convert non-number to number
+ // Standard numeric subtraction applies after conversion
+ assertThat(handler.executeSub(10, 5, ctx)).isEqualTo(new BigDecimal("5"));
+ assertThat(handler.executeSub(new BigDecimal("20.5"), new BigDecimal("10.5"), ctx))
+ .isEqualTo(new BigDecimal("10.0"));
+
+ // Case 3: DATE - If either operand is a date, convert non-date to duration
+ // Left: date - duration
+ assertThat(handler.executeSub(LocalDate.of(2024, 1, 10), Duration.ofDays(5), ctx))
+ .isEqualTo(LocalDate.of(2024, 1, 5));
+ assertThat(handler.executeSub(LocalDate.of(2024, 3, 1), Period.ofMonths(1), ctx))
+ .isEqualTo(LocalDate.of(2024, 2, 1));
+
+ //Case 4 : DAYS AND TIME DURATION - Duration subtraction
+ assertThat(handler.executeSub(Duration.ofDays(10), Duration.ofDays(3), ctx))
+ .isEqualTo(Duration.ofDays(7));
+ }
+
+ @Test
+ void testMultiplication() {
+ // basic multiplication operation
+ assertThat(handler.executeMult(5, 10, ctx)).isEqualTo(new BigDecimal("50"));
+
+ // multiplication - invalid types
+ assertThat(handler.executeMult("test", 5, ctx)).isEqualTo(BigDecimal.ZERO);
+ assertThat(handler.executeMult(Duration.ofDays(5), null, ctx)).isEqualTo(Duration.ZERO);
+ assertThat(handler.executeMult(Period.ofMonths(3), null, ctx)).isEqualTo(ComparablePeriod.ofMonths(0));
+
+ // Duration × Duration (same type) → zero duration (B-FEEL default)
+ assertThat(handler.executeMult(Duration.ofDays(5), Duration.ofDays(3), ctx)).isEqualTo(Duration.ZERO);
+ assertThat(handler.executeMult(Period.ofMonths(6), Period.ofMonths(2), ctx)).isEqualTo(ComparablePeriod.ofMonths(0));
+
+ // YEARS AND MONTHS DURATION (Period) × Number
+ // Left: period × number
+ assertThat(handler.executeMult(Period.ofMonths(12), 2, ctx))
+ .isEqualTo(ComparablePeriod.ofMonths(24));
+ // Right: number × period
+ assertThat(handler.executeMult(2, Period.ofMonths(12), ctx))
+ .isEqualTo(ComparablePeriod.ofMonths(24));
+
+ // DAYS AND TIME DURATION (Duration) × Number
+ // Left: duration × number
+ assertThat(handler.executeMult(Duration.ofDays(5), 2, ctx))
+ .isEqualTo(Duration.ofDays(10));
+ // Right: number × duration
+ assertThat(handler.executeMult(2, Duration.ofDays(5), ctx))
+ .isEqualTo(Duration.ofDays(10));
+
+ // Period × Duration: B-FEEL Section 11.10 - YMD has higher precedence than DTD
+ // Duration converted to seconds (as number), result is Period
+ // P1Y (12 months) × PT1S (1 second) = P12M
+ assertThat(handler.executeMult(Period.ofYears(1), Duration.ofSeconds(1), ctx))
+ .isEqualTo(ComparablePeriod.ofMonths(12));
+
+ // Duration × Period: B-FEEL Section 11.10 - YMD has higher precedence than DTD
+ // Duration converted to seconds (as number), result is Period
+ // PT1S (1 second) × P1Y (12 months) = P12M (or P1Y)
+ assertThat(handler.executeMult(Duration.ofSeconds(1), Period.ofYears(1), ctx))
+ .isEqualTo(ComparablePeriod.ofMonths(12));
+ }
+
+ @Test
+ void testDivision() {
+ // basic division operation
+ assertThat(handler.executeDivision(20, 4, ctx)).isEqualTo(new BigDecimal("5"));
+ assertThat(handler.executeDivision(10, 0, ctx)).isEqualTo(BigDecimal.ZERO); // Division by zero returns 0
+
+ // null or non-numeric left operand
+ assertThat(handler.executeDivision("test", 5, ctx)).isEqualTo(BigDecimal.ZERO);
+ assertThat(handler.executeDivision(null, 5, ctx)).isEqualTo(BigDecimal.ZERO);
+ assertThat(handler.executeDivision(Duration.ofDays(5), null, ctx)).isEqualTo(Duration.ZERO);
+ assertThat(handler.executeDivision(Period.ofMonths(3), null, ctx)).isEqualTo(ComparablePeriod.ofMonths(0));
+
+ // Number ÷ Duration: Duration converted to seconds
+ assertThat(handler.executeDivision(10, Duration.ofDays(5), ctx)).isInstanceOf(BigDecimal.class);
+ assertThat(((BigDecimal) handler.executeDivision(10, Duration.ofDays(5), ctx)).compareTo(BigDecimal.ZERO))
+ .isGreaterThan(0);
+
+ // Number ÷ Period: Period converted to months
+ assertThat(handler.executeDivision(26, Period.ofYears(2), ctx)).isInstanceOf(BigDecimal.class);
+ BigDecimal result = (BigDecimal) handler.executeDivision(26, Period.ofYears(2), ctx);
+ assertThat(result.compareTo(new BigDecimal("1.08"))).isGreaterThan(0);
+ assertThat(result.compareTo(new BigDecimal("1.09"))).isLessThan(0);
+
+ // Duration ÷ Duration → number (returns ratio)
+ assertThat(handler.executeDivision(Duration.ofSeconds(10), Duration.ofSeconds(5), ctx))
+ .isEqualTo(new BigDecimal("2"));
+ assertThat(handler.executeDivision(Duration.ofSeconds(10), Duration.ZERO, ctx))
+ .isEqualTo(BigDecimal.ZERO);
+
+ // Period ÷ Period → number (returns ratio)
+ assertThat(handler.executeDivision(Period.ofYears(2), Period.ofYears(1), ctx))
+ .isEqualTo(new BigDecimal("2"));
+ assertThat(handler.executeDivision(Period.ofMonths(12), Period.ofMonths(0), ctx))
+ .isEqualTo(BigDecimal.ZERO);
+
+ // division by zero
+ assertThat(handler.executeDivision(10, Duration.ZERO, ctx)).isEqualTo(BigDecimal.ZERO);
+ assertThat(handler.executeDivision(10, Period.ofMonths(0), ctx)).isEqualTo(BigDecimal.ZERO);
+ }
+
+ @Test
+ void testExponentiation() {
+ // basic cases
+ assertThat(handler.executePow(2, 3, ctx)).isEqualTo(new BigDecimal("8"));
+ assertThat(handler.executePow(4, 2, ctx)).isEqualTo(new BigDecimal("16"));
+ assertThat(handler.executePow(-4, 2, ctx)).isEqualTo(new BigDecimal("16"));
+ assertThat(handler.executePow(-2, 3, ctx)).isEqualTo(new BigDecimal("-8"));
+
+ // testExponentiation_ImplicitCoercion
+ assertThat(handler.executePow("2", 3, ctx)).isEqualTo(new BigDecimal("8"));
+ assertThat(handler.executePow(2, "3", ctx)).isEqualTo(new BigDecimal("8"));
+ assertThat(handler.executePow(Duration.ofSeconds(2), 3, ctx)).isEqualTo(new BigDecimal("8"));
+
+ // testExponentiation_ErrorHandling
+ assertThat(handler.executePow(null, 2, ctx)).isEqualTo(BigDecimal.ZERO);
+ assertThat(handler.executePow(2, null, ctx)).isEqualTo(BigDecimal.ZERO);
+ assertThat(handler.executePow("invalid", 2, ctx)).isEqualTo(BigDecimal.ZERO);
+ assertThat(handler.executePow(2, "invalid", ctx)).isEqualTo(BigDecimal.ZERO);
+
+ // testExponentiation_RangeValidation
+ assertThat(handler.executePow(2, 999999999, ctx)).isInstanceOf(BigDecimal.class);
+ assertThat(handler.executePow(2, -999999999, ctx)).isInstanceOf(BigDecimal.class);
+ assertThat(handler.executePow(2, new BigDecimal("1000000000"), ctx)).isEqualTo(BigDecimal.ZERO);
+ assertThat(handler.executePow(2, new BigDecimal("-1000000000"), ctx)).isEqualTo(BigDecimal.ZERO);
+
+ // testExponentiation_FractionalExponents
+ BigDecimal sqrtResult = (BigDecimal) handler.executePow(4, 0.5, ctx);
+ assertThat(sqrtResult.compareTo(new BigDecimal("2"))).isEqualTo(0);
+ assertThat(handler.executePow(8, new BigDecimal("0.333333333333333"), ctx))
+ .isInstanceOf(BigDecimal.class);
+
+ // testExponentiation_BoundaryCases
+ assertThat(handler.executePow(0, 5, ctx)).isEqualTo(BigDecimal.ZERO);
+ assertThat(handler.executePow(5, 0, ctx)).isEqualTo(BigDecimal.ONE);
+ assertThat(handler.executePow(1, 1000, ctx)).isEqualTo(BigDecimal.ONE);
+ }
+
+ @Test
+ void testLogicalOperations() {
+ // LogicalOperations_And
+ assertThat(handler.executeAnd(Boolean.TRUE, Boolean.TRUE, ctx)).isEqualTo(Boolean.TRUE);
+ assertThat(handler.executeAnd(Boolean.TRUE, Boolean.FALSE, ctx)).isEqualTo(Boolean.FALSE);
+ assertThat(handler.executeAnd("test", Boolean.TRUE, ctx)).isEqualTo(Boolean.FALSE);
+ assertThat(handler.executeAnd("notBoolean", "alsoNotBoolean", ctx)).isEqualTo(Boolean.FALSE);
+
+ // LogicalOperations_Or
+ assertThat(handler.executeOr(Boolean.TRUE, Boolean.FALSE, ctx)).isEqualTo(Boolean.TRUE);
+ assertThat(handler.executeOr(Boolean.FALSE, Boolean.FALSE, ctx)).isEqualTo(Boolean.FALSE);
+ }
+
+ @Test
+ void testComparisonOperations() {
+ // Comparison_Equality
+ assertThat(handler.executeEqual(null, null, ctx)).isEqualTo(Boolean.TRUE);
+ assertThat(handler.executeEqual(10, 10, ctx)).isEqualTo(Boolean.TRUE);
+ assertThat(handler.executeEqual(10, 20, ctx)).isEqualTo(Boolean.FALSE);
+ assertThat(handler.executeEqual("10", 10, ctx)).isEqualTo(Boolean.FALSE);
+ assertThat(handler.executeEqual(null, 10, ctx)).isEqualTo(Boolean.FALSE);
+
+ assertThat(handler.executeNotEqual(null, null, ctx)).isEqualTo(Boolean.FALSE);
+ assertThat(handler.executeNotEqual(10, 20, ctx)).isEqualTo(Boolean.TRUE);
+
+ // Comparison_Relational
+ assertThat(handler.executeGte(Boolean.FALSE, Boolean.FALSE, ctx)).isEqualTo(Boolean.FALSE);
+ assertThat(handler.executeGte("test", Boolean.TRUE, ctx)).isEqualTo(Boolean.TRUE);
+ assertThat(handler.executeGte(20, 10, ctx)).isEqualTo(Boolean.FALSE);
+ assertThat(handler.executeGte("test", 10, ctx)).isEqualTo(Boolean.FALSE);
+
+ assertThat(handler.executeGt(20, 10, ctx)).isEqualTo(Boolean.FALSE);
+ assertThat(handler.executeGt(new Object(), new Object(), ctx)).isEqualTo(Boolean.FALSE);
+ assertThat(handler.executeLte(5, 10, ctx)).isEqualTo(Boolean.FALSE);
+ assertThat(handler.executeLt(5, 10, ctx)).isEqualTo(Boolean.FALSE);
+
+ // Comparison_CompareMethod
+ assertThat(handler.compare(null, null, (l, r) -> l.compareTo(r) > 0)).isEqualTo(Boolean.FALSE);
+ assertThat(handler.compare(20, 10, (l, r) -> l.compareTo(r) > 0)).isTrue();
+ assertThat(handler.compare("test", "test", (l, r) -> l.compareTo(r) == 0)).isTrue();
+ assertThat(handler.compare("test", 10, (l, r) -> l.compareTo(r) > 0)).isEqualTo(Boolean.FALSE);
+ }
+
+}
+
diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/lang/ast/dialectHandlers/DefaultDialectHandlerTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/lang/ast/dialectHandlers/DefaultDialectHandlerTest.java
new file mode 100644
index 0000000000..032b4ab02a
--- /dev/null
+++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/lang/ast/dialectHandlers/DefaultDialectHandlerTest.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); 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.kie.dmn.feel.lang.ast.dialectHandlers;
+
+import java.time.LocalTime;
+import java.time.Period;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.BiPredicate;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Unit tests for DefaultDialectHandler static utility methods and inner classes.
+ *
+ * Note: This is an abstract class. Its instance methods are tested through
+ * concrete implementations (FEELDialectHandler and BFEELDialectHandler):
+ *
+ * This test class focuses on:
+ * - Static utility methods (isEqual, compare with fallback suppliers)
+ * - Inner classes (CheckedPredicate)
+ */
+class DefaultDialectHandlerTest {
+
+ @Test
+ void testIsEqual_StaticMethod() {
+ // Basic equality
+ assertThat(DefaultDialectHandler.isEqual(null, null, () -> Boolean.FALSE, () -> null))
+ .isEqualTo(Boolean.FALSE);
+ assertThat(DefaultDialectHandler.isEqual(10, 10, () -> null, () -> null)).isTrue();
+ assertThat(DefaultDialectHandler.isEqual(10, 20, () -> null, () -> null)).isFalse();
+ assertThat(DefaultDialectHandler.isEqual("test", "test", () -> null, () -> null)).isTrue();
+ assertThat(DefaultDialectHandler.isEqual("test1", "test2", () -> null, () -> null)).isFalse();
+
+ // Singleton collection and element
+ List<Integer> list = Arrays.asList(10);
+ assertThat(DefaultDialectHandler.isEqual(list, 10, () -> null, () -> null)).isTrue();
+ assertThat(DefaultDialectHandler.isEqual(10, list, () -> null, () -> null)).isTrue();
+
+ // Temporal types
+ assertThat(DefaultDialectHandler.isEqual(Period.ofMonths(6), Period.ofMonths(6), () -> null, () -> null))
+ .isTrue();
+ assertThat(DefaultDialectHandler.isEqual(LocalTime.of(14, 30), LocalTime.of(14, 30), () -> null, () -> null))
+ .isTrue();
+
+ // Null fallback suppliers throw exception
+ assertThatThrownBy(() -> DefaultDialectHandler.isEqual(10, 20, null, () -> null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Fallback suppliers must not be null");
+
+ assertThatThrownBy(() -> DefaultDialectHandler.isEqual(10, 20, () -> null, null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Fallback suppliers must not be null");
+ }
+
+ @Test
+ void testCheckedPredicate() {
+ BiPredicate<Object, Object> predicate = (l, r) -> l instanceof Number && r instanceof Number;
+ DefaultDialectHandler.CheckedPredicate cp1 = new DefaultDialectHandler.CheckedPredicate(predicate, false);
+ DefaultDialectHandler.CheckedPredicate cp2 = new DefaultDialectHandler.CheckedPredicate(predicate, true);
+
+ // Equality based on predicate only (not on checked flag)
+ assertThat(cp1).isEqualTo(cp2);
+ assertThat(cp1.hashCode()).isEqualTo(cp2.hashCode());
+
+ // Self equality
+ assertThat(cp1).isEqualTo(cp1);
+
+ // Not equal to different types
+ assertThat(cp1).isNotEqualTo("string");
+ assertThat(cp1).isNotEqualTo(null);
+ }
+}
+
diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/lang/ast/dialectHandlers/DialectHandlerFactoryTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/lang/ast/dialectHandlers/DialectHandlerFactoryTest.java
new file mode 100644
index 0000000000..904293f74c
--- /dev/null
+++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/lang/ast/dialectHandlers/DialectHandlerFactoryTest.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); 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.kie.dmn.feel.lang.ast.dialectHandlers;
+
+import org.junit.jupiter.api.Test;
+import org.kie.dmn.feel.lang.EvaluationContext;
+import org.kie.dmn.feel.lang.FEELDialect;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for DialectHandlerFactory.getHandler() method
+ */
+class DialectHandlerFactoryTest {
+
+ @Test
+ void testGetHandler_NullContextReturnsFEELHandler() {
+ DialectHandler handler = DialectHandlerFactory.getHandler(null);
+
+ assertThat(handler).isNotNull();
+ assertThat(handler).isInstanceOf(FEELDialectHandler.class);
+ }
+
+ @Test
+ void testGetHandler_NullDialectReturnsFEELHandler() {
+ EvaluationContext ctx = mock(EvaluationContext.class);
+ when(ctx.getFEELDialect()).thenReturn(null);
+
+ DialectHandler handler = DialectHandlerFactory.getHandler(ctx);
+
+ assertThat(handler).isNotNull();
+ assertThat(handler).isInstanceOf(FEELDialectHandler.class);
+ }
+
+ @Test
+ void testGetHandler_FEELDialectReturnsFEELHandler() {
+ EvaluationContext ctx = mock(EvaluationContext.class);
+ when(ctx.getFEELDialect()).thenReturn(FEELDialect.FEEL);
+
+ DialectHandler handler = DialectHandlerFactory.getHandler(ctx);
+
+ assertThat(handler).isNotNull();
+ assertThat(handler).isInstanceOf(FEELDialectHandler.class);
+ }
+
+ @Test
+ void testGetHandler_BFEELDialectReturnsBFEELHandler() {
+ EvaluationContext ctx = mock(EvaluationContext.class);
+ when(ctx.getFEELDialect()).thenReturn(FEELDialect.BFEEL);
+
+ DialectHandler handler = DialectHandlerFactory.getHandler(ctx);
+
+ assertThat(handler).isNotNull();
+ assertThat(handler).isInstanceOf(BFEELDialectHandler.class);
+ }
+
+ @Test
+ void testGetHandler_DifferentDialectsReturnDifferentHandlers() {
+ EvaluationContext ctxFEEL = mock(EvaluationContext.class);
+ when(ctxFEEL.getFEELDialect()).thenReturn(FEELDialect.FEEL);
+
+ EvaluationContext ctxBFEEL = mock(EvaluationContext.class);
+ when(ctxBFEEL.getFEELDialect()).thenReturn(FEELDialect.BFEEL);
+
+ DialectHandler handlerFEEL = DialectHandlerFactory.getHandler(ctxFEEL);
+ DialectHandler handlerBFEEL = DialectHandlerFactory.getHandler(ctxBFEEL);
+
+ assertThat(handlerFEEL).isInstanceOf(FEELDialectHandler.class);
+ assertThat(handlerBFEEL).isInstanceOf(BFEELDialectHandler.class);
+ assertThat(handlerFEEL).isNotSameAs(handlerBFEEL);
+ }
+
+ @Test
+ void testGetHandler_FEELCachesSingletonInstances() {
+ EvaluationContext ctx1 = mock(EvaluationContext.class);
+ when(ctx1.getFEELDialect()).thenReturn(FEELDialect.FEEL);
+
+ EvaluationContext ctx2 = mock(EvaluationContext.class);
+ when(ctx2.getFEELDialect()).thenReturn(FEELDialect.FEEL);
+
+ DialectHandler handler1 = DialectHandlerFactory.getHandler(ctx1);
+ DialectHandler handler2 = DialectHandlerFactory.getHandler(ctx2);
+
+ // Should return the same cached instance
+ assertThat(handler1).isSameAs(handler2);
+ }
+
+ @Test
+ void testGetHandler_BFEELCachesSingletonInstances() {
+ EvaluationContext ctx1 = mock(EvaluationContext.class);
+ when(ctx1.getFEELDialect()).thenReturn(FEELDialect.BFEEL);
+
+ EvaluationContext ctx2 = mock(EvaluationContext.class);
+ when(ctx2.getFEELDialect()).thenReturn(FEELDialect.BFEEL);
+
+ DialectHandler handler1 = DialectHandlerFactory.getHandler(ctx1);
+ DialectHandler handler2 = DialectHandlerFactory.getHandler(ctx2);
+
+ // Should return the same cached instance
+ assertThat(handler1).isSameAs(handler2);
+ }
+}
diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/lang/ast/dialectHandlers/FEELDialectHandlerTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/lang/ast/dialectHandlers/FEELDialectHandlerTest.java
new file mode 100644
index 0000000000..50a5de98a1
--- /dev/null
+++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/lang/ast/dialectHandlers/FEELDialectHandlerTest.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); 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.kie.dmn.feel.lang.ast.dialectHandlers;
+
+import java.math.BigDecimal;
+import java.time.Duration;
+import java.time.LocalDate;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.kie.dmn.feel.lang.EvaluationContext;
+import org.kie.dmn.feel.util.EvaluationContextTestUtil;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit tests for FEELDialectHandler - FEEL returns null for invalid operations
+ */
+class FEELDialectHandlerTest {
+
+ private FEELDialectHandler handler;
+ private EvaluationContext ctx;
+
+ @BeforeEach
+ void setUp() {
+ handler = new FEELDialectHandler();
+ ctx = EvaluationContextTestUtil.newEmptyEvaluationContext();
+ }
+
+ @Test
+ void testAddOperations() {
+ // Valid operations
+ assertThat(handler.executeAdd("Hello", "World", ctx)).isEqualTo("HelloWorld");
+ assertThat(handler.executeAdd(10, 20, ctx)).isEqualTo(new BigDecimal("30"));
+ assertThat(handler.executeAdd(LocalDate.of(2024, 1, 1), Duration.ofDays(5), ctx))
+ .isEqualTo(LocalDate.of(2024, 1, 6));
+
+ // Invalid operations return null
+ assertThat(handler.executeAdd("Hello", 123, ctx)).isNull();
+ assertThat(handler.executeAdd(LocalDate.of(2024, 1, 1), 5, ctx)).isNull();
+ assertThat(handler.executeAdd(10, null, ctx)).isNull();
+ }
+
+ @Test
+ void testLogicalOperations() {
+ // AND operations
+ assertThat(handler.executeAnd(Boolean.TRUE, Boolean.TRUE, ctx)).isEqualTo(Boolean.TRUE);
+ assertThat(handler.executeAnd(Boolean.TRUE, Boolean.FALSE, ctx)).isEqualTo(Boolean.FALSE);
+ assertThat(handler.executeAnd(Boolean.TRUE, null, ctx)).isNull();
+
+ // OR operations
+ assertThat(handler.executeOr(Boolean.TRUE, Boolean.FALSE, ctx)).isEqualTo(Boolean.TRUE);
+ assertThat(handler.executeOr(Boolean.FALSE, Boolean.FALSE, ctx)).isEqualTo(Boolean.FALSE);
+ assertThat(handler.executeOr(Boolean.FALSE, null, ctx)).isNull();
+ }
+
+ @Test
+ void testEqualityOperations() {
+ // Equal
+ assertThat(handler.executeEqual(null, null, ctx)).isEqualTo(Boolean.TRUE);
+ assertThat(handler.executeEqual(10, 10, ctx)).isEqualTo(Boolean.TRUE);
+ assertThat(handler.executeEqual(10, 20, ctx)).isEqualTo(Boolean.FALSE);
+ assertThat(handler.executeEqual("10", 10, ctx)).isNull(); // Different types
+
+ // Not Equal
+ assertThat(handler.executeNotEqual(null, null, ctx)).isEqualTo(Boolean.FALSE);
+ assertThat(handler.executeNotEqual(10, 20, ctx)).isEqualTo(Boolean.TRUE);
+ }
+
+ @Test
+ void testComparisonOperations() {
+ // Greater than or equal
+ assertThat(handler.executeGte(Boolean.TRUE, null, ctx)).isEqualTo(Boolean.TRUE);
+ assertThat(handler.executeGte(Boolean.FALSE, null, ctx)).isNull();
+ assertThat(handler.executeGte(20, 10, ctx)).isEqualTo(Boolean.TRUE);
+ assertThat(handler.executeGte(10, 10, ctx)).isEqualTo(Boolean.TRUE);
+
+ // Greater than
+ assertThat(handler.executeGt(20, 10, ctx)).isEqualTo(Boolean.TRUE);
+ assertThat(handler.executeGt(10, 10, ctx)).isEqualTo(Boolean.FALSE);
+
+ // Less than or equal
+ assertThat(handler.executeLte(5, 10, ctx)).isEqualTo(Boolean.TRUE);
+ assertThat(handler.executeLte(10, 10, ctx)).isEqualTo(Boolean.TRUE);
+
+ // Less than
+ assertThat(handler.executeLt(5, 10, ctx)).isEqualTo(Boolean.TRUE);
+ assertThat(handler.executeLt(10, 10, ctx)).isEqualTo(Boolean.FALSE);
+ }
+
+ @Test
+ void testArithmeticOperations() {
+ // Power
+ assertThat(handler.executePow("2", 3, ctx)).isNull(); // Invalid
+ assertThat(handler.executePow(2, 3, ctx)).isEqualTo(new BigDecimal("8"));
+
+ // Subtraction
+ assertThat(handler.executeSub("10", 5, ctx)).isNull(); // Invalid
+ assertThat(handler.executeSub(20, 10, ctx)).isEqualTo(new BigDecimal("10"));
+ assertThat(handler.executeSub(LocalDate.of(2024, 1, 10), Duration.ofDays(5), ctx))
+ .isEqualTo(LocalDate.of(2024, 1, 5));
+
+ // Multiplication
+ assertThat(handler.executeMult("test", 5, ctx)).isNull(); // Invalid
+ assertThat(handler.executeMult(5, 10, ctx)).isEqualTo(new BigDecimal("50"));
+ assertThat(handler.executeMult(Duration.ofDays(5), null, ctx)).isNull();
+ assertThat(handler.executeMult(Duration.ofDays(5), Duration.ofDays(3), ctx)).isNull();
+ assertThat(handler.executeMult(LocalDate.of(2024, 1, 1), 5, ctx)).isNull();
+
+ // Division
+ assertThat(handler.executeDivision("test", 5, ctx)).isNull(); // Invalid
+ assertThat(handler.executeDivision(20, 4, ctx)).isEqualTo(new BigDecimal("5"));
+ assertThat(handler.executeDivision(10, Duration.ofDays(5), ctx)).isNull();
+ assertThat(handler.executeDivision(Duration.ofDays(5), null, ctx)).isNull();
+ assertThat(handler.executeDivision(null, 5, ctx)).isNull();
+ }
+
+ @Test
+ void testCompareMethod() {
+ assertThat(handler.compare(null, null, (l, r) -> l.compareTo(r) > 0)).isNull();
+ assertThat(handler.compare(20, 10, (l, r) -> l.compareTo(r) > 0)).isTrue();
+ assertThat(handler.compare("test", "test", (l, r) -> l.compareTo(r) == 0)).isTrue();
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org