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;
+ }
+}