This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 98f8f96c0c Pass BigNumber fields as numeric values in Formula 
transform (#7180)
98f8f96c0c is described below

commit 98f8f96c0c7336173805081d164bea5d83612359
Author: Lance <[email protected]>
AuthorDate: Fri May 29 21:33:08 2026 +0800

    Pass BigNumber fields as numeric values in Formula transform (#7180)
    
    Signed-off-by: lance <[email protected]>
---
 .../formula/editor/util/CompletionProposal.java    |  35 ----
 .../transforms/formula/util/FormulaParser.java     |   6 +-
 .../formula/function/FunctionDescriptionTest.java  | 177 ++++++++++++++++++
 .../formula/function/FunctionExampleTest.java      |  79 ++++++++
 .../formula/function/FunctionLibTest.java          |  53 +++++-
 .../formula/util/FormulaFieldsExtractorTest.java   |  33 +++-
 .../formula/util/FormulaParserEvaluationTest.java  | 208 +++++++++++++++++++++
 .../transforms/formula/util/FormulaParserTest.java |  48 +++++
 .../formula/util/StringToTypeConverterTest.java    |  61 ++++++
 9 files changed, 657 insertions(+), 43 deletions(-)

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 27c3e49b74..0000000000
--- 
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 9dccc8de3f..ea2b950f4e 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.row.IValueMeta;
 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 class FormulaParser {
 
       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 0000000000..ec8088ba16
--- /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 =
+      """
+      <function>
+        <name>ABS</name>
+        <category>%Category.Mathematical</category>
+        <description>Returns the absolute value of a number.</description>
+        <syntax>ABS( NUMBER N )</syntax>
+        <returns>Number</returns>
+        <constraints></constraints>
+        <semantics>If N &lt; 0, returns -N.</semantics>
+        <examples>
+          <example>
+            <expression>ABS(2)</expression>
+            <result>2</result>
+            <level>1</level>
+            <comment>Positive values return unchanged.</comment>
+          </example>
+        </examples>
+      </function>
+      """;
+
+  @Test
+  void constructorSetsAllFields() {
+    List<FunctionExample> 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 =
+        """
+        <function>
+          <name>NA</name>
+          <category>%Category.Information</category>
+          <description>Not available.</description>
+        </function>
+        """;
+    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("<H2>ABS</H2>"));
+    assertTrue(html.contains("<b><u>Description:</u></b>"));
+    assertTrue(html.contains("Absolute value"));
+    assertTrue(html.contains("<b><u>Syntax:</u></b>"));
+    assertTrue(html.contains("ABS(N)"));
+    assertTrue(html.contains("<b><u>Returns:</u></b>"));
+    assertTrue(html.contains("<b><u>Constraints:</u></b>"));
+    assertTrue(html.contains("<b><u>Semantics:</u></b>"));
+    assertTrue(html.contains("<b><u>Examples:</u></b>"));
+    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("<H2>MIN</H2>"));
+    assertTrue(html.contains("Minimum"));
+    assertFalse(html.contains("<b><u>Syntax:</u></b>"));
+    assertFalse(html.contains("<b><u>Returns:</u></b>"));
+    assertFalse(html.contains("<b><u>Constraints:</u></b>"));
+    assertFalse(html.contains("<b><u>Semantics:</u></b>"));
+    assertFalse(html.contains("<b><u>Examples:</u></b>"));
+  }
+
+  @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("<td>3</td>"));
+  }
+}
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 0000000000..483b09e2f1
--- /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 =
+      """
+                                       <example>
+                                         <expression>ABS(2)</expression>
+                                         <result>2</result>
+                                         <level>1</level>
+                                         <comment>Positive values return 
unchanged.</comment>
+                                       </example>
+                                       """;
+
+  @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 =
+        """
+                                               <example>
+                                                 <expression>1+1</expression>
+                                                 <result>2</result>
+                                               </example>
+                                               """;
+    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 ad3e659cdf..9646aa9d87 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.assertNull;
 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 @@ class FunctionLibTest {
   void testFunctionDescriptionProperties() {
     List<FunctionDescription> 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 @@ class FunctionLibTest {
     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 96d839a3ac..9aaa40ab39 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 static org.junit.jupiter.api.Assertions.assertTrue;
 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<String> 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 @@ class FormulaFieldsExtractorTest {
 
     assertNotNull(fields);
     assertEquals(1, fields.size());
-    assertEquals("name", fields.get(0));
+    assertEquals("name", fields.getFirst());
   }
 
   @Test
@@ -106,7 +108,7 @@ class FormulaFieldsExtractorTest {
 
     assertNotNull(fields);
     assertEquals(1, fields.size());
-    assertEquals("incomplete + [complete", fields.get(0));
+    assertEquals("incomplete + [complete", fields.getFirst());
   }
 
   @Test
@@ -142,4 +144,27 @@ class FormulaFieldsExtractorTest {
     assertTrue(fields.contains("field_with_underscores"));
     assertTrue(fields.contains("field.with.dots"));
   }
+
+  @Test
+  void fieldOrderIsPreserved() {
+    List<String> fields =
+        FormulaFieldsExtractor.getFormulaFieldList("[first] + [second] * 
[third]");
+
+    assertEquals(List.of("first", "second", "third"), fields);
+  }
+
+  @Test
+  void onlyOpenBracketWithoutClosingBracket() {
+    List<String> fields = 
FormulaFieldsExtractor.getFormulaFieldList("[onlyOpen");
+
+    assertEquals(0, fields.size());
+  }
+
+  @Test
+  void trailingOpenBracketWithoutClose() {
+    List<String> 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 0000000000..aa85a431d1
--- /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<String, String> 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<FieldBinding> bindings, String formula, 
boolean setNa)
+      throws Exception {
+    return evaluate(new Variables(), bindings, formula, setNa);
+  }
+
+  private CellValue evaluate(
+      Variables variables, List<FieldBinding> 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 29e7942b0f..97272580f0 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.IRowMeta;
 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.Formula;
 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 @@ class FormulaParserTest {
     assertEquals(maxSize, counter.get());
   }
 
+  /**
+   * Regression for <a 
href="https://github.com/apache/hop/issues/7006";>#7006</a>: 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 0000000000..7863a26f55
--- /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));
+  }
+}


Reply via email to