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 6b21940693 Issue #6111 : Avoid scientific notation when converting 
Number to BigNumber (#7744)
6b21940693 is described below

commit 6b21940693d1a17db2fc5b368ff5aba810f67b0b
Author: vbhanuchander-lang <[email protected]>
AuthorDate: Mon Aug 3 02:57:27 2026 -0400

    Issue #6111 : Avoid scientific notation when converting Number to BigNumber 
(#7744)
    
    BigDecimal.valueOf(double) is specified as new 
BigDecimal(Double.toString(val)),
    and Double.toString() switches to scientific notation from 10^7 onwards. For
    55487400.0 it returns "5.54874E7", which parses into an unscaled value of 
554874
    with a scale of -2. It is that negative scale which makes 
BigDecimal.toString()
    render the value as "5.54874E+7" again, so every consumer that serializes a
    BigNumber through toString() emits scientific notation for a value that 
started
    out as plain digits:
    
     - JDBC drivers that inline statement parameters, which is how the Table 
Output
       INSERT in the reported case ends up carrying 5.54874E+7
     - writeBigNumber(), used by the binary row serialization
     - getDataXml(), used by the XML data serialization
    
    Route the four Double to BigDecimal conversions in ValueMetaBase through a 
helper
    that rescales to zero when the scale would be negative. Rescaling upwards 
from a
    negative scale is exact, so no rounding takes place and the numeric value is
    unchanged. Values that already convert to a non-negative scale keep the 
exact
    BigDecimal they produced before.
    
    The string conversion paths format through DecimalFormat and never emitted
    scientific notation; a regression test pins that behaviour down as well.
---
 .../apache/hop/core/row/value/ValueMetaBase.java   |  30 +++++-
 .../hop/core/row/value/ValueMetaBaseTest.java      | 106 +++++++++++++++++++++
 2 files changed, 132 insertions(+), 4 deletions(-)

diff --git 
a/core/src/main/java/org/apache/hop/core/row/value/ValueMetaBase.java 
b/core/src/main/java/org/apache/hop/core/row/value/ValueMetaBase.java
index 5dae65ca35..823d60ca34 100644
--- a/core/src/main/java/org/apache/hop/core/row/value/ValueMetaBase.java
+++ b/core/src/main/java/org/apache/hop/core/row/value/ValueMetaBase.java
@@ -1624,6 +1624,28 @@ public class ValueMetaBase implements IValueMeta {
     }
   }
 
+  /**
+   * Converts a double into a BigDecimal that does not carry a negative scale.
+   *
+   * <p>{@link BigDecimal#valueOf(double)} is built from {@link 
Double#toString(double)}, which
+   * switches to scientific notation from 10<sup>7</sup> onwards. The 
resulting string "5.54874E7"
+   * parses into an unscaled value of 554874 with a scale of -2, and it is 
that negative scale which
+   * makes {@link BigDecimal#toString()} render the value as "5.54874E+7" 
again. Every consumer that
+   * serializes the value through toString() - JDBC drivers that inline 
statement parameters, {@link
+   * #writeBigNumber(java.io.DataOutputStream, BigDecimal)} and {@link 
#getDataXml(Object)} - then
+   * emits scientific notation for a value that started out as plain digits.
+   *
+   * <p>Rescaling to zero is exact whenever the scale is negative, so the 
numeric value is left
+   * untouched and no rounding can occur.
+   *
+   * @param number the double to convert
+   * @return the value as a BigDecimal, rescaled to zero when the scale would 
be negative
+   */
+  protected static BigDecimal convertDoubleToBigNumber(double number) {
+    BigDecimal bigDecimal = BigDecimal.valueOf(number);
+    return bigDecimal.scale() < 0 ? bigDecimal.setScale(0) : bigDecimal;
+  }
+
   protected synchronized BigDecimal convertStringToBigNumber(String string)
       throws HopValueException {
     string = Const.trimToType(string, getTrimType()); // see if trimming needs
@@ -1658,7 +1680,7 @@ public class ValueMetaBase implements IValueMeta {
       //            If the Number is not a BigDecimal.
       //
       if (number instanceof Double) {
-        return BigDecimal.valueOf(number.doubleValue());
+        return convertDoubleToBigNumber(number.doubleValue());
       } else if (number instanceof Long) {
         return BigDecimal.valueOf(number.longValue());
       }
@@ -2605,11 +2627,11 @@ public class ValueMetaBase implements IValueMeta {
           };
         case TYPE_NUMBER:
           return switch (storageType) {
-            case STORAGE_TYPE_NORMAL -> BigDecimal.valueOf((Double) object);
+            case STORAGE_TYPE_NORMAL -> convertDoubleToBigNumber((Double) 
object);
             case STORAGE_TYPE_BINARY_STRING ->
-                BigDecimal.valueOf((Double) 
convertBinaryStringToNativeType((byte[]) object));
+                convertDoubleToBigNumber((Double) 
convertBinaryStringToNativeType((byte[]) object));
             case STORAGE_TYPE_INDEXED ->
-                BigDecimal.valueOf((Double) index[((Integer) 
object).intValue()]);
+                convertDoubleToBigNumber((Double) index[((Integer) 
object).intValue()]);
             default ->
                 throw new HopValueException(
                     this
diff --git 
a/core/src/test/java/org/apache/hop/core/row/value/ValueMetaBaseTest.java 
b/core/src/test/java/org/apache/hop/core/row/value/ValueMetaBaseTest.java
index 1d09e760cb..a5bc242c96 100644
--- a/core/src/test/java/org/apache/hop/core/row/value/ValueMetaBaseTest.java
+++ b/core/src/test/java/org/apache/hop/core/row/value/ValueMetaBaseTest.java
@@ -443,6 +443,112 @@ class ValueMetaBaseTest {
     assertThrows(HopValueException.class, () -> 
valueMeta.getBigNumber("1234567890"));
   }
 
+  @Test
+  void testGetBigNumberFromNumberKeepsPlainNotation() throws HopValueException 
{
+    // BigDecimal.valueOf(55487400.0) is built from Double.toString(), which 
returns "5.54874E7".
+    // The resulting BigDecimal has an unscaled value of 554874 and a scale of 
-2, so rendering it
+    // with toString() yields "5.54874E+7" rather than "55487400".
+    ValueMetaBase valueMeta = new ValueMetaNumber("float53");
+
+    BigDecimal bigNumber = valueMeta.getBigNumber(55487400.0d);
+
+    assertEquals(0, bigNumber.compareTo(new BigDecimal("55487400")));
+    assertTrue(bigNumber.scale() >= 0, "unexpected negative scale: " + 
bigNumber.scale());
+    assertEquals("55487400", bigNumber.toString());
+  }
+
+  @Test
+  void testGetBigNumberFromNumberKeepsPlainNotationAcrossMagnitudes() throws 
HopValueException {
+    ValueMetaBase valueMeta = new ValueMetaNumber("float53");
+    double[] values = {1.0e7, 5.54874e7, 1.23456789e8, 1.5e10, 
9.007199254740992e15, -5.54874e7};
+
+    for (double value : values) {
+      BigDecimal bigNumber = valueMeta.getBigNumber(value);
+
+      assertTrue(bigNumber.scale() >= 0, "negative scale for " + value + ": " 
+ bigNumber.scale());
+      assertEquals(
+          bigNumber.toPlainString(), bigNumber.toString(), "scientific 
notation for " + value);
+      assertEquals(0, bigNumber.compareTo(BigDecimal.valueOf(value)), "value 
changed for " + value);
+    }
+  }
+
+  @Test
+  void testGetBigNumberFromNumberLeavesNonNegativeScalesUntouched() throws 
HopValueException {
+    // Values that already convert to a non-negative scale must keep the exact 
BigDecimal they
+    // produced before, scale included. BigDecimal.equals() is scale 
sensitive, so this pins that
+    // down rather than only comparing numeric values.
+    ValueMetaBase valueMeta = new ValueMetaNumber("number");
+    double[] values = {0.0, 0.1, 123.45, 1.0e-7, 55487400.5};
+
+    for (double value : values) {
+      assertEquals(BigDecimal.valueOf(value), valueMeta.getBigNumber(value), 
"changed " + value);
+    }
+  }
+
+  @Test
+  void testConvertNumberToBigNumberKeepsPlainNotation() throws 
HopValueException {
+    // The metadata change a Select Values transform performs when a Number 
field is turned into a
+    // BigNumber field.
+    IValueMeta source = new ValueMetaNumber("float53");
+    IValueMeta target = new ValueMetaBigNumber("decimal");
+
+    BigDecimal converted = (BigDecimal) target.convertData(source, 
55487400.0d);
+
+    assertEquals("55487400", converted.toString());
+  }
+
+  @Test
+  void testGetBigNumberFromStringKeepsPlainNotation() throws HopValueException 
{
+    ValueMetaBase valueMeta = new ValueMetaString("float53");
+
+    BigDecimal bigNumber = valueMeta.getBigNumber("55487400");
+
+    assertTrue(bigNumber.scale() >= 0, "unexpected negative scale: " + 
bigNumber.scale());
+    assertEquals(0, bigNumber.compareTo(new BigDecimal("55487400")));
+  }
+
+  @Test
+  void testWriteBigNumberConvertedFromNumberUsesPlainNotation() throws 
Exception {
+    // writeBigNumber() serializes with BigDecimal.toString(), so a negative 
scale would put
+    // scientific notation on the wire as well.
+    BigDecimal bigNumber = new 
ValueMetaNumber("float53").getBigNumber(55487400.0d);
+    ValueMetaBase valueMeta = new ValueMetaBigNumber("decimal");
+
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    try (DataOutputStream outputStream = new DataOutputStream(out)) {
+      valueMeta.writeData(outputStream, bigNumber);
+    }
+
+    BigDecimal restored;
+    try (DataInputStream inputStream =
+        new DataInputStream(new ByteArrayInputStream(out.toByteArray()))) {
+      restored = (BigDecimal) valueMeta.readData(inputStream);
+    }
+
+    // readBigNumber() keeps whatever scale was written, so a plain result 
proves a plain wire form.
+    assertEquals("55487400", restored.toString());
+  }
+
+  @Test
+  void testGetDataXmlForBigNumberConvertedFromNumberUsesPlainNotation() throws 
Exception {
+    BigDecimal bigNumber = new 
ValueMetaNumber("float53").getBigNumber(55487400.0d);
+    ValueMetaBase valueMeta = new ValueMetaBigNumber("decimal");
+
+    String xml = valueMeta.getDataXml(bigNumber);
+
+    assertTrue(xml.contains("55487400"), xml);
+    assertFalse(xml.contains("E+"), xml);
+  }
+
+  @Test
+  void testConvertNumberToStringDoesNotUseScientificNotation() throws 
HopValueException {
+    // Guards the string conversion path, which formats through DecimalFormat 
and was already
+    // correct, against a regression from the BigDecimal change.
+    assertFalse(new 
ValueMetaNumber("float53").getString(55487400.0d).contains("E"));
+    assertFalse(
+        new ValueMetaBigNumber("decimal").getString(new 
BigDecimal("5.54874E+7")).contains("E"));
+  }
+
   @Test
   void testGetIntegerThrowsHopValueException() {
     ValueMetaBase valueMeta = new ValueMetaInteger();

Reply via email to