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

bamaer 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 18d16444b3 Fix Parse an ASCII minus when converting strings to numbers 
(#8639)
18d16444b3 is described below

commit 18d16444b31b50cff4dd24b8366a5e25de987f15
Author: Lance <[email protected]>
AuthorDate: Sun Sep 27 19:19:55 2026 +0800

    Fix Parse an ASCII minus when converting strings to numbers (#8639)
    
    Signed-off-by: leehaut <[email protected]>
---
 .../apache/hop/core/row/value/ValueMetaBase.java   | 127 ++++++++++++---------
 .../row/value/ValueMetaBaseLocaleMinusTest.java    | 100 ++++++++++++++++
 2 files changed, 173 insertions(+), 54 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 5fb5417fa1..5226157413 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
@@ -1226,29 +1226,82 @@ public class ValueMetaBase implements IValueMeta {
     }
 
     try {
-      DecimalFormat format = getDecimalFormat(false);
-      Number number;
-      if (lenientStringToNumber) {
-        number = format.parse(string);
-      } else {
-        ParsePosition parsePosition = new ParsePosition(0);
-        number = format.parse(string, parsePosition);
+      return parseStringAsNumber(string, 
getDecimalFormat(false)).doubleValue();
+    } catch (Exception e) {
+      throw new HopValueException(this + " : couldn't convert String to number 
", e);
+    }
+  }
 
-        if (parsePosition.getIndex() < string.length()) {
-          throw new HopValueException(
-              this
-                  + CONST_STRING_TO_NUMBER
-                  + (parsePosition.getIndex() + 1)
-                  + MSG_FOR_VALUE
-                  + string
-                  + "]");
+  /**
+   * Parses {@code string} with {@code format}.
+   *
+   * <p>{@link DecimalFormat} accepts only its locale negative prefix. JSON, 
data grids and
+   * calculator constants use an ASCII hyphen-minus ({@code '-'}), while some 
locales use {@code
+   * U+2212} or a bidi mark in front of the sign. When parsing rejects the 
first character, a
+   * leading ASCII or Unicode minus is rewritten to that prefix and parsing is 
tried once more.
+   */
+  private Number parseStringAsNumber(String string, DecimalFormat format)
+      throws HopValueException, ParseException {
+    if (lenientStringToNumber) {
+      try {
+        return format.parse(string);
+      } catch (ParseException first) {
+        String adapted = alignLeadingMinus(string, format);
+        if (adapted.equals(string)) {
+          throw first;
         }
+        return format.parse(adapted);
       }
+    }
 
-      return number.doubleValue();
-    } catch (Exception e) {
-      throw new HopValueException(this + " : couldn't convert String to number 
", e);
+    ParsePosition parsePosition = new ParsePosition(0);
+    Number number = format.parse(string, parsePosition);
+    if (number != null && parsePosition.getIndex() >= string.length()) {
+      return number;
+    }
+    if (parsePosition.getIndex() == 0) {
+      String adapted = alignLeadingMinus(string, format);
+      if (!adapted.equals(string)) {
+        ParsePosition retry = new ParsePosition(0);
+        Number retried = format.parse(adapted, retry);
+        if (retried != null && retry.getIndex() >= adapted.length()) {
+          return retried;
+        }
+      }
+    }
+    throw new HopValueException(
+        this
+            + CONST_STRING_TO_NUMBER
+            + (parsePosition.getIndex() + 1)
+            + MSG_FOR_VALUE
+            + string
+            + "]");
+  }
+
+  /**
+   * Rewrites a leading ASCII hyphen-minus or Unicode minus ({@code U+2212}) 
to {@code format}'s
+   * negative prefix when they differ. Leading whitespace is left in place.
+   */
+  static String alignLeadingMinus(String string, DecimalFormat format) {
+    if (string == null || string.isEmpty() || format == null) {
+      return string;
+    }
+    String prefix = format.getNegativePrefix();
+    if (prefix == null || prefix.isEmpty()) {
+      return string;
+    }
+    int start = 0;
+    while (start < string.length() && 
Character.isWhitespace(string.charAt(start))) {
+      start++;
+    }
+    if (start >= string.length() || string.startsWith(prefix, start)) {
+      return string;
+    }
+    char sign = string.charAt(start);
+    if (sign != '-' && sign != '\u2212') {
+      return string;
     }
+    return string.substring(0, start) + prefix + string.substring(start + 1);
   }
 
   public String convertJsonToString(JsonNode jsonNode) throws 
HopValueException {
@@ -1588,24 +1641,7 @@ public class ValueMetaBase implements IValueMeta {
     }
 
     try {
-      Number number;
-      if (lenientStringToNumber) {
-        number = getDecimalFormat(false).parse(string).longValue();
-      } else {
-        ParsePosition parsePosition = new ParsePosition(0);
-        number = getDecimalFormat(false).parse(string, parsePosition);
-
-        if (parsePosition.getIndex() < string.length()) {
-          throw new HopValueException(
-              this
-                  + CONST_STRING_TO_NUMBER
-                  + (parsePosition.getIndex() + 1)
-                  + MSG_FOR_VALUE
-                  + string
-                  + "]");
-        }
-      }
-      return number.longValue();
+      return parseStringAsNumber(string, getDecimalFormat(false)).longValue();
     } catch (Exception e) {
       throw new HopValueException(this + " : couldn't convert String to 
Integer", e);
     }
@@ -1657,24 +1693,7 @@ public class ValueMetaBase implements IValueMeta {
     }
 
     try {
-      DecimalFormat format = getDecimalFormat(bigNumberFormatting);
-      Number number;
-      if (lenientStringToNumber) {
-        number = format.parse(string);
-      } else {
-        ParsePosition parsePosition = new ParsePosition(0);
-        number = format.parse(string, parsePosition);
-
-        if (parsePosition.getIndex() < string.length()) {
-          throw new HopValueException(
-              this
-                  + CONST_STRING_TO_NUMBER
-                  + (parsePosition.getIndex() + 1)
-                  + MSG_FOR_VALUE
-                  + string
-                  + "]");
-        }
-      }
+      Number number = parseStringAsNumber(string, 
getDecimalFormat(bigNumberFormatting));
 
       // Cannot simply cast a number to a BigDecimal,
       //            If the Number is not a BigDecimal.
diff --git 
a/core/src/test/java/org/apache/hop/core/row/value/ValueMetaBaseLocaleMinusTest.java
 
b/core/src/test/java/org/apache/hop/core/row/value/ValueMetaBaseLocaleMinusTest.java
new file mode 100644
index 0000000000..25ce5db5f7
--- /dev/null
+++ 
b/core/src/test/java/org/apache/hop/core/row/value/ValueMetaBaseLocaleMinusTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.value;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.math.BigDecimal;
+import java.util.Locale;
+import org.apache.hop.core.exception.HopValueException;
+import org.junit.jupiter.api.Test;
+
+/** ASCII and Unicode minus signs must parse in locales whose negative prefix 
is not '-'. */
+class ValueMetaBaseLocaleMinusTest {
+
+  @Test
+  void asciiMinusParsesInLocalesWithADifferentNegativePrefix() {
+    assertParsesNegativeTwo(Locale.forLanguageTag("nb-NO"));
+    assertParsesNegativeTwo(Locale.forLanguageTag("sv-SE"));
+    assertParsesNegativeTwo(Locale.forLanguageTag("fi-FI"));
+    assertParsesNegativeTwo(Locale.forLanguageTag("ar-SA"));
+  }
+
+  @Test
+  void unicodeMinusParsesWithAnAsciiNegativePrefix() throws Exception {
+    Locale original = Locale.getDefault();
+    Locale originalFormat = Locale.getDefault(Locale.Category.FORMAT);
+    try {
+      Locale.setDefault(Locale.US);
+      Locale.setDefault(Locale.Category.FORMAT, Locale.US);
+      ValueMetaInteger meta = new ValueMetaInteger("balance");
+      assertEquals(-2L, meta.convertStringToInteger("-2"));
+      assertEquals(-2L, meta.convertStringToInteger("\u22122"));
+    } finally {
+      Locale.setDefault(original);
+      Locale.setDefault(Locale.Category.FORMAT, originalFormat);
+    }
+  }
+
+  @Test
+  void nonNumericTextStillFails() {
+    withLocale(
+        Locale.forLanguageTag("nb-NO"),
+        () -> {
+          ValueMetaInteger meta = new ValueMetaInteger("balance");
+          assertThrows(HopValueException.class, () -> 
meta.convertStringToInteger("x"));
+          assertThrows(HopValueException.class, () -> 
meta.convertStringToInteger("-2x"));
+        });
+  }
+
+  private static void assertParsesNegativeTwo(Locale locale) {
+    withLocale(
+        locale,
+        () -> {
+          assertEquals(-2L, new 
ValueMetaInteger("balance").convertStringToInteger("-2"));
+          assertEquals(2L, new 
ValueMetaInteger("balance").convertStringToInteger("2"));
+          assertEquals(-2L, new 
ValueMetaInteger("balance").convertStringToInteger("\u22122"));
+          assertEquals(-2.0d, new 
ValueMetaNumber("balance").convertStringToNumber("-2"));
+          assertEquals(
+              0,
+              new BigDecimal("-2")
+                  .compareTo(new 
ValueMetaBigNumber("balance").convertStringToBigNumber("-2")));
+        });
+  }
+
+  private static void withLocale(Locale locale, LocaleCheck check) {
+    Locale original = Locale.getDefault();
+    Locale originalFormat = Locale.getDefault(Locale.Category.FORMAT);
+    try {
+      Locale.setDefault(locale);
+      Locale.setDefault(Locale.Category.FORMAT, locale);
+      check.run();
+    } catch (Exception e) {
+      throw new AssertionError(locale.toString(), e);
+    } finally {
+      Locale.setDefault(original);
+      Locale.setDefault(Locale.Category.FORMAT, originalFormat);
+    }
+  }
+
+  @FunctionalInterface
+  private interface LocaleCheck {
+    void run() throws Exception;
+  }
+}

Reply via email to