From 201973ae7fdf576b767a7fc0bdab6d366b642155 Mon Sep 17 00:00:00 2001 From: Sahana Bogar Date: Tue, 28 Jul 2026 12:42:41 +0530 Subject: [PATCH] drop the symbol separator in the currency and percent re-parse The lenient fallback rebuilt the pattern without the symbol but kept the space next to it, so the separator stayed mandatory for suffix-symbol locales. --- .../validator/routines/CurrencyValidator.java | 19 ++++++++++++++++-- .../validator/routines/PercentValidator.java | 19 ++++++++++++++++-- .../routines/CurrencyValidatorTest.java | 18 +++++++++++++++++ .../routines/PercentValidatorTest.java | 20 +++++++++++++++++++ 4 files changed, 72 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/apache/commons/validator/routines/CurrencyValidator.java b/src/main/java/org/apache/commons/validator/routines/CurrencyValidator.java index 775eb4aad..7bf935f54 100644 --- a/src/main/java/org/apache/commons/validator/routines/CurrencyValidator.java +++ b/src/main/java/org/apache/commons/validator/routines/CurrencyValidator.java @@ -59,6 +59,18 @@ public static BigDecimalValidator getInstance() { return VALIDATOR; } + /** + * Tests whether the character at the given position of a pattern sits next to the currency symbol. + * + * @param pattern The pattern being stripped of its currency symbol. + * @param index The position to test. + * @return {@code true} if the character borders the currency symbol. + */ + private static boolean isNextToSymbol(final String pattern, final int index) { + return index > 0 && pattern.charAt(index - 1) == CURRENCY_SYMBOL + || index + 1 < pattern.length() && pattern.charAt(index + 1) == CURRENCY_SYMBOL; + } + /** * Constructs a strict instance. */ @@ -106,8 +118,11 @@ protected Object parse(final String value, final Format formatter) { if (pattern.indexOf(CURRENCY_SYMBOL) >= 0) { final StringBuilder buffer = new StringBuilder(pattern.length()); for (int i = 0; i < pattern.length(); i++) { - if (pattern.charAt(i) != CURRENCY_SYMBOL) { - buffer.append(pattern.charAt(i)); + final char chr = pattern.charAt(i); + // A locale that suffixes the symbol usually separates it from the number with a (non-breaking) + // space. That space belongs to the symbol, so dropping only the symbol would leave it mandatory. + if (chr != CURRENCY_SYMBOL && !(Character.isSpaceChar(chr) && isNextToSymbol(pattern, i))) { + buffer.append(chr); } } decimalFormat.applyPattern(buffer.toString()); diff --git a/src/main/java/org/apache/commons/validator/routines/PercentValidator.java b/src/main/java/org/apache/commons/validator/routines/PercentValidator.java index ddbd8f0a0..81d94a7df 100644 --- a/src/main/java/org/apache/commons/validator/routines/PercentValidator.java +++ b/src/main/java/org/apache/commons/validator/routines/PercentValidator.java @@ -64,6 +64,18 @@ public static BigDecimalValidator getInstance() { return VALIDATOR; } + /** + * Tests whether the character at the given position of a pattern sits next to the percent symbol. + * + * @param pattern The pattern being stripped of its percent symbol. + * @param index The position to test. + * @return {@code true} if the character borders the percent symbol. + */ + private static boolean isNextToSymbol(final String pattern, final int index) { + return index > 0 && pattern.charAt(index - 1) == PERCENT_SYMBOL + || index + 1 < pattern.length() && pattern.charAt(index + 1) == PERCENT_SYMBOL; + } + /** * Constructs a strict instance. */ @@ -109,8 +121,11 @@ protected Object parse(final String value, final Format formatter) { if (pattern.indexOf(PERCENT_SYMBOL) >= 0) { final StringBuilder buffer = new StringBuilder(pattern.length()); for (int i = 0; i < pattern.length(); i++) { - if (pattern.charAt(i) != PERCENT_SYMBOL) { - buffer.append(pattern.charAt(i)); + final char chr = pattern.charAt(i); + // A locale that suffixes the symbol usually separates it from the number with a (non-breaking) + // space. That space belongs to the symbol, so dropping only the symbol would leave it mandatory. + if (chr != PERCENT_SYMBOL && !(Character.isSpaceChar(chr) && isNextToSymbol(pattern, i))) { + buffer.append(chr); } } decimalFormat.applyPattern(buffer.toString()); diff --git a/src/test/java/org/apache/commons/validator/routines/CurrencyValidatorTest.java b/src/test/java/org/apache/commons/validator/routines/CurrencyValidatorTest.java index 4f733b566..ecc99f87c 100644 --- a/src/test/java/org/apache/commons/validator/routines/CurrencyValidatorTest.java +++ b/src/test/java/org/apache/commons/validator/routines/CurrencyValidatorTest.java @@ -38,6 +38,9 @@ class CurrencyValidatorTest { private static final char CURRENCY_SYMBOL = '\u00A4'; + /** The character locales such as de-DE use between the number and a trailing currency symbol. */ + private static final char NON_BREAKING_SPACE = '\u00A0'; + private String usDollar; private String ukPound; @@ -176,6 +179,21 @@ void testPattern() { assertFalse(validator.isValid(ukPound + "1,234.567", pattern, Locale.US), "invalid symbol"); } + /** + * Test currency values with a pattern that suffixes the symbol, which locales such as de-DE separate from the number with a non-breaking space. The symbol + * is optional, so its separator has to be optional too. + */ + @Test + void testSuffixSymbolPattern() { + final BigDecimalValidator validator = CurrencyValidator.getInstance(); + final String pattern = "#,##0.00" + NON_BREAKING_SPACE + CURRENCY_SYMBOL; + final BigDecimal expected = new BigDecimal("1234.56"); + + assertEquals(expected, validator.validate("1,234.56" + NON_BREAKING_SPACE + usDollar, pattern, Locale.US), "symbol"); + assertEquals(expected, validator.validate("1,234.56", pattern, Locale.US), "no symbol"); + assertNull(validator.validate("1,234.56" + NON_BREAKING_SPACE, pattern, Locale.US), "separator without symbol"); + } + /** * Test Valid currency values */ diff --git a/src/test/java/org/apache/commons/validator/routines/PercentValidatorTest.java b/src/test/java/org/apache/commons/validator/routines/PercentValidatorTest.java index 24f1fb992..19f9927f3 100644 --- a/src/test/java/org/apache/commons/validator/routines/PercentValidatorTest.java +++ b/src/test/java/org/apache/commons/validator/routines/PercentValidatorTest.java @@ -35,6 +35,11 @@ */ class PercentValidatorTest { + private static final char PERCENT_SYMBOL = '%'; + + /** The character locales such as fr-FR use between the number and a trailing percent symbol. */ + private static final char NON_BREAKING_SPACE = '\u00A0'; + protected PercentValidator validator; private Locale originalLocale; @@ -100,6 +105,21 @@ void testNumberRangeExactBound() { assertFalse(instance.minValue(new BigDecimal("5"), new BigDecimal("5.5"))); } + /** + * Test percentage values with a pattern that suffixes the symbol, which locales such as fr-FR separate from the number with a non-breaking space. The + * symbol is optional, so its separator has to be optional too. + */ + @Test + void testSuffixSymbolPattern() { + final BigDecimalValidator validator = PercentValidator.getInstance(); + final String pattern = "#,##0" + NON_BREAKING_SPACE + PERCENT_SYMBOL; + final BigDecimal expected = new BigDecimal("0.12"); + + assertEquals(expected, validator.validate("12" + NON_BREAKING_SPACE + PERCENT_SYMBOL, pattern, Locale.US), "symbol"); + assertEquals(expected, validator.validate("12", pattern, Locale.US), "no symbol"); + assertNull(validator.validate("12" + NON_BREAKING_SPACE, pattern, Locale.US), "separator without symbol"); + } + /** * Test Valid percentage values */