diff --git a/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/editor/util/CompletionProposal.java b/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/editor/util/CompletionProposal.java
deleted file mode 100644
index 27c3e49b74e..00000000000
--- a/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/editor/util/CompletionProposal.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * 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.apache.hop.pipeline.transforms.formula.editor.util;
-
-import lombok.Getter;
-import lombok.Setter;
-
-@Getter
-@Setter
-public class CompletionProposal {
- private String menuText;
- private String completionString;
- int offset;
-
- public CompletionProposal(String menuText, String completionString, int offset) {
- this.menuText = menuText;
- this.completionString = completionString;
- this.offset = offset;
- }
-}
diff --git a/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/util/FormulaParser.java b/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/util/FormulaParser.java
index 9dccc8de3fb..ea2b950f4ed 100644
--- a/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/util/FormulaParser.java
+++ b/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/util/FormulaParser.java
@@ -28,7 +28,6 @@
import org.apache.hop.core.variables.IVariables;
import org.apache.hop.pipeline.transforms.formula.FormulaMetaFunction;
import org.apache.hop.pipeline.transforms.formula.FormulaPoi;
-import org.apache.poi.hssf.usermodel.HSSFRichTextString;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellValue;
import org.apache.poi.ss.usermodel.Row;
@@ -108,12 +107,13 @@ public CellValue getFormulaValue() throws HopValueException {
IValueMeta fieldMeta = rowMeta.getValueMeta(fieldPosition);
if (dataRow[fieldPosition] != null) {
- if (fieldMeta.isString()) { // most common first to avoid a lot of "if" for nothing
+ // most common first to avoid a lot of "if" for nothing
+ if (fieldMeta.isString()) {
cell.setCellValue(rowMeta.getString(dataRow, fieldPosition));
} else if (fieldMeta.isBoolean()) {
cell.setCellValue(rowMeta.getBoolean(dataRow, fieldPosition));
} else if (fieldMeta.isBigNumber()) {
- cell.setCellValue(new HSSFRichTextString(rowMeta.getString(dataRow, fieldPosition)));
+ cell.setCellValue(rowMeta.getNumber(dataRow, fieldPosition));
} else if (fieldMeta.isDate()) {
cell.setCellValue(rowMeta.getDate(dataRow, fieldPosition));
} else if (fieldMeta.isInteger()) {
diff --git a/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/function/FunctionDescriptionTest.java b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/function/FunctionDescriptionTest.java
new file mode 100644
index 00000000000..ec8088ba16b
--- /dev/null
+++ b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/function/FunctionDescriptionTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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.apache.hop.pipeline.transforms.formula.function;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Collections;
+import java.util.List;
+import org.apache.hop.core.exception.HopXmlException;
+import org.apache.hop.core.xml.XmlHandler;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Node;
+
+/** Unit test for {@link FunctionDescription} */
+class FunctionDescriptionTest {
+
+ private static final String FUNCTION_XML =
+ """
+
+ ABS
+ %Category.Mathematical
+ Returns the absolute value of a number.
+ ABS( NUMBER N )
+ Number
+
+ If N < 0, returns -N.
+
+
+ ABS(2)
+ 2
+ 1
+ Positive values return unchanged.
+
+
+
+ """;
+
+ @Test
+ void constructorSetsAllFields() {
+ List examples = List.of(new FunctionExample("ABS(-2)", "2", "1", "Negation"));
+
+ FunctionDescription description =
+ new FunctionDescription(
+ "Math",
+ "ABS",
+ "Absolute value",
+ "ABS(N)",
+ "Number",
+ "N must be numeric",
+ "Returns |N|",
+ examples);
+
+ assertEquals("Math", description.getCategory());
+ assertEquals("ABS", description.getName());
+ assertEquals("Absolute value", description.getDescription());
+ assertEquals("ABS(N)", description.getSyntax());
+ assertEquals("Number", description.getReturns());
+ assertEquals("N must be numeric", description.getConstraints());
+ assertEquals("Returns |N|", description.getSemantics());
+ assertEquals(1, description.getFunctionExamples().size());
+ assertEquals("ABS(-2)", description.getFunctionExamples().getFirst().getExpression());
+ }
+
+ @Test
+ void parsesFromXmlNode() throws HopXmlException {
+ Node node = XmlHandler.loadXmlString(FUNCTION_XML, FunctionDescription.XML_TAG);
+ FunctionDescription description = new FunctionDescription(node);
+
+ assertEquals("ABS", description.getName());
+ assertEquals("%Category.Mathematical", description.getCategory());
+ assertEquals("Returns the absolute value of a number.", description.getDescription());
+ assertEquals("ABS( NUMBER N )", description.getSyntax());
+ assertEquals("Number", description.getReturns());
+ assertEquals("If N < 0, returns -N.", description.getSemantics());
+ assertEquals(1, description.getFunctionExamples().size());
+ assertEquals("ABS(2)", description.getFunctionExamples().getFirst().getExpression());
+ }
+
+ @Test
+ void parsesFromXmlNodeWithoutExamples() throws HopXmlException {
+ String xml =
+ """
+
+ NA
+ %Category.Information
+ Not available.
+
+ """;
+ FunctionDescription description =
+ new FunctionDescription(XmlHandler.loadXmlString(xml, FunctionDescription.XML_TAG));
+
+ assertEquals("NA", description.getName());
+ assertNotNull(description.getFunctionExamples());
+ assertTrue(description.getFunctionExamples().isEmpty());
+ }
+
+ @Test
+ void getHtmlReportIncludesAllSections() {
+ FunctionDescription description =
+ new FunctionDescription(
+ "Math",
+ "ABS",
+ "Absolute value",
+ "ABS(N)",
+ "Number",
+ "None",
+ "Standard abs",
+ List.of(new FunctionExample("ABS(-1)", "1", "1", "Sample")));
+
+ String html = description.getHtmlReport();
+
+ assertTrue(html.contains("ABS
"));
+ assertTrue(html.contains("Description:"));
+ assertTrue(html.contains("Absolute value"));
+ assertTrue(html.contains("Syntax:"));
+ assertTrue(html.contains("ABS(N)"));
+ assertTrue(html.contains("Returns:"));
+ assertTrue(html.contains("Constraints:"));
+ assertTrue(html.contains("Semantics:"));
+ assertTrue(html.contains("Examples:"));
+ assertTrue(html.contains("ABS(-1)"));
+ assertTrue(html.contains("Sample"));
+ }
+
+ @Test
+ void getHtmlReportOmitsEmptyOptionalSections() {
+ FunctionDescription description =
+ new FunctionDescription("Math", "MIN", "Minimum", "", "", "", "", Collections.emptyList());
+
+ String html = description.getHtmlReport();
+
+ assertTrue(html.contains("MIN
"));
+ assertTrue(html.contains("Minimum"));
+ assertFalse(html.contains("Syntax:"));
+ assertFalse(html.contains("Returns:"));
+ assertFalse(html.contains("Constraints:"));
+ assertFalse(html.contains("Semantics:"));
+ assertFalse(html.contains("Examples:"));
+ }
+
+ @Test
+ void getHtmlReportExampleWithoutComment() {
+ FunctionDescription description =
+ new FunctionDescription(
+ "Math",
+ "SUM",
+ "Sum",
+ "SUM(a,b)",
+ "Number",
+ "",
+ "",
+ List.of(new FunctionExample("SUM(1,2)", "3", "1", "")));
+
+ String html = description.getHtmlReport();
+
+ assertTrue(html.contains("SUM(1,2)"));
+ assertTrue(html.contains("3 | "));
+ }
+}
diff --git a/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/function/FunctionExampleTest.java b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/function/FunctionExampleTest.java
new file mode 100644
index 00000000000..483b09e2f19
--- /dev/null
+++ b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/function/FunctionExampleTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.apache.hop.pipeline.transforms.formula.function;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.apache.hop.core.exception.HopXmlException;
+import org.apache.hop.core.xml.XmlHandler;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Node;
+
+/** Unit test for {@link FunctionExample} */
+class FunctionExampleTest {
+
+ private static final String EXAMPLE_XML =
+ """
+
+ ABS(2)
+ 2
+ 1
+ Positive values return unchanged.
+
+ """;
+
+ @Test
+ void constructorSetsAllFields() {
+ FunctionExample example =
+ new FunctionExample("ABS(2)", "2", "1", "Positive values return unchanged.");
+
+ assertEquals("ABS(2)", example.getExpression());
+ assertEquals("2", example.getResult());
+ assertEquals("1", example.getLevel());
+ assertEquals("Positive values return unchanged.", example.getComment());
+ }
+
+ @Test
+ void parsesFromXmlNode() throws HopXmlException {
+ Node node = XmlHandler.loadXmlString(EXAMPLE_XML, FunctionExample.XML_TAG);
+ FunctionExample example = new FunctionExample(node);
+
+ assertEquals("ABS(2)", example.getExpression());
+ assertEquals("2", example.getResult());
+ assertEquals("1", example.getLevel());
+ assertEquals("Positive values return unchanged.", example.getComment());
+ }
+
+ @Test
+ void parsesFromXmlNodeWithMissingOptionalTags() throws HopXmlException {
+ String xml =
+ """
+
+ 1+1
+ 2
+
+ """;
+ FunctionExample example = new FunctionExample(XmlHandler.loadXmlString(xml, "example"));
+
+ assertEquals("1+1", example.getExpression());
+ assertEquals("2", example.getResult());
+ assertNull(example.getLevel());
+ assertNull(example.getComment());
+ }
+}
diff --git a/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/function/FunctionLibTest.java b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/function/FunctionLibTest.java
index ad3e659cdf2..9646aa9d87e 100644
--- a/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/function/FunctionLibTest.java
+++ b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/function/FunctionLibTest.java
@@ -24,11 +24,13 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.util.Arrays;
import java.util.List;
import org.apache.hop.core.exception.HopXmlException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+/** Unit test for {@link FunctionLib} */
class FunctionLibTest {
private FunctionLib functionLib;
@@ -137,7 +139,7 @@ void testInvalidXmlFile() {
void testFunctionDescriptionProperties() {
List functions = functionLib.getFunctions();
if (!functions.isEmpty()) {
- FunctionDescription firstFunction = functions.get(0);
+ FunctionDescription firstFunction = functions.getFirst();
assertNotNull(firstFunction.getName());
assertNotNull(firstFunction.getCategory());
// Description can be null/empty for some functions
@@ -153,4 +155,53 @@ void testSetFunctions() {
functionLib.setFunctions(originalFunctions);
assertEquals(originalSize, functionLib.getFunctions().size());
}
+
+ @Test
+ void getFunctionDescriptionIsCaseInsensitive() {
+ FunctionDescription upper = functionLib.getFunctionDescription("ABS");
+ FunctionDescription lower = functionLib.getFunctionDescription("abs");
+ FunctionDescription mixed = functionLib.getFunctionDescription("AbS");
+
+ assertNotNull(upper);
+ assertNotNull(lower);
+ assertNotNull(mixed);
+ assertEquals(upper.getName(), lower.getName());
+ assertEquals(upper.getName(), mixed.getName());
+ }
+
+ @Test
+ void getFunctionsForACategoryIsCaseInsensitive() {
+ FunctionDescription abs = functionLib.getFunctionDescription("ABS");
+ assertNotNull(abs);
+
+ String[] functions = functionLib.getFunctionsForACategory(abs.getCategory().toLowerCase());
+ assertTrue(Arrays.asList(functions).contains("ABS"));
+
+ for (int i = 1; i < functions.length; i++) {
+ assertTrue(functions[i - 1].compareTo(functions[i]) <= 0);
+ }
+ }
+
+ @Test
+ void getFunctionNamesCountMatchesLibrarySize() {
+ assertEquals(functionLib.getFunctions().size(), functionLib.getFunctionNames().length);
+ }
+
+ @Test
+ void getFunctionCategoriesAreUnique() {
+ String[] categories = functionLib.getFunctionCategories();
+ long distinct = Arrays.stream(categories).distinct().count();
+ assertEquals(categories.length, distinct);
+ }
+
+ @Test
+ void absFunctionLoadedFromXmlWithExamples() {
+ FunctionDescription abs = functionLib.getFunctionDescription("ABS");
+ assertNotNull(abs);
+ assertEquals("%Category.Mathematical", abs.getCategory());
+ assertFalse(abs.getDescription().isEmpty());
+ assertEquals(3, abs.getFunctionExamples().size());
+ assertNotNull(abs.getHtmlReport());
+ assertTrue(abs.getHtmlReport().contains("ABS(2)"));
+ }
}
diff --git a/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/util/FormulaFieldsExtractorTest.java b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/util/FormulaFieldsExtractorTest.java
index 96d839a3ac9..9aaa40ab39c 100644
--- a/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/util/FormulaFieldsExtractorTest.java
+++ b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/util/FormulaFieldsExtractorTest.java
@@ -24,17 +24,19 @@
import java.util.List;
import org.junit.jupiter.api.Test;
+/** Unit test for {@link FormulaFieldsExtractor} */
class FormulaFieldsExtractorTest {
@Test
void testSimpleFieldExtraction() {
- String formula = "[field1] + [field2]";
+ String formula = "[field1] + [field2] + [field3]";
List fields = FormulaFieldsExtractor.getFormulaFieldList(formula);
assertNotNull(fields);
- assertEquals(2, fields.size());
+ assertEquals(3, fields.size());
assertTrue(fields.contains("field1"));
assertTrue(fields.contains("field2"));
+ assertTrue(fields.contains("field3"));
}
@Test
@@ -44,7 +46,7 @@ void testSingleFieldExtraction() {
assertNotNull(fields);
assertEquals(1, fields.size());
- assertEquals("name", fields.get(0));
+ assertEquals("name", fields.getFirst());
}
@Test
@@ -106,7 +108,7 @@ void testUnmatchedBrackets() {
assertNotNull(fields);
assertEquals(1, fields.size());
- assertEquals("incomplete + [complete", fields.get(0));
+ assertEquals("incomplete + [complete", fields.getFirst());
}
@Test
@@ -142,4 +144,27 @@ void testSpecialCharactersInFieldNames() {
assertTrue(fields.contains("field_with_underscores"));
assertTrue(fields.contains("field.with.dots"));
}
+
+ @Test
+ void fieldOrderIsPreserved() {
+ List fields =
+ FormulaFieldsExtractor.getFormulaFieldList("[first] + [second] * [third]");
+
+ assertEquals(List.of("first", "second", "third"), fields);
+ }
+
+ @Test
+ void onlyOpenBracketWithoutClosingBracket() {
+ List fields = FormulaFieldsExtractor.getFormulaFieldList("[onlyOpen");
+
+ assertEquals(0, fields.size());
+ }
+
+ @Test
+ void trailingOpenBracketWithoutClose() {
+ List fields = FormulaFieldsExtractor.getFormulaFieldList("[complete] + [incomplete");
+
+ assertEquals(1, fields.size());
+ assertEquals("complete", fields.getFirst());
+ }
}
diff --git a/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/util/FormulaParserEvaluationTest.java b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/util/FormulaParserEvaluationTest.java
new file mode 100644
index 00000000000..aa85a431d11
--- /dev/null
+++ b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/util/FormulaParserEvaluationTest.java
@@ -0,0 +1,208 @@
+/*
+ * 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.apache.hop.pipeline.transforms.formula.util;
+
+import static org.apache.hop.pipeline.transforms.formula.util.FormulaFieldsExtractor.getFormulaFieldList;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.List;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaBigNumber;
+import org.apache.hop.core.row.value.ValueMetaBoolean;
+import org.apache.hop.core.row.value.ValueMetaInteger;
+import org.apache.hop.core.row.value.ValueMetaNumber;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.pipeline.transforms.formula.FormulaMetaFunction;
+import org.apache.hop.pipeline.transforms.formula.FormulaPoi;
+import org.apache.poi.ss.usermodel.CellType;
+import org.apache.poi.ss.usermodel.CellValue;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link FormulaParser} formula evaluation and field binding. */
+class FormulaParserEvaluationTest {
+
+ private FormulaPoi poi;
+
+ @AfterEach
+ void tearDown() throws Exception {
+ if (poi != null) {
+ poi.destroy();
+ poi = null;
+ }
+ }
+
+ @Test
+ void numberFieldGreaterThanLiteral() throws Exception {
+ CellValue result =
+ evaluate(List.of(field(new ValueMetaNumber("amount"), 150.0)), "[amount] > 100", false);
+
+ assertEquals(CellType.BOOLEAN, result.getCellType());
+ assertTrue(result.getBooleanValue());
+ }
+
+ @Test
+ void stringFieldEqualsLiteral() throws Exception {
+ CellValue result =
+ evaluate(
+ List.of(field(new ValueMetaString("status"), "active")),
+ "[status] = \"active\"",
+ false);
+
+ assertEquals(CellType.BOOLEAN, result.getCellType());
+ assertTrue(result.getBooleanValue());
+ }
+
+ @Test
+ void booleanFieldIsTrue() throws Exception {
+ CellValue result =
+ evaluate(
+ List.of(field(new ValueMetaBoolean("flag"), Boolean.TRUE)), "[flag] = TRUE", false);
+
+ assertEquals(CellType.BOOLEAN, result.getCellType());
+ assertTrue(result.getBooleanValue());
+ }
+
+ @Test
+ void bigNumberFieldGreaterThanLiteral() throws Exception {
+ CellValue result =
+ evaluate(
+ List.of(field(new ValueMetaBigNumber("amount"), new BigDecimal("200.5"))),
+ "[amount] > 100",
+ false);
+
+ assertEquals(CellType.BOOLEAN, result.getCellType());
+ assertTrue(result.getBooleanValue());
+ }
+
+ @Test
+ void nullFieldIsBlankWhenSetNaIsFalse() throws Exception {
+ CellValue result =
+ evaluate(
+ List.of(field(new ValueMetaInteger("amount"), null)),
+ "IF(ISBLANK([amount]), 1, 0)",
+ false);
+
+ assertEquals(CellType.NUMERIC, result.getCellType());
+ assertEquals(1.0, result.getNumberValue());
+ }
+
+ @Test
+ void nullFieldIsNaWhenSetNaIsTrue() throws Exception {
+ CellValue result =
+ evaluate(List.of(field(new ValueMetaInteger("amount"), null)), "ISNA([amount])", true);
+
+ assertEquals(CellType.BOOLEAN, result.getCellType());
+ assertTrue(result.getBooleanValue());
+ }
+
+ @Test
+ void twoIntegerFieldsAreSummed() throws Exception {
+ CellValue result =
+ evaluate(
+ List.of(field(new ValueMetaInteger("a"), 10L), field(new ValueMetaInteger("b"), 20L)),
+ "[a] + [b]",
+ false);
+
+ assertEquals(CellType.NUMERIC, result.getCellType());
+ assertEquals(30.0, result.getNumberValue());
+ }
+
+ @Test
+ void replaceMapRedirectsFormulaFieldToRealColumn() throws Exception {
+ RowMeta rowMeta = new RowMeta();
+ rowMeta.addValueMeta(new ValueMetaInteger("realAmount"));
+ Object[] row = new Object[] {42L};
+
+ HashMap replaceMap = new HashMap<>();
+ replaceMap.put("aliasAmount", "realAmount");
+
+ String formula = "[aliasAmount] * 2";
+ FormulaMetaFunction fn =
+ new FormulaMetaFunction("result", formula, IValueMeta.TYPE_INTEGER, -1, -1, "", false);
+
+ poi = new FormulaPoi(msg -> {});
+ FormulaParser parser =
+ new FormulaParser(
+ fn, rowMeta, row, poi, new Variables(), replaceMap, getFormulaFieldList(formula));
+
+ CellValue result = parser.getFormulaValue();
+ assertEquals(CellType.NUMERIC, result.getCellType());
+ assertEquals(84.0, result.getNumberValue());
+ }
+
+ @Test
+ void variablesAreResolvedBeforeEvaluation() throws Exception {
+ Variables variables = new Variables();
+ variables.setVariable("THRESHOLD", "50");
+
+ CellValue result =
+ evaluate(
+ variables,
+ List.of(field(new ValueMetaInteger("amount"), 60L)),
+ "[amount] > ${THRESHOLD}",
+ false);
+
+ assertEquals(CellType.BOOLEAN, result.getCellType());
+ assertTrue(result.getBooleanValue());
+ }
+
+ private CellValue evaluate(List bindings, String formula, boolean setNa)
+ throws Exception {
+ return evaluate(new Variables(), bindings, formula, setNa);
+ }
+
+ private CellValue evaluate(
+ Variables variables, List bindings, String formula, boolean setNa)
+ throws Exception {
+ RowMeta rowMeta = new RowMeta();
+ Object[] row = new Object[bindings.size()];
+ for (int i = 0; i < bindings.size(); i++) {
+ FieldBinding binding = bindings.get(i);
+ rowMeta.addValueMeta(binding.meta);
+ row[i] = binding.value;
+ }
+
+ FormulaMetaFunction fn =
+ new FormulaMetaFunction("result", formula, IValueMeta.TYPE_STRING, -1, -1, "", setNa);
+
+ poi = new FormulaPoi(msg -> {});
+ FormulaParser parser =
+ new FormulaParser(
+ fn,
+ rowMeta,
+ row,
+ poi,
+ variables,
+ new HashMap<>(),
+ getFormulaFieldList(variables.resolve(formula)));
+
+ return parser.getFormulaValue();
+ }
+
+ private static FieldBinding field(IValueMeta meta, Object value) {
+ return new FieldBinding(meta, value);
+ }
+
+ private record FieldBinding(IValueMeta meta, Object value) {}
+}
diff --git a/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/util/FormulaParserTest.java b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/util/FormulaParserTest.java
index 29e7942b0f0..97272580f05 100644
--- a/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/util/FormulaParserTest.java
+++ b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/util/FormulaParserTest.java
@@ -18,9 +18,11 @@
package org.apache.hop.pipeline.transforms.formula.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import java.math.BigDecimal;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.hop.core.BlockingRowSet;
@@ -39,8 +41,10 @@
import org.apache.hop.core.row.IValueMeta;
import org.apache.hop.core.row.RowMeta;
import org.apache.hop.core.row.value.ValueMetaBigNumber;
+import org.apache.hop.core.row.value.ValueMetaInteger;
import org.apache.hop.core.row.value.ValueMetaPluginType;
import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.core.variables.Variables;
import org.apache.hop.pipeline.Pipeline;
import org.apache.hop.pipeline.PipelineMeta;
import org.apache.hop.pipeline.config.PipelineRunConfiguration;
@@ -52,8 +56,12 @@
import org.apache.hop.pipeline.transforms.formula.FormulaData;
import org.apache.hop.pipeline.transforms.formula.FormulaMeta;
import org.apache.hop.pipeline.transforms.formula.FormulaMetaFunction;
+import org.apache.hop.pipeline.transforms.formula.FormulaPoi;
+import org.apache.poi.ss.usermodel.CellType;
+import org.apache.poi.ss.usermodel.CellValue;
import org.junit.jupiter.api.Test;
+/** Unit test for {@link FormulaParser} */
class FormulaParserTest {
static {
HopLogStore.setLogChannelFactory(new ConsoleLogChannelFactory());
@@ -163,6 +171,46 @@ public void rowWrittenEvent(final IRowMeta rowMeta, final Object[] row) {
assertEquals(maxSize, counter.get());
}
+ /**
+ * Regression for #7006: BigNumber fields
+ * must be passed to POI as numeric values so comparisons with numeric literals work.
+ */
+ @Test
+ void bigNumberComparesEqualToNumericLiteral() throws Exception {
+ assertTrue(
+ evaluateEqualsZero(new ValueMetaBigNumber("amount"), new Object[] {BigDecimal.ZERO}),
+ "BigNumber 0 should equal numeric literal 0 in a Formula expression");
+ }
+
+ @Test
+ void integerComparesEqualToNumericLiteral() throws Exception {
+ assertTrue(
+ evaluateEqualsZero(new ValueMetaInteger("amount"), new Object[] {0L}),
+ "Integer 0 should equal numeric literal 0 in a Formula expression");
+ }
+
+ private static boolean evaluateEqualsZero(IValueMeta valueMeta, Object[] row) throws Exception {
+ RowMeta rowMeta = new RowMeta();
+ rowMeta.addValueMeta(valueMeta);
+
+ FormulaMetaFunction fn =
+ new FormulaMetaFunction(
+ "result", "[amount] = 0", IValueMeta.TYPE_BOOLEAN, -1, -1, "", false);
+
+ FormulaPoi poi = new FormulaPoi(msg -> {});
+ try {
+ FormulaParser parser =
+ new FormulaParser(
+ fn, rowMeta, row, poi, new Variables(), new HashMap<>(), List.of("amount"));
+
+ CellValue cellValue = parser.getFormulaValue();
+ assertEquals(CellType.BOOLEAN, cellValue.getCellType());
+ return cellValue.getBooleanValue();
+ } finally {
+ poi.destroy();
+ }
+ }
+
private static class ConsoleLogChannelFactory implements ILogChannelFactory {
private final ILogChannel simpleLog =
new LogChannel("test") {
diff --git a/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/util/StringToTypeConverterTest.java b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/util/StringToTypeConverterTest.java
new file mode 100644
index 00000000000..7863a26f553
--- /dev/null
+++ b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/util/StringToTypeConverterTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.apache.hop.pipeline.transforms.formula.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/** Unit test for {@link StringToTypeConverter} */
+@ExtendWith(RestoreHopEngineEnvironmentExtension.class)
+class StringToTypeConverterTest {
+
+ private StringToTypeConverter converter;
+
+ @BeforeEach
+ void setUp() {
+ converter = new StringToTypeConverter();
+ }
+
+ @Test
+ void string2intPrimitiveMapsKnownTypes() throws Exception {
+ assertEquals(IValueMeta.TYPE_STRING, converter.string2intPrimitive("String"));
+ assertEquals(IValueMeta.TYPE_NUMBER, converter.string2intPrimitive("Number"));
+ assertEquals(IValueMeta.TYPE_INTEGER, converter.string2intPrimitive("Integer"));
+ assertEquals(IValueMeta.TYPE_BIGNUMBER, converter.string2intPrimitive("BigNumber"));
+ assertEquals(IValueMeta.TYPE_BOOLEAN, converter.string2intPrimitive("Boolean"));
+ assertEquals(IValueMeta.TYPE_DATE, converter.string2intPrimitive("Date"));
+ }
+
+ @Test
+ void string2intPrimitiveIsCaseInsensitive() throws Exception {
+ assertEquals(IValueMeta.TYPE_STRING, converter.string2intPrimitive("string"));
+ assertEquals(IValueMeta.TYPE_NUMBER, converter.string2intPrimitive("NUMBER"));
+ assertEquals(IValueMeta.TYPE_INTEGER, converter.string2intPrimitive("InTeGeR"));
+ }
+
+ @Test
+ void string2intPrimitiveReturnsNoneForUnknownType() throws Exception {
+ assertEquals(IValueMeta.TYPE_NONE, converter.string2intPrimitive("NotARealValueMetaType"));
+ assertEquals(IValueMeta.TYPE_NONE, converter.string2intPrimitive(null));
+ }
+}