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 26fbc2693f Add URL Encode and Decode functions to the calculator, 
fixes #6044 (#6085)
26fbc2693f is described below

commit 26fbc2693f9648d1f2cf04c7618ead5e82b8e42d
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Thu Nov 27 10:17:15 2025 +0100

    Add URL Encode and Decode functions to the calculator, fixes #6044 (#6085)
---
 .../org/apache/hop/core/row/ValueDataUtil.java     |   27 +
 .../org/apache/hop/core/row/ValueDataUtilTest.java | 1254 ++++++++++++++++++++
 .../org/apache/hop/core/row/invalid-xml-sample.xml |   24 +
 .../org/apache/hop/core/row/txt-sample.txt         |    1 +
 .../org/apache/hop/core/row/xml-sample.xml         |   25 +
 .../ROOT/pages/pipeline/transforms/calculator.adoc |    2 +
 .../org/apache/hop/core/row/ValueDataUtilTest.java |  397 -------
 .../transforms/calculator/CalculationType.java     |    6 +-
 .../calculations/conversion/UrlDecode.java         |   51 +
 .../calculations/conversion/UrlEncode.java         |   51 +
 .../calculator/messages/messages_en_US.properties  |    2 +
 11 files changed, 1442 insertions(+), 398 deletions(-)

diff --git a/core/src/main/java/org/apache/hop/core/row/ValueDataUtil.java 
b/core/src/main/java/org/apache/hop/core/row/ValueDataUtil.java
index f66baac362..348f4fd473 100644
--- a/core/src/main/java/org/apache/hop/core/row/ValueDataUtil.java
+++ b/core/src/main/java/org/apache/hop/core/row/ValueDataUtil.java
@@ -29,10 +29,13 @@ import java.util.Locale;
 import java.util.zip.Adler32;
 import java.util.zip.CRC32;
 import java.util.zip.CheckedInputStream;
+import org.apache.commons.codec.DecoderException;
+import org.apache.commons.codec.EncoderException;
 import org.apache.commons.codec.language.DoubleMetaphone;
 import org.apache.commons.codec.language.Metaphone;
 import org.apache.commons.codec.language.RefinedSoundex;
 import org.apache.commons.codec.language.Soundex;
+import org.apache.commons.codec.net.URLCodec;
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang.StringUtils;
 import org.apache.commons.lang.WordUtils;
@@ -88,6 +91,8 @@ public class ValueDataUtil {
    */
   private static final int ROUND_2_MODE = readRound2Mode();
 
+  private static final URLCodec urlCodec = new URLCodec();
+
   private static int readRound2Mode() {
     int round2Mode = ROUND_2_MODE_DEFAULT_VALUE;
     final String rpaValue = System.getProperty(SYS_PROPERTY_ROUND_2_MODE);
@@ -2017,4 +2022,26 @@ public class ValueDataUtil {
         }
     }
   }
+
+  public static String urlEncode(IValueMeta metaA, Object dataA) {
+    if (dataA == null) {
+      return null;
+    }
+    try {
+      return urlCodec.encode(dataA.toString());
+    } catch (EncoderException e) {
+      return null;
+    }
+  }
+
+  public static String urlDecode(IValueMeta metaA, Object dataA) {
+    if (dataA == null) {
+      return null;
+    }
+    try {
+      return urlCodec.decode(dataA.toString());
+    } catch (DecoderException e) {
+      return null;
+    }
+  }
 }
diff --git a/core/src/test/java/org/apache/hop/core/row/ValueDataUtilTest.java 
b/core/src/test/java/org/apache/hop/core/row/ValueDataUtilTest.java
new file mode 100644
index 0000000000..afede510f9
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/core/row/ValueDataUtilTest.java
@@ -0,0 +1,1254 @@
+/*
+ * 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.core.row;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import org.apache.commons.lang.StringUtils;
+import org.apache.hop.core.exception.HopFileNotFoundException;
+import org.apache.hop.core.exception.HopValueException;
+import org.apache.hop.core.row.value.ValueMetaBigNumber;
+import org.apache.hop.core.row.value.ValueMetaBoolean;
+import org.apache.hop.core.row.value.ValueMetaDate;
+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.junit.Test;
+import org.mockito.stubbing.Answer;
+
+public class ValueDataUtilTest {
+
+  @Test
+  public void testPlus() throws HopValueException {
+
+    long longValue = 1;
+
+    assertEquals(
+        longValue,
+        ValueDataUtil.plus(
+            new ValueMetaInteger(), longValue, new ValueMetaString(), 
StringUtils.EMPTY));
+  }
+
+  @Test
+  public void checksumTest() throws Exception {
+    String path = getClass().getResource("txt-sample.txt").getPath();
+    String checksum = ValueDataUtil.createChecksum(new ValueMetaString(), 
path, "MD5", false);
+    assertEquals("098f6bcd4621d373cade4e832627b4f6", checksum);
+  }
+
+  @Test
+  public void checksumMissingFileTest() throws Exception {
+    String nonExistingFile = "nonExistingFile";
+    String checksum =
+        ValueDataUtil.createChecksum(new ValueMetaString(), nonExistingFile, 
"MD5", false);
+    assertNull(checksum);
+  }
+
+  @Test
+  public void checksumWithFailIfNoFileTest() throws Exception {
+    String path = getClass().getResource("txt-sample.txt").getPath();
+    String checksum = ValueDataUtil.createChecksum(new ValueMetaString(), 
path, "MD5", true);
+    assertEquals("098f6bcd4621d373cade4e832627b4f6", checksum);
+  }
+
+  @Test(expected = HopFileNotFoundException.class)
+  public void checksumFailIfNoFileTest() throws HopFileNotFoundException {
+    String nonExistingPath = "nonExistingPath";
+    ValueDataUtil.createChecksum(new ValueMetaString(), nonExistingPath, 
"MD5", true);
+  }
+
+  @Test
+  public void checksumNullPathNoFailTest() throws HopFileNotFoundException {
+    assertNull(ValueDataUtil.createChecksum(new ValueMetaString(), null, 
"MD5", false));
+  }
+
+  @Test
+  public void checksumNullPathFailTest() throws HopFileNotFoundException {
+    assertNull(ValueDataUtil.createChecksum(new ValueMetaString(), null, 
"MD5", true));
+  }
+
+  @Test
+  public void checksumCRC32Test() throws Exception {
+    String path = getClass().getResource("txt-sample.txt").getPath();
+    long checksum = ValueDataUtil.checksumCRC32(new ValueMetaString(), path, 
false);
+    assertEquals(3632233996l, checksum);
+  }
+
+  @Test
+  public void checksumCRC32MissingFileTest() throws Exception {
+    String nonExistingFile = "nonExistingFile";
+    long checksum = ValueDataUtil.checksumCRC32(new ValueMetaString(), 
nonExistingFile, false);
+    assertEquals(0, checksum);
+  }
+
+  @Test
+  public void checksumCRC32NoFailIfNoFileTest() throws 
HopFileNotFoundException {
+    String nonExistingPath = "nonExistingPath";
+    long checksum = ValueDataUtil.checksumCRC32(new ValueMetaString(), 
nonExistingPath, false);
+    assertEquals(0, checksum);
+  }
+
+  @Test(expected = HopFileNotFoundException.class)
+  public void checksumCRC32FailIfNoFileTest() throws HopFileNotFoundException {
+    String nonExistingPath = "nonExistingPath";
+    ValueDataUtil.checksumCRC32(new ValueMetaString(), nonExistingPath, true);
+  }
+
+  @Test
+  public void checksumCRC32NullPathNoFailTest() throws 
HopFileNotFoundException {
+    long checksum = ValueDataUtil.checksumCRC32(new ValueMetaString(), null, 
false);
+    assertEquals(0, checksum);
+  }
+
+  @Test
+  public void checksumCRC32NullPathFailTest() throws HopFileNotFoundException {
+    long checksum = ValueDataUtil.checksumCRC32(new ValueMetaString(), null, 
true);
+    assertEquals(0, checksum);
+  }
+
+  @Test
+  public void checksumAdlerWithFailIfNoFileTest() throws Exception {
+    String path = getClass().getResource("txt-sample.txt").getPath();
+    long checksum = ValueDataUtil.checksumAdler32(new ValueMetaString(), path, 
true);
+    assertEquals(73204161L, checksum);
+  }
+
+  @Test
+  public void checksumAdlerWithoutFailIfNoFileTest() throws Exception {
+    String path = getClass().getResource("txt-sample.txt").getPath();
+    long checksum = ValueDataUtil.checksumAdler32(new ValueMetaString(), path, 
false);
+    assertEquals(73204161L, checksum);
+  }
+
+  @Test
+  public void checksumAdlerNoFailIfNoFileTest() throws 
HopFileNotFoundException {
+    String nonExistingPath = "nonExistingPath";
+    long checksum = ValueDataUtil.checksumAdler32(new ValueMetaString(), 
nonExistingPath, false);
+    assertEquals(0, checksum);
+  }
+
+  @Test(expected = HopFileNotFoundException.class)
+  public void checksumAdlerFailIfNoFileTest() throws HopFileNotFoundException {
+    String nonExistingPath = "nonExistingPath";
+    ValueDataUtil.checksumAdler32(new ValueMetaString(), nonExistingPath, 
true);
+  }
+
+  @Test
+  public void checksumAdlerNullPathNoFailTest() throws 
HopFileNotFoundException {
+    long checksum = ValueDataUtil.checksumAdler32(new ValueMetaString(), null, 
false);
+    assertEquals(0, checksum);
+  }
+
+  @Test
+  public void checksumAdlerNullPathFailTest() throws HopFileNotFoundException {
+    long checksum = ValueDataUtil.checksumAdler32(new ValueMetaString(), null, 
true);
+    assertEquals(0, checksum);
+  }
+
+  @Test
+  public void xmlFileWellFormedTest() throws HopFileNotFoundException {
+    String xmlFilePath = getClass().getResource("xml-sample.xml").getPath();
+    boolean wellFormed =
+        ValueDataUtil.isXmlFileWellFormed(new ValueMetaString(), xmlFilePath, 
true);
+    assertTrue(wellFormed);
+  }
+
+  @Test
+  public void xmlFileBadlyFormedTest() throws HopFileNotFoundException {
+    String invalidXmlFilePath = 
getClass().getResource("invalid-xml-sample.xml").getPath();
+    boolean wellFormed =
+        ValueDataUtil.isXmlFileWellFormed(new ValueMetaString(), 
invalidXmlFilePath, true);
+    assertFalse(wellFormed);
+  }
+
+  @Test
+  public void xmlFileWellFormedWithoutFailIfNoFileTest() throws 
HopFileNotFoundException {
+    String xmlFilePath = getClass().getResource("xml-sample.xml").getPath();
+    boolean wellFormed =
+        ValueDataUtil.isXmlFileWellFormed(new ValueMetaString(), xmlFilePath, 
false);
+    assertTrue(wellFormed);
+  }
+
+  @Test
+  public void xmlFileBadlyFormedWithNoFailIfNoFileTest() throws 
HopFileNotFoundException {
+    String invalidXmlFilePath = 
getClass().getResource("invalid-xml-sample.xml").getPath();
+    boolean wellFormed =
+        ValueDataUtil.isXmlFileWellFormed(new ValueMetaString(), 
invalidXmlFilePath, false);
+    assertFalse(wellFormed);
+  }
+
+  @Test
+  public void xmlFileWellFormedNoFailIfNoFileTest() throws 
HopFileNotFoundException {
+    String nonExistingPath = "nonExistingPath";
+    boolean wellFormed =
+        ValueDataUtil.isXmlFileWellFormed(new ValueMetaString(), 
nonExistingPath, false);
+    assertFalse(wellFormed);
+  }
+
+  @Test(expected = HopFileNotFoundException.class)
+  public void xmlFileWellFormedFailIfNoFileTest() throws 
HopFileNotFoundException {
+    String nonExistingPath = "nonExistingPath";
+    ValueDataUtil.isXmlFileWellFormed(new ValueMetaString(), nonExistingPath, 
true);
+  }
+
+  @Test
+  public void xmlFileWellFormedNullPathNoFailTest() throws 
HopFileNotFoundException {
+    boolean wellFormed = ValueDataUtil.isXmlFileWellFormed(new 
ValueMetaString(), null, false);
+    assertFalse(wellFormed);
+  }
+
+  @Test
+  public void xmlFileWellFormedNullPathFailTest() throws 
HopFileNotFoundException {
+    boolean wellFormed = ValueDataUtil.isXmlFileWellFormed(new 
ValueMetaString(), null, true);
+    assertFalse(wellFormed);
+  }
+
+  @Test
+  public void loadFileContentInBinary() throws Exception {
+    String path = getClass().getResource("txt-sample.txt").getPath();
+    byte[] content = ValueDataUtil.loadFileContentInBinary(new 
ValueMetaString(), path, true);
+    assertTrue(Arrays.equals("test".getBytes(), content));
+  }
+
+  @Test
+  public void loadFileContentInBinaryNoFailIfNoFileTest() throws Exception {
+    String nonExistingPath = "nonExistingPath";
+    assertNull(
+        ValueDataUtil.loadFileContentInBinary(new ValueMetaString(), 
nonExistingPath, false));
+  }
+
+  @Test(expected = HopFileNotFoundException.class)
+  public void loadFileContentInBinaryFailIfNoFileTest()
+      throws HopFileNotFoundException, HopValueException {
+    String nonExistingPath = "nonExistingPath";
+    ValueDataUtil.loadFileContentInBinary(new ValueMetaString(), 
nonExistingPath, true);
+  }
+
+  @Test
+  public void loadFileContentInBinaryNullPathNoFailTest() throws Exception {
+    assertNull(ValueDataUtil.loadFileContentInBinary(new ValueMetaString(), 
null, false));
+  }
+
+  @Test
+  public void loadFileContentInBinaryNullPathFailTest()
+      throws HopFileNotFoundException, HopValueException {
+    assertNull(ValueDataUtil.loadFileContentInBinary(new ValueMetaString(), 
null, true));
+  }
+
+  @Test
+  public void getFileEncodingWithFailIfNoFileTest() throws Exception {
+    String path = getClass().getResource("txt-sample.txt").getPath();
+    String encoding = ValueDataUtil.getFileEncoding(new ValueMetaString(), 
path, true);
+    assertEquals("US-ASCII", encoding);
+  }
+
+  @Test
+  public void getFileEncodingWithoutFailIfNoFileTest() throws Exception {
+    String path = getClass().getResource("txt-sample.txt").getPath();
+    String encoding = ValueDataUtil.getFileEncoding(new ValueMetaString(), 
path, false);
+    assertEquals("US-ASCII", encoding);
+  }
+
+  @Test
+  public void getFileEncodingNoFailIfNoFileTest() throws Exception {
+    String nonExistingPath = "nonExistingPath";
+    String encoding = ValueDataUtil.getFileEncoding(new ValueMetaString(), 
nonExistingPath, false);
+    assertNull(encoding);
+  }
+
+  @Test(expected = HopFileNotFoundException.class)
+  public void getFileEncodingFailIfNoFileTest() throws 
HopFileNotFoundException, HopValueException {
+    String nonExistingPath = "nonExistingPath";
+    ValueDataUtil.getFileEncoding(new ValueMetaString(), nonExistingPath, 
true);
+  }
+
+  @Test
+  public void getFileEncodingNullPathNoFailTest() throws Exception {
+    String encoding = ValueDataUtil.getFileEncoding(new ValueMetaString(), 
null, false);
+    assertNull(encoding);
+  }
+
+  @Test
+  public void getFileEncodingNullPathFailTest() throws 
HopFileNotFoundException, HopValueException {
+    String encoding = ValueDataUtil.getFileEncoding(new ValueMetaString(), 
null, true);
+    assertNull(encoding);
+  }
+
+  @Test
+  public void testMulitplyBigNumbers() {
+    BigDecimal field1 = new 
BigDecimal("123456789012345678901.1234567890123456789");
+    BigDecimal field2 = new BigDecimal("1.0");
+    BigDecimal field3 = new BigDecimal("2.0");
+
+    BigDecimal expResult1 = new 
BigDecimal("123456789012345678901.1234567890123456789");
+    BigDecimal expResult2 = new 
BigDecimal("246913578024691357802.2469135780246913578");
+
+    BigDecimal expResult3 = new 
BigDecimal("123456789012345678901.1200000000000000000");
+    BigDecimal expResult4 = new BigDecimal("246913578024691357802");
+
+    assertEquals(expResult1, ValueDataUtil.multiplyBigDecimals(field1, field2, 
null));
+    assertEquals(expResult2, ValueDataUtil.multiplyBigDecimals(field1, field3, 
null));
+
+    assertEquals(
+        expResult3, ValueDataUtil.multiplyBigDecimals(field1, field2, new 
MathContext(23)));
+    assertEquals(
+        expResult4, ValueDataUtil.multiplyBigDecimals(field1, field3, new 
MathContext(21)));
+  }
+
+  @Test
+  public void testDivisionBigNumbers() {
+    BigDecimal field1 = new 
BigDecimal("123456789012345678901.1234567890123456789");
+    BigDecimal field2 = new BigDecimal("1.0");
+    BigDecimal field3 = new BigDecimal("2.0");
+
+    BigDecimal expResult1 = new 
BigDecimal("123456789012345678901.1234567890123456789");
+    BigDecimal expResult2 = new 
BigDecimal("61728394506172839450.56172839450617283945");
+
+    BigDecimal expResult3 = new BigDecimal("123456789012345678901.12");
+    BigDecimal expResult4 = new BigDecimal("61728394506172839450.6");
+
+    assertEquals(expResult1, ValueDataUtil.divideBigDecimals(field1, field2, 
null));
+    assertEquals(expResult2, ValueDataUtil.divideBigDecimals(field1, field3, 
null));
+
+    assertEquals(expResult3, ValueDataUtil.divideBigDecimals(field1, field2, 
new MathContext(23)));
+    assertEquals(expResult4, ValueDataUtil.divideBigDecimals(field1, field3, 
new MathContext(21)));
+  }
+
+  @Test
+  public void testRemainderBigNumbers() throws Exception {
+    BigDecimal field1 = new 
BigDecimal("123456789012345678901.1234567890123456789");
+    BigDecimal field2 = new BigDecimal("1.0");
+    BigDecimal field3 = new BigDecimal("2.0");
+
+    BigDecimal expResult1 = new BigDecimal("0.1234567890123456789");
+    BigDecimal expResult2 = new BigDecimal("1.1234567890123456789");
+
+    assertEquals(
+        expResult1,
+        ValueDataUtil.remainder(
+            new ValueMetaBigNumber(), field1, new ValueMetaBigNumber(), 
field2));
+    assertEquals(
+        expResult2,
+        ValueDataUtil.remainder(
+            new ValueMetaBigNumber(), field1, new ValueMetaBigNumber(), 
field3));
+  }
+
+  @Test
+  public void testSumWithNullValues() throws Exception {
+    IValueMeta metaA = new ValueMetaInteger();
+    metaA.setStorageType(IValueMeta.STORAGE_TYPE_NORMAL);
+    IValueMeta metaB = new ValueMetaInteger();
+    metaA.setStorageType(IValueMeta.STORAGE_TYPE_NORMAL);
+
+    assertNull(ValueDataUtil.sum(metaA, null, metaB, null));
+
+    Long valueB = Long.valueOf(2);
+    ValueDataUtil.sum(metaA, null, metaB, valueB);
+  }
+
+  @Test
+  public void testSumConvertingStorageTypeToNormal() throws Exception {
+    IValueMeta metaA = mock(ValueMetaInteger.class);
+    metaA.setStorageType(IValueMeta.STORAGE_TYPE_BINARY_STRING);
+
+    IValueMeta metaB = new ValueMetaInteger();
+    metaB.setStorageType(IValueMeta.STORAGE_TYPE_BINARY_STRING);
+    Object valueB = "2";
+
+    when(metaA.convertData(metaB, valueB)).thenAnswer((Answer<Long>) 
invocation -> Long.valueOf(2));
+
+    Object returnValue = ValueDataUtil.sum(metaA, null, metaB, valueB);
+    verify(metaA).convertData(metaB, valueB);
+    assertEquals(2L, returnValue);
+    assertEquals(IValueMeta.STORAGE_TYPE_NORMAL, metaA.getStorageType());
+  }
+
+  // String manipulation tests
+  @Test
+  public void testInitCap() {
+    assertEquals("Hello World", ValueDataUtil.initCap(new ValueMetaString(), 
"hello world"));
+    assertEquals("Apache Hop", ValueDataUtil.initCap(new ValueMetaString(), 
"apache hop"));
+    assertNull(ValueDataUtil.initCap(new ValueMetaString(), null));
+  }
+
+  @Test
+  public void testUpperCase() {
+    assertEquals("HELLO", ValueDataUtil.upperCase(new ValueMetaString(), 
"hello"));
+    assertEquals("APACHE HOP", ValueDataUtil.upperCase(new ValueMetaString(), 
"Apache Hop"));
+    assertNull(ValueDataUtil.upperCase(new ValueMetaString(), null));
+  }
+
+  @Test
+  public void testLowerCase() {
+    assertEquals("hello", ValueDataUtil.lowerCase(new ValueMetaString(), 
"HELLO"));
+    assertEquals("apache hop", ValueDataUtil.lowerCase(new ValueMetaString(), 
"Apache Hop"));
+    assertNull(ValueDataUtil.lowerCase(new ValueMetaString(), null));
+  }
+
+  @Test
+  public void testRemoveCR() {
+    assertEquals("helloworld", ValueDataUtil.removeCR(new ValueMetaString(), 
"hello\rworld"));
+    assertNull(ValueDataUtil.removeCR(new ValueMetaString(), null));
+  }
+
+  @Test
+  public void testRemoveLF() {
+    assertEquals("helloworld", ValueDataUtil.removeLF(new ValueMetaString(), 
"hello\nworld"));
+    assertNull(ValueDataUtil.removeLF(new ValueMetaString(), null));
+  }
+
+  @Test
+  public void testRemoveCRLF() {
+    assertEquals("helloworld", ValueDataUtil.removeCRLF(new ValueMetaString(), 
"hello\r\nworld"));
+    assertNull(ValueDataUtil.removeCRLF(new ValueMetaString(), null));
+  }
+
+  @Test
+  public void testRemoveTAB() {
+    assertEquals("helloworld", ValueDataUtil.removeTAB(new ValueMetaString(), 
"hello\tworld"));
+    assertNull(ValueDataUtil.removeTAB(new ValueMetaString(), null));
+  }
+
+  @Test
+  public void testGetDigits() {
+    assertEquals("123", ValueDataUtil.getDigits(new ValueMetaString(), 
"abc123def"));
+    assertEquals("", ValueDataUtil.getDigits(new ValueMetaString(), "abcdef"));
+    assertNull(ValueDataUtil.getDigits(new ValueMetaString(), null));
+  }
+
+  @Test
+  public void testRemoveDigits() {
+    assertEquals("abcdef", ValueDataUtil.removeDigits(new ValueMetaString(), 
"abc123def"));
+    assertEquals("abcdef", ValueDataUtil.removeDigits(new ValueMetaString(), 
"abcdef"));
+    assertNull(ValueDataUtil.removeDigits(new ValueMetaString(), null));
+  }
+
+  @Test
+  public void testStringLen() {
+    assertEquals(5, ValueDataUtil.stringLen(new ValueMetaString(), "hello"));
+    assertEquals(0, ValueDataUtil.stringLen(new ValueMetaString(), ""));
+    assertEquals(0, ValueDataUtil.stringLen(new ValueMetaString(), null));
+  }
+
+  // String distance/similarity tests
+  @Test
+  public void testLevenshteinDistance() {
+    Long distance =
+        ValueDataUtil.getLevenshtein_Distance(
+            new ValueMetaString(), "kitten", new ValueMetaString(), "sitting");
+    assertEquals(Long.valueOf(3), distance);
+
+    assertNull(
+        ValueDataUtil.getLevenshtein_Distance(
+            new ValueMetaString(), null, new ValueMetaString(), "test"));
+    assertNull(
+        ValueDataUtil.getLevenshtein_Distance(
+            new ValueMetaString(), "test", new ValueMetaString(), null));
+  }
+
+  @Test
+  public void testDamerauLevenshteinDistance() {
+    Long distance =
+        ValueDataUtil.getDamerauLevenshtein_Distance(
+            new ValueMetaString(), "abc", new ValueMetaString(), "acb");
+    assertEquals(Long.valueOf(1), distance);
+
+    assertNull(
+        ValueDataUtil.getDamerauLevenshtein_Distance(
+            new ValueMetaString(), null, new ValueMetaString(), "test"));
+  }
+
+  @Test
+  public void testJaroSimilitude() {
+    Double similarity =
+        ValueDataUtil.getJaro_Similitude(
+            new ValueMetaString(), "martha", new ValueMetaString(), "marhta");
+    assertTrue(similarity > 0.9);
+
+    assertNull(
+        ValueDataUtil.getJaro_Similitude(
+            new ValueMetaString(), null, new ValueMetaString(), "test"));
+  }
+
+  @Test
+  public void testJaroWinklerSimilitude() {
+    Double similarity =
+        ValueDataUtil.getJaroWinkler_Similitude(
+            new ValueMetaString(), "martha", new ValueMetaString(), "marhta");
+    assertTrue(similarity > 0.9);
+
+    assertNull(
+        ValueDataUtil.getJaroWinkler_Similitude(
+            new ValueMetaString(), null, new ValueMetaString(), "test"));
+  }
+
+  // Phonetic algorithms tests
+  @Test
+  public void testMetaphone() {
+    String metaphone = ValueDataUtil.get_Metaphone(new ValueMetaString(), 
"hello");
+    assertEquals("HL", metaphone);
+    assertNull(ValueDataUtil.get_Metaphone(new ValueMetaString(), null));
+  }
+
+  @Test
+  public void testDoubleMetaphone() {
+    String doubleMetaphone = ValueDataUtil.get_Double_Metaphone(new 
ValueMetaString(), "hello");
+    assertEquals("HL", doubleMetaphone);
+    assertNull(ValueDataUtil.get_Double_Metaphone(new ValueMetaString(), 
null));
+  }
+
+  @Test
+  public void testSoundEx() {
+    String soundex = ValueDataUtil.get_SoundEx(new ValueMetaString(), "hello");
+    assertEquals("H400", soundex);
+    assertNull(ValueDataUtil.get_SoundEx(new ValueMetaString(), null));
+  }
+
+  @Test
+  public void testRefinedSoundEx() {
+    String refinedSoundex = ValueDataUtil.get_RefinedSoundEx(new 
ValueMetaString(), "hello");
+    assertNull(ValueDataUtil.get_RefinedSoundEx(new ValueMetaString(), null));
+  }
+
+  // Math operations tests
+  @Test
+  public void testAbs() throws HopValueException {
+    assertEquals(Long.valueOf(5), ValueDataUtil.abs(new ValueMetaInteger(), 
Long.valueOf(-5)));
+    assertEquals(
+        Double.valueOf(5.5), ValueDataUtil.abs(new ValueMetaNumber(), 
Double.valueOf(-5.5)));
+    assertEquals(
+        new BigDecimal("5.5"), ValueDataUtil.abs(new ValueMetaBigNumber(), new 
BigDecimal("-5.5")));
+  }
+
+  @Test
+  public void testSqrt() throws HopValueException {
+    Object result = ValueDataUtil.sqrt(new ValueMetaNumber(), 
Double.valueOf(16.0));
+    assertEquals(4.0, ((Double) result).doubleValue(), 0.001);
+
+    result = ValueDataUtil.sqrt(new ValueMetaInteger(), Long.valueOf(25));
+    assertEquals(Long.valueOf(5), result);
+  }
+
+  @Test
+  public void testCeil() throws HopValueException {
+    assertEquals(
+        Double.valueOf(6.0), ValueDataUtil.ceil(new ValueMetaNumber(), 
Double.valueOf(5.3)));
+    assertEquals(Long.valueOf(5), ValueDataUtil.ceil(new ValueMetaInteger(), 
Long.valueOf(5)));
+    assertEquals(
+        BigDecimal.valueOf(6.0),
+        ValueDataUtil.ceil(new ValueMetaBigNumber(), new BigDecimal("5.3")));
+  }
+
+  @Test
+  public void testFloor() throws HopValueException {
+    assertEquals(
+        Double.valueOf(5.0), ValueDataUtil.floor(new ValueMetaNumber(), 
Double.valueOf(5.9)));
+    assertEquals(Long.valueOf(5), ValueDataUtil.floor(new ValueMetaInteger(), 
Long.valueOf(5)));
+    assertEquals(
+        BigDecimal.valueOf(5.0),
+        ValueDataUtil.floor(new ValueMetaBigNumber(), new BigDecimal("5.9")));
+  }
+
+  @Test
+  public void testRoundNoDigits() throws HopValueException {
+    assertEquals(
+        Double.valueOf(5.0), ValueDataUtil.round(new ValueMetaNumber(), 
Double.valueOf(5.4)));
+    assertEquals(
+        Double.valueOf(6.0), ValueDataUtil.round(new ValueMetaNumber(), 
Double.valueOf(5.5)));
+    assertEquals(Long.valueOf(5), ValueDataUtil.round(new ValueMetaInteger(), 
Long.valueOf(5)));
+  }
+
+  @Test
+  public void testRoundWithDigits() throws HopValueException {
+    Object result =
+        ValueDataUtil.round(
+            new ValueMetaNumber(), Double.valueOf(5.456),
+            new ValueMetaInteger(), Long.valueOf(2));
+    assertEquals(5.46, ((Double) result).doubleValue(), 0.001);
+  }
+
+  // Date operations tests
+  @Test
+  public void testRemoveTimeFromDate() throws HopValueException {
+    Calendar cal = Calendar.getInstance();
+    cal.set(2024, Calendar.JANUARY, 15, 14, 30, 45);
+    Date dateWithTime = cal.getTime();
+
+    Date result = (Date) ValueDataUtil.removeTimeFromDate(new ValueMetaDate(), 
dateWithTime);
+
+    Calendar resultCal = Calendar.getInstance();
+    resultCal.setTime(result);
+    assertEquals(0, resultCal.get(Calendar.HOUR_OF_DAY));
+    assertEquals(0, resultCal.get(Calendar.MINUTE));
+    assertEquals(0, resultCal.get(Calendar.SECOND));
+    assertEquals(0, resultCal.get(Calendar.MILLISECOND));
+  }
+
+  @Test
+  public void testAddDays() throws HopValueException {
+    Calendar cal = Calendar.getInstance();
+    cal.set(2024, Calendar.JANUARY, 15, 0, 0, 0);
+    Date date = cal.getTime();
+
+    Date result =
+        (Date)
+            ValueDataUtil.addDays(
+                new ValueMetaDate(), date, new ValueMetaInteger(), 
Long.valueOf(5));
+
+    Calendar resultCal = Calendar.getInstance();
+    resultCal.setTime(result);
+    assertEquals(20, resultCal.get(Calendar.DAY_OF_MONTH));
+  }
+
+  @Test
+  public void testAddHours() throws HopValueException {
+    Calendar cal = Calendar.getInstance();
+    cal.set(2024, Calendar.JANUARY, 15, 10, 0, 0);
+    Date date = cal.getTime();
+
+    Date result =
+        (Date)
+            ValueDataUtil.addHours(
+                new ValueMetaDate(), date, new ValueMetaInteger(), 
Long.valueOf(5));
+
+    Calendar resultCal = Calendar.getInstance();
+    resultCal.setTime(result);
+    assertEquals(15, resultCal.get(Calendar.HOUR_OF_DAY));
+  }
+
+  @Test
+  public void testAddMinutes() throws HopValueException {
+    Calendar cal = Calendar.getInstance();
+    cal.set(2024, Calendar.JANUARY, 15, 10, 30, 0);
+    Date date = cal.getTime();
+
+    Date result =
+        (Date)
+            ValueDataUtil.addMinutes(
+                new ValueMetaDate(), date, new ValueMetaInteger(), 
Long.valueOf(45));
+
+    Calendar resultCal = Calendar.getInstance();
+    resultCal.setTime(result);
+    assertEquals(15, resultCal.get(Calendar.MINUTE));
+  }
+
+  @Test
+  public void testYearOfDate() throws HopValueException {
+    Calendar cal = Calendar.getInstance();
+    cal.set(2024, Calendar.JANUARY, 15);
+    Date date = cal.getTime();
+
+    Long year = (Long) ValueDataUtil.yearOfDate(new ValueMetaDate(), date);
+    assertEquals(Long.valueOf(2024), year);
+  }
+
+  @Test
+  public void testMonthOfDate() throws HopValueException {
+    Calendar cal = Calendar.getInstance();
+    cal.set(2024, Calendar.MARCH, 15);
+    Date date = cal.getTime();
+
+    Long month = (Long) ValueDataUtil.monthOfDate(new ValueMetaDate(), date);
+    assertEquals(Long.valueOf(3), month);
+  }
+
+  @Test
+  public void testDayOfMonth() throws HopValueException {
+    Calendar cal = Calendar.getInstance();
+    cal.set(2024, Calendar.JANUARY, 15);
+    Date date = cal.getTime();
+
+    Long day = (Long) ValueDataUtil.dayOfMonth(new ValueMetaDate(), date);
+    assertEquals(Long.valueOf(15), day);
+  }
+
+  @Test
+  public void testHourOfDay() throws HopValueException {
+    Calendar cal = Calendar.getInstance();
+    cal.set(2024, Calendar.JANUARY, 15, 14, 30, 0);
+    Date date = cal.getTime();
+
+    Long hour = (Long) ValueDataUtil.hourOfDay(new ValueMetaDate(), date);
+    assertEquals(Long.valueOf(14), hour);
+  }
+
+  // NVL function tests
+  @Test
+  public void testNvlWithNullFirstValue() throws HopValueException {
+    String result =
+        (String) ValueDataUtil.nvl(new ValueMetaString(), null, new 
ValueMetaString(), "default");
+    assertEquals("default", result);
+  }
+
+  @Test
+  public void testNvlWithNonNullFirstValue() throws HopValueException {
+    String result =
+        (String)
+            ValueDataUtil.nvl(
+                new ValueMetaString(), "value",
+                new ValueMetaString(), "default");
+    assertEquals("value", result);
+  }
+
+  @Test
+  public void testNvlWithIntegers() throws HopValueException {
+    Long result =
+        (Long)
+            ValueDataUtil.nvl(
+                new ValueMetaInteger(), null, new ValueMetaInteger(), 
Long.valueOf(100));
+    assertEquals(Long.valueOf(100), result);
+  }
+
+  // URL encoding/decoding tests
+  @Test
+  public void testUrlEncode() {
+    String encoded = ValueDataUtil.urlEncode(new ValueMetaString(), "hello 
world");
+    assertEquals("hello+world", encoded);
+    assertNull(ValueDataUtil.urlEncode(new ValueMetaString(), null));
+  }
+
+  @Test
+  public void testUrlDecode() {
+    String decoded = ValueDataUtil.urlDecode(new ValueMetaString(), 
"hello+world");
+    assertEquals("hello world", decoded);
+    assertNull(ValueDataUtil.urlDecode(new ValueMetaString(), null));
+  }
+
+  // Hex encoding/decoding tests
+  @Test
+  public void testByteToHexEncode() throws HopValueException {
+    String hex = ValueDataUtil.byteToHexEncode(new ValueMetaString(), "test");
+    assertEquals("74657374", hex);
+  }
+
+  @Test
+  public void testHexToByteDecode() throws HopValueException {
+    String decoded = ValueDataUtil.hexToByteDecode(new ValueMetaString(), 
"74657374");
+    assertEquals("test", decoded);
+  }
+
+  // XML/HTML escape tests
+  @Test
+  public void testEscapeXml() {
+    String escaped = ValueDataUtil.escapeXml(new ValueMetaString(), "<hello>");
+    assertTrue(escaped.contains("&lt;") && escaped.contains("&gt;"));
+    assertNull(ValueDataUtil.escapeXml(new ValueMetaString(), null));
+  }
+
+  @Test
+  public void testUnEscapeXml() {
+    String unescaped = ValueDataUtil.unEscapeXml(new ValueMetaString(), 
"&lt;hello&gt;");
+    assertEquals("<hello>", unescaped);
+    assertNull(ValueDataUtil.unEscapeXml(new ValueMetaString(), null));
+  }
+
+  @Test
+  public void testEscapeHtml() {
+    String escaped = ValueDataUtil.escapeHtml(new ValueMetaString(), 
"<hello>");
+    assertTrue(escaped.contains("&lt;") && escaped.contains("&gt;"));
+    assertNull(ValueDataUtil.escapeHtml(new ValueMetaString(), null));
+  }
+
+  @Test
+  public void testUnEscapeHtml() {
+    String unescaped = ValueDataUtil.unEscapeHtml(new ValueMetaString(), 
"&lt;hello&gt;");
+    assertEquals("<hello>", unescaped);
+    assertNull(ValueDataUtil.unEscapeHtml(new ValueMetaString(), null));
+  }
+
+  // Test getZeroForValueMetaType
+  @Test
+  public void testGetZeroForValueMetaType() throws HopValueException {
+    assertEquals(Long.valueOf(0), ValueDataUtil.getZeroForValueMetaType(new 
ValueMetaInteger()));
+    assertEquals(Double.valueOf(0), ValueDataUtil.getZeroForValueMetaType(new 
ValueMetaNumber()));
+    assertEquals(
+        new BigDecimal(0), ValueDataUtil.getZeroForValueMetaType(new 
ValueMetaBigNumber()));
+    assertEquals("", ValueDataUtil.getZeroForValueMetaType(new 
ValueMetaString()));
+  }
+
+  @Test(expected = HopValueException.class)
+  public void testGetZeroForValueMetaTypeWithNull() throws HopValueException {
+    ValueDataUtil.getZeroForValueMetaType(null);
+  }
+
+  @Test(expected = HopValueException.class)
+  public void testGetZeroForValueMetaTypeWithUnsupportedType() throws 
HopValueException {
+    ValueDataUtil.getZeroForValueMetaType(new ValueMetaBoolean());
+  }
+
+  // Test minus operation
+  @Test
+  public void testMinus() throws HopValueException {
+    Object result =
+        ValueDataUtil.minus(
+            new ValueMetaInteger(), Long.valueOf(10),
+            new ValueMetaInteger(), Long.valueOf(3));
+    assertEquals(Long.valueOf(7), result);
+
+    result =
+        ValueDataUtil.minus(
+            new ValueMetaNumber(), Double.valueOf(10.5),
+            new ValueMetaNumber(), Double.valueOf(3.2));
+    assertEquals(7.3, ((Double) result).doubleValue(), 0.001);
+  }
+
+  // Test multiply operation
+  @Test
+  public void testMultiply() throws HopValueException {
+    Object result =
+        ValueDataUtil.multiply(
+            new ValueMetaInteger(), Long.valueOf(5),
+            new ValueMetaInteger(), Long.valueOf(3));
+    assertEquals(Long.valueOf(15), result);
+
+    result =
+        ValueDataUtil.multiply(
+            new ValueMetaNumber(), Double.valueOf(2.5),
+            new ValueMetaNumber(), Double.valueOf(4.0));
+    assertEquals(10.0, ((Double) result).doubleValue(), 0.001);
+  }
+
+  // Test divide operation
+  @Test
+  public void testDivide() throws HopValueException {
+    Object result =
+        ValueDataUtil.divide(
+            new ValueMetaInteger(), Long.valueOf(10),
+            new ValueMetaInteger(), Long.valueOf(2));
+    assertEquals(Long.valueOf(5), result);
+
+    result =
+        ValueDataUtil.divide(
+            new ValueMetaNumber(), Double.valueOf(10.0),
+            new ValueMetaNumber(), Double.valueOf(2.0));
+    assertEquals(5.0, ((Double) result).doubleValue(), 0.001);
+  }
+
+  // Test isXmlWellFormed (for string content, not file)
+  @Test
+  public void testIsXmlWellFormed() {
+    assertTrue(
+        ValueDataUtil.isXmlWellFormed(
+            new ValueMetaString(), "<?xml 
version=\"1.0\"?><root>test</root>"));
+    assertFalse(ValueDataUtil.isXmlWellFormed(new ValueMetaString(), 
"<root>test"));
+    assertFalse(ValueDataUtil.isXmlWellFormed(new ValueMetaString(), null));
+  }
+
+  // SQL escape test
+  @Test
+  public void testEscapeSql() {
+    String escaped = ValueDataUtil.escapeSql(new ValueMetaString(), 
"O'Reilly");
+    assertEquals("O''Reilly", escaped);
+    assertNull(ValueDataUtil.escapeSql(new ValueMetaString(), null));
+  }
+
+  // CDATA test
+  @Test
+  public void testUseCDATA() {
+    String cdata = ValueDataUtil.useCDATA(new ValueMetaString(), 
"<test>data</test>");
+    assertTrue(cdata.startsWith("<![CDATA[") && cdata.endsWith("]]>"));
+    assertNull(ValueDataUtil.useCDATA(new ValueMetaString(), null));
+  }
+
+  // Percentage functions tests
+  @Test
+  public void testPercent1() throws HopValueException {
+    // percent1: A / B * 100
+    Object result =
+        ValueDataUtil.percent1(
+            new ValueMetaNumber(),
+            Double.valueOf(50.0),
+            new ValueMetaNumber(),
+            Double.valueOf(200.0));
+    assertEquals(25.0, ((Double) result).doubleValue(), 0.001);
+  }
+
+  @Test
+  public void testPercent2() throws HopValueException {
+    // percent2: A - (A * B / 100)
+    Object result =
+        ValueDataUtil.percent2(
+            new ValueMetaNumber(),
+            Double.valueOf(100.0),
+            new ValueMetaNumber(),
+            Double.valueOf(20.0));
+    assertEquals(80.0, ((Double) result).doubleValue(), 0.001);
+  }
+
+  @Test
+  public void testPercent3() throws HopValueException {
+    // percent3: A + (A * B / 100)
+    Object result =
+        ValueDataUtil.percent3(
+            new ValueMetaNumber(),
+            Double.valueOf(100.0),
+            new ValueMetaNumber(),
+            Double.valueOf(20.0));
+    assertEquals(120.0, ((Double) result).doubleValue(), 0.001);
+  }
+
+  // Combination functions tests
+  @Test
+  public void testCombination1() throws HopValueException {
+    // combination1: A + B * C
+    Object result =
+        ValueDataUtil.combination1(
+            new ValueMetaNumber(),
+            Double.valueOf(10.0),
+            new ValueMetaNumber(),
+            Double.valueOf(5.0),
+            new ValueMetaNumber(),
+            Double.valueOf(2.0));
+    assertEquals(20.0, ((Double) result).doubleValue(), 0.001);
+  }
+
+  @Test
+  public void testCombination2() throws HopValueException {
+    // combination2: SQRT(A * A + B * B)
+    Object result =
+        ValueDataUtil.combination2(
+            new ValueMetaNumber(), Double.valueOf(3.0), new ValueMetaNumber(), 
Double.valueOf(4.0));
+    assertEquals(5.0, ((Double) result).doubleValue(), 0.001);
+  }
+
+  // Additional date operations tests
+  @Test
+  public void testAddSeconds() throws HopValueException {
+    Calendar cal = Calendar.getInstance();
+    cal.set(2024, Calendar.JANUARY, 15, 10, 30, 0);
+    Date date = cal.getTime();
+
+    Date result =
+        (Date)
+            ValueDataUtil.addSeconds(
+                new ValueMetaDate(), date, new ValueMetaInteger(), 
Long.valueOf(90));
+
+    Calendar resultCal = Calendar.getInstance();
+    resultCal.setTime(result);
+    assertEquals(31, resultCal.get(Calendar.MINUTE));
+    assertEquals(30, resultCal.get(Calendar.SECOND));
+  }
+
+  @Test
+  public void testAddMonths() throws HopValueException {
+    Calendar cal = Calendar.getInstance();
+    cal.set(2024, Calendar.JANUARY, 15);
+    Date date = cal.getTime();
+
+    Date result =
+        (Date)
+            ValueDataUtil.addMonths(
+                new ValueMetaDate(), date, new ValueMetaInteger(), 
Long.valueOf(3));
+
+    Calendar resultCal = Calendar.getInstance();
+    resultCal.setTime(result);
+    assertEquals(Calendar.APRIL, resultCal.get(Calendar.MONTH));
+  }
+
+  @Test
+  public void testQuarterOfDate() throws HopValueException {
+    Calendar cal = Calendar.getInstance();
+
+    // Q1 - January
+    cal.set(2024, Calendar.JANUARY, 15);
+    Long q1 = (Long) ValueDataUtil.quarterOfDate(new ValueMetaDate(), 
cal.getTime());
+    assertEquals(Long.valueOf(1), q1);
+
+    // Q2 - May
+    cal.set(2024, Calendar.MAY, 15);
+    Long q2 = (Long) ValueDataUtil.quarterOfDate(new ValueMetaDate(), 
cal.getTime());
+    assertEquals(Long.valueOf(2), q2);
+
+    // Q3 - August
+    cal.set(2024, Calendar.AUGUST, 15);
+    Long q3 = (Long) ValueDataUtil.quarterOfDate(new ValueMetaDate(), 
cal.getTime());
+    assertEquals(Long.valueOf(3), q3);
+
+    // Q4 - November
+    cal.set(2024, Calendar.NOVEMBER, 15);
+    Long q4 = (Long) ValueDataUtil.quarterOfDate(new ValueMetaDate(), 
cal.getTime());
+    assertEquals(Long.valueOf(4), q4);
+  }
+
+  @Test
+  public void testWeekOfYear() throws HopValueException {
+    Calendar cal = Calendar.getInstance();
+    cal.set(2024, Calendar.JANUARY, 15);
+    Date date = cal.getTime();
+
+    Long week = (Long) ValueDataUtil.weekOfYear(new ValueMetaDate(), date);
+    assertTrue(week >= 1 && week <= 53);
+  }
+
+  @Test
+  public void testWeekOfYearISO8601() throws HopValueException {
+    Calendar cal = Calendar.getInstance();
+    cal.set(2024, Calendar.JANUARY, 15);
+    Date date = cal.getTime();
+
+    Long week = (Long) ValueDataUtil.weekOfYearISO8601(new ValueMetaDate(), 
date);
+    assertTrue(week >= 1 && week <= 53);
+  }
+
+  @Test
+  public void testYearOfDateISO8601() throws HopValueException {
+    Calendar cal = Calendar.getInstance();
+    cal.set(2024, Calendar.JANUARY, 1);
+    Date date = cal.getTime();
+
+    Long year = (Long) ValueDataUtil.yearOfDateISO8601(new ValueMetaDate(), 
date);
+    assertTrue(year >= 2023 && year <= 2024); // ISO week year can differ at 
year boundaries
+  }
+
+  @Test
+  public void testDayOfYear() throws HopValueException {
+    Calendar cal = Calendar.getInstance();
+    cal.set(2024, Calendar.JANUARY, 1);
+    Date date = cal.getTime();
+
+    Long day = (Long) ValueDataUtil.dayOfYear(new ValueMetaDate(), date);
+    assertEquals(Long.valueOf(1), day);
+
+    cal.set(2024, Calendar.DECEMBER, 31);
+    date = cal.getTime();
+    day = (Long) ValueDataUtil.dayOfYear(new ValueMetaDate(), date);
+    assertTrue(day == 366); // 2024 is a leap year
+  }
+
+  @Test
+  public void testMinuteOfHour() throws HopValueException {
+    Calendar cal = Calendar.getInstance();
+    cal.set(2024, Calendar.JANUARY, 15, 14, 35, 0);
+    Date date = cal.getTime();
+
+    Long minute = (Long) ValueDataUtil.minuteOfHour(new ValueMetaDate(), date);
+    assertEquals(Long.valueOf(35), minute);
+  }
+
+  @Test
+  public void testSecondOfMinute() throws HopValueException {
+    Calendar cal = Calendar.getInstance();
+    cal.set(2024, Calendar.JANUARY, 15, 14, 30, 45);
+    Date date = cal.getTime();
+
+    Long second = (Long) ValueDataUtil.secondOfMinute(new ValueMetaDate(), 
date);
+    assertEquals(Long.valueOf(45), second);
+  }
+
+  @Test
+  public void testDayOfWeek() throws HopValueException {
+    Calendar cal = Calendar.getInstance();
+    cal.set(2024, Calendar.JANUARY, 15); // Monday
+    Date date = cal.getTime();
+
+    Long dayOfWeek = (Long) ValueDataUtil.dayOfWeek(new ValueMetaDate(), date);
+    assertTrue(dayOfWeek >= 1 && dayOfWeek <= 7);
+  }
+
+  // Hex encoding tests
+  @Test
+  public void testCharToHexEncode() throws HopValueException {
+    String hex = ValueDataUtil.charToHexEncode(new ValueMetaString(), "A");
+    assertEquals("0041", hex);
+  }
+
+  @Test
+  public void testHexToCharDecode() throws HopValueException {
+    String decoded = ValueDataUtil.hexToCharDecode(new ValueMetaString(), 
"0041");
+    assertEquals("A", decoded);
+  }
+
+  // String utility functions tests
+  @Test
+  public void testRightPadString() {
+    assertEquals("test  ", ValueDataUtil.rightPad("test", 6));
+    assertEquals("test", ValueDataUtil.rightPad("test", 4));
+    assertEquals("te", ValueDataUtil.rightPad("test", 2)); // Truncates if 
limit is shorter
+  }
+
+  @Test
+  public void testRightPadStringBuffer() {
+    StringBuffer sb = new StringBuffer("test");
+    String result = ValueDataUtil.rightPad(sb, 6);
+    assertEquals("test  ", result);
+  }
+
+  @Test
+  public void testReplace() {
+    String result = ValueDataUtil.replace("hello world", "world", "universe");
+    assertEquals("hello universe", result);
+
+    result = ValueDataUtil.replace("test test test", "test", "TEST");
+    assertEquals("TEST TEST TEST", result);
+  }
+
+  @Test
+  public void testReplaceBuffer() {
+    StringBuffer buffer = new StringBuffer("hello world");
+    ValueDataUtil.replaceBuffer(buffer, "world", "universe");
+    assertEquals("hello universe", buffer.toString());
+  }
+
+  @Test
+  public void testNrSpacesBefore() {
+    assertEquals(0, ValueDataUtil.nrSpacesBefore("test"));
+    assertEquals(3, ValueDataUtil.nrSpacesBefore("   test"));
+    assertEquals(0, ValueDataUtil.nrSpacesBefore(""));
+  }
+
+  @Test
+  public void testNrSpacesAfter() {
+    assertEquals(0, ValueDataUtil.nrSpacesAfter("test"));
+    assertEquals(3, ValueDataUtil.nrSpacesAfter("test   "));
+    assertEquals(0, ValueDataUtil.nrSpacesAfter(""));
+  }
+
+  @Test
+  public void testOnlySpaces() {
+    assertTrue(ValueDataUtil.onlySpaces("   "));
+    assertTrue(ValueDataUtil.onlySpaces(""));
+    assertFalse(ValueDataUtil.onlySpaces("  a  "));
+    assertFalse(ValueDataUtil.onlySpaces("test"));
+  }
+
+  // Date difference tests
+  @Test
+  public void testDateDiff() throws HopValueException {
+    Calendar cal1 = Calendar.getInstance();
+    cal1.set(2024, Calendar.JANUARY, 1, 0, 0, 0);
+    Date date1 = cal1.getTime();
+
+    Calendar cal2 = Calendar.getInstance();
+    cal2.set(2024, Calendar.JANUARY, 6, 0, 0, 0);
+    Date date2 = cal2.getTime();
+
+    Long diff =
+        (Long) ValueDataUtil.DateDiff(new ValueMetaDate(), date2, new 
ValueMetaDate(), date1, "d");
+    assertEquals(Long.valueOf(5), diff);
+  }
+
+  @Test
+  public void testDateDiffHours() throws HopValueException {
+    Calendar cal1 = Calendar.getInstance();
+    cal1.set(2024, Calendar.JANUARY, 1, 0, 0, 0);
+    Date date1 = cal1.getTime();
+
+    Calendar cal2 = Calendar.getInstance();
+    cal2.set(2024, Calendar.JANUARY, 1, 5, 0, 0);
+    Date date2 = cal2.getTime();
+
+    Long diff =
+        (Long) ValueDataUtil.DateDiff(new ValueMetaDate(), date2, new 
ValueMetaDate(), date1, "h");
+    assertEquals(Long.valueOf(5), diff);
+  }
+
+  @Test
+  public void testDateDiffMinutes() throws HopValueException {
+    Calendar cal1 = Calendar.getInstance();
+    cal1.set(2024, Calendar.JANUARY, 1, 0, 0, 0);
+    Date date1 = cal1.getTime();
+
+    Calendar cal2 = Calendar.getInstance();
+    cal2.set(2024, Calendar.JANUARY, 1, 0, 30, 0);
+    Date date2 = cal2.getTime();
+
+    Long diff =
+        (Long) ValueDataUtil.DateDiff(new ValueMetaDate(), date2, new 
ValueMetaDate(), date1, "mn");
+    assertEquals(Long.valueOf(30), diff);
+  }
+
+  // Test addTimeToDate
+  @Test
+  public void testAddTimeToDate() throws HopValueException {
+    Calendar dateCal = Calendar.getInstance();
+    dateCal.set(2024, Calendar.JANUARY, 15, 0, 0, 0);
+    Date date = dateCal.getTime();
+
+    // addTimeToDate takes a date, a time string, and an optional time format
+    Date result =
+        (Date)
+            ValueDataUtil.addTimeToDate(
+                new ValueMetaDate(), date, new ValueMetaString(), "14:30:45", 
null, null);
+
+    Calendar resultCal = Calendar.getInstance();
+    resultCal.setTime(result);
+    assertEquals(2024, resultCal.get(Calendar.YEAR));
+    assertEquals(Calendar.JANUARY, resultCal.get(Calendar.MONTH));
+    assertEquals(15, resultCal.get(Calendar.DAY_OF_MONTH));
+    assertEquals(14, resultCal.get(Calendar.HOUR_OF_DAY));
+    assertEquals(30, resultCal.get(Calendar.MINUTE));
+    assertEquals(45, resultCal.get(Calendar.SECOND));
+  }
+
+  // Test plus3
+  @Test
+  public void testPlus3() throws HopValueException {
+    Object result =
+        ValueDataUtil.plus3(
+            new ValueMetaInteger(),
+            Long.valueOf(10),
+            new ValueMetaInteger(),
+            Long.valueOf(20),
+            new ValueMetaInteger(),
+            Long.valueOf(30));
+    assertEquals(Long.valueOf(60), result);
+
+    result =
+        ValueDataUtil.plus3(
+            new ValueMetaNumber(),
+            Double.valueOf(10.5),
+            new ValueMetaNumber(),
+            Double.valueOf(20.3),
+            new ValueMetaNumber(),
+            Double.valueOf(5.2));
+    assertEquals(36.0, ((Double) result).doubleValue(), 0.001);
+  }
+
+  // Test multiplyDoubles and multiplyLongs
+  @Test
+  public void testMultiplyDoubles() {
+    assertEquals(Double.valueOf(6.0), ValueDataUtil.multiplyDoubles(2.0, 3.0));
+  }
+
+  @Test
+  public void testMultiplyLongs() {
+    assertEquals(Long.valueOf(6), ValueDataUtil.multiplyLongs(2L, 3L));
+  }
+
+  // Test divideDoubles and divideLongs
+  @Test
+  public void testDivideDoubles() {
+    assertEquals(Double.valueOf(2.0), ValueDataUtil.divideDoubles(6.0, 3.0));
+  }
+
+  @Test
+  public void testDivideLongs() {
+    assertEquals(Long.valueOf(2), ValueDataUtil.divideLongs(6L, 3L));
+  }
+}
diff --git 
a/core/src/test/resources/org/apache/hop/core/row/invalid-xml-sample.xml 
b/core/src/test/resources/org/apache/hop/core/row/invalid-xml-sample.xml
new file mode 100644
index 0000000000..344baf9d95
--- /dev/null
+++ b/core/src/test/resources/org/apache/hop/core/row/invalid-xml-sample.xml
@@ -0,0 +1,24 @@
+<!--
+  ~ 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.
+  ~
+  -->
+
+<test>
+    <value1>Value1</value1>
+    <value2>Value2</value2>
+    <value3>
+        <value4>Value4</value4>
+    </value3>
diff --git a/core/src/test/resources/org/apache/hop/core/row/txt-sample.txt 
b/core/src/test/resources/org/apache/hop/core/row/txt-sample.txt
new file mode 100644
index 0000000000..30d74d2584
--- /dev/null
+++ b/core/src/test/resources/org/apache/hop/core/row/txt-sample.txt
@@ -0,0 +1 @@
+test
\ No newline at end of file
diff --git a/core/src/test/resources/org/apache/hop/core/row/xml-sample.xml 
b/core/src/test/resources/org/apache/hop/core/row/xml-sample.xml
new file mode 100644
index 0000000000..e7778579d4
--- /dev/null
+++ b/core/src/test/resources/org/apache/hop/core/row/xml-sample.xml
@@ -0,0 +1,25 @@
+<!--
+  ~ 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.
+  ~
+  -->
+
+<test>
+    <value1>Value1</value1>
+    <value2>Value2</value2>
+    <value3>
+        <value4>Value4</value4>
+    </value3>
+</test>
\ No newline at end of file
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/calculator.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/calculator.adoc
index 382d0d6479..ff2a50ab1b 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/calculator.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/calculator.adoc
@@ -195,6 +195,8 @@ TIP: Only integer values for B are supported. If you need 
non-integer calculatio
 |Base64 Decode|BASE64_DECODE|Decode a Base64 encoded string. Supports both 
padded or non-padded input.
 |First day of month (A) date|FIRST_DAY_OF_MONTH|Set the day of field (A) to 1.
 |Last day of month (A) date|LAST_DAY_OF_MONTH|Set the day of field (A) to the 
last day of the given month.
+|Encode a URL|URL_ENCODE|Encode a URL to have browser/webservice save url 
calls.
+|Decode a URL|URL_DECODE|Decode a URL.
 |===
 
 == Metadata Injection support
diff --git 
a/engine/src/test/java/org/apache/hop/core/row/ValueDataUtilTest.java 
b/engine/src/test/java/org/apache/hop/core/row/ValueDataUtilTest.java
deleted file mode 100644
index 4f6ed46708..0000000000
--- a/engine/src/test/java/org/apache/hop/core/row/ValueDataUtilTest.java
+++ /dev/null
@@ -1,397 +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.core.row;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.math.BigDecimal;
-import java.math.MathContext;
-import java.util.Arrays;
-import org.apache.commons.lang.StringUtils;
-import org.apache.hop.core.HopEnvironment;
-import org.apache.hop.core.exception.HopException;
-import org.apache.hop.core.exception.HopFileNotFoundException;
-import org.apache.hop.core.exception.HopValueException;
-import org.apache.hop.core.row.value.ValueMetaBigNumber;
-import org.apache.hop.core.row.value.ValueMetaInteger;
-import org.apache.hop.core.row.value.ValueMetaString;
-import org.apache.hop.junit.rules.RestoreHopEngineEnvironment;
-import org.junit.BeforeClass;
-import org.junit.ClassRule;
-import org.junit.Test;
-import org.mockito.stubbing.Answer;
-
-public class ValueDataUtilTest {
-  @ClassRule public static RestoreHopEngineEnvironment env = new 
RestoreHopEngineEnvironment();
-
-  @BeforeClass
-  public static void setUpBeforeClass() throws HopException {
-    HopEnvironment.init();
-  }
-
-  @Test
-  public void testPlus() throws HopValueException {
-
-    long longValue = 1;
-
-    assertEquals(
-        longValue,
-        ValueDataUtil.plus(
-            new ValueMetaInteger(), longValue, new ValueMetaString(), 
StringUtils.EMPTY));
-  }
-
-  @Test
-  public void checksumTest() throws Exception {
-    String path = getClass().getResource("txt-sample.txt").getPath();
-    String checksum = ValueDataUtil.createChecksum(new ValueMetaString(), 
path, "MD5", false);
-    assertEquals("098f6bcd4621d373cade4e832627b4f6", checksum);
-  }
-
-  @Test
-  public void checksumMissingFileTest() throws Exception {
-    String nonExistingFile = "nonExistingFile";
-    String checksum =
-        ValueDataUtil.createChecksum(new ValueMetaString(), nonExistingFile, 
"MD5", false);
-    assertNull(checksum);
-  }
-
-  @Test
-  public void checksumWithFailIfNoFileTest() throws Exception {
-    String path = getClass().getResource("txt-sample.txt").getPath();
-    String checksum = ValueDataUtil.createChecksum(new ValueMetaString(), 
path, "MD5", true);
-    assertEquals("098f6bcd4621d373cade4e832627b4f6", checksum);
-  }
-
-  @Test(expected = HopFileNotFoundException.class)
-  public void checksumFailIfNoFileTest() throws HopFileNotFoundException {
-    String nonExistingPath = "nonExistingPath";
-    ValueDataUtil.createChecksum(new ValueMetaString(), nonExistingPath, 
"MD5", true);
-  }
-
-  @Test
-  public void checksumNullPathNoFailTest() throws HopFileNotFoundException {
-    assertNull(ValueDataUtil.createChecksum(new ValueMetaString(), null, 
"MD5", false));
-  }
-
-  @Test
-  public void checksumNullPathFailTest() throws HopFileNotFoundException {
-    assertNull(ValueDataUtil.createChecksum(new ValueMetaString(), null, 
"MD5", true));
-  }
-
-  @Test
-  public void checksumCRC32Test() throws Exception {
-    String path = getClass().getResource("txt-sample.txt").getPath();
-    long checksum = ValueDataUtil.checksumCRC32(new ValueMetaString(), path, 
false);
-    assertEquals(3632233996l, checksum);
-  }
-
-  @Test
-  public void checksumCRC32MissingFileTest() throws Exception {
-    String nonExistingFile = "nonExistingFile";
-    long checksum = ValueDataUtil.checksumCRC32(new ValueMetaString(), 
nonExistingFile, false);
-    assertEquals(0, checksum);
-  }
-
-  @Test
-  public void checksumCRC32NoFailIfNoFileTest() throws 
HopFileNotFoundException {
-    String nonExistingPath = "nonExistingPath";
-    long checksum = ValueDataUtil.checksumCRC32(new ValueMetaString(), 
nonExistingPath, false);
-    assertEquals(0, checksum);
-  }
-
-  @Test(expected = HopFileNotFoundException.class)
-  public void checksumCRC32FailIfNoFileTest() throws HopFileNotFoundException {
-    String nonExistingPath = "nonExistingPath";
-    ValueDataUtil.checksumCRC32(new ValueMetaString(), nonExistingPath, true);
-  }
-
-  @Test
-  public void checksumCRC32NullPathNoFailTest() throws 
HopFileNotFoundException {
-    long checksum = ValueDataUtil.checksumCRC32(new ValueMetaString(), null, 
false);
-    assertEquals(0, checksum);
-  }
-
-  @Test
-  public void checksumCRC32NullPathFailTest() throws HopFileNotFoundException {
-    long checksum = ValueDataUtil.checksumCRC32(new ValueMetaString(), null, 
true);
-    assertEquals(0, checksum);
-  }
-
-  @Test
-  public void checksumAdlerWithFailIfNoFileTest() throws Exception {
-    String path = getClass().getResource("txt-sample.txt").getPath();
-    long checksum = ValueDataUtil.checksumAdler32(new ValueMetaString(), path, 
true);
-    assertEquals(73204161L, checksum);
-  }
-
-  @Test
-  public void checksumAdlerWithoutFailIfNoFileTest() throws Exception {
-    String path = getClass().getResource("txt-sample.txt").getPath();
-    long checksum = ValueDataUtil.checksumAdler32(new ValueMetaString(), path, 
false);
-    assertEquals(73204161L, checksum);
-  }
-
-  @Test
-  public void checksumAdlerNoFailIfNoFileTest() throws 
HopFileNotFoundException {
-    String nonExistingPath = "nonExistingPath";
-    long checksum = ValueDataUtil.checksumAdler32(new ValueMetaString(), 
nonExistingPath, false);
-    assertEquals(0, checksum);
-  }
-
-  @Test(expected = HopFileNotFoundException.class)
-  public void checksumAdlerFailIfNoFileTest() throws HopFileNotFoundException {
-    String nonExistingPath = "nonExistingPath";
-    ValueDataUtil.checksumAdler32(new ValueMetaString(), nonExistingPath, 
true);
-  }
-
-  @Test
-  public void checksumAdlerNullPathNoFailTest() throws 
HopFileNotFoundException {
-    long checksum = ValueDataUtil.checksumAdler32(new ValueMetaString(), null, 
false);
-    assertEquals(0, checksum);
-  }
-
-  @Test
-  public void checksumAdlerNullPathFailTest() throws HopFileNotFoundException {
-    long checksum = ValueDataUtil.checksumAdler32(new ValueMetaString(), null, 
true);
-    assertEquals(0, checksum);
-  }
-
-  @Test
-  public void xmlFileWellFormedTest() throws HopFileNotFoundException {
-    String xmlFilePath = getClass().getResource("xml-sample.xml").getPath();
-    boolean wellFormed =
-        ValueDataUtil.isXmlFileWellFormed(new ValueMetaString(), xmlFilePath, 
true);
-    assertTrue(wellFormed);
-  }
-
-  @Test
-  public void xmlFileBadlyFormedTest() throws HopFileNotFoundException {
-    String invalidXmlFilePath = 
getClass().getResource("invalid-xml-sample.xml").getPath();
-    boolean wellFormed =
-        ValueDataUtil.isXmlFileWellFormed(new ValueMetaString(), 
invalidXmlFilePath, true);
-    assertFalse(wellFormed);
-  }
-
-  @Test
-  public void xmlFileWellFormedWithoutFailIfNoFileTest() throws 
HopFileNotFoundException {
-    String xmlFilePath = getClass().getResource("xml-sample.xml").getPath();
-    boolean wellFormed =
-        ValueDataUtil.isXmlFileWellFormed(new ValueMetaString(), xmlFilePath, 
false);
-    assertTrue(wellFormed);
-  }
-
-  @Test
-  public void xmlFileBadlyFormedWithNoFailIfNoFileTest() throws 
HopFileNotFoundException {
-    String invalidXmlFilePath = 
getClass().getResource("invalid-xml-sample.xml").getPath();
-    boolean wellFormed =
-        ValueDataUtil.isXmlFileWellFormed(new ValueMetaString(), 
invalidXmlFilePath, false);
-    assertFalse(wellFormed);
-  }
-
-  @Test
-  public void xmlFileWellFormedNoFailIfNoFileTest() throws 
HopFileNotFoundException {
-    String nonExistingPath = "nonExistingPath";
-    boolean wellFormed =
-        ValueDataUtil.isXmlFileWellFormed(new ValueMetaString(), 
nonExistingPath, false);
-    assertFalse(wellFormed);
-  }
-
-  @Test(expected = HopFileNotFoundException.class)
-  public void xmlFileWellFormedFailIfNoFileTest() throws 
HopFileNotFoundException {
-    String nonExistingPath = "nonExistingPath";
-    ValueDataUtil.isXmlFileWellFormed(new ValueMetaString(), nonExistingPath, 
true);
-  }
-
-  @Test
-  public void xmlFileWellFormedNullPathNoFailTest() throws 
HopFileNotFoundException {
-    boolean wellFormed = ValueDataUtil.isXmlFileWellFormed(new 
ValueMetaString(), null, false);
-    assertFalse(wellFormed);
-  }
-
-  @Test
-  public void xmlFileWellFormedNullPathFailTest() throws 
HopFileNotFoundException {
-    boolean wellFormed = ValueDataUtil.isXmlFileWellFormed(new 
ValueMetaString(), null, true);
-    assertFalse(wellFormed);
-  }
-
-  @Test
-  public void loadFileContentInBinary() throws Exception {
-    String path = getClass().getResource("txt-sample.txt").getPath();
-    byte[] content = ValueDataUtil.loadFileContentInBinary(new 
ValueMetaString(), path, true);
-    assertTrue(Arrays.equals("test".getBytes(), content));
-  }
-
-  @Test
-  public void loadFileContentInBinaryNoFailIfNoFileTest() throws Exception {
-    String nonExistingPath = "nonExistingPath";
-    assertNull(
-        ValueDataUtil.loadFileContentInBinary(new ValueMetaString(), 
nonExistingPath, false));
-  }
-
-  @Test(expected = HopFileNotFoundException.class)
-  public void loadFileContentInBinaryFailIfNoFileTest()
-      throws HopFileNotFoundException, HopValueException {
-    String nonExistingPath = "nonExistingPath";
-    ValueDataUtil.loadFileContentInBinary(new ValueMetaString(), 
nonExistingPath, true);
-  }
-
-  @Test
-  public void loadFileContentInBinaryNullPathNoFailTest() throws Exception {
-    assertNull(ValueDataUtil.loadFileContentInBinary(new ValueMetaString(), 
null, false));
-  }
-
-  @Test
-  public void loadFileContentInBinaryNullPathFailTest()
-      throws HopFileNotFoundException, HopValueException {
-    assertNull(ValueDataUtil.loadFileContentInBinary(new ValueMetaString(), 
null, true));
-  }
-
-  @Test
-  public void getFileEncodingWithFailIfNoFileTest() throws Exception {
-    String path = getClass().getResource("txt-sample.txt").getPath();
-    String encoding = ValueDataUtil.getFileEncoding(new ValueMetaString(), 
path, true);
-    assertEquals("US-ASCII", encoding);
-  }
-
-  @Test
-  public void getFileEncodingWithoutFailIfNoFileTest() throws Exception {
-    String path = getClass().getResource("txt-sample.txt").getPath();
-    String encoding = ValueDataUtil.getFileEncoding(new ValueMetaString(), 
path, false);
-    assertEquals("US-ASCII", encoding);
-  }
-
-  @Test
-  public void getFileEncodingNoFailIfNoFileTest() throws Exception {
-    String nonExistingPath = "nonExistingPath";
-    String encoding = ValueDataUtil.getFileEncoding(new ValueMetaString(), 
nonExistingPath, false);
-    assertNull(encoding);
-  }
-
-  @Test(expected = HopFileNotFoundException.class)
-  public void getFileEncodingFailIfNoFileTest() throws 
HopFileNotFoundException, HopValueException {
-    String nonExistingPath = "nonExistingPath";
-    ValueDataUtil.getFileEncoding(new ValueMetaString(), nonExistingPath, 
true);
-  }
-
-  @Test
-  public void getFileEncodingNullPathNoFailTest() throws Exception {
-    String encoding = ValueDataUtil.getFileEncoding(new ValueMetaString(), 
null, false);
-    assertNull(encoding);
-  }
-
-  @Test
-  public void getFileEncodingNullPathFailTest() throws 
HopFileNotFoundException, HopValueException {
-    String encoding = ValueDataUtil.getFileEncoding(new ValueMetaString(), 
null, true);
-    assertNull(encoding);
-  }
-
-  @Test
-  public void testMulitplyBigNumbers() {
-    BigDecimal field1 = new 
BigDecimal("123456789012345678901.1234567890123456789");
-    BigDecimal field2 = new BigDecimal("1.0");
-    BigDecimal field3 = new BigDecimal("2.0");
-
-    BigDecimal expResult1 = new 
BigDecimal("123456789012345678901.1234567890123456789");
-    BigDecimal expResult2 = new 
BigDecimal("246913578024691357802.2469135780246913578");
-
-    BigDecimal expResult3 = new 
BigDecimal("123456789012345678901.1200000000000000000");
-    BigDecimal expResult4 = new BigDecimal("246913578024691357802");
-
-    assertEquals(expResult1, ValueDataUtil.multiplyBigDecimals(field1, field2, 
null));
-    assertEquals(expResult2, ValueDataUtil.multiplyBigDecimals(field1, field3, 
null));
-
-    assertEquals(
-        expResult3, ValueDataUtil.multiplyBigDecimals(field1, field2, new 
MathContext(23)));
-    assertEquals(
-        expResult4, ValueDataUtil.multiplyBigDecimals(field1, field3, new 
MathContext(21)));
-  }
-
-  @Test
-  public void testDivisionBigNumbers() {
-    BigDecimal field1 = new 
BigDecimal("123456789012345678901.1234567890123456789");
-    BigDecimal field2 = new BigDecimal("1.0");
-    BigDecimal field3 = new BigDecimal("2.0");
-
-    BigDecimal expResult1 = new 
BigDecimal("123456789012345678901.1234567890123456789");
-    BigDecimal expResult2 = new 
BigDecimal("61728394506172839450.56172839450617283945");
-
-    BigDecimal expResult3 = new BigDecimal("123456789012345678901.12");
-    BigDecimal expResult4 = new BigDecimal("61728394506172839450.6");
-
-    assertEquals(expResult1, ValueDataUtil.divideBigDecimals(field1, field2, 
null));
-    assertEquals(expResult2, ValueDataUtil.divideBigDecimals(field1, field3, 
null));
-
-    assertEquals(expResult3, ValueDataUtil.divideBigDecimals(field1, field2, 
new MathContext(23)));
-    assertEquals(expResult4, ValueDataUtil.divideBigDecimals(field1, field3, 
new MathContext(21)));
-  }
-
-  @Test
-  public void testRemainderBigNumbers() throws Exception {
-    BigDecimal field1 = new 
BigDecimal("123456789012345678901.1234567890123456789");
-    BigDecimal field2 = new BigDecimal("1.0");
-    BigDecimal field3 = new BigDecimal("2.0");
-
-    BigDecimal expResult1 = new BigDecimal("0.1234567890123456789");
-    BigDecimal expResult2 = new BigDecimal("1.1234567890123456789");
-
-    assertEquals(
-        expResult1,
-        ValueDataUtil.remainder(
-            new ValueMetaBigNumber(), field1, new ValueMetaBigNumber(), 
field2));
-    assertEquals(
-        expResult2,
-        ValueDataUtil.remainder(
-            new ValueMetaBigNumber(), field1, new ValueMetaBigNumber(), 
field3));
-  }
-
-  @Test
-  public void testSumWithNullValues() throws Exception {
-    IValueMeta metaA = new ValueMetaInteger();
-    metaA.setStorageType(IValueMeta.STORAGE_TYPE_NORMAL);
-    IValueMeta metaB = new ValueMetaInteger();
-    metaA.setStorageType(IValueMeta.STORAGE_TYPE_NORMAL);
-
-    assertNull(ValueDataUtil.sum(metaA, null, metaB, null));
-
-    Long valueB = Long.valueOf(2);
-    ValueDataUtil.sum(metaA, null, metaB, valueB);
-  }
-
-  @Test
-  public void testSumConvertingStorageTypeToNormal() throws Exception {
-    IValueMeta metaA = mock(ValueMetaInteger.class);
-    metaA.setStorageType(IValueMeta.STORAGE_TYPE_BINARY_STRING);
-
-    IValueMeta metaB = new ValueMetaInteger();
-    metaB.setStorageType(IValueMeta.STORAGE_TYPE_BINARY_STRING);
-    Object valueB = "2";
-
-    when(metaA.convertData(metaB, valueB)).thenAnswer((Answer<Long>) 
invocation -> Long.valueOf(2));
-
-    Object returnValue = ValueDataUtil.sum(metaA, null, metaB, valueB);
-    verify(metaA).convertData(metaB, valueB);
-    assertEquals(2L, returnValue);
-    assertEquals(IValueMeta.STORAGE_TYPE_NORMAL, metaA.getStorageType());
-  }
-}
diff --git 
a/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/CalculationType.java
 
b/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/CalculationType.java
index debae69ac1..37f8c7e962 100644
--- 
a/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/CalculationType.java
+++ 
b/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/CalculationType.java
@@ -36,6 +36,8 @@ import 
org.apache.hop.pipeline.transforms.calculator.calculations.conversion.Hex
 import 
org.apache.hop.pipeline.transforms.calculator.calculations.conversion.MaskXML;
 import 
org.apache.hop.pipeline.transforms.calculator.calculations.conversion.UnescapeHTML;
 import 
org.apache.hop.pipeline.transforms.calculator.calculations.conversion.UnescapeXML;
+import 
org.apache.hop.pipeline.transforms.calculator.calculations.conversion.UrlDecode;
+import 
org.apache.hop.pipeline.transforms.calculator.calculations.conversion.UrlEncode;
 import 
org.apache.hop.pipeline.transforms.calculator.calculations.conversion.UseCDATA;
 import org.apache.hop.pipeline.transforms.calculator.calculations.date.AddDays;
 import 
org.apache.hop.pipeline.transforms.calculator.calculations.date.AddHours;
@@ -207,7 +209,9 @@ public enum CalculationType implements 
IEnumHasCodeAndDescription {
   BASE64_ENCODE_PADDED(new Base64EncodePadded()),
   BASE64_DECODE(new Base64Decode()),
   FIRST_DAY_OF_MONTH(new FirstDayOfMonth()),
-  LAST_DAY_OF_MONTH(new LastDayOfMonth());
+  LAST_DAY_OF_MONTH(new LastDayOfMonth()),
+  URL_ENCODE(new UrlEncode()),
+  URL_DECODE(new UrlDecode());
 
   public static final String[] descriptions =
       Stream.of(values()).map(e -> 
e.calculation.getDescription()).toArray(String[]::new);
diff --git 
a/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/calculations/conversion/UrlDecode.java
 
b/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/calculations/conversion/UrlDecode.java
new file mode 100644
index 0000000000..3832ed286f
--- /dev/null
+++ 
b/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/calculations/conversion/UrlDecode.java
@@ -0,0 +1,51 @@
+/*
+ * 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.calculator.calculations.conversion;
+
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.ValueDataUtil;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.pipeline.transforms.calculator.CalculationInput;
+import org.apache.hop.pipeline.transforms.calculator.CalculationOutput;
+import org.apache.hop.pipeline.transforms.calculator.ICalculation;
+
+public class UrlDecode implements ICalculation {
+  private static final String description =
+      BaseMessages.getString(PKG, 
"CalculatorMetaFunction.CalcFunctions.UrlDecode");
+
+  @Override
+  public String getCode() {
+    return "URL_DECODE";
+  }
+
+  @Override
+  public String getDescription() {
+    return description;
+  }
+
+  @Override
+  public int getDefaultResultType() {
+    return IValueMeta.TYPE_STRING;
+  }
+
+  @Override
+  public CalculationOutput calculate(CalculationInput in) {
+    return new CalculationOutput(
+        getDefaultResultType(), ValueDataUtil.urlDecode(in.metaA, in.dataA));
+  }
+}
diff --git 
a/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/calculations/conversion/UrlEncode.java
 
b/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/calculations/conversion/UrlEncode.java
new file mode 100644
index 0000000000..21ab20d56b
--- /dev/null
+++ 
b/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/calculations/conversion/UrlEncode.java
@@ -0,0 +1,51 @@
+/*
+ * 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.calculator.calculations.conversion;
+
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.ValueDataUtil;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.pipeline.transforms.calculator.CalculationInput;
+import org.apache.hop.pipeline.transforms.calculator.CalculationOutput;
+import org.apache.hop.pipeline.transforms.calculator.ICalculation;
+
+public class UrlEncode implements ICalculation {
+  private static final String description =
+      BaseMessages.getString(PKG, 
"CalculatorMetaFunction.CalcFunctions.UrlEncode");
+
+  @Override
+  public String getCode() {
+    return "URL_ENCODE";
+  }
+
+  @Override
+  public String getDescription() {
+    return description;
+  }
+
+  @Override
+  public int getDefaultResultType() {
+    return IValueMeta.TYPE_STRING;
+  }
+
+  @Override
+  public CalculationOutput calculate(CalculationInput in) {
+    return new CalculationOutput(
+        getDefaultResultType(), ValueDataUtil.urlEncode(in.metaA, in.dataA));
+  }
+}
diff --git 
a/plugins/transforms/calculator/src/main/resources/org/apache/hop/pipeline/transforms/calculator/messages/messages_en_US.properties
 
b/plugins/transforms/calculator/src/main/resources/org/apache/hop/pipeline/transforms/calculator/messages/messages_en_US.properties
index 1e48e70734..7667e5418e 100644
--- 
a/plugins/transforms/calculator/src/main/resources/org/apache/hop/pipeline/transforms/calculator/messages/messages_en_US.properties
+++ 
b/plugins/transforms/calculator/src/main/resources/org/apache/hop/pipeline/transforms/calculator/messages/messages_en_US.properties
@@ -150,3 +150,5 @@ CalculatorMetaFunction.CalcFunctions.WeekOfYear=Week of 
year of date A
 CalculatorMetaFunction.CalcFunctions.WeekOfYearISO8601=ISO8601 Week of year of 
date A
 CalculatorMetaFunction.CalcFunctions.YearOfDate=Year of date A
 CalculatorMetaFunction.CalcFunctions.YearOfDateISO8601=ISO8601 Year of date A
+CalculatorMetaFunction.CalcFunctions.UrlEncode=Encode a url
+CalculatorMetaFunction.CalcFunctions.UrlDecode=Decode a url

Reply via email to